อะไรคือความแตกต่างระหว่าง System.ValueTuple และ System.Tuple?


144

ฉันถอดรหัสไลบรารี C # 7 บางส่วนและเห็นValueTupleการใช้งานทั่วไป อะไรคืออะไรValueTuplesและทำไมไม่Tupleแทน?


ฉันคิดว่ามันหมายถึงคลาส Dot NEt Tuple คุณช่วยกรุณาแบ่งปันรหัสตัวอย่าง เพื่อให้เข้าใจได้ง่าย
Ranadip Dutta

15
@Ranadip Dutta: ถ้าคุณรู้ว่าทูเพิลคืออะไรคุณไม่จำเป็นต้องใช้โค้ดตัวอย่างเพื่อทำความเข้าใจคำถาม คำถามนั้นตรงไปตรงมา: ValueTuple คืออะไรและแตกต่างจาก Tuple อย่างไร?
BoltClock

1
@BoltClock: นั่นคือเหตุผลที่ฉันไม่ตอบอะไรในบริบทนั้น ฉันรู้ใน c # มีคลาส Tuple ที่ฉันใช้บ่อยและคลาสเดียวกันบางครั้งฉันเรียกมันว่าพาวเวอร์เชลล์ด้วย เป็นประเภทอ้างอิง ตอนนี้เห็นคำตอบอื่น ๆ แล้วฉันเข้าใจว่ามีประเภทค่าซึ่งเรียกว่า Valuetuple ถ้ามีตัวอย่างอยากทราบการใช้งานเหมือนกัน
Ranadip Dutta

2
ทำไมคุณถึงแยกไฟล์เหล่านี้ในเมื่อซอร์สโค้ดสำหรับ Roslyn พร้อมใช้งานบน github
Zein Makki

1
@ user3185569 อาจเป็นเพราะ F12 ถอดรหัสสิ่งต่าง ๆ โดยอัตโนมัติและง่ายกว่าการกระโดดไปที่ GitHub
John Zabroski

คำตอบ:


212

อะไรคืออะไรValueTuplesและทำไมไม่Tupleแทน?

A ValueTupleคือโครงสร้างที่สะท้อนทูเพิลเช่นเดียวกับSystem.Tupleคลาสดั้งเดิม

ความแตกต่างที่สำคัญระหว่างTupleและValueTupleคือ:

  • System.ValueTupleเป็นประเภทค่า (โครงสร้าง) ในขณะที่System.Tupleเป็นประเภทการอ้างอิง ( class) สิ่งนี้มีความหมายเมื่อพูดถึงการจัดสรรและแรงกดดัน GC
  • System.ValueTupleไม่เพียง แต่เป็นstructสิ่งที่ไม่แน่นอนและต้องระวังเมื่อใช้เช่นนี้ คิดว่าจะเกิดอะไรขึ้นเมื่อชั้นเรียนถือSystem.ValueTupleเป็นเขตข้อมูล
  • System.ValueTuple แสดงรายการผ่านฟิลด์แทนคุณสมบัติ

จนถึง C # 7 การใช้ tuples ไม่สะดวกมากนัก ชื่อเขตของพวกเขาจะItem1, Item2ฯลฯ และภาษาที่ไม่ได้จัดมาให้น้ำตาลไวยากรณ์สำหรับพวกเขาเช่นภาษาอื่น ๆ ส่วนใหญ่ทำ (Python, Scala)

เมื่อทีมออกแบบภาษา. NET ตัดสินใจที่จะรวม tuples และเพิ่มน้ำตาลไวยากรณ์ให้กับพวกเขาในระดับภาษาปัจจัยที่สำคัญคือประสิทธิภาพ ด้วยValueTupleการเป็นประเภทค่าคุณสามารถหลีกเลี่ยงแรงกดดัน GC เมื่อใช้เนื่องจาก (เป็นรายละเอียดการนำไปใช้งาน) พวกเขาจะได้รับการจัดสรรในสแต็ก

