O uso de tuplas do .NET 4.0 no meu código C # é uma péssima decisão de design?

166

Com a adição da classe Tuple no .net 4, tenho tentado decidir se usá-los em meu design é uma má escolha ou não. Do meu ponto de vista, uma tupla pode ser um atalho para escrever uma classe de resultado (tenho certeza de que há outros usos também).

Então, é isso:

public class ResultType
{
    public string StringValue { get; set; }
    public int IntValue { get; set; }
}

public ResultType GetAClassedValue()
{
    //..Do Some Stuff
    ResultType result = new ResultType { StringValue = "A String", IntValue = 2 };
    return result;
}

É equivalente a isso:

public Tuple<string, int> GetATupledValue()
{
    //...Do Some stuff
    Tuple<string, int> result = new Tuple<string, int>("A String", 2);
    return result;
}

Então, deixando de lado a possibilidade de eu estar perdendo o ponto das Tuplas, o exemplo de uma Tupla é uma má escolha de design? Para mim, parece menos confuso, mas não tão auto-documentável e limpo. Isso significa que, com o tipo ResultType, fica muito claro mais tarde o que cada parte da classe significa, mas você tem um código extra para manter. Com o, Tuple<string, int>você precisará procurar e descobrir o que cada um Itemrepresenta, mas você escreve e mantém menos código.

Qualquer experiência que você teve com essa escolha seria muito apreciada.

Jason Webb
fonte
16
@Boltbait: porque tupla é da teoria dos conjuntos pt.wikipedia.org/wiki/Tuple
Matthew Whited
16
@BoltBait: Uma "tupla" é um conjunto ordenado de valores e não necessariamente tem nada a ver com linhas em um banco de dados.
FrustratedWithFormsDesigner
19
Tuplas em c # seria ainda melhor se houvesse alguma boa tupla ação desembalar :)
Justin
4
@ John Saunders Minha pergunta é especificamente sobre decisões de design em c #. Como não uso outros idiomas .net, não tenho certeza de como as tuplas os afetam. Então eu marquei c #. Pareceu-me razoável, mas a mudança para .net também está bem, eu acho.
Jason Webb
13
@ John Saunders: Eu tenho certeza que ele está perguntando sobre as decisões de design relacionadas ao uso ou não do Tuples em um aplicativo escrito exclusivamente em C #. Não acredito que ele esteja perguntando sobre as decisões de design que foram usadas para criar a linguagem C #.
Brett Widmeier

Respostas:

163

As tuplas são ótimas se você controla a criação e o uso delas - é possível manter o contexto, essencial para entendê-las.

Em uma API pública, no entanto, eles são menos eficazes. O consumidor (não você) precisa adivinhar ou procurar documentação, especialmente para coisas como Tuple<int, int>.

Eu os usaria para membros privados / internos, mas usaria classes de resultado para membros públicos / protegidos.

Esta resposta também tem algumas informações.

Bryan Watts
fonte
19
A diferença entre público / privado é realmente importante, obrigado por mencioná-lo. Retornar tuplas em sua API pública é uma péssima idéia, mas internamente, onde as coisas podem ser fortemente acopladas (mas tudo bem ), tudo bem.
Clinton Pierce
36
Bem acoplado ou bem acoplado? ;)
contactmatt
A documentação apropriada não pode ser útil aqui? Por exemplo, o Scala StringOps(basicamente uma classe de "métodos de extensão" para Java String)) possui um método parition(Char => Boolean): (String, String)(pega no predicado, retorna uma tupla de 2 strings). É óbvio qual é a saída (obviamente, um será os caracteres para os quais o predicado era verdadeiro e o outro para o qual era falso, mas não está claro qual é qual). É a documentação que esclarece isso (e a correspondência de padrões pode ser usada para deixar claro no uso qual é qual).
Kat
@ Mike: Isso faz com que seja difícil de acertar e fácil de errar, a marca de uma API que irá causar dor para alguém em algum lugar :-)
Bryan Watts
79

Na minha opinião, uma tupla é um atalho para escrever uma classe de resultado (tenho certeza de que há outros usos também).

