web-dev-qa-db-ja.com

「グループ化」列挙型にフラグを使用するのは間違っていますか?

私の理解は[Flag]列挙型は通常、組み合わせ可能なものに使用されます。個々の値は相互に排他的ではありません

例えば:

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

ここで、SomeAttributes値は、FooBar、およびBazの組み合わせにすることができます。

より複雑な 実際のシナリオ では、列挙型を使用して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
}

明らかに、指定されたDeclarationは、VariableLibraryProcedureの両方にすることはできません。2つの個別の値を組み合わせることはできません。

これらのフラグは非常に便利です(与えられたDeclarationTypePropertyであるかModuleであるかを確認するのは非常に簡単です)が、フラグはそうではないので「間違っている」と感じますreallycombining値に使用されますが、groupingそれらを「サブタイプ」に。

したがって、これは列挙型フラグを乱用していると言われます- この答え 本質的に、リンゴに適用できる値のセットとオレンジに適用できる別のセットがある場合、リンゴには別の列挙型が必要ですもう1つはオレンジ用です。ここでの問題は、すべての宣言に共通のインターフェースが必要であり、DeclarationTypeがベースDeclarationクラスで公開されていることです:PropertyType enumはまったく役に立ちません。

これはずさんな/意外な/虐待的なデザインですか?もしそうなら、その問題は通常どのように解決されますか?

12
Mathieu Guindon

これは間違いなく列挙型とフラグを悪用しています!それはあなたのために働くかもしれませんが、コードを読んでいる他の誰もが非常に混乱するでしょう。

私が正しく理解していれば、宣言の階層分類があります。これは、単一の列挙型でエンコードする多くの情報に対してfarです。しかし、明らかな代替策があります:クラスと継承を使用してください!したがって、MemberDeclarationTypeから継承し、PropertyMemberから継承します。

列挙型は特定の状況に適しています。値が常に限られた数のオプションの1つである場合、または限られた数のオプション(フラグ)の任意の組み合わせである場合。これより複雑または構造化された情報は、オブジェクトを使用して表現する必要があります。

編集:「実際のシナリオ」では、 動作が選択されている の値に応じて複数の場所があるようです列挙型。 switch + enumを「貧乏人のポリモーフィズム」として使用しているため、これは実際にはアンチパターンです。列挙型の値を、宣言固有の動作をカプセル化する個別のクラスに変換するだけで、コードがよりクリーンになります

10
JacquesB

私はこのアプローチを読んで理解するのに十分簡単だと思います。私見、それは混乱するものではありません。そうは言っても、私はこのアプローチについていくつか懸念があります。

  1. 私の主な予約は、これを強制する方法はないということです。

    明らかに、指定された宣言は、変数とLibraryProcedureの両方にすることはできません。2つの個別の値組み合わせることはできません ..でありません。

    上記の組み合わせは宣言しませんが、このコードは

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;
    

    完全に有効です。そして、コンパイル時にこれらのエラーをキャッチする方法はありません。

  2. ビットフラグでエンコードできる情報の量には制限があります。これは64ビットですか。今のところ、intのサイズに危険なほど近づいています。この列挙型が増え続けると、最終的にビットが足りなくなる可能性があります...

結論としては、これは有効なアプローチだと思いますが、フラグの大規模/複雑な階層に使用するのをためらいます。

6
Nikita B

TL; DR一番下までスクロールします。


私が見るところによると、あなたはC#の上に新しい言語を実装しています。列挙型は識別子のタイプ(または名前があり、新しい言語のソースコードに表示されるもの)を示しているようです。これは、プログラムのツリー表現に追加されるノードに適用されるようです。

この特定の状況では、異なるタイプのノード間でのポリモーフィックな動作はほとんどありません。つまり、ツリーには非常に異なるタイプ(バリアント)のノードを含めることができる必要がありますが、これらのノードへの実際のアクセスは、基本的に巨大なif-then-elseチェーン(またはinstanceof/isチェック)。これらの巨大なチェックは、プロジェクトのさまざまな場所で発生する可能性があります。これが、列挙型が役立つように見える理由、または列挙型がinstanceof/isチェックと少なくとも同じくらい役立つ理由です。

