Что такое Generics в Dart?

Ответ

Generics в Dart — это синтаксис и система типов, позволяющие писать код, параметризованный типами. Это основа для создания типобезопасных коллекций и утилит, которые являются краеугольным камнем языка.

Зачем они нужны? Без Generics пришлось бы использовать тип dynamic или Object, что лишает проверки типов и требует постоянных приведений (casts).

// Без Generics (небезопасно и неудобно)
class OldBox {
  final Object value;
  OldBox(this.value);
}

void main() {
  var box = OldBox('строка');
  int length = (box.value as String).length; // Требуется явное приведение типа
}

// С Generics (безопасно и удобно)
class NewBox<T> {
  final T value;
  NewBox(this.value);
}

void main() {
  var box = NewBox<String>('строка');
  int length = box.value.length; // Компилятор знает, что value — это String
}

Generics в функциях и методах:

// Функция, возвращающая первый элемент списка любого типа
T getFirst<T>(List<T> list) {
  if (list.isEmpty) throw StateError('List is empty');
  return list.first;
}

void main() {
  print(getFirst<int>([1, 2, 3])); // 1
  print(getFirst<String>(['a', 'b'])); // a
  // Тип часто выводится автоматически:
  print(getFirst([1.5, 2.7])); // 1.5, T выводится как double
}

Использование в Flutter API:

// FutureBuilder автоматически выводит тип из Future<T>
FutureBuilder<List<User>>(
  future: fetchUsers(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      // snapshot.data имеет тип List<User>
      return ListView.builder(
        itemCount: snapshot.data!.length,
        itemBuilder: (ctx, i) => Text(snapshot.data![i].name),
      );
    }
    return CircularProgressIndicator();
  },
)

Ответ 18+ 🔞

А, дженерики в Dart! Ну это ж, ёпта, как в армии — всё по полочкам, всё по типам, чтоб не было бардака. Смотри, без них ты как в тумане: суёшь в коробку что попало, а потом пытаешься вытащить и понять, что это за хуйня. А компилятор тебе: «Сам разбирайся, мудила, я тебе не нянька».

Вот представь, старый способ — это как коробка из-под обуви, куда ты скидываешь и носки, и гвозди, и бутерброд. Тип Object или dynamic. Вытаскиваешь — а там, блядь, сюрприз. Хотел строку, а тебе в руки бутерброд с колбасой выпадает. И приходится каждый раз гадать и приводить типы вручную, как будто ты экстрасенс, ёбана.

// Вот это пиздец, товарищи. Полная анархия.
class OldBox {
  final Object value; // Может быть чем угодно! Даже твоей старой любовницей!
  OldBox(this.value);
}

void main() {
  var box = OldBox('строка');
  // А теперь, внимание, фокус! Я знаю, что там строка! А компилятор — нет.
  int length = (box.value as String).length; // Каждый раз этот ёбаный cast!
}

А теперь с дженериками — это как купить коробку с надписью «ТОЛЬКО ДЛЯ СТРОК». Суёшь туда что-то другое — компилятор тебе сразу: «Э, сабака сука, куда прешь? Не лезет!». И волнение ебать — никакого. Всё типобезопасно, красиво, как у швейцара в дорогом отеле.

// Вот это уже цивилизация, блядь. Порядок.
class NewBox<T> { // Вот этот <T> — это типа «шаблон». Подставишь String — будет коробка для строк.
  final T value; // Компилятор теперь в курсе, что здесь лежит Т.
  NewBox(this.value);
}

void main() {
  var box = NewBox<String>('строка'); // Говорим прямо: «Бокс, будь добр, только строки».
  int length = box.value.length; // И тут уже нихуя не приводим! Компилятор сам всё понял. Умница.
}

В функциях — вообще магия. Написал один раз универсальную функцию — и она работает с чем угодно. С числами, со строками, с твоими глупыми мыслями. Главное — тип указать.

// Функция-выручалочка. Возвращает первый элемент из любого списка.
T getFirst<T>(List<T> list) { // Смотри: принимает List<T> и возвращает T. Гениально и просто.
  if (list.isEmpty) throw StateError('List is empty'); // На пустой список — сразу в пизду.
  return list.first; // Берём первый и не паримся.
}

void main() {
  print(getFirst<int>([1, 2, 3])); // Говорим: «T — это int». Получаем 1.
  print(getFirst<String>(['a', 'b'])); // Теперь T — String. Получаем 'a'.
  // А часто компилятор и сам догадывается, умный такой, ядрёна вошь!
  print(getFirst([1.5, 2.7])); // Смотрит на список — видит double. T = double. Всё, красота.
}

Ну а в Flutter без них вообще нихуя. Весь фреймворк на них стоит. Вот смотри, FutureBuilder — он же не знает заранее, какой Future ты ему подсунешь. Может, список юзеров, а может, погоду в Зимбабве. Так вот дженерик <T> ему и говорит: «Слушай, брат, я буду строить виджет на основе Future<T>. А T — это то, что ты мне дашь».

FutureBuilder<List<User>>( // Вот! Кричим на весь мир: T = List<User>!
  future: fetchUsers(), // Эта функция возвращает Future<List<User>>. Всё сходится.
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      // И тут уже snapshot.data — это не какая-то хуйня, а конкретно List<User>!
      // Никаких сомнений, никаких приведений. Чистая работа.
      return ListView.builder(
        itemCount: snapshot.data!.length,
        itemBuilder: (ctx, i) => Text(snapshot.data![i].name), // И обращаемся к полям юзера напрямую.
      );
    }
    return CircularProgressIndicator(); // Нет данных — крутись, шарик, крутись.
  },
)

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