Вчера вышел Dart 3.2. В официальном анонсе сказано, что там нового. Но там не сказано про новое правило линтера.

annotate_redeclares

Начнём издалека. В одной из следующих версий появится новая конструкция, которая называется extension type. Её было проще объяснить, когда рабочее название было ‘view’, но авторы не захотели вводить новое ключевое слово, поэтому переименовали её в ‘extension type’.

В общем, можно будет делать ‘view’ на класс, чтобы показывать только часть его интерфейса. Это полезно, если:

  • Вы не контролируете иерархию. Например, вы хотите AddOnlyMap, который обернёт Map, но откроет доступ только к методам чтения и добавления. Было бы идеально, если Map расширял интерфейс AddOnlyMap, потому что наследование -- это интуитивно понятный способ добавлять функциональность, но вы не котролируете встроенные классы. Поэтому делаете view.

  • Вам нужно где-то показывать только часть интерфейса, но случай слишком мелкий, чтобы добавлять интерфейс в иерархию, которую увидят все ваши пользователи.

Синтаксис:

extension type AddOnlyMap<K, V>(Map<K, V> map) {
  void addAll(Map<K, V> other) => map.addAll(other);
  // Все другие методы чтения и добавления.
}

Это никак не связано с обычным extension, просто забудьте про него на время.

В первой строке пишем, что этот ‘view’ оборачивает. В этом примере -- Map<K, V> под названием map. По этому названию мы потом можем обращаться к объекту, чтобы форвардить вызовы.

Теперь можно создавать AddOnlyMap вот так. Обратите внимание, что видны только методы, которые мы создали, например, addAll():

Также видно, что обёрнутый объект всё равно доступен, и можно сделать так:

aom.map.clear();

Можно сделать защиту от дурака с приватной переменной:

extension type AddOnlyMap<K, V>(Map<K, V> _map) {
// ...

Но всё равно можно сделать каст:

(aom as Map).clear();

Всё это можно было делать и раньше, если обернуть объект в новый класс:

class AddOnlyMap<K, V> {
  final Map<K, V> _map;
  const AddOnlyMap(this._map);

  void addAll(Map<K, V> 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

Комментарии (0)