English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Proprietà in Rust

I programmi informatici devono gestire le risorse di memoria che utilizzano durante l'esecuzione.

La maggior parte dei linguaggi di programmazione ha la funzione di gestione della memoria:

Linguaggi come C/C++ gestiscono la memoria principalmente in modo manuale, gli sviluppatori devono richiedere e liberare manualmente le risorse di memoria. Ma per migliorare l'efficienza di sviluppo, molti sviluppatori non hanno l'abitudine di liberare la memoria in tempo, quindi il modo di gestione manuale della memoria spesso causa uno spreco di risorse.

I programmi scritti in Java eseguono nel virtual machine (JVM), il JVM ha la funzione di recupero automatico delle risorse di memoria. Ma questo metodo spesso riduce l'efficienza di esecuzione, quindi il JVM recupera le risorse il meno possibile, il che porta anche a un uso maggiore delle risorse di memoria del programma.

Il concetto di proprietà è un concetto nuovo per molti sviluppatori, è un meccanismo di sintassi progettato dal linguaggio Rust per l'uso efficiente della memoria. Il concetto di proprietà è nato per permettere a Rust di analizzare più efficacemente le risorse di memoria durante la fase di compilazione, realizzando così la gestione della memoria.

Regole di proprietà

La proprietà ha tre regole principali:

  • Ogni valore in Rust ha una variabile chiamata proprietario.

  • Può esserci solo un proprietario alla volta.

  • Quando il proprietario non è nella portata dell'esecuzione del programma, il valore verrà eliminato.

Queste tre regole sono la base del concetto di proprietà.

Verranno presentati i concetti relativi al concetto di proprietà.

Portata delle variabili

Descriviamo il concetto di portata delle variabili con il seguente programma:

{
    // Prima della dichiarazione, la variabile s non è valida
    let s = "w3codebox";
    // Questo è l'intervallo di validità della variabile s
}
// L'intervallo della variabile è terminato, la variabile s non è valida

L'intervallo di variabile è un attributo della variabile che rappresenta la sfera di validità della variabile, che è valido di default dal momento della dichiarazione fino alla fine della sfera di validità della variabile.

Memoria e assegnazione

Se definiamo una variabile e le assegniamo un valore, il valore della variabile esiste nella memoria. Questo è un caso comune. Ma se i dati che dobbiamo memorizzare hanno una lunghezza incerta (ad esempio, una stringa inserita dall'utente), non possiamo specificare la lunghezza dei dati al momento della definizione e non possiamo assegnare uno spazio di memoria fisso per l'archiviazione dei dati durante la fase di compilazione. (Alcuni dicono che allocare uno spazio più grande può risolvere il problema, ma questo metodo è poco educato). Questo richiede un meccanismo che permetta al programma di richiedere memoria da utilizzare durante l'esecuzione. Questo capitolo parla di tutte le "risorse di memoria" come spazio di memoria utilizzato dal heap.

Se ci sono assegnamenti, ci devono essere anche liberazioni; il programma non può continuare a utilizzare una risorsa di memoria. Pertanto, il fattore chiave che determina se una risorsa è sprecata è se viene liberata in tempo.

Scriviamo un programma di esempio di stringa in C equivalente:

{
    char *s = "w3codebox";
    free(s); // Liberare la risorsa s
}

Chiaramente, in Rust non viene chiamata la funzione free per liberare le risorse della stringa s (lo so che è un modo scorretto in C, perché "w3codebox" non è nel heap, qui supponiamo che lo sia). Rust non richiede una fase esplicita di liberazione perché il compilatore di Rust aggiunge automaticamente la chiamata alla funzione di liberazione delle risorse alla fine della vita della variabile.

Questo meccanismo sembra molto semplice: non fa altro che aggiungere una chiamata di funzione per liberare le risorse nel punto giusto per il programmatore. Tuttavia, questo meccanismo semplice può risolvere un problema di programmazione che ha dato tanto fastidio ai programmatori.

Modi di interazione delle variabili con i dati

Il modo in cui le variabili interagiscono con i dati è principalmente lo spostamento (Move) e la duplicazione (Clone):

Spostamento

Più variabili possono interagire con lo stesso dato in modi diversi in Rust:

let x = 5;
let y = x;

Questo programma assegna il valore 5 alla variabile x, copia il valore di x e lo assegna alla variabile y. Ora ci saranno due valori 5 nello stack. In questo caso i dati sono di tipo "dati base" e non devono essere memorizzati nel heap, ma sono memorizzati direttamente nello stack. Questo non richiede più tempo o più spazio di archiviazione. I tipi di dati "base" sono questi:

  • Tutti i tipi di numero intero, ad esempio i32, u32, i64 ecc.

  • Tipo booleano bool, con valori true o false.

  • Tutti i tipi di numero a virgola mobile, f32 e f64.

  • Tipo di carattere char.

  • Tuple che contengono solo i tipi di dati elencati sopra.}

