Generics - Обобщённые типы

Что такое Generics?

Generics  позволяют писать гибкий и безопасный код, который может работать с разными типами данных без дублирования кода.

Это как универсальный шаблон, в который можно подставить любой тип, и Dart автоматически будет следить за правильностью типов.

Улучшение читаемости и безопасности кода

Допустим, у нас есть список List. По умолчанию в него можно добавить что угодно.

Проблема без Generics:

Dart - Список без Generics

Светлая тема Темная тема
void main() {  
  List data = []; // Нет ограничений на тип данных  
  
  data.add(42);  
  data.add("Dart");  
  data.add(true);  
}

Решение с Generics:

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

Dart - Список с Generics

Светлая тема Темная тема
void main() {  
  List<String> names = []; // Теперь в список можно добавлять только строки  
  names.add("Dart");  
  names.add("Flutter");  
  
  // names.add(42); // Ошибка: Нельзя добавить число в список  
  
  print(names); // [Dart, Flutter]  
}

Теперь Dart заставляет следовать правилам типов и предотвращает ошибки.

Избегаем дублирования кода

Представь, что мы пишем систему кеширования данных.
Без Generics нам пришлось бы делать отдельный класс для каждого типа данных:

Много лишнего кода

Dart - Дублирование кода

Светлая тема Темная тема
abstract class StringCache {  
  String getByKey(String key);  
  void setByKey(String key, String value);  
}  
  
abstract class IntCache {  
  int getByKey(String key);  
  void setByKey(String key, int value);  
}

Используем Generics и избавляемся от дублирования

Dart - Универсальный класс с Generics

Светлая тема Темная тема
abstract class Cache<T> {  
  T getByKey(String key);  
  void setByKey(String key, T value);  
}

Теперь можно создать универсальный кеш, который хранит любой тип:

Вместо T внутри скобок <T> будет подставляться тип который мы укажем при вызове

Dart - Реализация универсального кеша

Светлая тема Темная тема
abstract class Cache<T> {  
  T getByKey(String key);  
  void setByKey(String key, T value);  
}  
  
class MyIntCache implements Cache<int> {  
  Map<String, int> _cache = {};  
  @override  
  int getByKey(String key) => _cache[key] ?? 0;  
  @override  
  void setByKey(String key, int value) {  
    _cache[key] = value;  
  }  
}  
  
class MyStringCache implements Cache<String> {  
  Map<String, String> _cache = {};  
  @override  
  String getByKey(String key) => _cache[key] ?? '';  
  @override  
  void setByKey(String key, String value) {  
    _cache[key] = value;  
  }  
}  
  
void main() {  
  final intCache = MyIntCache();  
  intCache.setByKey('age', 18);  
  print(intCache.getByKey('age')); // 30  
  
  final stringCache = MyStringCache();  
  stringCache.setByKey('name', 'Наташа');  
  print(stringCache.getByKey('name')); // John  
}

Теперь неважно, что мы кешируем (строки, числа, объекты) — один класс справится со всеми случаями!

Реализация стека (Stack)

Сделаем универсальный стек, который работает с любыми типами

Dart - Универсальный стек

Светлая тема Темная тема
class Stack<T> {  
  List<T> _storage = [];  
  void push(T item) => _storage.add(item);  
  T pop() => _storage.removeLast();  
}  
  
void main() {  
  var stackInt = Stack<int>();  
  stackInt.push(42);  
  print(stackInt.pop()); // 42  
  var stackString = Stack<String>();  
  stackString.push("Hello");  
  print(stackString.pop()); // "Hello"  
}

Generics в методах

Можно использовать Generics не только в классах, но и в методах.
Этот метод работает с любыми типами и автоматически подставляет нужный тип

Dart - Generic-метод

Светлая тема Темная тема
T first<T>(List<T> list) {  
  return list[0]; // Возвращаем первый элемент списка  
}  
  
void main() {  
  print(first([1, 2, 3])); // 1  
  print(first(["A", "B", "C"])); // "A"  
}

Ограничение типов с extends

Иногда Generics должны работать только с определёнными типами.
Например, мы хотим, чтобы класс мог работать только с наследниками BaseClass.
Теперь ChildClass<T> может работать только с BaseClass и его наследниками.

Dart - Ограничение типов

Светлая тема Темная тема
class BaseClass {}  
  
class ChildClass<T extends BaseClass> {  
  String toString() => "Экземпляр класса 'ChildClass<$T>'";  
}  
  
class Extender extends BaseClass {}  
  
void main() {  
  var obj1 = ChildClass<BaseClass>(); // Работает  
  var obj2 = ChildClass<Extender>(); // Работает  
  var obj3 = ChildClass<int>(); // Ошибка: int не является BaseClass
}