Traço oposto ao empréstimo para tipos de cópia?

Aug 18 2020

Eu vi o Borrowtraço usado para definir funções que aceitam tanto um tipo pertencente quanto uma referência, por exemplo, Tou &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 Tou &Te obtém T) para Copytipos?

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 Tfoi passado e desreferencia se &Tfoi fornecido? Ou é a forma idiomática de escrever esse tipo de coisa?

Respostas

5 trentcl Aug 18 2020 at 14:09

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 Stringseria uma tolice aqui, porque putsnão precisa se apropriar dos dados, mas aceitar &strsignifica 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 outputnão é necessário depois de passado para puts, e o chamador sabe disso, mas putsrequer uma referência, então outputtem 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 putstambé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: Borrowpara putsdá 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 &strseria bobagem, pois wrapteria que invocar to_owned(). Se o chamador tem um Stringque 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 Borrowcaracterística " inversa " não acrescentaria nenhuma flexibilidade que arg: Stringjá não seja fornecida.

Mas Stringnão é sempre o argumento mais ergonómico, porque há vários tipos diferentes de string: &str, Cow<str>, Box<str>... Nós podemos fazer wrapum pouco mais ergonômico, dizendo que aceita qualquer coisa que possa ser convertido intoa 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 Copytipos?

Agora, você perguntou sobre os Copytipos. 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 wrapessa precisa do todo A, é mais flexível apenas aceitar A. Mas, para os Copytipos, 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 &u32não implementa Into<u32>, esse truque específico não funcionaria aqui de qualquer maneira.
  • Como os Copytipos estão prontamente disponíveis como valores de propriedade, é menos comum usá-los como referência em primeiro lugar.
  • Por fim, transformar um &Aem um Aquando A: 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, fooquase certamente deve apenas aceitar value: u32e 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?
2 Sunreef Aug 18 2020 at 11:00

Com a função que você tem, você só pode usar um u32ou 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 Tporque &Tpode implementar várias Borrowcaracterísticas para diferentes tipos. Você precisa especificar explicitamente qual deseja usar como saída.

let output: usize = foo(&v);