Retornando dois valores, Tupla vs 'fora' vs 'estrutura'

86

Considere uma função que retorna dois valores. Nós podemos escrever:

// Using out:
string MyFunction(string input, out int count)

// Using Tuple class:
Tuple<string, int> MyFunction(string input)

// Using struct:
MyStruct MyFunction(string input)

Qual é a melhor prática e por quê?

Xaqron
fonte
String não é um tipo de valor. Acho que você quis dizer "considere uma função que retorna dois valores".
Eric Lippert
@Eric: Você está certo. Eu quis dizer tipos imutáveis.
Xaqron
e o que há de errado com uma classe?
Lukasz Madon
1
@lukas: Nada, mas certamente não está nas melhores práticas. Este é um valor leve (<16 KB) e se eu for adicionar um código personalizado, continuarei structconforme Ericmencionado.
Xaqron
1
Eu diria que use apenas quando precisar do valor de retorno para decidir se deve processar os dados de retorno, como em TryParse, caso contrário, você deve sempre retornar um objeto estruturado, como se o objeto estruturado devesse ser do tipo valor ou uma referência o tipo depende de qual uso adicional você faz dos dados
MikeT

Respostas:

93

Cada um deles tem seus prós e contras.

Os parâmetros de saída são rápidos e baratos, mas exigem que você passe uma variável e dependa da mutação. É quase impossível usar corretamente um parâmetro out com LINQ.

Tuplas pressionam a coleta de lixo e não são autodocumentadas. "Item1" não é muito descritivo.

Estruturas personalizadas podem ser lentas para copiar se forem grandes, mas são autodocumentáveis ​​e eficientes se forem pequenas. No entanto, também é uma dor definir um monte de estruturas personalizadas para usos triviais.

Eu estaria inclinado para a solução de estrutura personalizada, todas as outras coisas sendo iguais. Ainda melhor, porém, é fazer uma função que retorne apenas um valor . Por que você está retornando dois valores em primeiro lugar?

ATUALIZAÇÃO: observe que as tuplas no C # 7, enviadas seis anos após a redação deste artigo, são tipos de valor e, portanto, menos propensos a criar pressão de coleção.

Eric Lippert
fonte
2
Retornar dois valores geralmente é um substituto para não ter tipos de opção ou ADT.
Anton Tykhyy
2
De minha experiência com outras linguagens, eu diria que geralmente tuplas são usadas para agrupamento rápido e sujo de itens. Normalmente é melhor criar uma classe ou estrutura simplesmente porque permite nomear cada item. Ao usar tuplas, os significados de cada valor podem ser difíceis de determinar. Mas isso evita que você perca tempo para criar a classe / estrutura, o que pode ser um exagero se a referida classe / estrutura não for usada em outro lugar.
Kevin Cathcart
23
@Xaqron: Se você achar que a ideia de "dados com um tempo limite" é comum em seu programa, você pode considerar fazer um tipo genérico "TimeLimited <T>" para que seu método retorne um TimeLimited <string> ou TimeLimited <Uri> ou qualquer outra coisa. A classe TimeLimited <T> pode ter métodos auxiliares que informam "quanto tempo nos resta?" ou "está expirado?" como queiras. Tente capturar semânticas interessantes como essa no sistema de tipos.
Eric Lippert
3
Com certeza, eu nunca usaria Tuple como parte da interface pública. Mas mesmo para o código 'privado', obtenho enorme legibilidade de um tipo adequado em vez de usar Tupla (especialmente como é fácil criar um tipo interno privado com Propriedades Automáticas).
SolutionYogi
2
O que significa pressão de coleta?
rola em
25

Somando-se às respostas anteriores, C # 7 traz tuplas de tipo de valor, ao contrário de System.Tupleser um tipo de referência e também oferece semântica aprimorada.

Você ainda pode deixá-los sem nome e usar a .Item*sintaxe:

(string, string, int) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.Item1; //John
person.Item2; //Doe
person.Item3;   //42

Mas o que é realmente poderoso sobre esse novo recurso é a capacidade de ter tuplas nomeadas. Portanto, poderíamos reescrever o acima assim:

(string FirstName, string LastName, int Age) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.FirstName; //John
person.LastName; //Doe
person.Age;   //42

A desestruturação também é suportada:

(string firstName, string lastName, int age) = getPerson()

Dimlucas
fonte
2
Estou correto em pensar que isso retorna basicamente uma estrutura com referências como membros sob o capô?
Austin_Anderson
4
nós sabemos como o desempenho disso se compara ao uso de parâmetros externos?
SpaceMonkey
20

Acho que a resposta depende da semântica do que a função está fazendo e da relação entre os dois valores.

Por exemplo, os TryParsemétodos usam um outparâmetro para aceitar o valor analisado e retornam a boolpara indicar se a análise foi bem-sucedida ou não. Os dois valores não pertencem realmente um ao outro, portanto, semanticamente, faz mais sentido, e a intenção do código é mais fácil de ler, usar o outparâmetro.

