Перетворення з Option <String> на Option <& str>


85

Дуже часто я отримую значення Option<String>з розрахунку, і я хотів би використати це значення або стандартне значення за замовчуванням.

Це було б тривіально з цілим числом:

let opt: Option<i32> = Some(3);
let value = opt.unwrap_or(0); // 0 being the default

Але за допомогою a Stringта a &strкомпілятор скаржиться на невідповідні типи:

let opt: Option<String> = Some("some value".to_owned());
let value = opt.unwrap_or("default string");

Точна помилка:

error[E0308]: mismatched types
 --> src/main.rs:4:31
  |
4 |     let value = opt.unwrap_or("default string");
  |                               ^^^^^^^^^^^^^^^^
  |                               |
  |                               expected struct `std::string::String`, found reference
  |                               help: try using a conversion method: `"default string".to_string()`
  |
  = note: expected type `std::string::String`
             found type `&'static str`

Одним із варіантів є перетворення фрагмента рядка у рядок, що належить, як запропоновано rustc:

let value = opt.unwrap_or("default string".to_string());

Але це спричиняє розподіл, що небажано, коли я хочу негайно перетворити результат назад на фрагмент рядка, як у цьому виклику до Regex::new():

let rx: Regex = Regex::new(&opt.unwrap_or("default string".to_string()));

Я б скоріше перетворив Option<String>на an, Option<&str>щоб уникнути такого розподілу.

Який ідоматичний спосіб це написати?

Відповіді:


66

Починаючи з Rust 1.40, стандартна бібліотека Option::as_derefповинна робити це:

fn main() {
    let opt: Option<String> = Some("some value".to_owned());
    let value = opt.as_deref().unwrap_or("default string");
}

Це працює для мене, але я не можу зрозуміти, чому моя інша версія mapне працює. Я не розумію, що відбувається з першим Options<String>та копією, яка посилається на нього, у випадку as_derefваріанту коду. Ось мій робочий код за допомогою as_deref: let device_id = UsbDeviceIdentifier::VidPidSn { vid: device.vendor_id, pid: device.product_id, sn: device.serial_number.as_deref().unwrap_or("") };і моя перша спроба let device_id = UsbDeviceIdentifier::VidPidSn { vid: device.vendor_id, pid: device.product_id, sn: device.serial_number.map(|s| s.as_str()).unwrap_or("") };.
Пол-Себастьян Маноле,

Моя помилка error[E0515]: cannot return value referencing function parameter `s`, s.as_str() returns a value referencing data owned by the current function. Добре, але чому це as_derefпрацює, адже він все одно створює посилання на оригінал, Option<String>повернутий .serial_number, так що все одно вказує на дані, якими володіє Option!!? Чи різниця as_derefполягає в тому, що відбувається трансформація, а не в тілі закриття, де воно відкидає джерело зрізу, який повертає? Якщо це так, чи є спосіб це виправити? Як повернути копію str?
Пол-Себастьян Маноле,

@ Paul-SebastianManole, будь ласка, прочитайте існуючу відповідь, яка показує, як користуватисяmap ; це вирішує вашу проблему?
Шепмастер

Так, але я все ще спантеличений.
Пол-Себастьян Маноле,


55

Ви можете використовувати as_ref()та map()перетворити файл Option<String>на Option<&str>.

fn main() {
    let opt: Option<String> = Some("some value".to_owned());
    let value = opt.as_ref().map(|x| &**x).unwrap_or("default string");
}

По-перше, as_ref()неявно приймає посилання на opt, даючи знак &Option<String>(оскільки as_ref()бере &self, тобто отримує посилання), і перетворює його в файл Option<&String>. Потім ми використовуємо mapдля перетворення його в Option<&str>. Ось що &**xробиться: крайній правий *(який оцінюється першим) просто переорієнтовує &String, надаючи значення Stringl. Потім крайній лівий *насправді викликає Derefознаку, оскільки String реалізує Deref<Target=str> , даючи нам strзначення. Нарешті, &приймає адресу strlvalue, даючи нам a &str.

