Có sai không khi sử dụng cờ cho nhóm enum của Enz?


12

Sự hiểu biết của tôi là [Flag]enums thường được sử dụng cho những thứ có thể được kết hợp, trong đó các giá trị riêng lẻ không loại trừ lẫn nhau .

Ví dụ:

[Flags]
public enum SomeAttributes
{
    Foo = 1 << 0,
    Bar = 1 << 1,
    Baz = 1 << 2,
}

Trường hợp bất kỳ SomeAttributesgiá trị có thể là sự kết hợp của Foo, BarBaz.

Trong một kịch bản thực tế , phức tạp hơn , tôi sử dụng một enum để mô tả DeclarationType:

[Flags]
public enum DeclarationType
{
    Project = 1 << 0,
    Module = 1 << 1,
    ProceduralModule = 1 << 2 | Module,
    ClassModule = 1 << 3 | Module,
    UserForm = 1 << 4 | ClassModule,
    Document = 1 << 5 | ClassModule,
    ModuleOption = 1 << 6,
    Member = 1 << 7,
    Procedure = 1 << 8 | Member,
    Function = 1 << 9 | Member,
    Property = 1 << 10 | Member,
    PropertyGet = 1 << 11 | Property | Function,
    PropertyLet = 1 << 12 | Property | Procedure,
    PropertySet = 1 << 13 | Property | Procedure,
    Parameter = 1 << 14,
    Variable = 1 << 15,
    Control = 1 << 16 | Variable,
    Constant = 1 << 17,
    Enumeration = 1 << 18,
    EnumerationMember = 1 << 19,
    Event = 1 << 20,
    UserDefinedType = 1 << 21,
    UserDefinedTypeMember = 1 << 22,
    LibraryFunction = 1 << 23 | Function,
    LibraryProcedure = 1 << 24 | Procedure,
    LineLabel = 1 << 25,
    UnresolvedMember = 1 << 26,
    BracketedExpression = 1 << 27,
    ComAlias = 1 << 28
}

Rõ ràng một giá trị nhất định Declarationkhông thể là cả a Variablevà a LibraryProcedure- hai giá trị riêng lẻ không thể được kết hợp .. và chúng không phải là.

Mặc dù các cờ này cực kỳ hữu ích (rất dễ xác minh xem một cờ đã cho DeclarationTypePropertyhay là một Module), nó cảm thấy "sai" vì các cờ không thực sự được sử dụng để kết hợp các giá trị, mà là để nhóm chúng thành "loại phụ".

Vì vậy, tôi đã nói rằng đây là lạm dụng cờ enum - câu trả lời này về cơ bản nói rằng nếu tôi có một bộ giá trị áp dụng cho táo và một bộ khác áp dụng cho cam, thì tôi cần một loại enum khác cho táo và một loại khác cho cam - vấn đề ở đây là tôi cần tất cả các khai báo để có một giao diện chung, vớiDeclarationType việc được hiển thị trong Declarationlớp cơ sở : có một PropertyTypeenum sẽ không hữu ích chút nào.

Đây có phải là một thiết kế cẩu thả / đáng ngạc nhiên / lạm dụng? Nếu vậy, thì vấn đề đó thường được giải quyết như thế nào?


Xem xét cách mọi người sẽ sử dụng các đối tượng của loại DeclarationType. Nếu tôi muốn xác định xem có phải xlà một kiểu con hay không y, có lẽ tôi sẽ muốn viết nó như là x.IsSubtypeOf(y), chứ không phải x && y == y.
Tanner Swett

1
@TannerSwett Đó chính xác là những gì x.HasFlag(DeclarationType.Member)....
Mathieu Guindon

Vâng đó là sự thật. Nhưng nếu phương thức của bạn được gọi HasFlagthay vì IsSubtypeOf, thì tôi cần một số cách khác để tìm ra rằng cái thực sự có nghĩa là "là kiểu con của". Bạn có thể tạo một phương thức mở rộng, nhưng là một người dùng, điều mà tôi thấy ít ngạc nhiên nhất là DeclarationTypechỉ là một cấu trúc có IsSubtypeOfnhư một phương thức thực sự .
Tanner Swett

