Подтвердить что ты не робот

Какая разница между System.ValueTuple и System.Tuple?

Я декомпилировал некоторые библиотеки С# 7 и увидел ValueTuple generics. Что такое ValueTuples и почему не Tuple вместо этого?

4b9b3361

Ответ 1

Что такое ValueTuples и почему бы не Tuple?

ValueTuple - это структура, которая отражает кортеж, так же, как и исходный класс System.Tuple.

Основное различие между Tuple и ValueTuple:

  • System.ValueTuple - это тип значения (структура), а System.Tuple - это ссылочный тип (class). Это имеет смысл, когда речь идет о распределении ресурсов и давлении GC.
  • System.ValueTuple - это не только struct, но и изменяемая struct, поэтому при использовании их как таковой следует соблюдать осторожность. Подумайте, что происходит, когда класс содержит System.ValueTuple в качестве поля.
  • System.ValueTuple предоставляет свои элементы через поля вместо свойств.

До С# 7 использование кортежей было не очень удобно. Их имена полей - Item1, Item2 и т.д., И язык не предоставил им синтаксический сахар, как это делают большинство других языков (Python, Scala).

Когда команда разработчиков языка .NET решила включить кортежи и добавить к ним синтаксический сахар на уровне языка, важным фактором была производительность. Поскольку ValueTuple является типом значения, вы можете избежать давления GC при их использовании, поскольку (в качестве подробностей реализации) они будут размещены в стеке.

Кроме того, struct получает автоматическую (поверхностную) семантику равенства во время выполнения, а class - нет. Хотя команда разработчиков позаботилась о том, чтобы для кортежей было еще более оптимизированное равенство, следовательно, реализовала собственное равенство для него.

Вот параграф из заметок о дизайне Tuples:

Структура или класс:

Как уже упоминалось, я предлагаю делать structs типов кортежей, а не classes, чтобы с ними не связывали штраф за распределение. Они должны быть максимально легкими.

Возможно, structs могут оказаться более дорогостоящими, потому что назначение копирует большее значение. Так что, если им назначено намного больше, чем они созданы, то structs будут плохим выбором.

Однако по самой своей мотивации кортежи эфемерны. Вы будете использовать их, когда части важнее целого. Таким образом, общей схемой будет создание, возврат и немедленная деконструкция. В этой ситуации структуры явно предпочтительнее.

Структуры также имеют ряд других преимуществ, которые станут очевидными в следующем.

Примеры:

Вы можете легко увидеть, что работа с System.Tuple очень быстро становится неоднозначной. Например, скажем, у нас есть метод, который вычисляет сумму и количество 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);
}

На приемном конце мы получаем:

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);

Способ, которым вы можете деконструировать кортежи значений в именованные аргументы, является реальной силой функции:

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;
}

И на приемном конце:

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

Или же:

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

Полезности компилятора:

Если мы посмотрим под крышку нашего предыдущего примера, то увидим, как именно компилятор интерпретирует ValueTuple когда мы просим его деконструировать:

[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;
}

Внутренне, скомпилированный код использует Item1 и Item2, но все это абстрагировано от нас, поскольку мы работаем с декомпозированным кортежем. Кортеж с именованными аргументами аннотируется TupleElementNamesAttribute. Если вместо разложения мы используем одну свежую переменную, мы получим:

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));
}

Обратите внимание, что при отладке нашего приложения компилятор все равно должен совершить какое-то волшебство (через атрибут), так как было бы странно видеть Item1, Item2.

Ответ 2

Разница между Tuple и ValueTuple заключается в том, что Tuple является ссылочным типом, а ValueTuple - тип значения. Последнее желательно, потому что изменения в языке на С# 7 имеют кортежи, которые используются гораздо чаще, но выделение нового объекта в куче для каждого кортежа является проблемой производительности, особенно когда это необязательно.

Однако в С# 7 идея состоит в том, что вам никогда не нужно явно использовать любой тип из-за добавления синтаксического сахара для использования кортежа. Например, в С# 6, если вы хотите использовать кортеж для возврата значения, вам нужно будет сделать следующее:

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

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

Однако в С# 7 вы можете использовать это:

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

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

Вы даже можете пойти дальше и дать имена значений:

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

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

... или полностью разрушить кортеж:

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

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

Кортежи не часто использовались в С# pre-7, потому что они были громоздкими и многословными и использовались только в тех случаях, когда построение класса данных/структуры только для одного экземпляра работы было бы более трудным, чем того стоило. Но в С# 7 кортежи теперь поддерживают языковой уровень, поэтому их использование намного чище и полезно.

Ответ 3

Я посмотрел на источник как Tuple, так и ValueTuple. Разница в том, что Tuple является class, а ValueTuple является struct, который реализует IEquatable.

Это означает, что Tuple == Tuple вернет false, если они не являются одним и тем же экземпляром, но ValueTuple == ValueTuple вернет true, если они одного типа, а Equals возвращает true для каждого из значения, которые они содержат.

Ответ 4

Другие ответы забыли упомянуть важные моменты. Вместо того, чтобы перефразировать, я буду ссылаться на документацию XML из исходного кода:

Типы ValueTuple (от 0 до 8) включают реализацию времени выполнения, которая лежит в основе кортежей в С# и структурных кортежей в F #.

Помимо созданного с помощью синтаксиса языка, их проще всего создавать с помощью фабричных методов ValueTuple.Create. Типы System.ValueTuple отличаются от типов System.Tuple тем, что:

  • они скорее структуры, чем классы,
  • они изменчивы, а не только для чтения, и
  • их члены (такие как Item1, Item2 и т.д.) являются полями, а не свойствами.

С введением этого типа и компилятора С# 7.0 вы можете легко написать

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

И вернуть два значения из метода:

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

В отличие от System.Tuple вы можете обновлять его члены (изменяемые), потому что они являются открытыми полями для чтения и записи, которым можно дать значимые имена:

(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";

Ответ 5

В дополнение к комментариям выше, один неудачный getcha ValueTuple заключается в том, что в качестве типа значения именованные аргументы стираются при компиляции в IL, поэтому они не доступны для сериализации во время выполнения.

то есть. Ваши сладкие именованные аргументы по-прежнему будут отображаться как "Item1", "Item2" и т.д. При сериализации через, например, Json.NET.

Ответ 6

Позднее присоединение, чтобы добавить краткое пояснение к этим двум фактоидам:

  • они скорее структуры, чем классы
  • они изменчивы, а не только для чтения

Можно было бы подумать, что массовое изменение кортежей значений будет простым:

 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;

Кто-то может попытаться обойти это так:

 // 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'

Причиной такого причудливого поведения является то, что кортежи-значения точно основаны на значениях (структурах), и поэтому вызов .Select(...) работает с клонированными структурами, а не с оригиналами. Для решения этой проблемы мы должны прибегнуть к:

 // 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

В качестве альтернативы, конечно, можно попробовать простой подход:

   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

Надеюсь, что это помогает кому-то изо всех сил пытаться сделать хвосты из размещенных в списке кортежей значений.