Perché non riesco a memorizzare un valore e un riferimento a quel valore nella stessa struttura?


222

Ho un valore e voglio memorizzare quel valore e un riferimento a qualcosa all'interno di quel valore nel mio tipo:

struct Thing {
    count: u32,
}

struct Combined<'a>(Thing, &'a u32);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing { count: 42 };

    Combined(thing, &thing.count)
}

A volte, ho un valore e voglio memorizzare quel valore e un riferimento a quel valore nella stessa struttura:

struct Combined<'a>(Thing, &'a Thing);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing::new();

    Combined(thing, &thing)
}

A volte, non sto nemmeno prendendo un riferimento al valore e ottengo lo stesso errore:

struct Combined<'a>(Parent, Child<'a>);

fn make_combined<'a>() -> Combined<'a> {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}

In ciascuno di questi casi, ricevo un errore che uno dei valori "non vive abbastanza a lungo". Cosa significa questo errore?


1
Per quest'ultimo esempio, una definizione Parente Childpotrebbe aiutare ...
Matthieu M.

1
@MatthieuM. Ne ho discusso, ma ho deciso di non basarmi sulle due domande collegate. Nessuna di queste domande ha esaminato la definizione della struttura o del metodo in questione, quindi ho pensato che sarebbe stato meglio imitare il fatto che a quelle persone è possibile abbinare più facilmente questa domanda alla propria situazione. Si noti che io faccio mostrare la firma del metodo nella risposta.
Shepmaster,

Risposte:


245

Diamo un'occhiata a una semplice implementazione di questo :

struct Parent {
    count: u32,
}

struct Child<'a> {
    parent: &'a Parent,
}

struct Combined<'a> {
    parent: Parent,
    child: Child<'a>,
}

impl<'a> Combined<'a> {
    fn new() -> Self {
        let parent = Parent { count: 42 };
        let child = Child { parent: &parent };

        Combined { parent, child }
    }
}

fn main() {}

Questo fallirà con l'errore:

error[E0515]: cannot return value referencing local variable `parent`
  --> src/main.rs:19:9
   |
17 |         let child = Child { parent: &parent };
   |                                     ------- `parent` is borrowed here
18 | 
19 |         Combined { parent, child }
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function

error[E0505]: cannot move out of `parent` because it is borrowed
  --> src/main.rs:19:20
   |