Ma se i dati di intercambio si trovano nella pila, è un'altra situazione:

let s1 = String::from("hello");
let s2 = s1;

Il primo passo crea un oggetto String con il valore "hello". "hello" può essere considerato un tipo di dati con lunghezza non determinata che deve essere memorizzato nella pila.

La situazione del secondo passo è leggermente diversa (Questo non è completamente vero, serve solo per fare riferimento):

Come illustrato nell'immagine: due oggetti String nella pila, ciascuno ha un puntatore che si riferisce alla stringa "hello" nella pila. Quando si assegna a s2, viene copiato solo il dato nella pila, la stringa nella pila rimane la stessa.

Come abbiamo detto prima, quando una variabile esce dal raggio di visibilità, Rust chiama automaticamente la funzione di rilascio delle risorse e pulisce la memoria heap della variabile. Ma se vengono liberate entrambe s1 e s2, la stringa "hello" nella zona heap viene liberata due volte, il che non è permesso dal sistema. Per garantire la sicurezza, s1 deve essere invalido quando viene assegnato a s2. Ha ragione, dopo aver assegnato il valore di s1 a s2, s1 non può essere utilizzato più. Questo programma è sbagliato:

let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // Errore! s1 non è più valido

Quindi la situazione reale è:

s1 è solo un nome, in realtà non esiste.

Clonazione

Rust cerca di ridurre al minimo i costi di esecuzione del programma, quindi per impostazione predefinita, i dati di lunghezza maggiore vengono memorizzati nella pila e utilizzati per l'intercambio di dati in modo mobile. Ma se si desidera copiare i dati in modo semplice per usarli in modo diverso, si può utilizzare il secondo modo di intercambio dei dati - la clonazione.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
    println!("s1 = {}, s2 = {}", s1, s2);
}

Risultato dell'esecuzione:

s1 = hello, s2 = hello

Qui viene effettivamente copiato "hello" nella pila, quindi s1 e s2 si legano a due valori ciascuno, e vengono liberati come due risorse.

Certo, la clonazione viene utilizzata solo quando è necessario copiare, poiché la copia dei dati richiede più tempo.

Meccanismo di possesso delle funzioni coinvolte

Per una variabile, questo è il caso più complesso.

Come gestire in modo sicuro la proprietà quando si passa una variabile come argomento a un'altra funzione?

Questo programma descrive il funzionamento del meccanismo di possesso in questo caso:

fn main() {
    let s = String::from("hello");
    // s viene dichiarato come valido
    takes_ownership(s);
    // Il valore di s viene passato come argomento alla funzione
    // Quindi può essere considerato che s è stato spostato e non è più valido da questo punto in poi
    let x = 5;
    // x viene dichiarato come valido
    makes_copy(x);
    // Il valore di x viene passato come argomento alla funzione
    // Ma x è di tipo di base, rimane valido
    // Puoi ancora usare x ma non s
} // Fine della funzione, x è invalido, poi è s. Ma s è stato spostato, quindi non deve essere liberato
fn takes_ownership(some_string: String) { 
    // Un parametro String some_string passato, valido
    println!("{}", some_string);
} // Fine della funzione, il parametro some_string qui liberato
fn makes_copy(some_integer: i32) { 
    // Un parametro i32 some_integer passato, valido
    println!("{}", some_integer);
} // Fine della funzione, il parametro some_integer è di tipo di base, non richiede liberazione

Se si passa una variabile come parametro alla funzione, l'effetto è lo stesso di un movimento.

Il meccanismo di possesso del valore di ritorno delle funzioni

fn main() {
    let s1 = gives_ownership();
    // gives_ownership sposta il suo valore di ritorno a s1
    let s2 = String::from("hello");
    // s2 è dichiarato come valido
    let s3 = takes_and_gives_back(s2);
    // s2 è spostato come parametro, s3 riceve il possesso del valore di ritorno
} // s3 è invalido e liberato, s2 è spostato, s1 è invalido e liberato.
fn gives_ownership() -> String {
    let some_string = String::from("hello");
    // some_string è dichiarato come valido
    return some_string;
    // some_string è considerato come valore di ritorno spostato fuori dalla funzione
}
fn takes_and_gives_back(a_string: String) -> String { 
    // a_string è dichiarato come valido
    a_string  // a_string è considerato come valore di ritorno spostato fuori dalla funzione
}

