เหตุใด C # 3.0 object initializer constructor จึงเป็นตัวเลือกเสริม


114

ดูเหมือนว่าไวยากรณ์ของตัวเริ่มต้นออบเจ็กต์ C # 3.0 จะอนุญาตให้มีหนึ่งในการยกเว้นคู่เปิด / ปิดของวงเล็บในตัวสร้างเมื่อมีตัวสร้างที่ไม่มีพารามิเตอร์อยู่ ตัวอย่าง:

var x = new XTypeName { PropA = value, PropB = value };

ตรงข้ามกับ:

var x = new XTypeName() { PropA = value, PropB = value };

ฉันสงสัยว่าทำไมคู่ของวงเล็บเปิด / ปิดตัวสร้างจึงเป็นตัวเลือกที่นี่หลังจากนั้นXTypeName?


9
นอกจากนี้เราพบสิ่งนี้ในการตรวจสอบโค้ดเมื่อสัปดาห์ที่แล้ว: var list = new List <Foo> {}; หากมีบางสิ่งบางอย่างสามารถถูกทำร้ายได้ ...
blu

@blu นั่นคือเหตุผลหนึ่งที่ฉันอยากถามคำถามนี้ ฉันสังเกตเห็นความไม่สอดคล้องกันในรหัสของเรา ความไม่สอดคล้องกันโดยทั่วไปรบกวนฉันดังนั้นฉันจึงคิดว่าฉันจะดูว่ามีเหตุผลที่ดีอยู่เบื้องหลังตัวเลือกในไวยากรณ์หรือไม่ :)
James Dunne

คำตอบ:


143

คำถามนี้เป็นเรื่องของบล็อกของฉันกันยายน 20 2010 คำตอบของ Josh และ Chad ("พวกเขาไม่เพิ่มคุณค่าแล้วทำไมต้องใช้" และ "เพื่อขจัดความซ้ำซ้อน") นั้นถูกต้องโดยพื้นฐาน เพื่อให้เนื้อออกมามากขึ้น:

คุณลักษณะที่ช่วยให้คุณสามารถกำหนดรายการอาร์กิวเมนต์ซึ่งเป็นส่วนหนึ่งของ "คุณลักษณะที่ใหญ่กว่า" ของวัตถุเริ่มต้นได้พบกับคุณลักษณะ "น้ำตาล" ของเรา บางประเด็นที่เราพิจารณา:

  • ต้นทุนการออกแบบและข้อกำหนดต่ำ
  • เรากำลังจะเปลี่ยนรหัสตัวแยกวิเคราะห์ที่จัดการกับการสร้างออบเจ็กต์อย่างกว้างขวาง ต้นทุนการพัฒนาเพิ่มเติมในการสร้างรายการพารามิเตอร์เป็นทางเลือกนั้นไม่มากเมื่อเทียบกับต้นทุนของคุณสมบัติที่ใหญ่กว่า
  • ภาระการทดสอบค่อนข้างน้อยเมื่อเทียบกับต้นทุนของคุณสมบัติที่ใหญ่กว่า
  • ภาระงานเอกสารค่อนข้างน้อยเมื่อเทียบ ...
  • ภาระการบำรุงรักษาคาดว่าจะน้อย ฉันจำไม่ได้ว่ามีข้อบกพร่องใด ๆ ที่รายงานในคุณลักษณะนี้ในช่วงหลายปีนับตั้งแต่มีการจัดส่ง
  • คุณลักษณะนี้ไม่ก่อให้เกิดความเสี่ยงที่ชัดเจนในทันทีต่อคุณลักษณะในอนาคตในด้านนี้ (สิ่งสุดท้ายที่เราต้องการทำคือการสร้างคุณลักษณะที่ง่ายและราคาถูกในตอนนี้ซึ่งจะทำให้การใช้คุณลักษณะที่น่าสนใจยิ่งขึ้นในอนาคตทำได้ยากขึ้นมาก)
  • คุณลักษณะนี้ไม่เพิ่มความคลุมเครือใหม่ ๆ ให้กับการวิเคราะห์ศัพท์ไวยากรณ์หรือความหมายของภาษา ไม่มีปัญหาสำหรับการวิเคราะห์ประเภท "โปรแกรมบางส่วน" ที่ดำเนินการโดยเอ็นจิ้น "IntelliSense" ของ IDE ในขณะที่คุณกำลังพิมพ์ และอื่น ๆ
  • คุณลักษณะนี้กระทบ "จุดหวาน" ทั่วไปสำหรับคุณลักษณะการเริ่มต้นของวัตถุที่มีขนาดใหญ่ขึ้น โดยทั่วไปถ้าคุณใช้ object initializer มันเป็นเพราะคอนสตรัคเตอร์ของอ็อบเจกต์ไม่อนุญาตให้คุณตั้งค่าคุณสมบัติที่คุณต้องการ เป็นเรื่องปกติมากที่วัตถุดังกล่าวจะเป็นเพียง "ถุงคุณสมบัติ" ที่ไม่มีพารามิเตอร์ใน ctor ตั้งแต่แรก

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