Câu trả lời:


10

Điều này chắc chắn là lạm dụng enums và cờ! Nó có thể làm việc cho bạn, nhưng bất kỳ ai khác đọc mã sẽ bị nhầm lẫn.

Nếu tôi hiểu chính xác, bạn có một phân loại khai báo phân cấp . Đây là rất nhiều thông tin để mã hóa trong một enum. Nhưng có một sự thay thế rõ ràng: Sử dụng các lớp và kế thừa! Vì vậy, Memberkế thừa từ DeclarationType, Propertythừa kế từ Membervà như vậy.

Enums phù hợp trong một số trường hợp cụ thể: Nếu một giá trị luôn là một trong số các tùy chọn giới hạn hoặc nếu đó là bất kỳ sự kết hợp nào của một số tùy chọn giới hạn (cờ). Bất kỳ thông tin nào phức tạp hoặc có cấu trúc hơn thông tin này nên được biểu diễn bằng các đối tượng.

Chỉnh sửa : Trong "kịch bản đời thực" của bạn, có vẻ như có nhiều nơi hành vi được chọn tùy thuộc vào giá trị của enum. Đây thực sự là một antipotype, vì bạn đang sử dụng switch+ enumnhư là một "đa hình người nghèo". Chỉ cần biến giá trị enum thành các lớp riêng biệt đóng gói hành vi cụ thể khai báo và mã của bạn sẽ sạch hơn nhiều


Kế thừa là một ý tưởng tốt nhưng câu trả lời của bạn hàm ý một lớp mỗi enum, có vẻ như quá mức cần thiết.
Frank Hileman

@FrankHileman: "Những chiếc lá" trong hệ thống phân cấp có thể được biểu diễn dưới dạng giá trị enum thay vì các lớp, nhưng tôi không chắc nó sẽ tốt hơn. Phụ thuộc nếu hành vi khác nhau sẽ được liên kết với các giá trị enum khác nhau, trong trường hợp đó các lớp riêng biệt sẽ tốt hơn.
JacquesB

4
Việc phân loại không theo thứ bậc, các kết hợp là các trạng thái được xác định trước. Sử dụng sự kế thừa cho điều này sẽ thực sự lạm dụng, lạm dụng các lớp. Với một lớp tôi mong đợi ít nhất một số dữ liệu hoặc một số hành vi. Một nhóm các lớp mà không có ... đúng, một enum.
Martin Maat

Về liên kết đó đến repo của tôi: hành vi theo kiểu có liên quan nhiều đến ParserRuleContextkiểu của lớp được tạo hơn là với enum. Mã đó đang cố gắng để có được vị trí mã thông báo nơi chèn một As {Type}mệnh đề trong khai báo; các ParserRuleContextlớp được tạo ra này được Antr4 tạo ra theo một ngữ pháp xác định các quy tắc của trình phân tích cú pháp - Tôi không thực sự kiểm soát hệ thống phân cấp thừa kế [khá nông] của các nút cây, mặc dù tôi có thể tận dụng tính đơn giản của chúng partialđể làm cho chúng thực hiện các giao diện, ví dụ như làm cho họ phơi bày một số AsTypeClauseTokenPositiontài sản .. rất nhiều công việc.
Mathieu Guindon

6

Tôi thấy cách tiếp cận này đủ dễ để đọc và hiểu. IMHO, nó không phải là một cái gì đó để nhầm lẫn về. Điều đó đang được nói, tôi có một số lo ngại về phương pháp này:

  1. Bảo lưu chính của tôi là không có cách nào để thực thi điều này:

    Rõ ràng một Tuyên bố nhất định không thể là cả Biến và Thư viện - hai giá trị riêng lẻ không thể được kết hợp .. và chúng không phải là.

    Trong khi bạn không khai báo kết hợp ở trên, mã này

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    Là hoàn toàn hợp lệ. Và không có cách nào để bắt những lỗi đó tại thời điểm biên dịch.

  2. Có giới hạn về số lượng thông tin bạn có thể mã hóa trong cờ bit, đó là gì, 64 bit? Hiện tại, bạn đang trở nên nguy hiểm gần với kích thước intvà nếu enum này tiếp tục phát triển, cuối cùng bạn có thể sẽ hết bit ...