Se, no entanto, sua função retornar as coordenadas X / Y de algum objeto na tela, então os dois valores semanticamente pertencem um ao outro e seria melhor usar a struct.

Eu pessoalmente evito usar um tuplepara qualquer coisa que seja visível para o código externo por causa da sintaxe estranha para recuperar os membros.

Andrew Cooper
fonte
1 para semântica. Sua resposta é mais apropriada com tipos de referência quando podemos deixar o outparâmetro null. Existem alguns tipos imutáveis ​​anuláveis ​​por aí.
Xaqron
3
Na verdade, os dois valores em TryParse realmente pertencem um ao outro, muito mais do que implicado por ter um como valor de retorno e o outro como parâmetro ByRef. De muitas maneiras, a coisa lógica a retornar seria um tipo anulável. Há alguns casos em que o padrão TryParse funciona bem, e alguns em que é uma dor (é bom que ele possa ser usado em uma instrução "if", mas há muitos casos em que retornar um valor anulável ou ser capaz de especificar um valor padrão seria mais conveniente).
supercat de
@supercat, eu concordo com o andrew, eles não pertencem um ao outro. embora estejam relacionados, o retorno informa se você precisa se preocupar com o valor, não com algo que precise ser processado em conjunto. então, depois de ter processado o retorno, ele não é mais necessário para nenhum outro processamento relacionado ao valor de saída, isso é diferente de, digamos, retornar um KeyValuePair de um dicionário onde há um link claro e contínuo entre a chave e o valor. embora eu concorde que se os tipos anuláveis ​​estivessem em .Net 1.1, eles provavelmente os teriam usado como nulos seria uma maneira adequada de sinalizar nenhum valor
MikeT
@MikeT: Eu considero extremamente lamentável que a Microsoft tenha sugerido que as estruturas deveriam ser usadas apenas para coisas que representam um único valor, quando na verdade uma estrutura de campo exposto é o meio ideal para passar um grupo de variáveis ​​independentes unidas com fita adesiva . O indicador de sucesso e o valor são significativos juntos no momento em que são retornados , mesmo que depois disso eles sejam mais úteis como variáveis ​​separadas. Os campos de uma estrutura de campo exposto armazenada em uma variável podem ser usados ​​como variáveis ​​separadas. Em qualquer caso ...
supercat
@MikeT: Por causa das maneiras como a covariância é e não é suportada na estrutura, o único trypadrão que funciona com interfaces covariantes é T TryGetValue(whatever, out bool success); essa abordagem teria permitido interfaces IReadableMap<in TKey, out TValue> : IReadableMap<out TValue>e permitir que o código que deseja mapear instâncias de Animalpara instâncias de Caraceitar um Dictionary<Cat, ToyotaCar>[usando TryGetValue<TKey>(TKey key, out bool success). Essa variação não é possível se TValuefor usado como um refparâmetro.
supercat
2

Eu irei com a abordagem de usar o parâmetro Out porque na segunda abordagem você exigiria criar um objeto da classe Tuple e então agregar valor a ele, o que eu acho uma operação cara em comparação com retornar o valor do parâmetro out. No entanto, se você quiser retornar vários valores na classe Tupla (o que de fato não pode ser feito retornando apenas um parâmetro de saída), irei para a segunda abordagem.

Sumit
fonte
Eu concordo out. Além disso, há uma paramspalavra - chave que não mencionei para esclarecer a questão.
Xaqron
2

Você não mencionou mais uma opção, que é ter uma classe personalizada em vez de struct. Se os dados têm semântica associada a eles que pode ser operada por funções, ou o tamanho da instância é grande o suficiente (> 16 bytes como regra prática), uma classe personalizada pode ser preferida. O uso de "out" não é recomendado na API pública por causa de sua associação a ponteiros e requer compreensão de como os tipos de referência funcionam.

https://msdn.microsoft.com/en-us/library/ms182131.aspx

Tuple é bom para uso interno, mas o uso é estranho em API pública. Então, meu voto é entre struct e classe para API pública.

JeanP
fonte
1
Se um tipo existe puramente com o propósito de retornar uma agregação de valores, eu diria que um tipo de valor de campo exposto simples é o ajuste mais claro para essa semântica. Se não houver nada no tipo além de seus campos, não haverá qualquer dúvida sobre quais tipos de validação de dados ele realiza (nenhuma, obviamente), se representa uma visualização capturada ou ao vivo (uma estrutura de campo aberto não pode agir como uma exibição ao vivo), etc. As classes imutáveis ​​são menos convenientes de se trabalhar e só oferecem um benefício de desempenho se as instâncias puderem ser transmitidas várias vezes.
supercat
0

Não existe uma "prática recomendada". É com o que você se sente confortável e o que funciona melhor em sua situação. Contanto que você seja consistente com isso, não há problema com nenhuma das soluções que você postou.

raposa
fonte
3
Claro que todos eles funcionam. Caso não haja vantagem técnica, continuo curioso para saber o que é mais utilizado pelos experts.
Xaqron