Как построить гибкую систему данных нескольких типов в Rust без клонирования строк? - PullRequest
0 голосов
/ 11 сентября 2018

Я хочу построить систему, в которой данные разных типов (i32, String, ...) передаются между функциями, которые модифицируют данные.Например, я хочу иметь функцию add, которая получает «некоторые» данные и добавляет их.

Функция add получает что-то типа Value, и если Value является i32, он добавляет два i32 значения, если он имеет тип String, он возвращает строку, которая объединяет обе строки.

Я знаю, что это было бы почти идеально для шаблонного программирования (или что бы то ни быловызывается в Rust, я из C ++), но в моем случае я хочу иметь небольшие блоки кода, которые обрабатывают вещи.

Например, с f64 и String, используя Floatи Text как имена, у меня есть:

pub struct Float {
    pub min: f64,
    pub max: f64,
    pub value: f64,
}

pub struct Text {
    pub value: String,
}

pub enum Value {
    Float(Float),
    Text(Text),
}

Теперь я хочу реализовать функцию, которая получает значение, которое должно быть строкой, и что-то с ним делает, поэтому я реализую to_string() метод для Value:

impl std::string::ToString for Value {
    fn to_string(&self) -> String {
        match self {
            Value::Float(f) => format!("{}", f.value).to_string(),
            Value::Text(t) => t.value.clone(),
        }
    }
}

Теперь функция будет делать что-то вроде:

fn do_something(value: Value) -> Value {
    let s = value.to_string();
    // do something with s, which probably leads to creating a new string

    let new_value = Text(new_string);
    Value::Text(new_value)
}

В случае Value::Float это создаст новый String,затем новый String с результатом и его возвращением, но в случае Value::Text это клонирует строку, причемch - ненужный шаг, а затем создайте новый.

Есть ли способ, при котором реализация to_string() может создать новый String для Value::Float, но вернуть ссылку Value::Textзначение?

1 Ответ

0 голосов
/ 12 сентября 2018

«Стандартный» способ справиться с возможностью либо String, либо &str - это использовать Cow<str>. COW означает клонирование при записи (или copy -on-write), и вы можете использовать его для других типов, кроме строк. Cow позволяет вам хранить либо ссылку, либо собственное значение, и клонировать ссылку только в собственное значение, когда вам нужно изменить его.

Есть несколько способов применить это к вашему коду:

  1. Вы можете просто добавить реализацию Into<Cow<str>>, а остальные оставить прежними.
  2. Измените ваши типы так, чтобы они содержали Cow<str> с, чтобы объекты Text могли содержать принадлежащие String или &str.

Первый вариант самый простой. Вы можете просто реализовать эту черту. Обратите внимание, что Into::into принимает self, поэтому вам необходимо реализовать это для &Value, а не Value, в противном случае заимствованные значения будут ссылаться на собственные значения, которые были использованы into и уже недействительны.

impl<'a> Into<Cow<'a, str>> for &'a Value {
    fn into(self) -> Cow<'a, str> {
        match self {
            Value::Float(f) => Cow::from(format!("{}", f.value).to_string()),
            Value::Text(t) => Cow::from(&t.value),
        }
    }
}

Реализация этого для &'a Value позволяет нам связать время жизни в Cow<'a, str> с источником данных. Это было бы невозможно, если бы мы реализовали только для Value, что хорошо, потому что данные исчезли бы!


Еще лучшим решением может быть использование Cow в вашем перечислении Text:

use std::borrow::Cow;

pub struct Text<'a> {
    pub value: Cow<'a, str>,
}

Это позволит вам взять взаймы &str:

let string = String::From("hello");

// same as Cow::Borrowed(&string)
let text = Text { value: Cow::from(&string) };

или String:

// same as Cow::Owned(string)
let text = Text { value: Cow::from(string) };

Поскольку Value теперь может косвенно хранить ссылку, ему потребуется собственный параметр времени жизни:

pub enum Value<'a> {
    Float(Float),
    Text(Text<'a>),
}

Теперь реализация Into<Cow<str>> может быть для самого Value, поскольку ссылочные значения могут быть перемещены:

impl<'a> Into<Cow<'a, str>> for Value<'a> {
    fn into(self) -> Cow<'a, str> {
        match self {
            Value::Float(f) => Cow::from(format!("{}", f.value).to_string()),
            Value::Text(t) => t.value,
        }
    }
}

Так же, как String, Cow<str> удовлетворяет Deref<Target = str>, так что его можно использовать везде, где ожидается &str, просто передавая ссылку. Это еще одна причина , почему вы всегда должны пытаться принять &str в аргументе функции , а не String или &String.


Как правило, вы можете использовать Cow с так же удобно, как String с, потому что они имеют много одинаковых impl с. Например:

let input = String::from("12.0");
{
    // This one is borrowed (same as Cow::Borrowed(&input))
    let text = Cow::from(&input);
}
// This one is owned (same as Cow::Owned(input))
let text = Cow::from(input);

// Most of the usual String/&str trait implementations are also there for Cow
let num: f64 = text.parse().unwrap();
...