Zdekompilowałem niektóre biblioteki C # 7 i zobaczyłem ValueTupleużywane typy generyczne. Czym są ValueTuplesi dlaczego nie Tuple?
Zdekompilowałem niektóre biblioteki C # 7 i zobaczyłem ValueTupleużywane typy generyczne. Czym są ValueTuplesi dlaczego nie Tuple?
Odpowiedzi:
Czym są
ValueTuplesi dlaczego nieTuple?
A ValueTuplejest strukturą, która odzwierciedla krotkę, taką samą jak oryginałSystem.Tuple klasa.
Główne różnice między Tuplei ValueTupleto:
System.ValueTuplejest typem wartości (struct), podczas gdy System.Tuplejest typem referencyjnym (class ). Ma to znaczenie, gdy mówimy o alokacjach i presji na GC.System.ValueTupleto 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.ValueTuplejako 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, Item2itd., 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 structotrzymuje automatyczną (płytką) semantykę równości przez środowisko wykonawcze, gdzie a classnie. 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ę
structsraczej 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
structsmoż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,structsbył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.Tuplebardzo 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, ValueTuplekiedy 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 Item1i 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 Tuplei ValueTuplepolega na tym, że Tuplejest to typ referencyjny i ValueTupletyp 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 Tuplei ValueTuple. Różnica polega na tym, że Tuplejest to a classi ValueTuplejest to structimplementacja IEquatable.
Oznacza to, że Tuple == Tuplezwróci, falsejeśli nie są one tą samą instancją, ale ValueTuple == ValueTuplezwróci, truejeśli są tego samego typu i Equalszwróci truedla 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.Createmetod fabrycznych. Te System.ValueTupletypy różnią się od System.Tuplerodzaju 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.Tupleciebie 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.