นอกจากนี้ยังstructได้รับความหมายความเท่าเทียมกันโดยอัตโนมัติ (ตื้น) โดยรันไทม์โดยที่ a classไม่ได้ แม้ว่าทีมออกแบบจะตรวจสอบให้แน่ใจว่าจะมีความเท่าเทียมกันที่เหมาะสมยิ่งขึ้นสำหรับทูเปิล แต่ก็ใช้ความเท่าเทียมกันที่กำหนดเอง

นี่คือย่อหน้าจากบันทึกการออกแบบของTuples :

โครงสร้างหรือชั้น:

ดังที่ได้กล่าวไปแล้วฉันขอเสนอให้สร้างประเภททูเพิลstructsแทน classesเพื่อไม่ให้มีการจัดสรรบทลงโทษใด ๆ ควรมีน้ำหนักเบาที่สุด

ที่จริงแล้วstructsอาจมีค่าใช้จ่ายสูงกว่าเนื่องจากการมอบหมายงานจะคัดลอกมูลค่าที่มากกว่า ดังนั้นหากพวกเขาได้รับมอบหมายมากกว่าที่พวกเขาสร้างขึ้นมาก็structsจะเป็นทางเลือกที่ไม่ดี

แม้ว่าสิ่งเหล่านี้จะมีแรงจูงใจอย่างมากสิ่งเหล่านี้เป็นสิ่งที่ไม่จีรัง คุณจะใช้เมื่อชิ้นส่วนมีความสำคัญมากกว่าทั้งหมด ดังนั้นรูปแบบทั่วไปคือการสร้างส่งคืนและแยกโครงสร้างออกทันที ในสถานการณ์เช่นนี้โครงสร้างเป็นที่ต้องการอย่างชัดเจน

โครงสร้างยังมีประโยชน์อื่น ๆ อีกมากมายซึ่งจะเห็นได้ชัดในสิ่งต่อไปนี้

ตัวอย่าง:

คุณสามารถเห็นได้อย่างง่ายดายว่าการทำงานกับมีSystem.Tupleความคลุมเครืออย่างรวดเร็ว ตัวอย่างเช่นสมมติว่าเรามีวิธีการคำนวณผลรวมและจำนวน a 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));
}

โปรดทราบว่าคอมไพเลอร์ยังคงมีการให้ความมหัศจรรย์บางอย่างเกิดขึ้น (ผ่านแอตทริบิวต์) เมื่อเราแก้ปัญหาโปรแกรมของเรามันจะเป็นเรื่องแปลกที่จะเห็น,Item1Item2


1
โปรดทราบว่าคุณสามารถใช้ไวยากรณ์ที่ง่ายกว่า (และในความคิดของฉันดีกว่า)var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Abion47

@ Abion47 จะเกิดอะไรขึ้นถ้าทั้งสองประเภทแตกต่างกัน?
Yuval Itzchakov

นอกจากนี้คะแนนของคุณ "มันเป็นโครงสร้างที่เปลี่ยนแปลงได้ " และ "มันแสดงฟิลด์แบบอ่านอย่างเดียว " ตกลงอย่างไร
CodesInChaos

@CodesInChaos ไม่ได้ ฉันเห็น [สิ่งนี้] ( github.com/dotnet/corefx/blob/master/src/Common/src/System/… ) แต่ฉันไม่คิดว่านั่นคือสิ่งที่คอมไพเลอร์ปล่อยออกมาเนื่องจากฟิลด์ในเครื่องไม่สามารถ อ่านอย่างเดียวอยู่แล้ว ฉันคิดว่าข้อเสนอนี้หมายความว่า"คุณสามารถทำให้อ่านได้อย่างเดียวหากต้องการ แต่ขึ้นอยู่กับคุณ"ซึ่งฉันตีความผิด
Yuval Itzchakov

2
nits บางตัว: "จะถูกจัดสรรบนสแต็ก" - จริงเฉพาะสำหรับตัวแปรภายใน ไม่ต้องสงสัยเลยว่าคุณรู้เรื่องนี้ แต่น่าเสียดายที่วิธีที่คุณใช้วลีนี้มีแนวโน้มที่จะยืดเยื้อตำนานที่ประเภทของค่ามักจะอยู่ในสแต็ก
Peter Duniho