Điểm mấu chốt là, tôi nghĩ rằng đây là một cách tiếp cận hợp lệ, nhưng tôi sẽ do dự khi sử dụng nó cho hệ thống phân cấp cờ lớn / phức tạp.


Vì vậy, các bài kiểm tra đơn vị phân tích kỹ lưỡng sẽ giải quyết # 1 sau đó. FWIW enum bắt đầu như một cờ không tiêu chuẩn enum khoảng 3 năm trước .. Sau khi mệt mỏi vì luôn luôn kiểm tra một số giá trị cụ thể (giả sử, trong kiểm tra mã) rằng cờ enum xuất hiện. Danh sách này thực sự sẽ không tăng theo thời gian, nhưng intdù sao thì khả năng đánh bại là một mối quan tâm thực sự.
Mathieu Guindon

3

TL; DR Cuộn xuống dưới cùng.


Từ những gì tôi thấy, bạn đang triển khai một ngôn ngữ mới trên đầu trang C #. Các enum dường như biểu thị loại định danh (hoặc, bất cứ thứ gì có tên và xuất hiện trong mã nguồn của ngôn ngữ mới), dường như được áp dụng cho các nút được thêm vào biểu diễn cây của chương trình.

Trong tình huống cụ thể này, có rất ít hành vi đa hình giữa các loại nút khác nhau. Nói cách khác, trong khi cây cần có thể chứa các nút thuộc các loại (biến thể) rất khác nhau, thì việc truy cập thực tế của các nút này về cơ bản sẽ dùng đến một chuỗi if-then-other (hoặc instanceof/ ischeck) khổng lồ . Những kiểm tra khổng lồ này có thể sẽ xảy ra ở nhiều nơi khác nhau trên toàn dự án. Đây là lý do tại sao enums có vẻ hữu ích, hoặc, ít nhất chúng cũng hữu ích như instanceof/ iskiểm tra.

Mẫu khách truy cập vẫn có thể hữu ích. Nói cách khác, có nhiều kiểu mã hóa khác nhau có thể được sử dụng thay vì chuỗi khổng lồ instanceof. Tuy nhiên, nếu bạn muốn thảo luận về các lợi ích và nhược điểm khác nhau, bạn sẽ chọn hiển thị một ví dụ mã từ chuỗi xấu nhất instanceoftrong dự án, thay vì ngụy biện về enum.

Điều này không có nghĩa là các lớp và hệ thống phân cấp thừa kế không hữu ích. Hoàn toàn ngược lại. Mặc dù không có bất kỳ hành vi đa hình nào hoạt động trên mọi loại khai báo (ngoài thực tế là mọi tuyên bố đều phải có Nametài sản), có rất nhiều hành vi đa hình phong phú được chia sẻ bởi anh chị em gần đó. Ví dụ, FunctionProcedurecó thể chia sẻ một số hành vi (cả hai đều có thể gọi được và chấp nhận danh sách các đối số đầu vào được nhập) và PropertyGetchắc chắn sẽ kế thừa các hành vi từ Function(cả hai đều có ReturnType). Bạn có thể sử dụng enum hoặc kiểm tra thừa kế cho chuỗi if-then-other khổng lồ, nhưng các hành vi đa hình, tuy nhiên bị phân mảnh, vẫn phải được thực hiện trong các lớp.

