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

Propriedade no Rust

Os programas de computador devem gerenciar os recursos de memória que usam durante a execução.

A maioria das linguagens de programação possui a função de gerenciamento de memória:

C/C++ Este tipo de linguagem geralmente gerencia a memória de forma manual, e os desenvolvedores precisam manualmente solicitar e liberar recursos de memória. No entanto, para melhorar a eficiência de desenvolvimento, muitos desenvolvedores não têm o hábito de liberar memória em tempo hábil, o que muitas vezes leva ao desperdício de recursos.

Programas escritos em Java são executados no ambiente virtual (JVM), que possui a função de reciclagem automática de recursos de memória. No entanto, este método muitas vezes reduz a eficiência de execução, então o JVM tenta reciclar o menor número possível de recursos, o que também faz com que o programa ocupe uma grande quantidade de recursos de memória.

O conceito de propriedade é um conceito novo para a maioria dos desenvolvedores, é um mecanismo de sintaxe projetado pelo idioma Rust para usar memória de forma eficiente. O conceito de propriedade foi criado para que o Rust possa analisar mais eficazmente os recursos de memória no estágio de compilação para implementar a gestão de memória.

Regras de propriedade

Existem três regras para a propriedade:

  • Cada valor no Rust tem uma variável chamada proprietário.

  • Apenas um proprietário pode existir ao mesmo tempo.

  • Quando o proprietário não está no escopo de execução do programa, esse valor será deletado.

Essas três regras são a base do conceito de propriedade.

A seguir, será introduzido o conceito relacionado ao conceito de propriedade.

Escopo da variável

Usamos o seguinte programa para descrever o conceito de escopo da variável:

{
    // Antes da declaração, a variável s é inválida
    let s = "w3codebox";
    // Aqui está o escopo disponível da variável s
}
// O escopo da variável s já terminou, a variável s é inválida

O escopo da variável é uma propriedade da variável, que representa o domínio de validade da variável, que por padrão é válido desde a declaração da variável até o final do domínio da variável.

Memória e alocação

Se definirmos uma variável e atribuirmos a ela um valor, o valor da variável existe na memória. Essa situação é muito comum. Mas se precisarmos armazenar dados cuja comprimento não é determinado (por exemplo, uma sequência de caracteres inserida pelo usuário), não podemos especificar o comprimento dos dados no momento da definição, nem podemos alocar um espaço de memória fixo na fase de compilação para o uso de armazenamento de dados. (Alguém diz que alocar espaço o mais grande possível pode resolver o problema, mas esse método é muito incivilizado). Isso requer fornecer um mecanismo que, durante a execução do programa, permita que o programa peça sozinho a utilização de memória - a pilha. Todos os "recursos de memória" discutidos neste capítulo se referem ao espaço de memória ocupado pela pilha.

Se houver alocação, há liberação, o programa não pode ocupar continuamente algum recurso de memória. Portanto, o fator chave para determinar se um recurso é desperdiçado é se ele é liberado em tempo hábil.

Vamos escrever o exemplo de string em C de forma equivalente:

{
    char *s = "w3codebox";
    free(s); // Liberar recursos de s
}

