Tại sao tôi có thể gán 0,0 cho các giá trị liệt kê mà không phải 1,0


90

Chỉ vì tò mò: tại sao tôi có thể gán 0,0 cho một biến thuộc kiểu liệt kê, mà không phải 1,0? Hãy xem đoạn mã sau:

public enum Foo
{
    Bar,
    Baz
}

class Program
{
    static void Main()
    {
        Foo value1 = 0.0;
        Foo value2 = 1.0;   // This line does not compile
        Foo value3 = 4.2;   // This line does not compile
    }
}

Tôi nghĩ rằng chuyển đổi giữa các kiểu số và giá trị liệt kê chỉ được phép thông qua phôi? Đó là tôi có thể viết Foo value2 = (Foo) 1.0;để dòng 2 trong Maincó thể biên dịch. Tại sao có một ngoại lệ cho giá trị 0.0trong C #?


17
Đối với tôi, thật kỳ lạ, bạn có thể gán gấp đôi chữ 0.0 cho enum tùy chỉnh. Không phải là bạn không thể gán 1.0chữ cho enum tùy chỉnh.
— Ilya Ivanov

2
Tôi nghi ngờ trình biên dịch đang xử lý nó như 0thay thế. Tôi đã có một câu hỏi tương tự một lần và Rawling đã đăng một câu trả lời tuyệt vời ở đây .
— Thân thiện

2
IdeOne không biên dịch nó.
— Johnny Mopp

Câu trả lời:


98

Đó là một lỗi mà bạn có thể sử dụng 0.0. Trình biên dịch xử lý ngầm tất cả các biểu thức hằng có giá trị bằng 0 chỉ bằng 0.

Bây giờ, nó là đúng cho trình biên dịch cho phép một chuyển đổi ngầm từ một hằng số intbiểu hiện của từ 0 đến enum của bạn theo mục 6.1.3 của 5 đặc điểm kỹ thuật C #:

Một chuyển đổi kiểu liệt kê ngầm cho phép chuyển đổi số thập phân-số nguyên-chữ 0 thành bất kỳ kiểu enum nào và thành bất kỳ kiểu nullable nào có kiểu cơ bản là kiểu enum. Trong trường hợp thứ hai, chuyển đổi được đánh giá bằng cách chuyển đổi thành kiểu enum bên dưới và bao bọc kết quả (§4.1.10).

Tôi đã nói chuyện với nhóm C # về điều này trước đây: họ muốn loại bỏ chuyển đổi ngẫu nhiên từ giá trị 0,0 (và thực tế là 0,0m và 0,0f) thành giá trị enum, nhưng thật không may, tôi thu thập nó đã làm hỏng quá nhiều mã - mặc dù nó không bao giờ được cho phép ngay từ đầu.

Mono mcsbiên dịch cấm tất cả các chuyển đổi dấu chấm động, mặc dù nó không cho phép:

const int Zero = 0;
...

SomeEnum x = Zero;

mặc dù thực tế đó Zerolà một biểu thức hằng nhưng không phải là một chữ số thập phân-số nguyên.

Tôi sẽ không ngạc nhiên khi thấy đặc điểm kỹ thuật C # thay đổi trong tương lai để cho phép bất kỳ biểu thức hằng số nguyên nào có giá trị 0 (tức là bắt chước mcs), nhưng tôi sẽ không mong đợi các chuyển đổi dấu phẩy động sẽ chính thức đúng. (Tất nhiên, tôi đã sai trước đây khi dự đoán tương lai của C # ...)


3
Mỗi spec, nó chỉ có nghĩa là đen 0. Vì vậy, nó nên từ chối 1-1- một hằng số intbiểu hiện với một giá trị 0. Nhưng như bạn quan sát, trình biên dịch không phù hợp với thông số kỹ thuật ở đây.
— Damien_The_Un Believer

4
it broke too much code- thật khó để tưởng tượng ra bất kỳ lý do nào để viết mã như vậy.
— Ilya Ivanov

1
@ObsidianPhoenix: Tôi không chắc ý bạn. Đó là chính xác tương đương với: SomeEnum x = (SomeEnum) 0;. Đó là trường hợp cho dù có giá trị 0 được đặt tên hay không.
— Jon Skeet

