3.2 Что можно требовать
Хороший рецепт не пишет “мука торговой марки X в упаковке по 500 граммов”. Он пишет “мука”. Плохой инструмент разницы не делает: требует ровно то, что требовалось в прошлый раз, и отказывает всему остальному без объяснений. Это принято называть строгостью. Правильное слово - случайная специфика.
Попытка
Перенесём создание задачи внутрь add: пусть принимает id и title напрямую, без Task::new у вызывающего.
Заголовок - строка, очевидный выбор типа:
fn add(tasks: &mut Vec<Task>, id: u64, title: String) -> Result<(), TqError> {
tasks.push(Task::new(id, &title)?);
Ok(())
}
Вызов выглядит естественно:
add(&mut tasks, 1, "Buy coffee")?;
Компилятор замечает несоответствие:
error[E0308]: mismatched types
--> src/main.rs:XX:XX
|
XX | add(&mut tasks, 1, "Buy coffee")?;
| ^^^^^^^^^^^^ expected `String`, found `&str`
|
= note: expected struct `String`
found reference `&str`
help: try using a conversion method
|
XX | add(&mut tasks, 1, "Buy coffee".to_string())?;
| +++++++++++++
Компилятор даже предлагает выход: .to_string(). Он не ошибается - это сработает. Но
перекладывать эту работу на каждого вызывающего - именно этого и хотелось избежать.
Что замечает компилятор
String и &str - разные типы (M1.2 объясняет оба). add объявил, что принимает
String. Литерал "Buy coffee" - &str. Компилятор держит их отдельно и не конвертирует
неявно.
Решение - не требовать конкретный тип, а требовать способность: “дай мне что-нибудь, что
умеет становиться String”. В Rust это записывается как граница на тип:
title: impl Into<String>
impl Into<String> означает: любой тип, реализующий трейт Into<String>. И &str, и
String его реализуют - для обоих существует From, а From даёт Into автоматически
(M3.1). Внутри функции title.into() превращает переданное значение в String - один раз,
в одном месте.
Заклинание
fn add(tasks: &mut Vec<Task>, id: u64, title: impl Into<String>) -> Result<(), TqError> {
let title = title.into();
tasks.push(Task::new(id, &title)?);
Ok(())
}
Теперь оба варианта работают без изменений у вызывающего:
add(&mut tasks, 1, "Buy coffee")?; // &str - работает
add(&mut tasks, 2, String::from("Buy milk"))?; // String - тоже работает
main становится чище: вместо add(&mut tasks, Task::new(1, "Buy coffee")?) - просто
add(&mut tasks, 1, "Buy coffee")?.
Два теста закрывают новое поведение:
#[test]
fn add_accepts_str_literal() {
let mut tasks = Vec::new();
assert!(add(&mut tasks, 1, "Buy coffee").is_ok());
assert_eq!(tasks.len(), 1);
}
#[test]
fn add_rejects_empty_title() {
let mut tasks = Vec::new();
assert!(add(&mut tasks, 1, "").is_err());
}
Первый - что &str проходит как есть. Второй - что валидация из Task::new никуда
не делась: пустой заголовок по-прежнему отклоняется. Полный набор тестов - в tq/.
make ci проходит. add принимает любую строку - и не перекладывает лишнюю работу на
вызывающего.
Полный код
tqдля этой главы -3-a-voice/02-what-you-can-demand/.