Descompilei algumas bibliotecas C # 7 e vi ValueTuple
genéricos sendo usados. O que são ValueTuples
e por que não Tuple
?
Descompilei algumas bibliotecas C # 7 e vi ValueTuple
genéricos sendo usados. O que são ValueTuples
e por que não Tuple
?
Respostas:
O que são
ValueTuples
e por que nãoTuple
?
A ValueTuple
é uma estrutura que reflete uma tupla, igual à System.Tuple
classe original .
A principal diferença entre Tuple
e ValueTuple
sã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.ValueTuple
nã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.ValueTuple
um 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
, Item2
etc, 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 ValueTuple
ser 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 struct
obtém semântica de igualdade automática (superficial) pelo tempo de execução, enquanto um class
nã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
structs
vez declasses
, para que nenhuma penalidade de alocação seja associada a eles. Eles devem ser o mais leve possível.Indiscutivelmente,
structs
pode acabar sendo mais caro, porque a atribuição copia um valor maior. Portanto, se eles receberem muito mais do que foram criados,structs
seria 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.
Você pode ver facilmente que o trabalho System.Tuple
se 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}");
Se olharmos para a capa do exemplo anterior, podemos ver exatamente como o compilador está interpretando ValueTuple
quando 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 Item1
e 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
.
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
A diferença entre Tuple
e 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.
Eu olhei para a fonte de ambos Tuple
e ValueTuple
. A diferença é que Tuple
é um class
e ValueTuple
é um struct
que implementa IEquatable
.
Isso significa que Tuple == Tuple
retornará false
se não forem a mesma instância, mas ValueTuple == ValueTuple
retornará true
se forem do mesmo tipo e Equals
retornará true
para cada um dos valores que eles contêm.
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.Create
métodos de fábrica. Os System.ValueTuple
tipos diferem dos System.Tuple
tipos em que:
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.Tuple
você 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";
class MyNonGenericType : MyGenericType<string, ValueTuple, int>
etc.
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.
Participação tardia para adicionar um esclarecimento rápido sobre esses dois factóides:
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.