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

Универсальные классы

Для определения универсального класса в Dart нам понадобится только его имя и параметр, заключенный в угловые скобки (в нашем случае это <T>):

void main() {
  final ClassName<int> intClass = ClassName<int>(73);
  final ClassName<String> stringClass = ClassName<String>('Hello, Dart!');
  
  final dynamic dynamicValue = <String, dynamic>{'key' : someStoredValue}['key'];
  final ClassName<dynamic> dynamicClass = ClassName(dynamicValue);
}

Универсальные методы

Для определения универсального метода используется аналогичный синтаксис.

T create<T>() { ... }

void main() {
  final int intValue = create<int>();
  final dynamic dynamicValue = create();
}

Ограничения на типы

К параметрам типам можно применять ограничения. Для этого используется знакомое слово extends. Давайте ограничим ранее созданную функцию на типе num.

T create<T extends num>() { ... }

void main() {
  final int intValue = create<int>();
  final num numValue = create();
  final String stringValue = create<String>(); // Не скомпилируется
}

Теперь наша функция может принимать только подтипы num (int и double) и сам тип ограничения. Функция попросту не позволит использовать ее с параметром типа, который не является наследником ограничения. Ранее я говорил, что если не указать тип, то автоматически подставляется dynamic, но это касается случая, когда у нас нет ограничения. В нашем случае типом по умолчанию будет тип ограничения.

Ограничения (уже другие)

Дженерики в ЯП, включая Dart, имеют определенные ограничения и запреты. Вот несколько ключевых моментов, о которых следует знать:

  1. Стирание типов: Dart использует стирание типов, что означает, что общие типы стираются во время выполнения (runtime). Конечно, это делается из соображений производительности, но это также означает, что мы не можем получить доступ к информации о типах во время выполнения;

  2. Ограничение на ограничения типа: Dart не позволяет использовать более одного аргумента для extends;

  3. Производительность: дженерики могут вносить дополнительные расходы из-за проверок типов и динамической диспетчеризации.

Универсальные коллекции

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

bool isListOfType<T>(List<dynamic> list) {
  // Способ №1 
  return list.every((dynamic element) => element is T);
  // Альтернативный способ №2
  // return list.whereType<String>().length == list.length;
  // Альтернативный способ №3 - НЕ ДЕЛАТЬ ТАК!
  // return list.every((dynamic element) => element.runtimeType == T);
}

void main() {
  final List<String> stringList = <String>['Hello', 'Dart'];
  final List<dynamic> dynamicStringList = <dynamic>['Hello', 'Dart'];

  print('${stringList is List<String>} - ${isListOfType<String>(stringList)}'); // true - true
  print('${dynamicStringList is List<String>} - ${isListOfType<String>(dynamicStringList)}'); // false - true

  print('${stringList.runtimeType == List<String>}'); // true
}

Обычно для проверки на принадлежность типу используется оператор is, но с коллекциями все не так просто. Казалось бы, внутри переменной dynamicStringList находятся только строковые значения, но почему эе проверка на принадлежность не работает? Дело в том, что List<String> не подтип List<dynamic>, поэтому если нужно проверить коллекцию на принадлежность типа, то самый верный вариант - перебор элементов с одним из предложенных вариантов. Способ №3 показывает как можно, но не стоит делать, так как runtimeType используется только для дебаг целей и может отражать не реальный тип, а внутренний.

Обратной совместимости также не завезли, поэтому стоит запомнить следующее:

void main() {
  final List<String> strings = <String>['Dart'];
  final List<dynamic> anything = strings; // OK
  final List<String> anythingCopy = anything; // Не скомпилируется
  
  anything.add(123); // Ошибка! Мы сломали список строк
}

Парсинг dynamic в коллекцию

Рассмотрим еще один пример, где нам нужно реализовать хранилище объектов в своем приложении, которое будет перманентным, но не строгим. Будем использовать SharedPreferences, который для оптимизации под капотом использует Map<String, Object> для быстрого доступа к значениям (в runtime). Чтобы было удобнее работать с классом хранилища определим класс-обертку PreferencesEntry.

class PreferencesEntry<T extends Object> {
  const PreferencesEntry({
    required String key,
    required SharedPreferences storage,
  })  : _key = key,
        _storage = storage;

  final String _key;
  final SharedPreferences _storage;

  T? get value {
    final Object? value = _storage.get(_key);
    
    print('require type - $T; actual type - ${value.runtimeType}; value - $value');
    
    return value as T?;
  }

  void setValue(T? value) => _storage.set(_key, value);
}

Если с базовыми типами все понятно, то как быть со списком строк? Тут не все просто, поскольку опять упираемся в ограничения.

Проблема та же, что указывалась выше: List<String> не подтип для List<Object?>. Попробуем исправить код.

T? get value {
  final Object? value = _storage.get(_key);
 
  if (T != List<String>) return value as T?;
 
  List<dynamic>? list = value as List<dynamic>?;
  if (list != null && list is! List<String>) {
    list = list.cast<String>().toList(growable: false);
  }
 
  return list as T?;
}

Если объект не List<String>, то мы используем cast, который создаёт новый список, элементы которого интерпретируются как String. Это нужно, чтобы избежать потенциальных ClassCastException.

Выводы

Универсальные типы являются неотъемлемой частью разработки, обеспечивают строгую проверку типов, гарантируя правильную обработку типов данных во время компиляции, однако стоит помнить не только о преимуществах, но и недостатках.

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