【问题标题】:enum case handling - better to use a switch or a dictionary?枚举案例处理 - 更好地使用开关或字典?
【发布时间】:2013-07-12 00:09:10
【问题描述】:

在逐个处理枚举值时,使用 switch 语句还是字典更好?

我认为字典会更快。在空间方面,它占用了一些内存,但是case语句也会占用一些内存,只是在程序本身所需的内存中。所以底线我认为只使用字典总是更好。

下面是两个实现并排比较:

鉴于这些枚举:

enum FruitType
{
    Other,
    Apple,
    Banana,
    Mango,
    Orange
}
enum SpanishFruitType
{
    Otra,
    Manzana, // Apple
    Naranja, // Orange
    Platano, // Banana
    Pitaya // Dragon fruit, only grown in Mexico and South American countries, lets say
    // let's say they don't have mangos, because I don't remember the word for it.
}

下面是使用 switch 语句的方法:

private static SpanishFruitType GetSpanishEquivalent(FruitType typeOfFruit)
{
    switch(typeOfFruit)
    {
        case FruitType.Apple:
            return SpanishFruitType.Manzana;
        case FruitType.Banana:
            return SpanishFruitType.Platano;
        case FruitType.Orange:
            return SpanishFruitType.Naranja;
        case FruitType.Mango:
        case FruitType.Other:
            return SpanishFruitType.Otra;
        default:
            throw new Exception("what kind of fruit is " + typeOfFruit + "?!");
    }
}

下面是使用字典的方法:

private static Dictionary<FruitType, SpanishFruitType> EnglishToSpanishFruit = new Dictionary<FruitType, SpanishFruitType>()
{
    {FruitType.Apple, SpanishFruitType.Manzana}
    ,{FruitType.Banana, SpanishFruitType.Platano}
    ,{FruitType.Mango, SpanishFruitType.Otra}
    ,{FruitType.Orange, SpanishFruitType.Naranja}
    ,{FruitType.Other, SpanishFruitType.Otra}
};
private static SpanishFruitType GetSpanishEquivalentWithDictionary(FruitType typeOfFruit)
{
    return EnglishToSpanishFruit[typeOfFruit]; // throws exception if it's not in the dictionary, which is fine.
}

字典不仅可以提高速度,而且代码中不必要的字符串也更少。那么使用字典总是更好吗?还有第三种更好的方法吗?

提前致谢。

【问题讨论】:

  • 您每秒执行多少次这些查找?如果它小于一个 bajillion,请不要担心。只需选择最容易阅读/维护的方法即可。
  • 实际切换速度更快
  • 当 Switch 语句包含超过 7 个选项时,编译器将把它翻译成一个“字典”(更准确地说是一个哈希),所以如果它对您来说更具可读性,请使用 switch 语句并让编译器尽最大努力进行优化。

标签: c# dictionary enums switch-statement


【解决方案1】:

事实上,字典比较慢。真的。只需编写简单的基准测试(我添加了将字典转换为数组的示例):

void Main()
{
    for (int itFac = 0; itFac < 7; itFac++ ) {
        var iterations = 100;
        iterations *= (int)Math.Pow(10, itFac);

        Console.WriteLine("Iterations: {0}", iterations);

        {
            Random r = new Random();
            int maxFruits = 5;
            var timer = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++) {
                var res =  Fruits.GetSpanishEquivalentWithArray((Fruits.FruitType)r.Next(maxFruits));
            }
            Console.WriteLine("Array time: {0}", timer.Elapsed);
        }       

        {
            Random r = new Random();
            int maxFruits = 5;
            var timer = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++) {
                var res = Fruits.GetSpanishEquivalent((Fruits.FruitType)r.Next(maxFruits));
            }
            Console.WriteLine("Switch time    : {0}", timer.Elapsed);
        }

        {
            Random r = new Random();
            int maxFruits = 5;
            var timer = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++) {
                var res =  Fruits.GetSpanishEquivalentWithDictionary((Fruits.FruitType)r.Next(maxFruits));
            }
            Console.WriteLine("Dictionary time: {0}", timer.Elapsed);
        }

        Console.WriteLine();
    }
}

