Numaraları “gruplandırmak” için bayrak kullanmak yanlış mı?


12

Anladığım kadarıyla, sıralamalar [Flag]genellikle bireysel değerlerin birbirini dışlamadığı, birleştirilebilen şeyler için kullanılır .

Örneğin:

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

Burada herhangi bir SomeAttributesdeğeri bir kombinasyonu olabilir Foo, Barve Baz.

Daha karmaşık, gerçek yaşam senaryosunda aşağıdakileri tanımlamak için bir numaralandırma kullanırım 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
}

Açıkçası, belirli bir Declarationa hem olamaz Variableve bir LibraryProcedureiki tek tek değerler kombine edilemez .. ve öyle değiller -.

Bu bayraklar son derece yararlı olsa da (bir istatistiğini verilen olup olmadığını doğrulamak için çok kolay DeclarationTypebir olduğunu Propertyya da Modulebayraklar değil, bunun nedeni "yanlış" hissediyor) gerçekten kullanılan birleştirerek değerleri değil, için gruplama "alt-türleri" içine.

Bu yüzden bunun enum bayraklarını kötüye kullandığı söylendi - bu cevap aslında elma için geçerli bir değer kümesi ve portakal için geçerli başka bir kümem varsa, o zaman elma için farklı bir numara türüne ve portakal için başka bir numaraya ihtiyacım olduğunu söylüyor. Burada sorun DeclarationType, temel Declarationsınıfta maruz kalma , ortak bir arayüze sahip tüm bildirimlere ihtiyacım var : bir PropertyTypenumaralandırma sahip olmak hiç yararlı olmaz.

Bu özensiz / şaşırtıcı / küfürlü bir tasarım mı? Eğer öyleyse, o zaman bu problem nasıl çözülür?


İnsanların tür nesnelerini nasıl kullanacaklarını düşünün DeclarationType. Eğer xbir alt türü olup olmadığını belirlemek yistersem, muhtemelen bunu x.IsSubtypeOf(y)değil olarak yazmak isteyeceğim x && y == y.
Tanner Swett

1
@TannerSwett Bu tam olarak ne x.HasFlag(DeclarationType.Member)yapıyor ....
Mathieu Guindon

Evet bu doğru. Ancak yöntem HasFlagyerine bunun yerine çağrılırsa IsSubtypeOf, o zaman gerçekten ne anlama geldiğini "alt türü" olduğunu bulmak için başka bir yol gerekir . Sen bir uzantısı yöntemi oluşturmak, ama az şaşırtıcı ne bulacağını bir kullanıcı olarak içindir olabilir DeclarationTypesadece sahip bir yapı olarak IsSubtypeOfbir olarak gerçek yöntemle.
Tanner Swett

Yanıtlar:


10

Bu kesinlikle numaralandırmalar ve bayrakları kötüye kullanıyor! Sizin için işe yarayabilir, ancak kodu okuyan herkesin kafası karışık olacaktır.

Doğru anlıyorsam , beyanların hiyerarşik bir sınıflandırmasına sahipsiniz . Bu, tek bir numarada kodlanacak kadar çok bilgi. Ancak bariz bir alternatif var: Sınıfları ve kalıtımı kullanın! Yani Membermiras alır DeclarationType, Propertymiras alır Membervb.

Numaralandırmalar bazı özel durumlarda uygundur: Bir değer her zaman sınırlı sayıda seçenekten biriyse veya sınırlı sayıda seçeneğin (bayrak) herhangi bir birleşimi ise. Bundan daha karmaşık veya yapılandırılmış herhangi bir bilgi, nesneler kullanılarak temsil edilmelidir.

Düzenleme : "Gerçek hayat senaryo" enum değerine bağlı olarak davranış seçildiği birden çok yer var gibi görünüyor . switch+ ' Yı enum"kötü adamın polimorfizmi" olarak kullandığınız için bu gerçekten bir antipattern . Enum değerini, bildirime özgü davranışı kapsayan farklı sınıflara dönüştürün ve kodunuz çok daha temiz olacak


Kalıtım iyi bir fikirdir ama cevabınız, numaralandırma başına aşırı sınıf gibi görünen bir sınıfı ima eder.
Frank Hileman

@FrankHileman: Hiyerarşideki "yapraklar" sınıflar yerine enum değerleri olarak gösterilebilir, ancak daha iyi olacağından emin değilim. Farklı davranışların farklı numaralandırma değerleriyle ilişkilendirilip ilişkilendirilmeyeceğine bağlıdır, bu durumda farklı sınıflar daha iyi olur.
JacquesB

4
Sınıflandırma hiyerarşik değildir, kombinasyonlar önceden tanımlanmış durumlardır. Bunun için miras kullanmak gerçekten istismar, sınıfların kötüye kullanılmasıdır. Bir sınıfla en azından bazı veriler veya davranışlar bekliyorum. İkisi de olmayan bir grup sınıf ... doğru, bir numara.
Martin Maat

