[Перевод] Новый линт в Dart 3.2
Вчера вышел Dart 3.2. В официальном анонсе сказано, что там нового. Но там не сказано про новое правило линтера.
annotate_redeclares
Начнём издалека. В одной из следующих версий появится новая конструкция, которая называется extension type. Её было проще объяснить, когда рабочее название было »view», но авторы не захотели вводить новое ключевое слово, поэтому переименовали её в »extension type».
В общем, можно будет делать »view» на класс, чтобы показывать только часть его интерфейса. Это полезно, если:
Вы не контролируете иерархию. Например, вы хотите
AddOnlyMap
, который обернётMap
, но откроет доступ только к методам чтения и добавления. Было бы идеально, еслиMap
расширял интерфейсAddOnlyMap
, потому что наследование — это интуитивно понятный способ добавлять функциональность, но вы не котролируете встроенные классы. Поэтому делаете view.Вам нужно где-то показывать только часть интерфейса, но случай слишком мелкий, чтобы добавлять интерфейс в иерархию, которую увидят все ваши пользователи.
Синтаксис:
extension type AddOnlyMap(Map map) {
void addAll(Map other) => map.addAll(other);
// Все другие методы чтения и добавления.
}
Это никак не связано с обычным extension
, просто забудьте про него на время.
В первой строке пишем, что этот «view» оборачивает. В этом примере — Map
под названием map
. По этому названию мы потом можем обращаться к объекту, чтобы форвардить вызовы.
Теперь можно создавать AddOnlyMap
вот так. Обратите внимание, что видны только методы, которые мы создали, например, addAll()
:
Также видно, что обёрнутый объект всё равно доступен, и можно сделать так:
aom.map.clear();
Можно сделать защиту от дурака с приватной переменной:
extension type AddOnlyMap(Map _map) {
// ...
Но всё равно можно сделать каст:
(aom as Map).clear();
Всё это можно было делать и раньше, если обернуть объект в новый класс:
class AddOnlyMap {
final Map _map;
const AddOnlyMap(this._map);
void addAll(Map other) => map.addAll(other);
// ...
}
Но это замедляет работу программы, потому что форвард методов происходит во время выполнения. В отличие от этого, extension type существует только во время компиляции, и вместо форвардов будут подставлены вызовы оборачиваемых методов на оригинальном объекте. Кстати, поэтому каст и работает. Подробнее об этом — в официальной дискуссии.
Эту конструкцию можно использовать уже в Dart 3.2 как эксперимент. Для этого добавьте флаг при сборке или запуске: --enable-experiment=inline-class
Когда разобрались с этой конструкцией, перейдём к кошкам, чтобы понять линт. Map
хорошо объяснял конструкцию, но не проблему.
Например, есть такая иерархия:
class Animal {
void sound() {
// По умолчанию без звука.
}
}
class Cat extends Animal {
@override
void sound() {
print('Мяу.');
}
void play() {
print('Кусь!');
}
}
Теперь вам нужен SoundOnlyCat
, чтобы передавать кошку клиентам и она их не укусила. Можно сделать каст к Animal
, но вы хотите обезопасить клиентов, чтобы им случайно не передали Wolf
, потому нужно сделать view на Cat
:
extension type SoundOnlyCat(Cat _c) {
void sound() => _c.sound();
}
Потом вам становится лень форвордить каждый метод отдельно, и вы хотите сделать это разом для всего интерфейса Animal
. Это делается так:
extension type SoundOnlyCat(Cat _c) implements Animal {}
Не лучшая идея для общего случая, потому что можно добавить в Animal
что-то, чему не место в SoundOnlyCat
, и забыть об этом, но всё равно бывают случаи, когда это полезно.
Дальше вы захотите, чтобы SoundOnlyCat
издавала какой-нибудь другой звук:
extension type SoundOnlyCat(Cat _c) implements Animal {
void sound() {
print('Хочу играть.');
}
}
И вот здесь проблема. У Animal
есть свой sound()
, но этот новый метод с ним никак не связан. Он скрывает старый метод, а не переопределяет:
final cat = SoundOnlyCat(Cat());
cat.sound(); // Хочу играть.
(cat as Cat).sound(); // Мяу.
У него даже может быть другая сигнатура:
extension type SoundOnlyCat(Cat _c) implements Animal {
void sound({required bool loud}) {
print('Хочу играть' + (loud ? '!!!' : '.'));
}
}
final cat = SoundOnlyCat(Cat());
cat.sound(loud: true); // Хочу играть!!!
(cat as Cat).sound(); // Мяу.
Получается беспорядок. Если переименовать метод в extension type, он перестанет скрывать метод из Animal
, и можно случайно вызвать не то, что хотелось. Если у методов одинаковая сигнатура, то вы узнаете об этом только из жалоб от клиентов.
Когда скрываете что-то, нужно быть уверенным, что вы скрываете это преднамеренно. Для этого добавили аннотацию @redeclare
, она появилась в пакете meta
в версии 1.10.0.
И вот мы подошли к новому линту: annotate_redeclares. Он заставляет писать эту аннотацию, если мы скрываем метод в extension type:
import 'package:meta/meta.dart';
extension type SoundOnlyCat(Cat _c) implements Animal {
@redeclare // OK, а без неё -- замечание линтера.
void sound() {
print('Хочу играть.');
}
}
И если этот метод случайно перестанет скрывать метод класса, то анализатор выдаст предупреждение, что метод с этой аннотацией ничего не переопределяет.
Более старые линты
Если вы пропустили мои прошлые статьи, то вот:
Как подключить новый линт
Здесь я пишу, как сделать это вручную.
Или можете использовать в своём проекте мой пакет total_lints, в котором включено большинство правил линтера. Я использую его, чтобы не повторять одну и ту же конфигурацию между своими проектами.
Не пропускайте мои статьи, добавляйтесь в Телеграм-канал: ainkin_com