14 | impl<'a> Combined<'a> {
   |      -- lifetime `'a` defined here
...
17 |         let child = Child { parent: &parent };
   |                                     ------- borrow of `parent` occurs here
18 | 
19 |         Combined { parent, child }
   |         -----------^^^^^^---------
   |         |          |
   |         |          move out of `parent` occurs here
   |         returning this value requires that `parent` is borrowed for `'a`

Per comprendere completamente questo errore, è necessario pensare a come i valori sono rappresentati in memoria e cosa succede quando si spostano tali valori. Annotiamo Combined::newcon alcuni ipotetici indirizzi di memoria che mostrano dove si trovano i valori:

let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42 
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000

Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?

Cosa dovrebbe succedere child? Se il valore fosse spostato come parent era, farebbe riferimento alla memoria che non garantisce più di avere un valore valido. A qualsiasi altro pezzo di codice è consentito memorizzare valori all'indirizzo di memoria 0x1000. L'accesso alla memoria presupponendo che fosse un numero intero potrebbe causare arresti anomali e / o bug di sicurezza ed è una delle principali categorie di errori che Rust impedisce.

Questo è esattamente il problema che impedisce la vita . Una durata è un po 'di metadati che consente a te e al compilatore di sapere per quanto tempo un valore sarà valido nella sua posizione di memoria corrente . Questa è una distinzione importante, in quanto è un errore comune che fanno i nuovi arrivati ​​di Rust. La durata della ruggine non è il periodo di tempo che intercorre tra la creazione di un oggetto e la sua distruzione!

Come analogia, pensala in questo modo: durante la vita di una persona, risiederanno in molti luoghi diversi, ognuno con un indirizzo distinto. La vita di Rust riguarda l'indirizzo in cui risiedi attualmente , non quando morirai in futuro (anche se morire cambia anche il tuo indirizzo). Ogni volta che ti sposti è rilevante perché il tuo indirizzo non è più valido.

È anche importante notare che le vite non cambiano il tuo codice; il tuo codice controlla le vite, le tue vite non controllano il codice. Il proverbio dice che "le vite sono descrittive, non prescrittive".

Annotiamo Combined::newcon alcuni numeri di riga che utilizzeremo per evidenziare le vite:

{                                          // 0
    let parent = Parent { count: 42 };     // 1
    let child = Child { parent: &parent }; // 2
                                           // 3
    Combined { parent, child }             // 4
}                                          // 5

La durata concreta di parentva da 1 a 4, compreso (che rappresenterò come [1,4]). La durata concreta di childè [2,4]e la durata concreta del valore di ritorno è [4,5]. È possibile avere vite concrete che iniziano da zero, il che rappresenterebbe la durata di un parametro per una funzione o qualcosa che esisteva al di fuori del blocco.

Si noti che la durata di childse stessa è [2,4], ma che si riferisce a un valore con una durata di [1,4]. Questo va bene fino a quando il valore di riferimento diventa non valido prima del valore di riferimento. Il problema si verifica quando proviamo a tornare childdal blocco. Ciò "prolungherebbe eccessivamente" la vita oltre la sua lunghezza naturale.

Questa nuova conoscenza dovrebbe spiegare i primi due esempi. Il terzo richiede di esaminare l'implementazione di Parent::child. Le probabilità sono, sarà simile a questo:

impl Parent {
    fn child(&self) -> Child { /* ... */ }
}

Questo utilizza l' elisione a vita per evitare di scrivere parametri di durata generici espliciti . È equivalente a:

impl Parent {
    fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}

In entrambi i casi, il metodo dice che Childverrà restituita una struttura che è stata parametrizzata con la durata concreta di self. Detto in altro modo, l' Childistanza contiene un riferimento a Parentciò che l'ha creata e quindi non può vivere più a lungo di tale Parentistanza.

Questo ci consente anche di riconoscere che qualcosa non va nella nostra funzione di creazione:

fn make_combined<'a>() -> Combined<'a> { /* ... */ }

Sebbene tu abbia maggiori probabilità di vederlo scritto in una forma diversa:

impl<'a> Combined<'a> {
    fn new() -> Combined<'a> { /* ... */ }
}

In entrambi i casi, non viene fornito alcun parametro di durata tramite un argomento. Ciò significa che la durata con cui Combinedverrà parametrizzata non è vincolata da nulla: può essere qualunque cosa il chiamante voglia che sia. Questo è senza senso, perché il chiamante potrebbe specificare la 'staticdurata e non c'è modo di soddisfare tale condizione.

Come lo aggiusto?

La soluzione più semplice e consigliata è quella di non tentare di mettere insieme questi elementi nella stessa struttura. In questo modo, l'annidamento della struttura imiterà la durata del codice. Metti insieme i tipi che possiedono i dati in una struttura e quindi fornisci metodi che ti consentano di ottenere riferimenti o oggetti contenenti riferimenti secondo necessità.

C'è un caso speciale in cui il monitoraggio della durata è troppo zelante: quando hai qualcosa inserito nell'heap. Ciò si verifica quando si utilizza a Box<T>, ad esempio. In questo caso, la struttura che viene spostata contiene un puntatore nell'heap. Il valore puntato rimarrà stabile, ma l'indirizzo del puntatore stesso si sposterà. In pratica, non importa, poiché segui sempre il puntatore.

La cassa a noleggio (NON PIÙ MANTENUTA O SUPPORTATA) o la cassa owning_ref sono modi di rappresentare questo caso, ma richiedono che l'indirizzo di base non si sposti mai . Questo esclude i vettori mutanti, che possono causare una riallocazione e uno spostamento dei valori allocati in heap.

Esempi di problemi risolti con il noleggio:

In altri casi, potresti voler passare a qualche tipo di conteggio dei riferimenti, ad esempio utilizzando Rco Arc.

Maggiori informazioni

Dopo essersi spostati parentnella struttura, perché il compilatore non è in grado di ottenere un nuovo riferimento parente assegnarlo alla childstruttura?

Sebbene sia teoricamente possibile farlo, ciò comporterebbe una grande complessità e costi generali. Ogni volta che l'oggetto viene spostato, il compilatore dovrebbe inserire il codice per "correggere" il riferimento. Ciò significherebbe che copiare una struttura non è più un'operazione molto economica che sposta solo alcuni bit. Potrebbe anche significare che codice come questo è costoso, a seconda di quanto sarebbe buono un ipotetico ottimizzatore:

let a = Object::new();
let b = a;
let c = b;

Invece di forzare questo accada per ogni mossa, il programmatore può scegliere quando ciò accadrà creando metodi che prenderanno i riferimenti appropriati solo quando li chiamerai.

Un tipo con un riferimento a se stesso

C'è un caso specifico in cui è possibile creare un tipo con un riferimento a se stesso. Devi usare qualcosa di simile Optionper farlo in due passaggi:

#[derive(Debug)]
struct WhatAboutThis<'a> {
    name: String,
    nickname: Option<&'a str>,
}