ビジターパターン はまだ役立つかもしれません。つまり、instanceofの巨大なチェーンの代わりに使用できるさまざまなコーディングスタイルがあります。ただし、さまざまな長所と短所について説明したい場合は、列挙型についていちいち説明するのではなく、プロジェクト内のinstanceofの最も醜いチェーンからコード例を紹介することを選択します。

これは、クラスや継承階層が役に立たないと言っているのではありません。まったく逆です。 すべての宣言型で動作する多態性の動作はありませんが(すべての宣言にNameプロパティが必要であるという事実を除いて)、たくさんあります近くの兄弟が共有する豊富なポリモーフィックな動作。たとえば、FunctionProcedureはおそらくいくつかの動作(呼び出し可能であり、型付き入力引数のリストを受け入れる)を共有し、PropertyGetFunction(両方にReturnTypeがあります)。巨大なif-then-elseチェーンには列挙型または継承チェックのいずれかを使用できますが、ポリモーフィックな動作は、断片化されていても、クラスに実装する必要があります。

instanceof/isチェックの乱用に対するオンラインのアドバイスはたくさんあります。 パフォーマンスは理由の1つではありません。 むしろ、理由は、instanceof/isが松葉杖であるかのように、プログラマーが適切な多態性の動作を有機的に発見できないようにすることです。ただし、これらのノードには共通点がほとんどないため、状況によっては他に選択肢はありません。

今ここにいくつかの具体的な提案があります。


非リーフのグループを表す方法はいくつかあります。


次の元のコードの抜粋を比較してください...

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

この変更されたバージョンに:

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

この変更されたバージョンでは、非コンクリート宣言型へのビットの割り当てが回避されます。代わりに、非具体的な宣言型(宣言型の抽象的なグループ)は、そのすべての子にわたってビット単位OR(ビットの和集合)である列挙値を持ちます。

警告があります:単一の子を持つ抽象宣言タイプがあり、抽象のもの(親)と具象のもの(子)を区別する必要がある場合、抽象のものには独自のビットが必要です。 。


この質問に固有の注意点の1つ:Propertyは最初は識別子です(コードでの使用方法を確認せずに名前を確認しただけです)が、PropertyGetに変換される場合があります/ PropertyLet/PropertySetコードでどのように使用されているかがわかるとすぐに。つまり、解析のさまざまな段階で、Property識別子を「この名前はプロパティを参照している」としてマークし、後で「このコード行がこのプロパティにアクセスしているコード」に変更する必要がある場合があります。特定の方法」。

この警告を解決するには、2セットの列挙が必要になる場合があります。 1つの列挙型は名前(識別子)が何であるかを示します。別の列挙型は、コードが実行しようとしていることを示します(たとえば、何かの本体を宣言する、特定の方法で何かを使用しようとする)。


代わりに、各列挙値に関する補助情報を配列から読み取ることができるかどうかを検討してください。

この提案は、2の累乗の値を小さな非負の整数値に戻す必要があるため、他の提案と相互に排他的です。

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

保守性が問題になるでしょう。


C#タイプ(継承階層のクラス)と列挙値の間の1対1のマッピングかどうかを確認します。

(または、列挙型の値を微調整して、型との1対1のマッピングを確実にすることができます。)

C#では、多くのライブラリが、良いか悪いかで、気の利いたType object.GetType()メソッドを乱用しています。

Enumを値として保存している場所ならどこでも、代わりにTypeを値として保存できるかどうかを自問するかもしれません。

このトリックを使用するには、次の2つの読み取り専用ハッシュテーブルを初期化できます。

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

クラスと継承階層を提案する人に対する最後の正当性...

列挙型が継承階層の近似値であることがわかると、次のアドバイスが適用されます。

  • 最初に継承階層を設計(または改善)します。
  • 次に戻って列挙型を設計し、その継承階層を近似します。
3
rwong

フラグの使用は本当にスマートで、クリエイティブで、エレガントで、潜在的に最も効率的です。読んでも全く問題ありません。

フラグは、状態を通知し、修飾する手段です。何かが果物かどうか知りたいなら、

thing&Organic.Fruit!= 0

より読みやすい

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

Flag列挙型の重要な点は、複数の状態を組み合わせられるようにすることです。これをより便利で読みやすくしました。あなたはコードで果物の概念を伝えます、私はAppleそしてオレンジとナシが果物を意味することを自分で理解する必要はありません。

この男にブラウニーポイントを与えてください!

1
Martin Maat