class Fruits {
    public enum FruitType
    {
        Other,
        Apple,
        Banana,
        Mango,
        Orange
    }
    public enum SpanishFruitType
    {
        Otra,
        Manzana, // Apple
        Naranja, // Orange
        Platano, // Banana
        // let's say they don't have mangos, because I don't remember the word for it.
    }

    public static SpanishFruitType GetSpanishEquivalent(FruitType typeOfFruit)
    {
        switch(typeOfFruit)
        {
            case FruitType.Apple:
                return SpanishFruitType.Manzana;
            case FruitType.Banana:
                return SpanishFruitType.Platano;
            case FruitType.Orange:
                return SpanishFruitType.Naranja;
            case FruitType.Mango:
            case FruitType.Other:
                return SpanishFruitType.Otra;
            default:
                throw new Exception("what kind of fruit is " + typeOfFruit + "?!");
        }
    }

    public static SpanishFruitType GetSpanishEquivalent(string typeOfFruit)
    {
        switch(typeOfFruit)
        {
            case "apple":
                return SpanishFruitType.Manzana;
            case "banana":
                return SpanishFruitType.Platano;
            case "orange":
                return SpanishFruitType.Naranja;
            case "mango":
            case "other":
                return SpanishFruitType.Otra;
            default:
                throw new Exception("what kind of fruit is " + typeOfFruit + "?!");
        }
    }

    public static Dictionary<FruitType, SpanishFruitType> EnglishToSpanishFruit = new Dictionary<FruitType, SpanishFruitType>()
    {
        {FruitType.Apple, SpanishFruitType.Manzana}
        ,{FruitType.Banana, SpanishFruitType.Platano}
        ,{FruitType.Mango, SpanishFruitType.Otra}
        ,{FruitType.Orange, SpanishFruitType.Naranja}
        ,{FruitType.Other, SpanishFruitType.Otra}
    };

    public static SpanishFruitType GetSpanishEquivalentWithDictionary(FruitType typeOfFruit)
    {
        return EnglishToSpanishFruit[typeOfFruit]; // throws exception if it's not in the dictionary, which is fine.
    }

    public static SpanishFruitType[] EnglishToSpanishFruitArray;

    static Fruits() {
        EnglishToSpanishFruitArray = new SpanishFruitType[EnglishToSpanishFruit.Select(p => (int)p.Key).Max() + 1];
        foreach (var pair in EnglishToSpanishFruit)
            EnglishToSpanishFruitArray[(int)pair.Key] = pair.Value;
    }

    public static SpanishFruitType GetSpanishEquivalentWithArray(FruitType typeOfFruit)
    {
        return EnglishToSpanishFruitArray[(int)typeOfFruit]; // throws exception if it's not in the dictionary, which is fine.
    }
}

结果:

Iterations: 100
Array time     : 00:00:00.0108628
Switch time    : 00:00:00.0002204
Dictionary time: 00:00:00.0008475

Iterations: 1000
Array time     : 00:00:00.0000410
Switch time    : 00:00:00.0000472
Dictionary time: 00:00:00.0004556

Iterations: 10000
Array time     : 00:00:00.0006095
Switch time    : 00:00:00.0011230
Dictionary time: 00:00:00.0074769

Iterations: 100000
Array time     : 00:00:00.0043019
Switch time    : 00:00:00.0047117
Dictionary time: 00:00:00.0611122

Iterations: 1000000
Array time     : 00:00:00.0468998
Switch time    : 00:00:00.0520848
Dictionary time: 00:00:00.5861588

Iterations: 10000000
Array time     : 00:00:00.4268453
Switch time    : 00:00:00.5002004
Dictionary time: 00:00:07.5352484

Iterations: 100000000
Array time     : 00:00:04.1720282
Switch time    : 00:00:04.9347176
Dictionary time: 00:00:56.0107932

会发生什么。我们看一下生成的IL代码:

