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

Steve Fan

Ответов: 6

Ответы (6)

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

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

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

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

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

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

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

Вот абзац из примечаний к дизайну Tuples:

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

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

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

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

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

Примеры:

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

public Tuple DoStuff(IEnumerable values)
{
    var sum = 0;
    var count = 0;

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

    return new Tuple(sum, count);
}

На принимающей стороне получаем:

Tuple 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 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 DoStuff(IEnumerable values)
{
    ValueTuple result;
    result..ctor(0, 0);
    foreach (int current in values)
    {
        result.Item1 += current;
        result.Item2++;
    }
    return result;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

public Tuple GetValues()
{
    // ...
    return new Tuple(stringVal, intVal);
}

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

Однако в C # 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;
• 100001 Но в C # 7 кортежи теперь имеют поддержку на уровне языка, поэтому их использование намного чище и полезнее.

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

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

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

 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

Hope this helps someone struggling to make heads of tails out of list-hosted value-tuples.

Я посмотрел исходники для Tuple и ValueTuple. Разница в том, что Tuple - это class, а ValueTuple - это struct, реализующая IEquatable.

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

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

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

2022 WebDevInsider