Я декомпілював деякі бібліотеки C # 7 і побачив, ValueTuple
як використовуються дженерики. Що таке, ValueTuples
а чому не Tuple
замість цього?
Я декомпілював деякі бібліотеки C # 7 і побачив, ValueTuple
як використовуються дженерики. Що таке, ValueTuples
а чому не Tuple
замість цього?
Відповіді:
Що таке,
ValueTuples
а чому неTuple
замість цього?
A ValueTuple
- це структура, яка відображає кортеж, такий же, як і оригінальний System.Tuple
клас.
Основна відмінність між Tuple
і ValueTuple
:
System.ValueTuple
є типом значення (структура), а System.Tuple
тип посилань ( class
). Це має сенс, якщо говорити про виділення та тиску GC.System.ValueTuple
це не тільки a struct
, це мутаційний , і потрібно бути обережним при використанні їх як таких. Подумайте, що станеться, коли клас містить System.ValueTuple
поле як поле.System.ValueTuple
виставляє свої елементи через поля замість властивостей.До C # 7 користуватися кортежами було не дуже зручно. Їх імена полів Item1
, Item2
і т.д., і мова не поставляється синтаксис для них , як і більшості інших мов робити (Python, Scala).
Коли команда дизайнерів мови .NET вирішила включити кортежі та додати до них синтаксичний цукор на мовному рівні, важливим фактором була продуктивність. З ValueTuple
того типу значення, ви можете уникнути тиску ГХ при їх використанні , тому що (як деталь реалізації) вони будуть виділені в стеці.
Додатково, програма 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
.
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Різниця між 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 кортежі мають підтримку на рівні мови, тому їх використання набагато чистіше і корисніше.
Я подивився на джерело і для, Tuple
і для ValueTuple
. Різниця полягає в тому, що Tuple
це class
та ValueTuple
є, struct
що реалізується IEquatable
.
Це означає, що Tuple == Tuple
вони повернуться, false
якщо вони не є тим самим екземпляром, але ValueTuple == ValueTuple
повернуться, true
якщо вони одного типу і Equals
повернуть true
для кожного зі значень, які вони містять.
Інші відповіді забули згадати важливі моменти. Замість перефразовування я посилаюся на XML-документацію з вихідного коду :
Типи ValueTuple (від атрибутів 0 до 8) містять реалізацію, яка лежить в основі кортежів у C # та структурує кортежі у F #.
Крім створених за допомогою синтаксису мови , вони найлегше створюються
ValueTuple.Create
фабричними методами. Ці System.ValueTuple
типи відрізняються від System.Tuple
типів тим , що:
Завдяки впровадженню цього типу та компілятору 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";
class MyNonGenericType : MyGenericType<string, ValueTuple, int>
і т. Д.
На додаток до вищезазначених коментарів, однією невдалою дією ValueTuple є те, що як тип значення названі аргументи стираються при компіляції в IL, тому вони недоступні для серіалізації під час виконання.
тобто ваші солодкі названі аргументи все ще залишатимуться як "Item1", "Item2" тощо, коли вони будуть серіалізовані через, наприклад, Json.NET.
Пізнє приєднання до швидкого роз'яснення цих двох фактоїдів:
Можна було б подумати, що масове зміна кортежів вартості:
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
Сподіваємось, це допомагає комусь, хто бореться за те, щоб зробити голови хвостів із списків, розміщених у списку.