2
@ObsidianPhoenix: Ồ không, bởi vì giá trị của Test.Foolà 1, không phải 0 ... một lần nữa, điều đó hoàn toàn giống như khi bạn đã viết Test v1 = (Test) 0;- và hành vi đó áp dụng cho bất kỳ giá trị nào không phải là giá trị được đặt tên trong enum.
— Jon Skeet

2
@JonSkeet có phải nó sẽ được sửa ở Roslyn không?
— Tối đa

98

Câu trả lời của Jon là đúng. Tôi sẽ thêm vào nó những điểm sau đây.

  • Tôi đã gây ra lỗi ngớ ngẩn và đáng xấu hổ này. Nhieu loi xin loi.

  • Lỗi do tôi hiểu sai ngữ nghĩa của một vị từ "biểu thức là số không" trong trình biên dịch; Tôi tin rằng nó chỉ đang kiểm tra sự bình đẳng của số nguyên, trong khi thực tế nó đang kiểm tra nhiều thứ hơn dọc theo dòng "đây có phải là giá trị mặc định của kiểu này không?" Trên thực tế, trong một phiên bản trước của lỗi, thực sự có thể gán giá trị mặc định của bất kỳ loại nào cho một enum! Bây giờ nó chỉ là giá trị mặc định của số. (Bài học: Đặt tên cho các vị từ trợ giúp của bạn một cách cẩn thận.)

  • Hành vi tôi đang cố gắng thực hiện mà tôi đã nhầm lẫn trên thực tế là một cách giải quyết cho một lỗi hơi khác. Bạn có thể đọc toàn bộ câu chuyện khủng khiếp tại đây: https://docs.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-one và https://docs.microsoft .com / en-us / archive / blogs / ericlippert / the-root-of-all-evil-part-hai (Bài học: Rất dễ tạo ra các lỗi mới tồi tệ hơn trong khi sửa các lỗi cũ.)

  • Nhóm C # quyết định che giấu hành vi lỗi này thay vì sửa chữa nó vì nguy cơ phá vỡ mã hiện có mà không có lợi ích hấp dẫn là quá cao. (Bài học: làm đúng ngay lần đầu tiên!)

  • Có thể tìm thấy mã tôi đã viết trong Roslyn để duy trì hành vi này trong phương thức IsConstantNumericZerotại https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs - xem nó để biết thêm chi tiết về hành vi chính xác của Roslyn. Tôi đã viết gần như tất cả mã trong thư mục Chuyển đổi; Tôi khuyến khích bạn đọc tất cả vì có rất nhiều sự thật thú vị về cách C # khác biệt với đặc điểm kỹ thuật trong các nhận xét. Tôi đã trang trí từng vi phạm cụ thể để dễ dàng tìm thấy chúng.

Một điểm quan tâm nữa: C # cũng cho phép bất kỳ giá trị enum nào được sử dụng trong trình khởi tạo enum bất kể độ không của nó:

enum E { A = 1 }
enum F { B = E.A }  // ???

Thông số kỹ thuật hơi mơ hồ về việc liệu điều này có hợp pháp hay không, nhưng một lần nữa, vì điều này đã có trong trình biên dịch từ lâu, nên các trình biên dịch mới có khả năng duy trì hành vi.


10
Điều này thực sự thú vị, cuối cùng tôi cũng được xem mã bạn đã viết. Thật tuyệt vời khi mã nguồn Roslyn là mã nguồn mở. Bây giờ tôi hoàn toàn hiểu rằng có những lý do hợp lệ (kỹ thuật / pháp lý) để không cung cấp lịch sử thay đổi nhưng sẽ thật tuyệt nếu xem lịch sử thay đổi để xem mã phát triển như thế nào.
— SolutionYogi

The C# team decided to enshrine this buggy behaviour rather than fixing it because the risk of breaking existing code for no compelling benefit was too high.Tôi không nghĩ rằng sẽ có nhiều người dựa vào hành vi này, và đó là một trong những điều kỳ quặc này có thể tốt hơn nếu chỉ sửa chữa. Tuy nhiên, nó cũng không thực sự gây hại nhiều (ngoại trừ các dự án thực hiện thông số kỹ thuật).
— Aidiakapi

5
@Aidiakapi: Thật vậy, số lượng người bị ảnh hưởng nên ít; nó không phải là số không. Nhóm C # rất coi trọng những thay đổi mang tính đột phá. Thật dễ dàng để bạn nói rằng tốt hơn là nên sửa chữa; bạn không cần phải đối phó với những khách hàng giận dữ gọi điện cho phó chủ tịch của bạn để phàn nàn rằng sự thay đổi nhỏ nhặt của bạn không mang lại lợi ích gì, dù vậy đã làm chậm quá trình tích hợp hệ thống của họ một ngày.
— Eric Lippert

3
Nó trở nên tồi tệ hơn. Tất cả những thay đổi vi phạm như vậy (lý tưởng là) sẽ được liệt kê trong hướng dẫn di chuyển Framework của Microsoft. Danh sách này càng dài, người dùng càng do dự trong việc di chuyển ứng dụng của họ. Vì vậy, ngay cả một thay đổi nhỏ bị hỏng cũng gây ra: 1. Một số ít ứng dụng bị hỏng. 2. Một số ít người dùng từ chối nâng cấp (ngay cả khi sự cố không ảnh hưởng đến họ). 3. Một số lượng nhỏ người dùng lãng phí tài nguyên để đánh giá xem thay đổi vi phạm có ảnh hưởng đến họ hay không. 4. Người dùng từ # 1, # 2 và # 3 để phàn nàn với những người khác.
— Brian

@EricLippert Nếu "Thông số kỹ thuật hơi mơ hồ", có phải cập nhật thông số kỹ thuật không? (Câu hỏi chính xác!)
— James

10

Các phép liệt kê trong C # là các giá trị tích phân theo định nghĩa. Để nhất quán, C # không nên chấp nhận một trong hai phép gán này, nhưng 0.0được coi là tích phân 0. Đây có thể là một phần giữ lại từ C, nơi chữ 0được xử lý đặc biệt và về cơ bản có thể nhận bất kỳ kiểu nào đã cho - số nguyên, số dấu phẩy động, con trỏ null… bạn đặt tên cho nó.


3
câu hỏi là tại sao ? Nếu bạn truy cập IL- nó đang đẩy giá trị số nguyên lên ngăn xếpIL_0001: ldc.i4.0
— Ilya Ivanov

@IlyaIvanov Xem cập nhật. Nhưng thành thật mà nói, câu trả lời là "không có lý do chính đáng".
— Konrad Rudolph

2
Tôi nghĩ rằng đây là một trong những trường hợp nếu bạn nhìn vào thông số kỹ thuật C # , nó không hợp pháp, nhưng nếu bạn nhìn vào bất kỳ trình biên dịch C # nào mà MS đã sản xuất, nó sẽ làm được điều này.
— Damien_The_Un Believer

3

enum thực sự nhằm mục đích (trong tất cả các ngôn ngữ hỗ trợ nó) là một cách để làm việc với các chuỗi (nhãn) có ý nghĩa và duy nhất chứ không phải là một giá trị số. Vì vậy, trong ví dụ của bạn, bạn chỉ nên sử dụng Bar và Baz khi xử lý kiểu dữ liệu liệt kê Foo . Bạn không bao giờ nên sử dụng (so sánh hoặc gán) một số nguyên, mặc dù nhiều trình biên dịch sẽ cho phép bạn sử dụng nó (enum thường là số nguyên bên trong) và trong trường hợp này, 0.0 được trình biên dịch coi là 0 một cách bất cẩn.

Về mặt khái niệm, sẽ ổn nếu thêm một số nguyên n vào một giá trị được liệt kê, để lấy n giá trị xuống dòng, hoặc lấy val2 - val1 để xem chúng cách nhau bao xa, nhưng trừ khi đặc tả ngôn ngữ cho phép rõ ràng điều này, tôi 'd tránh nó. (Hãy nghĩ về một giá trị được liệt kê giống như một con trỏ C, theo những cách bạn có thể sử dụng nó.) Không có lý do gì mà không thể triển khai các giá trị cộng với các số dấu phẩy động và một số gia cố định giữa chúng, nhưng tôi chưa nghe nói về điều này đang được thực hiện bằng bất kỳ ngôn ngữ nào.


Tôi biết rằng tôi đã không sử dụng enum theo cách đó trong C # - nhưng tôi đã tìm thấy lời trêu ghẹo não này và muốn biết tại sao 0.0 hoạt động, nhưng 1.0 thì không. Tôi biết rằng nó phải là một cái gì đó với trình biên dịch C # vì bạn có thể thấy rằng IL Code for Foo v1 = 0.0;cũng giống như for Foo v2 = Foo.Bar.
— feO2x
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 cookie và Chính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.