Дарт - невероятный и гибкий язык. Если вы пришли из строго типизированных и статических языков, таких как Java и C #, или из динамических и слабо типизированных языков, таких как JavaScript и Python, вы заметите, что Dart может вести себя почти так же, как вы знакомы. Это действительно потрясающе, и некоторые функции и концепции можно использовать на нескольких языках. Сегодня мы немного поговорим о Generics, одном из самых мощных инструментов Java и C #.

Если вы используете GetIt, Flutter Modular, GetX или даже List определенного типа данных, вы уже использовали Generics в Dart. Взгляните на следующий пример:

Modular.get<AppController>();

Таким образом мы получаем экземпляр зарегистрированного объекта с помощью Flutter Modular. Обратите внимание, что у нас есть статическая функция с именем get, у которой нет параметров. Вместо этого мы просто передаем тип класса внутри символов «меньше» и «больше». Это полезно, когда у нас есть разные типы данных в коллекции. Но как мы можем это сделать сами?

Представим, что вам нужно добавить элементы любого вида в список внутри класса. Как бы Вы это сделали? Вы можете создать класс и использовать дженерики, чтобы сообщить компилятору, что мы хотим иметь возможность хранить любые данные в списке.

class MyClass {
  var _myList = <dynamic>[];
}

Нам нужен метод для добавления нового элемента в этот список. Это просто, правда?

void add(dynamic element) => _myList.add(element);

Теперь представьте, что вам нужно восстановить данные определенного типа из этого списка. Подумайте об этом: как мы можем сказать компилятору, какой тип данных нам нужен? Мы не можем передавать тип в качестве параметра, как мы это делаем с переменными. Решение этой проблемы - ожидать тип с именем T (имя не имеет значения), а затем использовать его для сравнения элементов списка. Сигнатура нашего метода будет такой:

get<T>();

Когда мы не сообщаем компилятору тип возвращаемого значения, по умолчанию он будет динамическим. Допустим, у вас есть 3 объекта одного типа. В этом случае нам нужно вернуть список товаров. Мы узнаем тип желаемого элемента во время компиляции, и нам не нужно возвращать динамический тип. Мы можем изменить подпись на это:

List<T> get<T>();

Реализация этого метода очень проста: сначала мы проверяем, принадлежит ли элемент в списке к тому же типу, который мы передали в метод, T. Если эта оценка верна, мы добавляем элемент в список. Нам нужно выполнить эту проверку для всех элементов в _myList, а затем вернуть список элементов с типом T. Любые данные в Dart будут иметь атрибут с именем runtimeType. Как вы можете догадаться, этот атрибут возвращает тип элемента. Итак, наша реализация будет такой:

List<T> get<T>() {
var elements = <T>[];
for (var element in _myList) {
  if (element.runtimeType == T) {
    elements.add(element);
    }
  }
return elements;
}

Удобно, что команда Dart включила метод в класс Iterable (который реализуется List), который имеет ту же цель. Наш код можно свести к:

List<T> get<T>() => _myList.whereType<T>().toList();

Да, так просто. Я добавил метод toList (), чтобы убедиться, что мы возвращаем список. С этими двумя методами наш класс закончен:

class MyClass {
  var _myList = <dynamic>[];
  
  void add(dynamic element) => _myList.add(element);
  List<T> get<T>() => _myList.whereType<T>().toList();
}

Давайте протестируем наш код:

void main() {
  final instance = MyClass();
  instance.add(1);
  instance.add(2);
  instance.add(3);
  instance.add(4.0);
  instance.add(5.0);
  instance.add(6.0);
  instance.add(true);
  instance.add(false);
  instance.add('Pedro');
  instance.add('Lemos');
  print(instance.get<int>());
  print(instance.get<double>());
  print(instance.get<bool>());
  print(instance.get<String>());
}

Результатом будет:

[1, 2, 3] // All int values
[4.0, 5.0, 6.0] // All double values
[true, false] // All bool values
[Pedro, Lemos] // All String values

Хорошо, этот пример работает, но нереален, правда? Давай займемся чем-нибудь более полезным. А теперь представьте, что мы разрабатываем систему для управления заказами для пекарни. У нас есть такие классы:

class Item {
  final String type;
  final double price;
  Item({this.type, this.price});
}
class Bread extends Item {
  Bread({String type, double price}) : super(price: price, type: type);
}
class Cake extends Item {
 Cake({String type, double price}) : super(price: price, type: type);
}
class Coffee extends Item {
  Coffee({String type, double price}) : super(price: price, type: type);
}

Мы можем переименовать MyClass в BakeryOrder:

class BakeryOrder {
  var _order = <dynamic>[];
  void addNewItem(dynamic item) => _order.add(item);
  List<T> getItemsOfType<T>() => _order.whereType<T>().toList();
}

Мы можем добавлять новые элементы в наши заказы с помощью метода addNewItem и получать список элементов определенного типа с помощью метода getItemsOfType:

void main() {
  final instance = BakeryOrder();
  instance.addNewItem(Bread());
  instance.addNewItem(Coffee());
  instance.addNewItem(Cake());
  print(instance.getItemsOfType<Bread>());
  print(instance.getItemsOfType<Coffee>());
  print(instance.getItemsOfType<Cake>());
}

Результатом будет:

[Instance of 'Bread']
[Instance of 'Coffee']
[Instance of 'Cake']

Вот и все! Вы можете использовать дженерики для достижения такого результата и многого другого. Надеюсь, вам понравилась эта статья!