Ви можете ще трохи спростити це, використовуючи map_orкомбінування mapта unwrap_orза одну операцію:

fn main() {
    let opt: Option<String> = Some("some value".to_owned());
    let value = opt.as_ref().map_or("default string", |x| &**x);
}

Якщо вам &**xздається занадто чарівним, String::as_strзамість цього ви можете написати :

fn main() {
    let opt: Option<String> = Some("some value".to_owned());
    let value = opt.as_ref().map_or("default string", String::as_str);
}

або String::as_refAsRefознаки, яка знаходиться в прелюдії ):

fn main() {
    let opt: Option<String> = Some("some value".to_owned());
    let value = opt.as_ref().map_or("default string", String::as_ref);
}

або String::deref(хоча вам теж потрібно імпортувати Derefознаку):

use std::ops::Deref;

fn main() {
    let opt: Option<String> = Some("some value".to_owned());
    let value = opt.as_ref().map_or("default string", String::deref);
}

Щоб будь-яка з них працювала, вам потрібно утримувати власника до тих Option<String>пір, поки Option<&str>нерозгорнута &strта залишається доступна. Якщо це занадто складно, ви можете використати Cow.

use std::borrow::Cow::{Borrowed, Owned};

fn main() {
    let opt: Option<String> = Some("some value".to_owned());
    let value = opt.map_or(Borrowed("default string"), |x| Owned(x));
}

13
bikesheddy коментар: замість того , щоб map(|x| &**x)ви могли також зробити map(String::as_ref).
fjh

2
Це працює добре, хоча все ще трохи багатослівно. Для того, щоб уточнити, тип opt.as_ref()IS Option<&String>, а потім opt.as_ref().map(String::as_ref)передає це &Stringдо String::as_ref, який повертає &str. Чому не може &Stringвід Option::as_refбути примушений до &str?
Джордж Хілліард,

1
@thirtythreeforty String::as_refповертає а &str, а не &&String(див. тут )
fjh

@fjh Я щойно це зрозумів і відредагував :). Моє питання примусу залишається.
Джордж Хілліард

1
@ little-dude: В &**, крайній лівий *насправді викликає Derefознаку, оскільки Stringреалізує Deref<Target=str>. Deref::derefі AsRef::as_refобидва забезпечують перетворення посилання на посилання, і трапляється, що перетворення з &Stringна &strдоступне з обома. Ви можете використовувати map(String::deref)замість map(String::as_ref), це також було б еквівалентно.
Френсіс Ганьє

20

Приємнішим способом може бути загальна реалізація цього для T: Deref:

use std::ops::Deref;

trait OptionDeref<T: Deref> {
    fn as_deref(&self) -> Option<&T::Target>;
}

impl<T: Deref> OptionDeref<T> for Option<T> {
    fn as_deref(&self) -> Option<&T::Target> {
        self.as_ref().map(Deref::deref)
    }
}

який ефективно узагальнює as_ref.


1
Це відмінна функція корисності. Я хотів би, щоб це була частина стандартної бібліотеки як функція на Option <T>!
Білл Фрейзер

1
Дякую! Це працює для мене - якщо я включаю цей код, то opt.as_deref()це справді Option<&str>.
rspeer

7

Хоча мені подобається відповідь Веедрака (я нею скористався), якщо вона вам потрібна лише в один момент, і ви хотіли б щось виразне, що ви можете використати as_ref(), mapта String::as_strланцюжок:

let opt: Option<String> = Some("some value".to_string());

assert_eq!(Some("some value"), opt.as_ref().map(String::as_str));

1

Ось один із способів це зробити. Майте на увазі, що вам потрібно тримати оригінал Stringнавколо, інакше на що це &strбуде скибочка?

let opt = Some(String::from("test")); // kept around

let unwrapped: &str = match opt.as_ref() {
  Some(s) => s, // deref coercion
  None => "default",
};

манеж


2
Це не перетворило його на Option <& str>.
rspeer
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.