enum case handling - better to use a switch or a dictionary?

前端 未结 4 1962
逝去的感伤
逝去的感伤 2021-02-20 16:20

When handling the values of an enum on a case by case basis, is it better to use a switch statement or a dictionary?

I would think that the dictionary would be faster.

相关标签:
4条回答
  • 2021-02-20 16:39

    In fact, a dictionary is slower. Really. Just write simple benchmark (I've added example with converting dictionary to array):

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

    Results:

    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
    

    What happens. Let's look on the generated IL code:

    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         
    

    What happens? Switch happens. For sequenced number of values switch can be optimized and replaced by jump to pointer from array. Why real array works faster than switch - dunno, it just works faster.

    Well, if you do not work with enums, but with strings there is no real difference between switch and dictionary on small number of variants. With more and more variants dictionary becomes faster.

    What to choose? Choose what is easier to read for you and your team. When you see that your solution creates performance issues you should replace Dictionary (if you use it) to switch or array like me. When your function of translation is rarely called there is no need to optimize it.

    Talking about your case - to get translation, all solutions are bad. Translations must be stored in resources. There must be only one FruitType, no other enums.

    0 讨论(0)
  • 2021-02-20 16:43

    Since the translations are one-to-one or one-to-none, why not assign IDs to each word. Then cast from one enum to another

    So define your enums as

    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
    }
    

    Then define your conversion method as

    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;
        }
    }
    
    0 讨论(0)
  • 2021-02-20 16:51

    This question seems to be looking for the fastest method of retrieving an item which is mapped to an Enum constant.

    The underlying type for almost all Enum types, which are not bit fields (i.e. not declared as [Flags]), is a 32-bit signed integer. There are sound performance reasons for this. The only real reason to use something different is if you absolutely must minimise memory usage. Bit fields are a different matter but we're not concerned with them here.

    In this typical scenario, an array map is ideal (and usually faster than a switch). Here's some generic code which is concise and optimised for retrieval. Unfortunately, due to the limitations of .NET generic constraints, a few hacks are required (such as having to pass a casting delegate into the instance constructor).

    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.");
            }
        }
    }
    

    You can then simply initialise the mappings using a nice fluent interface:

    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);
    

    And easily retrieve the mapped value:

    ValueType value = mapper[EnumType.Constant3];
    

    The x86 assembly (generated using the Visual Studio 2013 compiler) for the retrieval method is minimal:

    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 
    
    0 讨论(0)
  • 2021-02-20 16:53

    It really depends on your scenario but, as an alternative, you could just have an attribute containing the translated text.

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

    Then you can have a method to read the description

    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
    }
    

    So you can then call it like this

    string translated = GetTranslation(FruitType.Apple);
    

    Since it uses reflection, this is likely to be the least efficient but might be easier to maintain depending on your situation and, as Chris mentioned in the comments, may not have any noticeable impact depending on how often it's called. You can swap Description for a custom attribute of course. Just another option for you to consider :)

    0 讨论(0)
提交回复
热议问题