Qual é a diferença entre System.ValueTuple e System.Tuple?

139

Descompilei algumas bibliotecas C # 7 e vi ValueTuplegenéricos sendo usados. O que são ValueTuplese por que não Tuple?

Steve Fan
fonte
Eu acho que está se referindo à classe Dot NEt Tuple. Você poderia compartilhar um código de exemplo. Para que fosse fácil de entender.
Ranadip Dutta
14
@Ranadip Dutta: Se você sabe o que é uma tupla, não precisa de código de amostra para entender a pergunta. A pergunta em si é direta: o que é ValueTuple e como ela difere da Tuple?
BoltClock
1
@BoltClock: Essa é a razão pela qual eu não respondi nada nesse contexto. Eu sei que em c #, há uma classe Tuple que eu uso com bastante frequência e a mesma classe algumas vezes eu a chamo de PowerShell também. É um tipo de referência. Agora, vendo as outras respostas, entendi que também existe um tipo de valor conhecido como Valuetuple. Se houver uma amostra, eu gostaria de saber o uso da mesma.
Ranadip Dutta
2
Por que você os descompila quando o código fonte do Roslyn está disponível no github?
Zein Makki
@ user3185569 provavelmente porque F12 auto-decompiles coisas e é mais fácil do salto para GitHub
John Zabroski

Respostas:

203

O que são ValueTuplese por que não Tuple?

A ValueTupleé uma estrutura que reflete uma tupla, igual à System.Tupleclasse original .

A principal diferença entre Tuplee ValueTuplesão:

  • System.ValueTupleé um tipo de valor (struct), enquanto System.Tupleé um tipo de referência ( class). Isso é significativo quando se fala de alocações e pressão do GC.
  • System.ValueTuplenão é apenas um struct, é um mutável , e é preciso ter cuidado ao usá-los como tal. Pense no que acontece quando uma classe possui System.ValueTupleum campo.
  • System.ValueTuple expõe seus itens por meio de campos em vez de propriedades.

Até o C # 7, o uso de tuplas não era muito conveniente. Seus nomes de campo são Item1, Item2etc, e a linguagem não havia fornecido açúcar de sintaxe para eles, como a maioria das outras linguagens (Python, Scala).

Quando a equipe de design da linguagem .NET decidiu incorporar tuplas e adicionar açúcar de sintaxe a elas no nível da linguagem, um fator importante foi o desempenho. Por ValueTupleser um tipo de valor, você pode evitar a pressão do GC ao usá-los porque (como um detalhe de implementação) eles serão alocados na pilha.

Além disso, um structobtém semântica de igualdade automática (superficial) pelo tempo de execução, enquanto um classnão. Embora a equipe de design tenha certeza de que haverá uma igualdade ainda mais otimizada para as tuplas, a empresa implementou uma igualdade personalizada para ela.

Aqui está um parágrafo das notas de design deTuples :

Estrutura ou classe:

Como mencionado, proponho criar tipos de tupla em structsvez de classes, para que nenhuma penalidade de alocação seja associada a eles. Eles devem ser o mais leve possível.

Indiscutivelmente, structspode acabar sendo mais caro, porque a atribuição copia um valor maior. Portanto, se eles receberem muito mais do que foram criados, structsseria uma má escolha.

Na sua própria motivação, porém, as tuplas são efêmeras. Você os usaria quando as partes fossem mais importantes que o todo. Portanto, o padrão comum seria construir, retornar e desconstruí-los imediatamente. Nesta situação, as estruturas são claramente preferíveis.

As estruturas também têm vários outros benefícios, que se tornarão óbvios a seguir.

Exemplos:

Você pode ver facilmente que o trabalho System.Tuplese torna ambíguo muito rapidamente. Por exemplo, digamos que temos um método que calcula uma soma e uma contagem de a List<Int>:

public Tuple<int, int> DoStuff(IEnumerable<int> values)
{
    var sum = 0;
    var count = 0;

    foreach (var value in values) { sum += value; count++; }

    return new Tuple(sum, count);
}

No final do recebimento, acabamos com:

Tuple<int, int> result = DoStuff(Enumerable.Range(0, 10));

// What is Item1 and what is Item2?
// Which one is the sum and which is the count?
Console.WriteLine(result.Item1);
Console.WriteLine(result.Item2);

A maneira como você pode desconstruir as tuplas de valor em argumentos nomeados é o poder real do recurso:

public (int sum, int count) DoStuff(IEnumerable<int> values) 
{
    var res = (sum: 0, count: 0);
    foreach (var value in values) { res.sum += value; res.count++; }
    return res;
}