Fruits.GetSpanishEquivalent:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  stloc.1     
IL_0003:  ldloc.1     
IL_0004:  switch      (IL_002B, IL_001F, IL_0023, IL_002B, IL_0027)
IL_001D:  br.s        IL_002F
IL_001F:  ldc.i4.1    
IL_0020:  stloc.0     
IL_0021:  br.s        IL_004A
IL_0023:  ldc.i4.3    
IL_0024:  stloc.0     
IL_0025:  br.s        IL_004A
IL_0027:  ldc.i4.2    
IL_0028:  stloc.0     
IL_0029:  br.s        IL_004A
IL_002B:  ldc.i4.0    
IL_002C:  stloc.0     
IL_002D:  br.s        IL_004A
IL_002F:  ldstr       "what kind of fruit is "
IL_0034:  ldarg.0     
IL_0035:  box         UserQuery+Fruits.FruitType
IL_003A:  ldstr       "?!"
IL_003F:  call        System.String.Concat
IL_0044:  newobj      System.Exception..ctor
IL_0049:  throw       
IL_004A:  ldloc.0     
IL_004B:  ret         

会发生什么? Switch 发生。对于顺序数量的值,可以通过从数组跳转到指针来优化和替换开关。为什么真正的数组比 switch 运行得更快 - 不知道,它只是运行得更快。

好吧,如果您不使用枚举,而是使用字符串,那么在少数变体上,开关和字典之间没有真正的区别。随着变体越来越多,字典变得越来越快。

选择什么?选择对您和您的团队更容易阅读的内容。当您看到您的解决方案产生性能问题时,您应该像我一样替换 Dictionary(如果您使用它)来切换或排列。当您的翻译功能很少被调用时,无需对其进行优化。

谈到您的情况 - 要获得翻译,所有解决方案都不好。翻译必须存储在资源中。必须只有一个 FruitType,没有其他枚举。

【讨论】:

  • 非常好。这个例子有点做作,而不是 enum -> other enum 它可能是 enum -> Action。不过,“存储在资源中”是什么意思?
  • @user420667 线路以“谈论您的案例”开始。谈论翻译。对于所有其他情况,这种方法不适用。关于资源:msdn.microsoft.com/en-us/library/9za7fxc7%28v=vs.80%29.aspx
  • 这个性能分析有很多错误,我真的不知道从哪里开始。秒表不仅测量Random 生成每个随机数所花费的时间(这比检索操作慢几个数量级并淹没它们),而且每次运行都使用不同的随机数。此外,您正在测量在Fruits.GetSpanishEquivalentWithArray 的第一次迭代中运行Fruits 的静态构造函数所花费的时间。这就是“100 次迭代”结果中“数组时间”如此之大的原因。
  • 如果您重写代码以仅测量检索操作,您会发现数组查找总是比开关快,因为生成的程序集是无分支的。相反,开关将被编译成一系列条件分支(除非案例数量很大,在这种情况下它将被编译成跳转表)。不过,使用数组查找的问题是 Enum 元素可以采用任何整数值。您需要处理边缘情况(例如,具有基础 long 数据类型或负值等的枚举)。
  • 添加了另一个概述最佳通用解决方案的答案。
【解决方案2】:

既然翻译是一对一或一对一的,为什么不为每个单词分配 ID。然后从一个枚举转换到另一个枚举

所以将你的枚举定义为

enum FruitType
{
    Other = 0,
    Apple = 1,
    Banana = 2,
    Mango = 3,
    Orange = 4
}
enum SpanishFruitType
{
    Otra = 0,
    Manzana = 1, // Apple
    Platano = 2, // Banana
    Naranja = 4, // Orange
}

然后将你的转换方法定义为

private static SpanishFruitType GetSpanishEquivalent(FruitType typeOfFruit)
{
    //'translate' with the word's ID. 
    //If there is no translation, the enum would be undefined
    SpanishFruitType translation = (SpanishFruitType)(int)typeOfFruit;

    //Check if the translation is defined
    if (Enum.IsDefined(typeof(SpanishFruitType), translation))
    {
        return translation;
    }
    else
    {
        return SpanishFruitType.Otra;
    }
}