Claramente, não há chamada de função free para liberar os recursos da string s no Rust (eu sei que isso é um código incorreto em C, porque "w3O "codebox" não está na pilha, aqui supondo que ele esteja). A razão pela qual o Rust não especifica explicitamente a etapa de liberação é porque o compilador do Rust adiciona automaticamente a chamada de função de liberação de recursos no final do escopo da variável.

Este mecanismo parece muito simples: ele não faz mais do que ajudar os programadores a adicionar uma chamada de função de liberação de recursos no local apropriado. No entanto, este mecanismo simples pode resolver eficazmente um dos problemas de programação mais frustrantes da história dos programadores.

as maneiras de interação entre variáveis e dados

As maneiras de interação entre variáveis e dados são principalmente mover (Move) e clonar (Clone) dois:

mover

Vários variáveis podem interagir com os mesmos dados de diferentes maneiras no Rust:

let x = 5;
let y = x;

Este programa atribui o valor 5 ligado ao variável x, e depois copiou e atribuiu o valor de x ao variável y. Agora haverão dois valores na pilha 5Neste caso, os dados são "tipos de dados básicos", que não precisam ser armazenados na pilha, e o "movimento" dos dados na pilha é diretamente copiado, o que não leva mais tempo ou mais espaço de armazenamento. "Tipos de dados básicos" têm os seguintes:

  • 。32 todos os tipos de números inteiros, por exemplo, i32 、 u64 、 i

  • etc.

  • o tipo booleano bool, com valores true ou false.32 todos os tipos de números decimais, f64e f

  • o tipo de caractere char.

apenas contém os tipos de dados acima, tuplas (Tuples).

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

Mas se os dados de interação ocorrerem na pilha, é outra situação:

A primeira etapa gera um objeto String com o valor "hello". O "hello" pode ser considerado um tipo de dados de comprimento incerto, que precisa ser armazenado na pilha.A situação da segunda etapa é um pouco diferente (Isso não é completamente verdadeiro, serve apenas como referência para comparação

):2 Como mostrado na figura: dois objetos String na pilha, cada objeto String tem um ponteiro apontando para a string "hello" na pilha. Ao dar s

apenas os dados na pilha são copiados, a string na pilha ainda é a mesma string original.1 e s2 já mencionamos que, quando uma variável está fora de escopo, o Rust chama automaticamente a função de liberação de recursos e limpa a memória da pilha da variável. Mas ao atribuir s2 quando todos os valores são liberados, o "hello" na pilha é liberado duas vezes, o que não é permitido pelo sistema. Para garantir a segurança, antes de dar s1 já está inválido. Claro, ao atribuir s1 é atribuído a s2 depois1 não pode ser usado novamente. Abaixo está o programa errado:

let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // Erro!s1 já está desativado

Portanto, a situação real é:

s1 nominalmente existe, na prática não.

Clonagem

O Rust tenta reduzir ao máximo o custo de execução do programa, então, por padrão, dados de grande tamanho são armazenados na pilha e usados de forma móvel para interagir com dados. Mas se precisar simplesmente copiar dados para usá-los em outro lugar, pode usar a segunda forma de interação de dados - clonagem.

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

Resultados da execução:

s1 = hello, s2 = hello

Aqui realmente estamos copiando uma cópia do "hello" na pilha, então s1 e s2 Cada um está vinculado a um valor, e será liberado como dois recursos ao liberar.

Claro, clonar deve ser usado apenas quando necessário,毕竟复制数据会花费更多的时间.

Involving the ownership mechanism of functions

Para as variáveis, este é o caso mais complexo.

Como lidar com a propriedade de segurança ao passar uma variável como parâmetro para outra função?

A seguir, este programa descreve o princípio de funcionamento do mecanismo de propriedade em tal situação:

fn main() {
    let s = String::from("hello");
    // s foi declarado válido
    recebe_propriedade(s);
    // O valor de s foi usado como parâmetro na função
    // Portanto, pode-se considerar que s já foi movido, a partir daqui é inválido
    let x = 5;
    // x foi declarado válido
    faz_copia(x);
    // O valor de x foi usado como parâmetro na função
    // Mas x é um tipo básico, ainda válido
    // Aqui ainda pode-se usar x, mas não pode-se usar s
} // A função terminou, x é inválido, então é s. Mas s foi movido, então não precisa ser liberado
fn recebe_propriedade(some_string: String) { 
    // Um parâmetro String some_string foi传入,válido
    println!("{}", some_string);
} // A função terminou, o parâmetro some_string aqui foi liberado
fn faz_copia(some_integer: i32) { 
    // Um i32 O parâmetro some_integer foi传入,válido
    println!("{}", some_integer);
} // A função terminou, o parâmetro some_integer é um tipo básico, não há necessidade de liberar

Se passar uma variável como parâmetro para uma função, os efeitos são os mesmos.

Mecanismo de propriedade do valor de retorno das funções

fn main() {
    let s1 = devolve_propriedade();
    // devolve_propriedade moviu seu valor de retorno para s1
    let s2 = String::from("hello");
    // s2 Foi declarado válido
    let s3 = recebe_e_devolve(s2);
    // s2 Foi movido como parâmetro, s3 Foi obtida a propriedade do valor de retorno
} // s3 O valor inválido foi liberado, s2 Foi movido, s1 O valor inválido foi liberado.
fn devolve_propriedade() -> String {
    let some_string = String::from("hello");
    // some_string foi declarado válido
    return some_string;
    // some_string foi movida como valor de retorno da função
}
fn recebe_e_devolve(a_string: String) -> String { 
    // a_string foi declarado válido
    a_string  // a_string é movida para fora da função como valor de retorno
}

a propriedade da variável que será retornada como valor da função será movida fora da função e retornada ao local da chamada da função, em vez de ser liberada diretamente.

Referência e Empréstimo

Referência (Reference) é C++ conceito mais familiar aos desenvolvedores.

se você está familiarizado com o conceito de ponteiros, você pode vê-lo como um tipo de ponteiro.

na verdade "referência" é uma forma de acesso indireto a variáveis.

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

Resultados da execução:

s1 é hello, s2 é hello

o operador & pode obter a "referência" da variável.

quando o valor de uma variável é referenciado, a variável em si não é considerada inválida. Porque "referência" não copia o valor da variável na pilha:

a razão para passar parâmetros de função é a mesma:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("O comprimento de '{}' é {}.", s1, len);
}
fn calculate_length(s: &String, len); -> usize {
    s.len()
}

