Irbisa · Шпаргалка

Система типов Dart в глубину

Верхние и нижние типы, reified-дженерики, вариантность, вывод типов, FutureOr, extension type

Senior-разработчик · 18 вопросов

  1. 01

    В Dart три top-типа: Object?, dynamic и void. Чем они отличаются и почему Object к ним не относится?

    Все три вмещают любое значение, включая null; разница только в том, что компилятор позволяет с ним делать.

  2. 02

    Что такое тип Never и когда функция должна возвращать Never, а не void?

    Never называют bottom-типом: это подтип любого типа без единого значения, поэтому выражение типа Never никогда не завершается; оно бросает исключение или зацикливается.

    // wrong: void; the call is an ordinary statement, flow continues past itvoid failVoid(String m) => throw StateError(m); // right: Never; the call does not returnNever fail(String m) => throw StateError(m);…
  3. 03

    Что значит, что система типов Dart является sound, и какие проверки типов всё ещё выполняются во время выполнения?

    Sound означает, что статический тип никогда не врёт: выражение типа String всегда вычисляется в String, или программа бросает исключение до того, как неверное значение будет использовано.

  4. 04

    Как flow analysis решает, что продвигают проверка на null и is, и что отменяет продвижение?

    Flow analysis сужает статический тип локальной переменной или параметра после проверки вроде x is String или x != null, чтобы использовать её как более узкий тип без приведения.

    void demo(Object value, String? path) {  if (value is! String) return;  print(value.length); // String for the rest of the body   if (path == null) throw ArgumentError.notNull('path');  print(path.length); // String: the null branch cannot continue…
  5. 05

    Дженерики в Dart реифицированы, в отличие от Java. Что можно делать с аргументом типа во время выполнения и чего по-прежнему нельзя?

    Каждый дженерик-объект хранит свои аргументы типа во время выполнения, поэтому is List<String> и as Map<String, int> выполняют настоящие проверки, а параметр типа T внутри тела обозначает настоящий тип.

    import 'dart:convert'; void main() {  final tags = jsonDecode('["a", "b"]');  print(tags.runtimeType); // List<dynamic>, fixed when the list was created…
  6. 06

    В чём разница между x is T и x.runtimeType == T и почему runtimeType не стоит использовать в логике?

    is задаёт вопрос о подтипе и продвигает переменную; runtimeType == T сравнивает точный класс вместе с аргументами типа.

    import 'dart:convert'; class Animal {} class Dog extends Animal {  void bark() {}…
  7. 07

    Как работает вывод типов в Dart и когда он молча откатывается к dynamic?

    Вывод типов достраивает типы, которые вы не написали, в двух направлениях: сверху вниз, из контекстного типа (List<String> a = [];), и снизу вверх, из самих выражений ([1, 2] даёт List<int>).

    import 'dart:convert'; class Animal {}class Dog extends Animal {}class Cat extends Animal {}…
  8. 08

    Этот код присваивает List<Dog> переменной List<Animal> и добавляет Cat. Скомпилируется ли он и что произойдёт при запуске?

    class Animal {}class Dog extends Animal {}class Cat extends Animal {} void addStray(List<Animal> animals) => animals.add(Cat()); void main() {  final dogs = <Dog>[Dog()];  final List<Animal> animals = dogs;  print(animals.length);  addStray(animals);  print(dogs.length);}

    Он компилируется, печатает 1, а затем addStray бросает type 'Cat' is not a subtype of type 'Dog' of 'value'.

  9. 09

    Что делает ключевое слово covariant у параметра и почему operator == должен принимать Object other?

    covariant позволяет переопределению сузить параметр до подтипа, что обычное правило переопределения запрещает.

    class Animal {} class Dog extends Animal {} abstract class Handler {  void handle(covariant Animal a); // overrides may narrow `a`…
  10. 10

    В этом парсере нигде нет as, но он падает с type 'Null' is not a subtype of type 'int'. Откуда взялся каст?

    class User {  final int id;  final String name;  User(this.id, this.name);   factory User.fromJson(Map<String, dynamic> json) {    final int id = json['id'];    return User(id, json['name']);  }} void main() {  print(User.fromJson({'name': 'Ada'}).name);}

    У json['id'] статический тип dynamic, а присваивание dynamic переменной int означает неявный даункаст: компилятор вставляет ту же проверку, что дал бы as int.

  11. 11

    Найдите баг: get<AuthRepository>() бросает «AuthRepository is not registered» сразу после того, как приложение зарегистрировало репозиторий.

    abstract interface class AuthRepository {}class HttpAuthRepository implements AuthRepository {} class Locator {  final _services = <Type, Object>{};   void register<T extends Object>(T service) => _services[T] = service;   T get<T extends Object>() =>      _services[T] as T? ?? (throw StateError('$T is not registered'));} void main() {  final locator = Locator()..register(HttpAuthRepository());  print(locator.get<AuthRepository>());}

    register(HttpAuthRepository()) выводит T из аргумента, поэтому сервис сохраняется под ключом HttpAuthRepository.

  12. 12

    Почему void Function(Object) можно передать туда, где ожидается void Function(Dog), а void Function(Dog) не подходит на место void Function(Animal)?

    Функциональные типы контравариантны по параметрам и ковариантны по возвращаемому типу: замена может принимать больше, чем обещает слот, и возвращать что-то более узкое.

    class Animal {} class Dog extends Animal {  void fetch() {}}…
  13. 13

    Ваш maxOf<T extends Comparable<T>> работает для DateTime и собственного Money, но maxOf<int> не компилируется. Почему и что написать вместо этого?

    int реализует лишь Comparable<num>, а Comparable<int> является подтипом Comparable<num>, но не наоборот, поэтому T extends Comparable<T> отвергает int.

    // F-bound: T must be comparable to its own kindT maxOf<T extends Comparable<T>>(List<T> xs) =>    xs.reduce((a, b) => a.compareTo(b) >= 0 ? a : b); class Money implements Comparable<Money> {  final int cents;…
  14. 14

    Что такое extension type, чем он отличается от класса-обёртки и где его гарантия исчезает во время выполнения?

    Extension type (Dart 3.3) описывает представление существующего типа, на этапе компиляции: extension type UserId(String value) даёт String собственный статический тип и API без объекта-обёртки.

    extension type UserId(String value) {  bool get isValid => value.isNotEmpty;  // String toString() => ...; // error: conflicts with a member of Object} // `implements` exposes the representation's members…
  15. 15

    Найдите баг: в логах видны полные номера телефонов, хотя каждое поле лога проходит через mask.

    extension type Email(String value) {}extension type Phone(String value) {} // Masks personal data before a log field is written.String mask(Object? field) {  if (field is Email) {    return '***@${field.value.split('@').last}';  }  if (field is Phone) {    return '***${field.value.substring(field.value.length - 4)}';  }  return '***';} void main() => print(mask(Phone('+77011234567')));

    Extension types стираются во время выполнения, поэтому field is Email проверяет лишь is String и даёт true для любой String.

  16. 16

    Что напечатает этот код и что он говорит о FutureOr<T> как о типе?

    import 'dart:async'; FutureOr<Object> load() => Future<String>.value('data'); Future<void> main() async {  final raw = load();  print(raw is Future);  final v = await raw;  print(v.runtimeType);  print(<FutureOr<Object?>>[] is List<Object?>);}

    Он печатает true, String, true.

  17. 17

    Код, полный Map<String, dynamic>, проходит анализ без замечаний и всё равно падает на кастах. Что ловят strict-casts, strict-raw-types и strict-inference?

    Эти падения ловит strict-casts: он запрещает неявные даункасты из dynamic, и String name = json['name']; становится ошибкой анализатора.

    # analysis_options.yamlinclude: package:flutter_lints/flutter.yaml analyzer:  language:    strict-casts: true      # String name = json['name'];  -> error…
  18. 18

    Вы публикуете пакет, главный класс которого является дженериком. Как сделать его публичный API типобезопасным для вызывающих?

    Ограничивайте параметры типа границей Object, чтобы null не значил две вещи, не пускайте dynamic и «сырые» типы в публичные сигнатуры, принимайте Iterable<T> только для чтения и возвращайте копии, а не внутренние коллекции.

    // wrong: unbounded; Cache<String, int?> makes `null` mean two thingsclass LooseCache<K, V> {  final _m = <K, V>{};  V? get(K key) => _m[key]; // "absent" or "stored null"?}…