Официальный анонс Dart 3.9 здесь. А вот два новых линта, которые там не упомянули.
switch_on_type
Есть такой странный способ проверить тип переменной — switch по runtimeType:
class A {}
void main() {
switch (variable.runtimeType) {
case A:
print('A');
default:
print('Something else');
}
}
Проблема в том, что обычно мы хотим, чтобы подклассы A тоже попадали в первый кейс. Иначе мы не сможем подменять объекты, и полиморфизм, как учили в школе, не будет работать. Это большая проблема, потому что:
Объекты могут создаваться в коде, который мы не контролируем, поэтому имплементации могут поменяться без нашего ведома.
Нельзя делать моки для тестов, потому что пакеты типа mocktail и mockito работают через подмену имплементации.
И даже если вы полностью контролируете код, ни к чему создавать такое место, про обновление которого всегда надо помнить, если добавите подкласс.
Чтобы люди так не делали, добавился линт. Он заставляет переписать такой switch через паттерны:
switch (variable) {
case A():
print('A');
default:
print('Something else');
}
Теперь, если сделать B extends A, он тоже будет покрываться паттерном A(), и все моки будут работать.
Ладно, а как отделить A от B extends A? Надо сначала отбросить всю ветку наследования, которая начинается с B:
switch (variable) {
case B(): // Соответствует B и всем его подклассам.
break;
case A(): // Соответствует A и всем подклассам, кроме B и его подклассов.
print('A');
default:
print('Нечто совершенно иное');
}
Безопасные типы — мой выбор!
unnecessary_unawaited
Когда вызываем функцию, которая возвращает Future, обычно мы хотим дождаться результата. Иначе выполнение продолжится раньше, чем случится нужный нам эффект:
Future<void> save() { /* ... */ }
void fn() {
save(); // LINT: discarded_futures
close(); // Э, мы ещё не сохранили!
}
Поэтому ещё в Dart 2.18 сделали линт discarded_futures, который это помечает.
Чтобы он не срабатывал, нужно или дождаться результата через await, или обернуть вызов в unawaited():
Future<void> save() { /* ... */ }
Future<void> fn() async {
unawaited(save()); // Это глупо.
await save(); // Так-то лучше.
close();
}
Однако, есть асинхронные функции, результат которых:
Почти никогда не нужен, поэтому глупо утомлять разработчиков таким линтом.
Иногда всё‑таки нужен, поэтому они всё равно возвращают
Future.
Обычно это логирование, освобождение ресурсов и тому подобное:
Future<LogMessage> log(String message) { ... }
void fn() {
unawaited(log('Message')); // Результат логирования обычно неважен.
}
И вот, чтобы это упростить, в пакет meta добавили аннотацию @awaitNotRequired, чтобы делать функции, которые не триггерят линт, если их не ждать:
@awaitNotRequired
Future<LogMessage> log(String message) { ... }
void fn() {
log('Message'); // Прекрасно!
}
А если всё‑таки хочется дождаться результата:
@awaitNotRequired
Future<LogMessage> log(String message) { ... }
Future<void> fn() async {
await log('Message'); // Никаких проблем.
}
Но если вы уже расставили везде unawaited(), то он теперь лишний. И новый линт поможет вам найти такие места:
@awaitNotRequired
Future<LogMessage> log(String message) { ... }
void fn() {
unawaited(log('Message')); // LINT: unnecessary_unawaited
}
Более старые линты
Если вы пропустили мои предыдущие статьи, то вот:
В Dart 3.3 не было новых линтов.
Как подключить новые линты
Прочитайте эту статью о том, как подключить эти правила вручную.
Или можете использовать в своём проекте мой пакет total_lints, в котором включено большинство правил линтера. Я использую его, чтобы не повторять одну и ту же конфигурацию между своими проектами.
Не пропускайте мои статьи, добавляйтесь в Телеграм‑канал: ainkin_com
Русские переводы реже и с задержкой здесь: ainkin_com_ru
lil_master
Не могу поставить плюс, поэтому ловите виртуальный) Спасибо, отличный разбор!
Вопросик:
В статье упоминаются подклассы, но не затрагивается тема
sealed-иерархий. Как, по-вашему, этот линт будет (или должен) взаимодействовать сexhaustive_cases? Кажется, что связкаswitch_on_type(как толчок к переходу на паттерны) иexhaustive_cases(дляsealed) — это и есть та "золотая середина" для безопасной работы с типами, к которой нас подталкивает язык. Не упускаем ли мы здесь более крупную картину, фокусируясь только наruntimeType?Я бы посоветовал всем, кто будет рефакторить свой код под
switch_on_type, не просто механически менять синтаксис. Стоит сразу оценить, не является ли вся эта иерархия классов-кандидатом наsealed-модификатор. Зачастую это решает проблему на более глубоком, архитектурном уровне, а не просто "лечит" линтер.