String vs &str в функциях Rust
Перевод | Автор оригинала: Herman J. Radtke III
Для всех людей, разочарованных необходимостью использовать to_string() для получения программ для компиляции, этот пост для вас. Для тех, кто не совсем понимает, почему в Rust есть два строковых типа String и &str, я надеюсь пролить немного света на этот вопрос.
Функции, принимающие строку
Я хочу обсудить, как создавать интерфейсы, принимающие строки. Я заядлый фанат гипермедиа и одержим созданием простых в использовании интерфейсов. Начнем с метода, который принимает String. Наш поиск подсказывает, что std::string::String - здесь хороший выбор.
fn print_me(msg: String) {
println!("the message is {}", msg);
}
fn main() {
let msg = "hello world";
print_me(msg);
}
Это дает ошибку компилятора:
expected `collections::string::String`,
found `&'static str`
Таким образом, строковый литерал имеет тип &str и несовместим с типом String. Мы можем изменить тип сообщения на String и успешно скомпилировать: let message = "hello world" .to_string() ;. Это работает, но аналогично использованию clone() для обхода ошибок владения / заимствования. Вот три причины изменить print_me, чтобы вместо этого принимать &str:
- Символ & является ссылочным типом и означает, что мы заимствуем переменную. Когда print_me завершит работу с переменной, право собственности вернется к первоначальному владельцу. Если у нас нет веской причины передать владение переменной сообщения нашей функции, мы должны выбрать заимствование.
- Использование ссылки более эффективно. Использование String для сообщения означает, что программа должна скопировать значение. При использовании ссылки, такой как &str, копирование не производится.
- Тип String можно волшебным образом превратить в тип &str, используя свойство Deref и приведение типа. Это станет более понятным на примере.
Пример принуждения Deref
В этом примере строки создаются четырьмя различными способами, которые работают с функцией print_me. Ключом к тому, чтобы все это работало, является передача значений по ссылке. Вместо того, чтобы передавать own_string как String в print_me, мы передаем его как &string. Когда компилятор видит, что &string передается функции, которая принимает &str, он переводит &string в &str. Такое же принуждение имеет место для строк с подсчетом ссылок и строк с атомарными ссылками. Строковая переменная уже является ссылкой, поэтому нет необходимости использовать & при вызове print_me (string). Зная это, нам больше не нужно, чтобы вызовы .to_string() засоряли наш код.
fn print_me(msg: &str) { println!("msg = {}", msg); }
fn main() {
let string = "hello world";
print_me(string);
let owned_string = "hello world".to_string(); // or String::from_str("hello world")
print_me(&owned_string);
let counted_string = std::rc::Rc::new("hello world".to_string());
print_me(&counted_string);
let atomically_counted_string = std::sync::Arc::new("hello world".to_string());
print_me(&atomically_counted_string);
}
Вы также можете использовать приведение Deref с другими типами, такими как Vector. В конце концов, String - это просто вектор из 8-байтовых символов. Подробнее о приведении Deref читайте в книге Rust lang.
Введение в структуру
На этом этапе у нас не должно быть посторонних вызовов to_string() для наших функций. Однако мы сталкиваемся с некоторыми проблемами, когда пытаемся ввести структуру. Используя то, что мы только что узнали, мы могли бы создать такую структуру:
struct Person {
name: &str,
}
fn main() {
let _person = Person { name: "Herman" };
}
Получаем ошибку:
<anon>:2:11: 2:15 error: missing lifetime specifier [E0106]
<anon>:2 name: &str,
Rust пытается сделать так, чтобы Person не пережил ссылку на name. Если Person все-таки удалось пережить имя, мы рискуем вывести нашу программу из строя. Вся суть Rust в том, чтобы предотвратить это. Итак, давайте начнем пытаться скомпилировать этот код. Нам нужно указать время жизни или область действия, чтобы Rust мог нас обезопасить. Стандартный спецификатор срока службы - a. Я не знаю, почему это было выбрано, но давайте продолжим.
struct Person {
name: &'a str,
}
fn main() {
let _person = Person { name: "Herman" };
}
Скомпилируйте снова, и мы получим еще одну ошибку:
<anon>:2:12: 2:14 error: use of undeclared lifetime name `'a` [E0261]
<anon>:2 name: &'a str,
Давай подумаем об этом. Мы знаем, что хотим намекнуть компилятору Rust, что наша структура Person не должна пережить имя. Итак, нам нужно позаботиться о нашей жизни в структуре Person. Некоторый поиск укажет нам на <'a> - это синтаксис для объявления времени жизни.
struct Person<'a> {
name: &'a str,
}
fn main() {
let _person = Person { name: "Herman" };
}
Это компилируется! Однако мы обычно реализуем методы в наших структурах. Давайте добавим функцию приветствия в наш класс Person.
struct Person<'a> {
name: &'a str,
}
impl Person {
fn greet(&self) {
println!("Hello, my name is {}", self.name);
}
}
fn main() {
let person = Person { name: "Herman" };
person.greet();
}
Теперь мы получаем ошибку:
<anon>:5:6: 5:12 error: wrong number of lifetime parameters: expected 1, found 0 [E0107]
<anon>:5 impl Person {
Наша структура Person имеет параметр времени жизни, поэтому он должен быть и в нашей реализации. Давайте объявим нашу жизнь реализацией Person, например impl Person <'a> {. К сожалению, это дает нам сбивающую с толку ошибку при компиляции:
<anon>:5:13: 5:15 error: use of undeclared lifetime name `'a` [E0261]
<anon>:5 impl Person<'a> {
Чтобы мы могли объявить время жизни, нам нужно указать время жизни сразу после impl, например impl <'a> Person {. Снова компилируем и получаем ошибку:
<anon>:5:10: 5:16 error: wrong number of lifetime parameters: expected 1, found 0 [E0107]
<anon>:5 impl<'a> Person {
Теперь мы вернулись в нужное русло. Давайте вернем наш параметр времени жизни обратно в реализацию Person, например impl <'a> Person <' a> {. Теперь наша программа компилируется. Вот полный рабочий код:
struct Person<'a> {
name: &'a str,
}
impl<'a> Person<'a> {
fn greet(&self) {
println!("Hello, my name is {}", self.name);
}
}
fn main() {
let person = Person { name: "Herman" };
person.greet();
}
Строка или &str в структуре
Теперь вопрос в том, использовать ли в вашей структуре String или &str. Другими словами, когда мы должны использовать ссылку на другой тип в структуре? Мы должны использовать ссылку, если нашей структуре не требуется владение переменной. Эта концепция может быть немного расплывчатой, но есть несколько правил, которые я использую, чтобы получить ответ.
- Нужно ли мне использовать переменную вне моей структуры? Вот надуманный пример:
struct Person {
name: String,
}
impl Person {
fn greet(&self) {
println!("Hello, my name is {}", self.name);
}
}
fn main() {
let name = String::from_str("Herman");
let person = Person { name: name };
person.greet();
println!("My name is {}", name); // move error
}
Я должен использовать здесь ссылку, так как мне нужно будет использовать переменную позже. Вот реальный пример в rustc_serialize. Структуре Encoder не нужно владеть записывающей переменной, которая реализует std::fmt::Write, просто используйте (заимствуйте) на некоторое время. Фактически String реализует Write. В этом примере с использованием функции кодирования переменная типа String передается в кодировщик, а затем возвращается вызывающей стороне кодирования.
- Мой шрифт большой? Если тип большой, то передача его по ссылке сэкономит ненужное использование памяти. Помните, что передача по ссылке не вызывает копии переменной. Рассмотрим строковый буфер, содержащий большой объем данных. Копирование всего этого приведет к тому, что программа будет работать намного медленнее.
Теперь мы должны иметь возможность создавать функции, которые принимают строки, независимо от того, являются ли они &str, String или подсчитываются ссылки на событие. Мы также можем создавать структуры, которые могут иметь переменные, являющиеся ссылками. Время жизни структуры связано с этими ссылочными переменными, чтобы гарантировать, что структура не переживет указанную переменную и не приведет к плохим вещам в нашей программе. У нас также есть начальное понимание того, должны ли переменные в нашей структуре быть типами или ссылками на типы.
А как насчет "статического"
Случайным образом, но я подумал, что стоит упомянуть. Мы можем использовать статическое время жизни для компиляции нашего исходного примера, но я предостерегаю от этого:
struct Person {
name: &'static str,
}
impl Person {
fn greet(&self) {
println!("Hello, my name is {}", self.name);
}
}
fn main() {
let person = Person { name: "Herman" };
person.greet();
}
Статическое время жизни действительно для всей программы. Возможно, вам не понадобится Человек или имя, чтобы прожить так долго.
Связанный
- Создание функции Rust, которая принимает String или &str