Generics. Обобщенные типы

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

Generics или обобщения делают код гибче, позволяют использовать более широкий набор типов данных.

Рассмотрим сначала проблему, которую призваны решить обобщения. Допустим, у нас есть следующая структура Person, которая описывает пользователя:

struct Person { 
	id: u32, 		// идентификатор
	name: String	// имя
}

fn main(){
    
	let tom = Person {id: 245, name: "Tom".to_string()};
	println!("id: {}  name: {}", tom.id, tom.name);
}

Одно из полей структуры - id представляет идентификатор пользователя. В данном случае он представляет целое число. Это довольно распространенное решение, когда идентификатор выражается через числовое значение. Однако нередко для идентификаторов также используются строки, либо даже какие-то другие составные типы данных. Как сделать, чтобы идентификатор мог представлять и числа, и строки, и в перспективе какие-то другие типы (например, могли бы создать для идентификатора отдельную структуру)? Обобщения решают эту проблему. И в данном случае нам надо сделать тип Person обобщенным или универсальным.

Для определения обобщенных типов после их названия в угловых скобках указываются параметры типа (type parameter):

struct название_структуры <название_параметра>{
	//..........
}

Например, сделаем структуру Person обобщенной:

struct Person<T> { 
	id: T, 		// идентификатор
	name: String	// имя
}

В данном случае параметр типа называется T. Как правило, название параметра типа представляет одну заглавную букву, чаще букву T (от слова type - тип).

После опредления параметра типа мы можем использовать его название как название обычного типа данных внутри обощенной структуры Person. Так, в данном случае поле id имеет тип T, то есть тип данных, который будет передаваться через этот параметр. Причем на момент определения структуры мы сами можем не знать, что это будет за тип данных.

Теперь используем эту структуру:

struct Person<T> { 
	id: T, 		// идентификатор
	name: String	// имя
}
fn main(){
    
	// поле id представляет число
	let tom = Person {id: 245, name: "Tom".to_string()};	
	println!("id: {}  name: {}", tom.id, tom.name);
	
	// поле id представляет строку
	let bob = Person {id: String::from("fhe34u847"), name: "Bob".to_string()};
	println!("id: {}  name: {}", bob.id, bob.name);
}

Для первого объекта структуры - tom параметр T будет представлять тип i32:

let tom = Person {id: 245, name: "Tom".to_string()};

Тогда как для второго объекта - bob параметр типа будет представлять тип String:

let bob = Person {id: String::from("fhe34u847"), name: "Bob".to_string()};

В итоге мы получим следующий консольный вывод:

id: 245  name: Tom
id: fhe34u847  name: Bob

Таким образом, мы ушли от жесткой привязки к типам данных и сделали программму чуть гибче.

Стоит отметить, в рамках объекта обобщенного типа один параметр типа может передавать только один тип. Например, возьмем следующую ситуацию:

struct Point<T> {
    x: T,
    y: T,
}

fn main(){
    
	let some_point = Point { x: 3, y: 2.6 };
	println!("x={}  y={}", some_point.x, some_point.y);
}

Структура Point содержит два поля (условно координаты x и y точки), тип которых задается с помощью параметра T. Однако в функции main при создании объекта структуры Point полю x передается значение типа i32, а полю y - значение типа f64. Поэтому компиляция программы завершится ошибкой. То есть нам надо для обоих полей задать значения одного и того же типа.

Установка нескольких параметров типа

При необходимости мы можем использовать более одного параметра типа. В этом случае они все перечисляются через запятую в угловых скобках. Например:

struct Point<T, S> {
    x: T,
    y: S,
}

fn main(){
    
	let some_point = Point { x: 3, y: 2.6 };
	println!("x={}  y={}", some_point.x, some_point.y);
}

В данном случае структура Point применяет два параметра - T и S. В этом случае поля структуры x и y могут представлять разные типы, представленные параметрами T и S.

Обобщенные перечисления

Аналогичным образом можно определять обобщенные перечисления:

enum DayTime<T>{
     
    Morning(T),
    Evening(T)
}

fn main(){
    
	let morning = DayTime::Morning("Доброе утро".to_string());	// параметр T представляет тип String
	if let DayTime::Morning(morning_value)= morning { println!("Morning: {}", morning_value);}
    
     
    let evening = DayTime::Evening(16);		// параметр T представляет тип i32
	if let DayTime::Evening(evening_value)= evening { println!("Evening: {}", evening_value);}
}

В данном случае перечисление DayTime типизируется параметром T. Поэтому при определении переменных мы можем передать константам перечисления значения разных типов.

Консольный вывод программы:

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