Repoma bağlantı hakkında: tür başına davranış, ParserRuleContextoluşturulan sınıfın türü ile enumdan daha fazla ilgilidir. Bu kod, As {Type}beyanda bir cümle eklemek için jeton konumunu almaya çalışıyor ; bu ParserRuleContext-derived sınıflar, ayrıştırıcı kurallarını tanımlayan bir dilbilgisi başına Antr4 tarafından oluşturulur - ayrıştırıcı ağaç düğümlerinin [oldukça sığ] kalıtım hiyerarşisini gerçekten kontrol etmiyorum, ancak partialarayüzlerini uygulamalarını sağlamak için -ness'ten yararlanabiliyorum, örneğin onları bazı AsTypeClauseTokenPositionmalları açığa çıkarmak .. çok iş.
Mathieu Guindon

6

Bu yaklaşımı okumak ve anlamak için yeterince kolay buluyorum. IMHO, kafan karışacak bir şey değil. Bununla birlikte, bu yaklaşımla ilgili bazı endişelerim var:

  1. Benim ana rezervasyon bu zorlamak için bir yolu yoktur:

    Belli bir Beyanname hem Değişken hem de LibraryProcedure olamaz - iki ayrı değer birleştirilemez .. ve değildir.

    Yukarıdaki kombinasyonu bildirmemenize rağmen, bu kod

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    Mükemmel bir şekilde geçerlidir. Ve bu hataları derleme zamanında yakalamanın bir yolu yoktur.

  2. Bit bayraklarında ne kadar bilgi kodlayabileceğinize dair bir sınır var, hangisi 64 bit? Şimdilik büyüklüğüne tehlikeli bir şekilde yaklaşıyorsunuz intve eğer bu numaralandırma büyümeye devam ederse, sonunda sadece bitleriniz bitebilir ...

Sonuç olarak, bunun geçerli bir yaklaşım olduğunu düşünüyorum, ancak büyük / karmaşık bayrak hiyerarşileri için kullanmakta tereddüt ederdim.


Böylece ayrıntılı ayrıştırıcı birimi testleri # 1'i ele alacaktır. FWIW enum yaklaşık 3 yıl önce standart bir bayraksız numaralandırma olarak başladı .. Numaralandırma bayraklarının ortaya çıktığı bazı belirli değerleri (örneğin, kod denetimlerinde) her zaman kontrol etmekten bıktınız. Liste de zamanla büyümeyecek, ancak evet, intkapasiteyi aşmak gerçek bir endişe kaynağı.
Mathieu Guindon

3

TL; DR En alta kaydırın.


Gördüğüm kadarıyla, C # üzerine yeni bir dil uyguluyorsunuz. Sıralamalar, programın ağaç temsiline eklenecek düğümlere uygulanmış gibi görünen bir tanımlayıcının türünü (veya adı olan ve yeni dilin kaynak kodunda görünen herhangi bir şeyi) belirtiyor gibi görünüyor.

Bu özel durumda, farklı tipteki düğümler arasında çok az polimorfik davranış vardır. Başka bir deyişle, ağacın çok farklı tiplerde (varyantlar) düğümler içerebilmesi gerekirken, bu düğümlerin gerçek ziyareti temelde dev bir if-then-else zincirine (veya instanceof/ iskontrollerine) başvurur . Bu dev denetimler muhtemelen proje boyunca birçok farklı yerde gerçekleşecek. Numaralandırmaların yararlı görünmesinin nedeni budur veya en azından instanceof/ isdenetimler kadar yararlıdır .

Ziyaretçi kalıbı yine de faydalı olabilir. Başka bir deyişle, dev zincirinin yerine kullanılabilecek çeşitli kodlama stilleri vardır instanceof. Bununla birlikte, çeşitli avantajlar ve dezavantajlar hakkında bir tartışma yapmak isterseniz, numaralandırmalar hakkında tartışmak instanceofyerine, projenin en çirkin zincirinden bir kod örneği göstermeyi seçersiniz.

Bu, sınıfların ve miras hiyerarşisinin yararlı olmadığı anlamına gelmez. Tam tersi. Her bildiri türünde (her bildirimin bir Nameözelliğe sahip olması gerçeğinin yanı sıra) çalışan herhangi bir polimorfik davranış olmasa da , yakın kardeşler tarafından paylaşılan çok sayıda zengin polimorfik davranış vardır. Örneğin Functionve Proceduremuhtemelen bazı davranışları paylaşın (her ikisi de çağrılabilir ve yazılan girdi bağımsız değişkenlerinin bir listesini kabul eder) ve PropertyGetdavranışları kesinlikle devralır Function(her ikisinde de a vardır ReturnType). Dev if-then-else zinciri için numaralandırmalar veya miras denetimleri kullanabilirsiniz, ancak parçalara ayrılmış polimorfik davranışlar yine de sınıflarda uygulanmalıdır.