De fato, existem outros usos valiosos paraTuple<> - a maioria deles envolve abstrair a semântica de um grupo específico de tipos que compartilham uma estrutura semelhante e tratá-los simplesmente como um conjunto ordenado de valores. Em todos os casos, um benefício das tuplas é que elas evitam sobrecarregar seu espaço de nomes com classes somente de dados que expõem propriedades, mas não métodos.

Aqui está um exemplo de uso razoável para Tuple<>:

var opponents = new Tuple<Player,Player>( playerBob, playerSam );

No exemplo acima, queremos representar um par de oponentes, uma tupla é uma maneira conveniente de emparelhar essas instâncias sem precisar criar uma nova classe. Aqui está outro exemplo:

var pokerHand = Tuple.Create( card1, card2, card3, card4, card5 );

Uma mão de pôquer pode ser considerada apenas um conjunto de cartas - e a tupla (pode ser) uma maneira razoável de expressar esse conceito.

deixando de lado a possibilidade de que estou perdendo o ponto das tuplas, o exemplo de uma tupla é uma má escolha de design?

Retornar Tuple<>instâncias fortemente tipadas como parte de uma API pública para um tipo público raramente é uma boa idéia. Como você mesmo reconhece, as tuplas exigem que as partes envolvidas (autor da biblioteca, usuário da biblioteca) concordem com antecedência sobre o objetivo e a interpretação dos tipos de tupla que estão sendo usados. É bastante desafiador criar APIs intuitivas e claras, o uso Tuple<>público apenas obscurece a intenção e o comportamento da API.

Tipos anônimos também são uma espécie de tupla - no entanto, são fortemente tipados e permitem especificar nomes claros e informativos para as propriedades pertencentes ao tipo. Mas tipos anônimos são difíceis de usar em diferentes métodos - eles foram adicionados principalmente para dar suporte a tecnologias como LINQ, em que as projeções produziriam tipos aos quais normalmente não queremos atribuir nomes. (Sim, eu sei que tipos anônimos com os mesmos tipos e propriedades nomeadas são consolidados pelo compilador).

Minha regra geral é: se você o devolverá da sua interface pública - torne-o um tipo nomeado .

Minha outra regra prática para usar tuplas é: argumentos do método name e variáveis ​​localc do tipo o Tuple<>mais claramente possível - faça o nome representar o significado dos relacionamentos entre os elementos da tupla. Pense no meu var opponents = ...exemplo.

Aqui está um exemplo de um caso do mundo real em que eu costumava Tuple<>evitar declarar um tipo somente de dados para uso somente dentro do meu próprio assembly . A situação envolve o fato de que, ao usar dicionários genéricos contendo tipos anônimos, torna-se difícil usar o TryGetValue()método para localizar itens no dicionário porque o método requer um outparâmetro que não pode ser nomeado:

public static class DictionaryExt 
{
    // helper method that allows compiler to provide type inference
    // when attempting to locate optionally existent items in a dictionary
    public static Tuple<TValue,bool> Find<TKey,TValue>( 
        this IDictionary<TKey,TValue> dict, TKey keyToFind ) 
    {
        TValue foundValue = default(TValue);
        bool wasFound = dict.TryGetValue( keyToFind, out foundValue );
        return Tuple.Create( foundValue, wasFound );
    }
}

public class Program
{
    public static void Main()
    {
        var people = new[] { new { LastName = "Smith", FirstName = "Joe" },
                             new { LastName = "Sanders", FirstName = "Bob" } };

        var peopleDict = people.ToDictionary( d => d.LastName );

        // ??? foundItem <= what type would you put here?
        // peopleDict.TryGetValue( "Smith", out ??? );

        // so instead, we use our Find() extension:
        var result = peopleDict.Find( "Smith" );
        if( result.First )
        {
            Console.WriteLine( result.Second );
        }
    }
}

PS Existe outra maneira (mais simples) de contornar os problemas decorrentes de tipos anônimos nos dicionários, e é usar a varpalavra-chave para permitir que o compilador 'deduza' o tipo para você. Aqui está essa versão:

var foundItem = peopleDict.FirstOrDefault().Value;
if( peopleDict.TryGetValue( "Smith", out foundItem ) )
{
   // use foundItem...
}
LBushkin
fonte
5
@stakx: Sim, eu concordo. Mas lembre-se - o exemplo é amplamente utilizado para ilustrar casos em que o uso de um Tuple<> poderia fazer sentido. Há sempre alternativas ...
LBushkin
1
Penso que substituir parâmetros por uma Tupla, como em TryGetValue ou TryParse, é um dos poucos casos em que pode fazer sentido que uma API pública retorne uma Tupla. De fato, pelo menos um idioma .NET faz essa alteração de API automaticamente para você.
Joel Mueller
1
@stakx: Tuplas também são imutáveis, enquanto matrizes não.
Robert Paulson
2
Entendi que as Tuplas são úteis apenas como objetos imutáveis ​​para jogadores de pôquer.
Gennady Vanin
2
+1 Ótima resposta - eu amo seus dois exemplos iniciais que realmente mudaram de idéia um pouco. Eu nunca vi exemplos antes em que uma tupla era pelo menos tão apropriada quanto uma estrutura ou tipo personalizado. No entanto, seu último "exemplo da vida real" para mim parece não agregar muito mais valor. Os dois primeiros exemplos são perfeitos e simples. O último é um pouco difícil de entender e seu tipo "PS" o torna inútil, pois sua abordagem alternativa é muito mais simples e não requer tuplas.
Chiccodoro
18

Tuplas podem ser úteis ... mas também podem ser uma dor depois. Se você possui um método que retorna Tuple<int,string,string,int>como você sabe quais são esses valores posteriormente. Eles ID, FirstName, LastName, Ageeram ou eram eles UnitNumber, Street, City, ZipCode.

Matthew Whited
fonte
9

As tuplas são uma adição bastante abaixo do esperado para o CLR da perspectiva de um programador de C #. Se você possui uma coleção de itens que variam em tamanho, não é necessário que eles tenham nomes estáticos exclusivos no momento da compilação.

Mas se você tiver uma coleção de comprimento constante, isso implica que cada local fixo na coleção possui um significado predefinido específico. E é sempre melhor dar-lhes nomes estáticos apropriados, nesse caso, ao invés de ter que lembrar o significado Item1, Item2etc.

As classes anônimas em C # já fornecem uma excelente solução para o uso privado mais comum de tuplas, e elas dão nomes significativos aos itens, portanto são realmente superiores nesse sentido. O único problema é que eles não podem vazar dos métodos nomeados. Eu preferiria ver essa restrição levantada (talvez apenas para métodos privados) do que ter suporte específico para tuplas em C #:

private var GetDesserts()
{
    return _icecreams.Select(
        i => new { icecream = i, topping = new Topping(i) }
    );
}

public void Eat()
{
    foreach (var dessert in GetDesserts())
    {
        dessert.icecream.AddTopping(dessert.topping);
        dessert.Eat();
    }
}
Daniel Earwicker
fonte
Como uma característica geral, o que eu gostaria de ver seria o .net para definir um padrão para alguns tipos especiais de tipos anônimos, como, por exemplo struct var SimpleStruct {int x, int y;}, definir uma estrutura com um nome que comece com um guia associado a estruturas simples e tem algo como {System.Int16 x}{System.Int16 y}anexado; qualquer nome começando com esse GUID seria necessário para representar uma estrutura contendo apenas esses elementos e conteúdo específico especificado; se várias definições para o mesmo nome estiverem no escopo e corresponderem, todas serão consideradas do mesmo tipo.
supercat
Isso seria útil para alguns tipos de tipos, incluindo classes e estruturas mutáveis ​​e imutáveis, bem como interfaces e delegados para uso em situações em que a estrutura de correspondência deve ser considerada como semântica de correspondência. Embora certamente haja razões pelas quais pode ser útil ter dois tipos de delegados com os mesmos parâmetros não considerados intercambiáveis, também seria útil se alguém pudesse especificar que um método deseja um delegado que use alguma combinação de parâmetros, mas além disso não não importa que tipo de delegado é.
supercat
É lamentável que, se duas pessoas diferentes quiserem escrever funções que tomam como delegados de entrada que precisam de um único refparâmetro, a única maneira possível de ter um delegado que pode ser passado para ambas as funções será se a pessoa produzir e compilar um tipo de delegado e dá para o outro construir contra. Não há como as duas pessoas indicarem que os dois tipos devem ser considerados sinônimos.
supercat
Você tem exemplos mais concretos de onde você usaria classes anônimas Tuple? Acabei de percorrer 50 lugares da minha base de código onde estou usando tuplas, e todos são valores de retorno (normalmente yield return) ou são elementos de coleções que são usadas em outros lugares, portanto, não vá para classes anônimas.
2
@ JonofAllTrades - as opiniões provavelmente variam nisso, mas eu tento criar os métodos públicos como se fossem usados ​​por outra pessoa, mesmo que essa pessoa seja eu, por isso estou prestando um bom atendimento ao cliente! Você costuma chamar uma função mais vezes do que a escreve. :)
Daniel Earwicker 4/13/13
8