Có nhiều lời khuyên trực tuyến chống lại việc lạm dụng instanceof/ iskiểm tra. Hiệu suất không phải là một trong những lý do. Thay vào đó, lý do là để ngăn chặn lập trình viên phát hiện ra các hành vi đa hình phù hợp, như thể instanceof/ islà một cái nạng. Nhưng trong tình huống của bạn, bạn không có lựa chọn nào khác, vì các nút này có rất ít điểm chung.

Bây giờ đây là một số gợi ý cụ thể.


Có một số cách để đại diện cho các nhóm không lá.


So sánh đoạn trích sau đây của mã gốc của bạn ...

[Flags]
public enum DeclarationType
{
    Member = 1 << 7,
    Procedure = 1 << 8 | Member,
    Function = 1 << 9 | Member,
    Property = 1 << 10 | Member,
    PropertyGet = 1 << 11 | Property | Function,
    PropertyLet = 1 << 12 | Property | Procedure,
    PropertySet = 1 << 13 | Property | Procedure,
    LibraryFunction = 1 << 23 | Function,
    LibraryProcedure = 1 << 24 | Procedure,
}

đến phiên bản sửa đổi này:

[Flags]
public enum DeclarationType
{
    Nothing = 0, // to facilitate bit testing

    // Let's assume Member is not a concrete thing, 
    // which means it doesn't need its own bit
    /* Member = 1 << 7, */

    // Procedure and Function are concrete things; meanwhile 
    // they can still have sub-types.
    Procedure = 1 << 8, 
    Function = 1 << 9, 
    Property = 1 << 10,

    PropertyGet = 1 << 11,
    PropertyLet = 1 << 12,
    PropertySet = 1 << 13,

    LibraryFunction = 1 << 23,
    LibraryProcedure = 1 << 24,

    // new
    Procedures = Procedure | PropertyLet | PropertySet | LibraryProcedure,
    Functions = Function | PropertyGet | LibraryFunction,
    Properties = PropertyGet | PropertyLet | PropertySet,
    Members = Procedures | Functions | Properties,
    LibraryMembers = LibraryFunction | LibraryProcedure 
}

Phiên bản sửa đổi này tránh phân bổ bit cho các loại khai báo không cụ thể. Thay vào đó, các kiểu khai báo không cụ thể (các nhóm trừu tượng của các kiểu khai báo) chỉ đơn giản là có các giá trị enum là bitwise-hoặc (union của các bit) trên tất cả các phần tử con của nó.

Có một cảnh báo: nếu có một kiểu khai báo trừu tượng có một con duy nhất và nếu cần phân biệt giữa kiểu trừu tượng (cha mẹ) với kiểu cụ thể (con), thì kiểu trừu tượng vẫn sẽ cần bit riêng của nó .


Một cảnh báo cụ thể cho câu hỏi này: Propertyban đầu là một định danh (khi bạn chỉ nhìn thấy tên của nó, mà không thấy cách nó được sử dụng trong mã), nhưng nó có thể chuyển thành PropertyGet/ PropertyLet/ PropertySetngay khi bạn thấy nó được sử dụng như thế nào trong mã. Nói cách khác, ở các giai đoạn phân tích cú pháp khác nhau, bạn có thể cần phải đánh dấu một mã Propertyđịnh danh là "tên này đề cập đến một thuộc tính" và sau đó thay đổi thành "dòng mã này đang truy cập thuộc tính này theo một cách nhất định".

Để giải quyết cảnh báo này, bạn có thể cần hai bộ enum; một enum biểu thị tên (định danh) là gì; enum khác biểu thị những gì mã đang cố gắng làm (ví dụ: khai báo phần thân của một cái gì đó; cố gắng sử dụng một cái gì đó theo một cách nhất định).


Xem xét liệu thông tin phụ trợ về mỗi giá trị enum có thể được đọc từ một mảng thay thế hay không.

Đề xuất này loại trừ lẫn nhau với các đề xuất khác vì nó yêu cầu chuyển đổi các giá trị lũy thừa của hai giá trị thành các giá trị nguyên không âm nhỏ.

