Tại sao dấu ngoặc đơn của bộ khởi tạo đối tượng C # 3.0 là tùy chọn?


114

Có vẻ như cú pháp của bộ khởi tạo đối tượng C # 3.0 cho phép người ta loại trừ cặp dấu ngoặc đơn mở / đóng trong hàm tạo khi có một hàm tạo không tham số tồn tại. Thí dụ:

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

Như trái ngược với:

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

Tôi tò mò tại sao cặp dấu ngoặc đơn mở / đóng hàm tạo là tùy chọn ở đây sau khi XTypeName?


9
Ngoài ra, chúng tôi đã tìm thấy điều này trong một bài đánh giá mã tuần trước: var list = new List <Foo> {}; Nếu một cái gì đó có thể bị lạm dụng ...
blu

@blu Đó là một lý do tôi muốn hỏi câu hỏi này. Tôi nhận thấy sự mâu thuẫn trong mã của chúng tôi. Nói chung, sự không nhất quán làm phiền tôi vì vậy tôi nghĩ rằng tôi sẽ xem liệu có lý do chính đáng đằng sau sự tùy chọn trong cú pháp hay không. :)
James Dunne

Câu trả lời:


143

Câu hỏi này là chủ đề của blog của tôi vào ngày 20 tháng 9 năm 2010 . Câu trả lời của Josh và Chad ("chúng không có giá trị gì thêm vậy tại sao lại yêu cầu chúng?" Và "để loại bỏ sự dư thừa") về cơ bản là đúng. Để làm rõ điều đó hơn một chút:

Tính năng cho phép bạn lướt qua danh sách đối số như một phần của "tính năng lớn hơn" của trình khởi tạo đối tượng đã đáp ứng thanh của chúng tôi cho các tính năng "có đường". Một số điểm chúng tôi đã xem xét:

  • chi phí thiết kế và đặc điểm kỹ thuật thấp
  • chúng tôi sẽ thay đổi rộng rãi mã phân tích cú pháp xử lý việc tạo đối tượng; chi phí phát triển bổ sung của việc làm cho danh sách tham số tùy chọn không lớn so với chi phí của tính năng lớn hơn
  • gánh nặng thử nghiệm tương đối nhỏ so với chi phí của tính năng lớn hơn
  • gánh nặng tài liệu tương đối nhỏ so với ...
  • gánh nặng bảo trì được dự đoán là nhỏ; Tôi không nhớ bất kỳ lỗi nào được báo cáo trong tính năng này trong những năm kể từ khi nó xuất xưởng.
  • tính năng này không gây ra bất kỳ rủi ro rõ ràng nào ngay lập tức cho các tính năng trong tương lai trong lĩnh vực này. (Điều cuối cùng chúng tôi muốn làm là tạo ra một tính năng rẻ, dễ dàng ngay bây giờ, điều này khiến việc triển khai một tính năng hấp dẫn hơn trong tương lai sẽ khó hơn nhiều).
  • tính năng này không bổ sung thêm sự mơ hồ mới vào phân tích từ vựng, ngữ pháp hoặc ngữ nghĩa của ngôn ngữ. Nó không gây ra vấn đề gì cho loại phân tích "chương trình một phần" được thực hiện bởi công cụ "IntelliSense" của IDE trong khi bạn đang nhập. Và như thế.
  • tính năng đạt được một "điểm ngọt" chung cho tính năng khởi tạo đối tượng lớn hơn; thường nếu bạn đang sử dụng một bộ khởi tạo đối tượng thì đó chính là vì hàm tạo của đối tượng không cho phép bạn thiết lập các thuộc tính mà bạn muốn. Rất phổ biến đối với những đối tượng như vậy chỉ đơn giản là "túi tài sản" mà không có tham số trong ctor ngay từ đầu.

Tại sao sau đó bạn không đặt dấu ngoặc đơn trống tùy chọn trong lệnh gọi hàm tạo mặc định của một biểu thức tạo đối tượng không có bộ khởi tạo đối tượng?

Hãy nhìn vào danh sách các tiêu chí ở trên. Một trong số đó là sự thay đổi không tạo ra bất kỳ sự mơ hồ mới nào trong phân tích từ vựng, ngữ pháp hoặc ngữ nghĩa của một chương trình. Thay đổi được đề xuất của bạn không tạo ra sự mơ hồ về phân tích ngữ nghĩa:

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
    }
}

Dòng 1 tạo một C mới, gọi hàm tạo mặc định, sau đó gọi phương thức thể hiện M trên đối tượng mới. Dòng 2 tạo một thể hiện mới của BM và gọi hàm tạo mặc định của nó. Nếu dấu ngoặc đơn trên dòng 1 là tùy chọn thì dòng 2 sẽ không rõ ràng. Sau đó, chúng tôi sẽ phải đưa ra một quy tắc giải quyết sự mơ hồ; chúng tôi không thể tạo thành lỗi vì đó sẽ là một thay đổi đột ngột làm thay đổi chương trình C # hợp pháp hiện có thành một chương trình bị hỏng.

Do đó, quy tắc sẽ phải rất phức tạp: về cơ bản, dấu ngoặc đơn chỉ là tùy chọn trong trường hợp chúng không tạo ra sự mơ hồ. Chúng tôi sẽ phải phân tích tất cả các trường hợp có thể gây ra sự mơ hồ và sau đó viết mã trong trình biên dịch để phát hiện chúng.

Trong ánh sáng đó, hãy quay lại và xem xét tất cả các chi phí mà tôi đề cập. Bao nhiêu trong số chúng bây giờ trở nên lớn? Các quy tắc phức tạp có chi phí thiết kế, thông số kỹ thuật, phát triển, thử nghiệm và tài liệu lớn. Các quy tắc phức tạp có nhiều khả năng gây ra sự cố với các tương tác không mong muốn với các tính năng trong tương lai.

Tất cả để làm gì? Một lợi ích khách hàng nhỏ không bổ sung sức mạnh đại diện mới cho ngôn ngữ, nhưng lại thêm vào những trường hợp ngớ ngẩn điên cuồng chỉ chờ hét lên "gotcha" với một linh hồn tội nghiệp không nghi ngờ nào đó chạy vào nó. Các tính năng như vậy sẽ bị cắt ngay lập tức và đưa vào danh sách "không bao giờ làm điều này".

Làm thế nào bạn xác định được sự mơ hồ cụ thể đó?

Điều đó ngay lập tức rõ ràng; Tôi khá quen thuộc với các quy tắc trong C # để xác định khi nào một tên có dấu chấm được mong đợi.

Khi xem xét một tính năng mới, làm thế nào để bạn xác định xem nó có gây ra bất kỳ sự mơ hồ nào không? Bằng tay, bằng chứng chính thức, bằng phân tích máy móc, cái gì?

Cả ba. Hầu hết chúng ta chỉ nhìn vào thông số kỹ thuật và mì trên đó, như tôi đã làm ở trên. Ví dụ: giả sử chúng ta muốn thêm một toán tử tiền tố mới vào C # được gọi là "frob":

x = frob 123 + 456;

(CẬP NHẬT: froblà tất nhiên await; phân tích ở đây về cơ bản là phân tích mà nhóm thiết kế đã trải qua khi thêm vào await.)

"frob" ở đây giống như "new" hoặc "++" - nó đứng trước một biểu thức của một số loại. Chúng tôi sẽ tìm ra mức độ ưu tiên và tính liên kết mong muốn, v.v. và sau đó bắt đầu đặt các câu hỏi như "điều gì sẽ xảy ra nếu chương trình đã có một kiểu, trường, thuộc tính, sự kiện, phương thức, hằng số hoặc cục bộ được gọi là frob?" Điều đó sẽ ngay lập tức dẫn đến các trường hợp như:

frob x = 10;

điều đó có nghĩa là "thực hiện phép toán Frob dựa trên kết quả của x = 10 hay tạo một biến kiểu frob được gọi là x và gán 10 cho nó?" (Hoặc, nếu việc đóng băng tạo ra một biến, nó có thể là một phép gán 10 cho frob x. Rốt cuộc, *x = 10;phân tích cú pháp và hợp pháp nếu xint*.)

G(frob + x)

Điều đó có nghĩa là "giá trị kết quả của toán tử cộng một bậc trên x" hoặc "thêm biểu thức giá trị vào x"?

Và như thế. Để giải quyết những sự mơ hồ này, chúng tôi có thể giới thiệu phương pháp heuristics. Khi bạn nói "var x = 10;" điều đó thật mơ hồ; nó có thể có nghĩa là "suy ra loại x" hoặc nó có thể có nghĩa là "x thuộc loại var". Vì vậy, chúng tôi có một heuristic: trước tiên chúng tôi cố gắng tìm kiếm một kiểu có tên var, và chỉ khi một kiểu không tồn tại, chúng tôi mới suy ra kiểu của x.

Hoặc, chúng tôi có thể thay đổi cú pháp để nó không mơ hồ. Khi họ thiết kế C # 2.0, họ đã gặp vấn đề này:

yield(x);

Điều đó có nghĩa là "lợi nhuận x trong một trình lặp" hoặc "gọi phương thức lợi nhuận với đối số x?" Bằng cách thay đổi nó thành

yield return(x);

nó bây giờ là rõ ràng.

Trong trường hợp các parens tùy chọn trong bộ khởi tạo đối tượng, có thể dễ dàng lập luận về việc liệu có sự mơ hồ nào được đưa vào hay không bởi vì số lượng tình huống được phép giới thiệu thứ gì đó bắt đầu bằng {là rất nhỏ . Về cơ bản chỉ là các ngữ cảnh câu lệnh khác nhau, lambdas câu lệnh, trình khởi tạo mảng và đó là về nó. Thật dễ dàng để suy luận qua tất cả các trường hợp và cho thấy rằng không có sự mơ hồ. Đảm bảo IDE vẫn hoạt động hiệu quả có phần khó hơn nhưng có thể được thực hiện mà không gặp quá nhiều khó khăn.

Loại này thường là đủ. Nếu đó là một tính năng đặc biệt phức tạp thì chúng tôi lấy ra các công cụ nặng hơn. Ví dụ: khi thiết kế LINQ, một trong những người biên dịch và một trong những người IDE, cả hai đều có nền tảng về lý thuyết phân tích cú pháp đã tự xây dựng cho mình một trình tạo trình phân tích cú pháp có thể phân tích ngữ pháp tìm kiếm sự mơ hồ và sau đó cung cấp ngữ pháp C # được đề xuất để hiểu truy vấn vào đó ; làm như vậy đã tìm thấy nhiều trường hợp truy vấn không rõ ràng.

Hoặc, khi chúng tôi thực hiện suy luận kiểu nâng cao trên lambdas trong C # 3.0, chúng tôi đã viết các đề xuất của mình và sau đó gửi chúng qua ao đến Microsoft Research ở Cambridge, nơi nhóm ngôn ngữ ở đó đủ giỏi để tạo ra một bằng chứng chính thức rằng đề xuất kiểu suy luận là về mặt lý thuyết là âm thanh.

Có sự mơ hồ nào trong C # ngày nay không?

Chắc chắn rồi.

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

Trong C # 1, rõ ràng điều đó có nghĩa là gì. Nó giống như:

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

Tức là nó gọi G với hai đối số là bools. Trong C # 2, điều đó có thể có nghĩa là trong C # 1, nhưng nó cũng có thể có nghĩa là "chuyển 0 cho phương thức chung F nhận các tham số kiểu A và B, sau đó chuyển kết quả của F cho G". Chúng tôi đã thêm một heuristic phức tạp vào trình phân tích cú pháp để xác định trường hợp nào trong hai trường hợp mà bạn có thể muốn nói.

Tương tự, các phôi là không rõ ràng ngay cả trong C # 1.0:

G((T)-x)

Đó là "ép -x thành T" hay "trừ x khỏi T"? Một lần nữa, chúng tôi có một phương pháp phỏng đoán giúp đưa ra một dự đoán chính xác.


3
Xin lỗi, tôi quên mất ... Phương pháp tiếp cận tín hiệu dơi, mặc dù nó có vẻ hoạt động, nhưng được ưu tiên cho (IMO) một phương tiện liên lạc trực tiếp, theo đó người ta sẽ không nhận được sự tiếp xúc công khai muốn được giáo dục công cộng dưới dạng một bài đăng SO rằng có thể lập chỉ mục, có thể tìm kiếm và có thể được giới thiệu dễ dàng. Thay vào đó, chúng ta có nên liên hệ trực tiếp để dàn dựng một bài nhảy / câu trả lời SO được dàn dựng không? :)
James Dunne

5
Tôi khuyên bạn nên tránh dàn dựng một bài đăng. Điều đó sẽ không công bằng đối với những người có thể có thêm hiểu biết về câu hỏi. Một cách tiếp cận tốt hơn là đăng câu hỏi, sau đó gửi liên kết đến câu hỏi đó để gửi email yêu cầu tham gia.
chilltemp

1
@James: Tôi đã cập nhật câu trả lời của mình để giải quyết câu hỏi tiếp theo của bạn.
Eric Lippert

