Prefácio : Esta resposta foi escrita antes que as características integradas opt-in - especificamente os Copy
aspectos - fossem implementadas. Usei aspas em bloco para indicar as seções que se aplicavam apenas ao esquema antigo (aquele que se aplicava quando a pergunta foi feita).
Antigo : para responder à pergunta básica, você pode adicionar um campo de marcador armazenando um NoCopy
valor . Por exemplo
struct Triplet {
one: int,
two: int,
three: int,
_marker: NoCopy
}
Você também pode fazer isso tendo um destruidor (por meio da implementação da Drop
característica ), mas usar os tipos de marcador é preferível se o destruidor não estiver fazendo nada.
Os tipos agora se movem por padrão, ou seja, quando você define um novo tipo, ele não implementa, a Copy
menos que você o implemente explicitamente para o seu tipo:
struct Triplet {
one: i32,
two: i32,
three: i32
}
impl Copy for Triplet {} // add this for copy, leave it out for move
A implementação só pode existir se cada tipo contido no novo struct
ou enum
for ele mesmo Copy
. Caso contrário, o compilador imprimirá uma mensagem de erro. Ele também só pode existir se o tipo não tiver uma Drop
implementação.
Para responder à pergunta que você não fez ... "o que há com moves e copy?":
Em primeiro lugar, definirei duas "cópias" diferentes:
- uma cópia de byte , que está apenas copiando superficialmente um objeto byte a byte, não seguindo ponteiros, por exemplo, se você tiver
(&usize, u64)
, é de 16 bytes em um computador de 64 bits, e uma cópia superficial tomaria esses 16 bytes e replicaria seus valor em algum outro pedaço de memória de 16 bytes, sem tocar usize
no na outra extremidade do &
. Ou seja, é equivalente a chamarmemcpy
.
- uma cópia semântica , duplicando um valor para criar uma nova instância (um tanto) independente que pode ser usada com segurança separadamente da antiga. Por exemplo, uma cópia semântica de um
Rc<T>
envolve apenas o aumento da contagem de referência, e uma cópia semântica de um Vec<T>
envolve a criação de uma nova alocação e, em seguida, a cópia semântica de cada elemento armazenado do antigo para o novo. Podem ser cópias profundas (por exemplo Vec<T>
) ou rasas (por exemplo Rc<T>
, não toca no armazenado T
), Clone
é definido vagamente como a menor quantidade de trabalho necessária para copiar semanticamente um valor do tipo T
de dentro de &T
para T
.
Rust é como C, cada uso por valor de um valor é uma cópia de byte:
let x: T = ...;
let y: T = x; // byte copy
fn foo(z: T) -> T {
return z // byte copy
}
foo(y) // byte copy
Eles são cópias de bytes, sejam ou não T
movidos ou ou "implicitamente copiáveis". (Para ser claro, eles não são necessariamente cópias literalmente byte a byte em tempo de execução: o compilador é livre para otimizar as cópias se o comportamento do código for preservado.)
No entanto, há um problema fundamental com cópias de byte: você acaba com valores duplicados na memória, o que pode ser muito ruim se eles tiverem destruidores, por exemplo
{
let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
} // destructors run here
Se w
fosse apenas uma cópia simples de v
então haveria dois vetores apontando para a mesma alocação, ambos com destruidores que o libertam ... causando um duplo livre , o que é um problema. NB. Isso seria perfeitamente normal, se fizéssemos uma cópia semântica do v
em w
, pois então w
seria independente Vec<u8>
e os destruidores não estariam atropelando uns aos outros.
Existem algumas soluções possíveis aqui:
- Deixe o programador lidar com isso, como C. (não há destruidores em C, então não é tão ruim ... você apenas fica com vazamentos de memória em vez disso.: P)
- Realiza uma cópia semântica implicitamente, de forma que
w
tenha sua própria alocação, como C ++ com seus construtores de cópia.
- Considerar por valor usa como uma transferência de propriedade, de forma que
v
não pode mais ser usado e não tem seu destruidor rodando.
O último é o que o Rust faz: um movimento é apenas um uso por valor em que a fonte é invalidada estaticamente, de modo que o compilador evita o uso futuro da memória agora inválida.
let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
println!("{}", v); // error: use of moved value
Tipos que têm destruidores devem se mover quando usados por valor (também conhecido como quando byte copiado), uma vez que eles têm gerenciamento / propriedade de algum recurso (por exemplo, uma alocação de memória ou um identificador de arquivo) e é muito improvável que uma cópia de byte duplique isso corretamente propriedade.
"Bem ... o que é uma cópia implícita?"
Pense em um tipo primitivo como u8
: uma cópia de byte é simples, basta copiar o byte único, e uma cópia semântica é tão simples, copiar o byte único. Em particular, uma cópia de byte é uma cópia semântica ... Rust ainda tem um traço embutidoCopy
que captura quais tipos têm cópias semânticas e de byte idênticas.
Portanto, para esses Copy
tipos, os usos por valor também são cópias semânticas automaticamente e, portanto, é perfeitamente seguro continuar usando o código-fonte.
let v: u8 = 1;
let w: u8 = v;
println!("{}", v); // perfectly fine
Antigo : O NoCopy
marcador substitui o comportamento automático do compilador de assumir que os tipos que podem ser Copy
(ou seja, contendo apenas agregados de primitivos e &
) são Copy
. No entanto, isso mudará quando as características incorporadas opcionais forem implementadas.
Conforme mencionado acima, características integradas opt-in são implementadas, portanto, o compilador não tem mais comportamento automático. No entanto, a regra usada para o comportamento automático no passado são as mesmas regras para verificar se é legal implementarCopy
.