将标志用于“分组”枚举是否错误?


12

我的理解是,[Flag]枚举通常用于可以组合的事物,其中各个值并不相互排斥

例如:

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

显然,给定的Declaration值不能同时是a VariableLibraryProcedurea-这两个值不能组合在一起。

尽管这些标志非常有用(很容易验证给定的DeclarationType是a Property还是a Module),但感觉是“错误的”,因为这些标志并不是真正用于组合值,而是用于它们分组为“子类型”。

所以我说,这是滥用枚举标志- 这样的回答基本上是说,如果我有一个适用的设定值,以苹果和适用于橘子另一套,那么我需要为苹果不同的枚举类型,另一个用于橘子-的这里的问题是,我需要所有声明都具有一个公共接口,并DeclarationType在基Declaration类中公开:拥有PropertyType枚举根本就没有用。

这是草率的/令人惊讶的/侮辱性的设计吗?如果是这样,那么通常如何解决该问题?


考虑人们如何使用类型的对象DeclarationType。如果我想确定是否x是的子类型y,我可能会想将其写为x.IsSubtypeOf(y),而不是x && y == y
Tanner Swett

1
@TannerSwett就是x.HasFlag(DeclarationType.Member)这样……
Mathieu Guindon

是的,这是真的。但是,如果调用您的方法HasFlag而不是IsSubtypeOf,那么我需要其他方法来找出它真正的意思是“是...的子类型”。你可以创建一个扩展方法,但作为一个用户,我会觉得至少令人惊讶的是DeclarationType仅仅是具有结构IsSubtypeOf作为真正的方法。
Tanner Swett

Answers:


10

这绝对是在滥用枚举和标志!它可能对您有用,但是其他任何阅读代码的人都会感到困惑。

如果我理解正确,则您具有声明的分层分类。这是在单个枚举多的信息进行编码。但是,还有一个明显的选择:使用类和继承!因此,Member继承自DeclarationTypeProperty继承自Member等等。

枚举在某些特定情况下是适当的:如果值始终是有限数量的选项之一,或者它是有限数量的选项(标志)的任意组合。任何比其复杂或结构化的信息都应使用对象来表示。

编辑:在您的“实际情况”中,似乎有多个地方根据枚举的值选择行为。这实际上是一种反模式,因为您将switch+ enum用作“穷人多态性”。只需将枚举值转换为封装声明特定行为的不同类,您的代码就会更加简洁


继承是一个好主意,但您的答案暗示着每个枚举都有一个类,这似乎太过分了。
Frank Hileman

@FrankHileman:层次结构中的“叶”可以表示为枚举值而不是类,但是我不确定它会更好。取决于是否将不同的行为与不同的枚举值相关联,在这种情况下,不同的类会更好。
JacquesB

4
分类不是分层的,组合是预定义的状态。为此使用继承将是真正的滥用,即滥用类。对于一个类,我希望至少有一些数据或某些行为。一堆都没有的类是...对,一个枚举。
Martin Maat

关于到我的仓库的链接:每个类型的行为更多地与ParserRuleContext生成的类的类型有关,而不是与枚举有关。该代码试图获取标记位置,以便As {Type}在声明中插入子句。这些ParserRuleContext派生的类是由Antr4按照定义解析器规则的语法生成的-尽管我可以利用它们的partial-ness来使它们实现接口,例如,让他们暴露一些AsTypeClauseTokenPosition财产..很多工作。
Mathieu Guindon

6

我发现这种方法很容易阅读和理解。恕我直言,这不是什么令人困惑的事情。话虽如此,我对这种方法有些担心:

  1. 我的主要保留意见是无法强制执行此操作:

    显然,给定的声明不能同时是变量和LibraryProcedure-两个单独的值不能组合 ..而不能。

    虽然您没有声明上述组合,但是此代码

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    是完全有效的。而且没有办法在编译时捕获这些错误。

  2. 您可以在位标志中编码多少信息有一个限制,即64位?现在,您的危险已接近于的大小,int如果这个枚举持续增长,您最终可能会用光了点...

最重要的是,我认为这是一种有效的方法,但对于将其用于大型/复杂的标记层次结构,我会犹豫不决。


