1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-FM.md 21 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Простые советы по использованию пакета equatable в Flutter и анализ макропрограммирования

Сегодня мы обсудим реализацию пакета equatable и используем его для понимания роли и реализации макропрограммирования в Dart. Для разработчиков Flutter макропрограммирование можно назвать "желанным" средством.

equatable

Как следует из названия пакета equatable, его основная задача проста — это помощь в создании классов с базовым значением сравнения «🟰». В следующем примере кода даже трехлетний программист знает, что при обычных условиях bob == Person("Bob") вернёт значение false, так как они представляют два разных экземпляра класса, а хэшкод по умолчанию будет отличаться:

class Person {
  const Person(this.name);

  final String name;
}

final Person bob = Person("Bob");

print(bob == Person("Bob")); // false

Если же требуется сделать эти объекты равными, то потребуется переопределить оператор ==, чтобы указать логику сравнения. Это может выглядеть не слишком сложно, но если класс имеет много параметров, количество повторяющегося кода возрастает. В этом случае пакет equatable помогает уменьшить объём работы.

class Person {
  const Person(this.name);

  final String name;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Person &&
          runtimeType == other.runtimeType &&
          name == other.name;

  @override
  int get hashCode => name.hashCode;
}
```Используя equatable, вам достаточно расширить класс от Equatable и переопределить метод `props`, чтобы автоматически получить нужную логику сравнения. Такой подход делает код более чистым и компактным.```dart
import 'package:equatable/equatable.dart';

class Person extends Equatable {
  const Person(this.name);

  final String name;

  @override
  List<Object> get props => [name];
}

class Person2 extends Equatable {
  const Person2(this.name, [this.age]);

  final String name;
  final int? age;

  @override
  List<Object?> get props => [name, age];
}

Конечно, есть некоторые ограничения при использовании equatable, такие как необходимость сделать все поля класса final. Это связано с тем, что официальная документация Dart рекомендует использовать неизменяемые значения при работе с hashCode, чтобы избежать проблем с коллекциями на основе хешей. При определении метода equals, также следует определить метод hashCode. Оба этих метода должны учитывать поля объекта, и если эти поля изменятся, это может привести к изменению хеш-кода объекта.

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

Возвращаясь к реализации пакета equatable, основной логикой является обработка метода equals для сравнения значений, а также генерация хэша с помощью метода mapPropsToHashCode для определения значения хэш-функции.

	@override
  bool operator ==(Object other) {
    return identical(this, other) ||
           other is Equatable &&
               runtimeType == other.runtimeType &&
               equals(props, other.props);
  }

  @override
  int get hashCode => runtimeType.hashCode ^ mapPropsToHashCode(props);
```Первым делом стоит отметить, что пользовательский метод `equals` сравнивает списки свойств (`props`) двух объектов класса. Важно обратить внимание на то, что свойства могут быть любыми объектами, включая коллекции, такие как `Map`, `Set` и т.д., поэтому используется объект `DeepCollectionEquality` из Dart для глубокого сравнения таких коллекций. Это значительно упрощает процесс сравнения.

> Класс `DeepCollectionEquality` предназначен для глубокого сравнения коллекций. Он может распознавать списки, множества, итерируемые объекты и карты, а затем выполнять глубокое сравнение их элементов, даже при необходимости учитывать порядок или отсутствие его.

```dart

const DeepCollectionEquality _equality = DeepCollectionEquality();

/// Определяет равенство между двумя списками [list1] и [list2].
bool equals(List<Object?>? list1, List<Object?>? list2) {
  if (identical(list1, list2)) return true;
  if (list1 == null || list2 == null) return false;
  final length = list1.length;
  if (length != list2.length) return false;

  for (var i = 0; i < length; i++) {
    final unit1 = list1[i];
    final unit2 = list2[i];

    if (_isEquatable(unit1) && _isEquatable(unit2)) {
      if (unit1 != unit2) return false;
    } else if (unit1 is Iterable || unit1 is Map) {
      if (!_equality.equals(unit1, unit2)) return false;
    } else if (unit1?.runtimeType != unit2?.runtimeType) {
      return false;
    } else if (unit1 != unit2) {
      return false;
    }
  }
  return true;
}

