ฉันถอดรหัสไลบรารี C # 7 บางส่วนและเห็นValueTuple
การใช้งานทั่วไป อะไรคืออะไรValueTuples
และทำไมไม่Tuple
แทน?
ฉันถอดรหัสไลบรารี C # 7 บางส่วนและเห็นValueTuple
การใช้งานทั่วไป อะไรคืออะไรValueTuples
และทำไมไม่Tuple
แทน?
คำตอบ:
อะไรคืออะไร
ValueTuples
และทำไมไม่Tuple
แทน?
A ValueTuple
คือโครงสร้างที่สะท้อนทูเพิลเช่นเดียวกับSystem.Tuple
คลาสดั้งเดิม
ความแตกต่างที่สำคัญระหว่างTuple
และValueTuple
คือ:
System.ValueTuple
เป็นประเภทค่า (โครงสร้าง) ในขณะที่System.Tuple
เป็นประเภทการอ้างอิง ( class
) สิ่งนี้มีความหมายเมื่อพูดถึงการจัดสรรและแรงกดดัน GCSystem.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));
}
โปรดทราบว่าคอมไพเลอร์ยังคงมีการให้ความมหัศจรรย์บางอย่างเกิดขึ้น (ผ่านแอตทริบิวต์) เมื่อเราแก้ปัญหาโปรแกรมของเรามันจะเป็นเรื่องแปลกที่จะเห็น,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;
... หรือแยกโครงสร้าง tuple ทั้งหมด:
public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}
var (S, I) = GetValues();
string s = S;
Tuples มักไม่ถูกใช้ใน C # pre-7 เนื่องจากมีความยุ่งยากและใช้งานได้จริงและใช้เฉพาะในกรณีที่การสร้างคลาส / โครงสร้างข้อมูลสำหรับงานเพียงอินสแตนซ์เดียวจะมีปัญหามากกว่าที่ควรจะเป็น แต่ใน C # 7 สิ่งเหล่านี้มีการรองรับระดับภาษาดังนั้นการใช้สิ่งเหล่านี้จะสะอาดและมีประโยชน์มากกว่า
ผมมองไปที่แหล่งที่มาสำหรับทั้งและTuple
ValueTuple
ความแตกต่างคือTuple
เป็นclass
และValueTuple
เป็นที่ดำเนินการstruct
IEquatable
นั่นหมายความว่าTuple == Tuple
จะส่งคืนfalse
หากไม่ใช่อินสแตนซ์เดียวกัน แต่ValueTuple == ValueTuple
จะส่งคืนtrue
หากเป็นประเภทเดียวกันและEquals
ส่งคืนค่าtrue
แต่ละค่าที่มีอยู่
นอกเหนือจากความคิดเห็นข้างต้นแล้วสิ่งที่น่าเสียดายอย่างหนึ่งของ ValueTuple ก็คือในฐานะประเภทค่าอาร์กิวเมนต์ที่ตั้งชื่อจะถูกลบเมื่อคอมไพล์เป็น IL ดังนั้นจึงไม่พร้อมใช้งานสำหรับการทำให้เป็นอนุกรมที่รันไทม์
เช่นอาร์กิวเมนต์ชื่อหวานของคุณจะยังคงลงท้ายด้วย "Item1", "Item2" ฯลฯ เมื่อต่ออนุกรมผ่านเช่น Json.NET
คำตอบอื่น ๆ ลืมที่จะพูดถึงประเด็นสำคัญแทนที่จะเปลี่ยนวลีฉันจะอ้างอิงเอกสาร XML จากซอร์สโค้ด :
ชนิด ValueTuple (จาก arity 0 ถึง 8) ประกอบด้วยการใช้งานรันไทม์ที่รองรับ tuples ใน C # และ struct tuples ใน 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>
อื่น ๆ
การเข้าร่วมล่าช้าเพื่อเพิ่มคำชี้แจงอย่างรวดเร็วเกี่ยวกับข้อเท็จจริงทั้งสองนี้:
ใครจะคิดว่าการเปลี่ยนแปลงมูลค่าเพิ่มขึ้นจะตรงไปตรงมา:
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
หวังว่าสิ่งนี้จะช่วยให้ใครบางคนที่ดิ้นรนเพื่อให้หัวก้อยออกจากรายการที่โฮสต์มูลค่าสิ่ง