Apakah salah menggunakan bendera untuk "pengelompokan" enum?

12

Pemahaman saya adalah bahwa [Flag]enum biasanya digunakan untuk hal-hal yang dapat digabungkan, di mana nilai-nilai individual tidak saling eksklusif .

Sebagai contoh:

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

Di mana SomeAttributesnilai apa pun bisa merupakan kombinasi dari Foo, Bardan Baz.

Dalam skenario kehidupan nyata yang lebih rumit , saya menggunakan enum untuk menggambarkan 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
}

Jelas suatu pemberian Declarationtidak dapat berupa a Variabledan a LibraryProcedure- dua nilai individual tidak dapat digabungkan .. dan tidak.

Walaupun flag ini sangat berguna (sangat mudah untuk memverifikasi apakah yang diberikan DeclarationTypeadalah a Propertyatau a Module), rasanya "salah" karena flag tersebut tidak benar - benar digunakan untuk menggabungkan nilai, tetapi lebih untuk mengelompokkannya menjadi "sub-tipe".

Jadi saya diberitahu bahwa ini adalah penyalahgunaan enum flags - jawaban ini pada dasarnya mengatakan bahwa jika saya memiliki satu set nilai yang berlaku untuk apel dan set lain yang berlaku untuk jeruk, maka saya memerlukan jenis enum berbeda untuk apel dan satu lagi untuk jeruk - yang masalah dengan itu di sini adalah bahwa saya perlu semua deklarasi untuk memiliki antarmuka yang sama, dengan DeclarationTypediekspos di Declarationkelas dasar : memiliki PropertyTypeenum tidak akan berguna sama sekali.

Apakah ini desain yang ceroboh / mengejutkan / kasar? Jika demikian, lalu bagaimana masalah itu biasanya diselesaikan?

Mathieu Guindon
sumber
Pertimbangkan bagaimana orang akan menggunakan objek bertipe DeclarationType. Jika saya ingin menentukan apakah xini subtipe atau tidak y, saya mungkin ingin menuliskannya sebagai x.IsSubtypeOf(y), bukan sebagai x && y == y.
Tanner Swett
1
@ TannerSwett Itulah tepatnya apa yang x.HasFlag(DeclarationType.Member)dilakukan ....
Mathieu Guindon
Ya itu benar. Tetapi jika metode Anda disebut HasFlagbukan IsSubtypeOf, maka saya perlu cara lain untuk mengetahui bahwa apa yang sebenarnya dimaksud adalah "adalah subtipe dari". Anda dapat membuat metode ekstensi, tetapi sebagai pengguna, apa yang saya temukan paling mengejutkan adalah DeclarationTypehanya menjadi struct yang dimiliki IsSubtypeOfsebagai metode nyata .
Tanner Swett

Jawaban:

10

Ini jelas menyalahgunakan enum dan bendera! Ini mungkin bekerja untuk Anda, tetapi siapa pun yang membaca kode akan sangat bingung.

Jika saya mengerti dengan benar, Anda memiliki klasifikasi deklarasi hirarkis . Ini adalah jauh untuk banyak informasi untuk encode dalam enum tunggal. Tetapi ada alternatif yang jelas: Gunakan kelas dan warisan! Jadi Membermewarisi dari DeclarationType, Propertymewarisi dari Memberdan seterusnya.

Enum sesuai dalam beberapa keadaan tertentu: Jika suatu nilai selalu merupakan salah satu dari sejumlah opsi yang terbatas, atau jika itu merupakan kombinasi dari sejumlah opsi (bendera) tertentu. Setiap informasi yang lebih kompleks atau terstruktur dari ini harus direpresentasikan menggunakan objek.

Sunting : Dalam "skenario kehidupan nyata" Anda, tampaknya ada beberapa tempat di mana perilaku dipilih tergantung pada nilai enum. Ini benar-benar antipattern, karena Anda menggunakan switch+ enumsebagai "polimorfisme orang miskin". Ubah saja nilai enum menjadi kelas yang berbeda yang merangkum perilaku khusus deklarasi, dan kode Anda akan jauh lebih bersih