ลองดูรายการเกณฑ์ด้านบนอีกครั้ง หนึ่งในนั้นคือการเปลี่ยนแปลงไม่ได้ทำให้เกิดความคลุมเครือใหม่ ๆ ในการวิเคราะห์คำศัพท์ไวยากรณ์หรือความหมายของโปรแกรม การเปลี่ยนแปลงที่เสนอของคุณจะแนะนำความคลุมเครือการวิเคราะห์ความหมาย:

class P
{
    class B
    {
        public class M { }
    }
    class C : B
    {
        new public void M(){}
    }
    static void Main()
    {
        new C().M(); // 1
        new C.M();   // 2
    }
}

บรรทัดที่ 1 สร้าง C ใหม่เรียกใช้ตัวสร้างเริ่มต้นจากนั้นเรียกวิธีอินสแตนซ์ M บนอ็อบเจ็กต์ใหม่ บรรทัดที่ 2 สร้างอินสแตนซ์ใหม่ของ BM และเรียกใช้ตัวสร้างเริ่มต้น หากวงเล็บในบรรทัดที่ 1 เป็นทางเลือกบรรทัดที่ 2 จะไม่ชัดเจน จากนั้นเราจะต้องสร้างกฎที่แก้ไขความคลุมเครือ เราไม่สามารถทำให้เป็นข้อผิดพลาดได้เพราะนั่นจะเป็นการเปลี่ยนแปลงขั้นสุดท้ายที่เปลี่ยนโปรแกรม C # ทางกฎหมายที่มีอยู่ให้เป็นโปรแกรมที่ใช้งานไม่ได้

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

ในแง่นั้นให้ย้อนกลับไปดูค่าใช้จ่ายทั้งหมดที่ฉันพูดถึง ตอนนี้มีกี่คนที่มีขนาดใหญ่? กฎที่ซับซ้อนมีค่าใช้จ่ายในการออกแบบข้อกำหนดการพัฒนาการทดสอบและการจัดทำเอกสารขนาดใหญ่ กฎที่ซับซ้อนมีแนวโน้มที่จะทำให้เกิดปัญหากับการโต้ตอบที่ไม่คาดคิดกับคุณลักษณะต่างๆในอนาคต

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

คุณพิจารณาความคลุมเครือนั้นได้อย่างไร?

สิ่งนั้นชัดเจนทันที ฉันค่อนข้างคุ้นเคยกับกฎใน C # ในการพิจารณาว่าเมื่อใดที่คาดว่าจะมีชื่อประ

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

ทั้งสาม. ส่วนใหญ่เราแค่ดูสเป็คและเส้นก๋วยเตี๋ยวตามที่ได้กล่าวไว้ข้างต้น ตัวอย่างเช่นสมมติว่าเราต้องการเพิ่มตัวดำเนินการคำนำหน้าใหม่ใน C # ที่เรียกว่า "frob":

x = frob 123 + 456;

(UPDATE: frobแน่นอนว่าawaitการวิเคราะห์ในที่นี้เป็นการวิเคราะห์ที่ทีมออกแบบดำเนินการเมื่อเพิ่มawait)

