c# - 启用Enum(带Flags属性)而不声明每个可能的组合?




enums switch-statement bit (8)

我如何打开一个设置了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 / route时是否应该有一个方法。

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

[ActionSelectorKeyCondition(CheckType.Form | CheckType.TempData, "SomeKey")]
public ActionResult Index()
{
    return View();
} 

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

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

为了更好地了解我在做什么,我已经粘贴了一个关于我在pastebin上实际做的简单示例 - 可以在这里找到http://pastebin.com/m478cc2b8


Answers

最简单的方法是只执行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;
}

更清洁,你不需要使用HasFlagif语句。 您可以进行任何所需的组合,然后使switch语句易于阅读。

我建议拆开你的enums ,尝试看看你是不是将不同的东西混合到同一个enum 。 您可以设置多个enums以减少案例数量。


只需使用HasFlag

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

这个怎么样。 当然,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;
                }
            }
        }
    }
}

你将填写的Dictionary<CheckType,Action>怎么样?

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]();
}

(代码未经测试)


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

编辑: 通过使用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
}

使用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;
    }
}

根据您的编辑和现实代码,我可能会更新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;
}

那么,你对时间进行计时的方式对我来说看起来非常讨厌。 整个循环的时间会更加明智:

var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
    Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);

这样你就不会受到微小时间,浮点运算和累积误差的影响。

做了这些改变后,看看“非catch”版本是否仍然比“catch”版本慢。

编辑:好吧,我自己尝试过 - 我看到了相同的结果。 很奇怪。 我想知道try / catch是否禁用了一些不良内联,但是使用[MethodImpl(MethodImplOptions.NoInlining)]并没有帮助...

基本上你需要查看cordbg下的优化JIT代码,我怀疑...

编辑:多一点的信息:

  • 把try / catch放在n++; 线仍然可以提高性能,但不会像将其放在整个区块中一样
  • 如果你捕捉到一个特定的异常(在我的测试中是ArgumentException ),它仍然很快
  • 如果你在catch块中打印异常,它仍然很快
  • 如果在catch块中重新抛出异常,它会再次变慢
  • 如果你使用finally块而不是catch块,它会再次变慢
  • 如果你使用finally块 catch块,它很快

奇怪的...

编辑:好的,我们已经拆卸...

这是使用C#2编译器和.NET 2(32位)CLR,用mdbg进行反汇编(因为我的机器上没有cordbg)。 即使在调试器下,我仍然可以看到相同的性能效果。 快速版本在变量声明和返回语句之间的所有内容上都使用try块,只需一个catch{}处理程序。 显然,慢版本是相同的,除非没有try / catch。 调用代码(即Main)在这两种情况下都是相同的,并且具有相同的程序集表示形式(所以它不是内联问题)。

快速版本的反汇编代码:

 [0000] push        ebp
 [0001] mov         ebp,esp
 [0003] push        edi
 [0004] push        esi
 [0005] push        ebx
 [0006] sub         esp,1Ch
 [0009] xor         eax,eax
 [000b] mov         dword ptr [ebp-20h],eax
 [000e] mov         dword ptr [ebp-1Ch],eax
 [0011] mov         dword ptr [ebp-18h],eax
 [0014] mov         dword ptr [ebp-14h],eax
 [0017] xor         eax,eax
 [0019] mov         dword ptr [ebp-18h],eax
*[001c] mov         esi,1
 [0021] xor         edi,edi
 [0023] mov         dword ptr [ebp-28h],1
 [002a] mov         dword ptr [ebp-24h],0
 [0031] inc         ecx
 [0032] mov         ebx,2
 [0037] cmp         ecx,2
 [003a] jle         00000024
 [003c] mov         eax,esi
 [003e] mov         edx,edi
 [0040] mov         esi,dword ptr [ebp-28h]
 [0043] mov         edi,dword ptr [ebp-24h]
 [0046] add         eax,dword ptr [ebp-28h]
 [0049] adc         edx,dword ptr [ebp-24h]
 [004c] mov         dword ptr [ebp-28h],eax
 [004f] mov         dword ptr [ebp-24h],edx
 [0052] inc         ebx
 [0053] cmp         ebx,ecx
 [0055] jl          FFFFFFE7
 [0057] jmp         00000007
 [0059] call        64571ACB
 [005e] mov         eax,dword ptr [ebp-28h]
 [0061] mov         edx,dword ptr [ebp-24h]
 [0064] lea         esp,[ebp-0Ch]
 [0067] pop         ebx
 [0068] pop         esi
 [0069] pop         edi
 [006a] pop         ebp
 [006b] ret

反汇编缓慢版本的代码:

 [0000] push        ebp
 [0001] mov         ebp,esp
 [0003] push        esi
 [0004] sub         esp,18h
*[0007] mov         dword ptr [ebp-14h],1
 [000e] mov         dword ptr [ebp-10h],0
 [0015] mov         dword ptr [ebp-1Ch],1
 [001c] mov         dword ptr [ebp-18h],0
 [0023] inc         ecx
 [0024] mov         esi,2
 [0029] cmp         ecx,2
 [002c] jle         00000031
 [002e] mov         eax,dword ptr [ebp-14h]
 [0031] mov         edx,dword ptr [ebp-10h]
 [0034] mov         dword ptr [ebp-0Ch],eax
 [0037] mov         dword ptr [ebp-8],edx
 [003a] mov         eax,dword ptr [ebp-1Ch]
 [003d] mov         edx,dword ptr [ebp-18h]
 [0040] mov         dword ptr [ebp-14h],eax
 [0043] mov         dword ptr [ebp-10h],edx
 [0046] mov         eax,dword ptr [ebp-0Ch]
 [0049] mov         edx,dword ptr [ebp-8]
 [004c] add         eax,dword ptr [ebp-1Ch]
 [004f] adc         edx,dword ptr [ebp-18h]
 [0052] mov         dword ptr [ebp-1Ch],eax
 [0055] mov         dword ptr [ebp-18h],edx
 [0058] inc         esi
 [0059] cmp         esi,ecx
 [005b] jl          FFFFFFD3
 [005d] mov         eax,dword ptr [ebp-1Ch]
 [0060] mov         edx,dword ptr [ebp-18h]
 [0063] lea         esp,[ebp-4]
 [0066] pop         esi
 [0067] pop         ebp
 [0068] ret

在每种情况下, *显示调试器在简单的“步入”中输入的位置。

编辑:好的,我现在已经查看了代码,我想我可以看到每个版本是如何工作的......我相信较慢的版本会更慢,因为它使用更少的寄存器和更多的堆栈空间。 对于n的小数值可能更快 - 但是当循环占用大部分时间时,速度会变慢。

可能try / catch块会强制更多的寄存器被保存和恢复,所以JIT也会将这些寄存器用于循环......这恰好可以提高整体性能。 目前尚不清楚JIT是否在“正常”代码中使用了很多寄存器是否合理。

编辑:刚在我的x64机器上试过这个。 x64 CLR比这个代码上的x86 CLR快得多(速度快3-4倍),而在x64下,try / catch块并没有明显的区别。





c# enums switch-statement flags bit