Aşırı kullanım instanceof/ iskontrollere karşı birçok çevrimiçi tavsiye var . Performans bunun nedenlerinden biri değildir. Aksine, neden sanki organik olarak, uygun polimorfik davranışları keşfetme gelen programcı önlemektir instanceof/ isbir koltuk değneği. Ancak sizin durumunuzda, başka seçeneğiniz yoktur, çünkü bu düğümlerin ortak noktası çok azdır.

Şimdi bazı somut öneriler.


Yaprak olmayan gruplamaları temsil etmenin birkaç yolu vardır.


Orijinal kodunuzun aşağıdaki alıntısını karşılaştırı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,
}

bu değiştirilmiş sürüme:

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

Bu değiştirilmiş versiyon, bitlerin somut olmayan bildirim türlerine tahsis edilmesini önler. Bunun yerine, somut olmayan bildirim türlerinin (bildiri türlerinin soyut gruplamaları) tüm çocuklarında bitsel veya bitlerin birliği olan enum değerleri vardır.

Bir uyarı var: tek bir çocuğu olan soyut bir bildiri türü varsa ve soyut olanı (ebeveyn) somut olandan (çocuk) ayırt etmeye ihtiyaç varsa, soyut olanın hala kendi bitine ihtiyacı olacaktır. .


Bu sorunun özgüdür Bir ihtar: Bir Property(sadece o kodda nasıl kullanıldığını görmeden, adını görünce) başlangıçta bir tanımlayıcı, ama gecirmislerdir olabilir PropertyGet/ PropertyLet/ PropertySeten kısa sürede bunu nasıl kullanıldığını görüyoruz kodda. Başka bir deyişle, ayrıştırma işleminin farklı aşamalarında, Propertytanımlayıcıyı "bu ad bir özelliğe başvuruyor" olarak işaretlemeniz ve daha sonra "bu kod satırı bu özelliğe belirli bir şekilde erişiyor" olarak değiştirmeniz gerekebilir .

Bu uyarıyı çözmek için iki dizi numaraya ihtiyacınız olabilir; bir enum, bir adın (tanımlayıcı) ne olduğunu belirtir; başka bir enum, kodun ne yapmaya çalıştığını gösterir (örneğin, bir şeyin gövdesini bildirmek; bir şeyi belirli bir şekilde kullanmaya çalışmak).


Her bir enum değeri hakkındaki yardımcı bilgilerin bunun yerine bir diziden okunup okunamayacağını düşünün.

Bu öneri diğer önerilerle karşılıklı olarak münhasırdır, çünkü iki güç değerini küçük negatif olmayan tamsayı değerlerine dönüştürmeyi gerektirir.

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

Sürdürülebilirlik sorunlu olacaktır.


C # türleri (kalıtım hiyerarşisindeki sınıflar) ile numaralandırma değerleriniz arasında bire bir eşleme olup olmadığını kontrol edin.

(Alternatif olarak, türlerle bire bir eşleme sağlamak için numaralandırma değerlerinizi düzenleyebilirsiniz.)

C # 'ta birçok kütüphane Type object.GetType()iyi veya kötü için şık yöntemi kötüye kullanır.

Numaralandırmayı bir değer olarak depoladığınız her yerde kendinize Typebunun yerine değer olarak depolayıp depolayamayacağınızı sorabilirsiniz .

Bu hileyi kullanmak için, iki salt okunur karma tabloyu başlatabilirsiniz:

// 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ınıfları ve miras hiyerarşisini önerenler için son onaylama ...

Numaralandırmaların kalıtım hiyerarşisine bir yaklaşım olduğunu görebildiğinizde , aşağıdaki öneriler geçerlidir:

  • Önce miras hiyerarşinizi tasarlayın (veya geliştirin),
  • Ardından geri dönün ve numaralarınızı bu miras hiyerarşisine yaklaşacak şekilde tasarlayın.

Proje aslında bir VBIDE eklentisidir - VBA kodunu ayrıştırıyorum ve analiz ediyorum =)
Mathieu Guindon

1

Bayrak kullanımınızı gerçekten akıllı, yaratıcı, zarif ve potansiyel olarak en verimli buluyorum. Ben de okurken hiç sorun yaşamıyorum.

Bayraklar, devlete işaret etme, yeterlilik sağlama aracıdır. Eğer bir şeyin meyve olup olmadığını bilmek istersem,

thingy & Organik Meyve! = 0

daha okunabilir

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

Bayrak numaralandırmalarının asıl noktası, birden çok durumu birleştirmenize izin vermektir. Bunu daha kullanışlı ve okunaklı yaptınız. Kodunuzdaki meyve kavramını aktarıyorsunuz, kendimi elma ve portakal ve armutun meyve anlamına geldiğini anlamak zorunda değilim.

Bu adama biraz brownie puanı verin!


4
thingy is Fruither ikisinden de daha okunabilir.
JacquesB
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.