Общее поведение с помощью трейтов

RustBeginner
Практиковаться сейчас

Введение

Иногда несвязанные типы данных должны гарантировать одинаковое поведение. У датчика в теплице и отправления разные поля, но оба объекта могут описывать своё текущее состояние. Трейты Rust позволяют назвать такую общую возможность, не заставляя типы иметь одинаковую структуру данных.

В этой лабораторной работе вы определите один небольшой трейт, реализуете его для двух подготовленных структур и передадите оба значения одной обобщённой функции формирования отчёта. Более объёмные структуры и примеры данных уже созданы, поэтому каждое изменение будет сосредоточено на одной задаче.

Определите общий контракт состояния

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

Перейдите в подготовленный пакет и просмотрите весь каркас. Команда sed -n выводит указанные строки, не изменяя их:

cd /home/labex/project/status-reporter
sed -n '1,160p' src/main.rs

У Sensor и Shipment разные поля. Подготовленные текстовые поля используют &'static str — заимствованные строковые литералы, действительные на протяжении всей программы. Аннотация времени жизни 'static не относится к теме этого трейта и намеренно рассматривается позже; сейчас вам не нужно её изменять или разбираться в ней.

Ниже мест для вставки уже находится обобщённая функция show_status. Читайте T: Status по частям: T обозначает конкретный тип, двоеточие означает «должен соответствовать», а Status — требуемый трейт. Проще говоря: «для любого типа T, если T реализует Status». Это обещание позволяет функции безопасно вызывать item.status().

Трейт должен существовать до того, как Rust сможет понять это ограничение. Откройте исходный файл:

nano src/main.rs

Замените // STATUS_TRAIT следующим кодом:

trait Status {
    fn status(&self) -> String;
}

Строка метода заканчивается точкой с запятой, потому что трейт объявляет обязательный метод, но не выбирает конкретную реализацию. &self позволяет методу просмотреть значение, не забирая его во владение, а String даёт каждому типу возможность сформировать собственный текст состояния. В каждой последующей реализации необходимо в точности повторить имя метода, получатель и возвращаемый тип, а затем предоставить собственное тело метода.

Сохраните файл с помощью Ctrl+O, нажмите Enter и выйдите с помощью Ctrl+X. Затем проверьте пакет:

cargo check

Вы можете увидеть предупреждения о том, что трейт и подготовленные элементы пока не используются. Важнее всего итоговая строка Finished: она подтверждает, что общий контракт и обобщённое ограничение корректны. На следующих двух шагах вы добавите реализации и вызовы.

Реализуйте Status для Sensor

На этом шаге вы выполните контракт трейта для Sensor и передадите датчик в общий отчёт.

Блок impl Trait for Type связывает существующий трейт с конкретным типом. Сигнатура метода должна совпадать с сигнатурой трейта, а тело метода может использовать собственные поля этого типа.

Откройте исходный файл:

nano src/main.rs

Замените // SENSOR_IMPLEMENTATION следующей короткой реализацией:

impl Status for Sensor {
    fn status(&self) -> String {
        format!("{}: {}°C", self.location, self.celsius)
    }
}

format! создаёт и возвращает значение типа String; сама по себе эта макрокоманда ничего не выводит. Реализация может использовать location и celsius, потому что self — это заимствованный Sensor.

Затем внутри main замените // SENSOR_REPORT следующим кодом:

    show_status("Sensor", &sensor);

Аргумент &sensor соответствует параметру отчёта &T, передаваемому по заимствованию, поэтому sensor остаётся во владении main. Сохраните файл, выйдите из Nano и запустите программу:

cargo run --quiet
Sensor: Greenhouse: 24°C

Эта строка подтверждает, что Sensor реализует Status и может быть передан в обобщённую функцию формирования отчёта с ограничением. Отправление пока не подключено.

Реализуйте Status для Shipment

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

Снова откройте исходный файл:

nano src/main.rs

Замените // SHIPMENT_IMPLEMENTATION следующим кодом:

impl Status for Shipment {
    fn status(&self) -> String {
        format!("#{} {}", self.id, self.stage)
    }
}

Этот метод следует тому же контракту трейта, но использует собственные поля отправления — id и stage. Затем замените // SHIPMENT_REPORT следующим кодом:

    show_status("Shipment", &shipment);

Сохраните файл и выйдите из Nano. Запустите готовый пакет:

cargo run --quiet
Sensor: Greenhouse: 24°C
Shipment: #204 in transit

Обе строки формирует show_status<T: Status>. Ограничение описывает поведение, необходимое функции, а каждая реализация определяет, как соответствующее значение предоставляет это поведение.

Итоги

Вы описали общее поведение с помощью трейта, независимо реализовали его для двух структур и использовали простое ограничение трейта, благодаря которому одна обобщённая функция формирования отчёта принимает оба типа.