JacquesB
sumber
Warisan adalah ide yang bagus tetapi jawaban Anda menyiratkan kelas per enum, yang tampaknya berlebihan.
Frank Hileman
@ FrankHileman: "daun" dalam hierarki dapat direpresentasikan sebagai nilai enum daripada kelas, tapi saya tidak yakin itu akan lebih baik. Tergantung jika perilaku yang berbeda akan dikaitkan dengan nilai enum yang berbeda, dalam hal ini kelas yang berbeda akan lebih baik.
JacquesB
4
Klasifikasi tidak hierarkis, kombinasinya adalah keadaan yang telah ditentukan. Menggunakan warisan untuk ini akan benar-benar penyalahgunaan, penyalahgunaan kelas itu. Dengan kelas saya berharap setidaknya beberapa data atau perilaku. Sekelompok kelas tanpa keduanya adalah ... benar, sebuah enum.
Martin Maat
Tentang tautan itu ke repo saya: perilaku per-tipe lebih terkait dengan tipe ParserRuleContextkelas yang dihasilkan daripada dengan enum. Kode itu sedang mencoba untuk mendapatkan posisi token tempat memasukkan As {Type}klausa dalam deklarasi; ini ParserRuleContextkelas -derived dihasilkan oleh Antr4 per tata bahasa yang mendefinisikan aturan parser - Saya tidak benar-benar mengontrol node pohon parsing [bukan dangkal] warisan hirarki, meskipun saya dapat memanfaatkan mereka partial-ness untuk membuat mereka mengimplementasikan antarmuka, misalnya untuk membuat mereka memperlihatkan beberapa AsTypeClauseTokenPositionproperti .. banyak pekerjaan.
Mathieu Guindon
6

Saya menemukan pendekatan ini cukup mudah untuk dibaca dan dipahami. IMHO, itu bukan sesuatu yang membingungkan. Yang sedang berkata, saya memiliki beberapa kekhawatiran tentang pendekatan ini:

  1. Reservasi utama saya adalah bahwa tidak ada cara untuk menegakkan ini:

    Jelas suatu Deklarasi yang diberikan tidak bisa berupa Variabel dan LibraryProcedure - dua nilai individual tidak dapat digabungkan .. dan tidak.

    Meskipun Anda tidak mendeklarasikan kombinasi di atas, kode ini

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    Sangat valid. Dan tidak ada cara untuk menangkap kesalahan itu pada waktu kompilasi.

  2. Ada batasan berapa banyak informasi yang dapat Anda encode dalam flag bit, yang mana, 64 bit? Untuk sekarang Anda semakin mendekati ukuran intdan jika enum ini terus tumbuh Anda mungkin akhirnya hanya kehabisan bit ...

Intinya adalah, saya pikir ini adalah pendekatan yang valid, tetapi saya akan ragu untuk menggunakannya untuk hierarki bendera yang besar / kompleks.

Nikita B
sumber
Jadi tes unit parser menyeluruh akan membahas # 1 saat itu. FWIW enum dimulai sebagai standar no-flag enum sekitar 3 tahun yang lalu .. Setelah semakin lelah selalu memeriksa beberapa nilai tertentu (misalnya, dalam inspeksi kode) yang muncul bendera enum. Daftar ini tidak benar-benar akan tumbuh dari waktu ke waktu juga, tapi ya mengalahkan intkapasitas adalah masalah nyata.
Mathieu Guindon
3

TL; DR Gulir ke bagian paling bawah.


Dari apa yang saya lihat, Anda menerapkan bahasa baru di atas C #. Enum tampaknya menunjukkan tipe pengidentifikasi (atau, apa pun yang memiliki nama dan yang muncul dalam kode sumber bahasa baru), yang tampaknya diterapkan pada node yang akan ditambahkan ke representasi pohon dari program.

Dalam situasi khusus ini, ada sangat sedikit perilaku polimorfik antara berbagai jenis node. Dengan kata lain, sementara itu perlu bagi pohon untuk dapat mengandung node dari jenis yang sangat berbeda (varian), visitasi sebenarnya node ini akan pada dasarnya resor untuk raksasa if-then-else rantai (atau instanceof/ iscek). Cek raksasa ini kemungkinan akan terjadi di banyak tempat berbeda di seluruh proyek. Ini adalah alasan mengapa enum mungkin tampak membantu, atau, mereka setidaknya sama bermanfaatnya dengan instanceof/ ismemeriksa.

Pola pengunjung mungkin masih bermanfaat. Dengan kata lain, ada berbagai gaya pengkodean yang dapat digunakan sebagai ganti rantai raksasa instanceof. Namun, jika Anda ingin diskusi tentang berbagai manfaat dan kelemahan, Anda akan memilih untuk menampilkan contoh kode dari rantai paling jelek instanceofdalam proyek, daripada berdalih tentang enum.

Ini bukan untuk mengatakan kelas dan hierarki warisan tidak berguna. Justru sebaliknya. Meskipun tidak ada perilaku polimorfik yang bekerja di setiap jenis deklarasi (selain dari fakta bahwa setiap deklarasi harus memiliki Nameproperti), ada banyak perilaku polimorfik kaya yang dimiliki oleh saudara kandung terdekat. Misalnya, Functiondan Proceduremungkin berbagi beberapa perilaku (keduanya bisa dipanggil dan menerima daftar argumen input yang diketik), dan PropertyGetpasti akan mewarisi perilaku dari Function(keduanya memiliki a ReturnType). Anda mungkin menggunakan enum atau pemeriksaan warisan untuk rantai raksasa jika-kemudian-yang lain, tetapi perilaku polimorfik, betapapun terfragmentasi, masih harus diimplementasikan di kelas.

