Обобщения Dart

Если открыть документацию API базового массивного типа List, вы увидите, что его реальный тип — List<E>. Угловые скобки <...> обозначают обобщенный тип (параметризованный тип), то есть тип с формальными типовыми параметрами. По соглашению почти все типовые переменные именуются одной буквой, например E, T, S, K, V.

Зачем использовать обобщения?

  • Главная задача обобщений — обеспечение типовой безопасности
  • Корректное указание обобщений позволяет генерировать более оптимизированный исполнимый код;
  • Использование обобщений значительно сокращает дублирование кода.

Если нужно, чтобы список хранил только строки, объявите его как List<String> — это список строк.

После объявления списка исключительно для строк попытка поместить в него значение другого типа приведёт к ошибке.

// Ошибка статической проверки типов
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Ошибка компиляции: 42 не является строкойCode language: PHP (php)
D:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:5:12: Error: The argument type 'int' can't be assigned to the parameter type 'String'.
        names.add(42); // Ошибка компиляции: 42 не является строкойCode language: PHP (php)

Устранение избыточности кода

Предположим, вам нужен абстрактный интерфейс для кэша объектов:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}
Code language: JavaScript (javascript)

Позже, если понадобится кэш только для строк, придётся создавать отдельный интерфейс:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}
Code language: Dart (dart)

Затем появится потребность в кэше для чисел… придётся многократно писать почти одинаковый код.

Например, вы напишете такой фрагмент:

abstract class IntCache {
  int getByKey(String key);
  void setByKey(String key, int value);
}Code language: Dart (dart)

Обобщения решают эту проблему одним решением: достаточно определить универсальный интерфейс с типовым параметром:

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}Code language: Dart (dart)

Здесь T — это плейсхолдер типа, обозначающий любой конкретный тип, передаваемый разработчиком позже.

Конкретный пример

// Абстрактный базовый класс обобщённого кэша
abstract class Cache<T> {
  T? getByKey(String key);
  void setByKey(String key, T value);
  void remove(String key);
}

// Реализация кэша в памяти (на основе Map)
class MemoryCache<T> implements Cache<T> {
  final Map<String, T> _storage = {};

  @override
  T? getByKey(String key) {
    return _storage[key];
  }

  @Override
  void setByKey(String key, T value) {
    _storage[key] = value;
  }

  @Override
  void remove(String key) {
    _storage.remove(key);
  }
}

void main() {
  // 1. Пример строкового кэша
  final Cache<String> strCache = MemoryCache<String>();
  strCache.setByKey("name", "Tom");
  print(strCache.getByKey("name")); // Tom

  // 2. Кэш целых чисел int
  final Cache<int> intCache = MemoryCache<int>();
  intCache.setByKey("age", 26);
  print(intCache.getByKey("age")); // 26

  // 3. Кэш пользовательских объектов
  final Cache<User> userCache = MemoryCache<User>();
  userCache.setByKey("u1", User("Jack", 18));
  final User? user = userCache.getByKey("u1");
  print(user?.name); // Jack

  // Тест удаления
  userCache.remove("u1");
  print(userCache.getByKey("u1")); // null
}

// Пользовательский сущностный класс
class User {
  final String name;
  final int age;
  User(this.name, this.age);
}Code language: Dart (dart)

Использование литералов обобщённых коллекций

Литералы List, Set и Map поддерживают параметризованные обобщения:

  • List / Set: перед открывающей квадратной скобкой пишите <Тип>
  • Map: перед открывающей фигурной скобкой пишите <ТипКлюча, ТипЗначения>

Примеры:

// Список строк
var names = <String>['Seth', 'Kathy', 'Lars'];

// Множество строк (автоматическое удаление дубликатов)
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};

// Словарь со строковыми ключами и значениями
var pages = <String, String>{
  'index.html': 'Главная страница',
  'robots.txt': 'Файл инструкций для поисковых ботов',
  'humans.txt': 'Описательный файл для разработчиков',
};
Code language: PHP (php)

Запуск примера кода

void main() {
  // 1. List<String> Список строк: упорядоченный, допускает дубликаты, доступ по индексу
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  print("=== List Список ===");
  print(names); // [Seth, Kathy, Lars, Seth] дубликаты разрешены
  print(names[0]); // Получить первый элемент по индексу: Seth
  names.add("Mike"); // Добавить элемент в конец
  print("После добавления: $names");

  // 2. Set<String> Множество строк: неупорядоченное, автоудаление дубликатов, без индексов
  var uniqueNames = <String>{'Seth', 'Kathy', 'Lars', 'Seth'};
  print("\n=== Set Множество (автоудаление дубликатов) ===");
  print(uniqueNames); // {Seth, Kathy, Lars} повторяющийся Seth удалён автоматически
  print(uniqueNames.contains("Kathy")); // Проверка наличия: true
  uniqueNames.remove("Lars");
  print("После удаления Lars: $uniqueNames");

  // 3. Map<String, String> Словарь ключ-значение: уникальные ключи, доступ по ключу
  var pages = <String, String>{
    'index.html': 'Главная страница',
    'robots.txt': 'Файл инструкций для поисковых ботов',
    'humans.txt': 'Описательный файл для разработчиков',
  };
  print("\n=== Map Словарь ===");
  print(pages);
  print(pages['index.html']); // Получить значение по ключу: Главная страница
  pages["about.html"] = "Страница «О нас»"; // Добавить пару ключ-значение
  print("После добавления about.html: $pages");

  // Популярные примеры обхода коллекций
  print("\n=== Обход List ===");
  for (var name in names) {
    print("- $name");
  }

  print("\n=== Обход всех записей Map ===");
  for (var entry in pages.entries) {
    print("Файл: ${entry.key} → Описание: ${entry.value}");
  }
}Code language: Dart (dart)