【讨论】:

  • 我想在这种特殊情况下,这可能是一个好方法。但是,我可能应该把这个放在问题中,但是可能有西班牙语枚举在英语中没有意义,在这种情况下,一对一或一对一就消失了。
【解决方案3】:

这实际上取决于您的情况,但作为替代方案,您可以只拥有一个包含翻译文本的属性。

public enum FruitType
{
   [Description("Otra")]
   Other,
   [Description("Manzana")]
   Apple,            
   [Description("Platano")]
   Banana,      
   Mango,
   [Description("Naranja")]
   Orange
}

那么你就可以有一个方法来阅读描述

public static string GetTranslation(FruitType fruit)
{
    var mi = typeof(FruitType).GetMember(fruit.ToString());
    var attr = mi[0].GetCustomAttributes(typeof(DescriptionAttribute),false);
    if (attr.Count() > 0)
        return ((DescriptionAttribute)attr[0]).Description;
    else
        return fruit.ToString(); //if no description added, return the original fruit
}

所以你可以这样称呼它

string translated = GetTranslation(FruitType.Apple);

由于它使用反射,这可能是效率最低的,但根据您的情况可能更容易维护,并且正如 Chris 在 cmets 中提到的那样,根据调用频率的不同,它可能不会产生任何明显的影响。当然,您可以将Description 换成自定义属性。只是您可以考虑的另一种选择:)

【讨论】:

  • 不,坏主意。不同的语言有不同的描述
  • @nsinreal - 枚举可以有多个属性(DescriptionAttribute 没有,但没有理由为什么将AllowMultiple 设置为true 的自定义属性无法工作)。诚然,它的性能可能不那么好(尽管它是否明显需要测试),但这意味着只需要一个更新源。
  • 无论如何这都是个坏主意。翻译必须在资源中,而不是在代码中。
  • @nsinreal 但这与 OP 的任何建议有何不同?我认为,除了直接访问资源文件来执行翻译之外,用资源文件中的字符串替换属性中的字符串比替换字典中的枚举或资源文件中的 switch 语句更容易。
  • 谁说原创解决方案很好?你必须说一些额外的信息,不是吗?但是作者的代码有点不同:它是关于将枚举转换为枚举,而不是关于将枚举转换为字符串。我不知道作者是如何选择水果的——或者他真的需要描述水果(可能没有),或者他只是展示了一个思考的例子。什么是原始任务 - 将枚举转换为枚举;或枚举到字符串 - 这是信息未知。
【解决方案4】:

这个问题似乎是在寻找检索映射到Enum 常量的项目的最快方法。

几乎所有Enum 类型的底层类型,不是位字段(即未声明为[Flags]),是一个32 位有符号整数。这有合理的性能原因。使用不同的东西的唯一真正原因是您绝对必须尽量减少内存使用。位域是另一回事,但我们在这里不关心它们。

在这种典型情况下,数组映射是理想的(通常比switch 更快)。这是一些简洁且针对检索进行优化的通用代码。不幸的是,由于 .NET 泛型约束的限制,需要一些技巧(例如必须将强制转换委托传递给实例构造函数)。

using System;
using System.Runtime.CompilerServices;

namespace DEMO
{
    public sealed class EnumMapper<TKey, TValue> where TKey : struct, IConvertible
    {
        private struct FlaggedValue<T>
        {
            public bool flag;
            public T value;
        }

        private static readonly int size;
        private readonly Func<TKey, int> func;
        private FlaggedValue<TValue>[] flaggedValues;

        public TValue this[TKey key]
        {
            get
            {
                int index = this.func.Invoke(key);

                FlaggedValue<TValue> flaggedValue = this.flaggedValues[index];

                if (flaggedValue.flag == false)
                {
                    EnumMapper<TKey, TValue>.ThrowNoMappingException(); // Don't want the exception code in the method. Make this callsite as small as possible to promote JIT inlining and squeeze out every last bit of performance.
                }

                return flaggedValue.value;
            }
        }

