Чтение сигнатур функций Rust
Перевод | Автор оригинала: Ana Hoverbear
В Rust сигнатуры функций рассказывают историю. Просто взглянув на сигнатуру функции, опытный пользователь Rust может сказать многое о ее поведении.
В этой статье мы исследуем некоторые подписи и поговорим о том, как их читать и извлекать из них информацию. Во время изучения вы можете найти множество отличных примеров сигнатур функций в документации Rust API. Можно поиграть на манеже.
В этой статье предполагается, что вы знакомы с Rust, небольшого упоминания в книге будет вполне достаточно, если вам этого не хватает, но вы уже программировали раньше.
Если вы привыкли программировать на чем-то вроде Python или Javascript, все это может показаться вам немного чуждым. Я надеюсь, что к концу вы убедитесь, что эта дополнительная информация полезна и не так часто встречается в языках с динамической типизацией.
Если вы привыкли к C++, C или другим системным языкам, надеюсь, все они должны показаться вам очень знакомыми, несмотря на различия в синтаксисе. В идеале к концу статьи вы будете больше думать о своих сигнатурах функций по мере их написания!
Шаги малыша
Ваше первое определение функции в Rust почти наверняка выглядит так:
fn main() {}
Итак, поскольку вы, скорее всего, уже написали это, давайте начнем здесь!
- fn: это синтаксис, который сообщает Rust, что мы объявляем функцию.
- main: это имя функции. main является особенным, потому что это то, что программа вызывает при сборке и запуске как двоичный файл. Имена функций всегда snake_case, а не camelCase. -(): список аргументов. В этом случае main не принимает аргументов.
- {}: разделители внутри функции. В данном случае он пуст.
Итак, что бы мы написали для функции, которая не делает ничего полезного?
fn do_nothing_useful() {}
Отлично, теперь и ты ничего полезного сделать не можешь!
Видимость
По умолчанию все функции являются частными и не могут использоваться вне модуля, в котором они находятся. Сделать их доступными для другого модуля очень просто.
mod dog {
fn private_function() {}
pub fn public_function() {}
}
// Optional to avoid `foo::`
use dog::public_function;
fn main() {
dog::public_function();
// With `use`
public_function();
}
Как и изменчивость, Rust консервативен в своих предположениях о таких вещах, как видимость. Если вы попытаетесь использовать частную функцию, компилятор сообщит вам об этом и поможет указать, где вам нужно сделать функцию общедоступной.
Если в вашем проекте есть такая функция, как foo::bar::baz::rad(), и вы хотите, чтобы ее можно было использовать как foo::rad(), добавьте pub use bar::baz::rad; в ваш модуль foo. Это называется реэкспортом.
Простые параметры
Вы больше не довольны do_nothing_useful() и решили завести собаку. Повезло тебе! Теперь у вас есть новая проблема, вы должны пройти ее и поиграть!
fn walk_dog(dog_name: String) {}
fn play_with(dog_name: String, game_name: String) {}
Параметры объявляются имя переменной: Тип и разделяются запятыми. Но давай! Наша собака - это намного больше, чем просто веревка! Хорошие новости, вы также можете использовать свои собственные шрифты.
struct Dog; // Let's not go overboard.
struct Game; // Simple types in demos!
fn walk_dog(dog: Dog) {}
fn play_with(dog: Dog, game: Game) {}
Отлично, уже выгляжу лучше. Давайте начнем этот замечательный день.
fn main() {
let rover = Dog;
walk_dog(rover);
let fetch = Game;
play_with(rover, fetch); // Compiler Error!
}
ВОУ ВОУ! Это отличный день, когда компилятор нас полностью разваливает! Роверу будет очень грустно.
Посмотрим на ошибку:
<anon>:11:15: 11:20 error: use of moved value: `rover`
<anon>:11 play_with(rover, fetch);
^~~~~
<anon>:9:14: 9:19 note: `rover` moved here because it has type `Dog`, which is non-copyable
<anon>:9 walk_dog(rover);
^~~~~
Здесь компилятор сообщает нам, что ровер был перемещен, когда мы передали его в walk_dog(). Это потому, что fn walk_dog (dog: Dog) {} принимает значение Dog, а мы не сообщили компилятору, что они копируемы! Значения с копией неявно копируются при передаче в функции. Вы можете сделать что-нибудь Копировать, добавив #[derive(Copy)] над объявлением.
Мы собираемся сделать так, чтобы Dog не копировался, потому что, черт возьми, вы не можете копировать собак. Так как же это исправить?
Мы могли бы клонировать вездеход. Но наша структура Dog тоже не клон! Клонирование означает, что мы можем явно сделать копию объекта. Вы можете сделать что-нибудь клонировать точно так же, как вы делали это как копию. Чтобы клонировать нашу собаку, вы можете сделать rover.clone()
Но на самом деле ни одно из этих возможных решений не решило настоящую проблему: мы хотим гулять и играть с одной и той же собакой!
Заимствование
Могу я одолжить твою собаку?
Вместо того, чтобы перемещать нашу собаку в функцию walk_dog(), мы просто хотим передать эту функцию нашей собаке. Когда вы выгуливаете собаку, она (как правило) возвращается с вами в дом, верно?
В Rust символ & используется для обозначения заимствования. Заимствование чего-либо сообщает компилятору, что, когда функция завершена, право собственности на значение возвращается обратно вызывающей стороне.
fn walk_dog(dog: &Dog) {}
fn play_with(dog: &Dog, game: Game) {}
Есть неизменные заимствования, а также изменяемые заимствования (&mut). Вы можете передать неизменяемое заимствование любому количеству объектов одновременно, а изменяемое заимствование - только одному объекту за раз. Это обеспечивает безопасность данных.
Так что наши новые функции заимствования на самом деле не сокращают, не так ли? Мы даже не можем мутировать Собаку! В любом случае попробуем увидеть сообщение об ошибке.
struct Dog {
walked: bool
}
fn walk_dog(dog: &Dog) {
dog.walked = true;
}
fn main() {
let rover = Dog { walked: false };
walk_dog(&rover);
assert_eq!(rover.walked, true);
}
Как мы и ожидали:
<anon>:6:5: 6:22 error: cannot assign to immutable field `dog.walked`
<anon>:6 dog.walked = true;
^~~~~~~~~~~~~~~~~
error: aborting due to previous error
Изменив сигнатуру функции на fn walk_dog (dog: &mut Dog) {} и обновив наш main(), мы можем решить эту проблему.
fn main() {
let mut rover = Dog { walked: false };
walk_dog(&mut rover);
assert_eq!(rover.walked, true);
}
Как видите, сигнатура функции сообщает программисту, является ли значение изменяемым и используется ли значение или используется ли ссылка на него.
Возвращение
Давайте вернемся к тому, как именно мы получаем Rover, потому что именно так мы можем исследовать возвращаемые типы! Допустим, нам нужна функция accept_dog(), которая принимает имя и дает нам Dog.
struct Dog {
name: String,
walked: bool,
}
fn adopt_dog(name: String) -> Dog {
Dog { name: name, walked: false }
}
fn main() {
let rover = adopt_dog(String::from("Rover"));
assert_eq!(rover.name, "Rover");
}
Таким образом, часть -> Dog в сигнатуре функции сообщает нам, что функция возвращает Dog. Обратите внимание, что имя перенесено и дано собаке, а не скопировано или клонировано.
Внутренние трэйты
Если вы реализуете функции в трейте, у вас также есть доступ к следующим двум инструментам:
- Тип возврата Self, который представляет текущий тип.
- Параметр self, который определяет заимствование / перемещение / изменчивость экземпляра структуры. В walk() ниже мы берем изменяемое заимствование, голое «я» перемещает значение.
Пример:
// ... `Dog` struct from before.
impl Dog {
pub fn adopt(name: String) -> Self {
Dog { name: name, walked: false }
}
pub fn walk(&mut self) {
self.walked = true
}
}
fn main() {
let mut rover = Dog::adopt(String::from("Rover"));
assert_eq!(rover.name, "Rover");
rover.walk();
assert_eq!(rover.walked, true);
}
Дженерики
Посмотрим правде в глаза, собак разных пород очень много! Но, тем более, видов животных очень много! По некоторым из них мы, возможно, тоже захотим прогуляться, например, наш Медведь.
Дженерики позволяют нам это делать. У нас может быть структура Dog и Bear, реализующая трэйту Walk, а затем функция walk_pet() принимает любую структуру с характерной чертой Walk!
Обобщения указываются для функций между именем и параметрами в квадратных скобках. В отношении универсальных шаблонов важно отметить, что когда вы принимаете универсальный шаблон, вы можете использовать только функции из ограничений. Это означает, что если вы передадите Read функции, которая хочет Write, она все равно не сможет Read в ней, если ограничения не включают ее.
struct Dog { walked: bool, }
struct Bear { walked: bool, }
trait Walk {
fn walk(&mut self);
}
impl Walk for Dog {
fn walk(&mut self) {
self.walked = true
}
}
impl Walk for Bear {
fn walk(&mut self) {
self.walked = true
}
}
fn walk_pet<W: Walk>(pet: &mut W) {
// Try setting `pet.walked` here!
// You can't!
pet.walk();
}
fn walk_pet_2(pet: &mut Walk) {
// Try setting `pet.walked` here!
// You can't!
pet.walk();
}
fn main() {
let mut rover = Dog { walked: false, };
walk_pet(&mut rover);
assert_eq!(rover.walked, true);
}
Вы также можете использовать другой синтаксис where, поскольку сигнатуры функций со сложными универсальными шаблонами могут быть довольно длинными.
fn walk_pet<W>(pet: &mut W)
where W: Walk {
pet.walk();
}
Если у вас несколько универсальных шаблонов, вы можете разделить их запятыми в обоих случаях. Если вам нужно более одного ограничения трэйты, вы можете использовать where W: Walk + Read или <W: Walk + Read>.
fn stuff<R, W>(r: &R, w: &mut W)
where W: Write, R: Read + Clone {}
Посмотрите на всю информацию, которую вы можете извлечь из этой сигнатуры функции! Это не очень полезное название, но вы все равно можете почти наверняка сказать, что он делает!
Есть еще такие сумасшедшие штуки, которые называются связанными типами, которые используются в таких вещах, как Iterator. Когда вы пишете в подписи, вы хотите использовать что-то вроде Iterator <Item = Dog>, чтобы сказать итератор Dogs.
Передача функций
Иногда желательно передавать функции другим функциям. В Rust довольно просто принять функцию в качестве аргумента. У функций есть трэйты, и они передаются как универсальные!
Вы обязательно должны использовать здесь синтаксис where.
struct Dog {
walked: bool
}
fn do_with<F>(dog: &mut Dog, action: F)
where F: Fn(&mut Dog) {
action(dog);
}
fn walk(dog: &mut Dog) {
dog.walked = true;
}
fn main() {
let mut rover = Dog { walked: false, };
// Fn
do_with(&mut rover, walk);
// Closure
do_with(&mut rover, |dog| dog.walked = true);
}
Функции в Rust реализуют трейты, которые определяют, куда (и как) они передаются:
- FnOnce - принимает счетчик по стоимости.
- FnMut - принимает изменяемый приемник.
- Fn - берет неизменный приемник.
Конкретный ответ Stack Overflow очень хорошо суммирует различия:
Замыкания |...| ... автоматически реализует как можно больше из них.
- Все замыкания реализуют FnOnce: замыкание, которое нельзя вызвать один раз, не заслуживает названия. Обратите внимание: если замыкание реализует только FnOnce, его можно вызвать только один раз.
- Замыкания, которые не выходят за пределы своих захватов, реализуют FnMut, что позволяет им вызываться более одного раза (если есть неограниченный доступ к объекту функции).
- Замыкания, которые не нуждаются в уникальном / изменяемом доступе к своим захватам, реализуют Fn, что позволяет их вызывать практически повсюду.
По сути, разница между разными типами заключается в том, как они взаимодействуют со своей средой. По моему опыту, вам действительно нужно беспокоиться только о различиях для замыканий, которые могут захватывать переменные в области видимости (в нашем примере выше, это функция main()).
Но не бойтесь! Сообщения компилятора, когда один тип предоставляется, когда нужен другой, очень полезны!
Время жизни
Итак, вы, вероятно, сейчас довольно хорошо себя чувствуете. Я имею в виду, посмотрите на эту полосу прокрутки, она почти до конца страницы! Вы быстро станете мастером подписи функций Rust!
Давайте закончим небольшим разговором о жизнях, потому что вы в конечном итоге столкнетесь с ними и, вероятно, сильно запутаетесь.
Позвольте мне быть честным с вами здесь. Для меня жизнь - это тайное искусство. Я использовал их немного назад в 0.7-0.10, и с тех пор мне действительно не приходилось их использовать. Если вы хоть что-нибудь знаете о них, вы гораздо более квалифицированы для написания этого раздела, чем я.
В Modern Rust есть действительно надежный и эффективный пожизненный эллипс, который устраняет подавляющее большинство жизненных упражнений, которые нам раньше приходилось беспокоить. Но когда вы это сделаете, все может начать распутываться.
Итак, если вы начинаете иметь дело с большим количеством жизней, вашим первым шагом действительно должно быть сесть и подумать об этом. Если ваш код не является достаточно сложным, вполне вероятно, что вам не придется иметь дело со сроками жизни. Если вы на простом примере сталкиваетесь с жизнями, ваше представление о проблеме, вероятно, неверно.
Вот функция со сроками жизни из реализации Option.
as_slice<'a>(&'a self) -> &'a [T]
Время жизни обозначается галочкой (') и дается имя. В этом случае «а», но они также могут быть чем-то вроде «буррито», если вы предпочитаете шутки внутри. По сути, это говорит о следующем:
Время жизни вызываемого Option
такое же, как время жизни возвращенного [T]
Здорово! Я действительно не могу больше писать о жизнях, но если у вас есть что добавить, дайте мне знать, и я буду вам доверять.
Время испытания
Ниже вы найдете набор функций, взятых из стандартной библиотеки, вместе со ссылками на их документацию. Можете ли вы сказать по их функциональной сигнатуре, что они делают? (Для большего удовольствия я удалил имя функции!)
// In `File`
fn name<P: AsRef<Path>>(path: P) -> Result<File>
Source
// In `Option<T>`
fn name<E, T>(self, err: E) -> Result<T, E>
Source
// In `Iterator<Item=T>`
fn name<B: FromIterator<Self::Item>>(self) -> B
where Self: Sized
Source
// In `Iterator<Item=T>`
fn name<B, F>(self, init: B, f: F) -> B
where Self: Sized, F: FnMut(B, Self::Item) -> B
Source
// In `Result<T,E>`
fn name<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T, F>
Source
Я надеюсь, что все прошло фантастически, я просто был здесь и подбадривал вас!