В Dart одиночное наследование, зато каждый класс неявно задаёт интерфейс, а поведение можно подмешивать миксинами. Dart 3 добавил модификаторы классов, и на собесе про них спрашивают всё чаще.
Конструкторы
class User {
final String name;
final int age;
const User(this.name, {this.age = 0}); // const-конструктор
User.guest() : this('гость'); // именованный, перенаправляет
factory User.fromJson(Map<String, dynamic> j) =>
User(j['name'] as String, age: j['age'] as int);
}
- Список инициализации (после
:) выполняется до тела конструктора. Там задаютfinal-поля, вызываютsuper(...)и пишутassert. const-конструктор возможен, если все поляfinal. Вызов сconstсоздаёт канонический экземпляр. Во Flutter это главный способ сэкономить перестроения:const Text('Привет')не пересоздаётся при каждомbuild.factoryне обязан создавать новый объект: может вернуть кешированный экземпляр или подкласс. Внутри нетthis.- Параметры бывают позиционными, необязательными позиционными
[int x = 0]и именованными{required String id}.
extends, implements, with
extendsнаследует реализацию, можно вызватьsuper.implementsберёт только интерфейс: все члены придётся реализовать самому. Реализовать можно любой класс, не только абстрактный.withподмешивает миксин: его методы копируются в цепочку наследования.
mixin Logger {
void log(String m) => print('[$runtimeType] $m');
}
mixin Retry on Api { // применим только к наследникам Api
Future<T> retry<T>(Future<T> Function() f) async { /* ... */ }
}
class UserApi extends Api with Logger, Retry {}
Линеаризация. class C extends A with M1, M2 строит цепочку A -> A+M1 -> A+M1+M2 -> C. Если метод есть и в M1, и в M2, побеждает тот, что правее (M2), а super внутри M2 указывает на M1. Это популярная задача «что выведет».
Во Flutter миксины на каждом шагу: SingleTickerProviderStateMixin, WidgetsBindingObserver, AutomaticKeepAliveClientMixin.
Модификаторы классов Dart 3
| Модификатор | Смысл |
|---|---|
abstract | нельзя создать экземпляр |
interface | вне библиотеки можно только implements, не extends |
base | вне библиотеки можно только extends, не implements |
final | вне библиотеки нельзя ни наследовать, ни реализовывать |
sealed | абстрактный, все подтипы в той же библиотеке, switch проверяет полноту |
mixin class | можно использовать и как класс, и как миксин |
sealed идеален для состояний и результатов:
sealed class LoadState {}
class Loading extends LoadState {}
class Data extends LoadState { final List<String> items; Data(this.items); }
class Failure extends LoadState { final Object error; Failure(this.error); }
Widget view(LoadState s) => switch (s) {
Loading() => const CircularProgressIndicator(),
Data(:final items) => Text('${items.length}'),
Failure(:final error) => Text('$error'),
};
Добавили новый подкласс: компилятор покажет все switch, где он не обработан.
Расширения
extension добавляет методы существующему типу без наследования:
extension on String {
String get capitalized => isEmpty ? this : this[0].toUpperCase() + substring(1);
}
Расширения разрешаются статически, по объявленному типу переменной. На dynamic они не работают, и переопределить их в подклассе нельзя.
extension type (Dart 3.3) это обёртка над типом без затрат в рантайме: например, UserId поверх int, чтобы не перепутать с OrderId.
Равенство
По умолчанию == сравнивает ссылки. Для значимых объектов переопределяют == и hashCode вместе, иначе объект «теряется» в Set и Map. На практике это делают пакетами equatable или freezed, а records уже сравниваются по значению.
Частые ошибки
- Переопределить
==безhashCode. - Ждать полиморфизма от расширения: вызов зависит от статического типа.
- Путать
implementsиextends: приimplementsреализация не наследуется.
Что спросят на собесе
- Виды конструкторов, зачем
factoryиconst. - Миксины: что такое
on, как разрешаются конфликты методов. - Модификаторы классов Dart 3 и зачем
sealed. - Чем расширение отличается от наследования.
- Как правильно реализовать равенство объектов.