Resultados da execução:

O comprimento de 'hello' é 5.

a referência não obterá a propriedade dos valores.

a referência pode apenas empréstimo (Borrow) a propriedade dos valores.

a referência em si também é um tipo e possui um valor, esse valor registra a posição de outros valores, mas a referência não possui a propriedade dos valores apontados:

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

este programa está incorreto: porque s2 o empréstimo de s1 já moveu a propriedade para s3, então s2 não será possível continuar a empréstimo de uso s1 a propriedade. Se precisar usar s2 para usar esse valor, é necessário fazer um empréstimo novamente:

fn main() {
    let s1 = String::from("hello");
    let mut s2 = &s1;
    let s3 = s2;
    s2 = &s3; // voltando de s3 empréstimo de propriedade
    println!("{}", s)2);
}

este programa está correto.

já que a referência não possui propriedade, mesmo que ela empréstimo a propriedade, ela só possui o uso (isso é o mesmo que alugar uma casa).

se tentar usar o direito do empréstimo para modificar dados será bloqueado:

fn main() {
    let s1 = String::from("run");
    let s2 = &s1; 
    println!("{}", s)2);
    s2.push_str("oob"); // erro, modificação proibida do valor do empréstimo
    println!("{}", s)2);
}

neste programa s2 tentar modificar s1 seus valores são bloqueados, e o proprietário não pode modificar os valores do proprietário.

Claro, também há um tipo de empréstimo mutável, como alugar uma casa, se o proprietário da propriedade permitir que o proprietário modifique a estrutura da casa, e também declarar no contrato que você tem esse direito ao alugar, você pode reformar a casa:

fn main() {
    let mut s1 = String::from("run");
    // s1 é mutável
    let s2 = &mut s1;
    // s2 é uma referência mutável
    s2.push_str("oob");
    println!("{}", s)2);
}

Este programa não tem problema. Usamos &mut para modificar o tipo de referência mutável.

Em comparação com referências imutáveis, além de diferentes permissões, referências mutáveis não permitem múltiplas referências, mas referências imutáveis podem:

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

Este programa não é correto porque múltiplas referências mutáveis a s.

O design do Rust para referências mutáveis é principalmente para considerar colisões de acesso a dados em estado concorrente, evitando esse tipo de coisa no estágio de compilação.

Devido ao fato de que uma das condições necessárias para o colisão de acesso a dados é que os dados sejam acessados pelo menos por um usuário e, ao mesmo tempo, pelo menos por outro usuário para leitura ou escrita, não é permitido que um valor seja referenciado novamente quando há referência mutável.

Referência Pendente (Dangling References)

É um conceito renomeado, se estivesse em um idioma de programação com conceito de ponteiros, ele se referiria a aqueles ponteiros que não apontam para um dado realmente acessível (observe que não é necessariamente um ponteiro nulo, pode ser um recurso liberado). Eles são como um objeto pendente sem corda, portanto, chamam-se "referência pendente".

"Referência pendente" não é permitida no idioma Rust, se existir, o compilador encontrará.

Abaixo está um caso clássico de referência pendente:

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

Claramente, com o fim da função dangle, o valor da variável local não foi usado como valor de retorno, foi liberado. Mas sua referência foi retornada, e o valor a que ela aponta já não pode ser determinado, então não é permitido que apareça.