Semelhante à palavra-chave var, destina-se como uma conveniência - mas é facilmente abusada.

Na minha humilde opinião, não exponha Tuplecomo uma classe de retorno. Use-o em particular, se a estrutura de dados de um serviço ou componente exigir, mas retorne classes conhecidas e bem formadas a partir de métodos públicos.

// one possible use of tuple within a private context. would never
// return an opaque non-descript instance as a result, but useful
// when scope is known [ie private] and implementation intimacy is
// expected
public class WorkflowHost
{
    // a map of uri's to a workflow service definition 
    // and workflow service instance. By convention, first
    // element of tuple is definition, second element is
    // instance
    private Dictionary<Uri, Tuple<WorkflowService, WorkflowServiceHost>> _map = 
        new Dictionary<Uri, Tuple<WorkflowService, WorkflowServiceHost>> ();
}
johnny g
fonte
2
As principais linguagens "RAD" (Java é o melhor exemplo) sempre escolheram a segurança e evitar se atirar nos cenários do tipo pé. Fico feliz que a Microsoft esteja se arriscando com C # e dando ao idioma um bom poder, mesmo que possa ser mal utilizado.
Matt Greer
4
A palavra-chave var não é possível abusar. Talvez você quis dizer dinâmico?
22810 Chris Marisic
@ Chris Marisic, só para esclarecer que eu não quis dizer no mesmo cenário - você está absolutamente correto, varnão pode ser devolvido ou existir fora do escopo declarado. no entanto, ainda é possível usá-lo em excesso de sua finalidade e ser "abusado"
g johnny
5
Eu continuo afirmando que o período var não pode ser abusado. É apenas uma palavra-chave para fazer uma definição de variável mais curta. Talvez seu argumento seja sobre tipos anônimos, mas mesmo aqueles que realmente não podem ser abusados ​​porque ainda são tipos vinculados estaticamente normais, agora a dinâmica é uma história bem diferente.
Chris Marisic
4
var pode ser abusado no sentido de que, se for usado em excesso, às vezes pode causar problemas para a pessoa que está lendo o código. Especialmente se você não estiver no Visual Studio e não tiver inteligência.
Matt Greer
5

Usar uma classe como ResultTypeé mais clara. Você pode dar nomes significativos aos campos da classe (enquanto que com uma tupla eles seriam chamados Item1e Item2). Isso é ainda mais importante se os tipos dos dois campos forem iguais: o nome distingue claramente entre eles.

Richard Fearn
fonte
Uma pequena vantagem adicional: você pode reordenar com segurança os parâmetros de uma classe. Se você fizer uma tupla, o código será quebrado e, se os campos tiverem tipos semelhantes, isso poderá não ser detectável no momento da compilação.
Concordo que as tuplas são usadas quando um desenvolvedor não tem energia para pensar em um nome de classe significativo agrupando nomes de propriedades significativos. A programação é sobre comunicação. As tuplas são um antipadrão para a comunicação. Eu teria preferido que a Microsoft deixasse de fora da linguagem para forçar os desenvolvedores a tomar boas decisões de design.
Mike gold
5

