Эта статья посвящена анализу реализации некоторых синтаксических сахаров в Dart для Flutter. Она раскрывает, что же происходит за кулисами простых ключевых слов.
Начнем с того, что вчера в группе был задан очень базовый вопрос: "Почему эта часть кода не может проверять пустоту объекта user
?"
На самом деле этот вопрос довольно прост:
late
, если они не были проинициализированы до обращения, приведут к ошибке.Поэтому решение этого вопроса заключается просто в том, чтобы заменить User? user
. Но почему объект, который не может быть пустым, становится допускающим отсроченную инициализацию после применения ключевого слова late
?
Для начала рассмотрим следующий пример кода, где мы используем ключевое слово late
для объявления объекта playerAnimation
. После компиляции кода с помощью dump_kernel.dart
мы можем извлечь информацию из файла app.dill
.
Как видно из извлеченного кода, объект playerAnimation
фактически преобразуется в nullable тип Animation?
. При попытке доступа к нему через get playerAnimation()
, если playerAnimation == null
, будет выброшено исключение LateError
.Таким образом, когда мы обращаемся к объекту, объявленному как late
, и он ещё не был инициализирован, то получаем исключение.
Перейдём к ключевому слову typedef
, которое с версии Dart 2.13 позволяет использовать новые возможности типа-синонима, такие как:
// Тип-синоним для функций (уже существовал)
typedef ValueChanged<T> = void Function(T value);
// Тип-синоним для классов (новая возможность)
typedef StringList = List<String>;
// Переименование классов без разрыва (новая возможность)
@Deprecated("Используйте NewClassName вместо")
typedef OldClassName<T> = NewClassName<T>;
Как работает typedef
? Как показывают изображения, метод _getDeviceInfo
после компиляции фактически заменяется на List<String>
. Таким образом, StringList
не участвует в выполнении скомпилированного кода и не влияет на производительность программы.
Допустим, как показано на следующем рисунке, можно заметить, что объявление selectItemChanged
через SelectItemChanged
после компиляции превращается в final поле (динамическое) → ? void selectItemChanged
.
Затем мы используем концепцию tear-off
в Dart для демонстрации другого явления. Как видно на следующем рисунке, мы извлекаем метод toString
из произвольного объекта x
, и с помощью замыкания можем вызывать его так же, как обычный метод объекта x
.
> Если вы вызываете функцию на объекте и пропускаете скобки, Dart называет это "tear-off": замыкание, которое принимает те же аргументы, что и функция, и при вызове этого замыкания выполняется функция внутри него. Например,
names.forEach(print);
эквивалентно names.forEach((name) { print(name); });
.
Как будет выглядеть метод getToString
после компиляции?
На следующем рисунке можно увидеть, что метод getToString
после компиляции становится статическим (static
) методом, а также ToStringFn
не участвует в выполнении и заменяется на соответствующий () -> core:String
.
Поэтому для кода после компиляции использование ключевого слова typedef
не оказывает влияния ни на производительность, ни на результат выполнения программы.
В Dart с помощью extension
можно легко расширять объекты. Как ключевое слово extension
реализует расширение на основе существующего объекта?
На следующем рисунке показано объявление перечисления Cat
и его расширение, чтобы каждому значению перечисления были присвоены значения, а также добавлен метод talk
.
На следующем рисунке показано, что после компиляции значения перечисления Cat
становятся статическими (static final
) полями с постоянными адресами, а методы talk
и value
из CatExtension
также переходят на новые места.
При рассмотрении соответствующей реализации было установлено, что методы name
и talk
из CatExtension
стали статическими методами в том же файле, и метод talk
был сначала определён как метод, а затем использован через tearoff
для вызова. Большинство методов, определённых в расширении, имеют соответствующие методы и tearoffs
.
Как показано на следующем рисунке, после компиляции вызов cat.talk()
в месте использования Cat
приводит к выполнению main::CatExtension|talk
.
И последнее, поговорим о async / await
. Мы знаем, что это синтаксический сахар для Future
в Dart, но как этот синтаксический сахар выполняется после компиляции?
Можно заметить, что метод loadMore
после компиляции получает множество дополнительных строк кода, где определяется _Future<void> async_future
, которая возвращается в конце. При этом код, который нам нужно выполнить, оборачивается в async_op
, а внутри этого блока происходит попытка выполнения содержимого с использованием try catch
, и через _completeOnAsyncError
возвращаются ошибки.
Это также объясняет, почему внешний
try-catch
для Future
не может захватывать ошибки. Поэтому, как показано на следующем рисунке, для Future
требуется использовать .onError((error, stackTrace) => null)
для захвата и обработки ошибок.
Понимание реализации этих ключевых слов поможет вам более эффективно организовать свой код во время повседневной работы с Flutter, избегая множества ненужных проблем.
Конечно, если вы не будете использовать эти знания, они могут пригодиться для "показухи" на собеседовании, не так ли?
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )