Do thao tác chập mạch || và && tồn tại cho boolean có thể nullable? RuntimeBinder đôi khi nghĩ như vậy


84

Tôi đã đọc Đặc tả ngôn ngữ C # về các toán tử logic có điều kiện ||&&, còn được gọi là toán tử logic ngắn mạch. Đối với tôi, dường như không rõ ràng nếu những điều này tồn tại cho boolean nullable, tức là loại toán hạng Nullable<bool>(cũng được viết bool?), vì vậy tôi đã thử nó với cách nhập không động:

bool a = true;
bool? b = null;
bool? xxxx = b || a;  // compile-time error, || can't be applied to these types

Điều đó dường như đã giải quyết được câu hỏi (tôi không thể hiểu thông số kỹ thuật rõ ràng, nhưng giả sử việc triển khai trình biên dịch Visual C # là đúng, bây giờ tôi đã biết).

Tuy nhiên, tôi cũng muốn thử với sự dynamicràng buộc. Vì vậy, tôi đã thử điều này thay thế:

static class Program
{
  static dynamic A
  {
    get
    {
      Console.WriteLine("'A' evaluated");
      return true;
    }
  }
  static dynamic B
  {
    get
    {
      Console.WriteLine("'B' evaluated");
      return null;
    }
  }

  static void Main()
  {
    dynamic x = A | B;
    Console.WriteLine((object)x);
    dynamic y = A & B;
    Console.WriteLine((object)y);

    dynamic xx = A || B;
    Console.WriteLine((object)xx);
    dynamic yy = A && B;
    Console.WriteLine((object)yy);
  }
}

Kết quả đáng ngạc nhiên là điều này chạy mà không có ngoại lệ.

Vâng, xykhông ngạc nhiên, tờ khai của họ dẫn đến cả hai thuộc tính được lấy ra, và các giá trị kết quả như mong đợi, xtrueynull.

Tuy nhiên, đánh giá đối với xxcác A || Bdẫn đến không ràng buộc thời gian ngoại lệ, và chỉ có tài sản Ađã được đọc, không phải B. Lý do tại sao điều này xảy ra? Như bạn có thể nói, chúng tôi có thể thay đổi Bgetter để trả về một đối tượng điên rồ, giống như "Hello world", và xxvẫn sẽ đánh giá truemà không có vấn đề ràng buộc ...

Đánh giá A && B(cho yy) cũng không dẫn đến lỗi thời gian ràng buộc. Và ở đây cả hai thuộc tính đều được truy xuất, tất nhiên. Tại sao điều này được cho phép bởi chất kết dính thời gian chạy? Nếu đối tượng trả về từ Bđược thay đổi thành đối tượng "xấu" (như a string), một ngoại lệ ràng buộc sẽ xảy ra.

Hành vi này có đúng không? (Làm thế nào bạn có thể suy ra điều đó từ thông số kỹ thuật?)

Nếu bạn thử Bnhư toán hạng đầu tiên, cả hai B || AB && Ađưa ra ngoại lệ chất kết dính thời gian chạy ( B | AB & Ahoạt động tốt vì mọi thứ đều bình thường với các toán tử không đoản mạch |& ).

(Đã thử với trình biên dịch C # của Visual Studio 2013 và phiên bản thời gian chạy .NET 4.5.2.)


4
Không có trường hợp nào Nullable<Boolean>liên quan, chỉ có các boolean đóng hộp được coi là dynamic- thử nghiệm của bạn với bool?là không liên quan. (Tất nhiên, đây không phải là một câu trả lời đầy đủ, chỉ có mầm mống của một.)
Jeroen Mostert

3
Điều này A || Bcó ý nghĩa nhất định, ở chỗ bạn không muốn đánh giá Btrừ khi Alà sai, còn thì không. Vì vậy, bạn không bao giờ biết loại của biểu thức, thực sự. Các A && Bphiên bản là đáng ngạc nhiên hơn - Tôi sẽ thấy những gì tôi có thể tìm thấy trong spec.
Jon Skeet

2
@JeroenMostert: Chà, trừ khi trình biên dịch quyết định rằng nếu kiểu Aboolvà giá trị của Bnull, thì một bool && bool?toán tử có thể tham gia.
Jon Skeet

4
Thật thú vị, có vẻ như điều này đã làm lộ ra một trình biên dịch hoặc lỗi đặc tả. Đặc tả C # 5.0 &&nói về việc giải quyết nó như thể nó &thay thế và đặc biệt bao gồm trường hợp cả hai toán hạng bool?- nhưng sau đó phần tiếp theo nó đề cập đến không xử lý trường hợp nullable. Tôi có thể thêm một loại câu trả lời chi tiết hơn về vấn đề đó, nhưng nó sẽ không giải thích đầy đủ.
Jon Skeet

14
Tôi đã gửi email cho Mads về vấn đề spec, để xem liệu nó chỉ là một vấn đề trong cách tôi đọc nó ...
Jon Skeet

Câu trả lời:


67

Trước hết, cảm ơn bạn đã chỉ ra rằng thông số kỹ thuật không rõ ràng trong trường hợp nullable-bool không động. Tôi sẽ sửa lỗi đó trong một phiên bản trong tương lai. Hành vi của trình biên dịch là hành vi dự định; &&|| không được cho là hoạt động trên các bools có thể chạy được.

Tuy nhiên, chất kết dính động dường như không thực hiện hạn chế này. Thay vào đó, nó liên kết các hoạt động thành phần riêng biệt: the &/ |and the ?:. Do đó, nó có thể xáo trộn nếu toán hạng đầu tiên xảy ra là truehoặc false(là các giá trị boolean và do đó được phép làm toán hạng đầu tiên của ?:), nhưng nếu bạn đưa ra nulllà toán hạng đầu tiên (ví dụ: nếu bạn thửB && A trong ví dụ trên), bạn sẽ nhận được một ngoại lệ ràng buộc thời gian chạy.

Nếu bạn nghĩ về nó, bạn có thể thấy lý do tại sao chúng tôi triển khai động &&||theo cách này thay vì như một hoạt động động lớn: các hoạt động động bị ràng buộc trong thời gian chạy sau khi các toán hạng của chúng được đánh giá , do đó ràng buộc có thể dựa trên các loại thời gian chạy của kết quả của những đánh giá đó. Nhưng sự đánh giá háo hức như vậy đánh bại mục đích của những người vận hành đoản mạch! Vì vậy, thay vào đó, mã được tạo cho động &&||chia đánh giá thành nhiều phần và sẽ tiến hành như sau:

  • Đánh giá toán hạng bên trái (chúng ta hãy gọi kết quả x )
  • Cố gắng biến nó thành một boolchuyển đổi ngầm định hoặc truehoặcfalse toán tử (thất bại nếu không thể)
  • Sử dụng xnhư điều kiện trong?: hoạt động
  • Trong nhánh thực, sử dụng x kết quả là
  • Trong nhánh sai, bây giờ hãy đánh giá toán hạng thứ hai (hãy gọi là kết quả y)
  • Cố gắng liên kết toán tử &hoặc |dựa trên loại thời gian chạy của xy (thất bại nếu không thể)
  • Áp dụng toán tử đã chọn

Đây là hành vi cho phép thông qua một số kết hợp "bất hợp pháp" của các toán hạng: ?:toán tử xử lý thành công toán hạng đầu tiên là boolean không thể nullable , toán tử &hoặc |thành công xử lý nó như là nullable boolean, và cả hai không bao giờ phối hợp để kiểm tra xem họ đồng ý .

Vì vậy, nó không động && và || làm việc trên nullable. Chỉ là chúng tình cờ được triển khai theo cách hơi quá khoan dung so với trường hợp tĩnh. Đây có lẽ nên được coi là một lỗi, nhưng chúng tôi sẽ không bao giờ sửa nó, vì đó sẽ là một thay đổi đột phá. Ngoài ra, nó sẽ khó giúp bất cứ ai để thắt chặt hành vi.

Hy vọng rằng điều này giải thích những gì xảy ra và tại sao! Đây là một lĩnh vực hấp dẫn và tôi thường cảm thấy bối rối bởi hậu quả của những quyết định mà chúng tôi đã đưa ra khi chúng tôi thực hiện động. Câu hỏi này rất hay - cảm ơn bạn đã đưa ra câu hỏi!

Mads


Tôi có thể thấy rằng các toán tử đoản mạch này là đặc biệt, vì với ràng buộc động, chúng ta không thực sự được phép biết loại toán hạng thứ hai trong trường hợp chúng ta đoản mạch. Có lẽ thông số kỹ thuật nên đề cập đến điều đó? Tất nhiên, vì tất cả mọi thứ bên trong một dynamicđược đóng hộp, chúng ta có thể không biết sự khác biệt giữa một bool?HasValue, và một "đơn giản" bool.
Jeppe Stig Nielsen

6

Hành vi này có đúng không?

Vâng, tôi khá chắc chắn là như vậy.

Làm thế nào bạn có thể suy ra điều đó từ thông số kỹ thuật?

Mục 7.12 của C # Thông số kỹ thuật Phiên bản 5.0, có thông tin liên quan đến các nhà khai thác có điều kiện &&||và làm thế nào ràng buộc động liên quan đến họ. Phần có liên quan:

Nếu một toán hạng của toán tử logic có điều kiện có kiểu thời gian biên dịch động, thì biểu thức được ràng buộc động (§7.2.2). Trong trường hợp này, kiểu thời gian biên dịch của biểu thức là động và giải pháp được mô tả dưới đây sẽ diễn ra tại thời điểm chạy bằng cách sử dụng kiểu thời gian chạy của những toán hạng có kiểu thời gian biên dịch động.

Đây là điểm mấu chốt trả lời câu hỏi của bạn, tôi nghĩ. Độ phân giải xảy ra tại thời điểm chạy là gì? Phần 7.12.2, Toán tử logic có điều kiện do người dùng xác định giải thích:

  • Phép toán x && y được đánh giá là T.false (x)? x: T. & (x, y), trong đó T.false (x) là lệnh gọi của toán tử được khai báo sai trong T và T. & (x, y) là lệnh của toán tử đã chọn &
  • Phép toán x || y được đánh giá là T.true (x)? x: T. | (x, y), trong đó T.true (x) là lệnh gọi của toán tử true được khai báo trong T, và T. | (x, y) là lệnh gọi của toán tử đã chọn |.

Trong cả hai trường hợp, toán hạng đầu tiên x sẽ được chuyển đổi thành bool bằng cách sử dụng các toán tử falsehoặc true. Sau đó, toán tử logic thích hợp được gọi. Với suy nghĩ này, chúng tôi có đủ thông tin để trả lời phần còn lại của câu hỏi của bạn.

Nhưng đánh giá cho xx của A || B dẫn đến không có ngoại lệ thời gian ràng buộc và chỉ thuộc tính A được đọc, không phải B. Tại sao điều này xảy ra?

Đối với ||nhà điều hành, chúng tôi biết nó theo sau true(A) ? A : |(A, B). Chúng tôi ngắn mạch, vì vậy chúng tôi sẽ không nhận được ngoại lệ thời gian ràng buộc. Ngay cả khi Afalse, chúng tôi vẫn sẽ không nhận được ngoại lệ ràng buộc thời gian chạy, vì các bước giải quyết đã chỉ định. Nếu Afalse, sau đó chúng tôi thực hiện |toán tử, có thể xử lý thành công các giá trị null, theo Phần 7.11.4.

Đánh giá A && B (cho yy) cũng không dẫn đến lỗi thời gian ràng buộc. Và tất nhiên ở đây cả hai thuộc tính đều được truy xuất. Tại sao điều này được cho phép bởi chất kết dính thời gian chạy? Nếu đối tượng trả về từ B bị thay đổi thành đối tượng "xấu" (như một chuỗi), một ngoại lệ ràng buộc sẽ xảy ra.

Vì những lý do tương tự, cái này cũng hoạt động. &&được đánh giá là false(x) ? x : &(x, y). Acó thể được chuyển đổi thành công thành a bool, vì vậy không có vấn đề gì ở đó. Vì Blà null, &toán tử được nâng (Phần 7.3.7) từ toán tử nhận a thành toán tử boolbool?tham số, và do đó không có ngoại lệ thời gian chạy.

Đối với cả hai toán tử có điều kiện, nếu Blà bất kỳ điều gì khác ngoài bool (hoặc động null), liên kết thời gian chạy không thành công vì nó không thể tìm thấy quá tải có tham số bool và không phải bool. Tuy nhiên, điều này chỉ xảy ra nếu Akhông thỏa mãn điều kiện đầu tiên cho toán tử ( truefor ||, falsefor &&). Lý do điều này xảy ra là vì liên kết động khá lười biếng. Nó sẽ không cố gắng ràng buộc toán tử logic trừ khi Asai và nó phải đi xuống đường dẫn đó để đánh giá toán tử logic. Một khi Akhông thỏa mãn điều kiện đầu tiên cho toán tử, nó sẽ không thành công với ngoại lệ ràng buộc.

Nếu bạn thử B là toán hạng đầu tiên, cả B || A và B && A đưa ra ngoại lệ chất kết dính thời gian chạy.

Hy vọng rằng bây giờ, bạn đã biết tại sao điều này xảy ra (hoặc tôi đã làm một công việc tồi tệ khi giải thích). Bước đầu tiên để giải quyết toán tử điều kiện này là lấy toán hạng đầu tiên Bvà sử dụng một trong các toán tử chuyển đổi bool ( false(B)hoặc true(B)) trước khi xử lý phép toán logic. Tất nhiên, B, con người nullkhông thể được chuyển đổi sang một trong hai truehoặc false, và do đó các ràng buộc thời gian chạy ngoại lệ xảy ra.


Không có gì ngạc nhiên khi dynamicràng buộc xảy ra tại thời điểm chạy bằng cách sử dụng các loại phiên bản thực tế, không phải các loại thời gian biên dịch (trích dẫn đầu tiên của bạn). Trích dẫn thứ hai của bạn không liên quan vì không có loại nào ở đây quá tải operator trueoperator false. Sự explicit operatortrở lại boollà một cái gì đó khác hơn operator truefalse. Thật khó để đọc thông số kỹ thuật theo bất kỳ cách nào cho phép A && B(trong ví dụ của tôi), mà không cho phép a && bnơi abđược nhập tĩnh boolean nullable, tức là bool? abool? b, với ràng buộc tại thời điểm biên dịch. Tuy nhiên, điều đó là không được phép.
Jeppe Stig Nielsen

-1

Kiểu Nullable không xác định các toán tử logic có điều kiện || và &&. Tôi đề nghị bạn mã sau:

bool a = true;
bool? b = null;

bool? xxxxOR = (b.HasValue == true) ? (b.Value || a) : a;
bool? xxxxAND = (b.HasValue == true) ? (b.Value && a) : false;
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.