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>.