"frob" ในที่นี้จะเหมือนกับ "new" หรือ "++" - มาก่อนนิพจน์บางประเภท เราจะหาลำดับความสำคัญและการเชื่อมโยงที่ต้องการและอื่น ๆ จากนั้นเริ่มถามคำถามเช่น "จะเกิดอะไรขึ้นถ้าโปรแกรมมีประเภทฟิลด์คุณสมบัติเหตุการณ์วิธีการค่าคงที่หรือที่เรียกว่า frob อยู่แล้ว นั่นจะนำไปสู่กรณีต่างๆทันทีเช่น:

frob x = 10;

หมายความว่า "ทำการดำเนินการ frob กับผลลัพธ์ของ x = 10 หรือสร้างตัวแปรประเภท frob ที่เรียกว่า x และกำหนด 10 ให้? (หรือถ้า frobbing สร้างตัวแปรก็อาจเป็นการกำหนด 10 ถึงfrob xหลังจากนั้นให้*x = 10;แยกวิเคราะห์และถูกต้องตามกฎหมายถ้าxเป็นint*)

G(frob + x)

หมายความว่า "frob ผลลัพธ์ของตัวดำเนินการยูนารีบวกบน x" หรือ "เพิ่มนิพจน์ frob เป็น x"?

และอื่น ๆ เพื่อแก้ไขความไม่ชัดเจนเหล่านี้เราอาจแนะนำฮิวริสติกส์ เมื่อคุณพูดว่า "var x = 10;" มันคลุมเครือ อาจหมายถึง "อนุมานประเภทของ x" หรืออาจหมายถึง "x เป็นประเภท var" ดังนั้นเราจึงมีฮิวริสติก: เราพยายามค้นหาประเภทที่ชื่อ var ก่อนและถ้าไม่มีอยู่เราจะอนุมานประเภทของ x

หรือเราอาจเปลี่ยนรูปแบบเพื่อไม่ให้มีความคลุมเครือ เมื่อพวกเขาออกแบบ C # 2.0 พวกเขามีปัญหานี้:

yield(x);

หมายความว่า "ให้ x ในตัววนซ้ำ" หรือ "เรียกวิธีการให้ผลตอบแทนด้วยอาร์กิวเมนต์ x" โดยเปลี่ยนเป็น

yield return(x);

ตอนนี้มันไม่ชัดเจน

