Generics

Последнее обновление: 09.04.2023

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.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850