        static EnumMapper()
        {
            Type keyType = typeof(TKey);

            if (keyType.IsEnum == false)
            {
                throw new Exception("The key type [" + keyType.AssemblyQualifiedName + "] is not an enumeration.");
            }

            Type underlyingType = Enum.GetUnderlyingType(keyType);

            if (underlyingType != typeof(int))
            {
                throw new Exception("The key type's underlying type [" + underlyingType.AssemblyQualifiedName + "] is not a 32-bit signed integer.");
            }

            var values = (int[])Enum.GetValues(keyType);

            int maxValue = 0;

            foreach (int value in values)
            {
                if (value < 0)
                {
                    throw new Exception("The key type has a constant with a negative value.");
                }

                if (value > maxValue)
                {
                    maxValue = value;
                }
            }

            EnumMapper<TKey, TValue>.size = maxValue + 1;
        }

        public EnumMapper(Func<TKey, int> func)
        {
            if (func == null)
            {
                throw new ArgumentNullException("func",
                                                "The func cannot be a null reference.");
            }

            this.func = func;

            this.flaggedValues = new FlaggedValue<TValue>[EnumMapper<TKey, TValue>.size];
        }

        public static EnumMapper<TKey, TValue> Construct(Func<TKey, int> func)
        {
            return new EnumMapper<TKey, TValue>(func);
        }

        public EnumMapper<TKey, TValue> Map(TKey key,
                                            TValue value)
        {
            int index = this.func.Invoke(key);

            FlaggedValue<TValue> flaggedValue;

            flaggedValue.flag = true;
            flaggedValue.value = value;

            this.flaggedValues[index] = flaggedValue;

            return this;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static void ThrowNoMappingException()
        {
            throw new Exception("No mapping exists corresponding to the key.");
        }
    }
}

然后,您可以使用漂亮的流畅界面简单地初始化映射:

var mapper = EnumMapper<EnumType, ValueType>.Construct((x) => (int)x)
                                            .Map(EnumType.Constant1, value1)
                                            .Map(EnumType.Constant2, value2)
                                            .Map(EnumType.Constant3, value3)
                                            .Map(EnumType.Constant4, value4)
                                            .Map(EnumType.Constant5, value5);

并轻松检索映射值:

ValueType value = mapper[EnumType.Constant3];

检索方法的 x86 程序集(使用 Visual Studio 2013 编译器生成)是最小的:

000007FE8E9909B0  push        rsi  
000007FE8E9909B1  sub         rsp,20h  
000007FE8E9909B5  mov         rsi,rcx  
000007FE8E9909B8  mov         rax,qword ptr [rsi+8]  
000007FE8E9909BC  mov         rcx,qword ptr [rax+8]  
000007FE8E9909C0  call        qword ptr [rax+18h]  // The casting delegate's callsite is optimised to just two instructions 
000007FE8E9909C3  mov         rdx,qword ptr [rsi+10h]  
000007FE8E9909C7  mov         ecx,dword ptr [rdx+8]  
000007FE8E9909CA  cmp         eax,ecx  
000007FE8E9909CC  jae         000007FE8E9909ED  
000007FE8E9909CE  movsxd      rax,eax  
000007FE8E9909D1  lea         rax,[rdx+rax*8+10h]  
000007FE8E9909D6  movzx       edx,byte ptr [rax]  
000007FE8E9909D9  mov         esi,dword ptr [rax+4]  
000007FE8E9909DC  test        dl,dl  
000007FE8E9909DE  jne         000007FE8E9909E5  
000007FE8E9909E0  call        000007FE8E9901B8  
000007FE8E9909E5  mov         eax,esi  
000007FE8E9909E7  add         rsp,20h  
000007FE8E9909EB  pop         rsi  
000007FE8E9909EC  ret  
000007FE8E9909ED  call        000007FEEE411A08  
000007FE8E9909F2  int         3 

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多