因此,全面的解析器单元测试将解决#1问题。FWIW枚举大约在3年前开始成为标准的无标记枚举。在厌倦了总是检查某些特定值(例如在代码检查中)之后,枚举标记出现了。这个列表也不会随着时间的推移而增长,但是,确实,跳动的int能力仍然是一个令人担忧的问题。
Mathieu Guindon

3

TL; DR滚动到最底部。


据我所知,您正在C#之上实现一种新语言。枚举似乎表示标识符的类型(或任何具有名称且出现在新语言的源代码中的名称),该标识符似乎适用于要添加到程序树表示形式的节点。

在这种特殊情况下,不同类型的节点之间几乎没有多态行为。换句话说,虽然树必须能够包含非常不同类型(变量)的节点,但是这些节点的实际访问基本上将诉诸于巨大的if-then-else链(或instanceof/ ischecks)。这些巨大的检查可能会在整个项目的许多不同地方进行。这就是为什么枚举似乎有用,或者它们至少和instanceof/ ischeck 一样有用的原因。

访客模式可能仍然有用。换句话说,可以使用多种编码样式来代替的巨型链instanceof。但是,如果您想讨论各种优点和缺点,则可以选择展示instanceof项目中最丑陋的链中的代码示例,而不是为枚举而烦恼。

这并不是说类和继承层次结构没有用。恰恰相反。尽管没有任何一种适用于每种声明类型的多态行为(除了每个声明都必须具有Name属性的事实),但附近的兄弟姐妹共享了许多丰富的多态行为。例如,Function并且Procedure可能共享一些行为(都可以被调用并接受一连串的输入参数),并且PropertyGet肯定会从Function(都具有ReturnType)继承行为。您可以对大型if-then-else链使用枚举或继承检查,但是多态行为(无论多么零碎)仍必须在类中实现。

有很多关于过度使用instanceof/ is支票的在线建议。性能不是原因之一。相反,其原因是为了防止程序员自然地发现合适的多态行为,就像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 
}

此修改后的版本避免为非具体的声明类型分配位。相反,非具体的声明类型(声明类型的抽象分组)仅具有枚举值,该枚举值在其所有子代中都是按位或(位的联合)。

需要注意的是:如果有一个具有单个子代的抽象声明类型,并且有必要将抽象的(父代)与具体的一个(子代)区分开,则抽象的那一类仍然需要自己的位。


一个需要注意的是具体到这样一个问题:一个Property是最初的标识符(当你只是看到它的名字,没有看到它是如何在代码中使用),但它可能会蜕变成PropertyGet/ PropertyLet/ PropertySet只要你看到它正在使用在代码中。换句话说,在解析的不同阶段,您可能需要将Property标识符标记为“此名称引用属性”,然后将其更改为“此代码行以某种方式访问​​此属性”。

要解决此警告,您可能需要两组枚举。一个枚举表示名称(标识符)是什么;另一个枚举表示代码正在尝试执行的操作(例如,声明某些内容的主体;尝试以某种方式使用某些内容)。


考虑是否可以从数组中读取有关每个枚举值的辅助信息。

该建议与其他建议互斥,因为它需要将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#类型(继承层次结构中的类)和您的枚举值之间是否一对一映射。

(或者,您可以调整枚举值以确保与类型的一对一映射。)

在C#中,Type object.GetType()无论好坏,许多库都滥用这种漂亮的方法。

在将枚举存储为值的任何地方,您都可能会问自己是否可以将其存储Type为值。

要使用此技巧,您可以初始化两个只读哈希表,即:

// 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

我发现您对标志的使用确实很聪明,富有创意,优雅并且可能是最有效的。我也没有任何困难阅读它。

标志是发信号通知状态,进行资格检查的一种手段。如果我想知道某事是否是水果,我会发现

whaty&Organic.Fruit!= 0

比...更具可读性

东西&(有机苹果|有机橙色|有机梨)= 0

Flag枚举的重点是允许您组合多个状态。您只是使它变得更加有用和易读。您在代码中传达了水果的概念,我不必弄清楚自己,苹果,橙子和梨意味着水果。

给这个家伙一些布朗尼积分!


4
thingy is Fruit比任何一个更具可读性。
JacquesB
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.