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


12

私の理解では、[Flag]enumは通常、個々の値が相互に排他的でない組み合わせ可能なものに使用されるということです。

例えば:

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

どこにどんなSomeAttributes値がの組み合わせとすることができるFooBarBaz

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

明らかに、Declarationa Variableとaの両方を指定することはできませんLibraryProcedure。2つの個別の値を組み合わせることはできません。そうではありません。

これらのフラグは非常に有用であるが(それは、与えられたのかどうかを確認することは非常に簡単ですDeclarationTypeであるPropertyModuleのフラグがされていないので、それは「間違った」と感じている)実際に使用する組み合わせた値を、ではなくのためのグループ化「サブタイプ」にそれらを。

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

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


人がタイプのオブジェクトをどう使用するか考えてくださいDeclarationType。のxサブタイプであるかどうかを判断したい場合は、yおそらくとしてx.IsSubtypeOf(y)ではなくとして記述したいと思いx && y == yます。
タナースウェット

1
何はかなり正確だ@TannerSwett x.HasFlag(DeclarationType.Member)....ない
マチューGuindon

はい、本当です。しかし、メソッドがのHasFlag代わりに呼び出される場合、それが本当に意味するものが「サブタイプ」であることを見つけるための別の方法がIsSubtypeOf必要です。拡張メソッドを作成することもできますが、ユーザーとしては、実際のメソッドとして持つ構造体であることに驚かないでしょう。DeclarationTypeIsSubtypeOf
タナースウェット

回答:


10

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

私が正しく理解していれば、宣言の階層的な分類ができています。これは、単一の列挙型でエンコードするには多くの情報とはかけ離れています。しかし、明らかな代替手段があります:クラスと継承を使用してください!ですからMemberから継承DeclarationTypeProperty継承からMemberというように。

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

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


継承は良い考えですが、あなたの答えは列挙型ごとのクラスを暗示しており、これはやり過ぎのようです。
フランクヒルマン

@FrankHileman:階層内の「葉」は、クラスではなく列挙値として表すことができますが、それが良いかどうかはわかりません。異なる列挙値に異なる動作が関連付けられるかどうかに依存します。この場合、異なるクラスがより良いでしょう。
ジャックB

4
分類は階層的ではなく、組み合わせは事前定義された状態です。これに継承を使用することは、クラスの不正使用、つまり実際の不正使用になります。クラスでは、少なくともいくつかのデータまたは動作が期待されます。どちらもないクラスの束は...正しい、列挙型です。
マーティンマート

私のレポへのリンクについて:型ごとの動作はParserRuleContext、enumよりも生成されたクラスの型に関係しています。そのコードはAs {Type}、宣言に句を挿入するトークンの位置を取得しようとしています。これらのParserRuleContext派生クラスは、パーサールールを定義する文法ごとにAntr4によって生成されます-解析ツリーノードの[やや浅い]継承階層は実際には制御しませんが、partialインターフェースを実装するために-nessを活用できますいくつかのAsTypeClauseTokenPositionプロパティを公開します。多くの作業。
マチューギンドン

6

このアプローチは読みやすく理解しやすいと思います。私見、それは混乱するものではありません。そうは言っても、このアプローチには懸念があります。

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

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

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

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

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

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

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


そのため、徹底的なパーサーユニットテストは、#1に対処します。FWIW列挙型は、3年前に標準のフラグなし列挙型として開始されました。列挙型フラグが表示される特定の値(コード検査など)を常にチェックすることに飽き飽きしてきました。リストは実際には時間の経過とともに大きくなることはありませんintが、それでも容量を破ることは本当の関心事です。
マチューギンドン

3

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


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

この特定の状況では、異なるタイプのノード間で多態的な動作はほとんどありません。言い換えると、ツリーには非常に異なるタイプのノード(バリアント)を含めることができる必要がありますが、これらのノードの実際の訪問は基本的に巨大なif-then-elseチェーン(またはinstanceof/ isチェック)に頼ります。これらの巨大なチェックは、プロジェクト全体のさまざまな場所で行われる可能性があります。これが、列挙型が役立つと思われる理由、または少なくともinstanceof/ isチェックと同じくらい役立つ理由です。

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

これは、クラスと継承階層が役に立たないと言っているわけではありません。まったく逆です。すべての宣言タイプで機能するポリモーフィックな動作はありませんが(すべての宣言にNameプロパティが必要であるという事実を除く)、近くの兄弟で共有される豊富なポリモーフィックな動作がたくさんあります。例えば、FunctionそしてProcedureおそらくいくつかの行動を(両方の呼び出し可能であることと、型指定された入力引数のリストを受け入れる)を共有し、そしてPropertyGet意志から間違いなく継承行動Function(両方とも持ちます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 
}

この変更されたバージョンでは、非具象宣言型にビットを割り当てることを避けています。代わりに、非具象宣言型(宣言型の抽象的なグループ化)には、すべての子のビット単位の論理和(ビットの結合)である列挙値があります。

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


この質問に固有の1つの注意:a 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()方法を悪用して悪用します。

列挙型を値として保存しているところはどこでも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
    ...
};

クラスと継承階層を提案している人の最終的な証明...

列挙型が継承階層の近似値であることがわかると、次のアドバイスが成り立ちます。

  • 最初に継承階層を設計(または改善)し、
  • その後、戻って列挙を設計し、その継承階層を概算します。

プロジェクトは実際にはVBIDEアドインです-VBAコードを解析および分析しています=)
Mathieu Guindon

1

フラグの使用は本当にスマートで、創造的で、エレガントで、潜在的に最も効率的だと思います。私もそれを読むのに全く問題ありません。

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

thingy&Organic.Fruit!= 0

より読みやすい

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

Flag列挙型のポイントは、複数の状態を結合できるようにすることです。これで、より便利で読みやすくなりました。あなたはあなたのコードで果物の概念を伝えます、私はリンゴとオレンジと梨が果物を意味することを自分で理解する必要はありません。

この男にいくつかのブラウニーポイントを与えます!


4
thingy is Fruitどちらよりも読みやすいです。
ジャックB
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.