Konwersja z Option <String> do Option <& str>

85

Bardzo często otrzymałem Option<String>z obliczeń i chciałbym użyć tej wartości lub domyślnej wartości zakodowanej na stałe.

Byłoby to trywialne w przypadku liczby całkowitej:

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

Ale z a Stringi a &strkompilator narzeka na niezgodne typy:

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

Dokładny błąd tutaj to:

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`

Jedną z opcji jest przekonwertowanie kawałka łańcucha na posiadany ciąg, jak sugeruje rustc:

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

Ale to powoduje alokację, która jest niepożądana, gdy chcę natychmiast przekonwertować wynik z powrotem na kawałek ciągu, jak w tym wywołaniu Regex::new():

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

Wolałbym zamienić na Option<String>an, Option<&str>aby uniknąć tego przydziału.

Jaki jest idomatyczny sposób, aby to napisać?

George Hilliard
źródło

Odpowiedzi:

66

Od wersji Rust 1.40 standardowa biblioteka musi Option::as_derefto robić:

fn main() {
    let opt: Option<String> = Some("some value".to_owned());
    let value = opt.as_deref().unwrap_or("default string");
}
Shepmaster
źródło
To działa dla mnie, ale nie mogę zrozumieć, dlaczego moja druga wersja mapnie działała. Nie rozumiem, co dzieje się z pierwszym Options<String>i kopią, która się do niego odwołuje w przypadku as_derefwariantu kodu. Oto mój działający kod przy użyciu as_deref: let device_id = UsbDeviceIdentifier::VidPidSn { vid: device.vendor_id, pid: device.product_id, sn: device.serial_number.as_deref().unwrap_or("") };i moja pierwsza próba let device_id = UsbDeviceIdentifier::VidPidSn { vid: device.vendor_id, pid: device.product_id, sn: device.serial_number.map(|s| s.as_str()).unwrap_or("") };.
Paul-Sebastian Manole
Mój błąd jest error[E0515]: cannot return value referencing function parameter `s`, s.as_str() returns a value referencing data owned by the current function. OK, ale dlaczego to as_derefdziała, ponieważ nadal tworzy odniesienie do oryginału Option<String>zwróconego przez .serial_number, więc nadal wskazuje na dane, które są jego własnością Option!? Czy różnica as_derefpolega na tym, że transformacja jest wykonywana w miejscu, a nie w ciele zamknięcia, gdzie odrzuca źródło wycinka, który zwraca? Jeśli tak, czy istnieje sposób, aby to naprawić? Jak zwrócić kopię str?
Paul-Sebastian Manole
@ Paul-SebastianManole proszę przeczytać istniejącą odpowiedź, która pokazuje, jak używaćmap ; czy to rozwiązuje twój problem?
Shepmaster
Tak, ale nadal jestem zdziwiony.
Paul-Sebastian Manole,
@ Paul-SebastianManole a co powiesz na wynik opcji :: mapa nie żyje wystarczająco długo
Shepmaster
55

Możesz użyć as_ref()i map()przekształcić Option<String>plik 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");
}

Po pierwsze, as_ref()niejawnie przyjmuje odniesienie opt, dając &Option<String>(ponieważ as_ref()bierze &self, czyli otrzymuje odniesienie) i zamienia je w Option<&String>. Następnie używamy mapdo konwersji go na plik Option<&str>. Oto, co &**xrobi: prawy *(który jest oceniany jako pierwszy) po prostu usuwa odwołanie &String, dając Stringlwartość. Następnie lewy *faktycznie wywołuje Derefcechę, ponieważ String implementuje Deref<Target=str> , dając nam strlwartość. Na koniec &przyjmuje adres strlwartości, dając nam &str.

Możesz to nieco uprościć, używając map_ordo łączenia mapi unwrap_orw jednej operacji:

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

Jeśli &**xwydaje Ci się to zbyt magiczne, możesz String::as_strzamiast tego napisać :

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

lub String::as_ref(z AsRefcechy, która jest w preludium ):

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

lub String::deref(chociaż musisz też zaimportować Derefcechę):

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);
}

Aby którykolwiek z nich działał, musisz zachować właściciela Option<String>tak długo, jak długo musi pozostać dostępny Option<&str>lub rozpakowany &str. Jeśli to zbyt skomplikowane, możesz użyć 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));
}
Francis Gagné
źródło
13
komentarz bikesheddy: zamiast map(|x| &**x)ciebie też możesz zrobić map(String::as_ref).
fjh,
2
Działa to ładnie, choć wciąż jest trochę rozwlekłe. Aby wyjaśnić, typ opt.as_ref()jest Option<&String>, a następnie opt.as_ref().map(String::as_ref)przekazuje to &Stringdo String::as_ref, co zwraca &str. Dlaczego nie &Stringod Option::as_refbyć zmuszany do &str?
George Hilliard,
1
@thirtythreeforty String::as_refzwraca an &str, not a &&String(patrz tutaj )
fjh
@fjh Właśnie to sobie uświadomiłem i zredagowałem :). Moje pytanie o przymus pozostaje.
George Hilliard,
1
@ little-dude: W &**, lewy *faktycznie wywołuje Derefcechę, ponieważ Stringimplementuje Deref<Target=str>. Deref::derefi AsRef::as_refoba zapewniają konwersje odniesienia do odniesienia, i zdarza się, że konwersja z &Stringna &strjest dostępna w obu. Możesz użyć map(String::deref)zamiast map(String::as_ref), byłoby to również równoważne.
Francis Gagné
20

Lepszym sposobem byłoby zaimplementowanie tego ogólnego w przypadku 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)
    }
}

co skutecznie uogólnia as_ref.

Veedrac
źródło
1
To świetna funkcja użytkowa. Chciałbym, żeby był częścią standardowej biblioteki jako funkcja w Option <T>!
Bill Fraser
1
Dzięki! To działa dla mnie - jeśli dołączę ten kod, opt.as_deref()to rzeczywiście jest Option<&str>.
rspeer
7

Chociaż kocham odpowiedź Veedrac za (użyłem go), jeśli jest to potrzebne w jednym punkcie i chcesz coś, co jest ekspresyjny można użyć as_ref(), mapi String::as_strłańcuch:

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

assert_eq!(Some("some value"), opt.as_ref().map(String::as_str));
Michele d'Amico
źródło
1

Oto jeden sposób, w jaki możesz to zrobić. Należy pamiętać, że muszą zachować oryginalne Stringwokół, inaczej co miałby &strbyć plasterek na?

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

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

kojec dla dziecka

Jorge Israel Peña
źródło
2
To nie zmieniło go w Option <& str>.
rspeer