Generics или обобщения позволяют добавить программе гибкости и уйти от жесткой привязки к определенным типам. Иногда возникает необходимость, определить функционал таким образом, чтобы он мог использовать данные любых типов.
Например, мы определяем класс пользователя, который содержит id (идентификатор пользователя):
class Person{ int id; // идентификатор пользователя String name; // имя пользователя Person(this.id, this.name); }
В данном случае id задан как числовое значение, то есть оно может быт равно 2, 5, 7 и так далее. Но возможно, нам захочется впоследствии использовать в качестве типа id строки или какие-то другие классы. Для добавления гибкости мы могли бы использовать оператор dynamic, чтобы избежать жесткой привязки к типу:
class Person{ dynamic id; // идентификатор пользователя String name; // имя пользователя Person(this.id, this.name); } void main (){ Person tom = Person(134, "Tom"); print(tom.id); Person bob = Person("324", "Bob"); print(bob.id); }
Однако мы можем не знать, какой именно объект представляет поле id, особенно если класс Person определен во внешней библиотеке, которая написана сторонними разработчиками. И при попытке получить число в данном случае мы столкнемся с исключением:
Person bob = Person("324", "Bob"); int id = bob.id; // Ошибка
Можно предусмотреть два типа Person для работы с разными типами:
class PersonInt{ int id; String name; PersonInt(this.id, this.name); } class PersonString{ string id; String name; PersonString(this.id, this.name); }
Но в данном случае мы сталкиваемся с другой проблемой - дублированием кода.
Generics или обобщения позволяют обеспечить большую безопасность типов и помогают избежать дублирования кода. Перепишем код класса Person с использованием generics:
void main (){ Person bob = Person("324", "Bob"); print(bob.id.runtimeType); // String Person sam = Person(123, "Sam"); print(sam.id.runtimeType); // int } class Person<T>{ T id; // идентификатор пользователя String name; // имя пользователя Person(this.id, this.name); }
С помощью выражения <T>
мы указываем, что класс Person типизирован определенным типом T. T еще называется универсальным параметром.
Причем название параметра может быть произвольным, но обычно используются заглавные буквы, часто буква T. После мы можем использовать T как обычный тип, например, определять переменные этого типа: T id;
.
При выполнении программы вместо Т будет подставляться конкретный тип. Причем тип будет вычислять динамически на основе переданных значений. С помощью поля runtimeType мы можем получить конкретный тип данных переменной. Мы также могли бы явным образом обозначить, какие типы будут использоваться в объектах:
Person<String> bob = Person<String>("324", "Bob"); print(bob.id.runtimeType); Person<int> sam = Person<int>(123, "Sam"); print(sam.id.runtimeType);
Тип, которым типизируется класс, указывается в угловых скобках после названия класса (Person<String>
).
Подобным образом мы можем определять generic-методы и функции. Например, определим и используем небольшую функцию логгирования:
void main (){ int x = 20; log(x); x = 34; log(x); String name = "Tom"; log(name); } void log<T>(T a){ // DateTime.now() - получает текущую дату и время print("${DateTime.now()} a=$a"); }
Для создания обобщенного метода после его имени указывается в угловых скобках название универсального параметра. После этого внутри этого универсальный тип T может использоваться внутри метода - в качестве типа параметров или переменных
Иногда необходимо использовать обобщения, однако принимать любой тип в функцию или класс вместо параметра T нежелательно. Например, у нас есть следующий класс Account, который представляет банковский счет::
class Account{ int id; // номер счета int sum; // сумма на счете Account(this.id, this.sum); }
Для перевода средств с одного счета на другой мы можем определить класс Transaction, который для выполнения всех операций будет использовать объекты класса Account.
Но у класса Account может быть много наследников: DepositAccount (депозитный счет), DemandAccount (счет до востребования) и т.д. И мы не можем знать, какие именно типы счетов будут использоваться в классе Transaction. Возможно, транзакции будут проводиться только между счетами до востребования. И в этом случае в качестве универсального параметра можно установить тип Account:
class Transaction<T extends Account>{ T fromAccount; // с какого счета перевод T toAccount; // на какой счет перевод int sum; // сумма перевода Transaction(this.fromAccount, this.toAccount, this.sum); void execute(){ if (fromAccount.sum > sum){ fromAccount.sum -= sum; toAccount.sum += sum; print("Счет ${fromAccount.id}: ${fromAccount.sum}\$ \nСчет ${toAccount.id}: ${toAccount.sum}\$"); } else { print("Недостаточно денег на счете ${fromAccount.id}"); } } }
С помощью выражения <T extends Account>
указываем, что используемый тип T обязательно должен быть классом
Account или его наследником. Благодаря подобному ограничению мы можем использовать внутри класса Transaction все объекты типа
T именно как объекты Account и соответственно обращаться к их полям и методам.
Теперь применим класс Transaction:
void main (){ Account acc1 = Account(1857, 4500); // sum = 4500; Account acc2 = Account(3453, 5000); // sum = 5000; Transaction transaction = Transaction<Account>(acc1, acc2, 1900); transaction.execute(); }
Консольный вывод:
Счет 1857: 2600$ Счет 3453: 6900$
Как и обычные классы, обобщенные классы могут наследоваться. И тут есть особенности. Например, если мы хотим сохранить обобщенность унаследованного функционала, то производный класс также определяется обобщенным:
void main (){ Employee bob = Employee(123, "Bob", "Google"); bob.display(); } class Person<T>{ T id; // идентификатор пользователя String name; // имя пользователя Person(this.id, this.name); void display() => print("id: $id \t name: $name"); } class Employee<T> extends Person<T>{ String company; Employee(super.id, super.name, this.company); @override void display(){ super.display(); print("Works in $company"); } }
В данном случае класс Employee также является обобщенным, благодаря чему в качестве id в его конструктор можно передать значение любого типа.
Но также можно при наследовании указать, что наследуется класс, типизированный определенным типом:
void main (){ Employee bob = Employee(123, "Bob", "Google"); bob.display(); } class Person<T>{ T id; // идентификатор пользователя String name; // имя пользователя Person(this.id, this.name); void display() => print("id: $id \t name: $name"); } class Employee extends Person<int>{ String company; Employee(super.id, super.name, this.company); @override void display(){ super.display(); print("Works in $company"); } }
Теперь класс Employee унаследован от класса Person, который типизирован типом int. Поэтому в классе Employee унаследованное поле id рассматривается как поле типа int.