Ada banyak saran online yang melarang penggunaan instanceof/ ispemeriksaan berlebihan . Performa bukan salah satu alasannya. Sebaliknya, alasannya adalah untuk mencegah programmer dari organik menemukan perilaku polimorfik cocok, seakan instanceof/ isadalah penopang. Tetapi dalam situasi Anda, Anda tidak punya pilihan lain, karena simpul-simpul ini memiliki sedikit persamaan.

Sekarang, inilah beberapa saran konkret.


Ada beberapa cara untuk mewakili pengelompokan non-daun.


Bandingkan kutipan kode asli Anda berikut ...

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

ke versi modifikasi ini:

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

Versi modifikasi ini menghindari mengalokasikan bit ke jenis deklarasi non-konkret. Sebaliknya, tipe deklarasi non-konkret (pengelompokan abstrak tipe deklarasi) hanya memiliki nilai enum yang merupakan bitwise-atau (penyatuan bit) di semua anak-anaknya.

Ada peringatan: jika ada jenis deklarasi abstrak yang memiliki anak tunggal, dan jika ada kebutuhan untuk membedakan antara yang abstrak (orang tua) dari yang konkret (anak), maka yang abstrak masih akan membutuhkan bitnya sendiri. .


Satu peringatan yang khusus untuk pertanyaan ini: a Propertypada awalnya adalah pengidentifikasi (ketika Anda baru saja melihat namanya, tanpa melihat bagaimana itu digunakan dalam kode), tetapi dapat mentransmutasikan ke PropertyGet/ PropertyLet/ PropertySetsegera setelah Anda melihat bagaimana itu digunakan dalam kode. Dengan kata lain, pada tahapan parsing yang berbeda, Anda mungkin perlu menandai Propertypengidentifikasi sebagai "nama ini merujuk ke properti", dan kemudian mengubahnya menjadi "baris kode ini mengakses properti ini dengan cara tertentu".

Untuk mengatasi peringatan ini, Anda mungkin perlu dua set enum; satu enum menunjukkan apa nama (pengidentifikasi) itu; enum lain menunjukkan apa yang coba dilakukan oleh kode (misalnya mendeklarasikan isi dari sesuatu; mencoba menggunakan sesuatu dengan cara tertentu).


Pertimbangkan apakah informasi tambahan tentang setiap nilai enum dapat dibaca dari sebuah array sebagai gantinya.

Saran ini saling eksklusif dengan saran lain karena memerlukan konversi nilai kekuatan-dua kembali ke nilai integer kecil non-negatif.

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), ...

Maintainability akan menjadi masalah.


Periksa apakah pemetaan satu-ke-satu antara tipe C # (kelas dalam hierarki warisan) dan nilai enum Anda.

(Atau, Anda dapat mengubah nilai enum Anda untuk memastikan pemetaan satu-ke-satu dengan tipe-tipe.)

Di C #, banyak perpustakaan menyalahgunakan Type object.GetType()metode bagus , baik atau buruk.

Di mana pun Anda menyimpan enum sebagai nilai, Anda mungkin bertanya pada diri sendiri apakah Anda dapat menyimpan enum sebagai Typenilai.

Untuk menggunakan trik ini, Anda dapat menginisialisasi dua tabel hash read-only, yaitu:

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

Pembenaran akhir bagi mereka yang menyarankan kelas dan hierarki warisan ...

Setelah Anda dapat melihat bahwa enum adalah perkiraan untuk hierarki warisan , saran berikut berlaku:

  • Desain (atau tingkatkan) hierarki warisan Anda terlebih dahulu,
  • Kemudian kembali dan rancang enum Anda untuk memperkirakan hierarki pewarisan itu.
rwong
sumber
Proyek ini sebenarnya adalah add-in VBIDE - Saya mem-parsing dan menganalisis kode VBA =)
Mathieu Guindon
1

Saya menemukan penggunaan bendera Anda sangat cerdas, kreatif, elegan dan berpotensi paling efisien. Saya tidak kesulitan sama sekali membacanya juga.

Bendera adalah sarana pensinyalan, kualifikasi. Jika saya ingin tahu apakah sesuatu itu buah, saya temukan

thingy & Organic.Buah! = 0

lebih mudah dibaca daripada

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

Inti dari Flag enums adalah untuk memungkinkan Anda menggabungkan beberapa status. Anda baru saja menjadikan ini lebih bermanfaat dan mudah dibaca. Anda menyampaikan konsep buah dalam kode Anda, saya tidak perlu mencari tahu sendiri bahwa apel dan jeruk dan pir berarti buah.

Berikan orang ini poin brownies!

Martin Maat
sumber
4
thingy is Fruitlebih mudah dibaca daripada keduanya.
JacquesB