Que tal usar Tuplas em um padrão de decorar, classificar ou não decoração? (Transformação Schwartziana para o povo Perl). Aqui está um exemplo artificial, com certeza, mas as Tuplas parecem ser uma boa maneira de lidar com esse tipo de coisa:

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string[] files = Directory.GetFiles("C:\\Windows")
                    .Select(x => new Tuple<string, string>(x, FirstLine(x)))
                    .OrderBy(x => x.Item2)
                    .Select(x => x.Item1).ToArray();
        }
        static string FirstLine(string path)
        {
            using (TextReader tr = new StreamReader(
                        File.Open(path, FileMode.Open)))
            {
                return tr.ReadLine();
            }
        }
    }
}

Agora, eu poderia ter usado um Object [] de dois elementos ou, neste exemplo específico, uma string [] de dois elementos. O ponto é que eu poderia ter usado qualquer coisa como o segundo elemento em uma tupla usada internamente e é muito fácil de ler.

Clinton Pierce
fonte
3
Você pode usar Tuple.Create em vez de construtor. É mais curto.
Kugel
um tipo anônimo será muito mais legível; como faria usando nomes de variáveis reais, em vez de 'x'
Dave Cousineau
Seria agora . Esse é um problema de SO, as respostas postadas anos e anos atrás nunca são limpas ou atualizadas para novas tecnologias.
Clinton Pierce
4

Na OMI, essas "tuplas" são basicamente todos os structtipos anônimos de acesso público com membros sem nome .

O único lugar que eu usaria a tupla é quando você precisa reunir rapidamente alguns dados, em um escopo muito limitado. A semântica dos dados deve ser óbvia , portanto, o código não é difícil de ler. Portanto, usar uma tupla ( int, int) para (linha, col) parece razoável. Mas tenho dificuldade em encontrar uma vantagem sobre um structmembro nomeado (para que nenhum erro seja cometido e a linha / coluna não seja acidentalmente trocada)

Se você estiver enviando dados de volta para o chamador ou aceitando dados de um chamador, realmente deve usar um structcom membros nomeados.

Veja um exemplo simples:

struct Color{ float r,g,b,a ; }
public void setColor( Color color )
{
}

A versão da tupla

public void setColor( Tuple<float,float,float,float> color )
{
  // why?
}

Não vejo nenhuma vantagem em usar tupla no lugar de uma estrutura com membros nomeados. O uso de membros sem nome é um passo atrás para a legibilidade e a compreensão do seu código.

Tuple me parece uma maneira preguiçosa de evitar a criação de um structmembro com nome real. O uso excessivo de tupla, onde você realmente sente que você / ou alguém que encontra seu código precisaria de membros nomeados é A Bad Thing ™, se eu já vi um.

bobobobo
fonte
3

Não me julgue, eu não sou especialista, mas com as novas Tuplas no C # 7.x agora, você pode retornar algo como:

return (string Name1, int Name2)

Pelo menos agora você pode nomear e os desenvolvedores podem ver algumas informações.

Denis Gordin
fonte
2

Depende, é claro! Como você disse, uma tupla pode economizar seu código e tempo quando você deseja agrupar alguns itens para consumo local. Você também pode usá-los para criar algoritmos de processamento mais genéricos do que se passar uma classe concreta. Não me lembro quantas vezes desejei ter algo além do KeyValuePair ou DataRow para passar rapidamente uma data de um método para outro.

Por outro lado, é bem possível exagerar e passar por tuplas, onde você só consegue adivinhar o que elas contêm. Se você usar uma tupla entre as classes, talvez seja melhor criar uma classe concreta.

Usadas com moderação, é claro, as tuplas podem levar a um código mais conciso e legível. Você pode procurar em C ++, STL e Boost exemplos de como as Tuplas são usadas em outros idiomas, mas no final, todos precisaremos experimentar para descobrir como eles se encaixam melhor no ambiente .NET.

Panagiotis Kanavos
fonte
1

