打开枚举(带有标志属性)而不声明所有可能的组合?

Posted

技术标签:

【中文标题】打开枚举(带有标志属性)而不声明所有可能的组合?【英文标题】:Switch on Enum (with Flags attribute) without declaring every possible combination? 【发布时间】:2010-11-05 15:48:56 【问题描述】:

我如何打开一个设置了 flags 属性的枚举(或者更准确地说是用于位操作)?

我希望能够在与声明的值匹配的开关中命中所有情况。

问题是,如果我有以下枚举

[Flags()]public enum CheckType

    Form = 1,   
    QueryString = 2,
    TempData = 4,

我想使用这样的开关

switch(theCheckType)

   case CheckType.Form:
       DoSomething(/*Some type of collection is passed */);
       break;

   case CheckType.QueryString:
       DoSomethingElse(/*Some other type of collection is passed */);
       break;

   case CheckType.TempData
       DoWhatever(/*Some different type of collection is passed */);
       break;

如果“theCheckType”同时设置为 CheckType.Form | CheckType.TempData 我希望它同时满足两种情况。显然,由于中断,它不会在我的示例中同时命中,但除此之外它也会失败,因为 CheckType.Form 不等于 CheckType.Form | CheckType.TempData

我所看到的唯一解决方案是为枚举值的每个可能组合提供一个案例?

类似

    case CheckType.Form | CheckType.TempData:
        DoSomething(/*Some type of collection is passed */);
        DoWhatever(/*Some different type of collection is passed */);
        break;

    case CheckType.Form | CheckType.TempData | CheckType.QueryString:
        DoSomething(/*Some type of collection is passed */);
        DoSomethingElse(/*Some other type of collection is passed */);
        break;

... and so on...

但这真的不是很理想(因为它会很快变得很大)

现在我有 3 个 If 条件紧随其后

有点像

if ((_CheckType & CheckType.Form) != 0)

    DoSomething(/*Some type of collection is passed */);


if ((_CheckType & CheckType.TempData) != 0)

    DoWhatever(/*Some type of collection is passed */);


....

但这也意味着,如果我有一个包含 20 个值的枚举,它必须每次都通过 20 个 If 条件,而不是像使用开关时那样“跳转”到只需要的“case”/。

有什么神奇的方法可以解决这个问题吗?

我想到了循环遍历声明的值然后使用开关的可能性,然后它只会为每个声明的值点击开关,但我不知道它是如何工作的,如果它的性能问题是好主意(与许多 if 相比)?

有没有一种简单的方法可以循环遍历所有声明的枚举值?

我只能想出使用 ToString() 并用 "," 分割,然后遍历数组并解析每个字符串。


更新:

我发现我的解释工作做得不够好。 我的例子很简单(试图简化我的场景)。

我将它用于 Asp.net MVC 中的 ActionMethodSelectorAttribute 以确定在解析 url/路由时方法是否可用。

我通过在方法上声明这样的东西来做到这一点

[ActionSelectorKeyCondition(CheckType.Form | CheckType.TempData, "SomeKey")]
public ActionResult Index()

    return View();
 

这意味着它应该检查 Form 或 TempData 是否具有为可用方法指定的键。

它将调用的方法(我之前的示例中的 doSomething()、doSomethingElse() 和 doWhatever())实际上将 bool 作为返回值,并将使用参数调用(不共享接口的不同集合可以使用 - 请参阅下面链接中的示例代码等)。

为了希望更好地了解我在做什么,我粘贴了一个简单的示例,说明我在 pastebin 上实际执行的操作 - 可以在这里找到 http://pastebin.com/m478cc2b8

【问题讨论】:

【参考方案1】:

将其转换为其基本类型,这样做的好处是它会告诉您何时存在重复值。

[Flags]
public enum BuildingBlocks_Property_Reflection_Filters

    None=0,
    Default=2,
    BackingField=4,
    StringAssignment=8,
    Base=16,
    External=32,
    List=64,
    Local=128,


switch ((int)incomingFilter)

    case (int)PropFilter.Default:
        break;
    case (int)PropFilter.BackingField:
        break;
    case (int)PropFilter.StringAssignment:
        break;
    case (int)PropFilter.Base:
        break;
    case (int)PropFilter.External:
        break;
    case (int)PropFilter.List:
        break;
    case (int)PropFilter.Local:
        break;
    case (int)(PropFilter.Local | PropFilter.Default):
        break;


【讨论】:

【参考方案2】:

在 C# 7 中应该是可能的

switch (t1)
    
        case var t when t.HasFlag(TST.M1):
            
                break;
            
        case var t when t.HasFlag(TST.M2):
            
                break;
            

【讨论】:

酷选项;兴趣步骤将很好地测试性能 谢谢 - 这应该是现在接受的答案【参考方案3】:

使用 C# 7,您现在可以编写如下内容:

public void Run(CheckType checkType)

    switch (checkType)
    
        case var type when CheckType.Form == (type & CheckType.Form):
            DoSomething(/*Some type of collection is passed */);
            break;

        case var type when CheckType.QueryString == (type & CheckType.QueryString):
            DoSomethingElse(/*Some other type of collection is passed */);
            break;

        case var type when CheckType.TempData == (type & CheckType.TempData):
            DoWhatever(/*Some different type of collection is passed */);
            break;
    

【讨论】:

这不会只针对一个案例而跳过处理其余案例吗?例如,如果您有CheckType.Form | CheckType.QueryString,它将匹配您的switch 上的第一个表达式,然后不处理QueryString 逻辑。 @julealgon 是的,但这就是开关盒的工作原理。如果需要,可以省略 break 语句。 @ezekg 不幸的是,如果 case 中有一些代码,break 不是可选的,有时它可以替换为 goto 但我不知道如何将它用于 C# 7 【参考方案4】:

标志枚举可以被视为一种简单的整数类型,其中每个单独的位对应于一个标志值。您可以利用此属性将带位标记的枚举值转换为布尔数组,然后从相关的委托数组中分派您关心的方法。

编辑: 我们当然可以通过使用 LINQ 和一些辅助函数使这段代码更紧凑,但我认为以不太复杂的形式更容易理解。这可能是可维护性胜过优雅的情况。

这是一个例子:

[Flags()]public enum CheckType

  Form = 1,       
  QueryString = 2,
  TempData = 4,


void PerformActions( CheckType c )

  // array of bits set in the parameter c
  bool[] actionMask =  false, false, false ;
  // array of delegates to the corresponding actions we can invoke...
  Action availableActions =  DoSomething, DoSomethingElse, DoAnotherThing ;

  // disassemble the flags into a array of booleans
  for( int i = 0; i < actionMask.Length; i++ )
    actionMask[i] = (c & (1 << i)) != 0;

  // for each set flag, dispatch the corresponding action method
  for( int actionIndex = 0; actionIndex < actionMask.Length; actionIndex++ )
  
      if( actionMask[actionIndex])
          availableActions[actionIndex](); // invoke the corresponding action
  

或者,如果您评估的顺序无关紧要,这里有一个更简单、更清晰的解决方案,也同样有效。如果顺序确实很重要,请将位移操作替换为包含标志的数组,该数组按您想要评估它们的顺序排列:

int flagMask = 1 << 31; // start with high-order bit...
while( flagMask != 0 )   // loop terminates once all flags have been compared

  // switch on only a single bit...
  switch( theCheckType & flagMask )
  
   case CheckType.Form:
     DoSomething(/*Some type of collection is passed */);
     break;

   case CheckType.QueryString:
     DoSomethingElse(/*Some other type of collection is passed */);
     break;

   case CheckType.TempData
     DoWhatever(/*Some different type of collection is passed */);
     break;
  

  flagMask >>= 1;  // bit-shift the flag value one bit to the right

【讨论】:

感谢您的回答。我已经更新了我原来的问题。这似乎是一个不错的解决方案,但不幸的是我的代码的结构方式我需要从被调用的方法中获取返回值并传递不同类型的参数(我已经链接到我正在尝试做的示例底部)。希望示例代码能比我的文字更好地解释我的问题。 :) 如果您需要使用None 选项(值0)执行不同的逻辑怎么办?您的第二个示例在到达 0 时停止标志迭代,但是使用带有 None = 0 选项的枚举是一种很好的做法,并且可能会有额外的逻辑与之相关。【参考方案5】:

最简单的方法是执行 ORed 枚举,在您的情况下,您可以执行以下操作:

[Flags()]public enum CheckType

    Form = 1,   
    QueryString = 2,
    TempData = 4,
    FormQueryString = Form | QueryString,
    QueryStringTempData = QueryString | TempData,
    All = FormQueryString | TempData

一旦您设置好了enum,现在就可以轻松地执行您的switch 语句了。

例如,如果我设置了以下内容:

var chkType = CheckType.Form | CheckType.QueryString;

我可以使用下面的switch 声明如下:

switch(chkType)
 case CheckType.Form:
   // Have Form
 break;
 case CheckType.QueryString:
   // Have QueryString
 break;
 case CheckType.TempData:
  // Have TempData
 break;
 case CheckType.FormQueryString:
  // Have both Form and QueryString
 break;
 case CheckType.QueryStringTempData:
  // Have both QueryString and TempData
 break;
 case CheckType.All:
  // All bit options are set
 break;

更简洁,您无需将if 语句与HasFlag 一起使用。您可以进行任何您想要的组合,然后使 switch 语句易于阅读。

我建议拆分你的enums,试试看你是否没有将不同的东西混入同一个enum。您可以设置多个enums 以减少案例数量。

【讨论】:

这会造成大量代码重复并且无法正确扩展。我强烈劝阻人们不要走这条路,除非枚举有 2 个选项并且预计不会增长。即使那样,由于代码重复方面,我个人也不会使用它。【参考方案6】:

只需使用HasFlag

if(theCheckType.HasFlag(CheckType.Form)) DoSomething(...);
if(theCheckType.HasFlag(CheckType.QueryString)) DoSomethingElse(...);
if(theCheckType.HasFlag(CheckType.TempData)) DoWhatever(...);

【讨论】:

这是迄今为止最简单的解决方案。应该更高。 @SlavaKnyazev 我敢肯定,除了 HasFlag 是在 2010 年在 .NET 4.0 中引入的。对于答案适用于哪个版本,SO 几乎没有用,而且只会随着时间的推移变得更糟。 公平地说,您总是可以按位形式执行上述操作:if((theCheckType &amp; CheckType.Form) == CheckType.Form) DoSomething (...); if((theCheckType &amp; CheckType.QueryString) == CheckType.QueryString) DoSomethingElse (...); 因此,没有人提出等效的建议有点令人惊讶。【参考方案7】:

这个怎么样。当然,DoSomething 的参数和返回类型等,可以是任何你喜欢的。

class Program

    [Flags]
    public enum CheckType
    
        Form = 1,
        QueryString = 2,
        TempData = 4,
    

    private static bool DoSomething(IEnumerable cln)
    
        Console.WriteLine("DoSomething");
        return true;
    

    private static bool DoSomethingElse(IEnumerable cln)
    
        Console.WriteLine("DoSomethingElse");
        return true;
    

    private static bool DoWhatever(IEnumerable cln)
    
        Console.WriteLine("DoWhatever");
        return true;
    

    static void Main(string[] args)
    
        var theCheckType = CheckType.QueryString | CheckType.TempData;
        var checkTypeValues = Enum.GetValues(typeof(CheckType));
        foreach (CheckType value in checkTypeValues)
        
            if ((theCheckType & value) == value)
            
                switch (value)
                
                    case CheckType.Form:
                        DoSomething(null);
                        break;
                    case CheckType.QueryString:
                        DoSomethingElse(null);
                        break;
                    case CheckType.TempData:
                        DoWhatever(null);
                        break;
                
            
        
    

【讨论】:

感谢您的回复。它很像 Lukes 解决方案,只是使用开关而不是字典映射。这似乎是最好的解决方案,尽管我真的希望可以以某种方式只循环“theCheckType”中的值而不是循环整个枚举,因为确实没有理由检查所有枚举值,只有那些设置在“检查类型”中。但我想这是不可能的。 我看不出与卢克的回答有什么相似之处;我认为我的要简单得多。如我所见,您要么必须对照枚举检查“theCheckType”,要么对照“theCheckType”检查枚举。无论哪种方式,您都必须检查所有可能的值。如果您将枚举声明为 long (Int64),您显然最多可以有 64 个值,因此在如此短的循环中无需担心性能。 看看这个:***.com/questions/1060760/… 再次感谢。关于与卢克斯答案的相似性,我的意思是它们都做同样的事情 - 遍历 CheckType 枚举中的每个值,在他的情况下调用使用字典,而你的使用开关(据我所知,这实际上是在幕后使用某种哈希表/字典的实现)。但我同意你的更简单,可能是最好的解决方案(将其标记为可接受的答案)如果我只能循环“theCheckType”中的值,我可以直接调用开关,它只会循环并调用开关必要的次数。【参考方案8】:

根据您的编辑和实际代码,我可能会将IsValidForRequest 方法更新为如下所示:

public sealed override bool IsValidForRequest
    (ControllerContext cc, MethodInfo mi)

    _ControllerContext = cc;

    var map = new Dictionary<CheckType, Func<bool>>
        
             CheckType.Form, () => CheckForm(cc.HttpContext.Request.Form) ,
             CheckType.Parameter,
                () => CheckParameter(cc.HttpContext.Request.Params) ,
             CheckType.TempData, () => CheckTempData(cc.Controller.TempData) ,
             CheckType.RouteData, () => CheckRouteData(cc.RouteData.Values) 
        ;

    foreach (var item in map)
    
        if ((item.Key & _CheckType) == item.Key)
        
            if (item.Value())
            
                return true;
            
        
    
    return false;

【讨论】:

感谢您的回答。我看不出这与“如果”条件有何不同。这将遍历“map”中的每个值(并且 CheckType 枚举中的每个值都必须在“map”字典中注册)。如果您的意思是我应该在属性中声明字典,我认为这是不可能的。我已经更新了我的原始问题,并提供了一些我实际尝试做的示例代码的链接,希望能更好地澄清它。 @MartinF,这是基于您更新的示例代码!你是对的,它与你的“如果”条件没有太大的不同。唯一的好处是任何新的 CheckType 值只需要包含在地图中。 我仍然不完全清楚您为什么认为您的“if”语句有问题。如果您出于性能原因试图避免使用它们,那么这对我来说听起来很像过早的优化。【参考方案9】:

Dictionary&lt;CheckType,Action&gt; 怎么样,你会填写喜欢

dict.Add(CheckType.Form, DoSomething);
dict.Add(CheckType.TempDate, DoSomethingElse);
...

你的价值分解

flags = Enum.GetValues(typeof(CheckType)).Where(e => (value & (CheckType)e) == (CheckType)e).Cast<CheckType>();

然后

foreach (var flag in flags)

   if (dict.ContainsKey(flag)) dict[flag]();

(未经测试的代码)

【讨论】:

感谢您的回答。我已经更新了我原来的帖子。我认为不可能使用字典,因为我在属性中声明它(参见示例) - 或者至少我不知道应该如何完成。 可能不足以解决 MartinF 的问题,但非常优雅... +1 ;) Enum(...).GetValues 返回一个数组;你不能使用'Where。

以上是关于打开枚举(带有标志属性)而不声明所有可能的组合?的主要内容,如果未能解决你的问题,请参考以下文章

标志枚举

带有标志属性的 Enum.TryParse

带有标志属性的 Enum.TryParse

枚举与位枚举

WPF DataGrid 带有一键组合框,显示按枚举名称排序的枚举值

Python:枚举列表中所有元素的可能组合