В чем разница между System.ValueTuple и System.Tuple?

139

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

Стив Фан
источник
Я думаю, что это относится к классу Dot NEt Tuple. Не могли бы вы поделиться примером кода. Так что это будет легко понять.
Ранадип Датта
14
@Ranadip Dutta: Если вы знаете, что такое кортеж, вам не нужен пример кода, чтобы понять вопрос. Сам вопрос прост: что такое ValueTuple и чем он отличается от Tuple?
BoltClock
1
@BoltClock: по этой причине я ничего не ответил в этом контексте. Я знаю, что в C # есть класс Tuple, которым я пользуюсь довольно часто, и один и тот же класс, иногда я называю его также в powershell. Это ссылочный тип. Теперь, увидев другие ответы, я понял, что существует также тип значения, известный как Valuetuple. Если есть образец, я хотел бы знать использование для того же самого.
Ранадип Датта
2
Почему вы декомпилируете их, когда исходный код для Roslyn доступен на github?
Зейн Макки,
@ user3185569, вероятно, потому что F12 автоматически декомпилирует вещи и это проще, чем переход на GitHub
Джон Заброски

Ответы:

203

Что есть ValueTuplesи почему нет Tuple?

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

Основным отличием между Tupleи ValueTupleявляются:

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

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

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

Кроме того, a 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.

Ювал Ицчаков
источник
1
Обратите внимание, что вы также можете использовать более простой (и, на мой взгляд, предпочтительный) синтаксисvar (sum, count) = DoStuff(Enumerable.Range(0, 10));
Abion47
@ Abion47 Что произойдет, если оба типа различаются?
Юваль Ицчаков
Кроме того, как совпадают ваши точки «это изменчивая структура» и «она предоставляет доступ только для чтения »?
CodesInChaos
@CodesInChaos Это не так. Я видел [это] ( github.com/dotnet/corefx/blob/master/src/Common/src/System/… ), но я не думаю, что это то, что в конечном итоге испускается компилятором, так как локальные поля не могут быть в любом случае, только для чтения. Я думаю, что предложение означало «вы можете сделать их только для чтения, если хотите, но это ваше дело» , что я неправильно истолковал.
Юваль Ицчаков
1
Некоторые nits: «они будут размещены в стеке» - верно только для локальных переменных. Без сомнения, вы это знаете, но, к сожалению, то, как вы это сформулировали, скорее всего, увековечит миф о том, что типы значений всегда живут в стеке.
Питер Дунихо,
26

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

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

public Tuple<string, int> 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;

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

Abion47
источник
10

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

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

Питер Моррис
источник
Хотя это больше, чем это.
BoltClock
2
@BoltClock Ваш комментарий был бы конструктивным, если бы вы уточнить
Питер Моррис
3
Кроме того, типы значений не обязательно помещаются в стек. Разница заключается в том, что семантически представляют значение, а не ссылку, всякий раз, когда эта переменная сохраняется, которая может быть или не быть стеком.
Serv
6

Другие ответы забыли упомянуть важные моменты. Вместо того, чтобы перефразировать, я буду ссылаться на документацию 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вас, вы можете обновить его члены (изменяемые), потому что они являются открытыми полями для чтения и записи, которым можно дать значимые имена:

(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";
Зейн Макки
источник
«Арити от 0 до 8». Ах, мне нравится тот факт, что они содержат 0-кортеж. Его можно использовать как пустой тип, и он будет разрешен в дженериках, когда какой-либо параметр типа не нужен, например, в class MyNonGenericType : MyGenericType<string, ValueTuple, int>и т. Д.
Jeppe Stig Nielsen
6

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

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

ZenSquirrel
источник
2
Технически, это сходство, а не разница;)
JAD
2

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

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

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

 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

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

XDS
источник