Как Generics (обобщения) связаны с полиморфизмом в Dart?

Ответ

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

Без Generics (отсутствие полиморфизма или небезопасно):

// 1. Код дублируется для каждого типа (нет полиморфизма).
class IntBox { int value; IntBox(this.value); }
class StringBox { String value; StringBox(this.value); }

// 2. Использование dynamic/Object (полиморфно, но небезопасно).
class DynamicBox {
  dynamic value; // Потеряна безопасность типов.
  DynamicBox(this.value);
}

С Generics (параметрический полиморфизм с безопасностью типов):

// Один полиморфный класс для множества типов.
class Box<T> {
  final T value;
  Box(this.value);

  T getValue() => value;
}

void main() {
  // Компилятор выводит конкретный тип для каждого экземпляра.
  final intBox = Box<int>(42); // Box<int>
  final stringBox = Box('Hello'); // Box<String> (тип выведен)

  // Безопасность типов сохраняется.
  int number = intBox.getValue(); // OK
  // String text = intBox.getValue(); // Ошибка компиляции: type 'int' is not a subtype of 'String'
}

Связь с другими видами полиморфизма в Dart:

  • Полиморфизм подтипов (наследование): class SiberianHusky extends DogSiberianHusky может использоваться везде, где ожидается Dog.
  • Параметрический полиморфизм (Generics): List<Dog> может содержать SiberianHusky, Beagle и т.д., потому что они — подтипы Dog. Это показывает, как Generics взаимодействуют с полиморфизмом подтипов.

Практический пример из Flutter:

// Виджет FutureBuilder полиморфен относительно типа данных Future.
FutureBuilder<List<Post>>(
  future: _fetchPosts(), // Future<List<Post>>
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      // snapshot.data имеет тип List<Post>? благодаря generics.
      return ListView.builder(
        itemCount: snapshot.data!.length,
        itemBuilder: (ctx, i) => PostWidget(snapshot.data![i]), // Тип Post известен.
      );
    }
    return CircularProgressIndicator();
  },
)

Таким образом, Generics делают код полиморфным (работает со многими типами), строго типизированным (ошибки ловятся на этапе компиляции) и избегающим дублирования.

Ответ 18+ 🔞

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

Путь первый, идиотский: заведи отдельную коробку под каждый предмет. Это как если бы ты для целых чисел писал один класс, а для строк — другой, абсолютно такой же, но с другим названием. Ну, ёпта, реально? Овердохуища одинакового кода, а толку — ноль. Чистое безумие.

class IntBox { int value; IntBox(this.value); }
class StringBox { String value; StringBox(this.value); }

Путь второй, опасный: возьми одну коробку, но кинь туда всё, что попало. Типа dynamic или Object. Вроде бы полиморфизм есть — одна коробка на все случаи жизни. Но безопасность типов накрылась медным тазом. Вытащишь ты оттуда, ожидая бутерброд, а там — кот, который цапнет тебя за руку уже в рантайме. Доверия к такому коду — ебать ноль.

class DynamicBox {
  dynamic value; // Типа "а хрен его знает, что тут лежит"
  DynamicBox(this.value);
}

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

class Box<T> { // Смотри-ка, буква T. Это типа "тип, который придёт позже"
  final T value;
  Box(this.value);

  T getValue() => value; // И отдавать будешь именно его, а не какую-то хуйню
}

void main() {
  final intBox = Box<int>(42); // Всё, коробка запечатана. Только для int.
  final stringBox = Box('Hello'); // Тут компилятор сам догадался, что это String.

  int number = intBox.getValue(); // Всё ок, мы договорились, что тут число.
  // String text = intBox.getValue(); // А вот это — НИХУЯ! Ошибка компиляции. Спасибо, дженерики!
}

Как это связано с другими плюшками Dart? Ну, смотри. Есть полиморфизм через наследование: SiberianHusky — это подтип Dog. Так вот, дженерики с этим дружат. Если у тебя есть List<Dog>, то ты можешь запихнуть туда и SiberianHusky, и Beagle. Потому что они все — собаки. Дженерики не ломают иерархию типов, а используют её. Это, блядь, элегантно.

Ну и где это в Flutter'е? Да везде, ебать копать! Самый простой пример — FutureBuilder. Без дженериков он был бы просто FutureBuilder, который возвращает dynamic, и ты бы каждый раз гадал, что там в snapshot.data. А так — красота.

FutureBuilder<List<Post>>( // Говорим прямо: ждём мы Future, который вернёт List<Post>
  future: _fetchPosts(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      // Ура! Компилятор и IDE знают, что snapshot.data — это List<Post>?
      // И автодополнение работает, и если ошибся — сразу подчеркнёт.
      return ListView.builder(
        itemCount: snapshot.data!.length,
        itemBuilder: (ctx, i) => PostWidget(snapshot.data![i]), // Тип Post известен, всё безопасно.
      );
    }
    return CircularProgressIndicator();
  },
)

Короче, суть в чём: дженерики — это не про сложность, а про простоту. Один раз написал умный код, который работает с любым типом, но при этом не превращается в тыкву с дырой вместо безопасности. И дублировать ничего не надо. Пизда рулю, а не фича.