bool _isEquatable(Object? object) {
  return object is Equatable || object is EquatableMixin;
}
```Что касается генерации хэшей, пакет `equatable` использует алгоритм Дженнингса для хэширования. Основная идея этого алгоритма заключается в преобразовании произвольной последовательности данных в фиксированное хэш-значение. Алгоритм прост в реализации; он использует операции сдвига битов и итерацию для создания хэш-значения, которое затем рекурсивно применяется ко всем параметрам для вычисления конечного хэш-значения. Например:

- Сдвинуть хеш-значение влево на 10 бит, сложить его с исходным хеш-значением, чтобы увеличить диапазон значений хэша и расширить область изменения.
- Сдвинуть хеш-значение вправо на 6 бит, выполнить побитовое исключающее ИЛИ (XOR) между ними, чтобы уменьшить линейную зависимость в хэш-значении.```dart
int mapPropsToHashCode(Iterable<Object?>? props) {
  return _finish(props == null ? 0 : props.fold(0, _combine));
}

int _combine(int hash, Object? object) {
  if (object is Map) {
    object.keys
        .sorted((Object? a, Object? b) => a.hashCode - b.hashCode)
        .forEach((Object? key) {
      hash = hash ^ _combine(hash, [key, (object! as Map)[key]]);
    });
    return hash;
  }
  if (object is Set) {
    object = object.sorted((Object? a, Object? b) => a.hashCode - b.hashCode);
  }
  if (object is Iterable) {
    for (final value in object) {
      hash = hash ^ _combine(hash, value);
    }
    return hash ^ object.length;
  }

  hash = 0x1fffffff & (hash + object.hashCode);
  hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
  return hash ^ (hash >> 6);
}

int _finish(int hash) {
  hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
  hash = hash ^ (hash >> 11);
  return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}

На этом мы рассмотрели основные аспекты работы пакета equatable. Однако использование этого пакета всё ещё требует некоторых усилий, таких как ручное указание свойств (props) и явное наследование от него, что делает процесс менее элегантным и простым. В таком случае можно использовать макросы.

Макросы

Действительно, в таких случаях использование макросов является наиболее подходящим решением для пакета equatable. Авторы пакета выпустили версию 3.0.0-dev.1, которая использует макросы для реализации. После модификаций код класса Person выглядит следующим образом:

@Equatable()
class Person {
  const Person(this.name);

  final String name;
}

