Zdekompilowałem niektóre biblioteki C # 7 i zobaczyłem ValueTuple
używane typy generyczne. Czym są ValueTuples
i dlaczego nie Tuple
?
Zdekompilowałem niektóre biblioteki C # 7 i zobaczyłem ValueTuple
używane typy generyczne. Czym są ValueTuples
i dlaczego nie Tuple
?
Odpowiedzi:
Czym są
ValueTuples
i dlaczego nieTuple
?
A ValueTuple
jest strukturą, która odzwierciedla krotkę, taką samą jak oryginałSystem.Tuple
klasa.
Główne różnice między Tuple
i ValueTuple
to:
System.ValueTuple
jest typem wartości (struct), podczas gdy System.Tuple
jest typem referencyjnym (class
). Ma to znaczenie, gdy mówimy o alokacjach i presji na GC.System.ValueTuple
to nie tylko a struct
, jest zmienna i należy zachować ostrożność, używając ich jako takich. Pomyśl, co się dzieje, gdy klasa ma System.ValueTuple
jako pole.System.ValueTuple
ujawnia swoje elementy za pośrednictwem pól zamiast właściwości.Aż do C # 7 używanie krotek nie było zbyt wygodne. Ich nazwy pól to Item1
, Item2
itd., A język nie dostarczył im cukru składniowego, jak robi to większość innych języków (Python, Scala).
Kiedy zespół projektantów języka .NET zdecydował się włączyć krotki i dodać do nich cukier składniowy na poziomie języka, ważnym czynnikiem była wydajność. ZValueTuple
bycia typ wartości, można uniknąć presji GC podczas korzystania z nich, ponieważ (jak szczegółowo wdrożenia) będą przydzielane na stosie.
Ponadto a struct
otrzymuje automatyczną (płytką) semantykę równości przez środowisko wykonawcze, gdzie a class
nie. Chociaż zespół projektowy upewnił się, że będzie jeszcze bardziej zoptymalizowana równość dla krotek, dlatego zaimplementował dla niej niestandardową równość.
Oto akapit z uwag projektowychTuples
:
Struktura lub klasa:
Jak wspomniano, proponuję
structs
raczej tworzyć typy krotek niżclasses
, aby nie wiązała się z nimi żadna kara za alokację. Powinny być tak lekkie, jak to tylko możliwe.Prawdopodobnie
structs
może się to okazać bardziej kosztowne, ponieważ przypisanie kopiuje większą wartość. Więc jeśli przypisuje się im znacznie więcej, niż są tworzone,structs
byłby to zły wybór.Jednak w samej ich motywacji krotki są efemeryczne. Używałbyś ich, gdy części są ważniejsze niż całość. Tak więc powszechnym schematem byłoby ich skonstruowanie, zwrócenie i natychmiastowa dekonstrukcja. W tej sytuacji struktury są zdecydowanie lepsze.
Struktury mają również szereg innych korzyści, które staną się oczywiste w dalszej części.
Możesz łatwo zauważyć, że praca z System.Tuple
bardzo szybko staje się niejednoznaczna. Na przykład, powiedzmy, że mamy metodę, która oblicza sumę i liczbę 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);
}
Po stronie odbiorczej otrzymujemy:
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);
Sposób, w jaki można dekonstruować krotki wartości na nazwane argumenty, to prawdziwa siła tej funkcji:
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;
}
A po stronie odbiorczej:
var result = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {result.Sum}, Count: {result.Count}");
Lub:
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {sum}, Count: {count}");
Jeśli spojrzymy pod okładkę naszego poprzedniego przykładu, możemy dokładnie zobaczyć, jak kompilator interpretuje, ValueTuple
kiedy poprosimy go o dekonstrukcję:
[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;
}
Wewnętrznie skompilowany kod wykorzystuje Item1
i Item2
, ale wszystko to jest od nas oderwane, ponieważ pracujemy na zdekomponowanej krotce. Krotka z nazwanymi argumentami zostanie oznaczona rozszerzeniem TupleElementNamesAttribute
. Jeśli zamiast dekompozycji użyjemy jednej świeżej zmiennej, otrzymamy:
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));
}
Zwróć uwagę, że kompilator nadal musi wykonać jakąś magię (za pośrednictwem atrybutu) podczas debugowania naszej aplikacji, ponieważ byłoby dziwne zobaczyć Item1
, że Item2
.
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Różnica między Tuple
i ValueTuple
polega na tym, że Tuple
jest to typ referencyjny i ValueTuple
typ wartości. To drugie jest pożądane, ponieważ zmiany języka w C # 7 powodują, że krotki są używane znacznie częściej, ale przydzielanie nowego obiektu na stercie dla każdej krotki jest problemem związanym z wydajnością, szczególnie gdy jest to niepotrzebne.
Jednak w C # 7 chodzi o to, że nigdy nie musisz jawnie używać żadnego typu z powodu dodawania cukru składniowego do użycia krotki. Na przykład w języku C # 6, jeśli chcesz użyć krotki do zwrócenia wartości, musisz wykonać następujące czynności:
public Tuple<string, int> GetValues()
{
// ...
return new Tuple(stringVal, intVal);
}
var value = GetValues();
string s = value.Item1;
Jednak w C # 7 możesz użyć tego:
public (string, int) GetValues()
{
// ...
return (stringVal, intVal);
}
var value = GetValues();
string s = value.Item1;
Możesz nawet pójść o krok dalej i nadać wartościom nazwy:
public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}
var value = GetValues();
string s = value.S;
... Lub całkowicie zdekonstruuj krotkę:
public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}
var (S, I) = GetValues();
string s = S;
Krotki nie były często używane w języku C # przed wersją 7, ponieważ były uciążliwe i rozwlekłe, a tak naprawdę używane tylko w przypadkach, gdy tworzenie klasy / struktury danych dla pojedynczego wystąpienia pracy byłoby bardziej kłopotliwe niż było warte. Ale w C # 7 krotki mają teraz obsługę na poziomie języka, więc ich używanie jest znacznie czystsze i bardziej przydatne.
Spojrzałem na źródło zarówno Tuple
i ValueTuple
. Różnica polega na tym, że Tuple
jest to a class
i ValueTuple
jest to struct
implementacja IEquatable
.
Oznacza to, że Tuple == Tuple
zwróci, false
jeśli nie są one tą samą instancją, ale ValueTuple == ValueTuple
zwróci, true
jeśli są tego samego typu i Equals
zwróci true
dla każdej z wartości, które zawierają.
W innych odpowiedziach zapomniałem wspomnieć o ważnych kwestiach, zamiast przeformułować odwołam się do dokumentacji XML z kodu źródłowego :
Typy ValueTuple (od arity 0 do 8) obejmują implementację środowiska uruchomieniowego, która opiera się na krotkach w C # i strukturach w języku F #.
Oprócz tworzenia za pomocą składni języka , najłatwiej jest je tworzyć za pomocą
ValueTuple.Create
metod fabrycznych. Te System.ValueTuple
typy różnią się od System.Tuple
rodzaju tym, że
Dzięki wprowadzeniu tego typu i kompilatorowi C # 7.0 można łatwo pisać
(int, string) idAndName = (1, "John");
I zwróć dwie wartości z metody:
private (int, string) GetIdAndName()
{
//.....
return (id, name);
}
W przeciwieństwie do System.Tuple
ciebie możesz aktualizować jego członków (Mutable), ponieważ są to publiczne pola do odczytu i zapisu, którym można nadać znaczące nazwy:
(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";
class MyNonGenericType : MyGenericType<string, ValueTuple, int>
itp.
Oprócz powyższych komentarzy, jedną niefortunną wadą ValueTuple jest to, że jako typ wartości, nazwane argumenty są usuwane podczas kompilacji do IL, więc nie są dostępne do serializacji w czasie wykonywania.
tzn. Twoje słodkie nazwane argumenty nadal będą kończyć się jako „Pozycja1”, „Pozycja2” itd. po serializacji przez np. Json.NET.
Późne dołączanie, aby dodać szybkie wyjaśnienie tych dwóch faktów:
Można by pomyśleć, że masowa zmiana krotek wartości byłaby prosta:
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;
Ktoś może spróbować obejść to w następujący sposób:
// 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'
Przyczyną tego dziwacznego zachowania jest to, że krotki wartości są dokładnie oparte na wartościach (struktury), a zatem wywołanie .Select (...) działa raczej na sklonowanych strukturach niż na oryginałach. Aby rozwiązać ten problem, musimy skorzystać z:
// 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
Alternatywnie można oczywiście spróbować prostego podejścia:
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
Mam nadzieję, że pomoże to komuś, kto zmaga się z krotkami wartości przechowywanymi na liście.