ในกรณีของการ parens ตัวเลือกในวัตถุ initializer มันจะตรงไปตรงมาเกี่ยวกับเหตุผลที่ว่ามีความงงงวยแนะนำหรือไม่เพราะจำนวนของสถานการณ์ในการที่จะได้รับอนุญาตที่จะแนะนำสิ่งที่เริ่มต้นด้วย {มีขนาดเล็กมาก โดยพื้นฐานแล้วมีเพียงบริบทคำสั่งต่างๆคำสั่ง lambdas ตัวเริ่มต้นอาร์เรย์และนั่นคือเกี่ยวกับมัน เป็นเรื่องง่ายที่จะให้เหตุผลในทุกกรณีและแสดงให้เห็นว่าไม่มีความคลุมเครือ การตรวจสอบให้แน่ใจว่า IDE มีประสิทธิภาพค่อนข้างยาก แต่สามารถทำได้โดยไม่มีปัญหามากเกินไป

การเล่นซอแบบนี้มักจะเพียงพอ หากเป็นคุณสมบัติที่ยุ่งยากเป็นพิเศษเราจะดึงเครื่องมือที่หนักกว่าออกมา ตัวอย่างเช่นเมื่อออกแบบ LINQ หนึ่งในกลุ่มคอมไพเลอร์และหนึ่งใน IDE ที่มีพื้นฐานในทฤษฎีตัวแยกวิเคราะห์ได้สร้างตัวสร้างตัวแยกวิเคราะห์ที่สามารถวิเคราะห์ไวยากรณ์เพื่อค้นหาความคลุมเครือจากนั้นจึงป้อนไวยากรณ์ C # ที่เสนอเพื่อความเข้าใจในการสืบค้น ; การทำเช่นนี้พบหลายกรณีที่ข้อความค้นหาไม่ชัดเจน

หรือเมื่อเราทำการอนุมานประเภทขั้นสูงเกี่ยวกับ lambdas ใน C # 3.0 เราได้เขียนข้อเสนอของเราแล้วส่งไปยัง Microsoft Research ในเคมบริดจ์ซึ่งทีมภาษามีดีพอที่จะพิสูจน์หลักฐานอย่างเป็นทางการว่าข้อเสนอการอนุมานประเภทคือ ในทางทฤษฎีเสียง

มีความคลุมเครือใน C # วันนี้หรือไม่?

แน่ใจ

G(F<A, B>(0))

ใน C # 1 มีความชัดเจนว่าหมายถึงอะไร มันเหมือนกับ:

G( (F<A), (B>0) )

นั่นคือมันเรียก G ด้วยอาร์กิวเมนต์สองตัวที่เป็นบูล ใน C # 2 นั่นอาจหมายถึงความหมายใน C # 1 แต่อาจหมายถึง "ส่ง 0 ไปยังวิธีการทั่วไป F ที่รับพารามิเตอร์ประเภท A และ B แล้วส่งผลลัพธ์ของ F ไปยัง G" เราได้เพิ่มฮิวริสติกที่ซับซ้อนให้กับตัวแยกวิเคราะห์ซึ่งกำหนดว่าคุณอาจหมายถึงกรณีใดในสองกรณี

ในทำนองเดียวกันการร่ายมีความคลุมเครือแม้ใน C # 1.0:

G((T)-x)

"เหวี่ยง -x เป็น T" หรือ "ลบ x ออกจาก T" หรือไม่ อีกครั้งเรามีฮิวริสติกที่ทำให้เดาได้ดี


3
ขออภัยฉันลืม ... วิธีการส่งสัญญาณค้างคาวในขณะที่ดูเหมือนจะใช้งานได้นั้นต้องการ (IMO) วิธีการติดต่อโดยตรงโดยที่เราจะไม่ได้รับการเปิดเผยต่อสาธารณะที่ต้องการให้มีการศึกษาสาธารณะในรูปแบบของโพสต์ SO ที่ สามารถจัดทำดัชนีค้นหาได้และสามารถอ้างถึงได้อย่างง่ายดาย เราจะติดต่อโดยตรงแทนเพื่อที่จะออกแบบท่าเต้น SO post / answer dance ได้หรือไม่? :)
James Dunne

5
ฉันขอแนะนำให้คุณหลีกเลี่ยงการแสดงโพสต์ นั่นจะไม่ยุติธรรมสำหรับผู้อื่นที่อาจมีความเข้าใจเพิ่มเติมเกี่ยวกับคำถาม แนวทางที่ดีกว่าคือโพสต์คำถามจากนั้นส่งลิงก์ไปยังอีเมลเพื่อขอเข้าร่วม
chilltemp

1
@ เจมส์: ฉันได้อัปเดตคำตอบเพื่อตอบคำถามติดตามของคุณ
Eric Lippert

8
@ เอริกเป็นไปได้ไหมที่คุณสามารถบล็อกเกี่ยวกับรายการ "ไม่เคยทำ" นี้ ฉันอยากรู้อยากเห็นตัวอย่างอื่น ๆ ที่จะไม่เป็นส่วนหนึ่งของภาษา C # :)
Ilya Ryzhenkov

2
@ เอริก: ฉันขอขอบคุณจริงๆที่อดทนรอ :) ขอบคุณ! ให้ข้อมูลมาก
James Dunne

12

เพราะนั่นเป็นวิธีระบุภาษา พวกเขาไม่เพิ่มมูลค่าแล้วทำไมต้องรวมไว้ด้วย?

นอกจากนี้ยังคล้ายกับอาร์เรย์ที่พิมพ์ผิด

var a = new[] { 1, 10, 100, 1000 };            // int[]
var b = new[] { 1, 1.5, 2, 2.5 };            // double[]
var c = new[] { "hello", null, "world" };      // string[]
var d = new[] { 1, "one", 2, "two" };         // Error

อ้างอิง: http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx


1
พวกเขาไม่ได้เพิ่มค่าในแง่ที่ว่ามันควรจะชัดเจนว่ามีเจตนาอะไร แต่มันแตกด้วยความสอดคล้องในตอนนี้เรามีไวยากรณ์การสร้างอ็อบเจกต์ที่แตกต่างกันสองแบบโดยหนึ่งมีวงเล็บที่จำเป็น (และนิพจน์อาร์กิวเมนต์คั่นด้วยจุลภาค) และอีกอันไม่มี .
James Dunne