8
@Eric, bạn có thể viết blog về danh sách "không bao giờ làm điều này" được không? Tôi tò mò muốn xem các ví dụ khác mà sẽ không bao giờ là một phần của ngôn ngữ C # :)
Ilya Ryzhenkov

2
@Eric: Tôi thực sự thực sự thực sự đánh giá cao sự kiên nhẫn của bạn với tôi :) Cảm ơn! Rất nhiều thông tin.
James Dunne

12

Bởi vì đó là cách ngôn ngữ được chỉ định. Chúng không thêm giá trị, vậy tại sao lại bao gồm chúng?

Nó cũng rất giống với các mảng đã nhập đơn giản

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

Tham khảo: http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx


1
Chúng không thêm giá trị theo nghĩa là phải rõ ràng mục đích ở đó là gì, nhưng nó phá vỡ tính nhất quán ở chỗ bây giờ chúng ta có hai cú pháp xây dựng đối tượng khác nhau, một cú pháp bắt buộc phải có dấu ngoặc đơn (và biểu thức đối số được phân tách bằng dấu phẩy trong đó) và một cú pháp không có .
James Dunne

1
@James Dunne, nó thực sự là cú pháp rất giống với cú pháp mảng được nhập ngầm, hãy xem chỉnh sửa của tôi. Không có loại, không có nhà xây dựng, và mục đích rõ ràng, do đó không cần phải khai báo nó
CaffGeek

7

Điều này được thực hiện để đơn giản hóa việc xây dựng các đối tượng. Các nhà thiết kế ngôn ngữ (theo hiểu biết của tôi) đã không cho biết cụ thể lý do tại sao họ cảm thấy rằng điều này hữu ích, mặc dù nó được đề cập rõ ràng trong trang Đặc tả C # Phiên bản 3.0 :

Một biểu thức tạo đối tượng có thể bỏ qua danh sách đối số phương thức khởi tạo và bao quanh dấu ngoặc đơn, miễn là nó bao gồm một đối tượng hoặc bộ khởi tạo tập hợp. Bỏ qua danh sách đối số phương thức khởi tạo và bao quanh dấu ngoặc đơn tương đương với việc chỉ định một danh sách đối số trống.

Tôi cho rằng họ cảm thấy dấu ngoặc đơn, trong trường hợp này, không cần thiết để thể hiện ý định của nhà phát triển, vì trình khởi tạo đối tượng cho thấy ý định xây dựng và đặt các thuộc tính của đối tượng thay thế.


4

Trong ví dụ đầu tiên của bạn, trình biên dịch cho rằng bạn đang gọi hàm tạo mặc định (Đặc tả ngôn ngữ C # 3.0 nói rằng nếu không có dấu ngoặc nào được cung cấp, thì hàm tạo mặc định được gọi).

Trong cách thứ hai, bạn gọi hàm tạo mặc định một cách rõ ràng.

Bạn cũng có thể sử dụng cú pháp đó để đặt thuộc tính trong khi chuyển các giá trị cho hàm tạo một cách rõ ràng. Nếu bạn có định nghĩa lớp sau:

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;
    }
}

Cả ba câu đều hợp lệ:

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

Đúng. Trong trường hợp đầu tiên và thứ hai trong ví dụ của bạn, chúng giống hệt nhau về chức năng, đúng không?
James Dunne

1
@James Dunne - Đúng. Đó là phần được chỉ định bởi thông số ngôn ngữ. Dấu ngoặc trống là dư thừa, nhưng bạn vẫn có thể cung cấp chúng.
Justin Niessner

1

Tôi không phải là Eric Lippert, vì vậy tôi không thể nói chắc chắn, nhưng tôi sẽ giả sử đó là vì trình biên dịch không cần dấu ngoặc trống để suy ra cấu trúc khởi tạo. Do đó nó trở thành thông tin thừa, và không cần thiết.


Đúng, nó là thừa, nhưng tôi chỉ tò mò tại sao việc giới thiệu chúng đột ngột là tùy chọn? Nó dường như phá vỡ với sự nhất quán về cú pháp ngôn ngữ. Nếu tôi không có dấu ngoặc nhọn mở để chỉ ra một khối khởi tạo, thì đây phải là cú pháp không hợp lệ. Thật buồn cười khi bạn nhắc đến ông Lippert, tôi đại loại là công khai câu trả lời của ông ấy để bản thân tôi và những người khác được hưởng lợi từ sự tò mò vu vơ. :)
James Dunne
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.