fn main() {
    let mut tricky = WhatAboutThis {
        name: "Annabelle".to_string(),
        nickname: None,
    };
    tricky.nickname = Some(&tricky.name[..4]);

    println!("{:?}", tricky);
}

Funziona, in un certo senso, ma il valore creato è fortemente limitato: non può mai essere spostato. In particolare, ciò significa che non può essere restituito da una funzione o trasmesso valore per nulla. Una funzione di costruzione mostra lo stesso problema con le vite precedenti:

fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }

Che dire Pin?

Pin, stabilizzato in Rust 1.33, ha questo nella documentazione del modulo :

Un primo esempio di tale scenario sarebbe la costruzione di strutture autoreferenziali, poiché lo spostamento di un oggetto con puntatori su se stesso li invaliderebbe, il che potrebbe causare un comportamento indefinito.

È importante notare che "autoreferenziale" non significa necessariamente usare un riferimento . In effetti, l' esempio di una struttura autoreferenziale dice specificamente (enfasi sulla mia):

Non possiamo informarne il compilatore con un riferimento normale, poiché questo modello non può essere descritto con le consuete regole di assunzione. Invece , utilizziamo un puntatore non elaborato , anche se è noto che non è nullo, poiché sappiamo che punta alla stringa.

La capacità di utilizzare un puntatore non elaborato per questo comportamento esiste da Rust 1.0. In effetti, possedere-ref e noleggio usano puntatori grezzi sotto il cofano.

L'unica cosa che si Pinaggiunge alla tabella è un modo comune per affermare che è garantito che un determinato valore non si sposti.

Guarda anche:


1
Qualcosa del genere ( is.gd/wl2IAt ) è considerato idiomatico? Vale a dire, per esporre i dati tramite metodi anziché i dati grezzi.
Peter Hall,

2
@PeterHall certo, significa solo che Combinedpossiede ciò Childche possiede il Parent. Ciò può o non ha senso a seconda dei tipi reali che hai. Restituire riferimenti ai tuoi dati interni è abbastanza tipico.
Shepmaster

Qual è la soluzione al problema dell'heap?
Derekdreery,

@derekdreery forse potresti espandere il tuo commento? Perché l'intero paragrafo parla della cassa owning_ref insufficiente?
Shepmaster,

1
@FynnBecker è ancora impossibile memorizzare un riferimento e un valore in quel riferimento. Pinè principalmente un modo per conoscere la sicurezza di una struttura contenente un puntatore autoreferenziale . La capacità di utilizzare un puntatore non elaborato per lo stesso scopo esiste da Rust 1.0.
Shepmaster

