Traço oposto ao empréstimo para tipos de cópia?
Eu vi o Borrow
traço usado para definir funções que aceitam tanto um tipo pertencente quanto uma referência, por exemplo, T
ou &T
. O borrow()
método é então chamado na função a ser obtida &T
.
Existe alguma característica que permite o oposto (ou seja, uma função que aceita T
ou &T
e obtém T
) para Copy
tipos?
Por exemplo, para este exemplo:
use std::borrow::Borrow;
fn foo<T: Borrow<u32>>(value: T) -> u32 {
*value.borrow()
}
fn main() {
println!("{}", foo(&5));
println!("{}", foo(5));
}
Isso chama borrow()
para obter uma referência, que é imediatamente desreferenciada.
Existe outra implementação que apenas copia o valor se T
foi passado e desreferencia se &T
foi fornecido? Ou é a forma idiomática de escrever esse tipo de coisa?
Respostas
Não há realmente uma característica inversa para Borrow
, porque não é realmente útil como um limite de funções da mesma forma Borrow
. O motivo tem a ver com propriedade.
Por que "inverso Borrow
" é menos útil do que Borrow
?
Funções que precisam de referências
Considere uma função que só precisa fazer referência a seu argumento:
fn puts(arg: &str) {
println!("{}", arg);
}
Aceitar String
seria uma tolice aqui, porque puts
não precisa se apropriar dos dados, mas aceitar &str
significa que às vezes podemos forçar o chamador a manter os dados por mais tempo do que o necessário:
{
let output = create_some_string();
output.push_str(some_other_string);
puts(&output);
// do some other stuff but never use `output` again
} // `output` isn't dropped until here
O problema é que output
não é necessário depois de passado para puts
, e o chamador sabe disso, mas puts
requer uma referência, então output
tem que permanecer ativo até o final do bloco. Obviamente, você sempre pode corrigir isso no chamador adicionando mais blocos e, às vezes let
, um , mas puts
também pode ser genérico para permitir que o chamador delegue a responsabilidade de limpar output
:
fn puts<T: Borrow<str>>(arg: T) {
println!("{}", arg.borrow());
}
Aceitar T: Borrow
para puts
dá ao chamador a flexibilidade de decidir se deseja manter o argumento ou movê-lo para a função.
Funções que precisam de valores próprios
Agora considere o caso de uma função que realmente precisa se apropriar:
struct Wrapper(String);
fn wrap(arg: String) -> Wrapper {
Wrapper(arg)
}
Nesse caso aceitar &str
seria bobagem, pois wrap
teria que invocar to_owned()
. Se o chamador tem um String
que não está mais usando, copia desnecessariamente os dados que poderiam ter sido movidos para a função. Nesse caso, aceitar String
é a opção mais flexível, pois permite que o chamador decida se deseja fazer um clone ou passar um existente String
. Ter uma Borrow
característica " inversa " não acrescentaria nenhuma flexibilidade que arg: String
já não seja fornecida.
Mas String
não é sempre o argumento mais ergonómico, porque há vários tipos diferentes de string: &str
, Cow<str>
, Box<str>
... Nós podemos fazer wrap
um pouco mais ergonômico, dizendo que aceita qualquer coisa que possa ser convertido into
a String
.
fn wrap<T: Into<String>>(arg: T) -> Wrapper {
Wrapper(arg.into())
}
Isso significa que você pode chamá-lo assim, wrap("hello, world")
sem precisar chamar .to_owned()
o literal. O que não é realmente uma vitória de flexibilidade - o chamador sempre pode ligar .into()
sem perda de generalidade - mas é uma vitória ergonômica .
E os Copy
tipos?
Agora, você perguntou sobre os Copy
tipos. Na maior parte, os argumentos acima ainda se aplicam. Se você estiver escrevendo uma função que, por exemplo puts
, só precisa de um &A
, o uso T: Borrow<A>
pode ser mais flexível para o chamador; para uma função como wrap
essa precisa do todo A
, é mais flexível apenas aceitar A
. Mas, para os Copy
tipos, a vantagem ergonômica de aceitar T: Into<A>
é muito menos clara.
- Para tipos inteiros, como os genéricos bagunçam a inferência de tipo, usá-los geralmente torna menos ergonômico usar literais; você pode acabar tendo que anotar explicitamente os tipos.
- Como
&u32
não implementaInto<u32>
, esse truque específico não funcionaria aqui de qualquer maneira. - Como os
Copy
tipos estão prontamente disponíveis como valores de propriedade, é menos comum usá-los como referência em primeiro lugar. - Por fim, transformar um
&A
em umA
quandoA: Copy
é tão simples quanto adicionar*
; ser capaz de pular essa etapa provavelmente não é uma vitória convincente o suficiente para contrabalançar a complexidade adicional do uso de genéricos na maioria dos casos.
Em conclusão, foo
quase certamente deve apenas aceitar value: u32
e deixar o chamador decidir como obter esse valor.
Veja também
- É mais convencional passar por valor ou passar por referência quando o método precisa da propriedade do valor?
Com a função que você tem, você só pode usar um u32
ou um tipo que pode ser emprestado como u32
.
Você pode tornar sua função mais genérica usando um segundo argumento de modelo.
fn foo<T: Copy, N: Borrow<T>>(value: N) -> T {
*value.borrow()
}
No entanto, esta é apenas uma solução parcial, pois exigirá anotações de tipo em alguns casos para funcionar corretamente.
Por exemplo, funciona imediatamente com usize
:
let v = 0usize;
println!("{}", foo(v));
Não há problema para o compilador adivinhar que foo(v)
é um usize
.
No entanto, se você tentar foo(&v)
, o compilador reclamará que não pode encontrar o tipo de saída correto T
porque &T
pode implementar várias Borrow
características para diferentes tipos. Você precisa especificar explicitamente qual deseja usar como saída.
let output: usize = foo(&v);