1
@ James Dunne เป็นไวยากรณ์ที่คล้ายกันมากกับไวยากรณ์อาร์เรย์ที่พิมพ์โดยปริยายโปรดดูการแก้ไขของฉัน ไม่มีประเภทไม่มีตัวสร้างและเจตนาชัดเจนดังนั้นจึงไม่จำเป็นต้องประกาศ
CaffGeek

7

สิ่งนี้ทำขึ้นเพื่อลดความซับซ้อนของการสร้างวัตถุ นักออกแบบภาษาไม่ได้ (สำหรับความรู้ของฉัน) พูดเป็นพิเศษว่าทำไมพวกเขารู้สึกว่าสิ่งนี้มีประโยชน์แม้ว่าจะมีการกล่าวถึงอย่างชัดเจนในหน้าข้อกำหนดC # เวอร์ชัน 3.0 :

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

ฉันคิดว่าพวกเขารู้สึกว่าในวงเล็บในกรณีนี้ไม่จำเป็นเพื่อแสดงเจตนาของนักพัฒนาเนื่องจากตัวเริ่มต้นวัตถุแสดงเจตนาในการสร้างและตั้งค่าคุณสมบัติของวัตถุแทน


4

ในตัวอย่างแรกของคุณคอมไพลเลอร์อนุมานว่าคุณกำลังเรียกใช้ตัวสร้างเริ่มต้น (ข้อกำหนดภาษา C # 3.0 ระบุว่าหากไม่มีการจัดเตรียมวงเล็บไว้ตัวสร้างเริ่มต้นจะถูกเรียก)

ในวินาทีนี้คุณเรียกตัวสร้างเริ่มต้นอย่างชัดเจน

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

public class SomeTest
{
    public string Value { get; private set; }
    public string AnotherValue { get; set; }
    public string YetAnotherValue { get; set;}

    public SomeTest() { }

    public SomeTest(string value)
    {
        Value = value;
    }
}

คำสั่งทั้งสามถูกต้อง:

var obj = new SomeTest { AnotherValue = "Hello", YetAnotherValue = "World" };
var obj = new SomeTest() { AnotherValue = "Hello", YetAnotherValue = "World"};
var obj = new SomeTest("Hello") { AnotherValue = "World", YetAnotherValue = "!"};

ขวา. ในกรณีแรกและครั้งที่สองในตัวอย่างของคุณมีฟังก์ชันเหมือนกันใช่หรือไม่?
James Dunne

1
@ James Dunne - ถูกต้อง. นั่นคือส่วนที่ระบุโดยสเป็คภาษา วงเล็บว่างนั้นซ้ำซ้อน แต่คุณยังสามารถใส่ได้
Justin Niessner

1

ฉันไม่ใช่ Eric Lippert ดังนั้นฉันจึงไม่สามารถพูดได้อย่างแน่นอน แต่ฉันคิดว่าเป็นเพราะคอมไพเลอร์ไม่ต้องการวงเล็บว่างในการอนุมานโครงสร้างการเริ่มต้น ดังนั้นจึงกลายเป็นข้อมูลที่ซ้ำซ้อนและไม่จำเป็น


ถูกต้องมันซ้ำซ้อน แต่ฉันแค่อยากรู้ว่าทำไมการแนะนำอย่างกะทันหันถึงเป็นทางเลือก? ดูเหมือนว่าจะทำลายด้วยความสอดคล้องของไวยากรณ์ภาษา หากฉันไม่มีวงเล็บปีกกาเปิดเพื่อระบุบล็อกตัวเริ่มต้นนี่ควรเป็นไวยากรณ์ที่ผิดกฎหมาย ตลกดีที่คุณพูดถึงมิสเตอร์ลิปเพิร์ตฉันตกปลาแบบเปิดเผยต่อสาธารณชนสำหรับคำตอบของเขาเพื่อให้ตัวเองและคนอื่น ๆ ได้รับประโยชน์จากความอยากรู้อยากเห็นที่ไม่ได้ใช้งาน :)
James Dunne
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.