Il possesso del variabile considerato come valore di ritorno della funzione verrà spostato fuori dalla funzione e restituito al punto di chiamata della funzione, senza essere liberato direttamente.

Referenza e prestito

La referenza (Reference) è un concetto abbastanza familiare agli sviluppatori C++.

Se sei familiare con il concetto di puntatore, puoi vederlo come un tipo di puntatore.

In sostanza, "referenza" è un modo indiretto di accesso alle variabili.

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    println!("s1 è {}, s2 è {}", s1, s2)
}

Risultato dell'esecuzione:

s1 è hello, s2 è hello

L'operatore & può ottenere il "riferimento" della variabile.

Quando il valore di una variabile viene riferito, la variabile stessa non viene considerata invalida. Poiché "riferimento" non copia il valore della variabile in memoria:

Il motivo della trasmissione dei parametri delle funzioni è lo stesso:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
    s.len()
}

Risultato dell'esecuzione:

The length of 'hello' is 5.

Il riferimento non ottiene il possesso del valore.

Il riferimento può solo noleggiare il possesso di un valore.

Il riferimento stesso è anche un tipo e ha un valore, questo valore registra la posizione di altri valori, ma il riferimento non ha la proprietà degli oggetti a cui si riferisce:

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    let s3 = s1;
    println!("{}", s2);
}

Questo programma non è corretto: perché s2 ha noleggiato la proprietà di s1 e ha spostato la proprietà a s3, quindi s2 non potrà più noleggiare l'uso della proprietà di s1. Se si desidera utilizzare il valore di s2, è necessario noleggiare di nuovo:

fn main() {
    let s1 = String::from("hello");
    let mut s2 = &s1;
    let s3 = s2;
    s2 = &s3; // noleggia di nuovo la proprietà da s3
    println!("{}", s2);
}

Questo programma è corretto.

Poiché il riferimento non ha la proprietà, anche se noleggia la proprietà, ha solo il diritto di utilizzo (è la stessa cosa che noleggiare una casa).

Se si tenta di utilizzare il diritto di noleggio per modificare i dati, viene bloccato:

fn main() {
    let s1 = String::from("run");
    let s2 = &s1; 
    println!("{}", s2);
    s2.push_str("oob"); // errore, modifica proibita del noleggio
    println!("{}", s2);
}

In questo programma, l'operazione di s2 che tenta di modificare il valore di s1 è bloccata, il proprietario del noleggio non può modificare il valore del proprietario.

Certo, esiste anche un modo di noleggio mutabile, come quando noleggi una casa e il proprietario autorizza il proprietario a modificare la struttura della casa, il proprietario dichiara anche nel contratto di conferire questo diritto, quindi puoi ristrutturare la casa:

fn main() {
    let mut s1 = String::from("run");
    // s1 è mutabile
    let s2 = &mut s1;
    // s2 è un riferimento mutabile
    s2.push_str("oob");
    println!("{}", s2);
}

Questo programma non ha problemi. Usiamo &mut per修饰abile il tipo di riferimento.

Oltre al diverso permesso, i riferimenti mutabili non permettono riferimenti multipli, mentre i riferimenti immutabili possono:

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);

Questo programma non è corretto perché ha più riferimenti mutabili a s.

Il design di Rust per i riferimenti mutabili è principalmente per evitare conflitti di accesso ai dati in stato concorrente, evitando questo tipo di problema già a livello di compilazione.

Poiché una delle condizioni necessarie per un conflitto di accesso ai dati è che i dati siano scritti da almeno un utente e contemporaneamente letti o scritti da almeno un altro utente, non è permesso che un valore venga rimosso da un riferimento mutabile.

Riferimenti pendenti (Dangling References)

Questo è un concetto che è stato rinominato, se messo in un linguaggio di programmazione con concetti di puntatore, si riferisce a quei puntatori che non puntano a un vero e proprio dato accessibile (attenzione, non necessariamente un puntatore nullo, potrebbe essere una risorsa liberata). Sembrano come oggetti sospesi senza corde, quindi sono chiamati "riferimenti pendenti".

I "riferimenti pendenti" non sono permessi nel linguaggio Rust, se presenti, il compilatore li rileverà.

Di seguito è riportato un esempio tipico di riferimento pendente:

fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

Evidentemente, alla fine della funzione dangle, il valore delle variabili locali non è stato utilizzato come valore di ritorno, è stato liberato. Ma il riferimento è stato restituito, il valore a cui il riferimento si riferisce non esiste più, quindi non è permesso che compaia.