public enum DeclarationType
{
    Procedure = 8,
    Function = 9,
    Property = 10,
    PropertyGet = 11,
    PropertyLet = 12,
    PropertySet = 13,
    LibraryFunction = 23,
    LibraryProcedure = 24,
}

static readonly bool[] DeclarationTypeIsMember = new bool[32]
{
    ?, ?, ?, ?, ?, ?, ?, ?,                   // bit[0] ... bit[7]
    true, true, true, true, true, true, ?, ?, // bit[8] ... bit[15]
    ?, ?, ?, ?, ?, ?, ?, true,                // bit[16] ... bit[23]
    true, ...                                 // bit[24] ... 
}

static bool IsMember(DeclarationType dt)
{
    int intValue = (int)dt;
    return (intValue < 0 || intValue >= 32) ? false : DeclarationTypeIsMember[intValue];
    // you can also throw an exception if the enum is outside range.
}

// likewise for IsFunction(dt), IsProcedure(dt), IsProperty(dt), ...

Bảo trì sẽ có vấn đề.


Kiểm tra xem ánh xạ một-một giữa các loại C # (các lớp trong hệ thống phân cấp thừa kế) và các giá trị enum của bạn.

(Ngoài ra, bạn có thể điều chỉnh các giá trị enum của mình để đảm bảo ánh xạ một-một với các loại.)

Trong C #, rất nhiều thư viện lạm dụng Type object.GetType()phương pháp tiện lợi , tốt hay xấu.

Bất cứ nơi nào bạn đang lưu trữ enum như một giá trị, bạn có thể tự hỏi liệu bạn có thể lưu trữ Typedưới dạng giá trị thay thế không.

Để sử dụng thủ thuật này, bạn có thể khởi tạo hai bảng băm chỉ đọc, cụ thể là:

// For disambiguation, I'll assume that the actual 
// (behavior-implementing) classes are under the 
// "Lang" namespace.

static readonly Dictionary<Type, DeclarationType> TypeToDeclEnum = ... 
{
    { typeof(Lang.Procedure), DeclarationType.Procedure },
    { typeof(Lang.Function), DeclarationType.Function },
    { typeof(Lang.Property), DeclarationType.Property },
    ...
};

static readonly Dictionary<DeclarationType, Type> DeclEnumToType = ...
{
    // same as the first dictionary; 
    // just swap the key and the value
    ...
};

Sự minh chứng cuối cùng cho những người gợi ý các lớp và hệ thống phân cấp thừa kế ...

Một khi bạn có thể thấy rằng các enum là một xấp xỉ với hệ thống phân cấp thừa kế , lời khuyên sau đây giữ:

  • Thiết kế (hoặc cải thiện) hệ thống phân cấp thừa kế của bạn trước,
  • Sau đó quay trở lại và thiết kế enum của bạn để ước tính thứ bậc kế thừa đó.

Dự án thực sự là một bổ trợ VBIDE - Tôi đang phân tích và phân tích mã VBA =)
Mathieu Guindon

1

Tôi thấy việc bạn sử dụng cờ thực sự thông minh, sáng tạo, thanh lịch và có khả năng hiệu quả nhất. Tôi cũng không gặp khó khăn gì khi đọc nó.

Cờ là một phương tiện báo hiệu nhà nước, đủ điều kiện. Nếu tôi muốn biết nếu một cái gì đó là trái cây, tôi tìm

điều & hữu cơ. Quả! = 0

dễ đọc hơn

thingy & (Organic.Apple | Organic.Orange | Organic.Pear)! = 0

Điểm chính của enum Flag là cho phép bạn kết hợp nhiều trạng thái. Bạn chỉ cần làm điều này hữu ích hơn và dễ đọc hơn. Bạn truyền đạt khái niệm về trái cây trong mã của bạn, tôi không phải tự nhận ra rằng táo và cam và lê có nghĩa là trái cây.

Cung cấp cho anh chàng này một số điểm brownie!


4
thingy is Fruitmặc dù dễ đọc hơn
JacquesB
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.