27

ความแตกต่างระหว่าง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; 

... หรือแยกโครงสร้าง tuple ทั้งหมด:

public (string S, int I) GetValues()
{
    // ...
    return (stringVal, intVal);
}

var (S, I) = GetValues();
string s = S;

Tuples มักไม่ถูกใช้ใน C # pre-7 เนื่องจากมีความยุ่งยากและใช้งานได้จริงและใช้เฉพาะในกรณีที่การสร้างคลาส / โครงสร้างข้อมูลสำหรับงานเพียงอินสแตนซ์เดียวจะมีปัญหามากกว่าที่ควรจะเป็น แต่ใน C # 7 สิ่งเหล่านี้มีการรองรับระดับภาษาดังนั้นการใช้สิ่งเหล่านี้จะสะอาดและมีประโยชน์มากกว่า


11

ผมมองไปที่แหล่งที่มาสำหรับทั้งและTuple ValueTupleความแตกต่างคือTupleเป็นclassและValueTupleเป็นที่ดำเนินการstructIEquatable

นั่นหมายความว่าTuple == Tupleจะส่งคืนfalseหากไม่ใช่อินสแตนซ์เดียวกัน แต่ValueTuple == ValueTupleจะส่งคืนtrueหากเป็นประเภทเดียวกันและEqualsส่งคืนค่าtrueแต่ละค่าที่มีอยู่


มันมากกว่านั้น
BoltClock

3
@BoltClock ความคิดเห็นของคุณจะสร้างสรรค์ถ้าคุณต้องอธิบายอย่างละเอียด
Peter Morris

3
นอกจากนี้ประเภทค่าไม่จำเป็นต้องอยู่ในกองซ้อน ความแตกต่างคือความหมายเป็นตัวแทนของค่าแทนที่จะเป็นการอ้างอิงเมื่อใดก็ตามที่ตัวแปรนั้นถูกเก็บไว้ซึ่งอาจเป็นหรือไม่เป็นสแต็กก็ได้
Servy

7

นอกเหนือจากความคิดเห็นข้างต้นแล้วสิ่งที่น่าเสียดายอย่างหนึ่งของ ValueTuple ก็คือในฐานะประเภทค่าอาร์กิวเมนต์ที่ตั้งชื่อจะถูกลบเมื่อคอมไพล์เป็น IL ดังนั้นจึงไม่พร้อมใช้งานสำหรับการทำให้เป็นอนุกรมที่รันไทม์

เช่นอาร์กิวเมนต์ชื่อหวานของคุณจะยังคงลงท้ายด้วย "Item1", "Item2" ฯลฯ เมื่อต่ออนุกรมผ่านเช่น Json.NET


3
ดังนั้นในทางเทคนิคนั่นคือความคล้ายคลึงกันและไม่ใช่ความแตกต่าง)
JAD

6

คำตอบอื่น ๆ ลืมที่จะพูดถึงประเด็นสำคัญแทนที่จะเปลี่ยนวลีฉันจะอ้างอิงเอกสาร XML จากซอร์สโค้ด :

ชนิด ValueTuple (จาก arity 0 ถึง 8) ประกอบด้วยการใช้งานรันไทม์ที่รองรับ tuples ใน C # และ struct tuples ใน 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";

"Arity 0 ถึง 8" อ่าฉันชอบที่มันมี 0-tuple สามารถใช้เป็นประเภทว่างได้และจะได้รับอนุญาตในชื่อสามัญเมื่อไม่ต้องการพารามิเตอร์บางประเภทเช่นในclass MyNonGenericType : MyGenericType<string, ValueTuple, int>อื่น ๆ
Jeppe Stig Nielsen

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

หวังว่าสิ่งนี้จะช่วยให้ใครบางคนที่ดิ้นรนเพื่อให้หัวก้อยออกจากรายการที่โฮสต์มูลค่าสิ่ง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.