Borrowing
Uma chamada não leva o que recebe. Todo parâmetro passa por borrow por padrão, salvo se a assinatura disser o contrário, e o call site não escreve nada: sem &, sem &mut, sem marcador na chamada.
struct Bag { items: Array<i64> }
fn total(b: Bag): i64 { b.items.len() }
let bag = Bag { items: [1, 2] };
let n = total(bag); // reading a parameter borrows it, so `bag` is still here
println(`${n} and ${bag.items.len()}`);O que uma chamada faz com o valor
Três coisas, e o compilador lê qual delas no corpo da função chamada. A mesma regra vale em um pub fn de outro module e dentro de um module, e nada disso é escrito no call site.
- Ler - o parâmetro é um borrow, e o valor continua com quem chamou.
- Escrever - o argumento precisa ser um lugar que pode ser escrito, e o valor de quem chamou muda.
- Ficar - a função guarda o valor, envolve o valor ou o captura em uma closure, então a variável de quem chamou é consumida. Um parâmetro que a função só lê e devolve inteiro passa por borrow em vez disso: veja borrows devolvidos.
Usar um valor que a função chamada ficou é recusado no ponto do uso, e a mensagem nomeia a chamada:
error: 'bag' was given to 'consume' on line 5, which keeps it; write 'bag.clone()' at the call if both uses are neededUm parâmetro que o corpo escreve é a variável do chamador
Em C, uma função que deve alterar a variável do chamador recebe um pointer, o chamador passa &x, e a função escreve através de *p. O Talor tem o mesmo efeito sem nada escrito em nenhum dos dois lugares: quando o corpo escreve em um parâmetro, o compilador o passa como um pointer para a variável do chamador, e depois da chamada o chamador lê o valor que a função escreveu.
struct Counter { n: i64 }
fn bump(c: Counter) { c.n = c.n + 1; }
fn fill(xs: Array<i64>, k: i64) {
let i: i64 = 0;
while i < k { xs.push(i); i += 1; }
}
fn main(): i32 {
let c = Counter { n: 40 };
bump(c);
bump(c);
let xs: Array<i64> = [];
fill(xs, 3);
(c.n + xs.len()) as i32 // 45: c and xs are the caller's, with the writes
}O programa sai com 45. bump e fill recebem o endereço de c e de xs do chamador, e não uma cópia, e o contrato que o compilador derivou é o que talor surface registra:
fn bump(c: Counter edits): ()
fn fill(xs: Array<i64> edits, k: i64): ()Três coisas que um pointer de C não promete, e isto promete:
Não existe null. O argumento é sempre uma variável do chamador, então não há nada a checar antes de escrever.
Dois argumentos escritos nunca são o mesmo lugar. Uma chamada que entrega a mesma variável a dois parâmetros que o corpo escreve é recusada, porque cada escrita alcançaria o que a outra está alterando:
talorstruct H { items: Array<i64> } fn swap_into(a: H, b: H) { a.items = [1]; b.items = [2]; } fn main(): i32 { let h = H { items: [] }; swap_into(h, h); // refused: both are written, and they are the same place 0 }error: argument 1 is edited and argument 2 is the same place 'h', so the write reaches what the other is read out of; pass two placesO que é lido do lugar que está sendo escrito é passado como cópia, então a escrita não o alcança. Aqui
move_overlêke escreveh, e a chamada entregah.k, uma parte do próprioh:talorstruct K { items: Array<i64> } struct H { k: K, total: i64 } // Reads `k`, writes `h`: `h.k.items` is emptied and replaced. fn move_over(k: K, h: H): i64 { h.k.items = []; h.k.items.push(99); let sum: i64 = 0; for x in k.items { sum += x; } // reads the k it was handed h.total = sum; sum } fn main(): i32 { let h = H { k: K { items: [10, 20, 30] }, total: 0 }; let s = move_over(h.k, h); // h.k is read out of the place h the call writes println(`sum=${s} total=${h.total} items=${h.k.items.len()} first=${h.k.items[0]}`); 0 }sum=60 total=60 items=1 first=99ké o array como estava na chamada, porque o compilador passou uma cópia deh.k, e a escrita alcançou ohdo chamador. Sem a cópia,kapontaria para dentro do array que a primeira linha do corpo libera. Compilado com--borrow strict, o compilador não faz nenhuma cópia por conta própria e recusa a chamada, emove_over(h.k.clone(), h)escrito à mão é o mesmo programa.
A clause: edits e takes
Uma clause depois da assinatura declara o efeito na própria assinatura, para quem prefere isso escrito a inferido:
pub fn append(dst: Array<i64>, src: Array<i64>) edits dst { dst.push(1); }
pub fn consume(b: Bag): i64 takes b { b.items.len() }
append(xs, extra); // the same call either way
let n = consume(bag); // `bag` is gone after this lineedits x- a função altera o valor de quem chamou, então o argumento precisa ser um lugar que pode ser escrito.takes x- a função fica com o valor, e usá-lo depois é um erro que nomeia a chamada.- Cada verbo nomeia todos os parâmetros a que se aplica,
selfé um dos nomes, e a clause vem depois do tipo de retorno.
A clause é opcional em toda parte, pub fn incluído, e ela nunca chega a quem chama: o compilador calcula o modo de cada parâmetro a partir do corpo e aplica as mesmas regras dos dois jeitos. Uma clause escrita é conferida contra o corpo, então ela não pode virar um comentário que mente:
error: 'size_of' edits 'h', and the body never writes it; drop the clauseUma clause que o corpo já derivaria é informação em vez de erro, e o compilador pode reportá-la - como aviso, desligado por padrão. O único caso em que a palavra escrita é a única fonte do fato é um parâmetro cujo tipo menciona um parâmetro de tipo: esse valor viaja por cópia qualquer que seja a instância, então o corpo não decide nada e a clause diz se a função fica com ele.
Alcançar um container é alterar
h.items.push(x) escreve no valor de quem chamou exatamente como h.n = x, e os dois são uma regra só:
struct Holder { items: Array<i64> }
fn add(h: Holder, x: i64) edits h { h.items.push(x); }
let h = Holder { items: [1] };
add(h, 2);
println(`${h.items.len()}`); // 2Com a clause escrita ou inferida, o argumento precisa ser um lugar que pode ser escrito. Ler um parâmetro não é alterar, e não precisa de clause.
Uma escrita que alcança o que outro argumento lê
Dois argumentos de uma chamada, em que a escrita alcança aquilo de que o outro é lido, seriam um lugar que a função escreve enquanto lê. O que só é lido é passado como sua cópia, que o compilador escreve sozinho:
struct H { items: Array<Array<i64>> }
fn g(dst: H, src: H): i64 {
let r = src.items[0]; // read out of `src`
dst.items = []; // the write, through the parameter the clause edits
r.len()
}
g(h, h); // accepted: `src` is a copy of `h`, taken before the call
g(h, k); // accepted: the write reaches neither, and nothing is copiedA cópia custa um clone e acontece só em uma chamada que a escrita alcança. O receiver de um método é um argumento como qualquer outro, nas duas direções: p.show(p.items), em que show escreve self.items, copia o argumento, e h.look(h.k), em que look escreve o seu parâmetro e só lê self, copia o receiver.
Três casos são recusados em vez disso, porque nenhuma cópia os resolve:
- dois lugares escritos que se sobrepõem, sejam dois argumentos ou um argumento e o receiver, já que a cópia de qualquer um dos dois levaria a escrita embora de quem chamou;
- um valor lido que não aceita clone, um que contém uma closure ou um recurso;
- um lugar que um lock entrega por borrow, e que nunca é copiado.
O que um borrow não é
- Uma view é read-only, e nenhuma palavra a escreve: o compilador a infere de como um valor é usado. Veja lifetimes.
- Um valor Copy é passado por valor, e um agregado lido não é copiado: ele viaja como um pointer para o valor de quem chamou.
- O tipo de um valor de função não carrega clause, então uma função que escreve, fica com ou devolve um parâmetro não pode ser usada como valor.
- O parâmetro de uma closure nunca é tomado, faça o corpo o que fizer.
Quanto tempo um borrow dura
Até o último uso dele, que é o assunto de lifetimes.