← Оглавление

3.1 Общий язык

Есть соглашения, которых не нужно подписывать. Два мастера из одной гильдии знают их наизусть: что можно просить, что обязан дать, в чём вправе отказать. Мастер, не знающий этих соглашений, объясняет - каждый раз, заново, вручную. Это принято называть индивидуальным подходом. Те, кто практикует его достаточно долго, знают другое слово.

Что нужно

Task умеет честно сообщать об ошибках и называть отсутствующее по имени. Но он всё ещё говорит на своём языке. Создать задачу можно только через Task::new(id, title)? - явно, с проверкой, каждый раз одинаково.

В экосистеме Rust есть соглашение для одной распространённой ситуации: “из B можно получить A”. Когда тип объявляет об этом, остальной код принимает это как данность: функции, ожидающие A, автоматически принимают B - без объяснений, без посредников.

Соглашение оформляется через ключевое слово trait. Task должен объявить одно из них: что он умеет создавать новую задачу из &str.

Сборка

В стандартной библиотеке соглашение о преобразовании типов выглядит так:

pub trait From<T> {
    fn from(value: T) -> Self;
}

trait определяет контракт: любой тип, реализующий этот трейт, обязан предоставить метод fn from(value: T) -> Self. Компилятор проверит, выполнено ли обещание. Self означает тип, для которого пишется impl - здесь это будет Task.

Реализуем его для Task:

impl From<&str> for Task {
    fn from(title: &str) -> Self {
        Task {
            id: 0,
            title: title.to_string(),
            status: Status::Todo,
        }
    }
}

У from нет доступа к списку задач - некому выдать следующий идентификатор. Пока используем id: 0; в M3.3 хранилище возьмёт присвоение идентификаторов на себя, и Task::from станет естественным путём создания при добавлении.

Теперь в main:

let from_task = Task::from("Quick note");
println!("{:?}", from_task);

Но у From есть следствие.

Стандартная библиотека содержит общее правило: реализовав From<A> for B, вы автоматически получаете возможность использовать .into() на значении типа A, чтобы получить B. Это не нужно реализовывать отдельно:

let task: Task = "Quick note".into();

Типовая аннотация : Task здесь необходима - компилятор должен знать, во что превращать строку.

Итог

Task::from("Buy coffee") и "Buy coffee".into() работают. Task теперь знает гильдейский стандарт - и любой код, принимающий “что-то, из чего получается Task”, примет строку без лишних слов. В следующей главе add воспользуется этим.

Два теста покрывают оба пути:

#[test]
fn task_from_str_sets_title() {
    let task = Task::from("Buy coffee");
    assert_eq!(task.title, "Buy coffee");
}

#[test]
fn str_converts_into_task() {
    let task: Task = "Buy milk".into();
    assert_eq!(task.title, "Buy milk");
}

make ci проходит. Task объяснять больше не нужно.

Полный код tq для этой главы - 3-a-voice/01-the-common-tongue/.


Lore: Обязательное и необязательное

Трейт может объявить сколько угодно методов - но не все из них обязательны. Метод без реализации по умолчанию обязан быть предоставлен: компилятор потребует его при каждом impl. Метод с реализацией по умолчанию можно переопределить, а можно оставить как есть.

pub trait Greet {
    fn name(&self) -> &str;                          // обязательный
    fn hello(&self) { println!("Hi, {}!", self.name()); } // необязательный
}

Тип, реализующий Greet, обязан написать name - и бесплатно получает hello.

From<T> полностью обязательный: один метод, реализации по умолчанию нет.


Lore: From и Into - две стороны одного соглашения

Реализовав From<&str> for Task, вы бесплатно получаете Into<Task> for &str. В стандартной библиотеке есть общий impl (обобщённые типы разберёт M3.3):

impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

Практическое правило отсюда: всегда реализуй From. Никогда не реализуй Into вручную - он появляется сам.

From - контракт без исключений: по соглашению он должен всегда завершаться успешно. Если преобразование может не получиться, Rust предлагает TryFrom<T> - та же идея, но метод возвращает Result<Self, E>. По существу, Task::new уже ведёт себя как TryFrom<&str>: принимает строку, возвращает Result<Task, TqError>.