Обобщённые структуры
Часть раздела Object Oriented Programming путешествия по Rust на Coddy — урок 24 из 61.
До сих пор каждая созданная вами структура имела поля с конкретными типами. У Pet есть имя типа String, у Rectangle есть размеры типа f64. Но что, если вы хотите создать структуру, которая может содержать данные любого типа?
Именно здесь на помощь приходят обобщения (generics). Обобщения позволяют писать гибкий, повторно используемый код, который работает с множеством типов. Вместо жесткого кодирования конкретного типа вы используете заполнитель — обычно T — который заменяется конкретным типом при использовании структуры.
Вот простая структура-обертка, которая может содержать любой тип:
struct Wrapper<T> {
value: T,
}
<T> после имени struct объявляет параметр обобщенного типа. Внутри struct T служит заполнителем для любого типа, который вы будете использовать на самом деле. При создании экземпляра Rust выводит конкретный тип:
let int_wrapper = Wrapper { value: 42 }; // T — это i32
let float_wrapper = Wrapper { value: 3.14 }; // T — это f64
let text_wrapper = Wrapper { value: "hello" }; // T — это &str
Каждый из них является отдельным конкретным типом — Wrapper<i32>, Wrapper<f64> и Wrapper<&str> — но все они используют одно и то же определение структуры. Это избавляет от необходимости писать отдельные структуры IntWrapper, FloatWrapper и StringWrapper, которые делают по сути одно и то же.
Буква T — это просто соглашение (сокращение от «Type»). Вы можете использовать любой допустимый идентификатор, но T является стандартным в Rust и большинстве других языков с поддержкой обобщений.
Задание
ЛегкоДавайте создадим универсальный контейнер, который может хранить значение любого типа! Вы создадите структуру Container, которая продемонстрирует, как обобщения (generics) делают ваш код гибким и пригодным для повторного использования.
Вы организуете свой код в двух файлах:
container.rs: Определите публичную универсальную структуру с именемContainer<T>с одним публичным полемitemтипаT. Этот контейнер должен иметь возможность хранить любой тип — целые числа, числа с плавающей запятой, строки или что-либо еще.main.rs: Подключите модуль контейнера и продемонстрируйте его гибкость, создав контейнеры, содержащие различные типы данных. Вы создадите три контейнера и выведете содержимое каждого из них.
В основном файле создайте следующие контейнеры:
- Контейнер, содержащий целое число (первый ввод, преобразованный в
i32) - Контейнер, содержащий число с плавающей запятой (второй ввод, преобразованный в
f64) - Контейнер, содержащий строку (третий ввод, сохраненный как
String)
Ваш вывод должен отображать содержимое каждого контейнера в следующем формате:
Integer container: {value}
Float container: {value}
String container: {value}Например, при входных данных 42, 3.14 и hello:
Integer container: 42
Float container: 3.14
String container: helloОбратите внимание, как одно и то же определение Container<T> работает для всех трех различных типов — в этом и заключается сила обобщений!
Вы получите три входных значения: целое число, число с плавающей запятой и строку.
Шпаргалка
Обобщения (Generics) позволяют создавать структуры, которые могут хранить данные любого типа, используя заполнитель параметра типа.
Чтобы объявить обобщенную структуру, используйте угловые скобки с параметром типа (по соглашению это T):
struct Wrapper<T> {
value: T,
}
<T> объявляет параметр обобщенного типа, а T внутри структуры выступает в качестве заполнителя для фактического типа.
При создании экземпляров Rust выводит конкретный тип:
let int_wrapper = Wrapper { value: 42 }; // T — это i32
let float_wrapper = Wrapper { value: 3.14 }; // T — это f64
let text_wrapper = Wrapper { value: "hello" }; // T — это &str
Каждый экземпляр становится отдельным конкретным типом (Wrapper<i32>, Wrapper<f64>, Wrapper<&str>), при этом используя одно и то же определение структуры.
Попробуйте сами
mod container;
use container::Container;
fn main() {
// Прочитать три входных значения
let mut input1 = String::new();
std::io::stdin().read_line(&mut input1).expect("Failed to read line");
let int_value: i32 = input1.trim().parse().expect("Failed to parse integer");
let mut input2 = String::new();
std::io::stdin().read_line(&mut input2).expect("Failed to read line");
let float_value: f64 = input2.trim().parse().expect("Failed to parse float");
let mut input3 = String::new();
std::io::stdin().read_line(&mut input3).expect("Failed to read line");
let string_value = input3.trim().to_string();
// TODO: Создать три контейнера, используя Container<T>
// 1. Контейнер для целого числа, содержащий int_value
// 2. Контейнер для числа с плавающей запятой, содержащий float_value
// 3. Контейнер для строки, содержащий string_value
// TODO: Вывести содержимое каждого контейнера в требуемом формате
// Integer container: {value}
// Float container: {value}
// String container: {value}
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Object Oriented Programming
1Методы и поведение
Введение в блоки реализацииПараметр SelfИзменяемые методыАссоциированные функцииНесколько блоков реализацииЦепочки вызовов методовИтоги: действия с Rectangle4Проект: Виртуальный питомец
Определение питомцаКормление питомца2Инкапсуляция и модули
Основы модулейКлючевое слово PublicПриватные поляГеттерыСеттерыИтоги — Безопасный сейф5Обобщения
Обобщённые структурыОбобщённые методыНесколько обобщённых типовОбобщённые функцииПовторение — Координатная точка8Трейты как ограничения
Синтаксис ограничений трейтовМножественные ограниченияКонструкция whereВозврат типов с трейтамиИтоги — Обобщённый принтер11Паттерны проектирования в Rust
Паттерн NewtypeКомпозицияТрейт DropFrom и IntoИтоги — Мок-объект Smart Pointer3Продвинутые перечисления
Перечисления с даннымиМетоды в перечисленияхСопоставление вариантов с даннымиСнова об Option EnumИтоги — перечисление Shape6Определение трейтов
Что такое трейт?Реализация трейтовРеализации по умолчаниюПереопределение реализаций по умолчаниюТрейты с параметрамиИтоги — Media Player