Tuplas são um recurso inútil da estrutura no .NET 4. Acho que uma grande oportunidade foi perdida no C # 4.0. Eu adoraria ter tuplas com membros nomeados, para que você pudesse acessar os vários campos de uma tupla por nome, em vez de Valor1 , Valor2 , etc ...

Isso exigiria uma alteração de idioma (sintaxe), mas teria sido muito útil.

Philippe Leybaert
fonte
Como as tuplas são um "recurso de idioma"? É uma classe disponível para todos os idiomas .net, não é?
Jason Webb
11
Relativamente inútil para C #, talvez. Mas o System.Tuple não foi adicionado à estrutura por causa do C #. Foi adicionado para facilitar a interoperabilidade entre o F # e outros idiomas.
Joel Mueller
@ Jason: Eu não disse que era um recurso de linguagem (eu fiz isso errado, mas o corrigi antes de você fazer esse comentário).
Philippe Leybaert
2
@ Jason - idiomas com suporte direto para tuplas têm suporte implícito para desconstruir tuplas em seus valores de componentes. O código escrito nesses idiomas geralmente nunca acessa as propriedades Item1, Item2. let success, value = MethodThatReturnsATuple()
Joel Mueller
@ Joel: esta pergunta está marcada como C #, então é de alguma forma sobre C #. Eu ainda acho que eles poderiam transformá-lo em um recurso de primeira classe no idioma.
Philippe Leybaert
1

Eu pessoalmente nunca usaria uma Tupla como um tipo de retorno, porque não há indicação do que os valores representam. As tuplas têm alguns usos valiosos porque, diferentemente dos objetos, são tipos de valor e, portanto, entendem a igualdade. Por esse motivo, utilizarei-as como chaves de dicionário se precisar de uma chave de várias partes ou como chave de uma cláusula GroupBy se desejar agrupar por várias variáveis ​​e não desejar agrupamentos aninhados (Quem nunca deseja agrupamentos aninhados?). Para superar o problema com extrema verbosidade, você pode criá-los com um método auxiliar. Lembre-se de que você está acessando membros com frequência (por meio do Item1, Item2, etc), provavelmente deve usar uma construção diferente, como uma estrutura ou uma classe anônima.

Seth Paulson
fonte
0

Eu usei tuplas, tanto a Tuplequanto a novaValueTuple , em vários cenários diferentes e cheguei à seguinte conclusão: não use .

Toda vez, eu encontrava os seguintes problemas:

  • o código ficou ilegível devido à falta de nomes fortes;
  • incapaz de usar recursos da hierarquia de classes, como DTO da classe base e DTOs da classe filho, etc .;
  • se eles são usados ​​em mais de um lugar, você acaba copiando e colando essas definições feias, em vez de um nome de classe limpo.

Minha opinião é que as tuplas são um detrimento, não um recurso, do C #.

Eu tenho críticas um pouco parecidas, mas muito menos duras, sobre Func<>e Action<>. Isso é útil em muitas situações, especialmente as simples Actione as Func<type>variantes, mas, além disso, descobri que criar um tipo de delegado é mais elegante, legível, sustentável e oferece mais recursos, como ref/ outparâmetros.

Mr. TA
fonte
Mencionei tuplas nomeadas - ValueTuple- e também não gosto delas. Primeiro, a nomeação não é forte, é mais uma sugestão, muito fácil de estragar, porque pode ser atribuída implicitamente a um Tupleou ValueTuplecom o mesmo número e tipos de itens. Segundo, a falta de herança e a necessidade de copiar e colar toda a definição de um lugar para outro ainda são desvantagens.
Sr. TA
Eu concordo com esses pontos. Tento usar as Tuplas apenas quando controlo o produtor e o consumidor e se elas não estão muito "distantes". Significado, produtor e consumidor no mesmo método = 'fechar', enquanto produtor e consumidor em diferentes conjuntos = 'anos-luz de distância'. Então, se eu criar tuplas no começo de um método, e usá-las no final? Claro, eu estou bem com isso. Também documento os termos e tal. Mas sim, concordo que geralmente não são a escolha certa.
9788 Mike