4

Un problema leggermente diverso che causa messaggi del compilatore molto simili è la dipendenza dalla durata dell'oggetto, piuttosto che la memorizzazione di un riferimento esplicito. Un esempio è la libreria ssh2 . Quando si sviluppa qualcosa di più grande di un progetto di test, si è tentati di provare a mettere il Sessione Channelottenuto da quella sessione uno accanto all'altro in una struttura, nascondendo i dettagli dell'implementazione da parte dell'utente. Tuttavia, si noti che la Channeldefinizione ha la 'sessdurata nella sua annotazione del tipo, mentre Sessionnon lo è.

Ciò causa errori del compilatore simili relativi alla durata della vita.

Un modo per risolverlo in un modo molto semplice è dichiarare l' Sessionesterno nel chiamante e quindi annotare il riferimento all'interno della struttura con una vita, simile alla risposta in questo post del forum dell'utente di Rust che parla dello stesso problema mentre incapsula SFTP . Questo non sembrerà elegante e potrebbe non essere sempre applicabile, perché ora hai due entità da affrontare, piuttosto che quella che volevi!

Risulta la soluzione di noleggio o la cassa owning_ref dall'altra risposta sono le soluzioni anche per questo problema. Consideriamo l'owning_ref, che ha lo scopo speciale per questo scopo esatto: OwningHandle. Per evitare lo spostamento dell'oggetto sottostante, lo allociamo sull'heap usando a Box, che ci offre la seguente possibile soluzione:

use ssh2::{Channel, Error, Session};
use std::net::TcpStream;

use owning_ref::OwningHandle;

struct DeviceSSHConnection {
    tcp: TcpStream,
    channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}

impl DeviceSSHConnection {
    fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
        use std::net::TcpStream;
        let mut session = Session::new().unwrap();
        let mut tcp = TcpStream::connect(targ).unwrap();

        session.handshake(&tcp).unwrap();
        session.set_timeout(5000);
        session.userauth_password(c_user, c_pass).unwrap();

        let mut sess = Box::new(session);
        let mut oref = OwningHandle::new_with_fn(
            sess,
            unsafe { |x| Box::new((*x).channel_session().unwrap()) },
        );

        oref.shell().unwrap();
        let ret = DeviceSSHConnection {
            tcp: tcp,
            channel: oref,
        };
        ret
    }
}

Il risultato di questo codice è che non possiamo più usare Session, ma è memorizzato insieme a quello Channelche useremo. Poiché l' OwningHandleoggetto dereferenzia Box, a cui si riferisce Channel, quando lo si memorizza in una struttura, lo chiamiamo come tale. NOTA: questa è solo la mia comprensione. Ho il sospetto che questo potrebbe non essere corretto, dal momento che sembra essere abbastanza vicino alla discussione OwningHandlesull'insicurezza .

Un dettaglio curioso qui è che Sessionlogicamente ha una relazione simile a TcpStreamquella che Channeldeve Session, ma la sua proprietà non è presa e non ci sono annotazioni di tipo in giro. Invece, spetta all'utente prendersi cura di questo, come dice la documentazione del metodo della stretta di mano :

Questa sessione non diventa proprietario del socket fornito, si consiglia di assicurarsi che il socket persista per tutta la durata della sessione per garantire che la comunicazione sia eseguita correttamente.

Si raccomanda inoltre che lo stream fornito non venga utilizzato contemporaneamente altrove per la durata di questa sessione poiché potrebbe interferire con il protocollo.

Quindi, con l' TcpStreamuso, dipende completamente dal programmatore per garantire la correttezza del codice. Con il OwningHandle, l'attenzione su dove accade la "magia pericolosa" viene attirata usando il unsafe {}blocco.

Un'ulteriore e più approfondita discussione di questo problema si trova in questo thread del forum dell'utente di Rust, che include un esempio diverso e la sua soluzione utilizzando la cassa a noleggio, che non contiene blocchi non sicuri.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.