E no lado receptor:

var result = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {result.Sum}, Count: {result.Count}");

Ou:

var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {sum}, Count: {count}");

Presentes do compilador:

Se olharmos para a capa do exemplo anterior, podemos ver exatamente como o compilador está interpretando ValueTuplequando pedimos que ele desconstrua:

[return: TupleElementNames(new string[] {
    "sum",
    "count"
})]
public ValueTuple<int, int> DoStuff(IEnumerable<int> values)
{
    ValueTuple<int, int> result;
    result..ctor(0, 0);
    foreach (int current in values)
    {
        result.Item1 += current;
        result.Item2++;
    }
    return result;
}

public void Foo()
{
    ValueTuple<int, int> expr_0E = this.DoStuff(Enumerable.Range(0, 10));
    int item = expr_0E.Item1;
    int arg_1A_0 = expr_0E.Item2;
}

Internamente, o código compilado utiliza Item1e Item2, mas tudo isso é abstraído de nós, pois trabalhamos com uma tupla decomposta. Uma tupla com argumentos nomeados é anotada com o TupleElementNamesAttribute. Se usarmos uma única variável nova em vez de decompor, obteremos:

public void Foo()
{
    ValueTuple<int, int> valueTuple = this.DoStuff(Enumerable.Range(0, 10));
    Console.WriteLine(string.Format("Sum: {0}, Count: {1})", valueTuple.Item1, valueTuple.Item2));
}

Note que o compilador ainda tem que fazer alguma mágica acontecer (por meio do atributo) quando depurar nossa aplicação, como seria estranho ver Item1, Item2.

Yuval Itzchakov
fonte
1
Observe que você também pode usar a sintaxe mais simples (e na minha opinião, preferível).var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Abion47
@ Abion47 O que aconteceria se os dois tipos diferissem?
Yuval Itzchakov
Além disso, como os seus pontos "é uma estrutura mutável " e "expõe campos somente leitura " concordam?
CodesInChaos
@CodesInChaos Não. Eu vi [isso] ( github.com/dotnet/corefx/blob/master/src/Common/src/System/… ), mas acho que não é isso que acaba sendo emitido pelo compilador, pois os campos locais não podem ser somente leitura de qualquer maneira. Acho que a proposta significava "você pode fazê-los somente leitura, se desejar, mas isso depende de você" , o que eu interpretei mal.
Yuval Itzchakov
1
Algumas lêndeas: "elas serão alocadas na pilha" - verdade apenas para variáveis ​​locais. Sem dúvida, você sabe disso, mas, infelizmente, a maneira como você expressou isso provavelmente perpetua o mito de que os tipos de valor sempre vivem na pilha.
precisa saber é o seguinte
26

A diferença entre Tuplee ValueTupleé que Tupleé um tipo de referência e ValueTupleé um tipo de valor. O último é desejável, pois as alterações no idioma no C # 7 usam tuplas com muito mais freqüência, mas alocar um novo objeto no heap para cada tupla é uma preocupação de desempenho, principalmente quando é desnecessário.

No entanto, no C # 7, a idéia é que você nunca precise usar explicitamente nenhum desses tipos, porque o açúcar da sintaxe foi adicionado para o uso da tupla. Por exemplo, em C # 6, se você quiser usar uma tupla para retornar um valor, faça o seguinte:

public Tuple<string, int> GetValues()
{
    // ...
    return new Tuple(stringVal, intVal);
}

var value = GetValues();
string s = value.Item1; 

No entanto, no C # 7, você pode usar isso:

public (string, int) GetValues()
{
    // ...
    return (stringVal, intVal);
}

var value = GetValues();
string s = value.Item1; 

Você pode dar um passo adiante e fornecer os nomes dos valores:

public (string S, int I) GetValues()
{
    // ...
    return (stringVal, intVal);
}

var value = GetValues();
string s = value.S; 

... Ou desconstrua totalmente a tupla:

public (string S, int I) GetValues()
{
    // ...
    return (stringVal, intVal);
}

var (S, I) = GetValues();
string s = S;

As tuplas não eram frequentemente usadas no C # pré-7 porque eram complicadas e detalhadas, e apenas realmente usadas nos casos em que a construção de uma classe / estrutura de dados para apenas uma única instância do trabalho seria mais problemática do que valeria a pena. Mas no C # 7, as tuplas agora têm suporte no nível do idioma, portanto, usá-las é muito mais limpa e mais útil.

Abion47
fonte
10

Eu olhei para a fonte de ambos Tuplee ValueTuple. A diferença é que Tupleé um classe ValueTupleé um structque implementa IEquatable.

Isso significa que Tuple == Tupleretornará falsese não forem a mesma instância, mas ValueTuple == ValueTupleretornará truese forem do mesmo tipo e Equalsretornará truepara cada um dos valores que eles contêm.

Peter Morris
fonte
É mais do que isso, no entanto.
BoltClock
2
@BoltClock O seu comentário seria construtivo se estivesse a elaborar
Peter Morris
3
Além disso, os tipos de valor não necessariamente vão para a pilha. A diferença é que a semântica representa o valor, e não uma referência, sempre que a variável é armazenada, que pode ou não ser a pilha.
Servy
6

Outras respostas se esqueceram de mencionar pontos importantes.Em vez de reformular, vou fazer referência à documentação XML do código-fonte :

Os tipos ValueTuple (da aridade de 0 a 8) compreendem a implementação do tempo de execução subjacente às tuplas em C # e as tuplas de estrutura em F #.

Além de criados através da sintaxe da linguagem , eles são criados com mais facilidade pelos ValueTuple.Createmétodos de fábrica. Os System.ValueTupletipos diferem dos System.Tupletipos em que:

  • são estruturas e não classes,
  • eles são mutáveis, e não somente de leitura , e
  • seus membros (como Item1, Item2, etc) são campos em vez de propriedades.

Com a introdução desse tipo e do compilador C # 7.0, você pode escrever facilmente

(int, string) idAndName = (1, "John");

E retorne dois valores de um método:

private (int, string) GetIdAndName()
{
   //.....
   return (id, name);
}

Ao contrário de System.Tuplevocê pode atualizar seus membros (Mutable) porque são campos públicos de leitura e gravação que podem receber nomes significativos:

(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";
Zein Makki
fonte
"Aridade 0 a 8". Ah, eu gosto do fato de que eles incluem uma tupla 0. Poderia ser usado como um tipo de tipo vazio e será permitido em genéricos quando algum parâmetro de tipo não for necessário, como em class MyNonGenericType : MyGenericType<string, ValueTuple, int>etc.
Jeppe Stig Nielsen -
6

Além dos comentários acima, um problema infeliz do ValueTuple é que, como um tipo de valor, os argumentos nomeados são apagados quando compilados no IL, para que não estejam disponíveis para serialização no tempo de execução.

Seus argumentos nomeados ainda serão finalizados como "Item1", "Item2" etc., quando serializados via, por exemplo, Json.NET.

ZenSquirrel
fonte
2
Então, tecnicamente isso é uma semelhança e não uma diferença;)
JAD
2

Participação tardia para adicionar um esclarecimento rápido sobre esses dois factóides:

  • eles são estruturas e não classes
  • eles são mutáveis ​​ao invés de somente leitura

Alguém poderia pensar que mudar em massa as tuplas de valor seria simples:

 foreach (var x in listOfValueTuples) { x.Foo = 103; } // wont even compile because x is a value (struct) not a variable

 var d = listOfValueTuples[0].Foo;

Alguém pode tentar solucionar isso assim:

 // initially *.Foo = 10 for all items
 listOfValueTuples.Select(x => x.Foo = 103);

 var d = listOfValueTuples[0].Foo; // 'd' should be 103 right? wrong! it is '10'

A razão para esse comportamento peculiar é que as tuplas de valor são exatamente baseadas em valor (estruturas) e, portanto, a chamada .Select (...) funciona em estruturas clonadas e não nos originais. Para resolver isso, devemos recorrer a:

 // initially *.Foo = 10 for all items
 listOfValueTuples = listOfValueTuples
     .Select(x => {
         x.Foo = 103;
         return x;
     })
     .ToList();

 var d = listOfValueTuples[0].Foo; // 'd' is now 103 indeed

Como alternativa, é claro que se pode tentar a abordagem direta:

   for (var i = 0; i < listOfValueTuples.Length; i++) {
        listOfValueTuples[i].Foo = 103; //this works just fine

        // another alternative approach:
        //
        // var x = listOfValueTuples[i];
        // x.Foo = 103;
        // listOfValueTuples[i] = x; //<-- vital for this alternative approach to work   if you omit this changes wont be saved to the original list
   }

   var d = listOfValueTuples[0].Foo; // 'd' is now 103 indeed

Espero que isso ajude alguém que luta para fazer cara de coroa fora das tuplas de valor hospedadas na lista.

XDS
fonte