Как видно, это обычный класс, который приобретает характеристики класса equatable благодаря аннотации @Equatable(). Это действительно делает код более элегантным и простым? ```> Кроме того, в отличие от старых версий build_runner, этот метод не создаёт файлы `.g.dart` прямо в вашем проекте.

После установки пакета equatable с использованием макросов вам достаточно запустить команду flutter run --enable-experiment=macros, чтобы получить те же результаты, что и раньше:

Иллюстрация

Использование @Equatable() и экспериментального @JsonCodable одновременно позволяет получить класс с возможностями сериализации и сравнения объектов при использовании двух аннотаций.

К слову, в VSCode есть область "Go to Augmentation", где можно нажать и перейти к augmented классу, то есть к файлу, который представляет собой что-то вроде макроса. Через этот augmented класс вы можете в реальном времени просматривать генерируемый код:

Эта возможность расширения относится к макросам, создающим augmented файлы, отличие от старых методов генерации заключается в том, что он существует в памяти, а не представлен в виде файла типа .g.dart.

Возможность расширения Dart позволяет изменять классы или функции путём добавления членов или замены основного содержимого вне оригинальной области, эта функциональность работает автономно от макросов.

Если же вы используете Android Studio, возможно, вам потребуется установка пакета show_augmentation, чтобы иметь аналогичную функцию предварительного просмотра. Выполните команду dart run show_augmentation --file=lib/person.dart, чтобы получить подобный вывод в консоли:

Перейдя к equatable, мы можем сказать немного о его реализации. Обычно это достигается через реализацию ClassDeclarationsMacro и ClassDefinitionMacro, позволяющих выполнять базовые операции макропрограммирования. Это делается путём вызова buildDeclarationsForClass для редактирования необходимых объявлений и buildDefinitionForClass для определения реализации:

Например, обычно при объявлениях используется Uri.parse('dart:core'), так как требуется поддержка Dart. Например, здесь final boolean = await builder.codeFrom(_dartCore, 'bool'). Конечно, реализация codeFrom была упакована в equatable, и ниже приведён пример, который поможет лучше понять это.

Как показано ниже, сначала мы получаем ядро Dart с помощью Uri.parse('dart:core'), затем используем MemberDeclarationBuilder, чтобы получить метод print из Dart:

Здесь мы получаем метод print через dart:core, а затем выводим все параметры в сгенерированном коде hello. Макрос ClassDeclarationsMacro сообщает компилятору, что он может применяться к классам.

Зачем же нам нужна загрузка dart:core? Один из её ключевых моментов — это возможность автоматического генерирования префиксов, который вы можете заметить при использовании командной строки для вывода:

> Префикс является динамическим, что гарантирует, что ваш код не будет конфликтовать с любыми основными элементами (например, print). Поскольку префикс динамический, вы не знаете заранее, как он будет выглядеть, поэтому вы не можете просто писать print(xxxxx) в коде, а значит, вам нужно использовать конструкцию parts для создания сгенерированного кода.

Перейдём к библиотеке Equatable. В процессе реализации сгенерированного кода, если вы используете какие-то свои функции, такие как final _equatable = Uri.parse('package:equatable/equatable.dart'), вы сможете использовать соответствующие возможности Equatable:

Из конечного результата видно, что Equatable использует макрос для повторного генерирования части кода, так что разработчики могут просто использовать аннотацию @Equatable(), чтобы импортировать необходимые функции, аналогично тому, как @JsonSerializable() используется для импорта toJson и fromJson.

Кроме того, можно ещё одним способом проверить эффект макроса, анализируя отладочный файл app.dill после компиляции. Это можно сделать с помощью скрипта dump_kernel.dart, который создаёт файл app.dill.txt:

dart pkg/vm/bin/dump_kernel.dart xxxxxx/app.dill xxxxxx/app.dill.txt

На следующем рисунке показано, что "dart:core" имеет динамически добавленные префиксы, а также указывается, что соответствующие методы были добавлены динамически, вместе с путями к макросам и файлам. Также видно успешное использование deepEquals и jenkinsHash.

Конечно, команда pkg/vm/bin/dump_kernel.dart доступна только в полной версии SDK Dart, которая находится в официальном репозитории dart-lang/sdk. Однако, если вы попробуете клонировать этот проект и выполнить dump_kernel, то скорее всего столкнетесь с такой ошибкой:

Потому что если вы хотите использовать dump_kernel, вам потребуется инструмент depot_tools. Инструмент depot_tools используется для управления исходными кодами проекта Chromium, а также требуются соответствующие условия окружения:

  • Окружение Python 3
  • Клонировать репозиторий с помощью команды git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git и настроить путь в переменной окружения export PATH=~/workspace/depot_tools:$PATH
  • Создайте директорию и выполните команду fetch dart, которая может занять некоторое время, так как размер составляет несколько гигабайт
  • Войдите в директорию SDK и выполните команду git checkout <tag>, чтобы перейти к нужному тегу версии Dart, поскольку обычно требуется, чтобы версия Dart, которую вы отладываете, совпадала с версией SDK
  • Выполните команду gclient sync -D
  • Теперь вы можете использовать команду dart pkg/vm/bin/dump_kernel.dart xxxxxx/app.dill xxxxxx/app.dill.txt для создания ядра, где путь pkg/vm/bin/dump_kernel.dart относится к пути SDK.

Подведение итоговВ данной статье мы рассмотрели основные понятия и навыки работы с Dart через библиотеку Equatable. Также мы подробнее остановились на концепции макросов и их применении, а также представили различные способы просмотра результатов макросов. Учитывая ожидаемую официальную поддержку макросов в будущем, можно предположить, что разработчики Flutter будут ждать этого момента с нетерпением. А вы уже успели попробовать экспериментальную поддержку макросов в Dart 3.5?

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master