Использование обобщённых параметров в конструкторах

При вызове конструктора укажите обобщённый тип в угловых скобках после имени класса:

var nameSet = Set<String>.of(names);Code language: JavaScript (javascript)

Следующий код создаёт словарь с ключами типа int и значениями типа View:

var views = SplayTreeMap<int, View>();
Code language: HTML, XML (xml)
import 'dart:collection';

void main() {
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  var nameSet = Set<String>.of(names);
  print("Исходный список: $names");
  print("Множество без дубликатов: $nameSet");

  // Для SplayTreeMap нужен импорт dart:collection
  var views = SplayTreeMap<int, String>();
  views[5] = "Страница настроек";
  views[2] = "Главная страница";
  views[9] = "Моя страница";
  views[1] = "Экран запуска";

  print("\nОтсортированный TreeMap:");
  for (var e in views.entries) {
    print("${e.key} : ${e.value}");
  }
}Code language: PHP (php)

Исполняемые типы обобщённых коллекций

Реификация типов (reified)

Обобщения Dart реализованы по принципу реификации: вся информация об обобщённом типе сохраняется во время исполнения.

Вы можете напрямую проверять конкретный обобщённый тип коллекции во время работы программы:

void main() {
    var names = <String>[];
	names.addAll(['Seth', 'Kathy', 'Lars']);
	print(names is List<String>); // Выведет true
}Code language: PHP (php)

Дополнительное сравнение: в Java обобщения используют стирание типов, все обобщённые параметры удаляются на этапе исполнения. В Java можно только проверить, является ли объект List, но отличить List<String> от List<int> невозможно.

* Ограничение типов обобщённых параметров

Граница типа (bound). Если вы ещё не изучали классы, достаточно поверхностного понимания этого раздела.

При определении обобщения можно ограничить передаваемые типы подклассами определённого суперкласса. Такое ограничение называется границей типа и реализуется через ключевое слово extends.

1 Ограничение на неподдерживающие null типы

Запретить передачу типов с возможным значением null (стандартная верхняя граница — допускающий null тип Object?):

class Foo<T extends Object> {
  // Переданный тип T не может допускать значение null
}
Code language: JavaScript (javascript)

2 Ограничение подклассами указанного суперкласса

Используйте extends для указания базового класса — тогда вы сможете напрямую вызывать все его член-методы:

class Foo<T extends SomeBaseClass> {
  String toString() => "Экземпляр Foo<$T>";
}

class Extender extends SomeBaseClass {}
Code language: JavaScript (javascript)

Допустимые типовые аргументы: сам суперкласс и любые его подклассы

var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();
Code language: HTML, XML (xml)

Если обобщённый параметр не указан, по умолчанию используется граничный тип:

var foo = Foo();
print(foo); // Выведет: Экземпляр Foo<SomeBaseClass>
Code language: PHP (php)

Передача не связанного типа вызовет статическую ошибку компиляции:

var foo = Foo<Object>(); // Ошибка компиляции: Object не является подклассом SomeBaseClass
Code language: JavaScript (javascript)

* Самоссылающиеся границы типов (F-bound ограниченные обобщения)

При наложении ограничений на обобщения граница может ссылаться на собственный типовой параметр, формируя самоограничение — это называется F-bound.

Стандартный пример: интерфейс сравнимости Comparable<T>

abstract interface class Comparable<T> {
  int compareTo(T o);
}

// T должен реализовывать Comparable<T>, сравнение возможно только с объектами того же типа
int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
    t1.compareTo(t2) + 1;

class A implements Comparable<A> {
  @override
  int compareTo(A other) {
    // Реализация логики сравнения
    return 0;
  }
}

int res = compareAndOffset(A(), A());
Code language: PHP (php)

Ограничение T extends Comparable<T> означает: объект типа T можно сравнивать только с экземплярами того же самого типа.

* Обобщённые методы

Обычные функции и классовые методы также поддерживают обобщённые типовые параметры:

T first<T>(List<T> ts) {
  // Логика предварительной проверки
  T tmp = ts[0];
  // Логика последующей обработки
  return tmp;
}
Code language: PHP (php)

Полный пример

T first<T>(List<T> ts) {
  if (ts.isEmpty) {
    throw ArgumentError("Список не может быть пустым");
  }
  T tmp = ts[0];
  return tmp;
}

void main() {
  List<String> strList = ["apple", "banana", "orange"];
  String firstStr = first(strList);
  print(firstStr); //apple

  List<int> numList = [10, 20, 30];
  int firstNum = first(numList);
  print(firstNum);//10
}Code language: Dart (dart)

Обобщённый параметр <T> влияет на три участка кода:

  1. Тип возвращаемого значения функции T
  2. Тип входного параметра List<T>
  3. Тип локальной переменной T tmp

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *