Descompilei algumas bibliotecas C # 7 e vi ValueTuplegenéricos sendo usados. O que são ValueTuplese por que não Tuple?
Descompilei algumas bibliotecas C # 7 e vi ValueTuplegenéricos sendo usados. O que são ValueTuplese por que não Tuple?
Respostas:
O que são
ValueTuplese por que nãoTuple?
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 declasses, 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.
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}");
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.
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
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.
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.
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:
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";
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.