枚举和性能

33

我的应用程序有许多不同的检索值,这些值从不改变,例如美国州。我想使用枚举而不是将它们放入数据库表中。

但是,我意识到这样做涉及到一些枚举和很多从“int”和“string”到我的枚举的转换。

另外,我看到有人提到使用Dictionary<>作为查找表,但是枚举实现似乎更加简洁。

所以,我想问一下保留大量枚举并进行转换是否会影响性能,还是应该使用查找表的方法来获得更好的性能?

编辑:需要进行强制类型转换是因为ID需要存储在其他数据库表中。


3
为什么需要把它们铸造? - LukeH
2
@LukeH:可能是因为在数据库中,这些值只是整数。 - Jon Skeet
@Jon:这正是我猜的,但最好从发帖者那里得到澄清。 - LukeH
1
Ray,请告诉我你对我的更新答案有什么看法。 - StriplingWarrior
@StriplingWarrior:非常感谢您的帮助,我已经为您的答案点赞。我决定采用两种方法,使用枚举来代替整数,并使用字典进行字符串查找。 - Ray
我知道这是一个老问题,但“这些值永远不会改变,例如美国州”是一个相当强大的假设。谁能说美国永远不会获得或失去州?阿拉斯加和夏威夷直到1959年才成为美国的州 - 不到一个世纪前。 - Pharap
8个回答

49

int转换为枚举类型是非常便宜的...它比字典查找更快。基本上这只是一个无操作,只是将位复制到具有不同概念类型的位置。

将字符串解析为枚举值会慢一些。

但是,老实说,我怀疑这不会成为您的瓶颈...如果不知道更多您在做什么的信息,很难给出建议,除了正常的“编写最简单,最可读和易于维护的代码,然后检查其性能是否足够。”


1
这可能是另一个问题,但与此相关:使用匹配的底层“int”值将枚举强制转换为彼此是否与“(int)”转换一样便宜? - CAD bloke
3
@CADBloke:我认为应该可以,但如果对你很重要的话,可以试一下 :) - Jon Skeet

20
你不会在这两个之间看到很大的性能差异,但我仍然建议使用字典,因为它将为您提供更多的未来灵活性。
首先,在C#中,枚举不能像Java那样自动关联一个类,因此,如果您想将其他信息与状态关联(全名、首都、邮政缩写等),创建一个“UnitedState”类将更容易地将所有这些信息打包到一个集合中。
另外,即使您认为这个值永远不会改变,它也不是完全不可变的。例如,您可能会有一个包括领土的新要求。或者也许您需要允许加拿大用户查看加拿大省份的名称。如果您像处理任何其他数据集合一样处理此集合(使用存储库从中检索值),则稍后可以更改存储库实现以从不同来源(数据库、Web服务、会话等)获取值。 枚举的适用性要小得多。
编辑:
关于性能论点:请记住,您不仅仅是将枚举转换为int:您还在对该枚举运行ToString(),这会增加相当多的处理时间。请考虑以下测试:
const int C = 10000;
int[] ids = new int[C];
string[] names = new string[C];
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i< C; i++)
{
    var id = (i % 50) + 1;
    names[i] = ((States)id).ToString();
}
sw.Stop();
Console.WriteLine("Enum: " + sw.Elapsed.TotalMilliseconds);
var namesById = Enum.GetValues(typeof(States)).Cast<States>()
                .ToDictionary(s => (int) s, s => s.ToString());
sw.Restart();
for (int i = 0; i< C; i++)
{
    var id = (i % 50) + 1;
    names[i] = namesById[id];
}
sw.Stop();
Console.WriteLine("Dictionary: " + sw.Elapsed.TotalMilliseconds);

结果:

Enum: 26.4875
Dictionary: 0.7684

如果性能确实是您的首要关注点,那么使用Dictionary绝对是最佳选择。但是,我们在这里谈论的速度非常快,以至于在关注速度问题之前,我会先处理半打其他问题。
C#中的枚举并不是为了提供值和字符串之间的映射而设计的。它们被设计为提供可以在代码中传递的强类型常量值。其两个主要优点是:
1.您有一个额外的编译器检查提示,可以帮助您避免参数传递错误等问题。 2.与在代码中放置"神奇"数字值(例如"42")相比,您可以说"States.Oklahoma",从而使您的代码更易读。
与Java不同,C#不会自动检查转换后的值是否有效(myState = (States)321),因此,如果没有手动进行运行时数据检查,则无法对输入进行任何运行时数据检查。如果没有引用明确指向州("States.Oklahoma"),那么您将无法从上述第2个优点中获得任何价值。这使得第1个优点成为使用枚举的唯一真正原因。如果这个原因对您来说足够好,那么我建议您使用枚举而不是int作为键值。然后,在需要与状态相关联的字符串或其他值时,执行Dictionary查找。
以下是我如何做到的:
public enum StateKey{
    AL = 1,AK,AS,AZ,AR,CA,CO,CT,DE,DC,FM,FL,GA,GU,
    HI,ID,IL,IN,IA,KS,KY,LA,ME,MH,MD,MA,MI,MN,MS,
    MO,MT,NE,NV,NH,NJ,NM,NY,NC,ND,MP,OH,OK,OR,PW,
    PA,PR,RI,SC,SD,TN,TX,UT,VT,VI,VA,WA,WV,WI,WY,
}

public class State
{
    public StateKey Key {get;set;}
    public int IntKey {get {return (int)Key;}}
    public string PostalAbbreviation {get;set;}
    
}

public interface IStateRepository
{
    State GetByKey(StateKey key);
}

public class StateRepository : IStateRepository
{
    private static Dictionary<StateKey, State> _statesByKey;
    static StateRepository()
    {
        _statesByKey = Enum.GetValues(typeof(StateKey))
        .Cast<StateKey>()
        .ToDictionary(k => k, k => new State {Key = k, PostalAbbreviation = k.ToString()});
    }
    public State GetByKey(StateKey key)
    {
        return _statesByKey[key];
    }
}

public class Foo
{
    IStateRepository _repository;
    // Dependency Injection makes this class unit-testable
    public Foo(IStateRepository repository) 
    {
        _repository = repository;
    }
    // If you haven't learned the wonders of DI, do this:
    public Foo()
    {
        _repository = new StateRepository();
    }
    
    public void DoSomethingWithAState(StateKey key)
    {
        Console.WriteLine(_repository.GetByKey(key).PostalAbbreviation);
    }
}

这样做有以下好处:

  1. 你可以传递强类型的值来代表一个状态,
  2. 如果给定无效输入,你的查找函数会立即失败,
  3. 你可以轻松更改实际状态数据存放的位置,
  4. 你可以轻松地在状态类中添加与状态相关的数据,
  5. 你可以轻松地在将来添加新的州、领地、区域、省份或其他内容。
  6. 从整数获取名称比使用Enum.ToString()仍然快约15倍

[grunt]


1
+1,我完全同意“不是完全不变”的部分。如果你的十年老应用程序基于过时的技术模型,而已经无法得到支持,那么你将会非常苦恼。 - Stefan Steinegger
1
不同意没有很大的性能差异;C#中的枚举只是int,因此在它们和int之间进行转换要快得多。你提出的论点很好,但我很难推荐为了可能不必要的灵活性而牺牲性能。 - TMN
3
即使使用枚举可能会更快,但我们谈论的是如此高的速度,以至于在绝大多数应用程序中不会察觉到差异。我很难建议为了微不足道的性能提升而牺牲灵活性。然而,事实证明,对于他所讨论的操作,使用字典查找实际上会更快。请参阅我的更新答案。 - StriplingWarrior
1
PS--请确保所有枚举值都被明确声明。否则,有人可能会意外地插入或重新排序一个值,并破坏您的数据库关系。 - StriplingWarrior

2
你可以使用类型安全枚举。
这是一个基类。
Public MustInherit Class AbstractTypeSafeEnum
    Private Shared ReadOnly syncroot As New Object
    Private Shared masterValue As Integer = 0

    Protected ReadOnly _name As String
    Protected ReadOnly _value As Integer

    Protected Sub New(ByVal name As String)
        Me._name = name
        SyncLock syncroot
            masterValue += 1
            Me._value = masterValue
        End SyncLock
    End Sub

    Public ReadOnly Property value() As Integer
        Get
            Return _value
        End Get
    End Property

    Public Overrides Function ToString() As String
        Return _name
    End Function

    Public Shared Operator =(ByVal ats1 As AbstractTypeSafeEnum, ByVal ats2 As AbstractTypeSafeEnum) As Boolean
        Return (ats1._value = ats2._value) And Type.Equals(ats1.GetType, ats2.GetType)
    End Operator

    Public Shared Operator <>(ByVal ats1 As AbstractTypeSafeEnum, ByVal ats2 As AbstractTypeSafeEnum) As Boolean
        Return Not (ats1 = ats2)
    End Operator

End Class

这里是一个枚举:

Public NotInheritable Class EnumProcType
    Inherits AbstractTypeSafeEnum

    Public Shared ReadOnly CREATE As New EnumProcType("Création")
    Public Shared ReadOnly MODIF As New EnumProcType("Modification")
    Public Shared ReadOnly DELETE As New EnumProcType("Suppression")

    Private Sub New(ByVal name As String)
        MyBase.New(name)
    End Sub

End Class

添加国际化功能变得更加容易了。

抱歉,它是用VB和法语编写的。

干杯!


由于整数值已映射到数据库中,因此最好能够指定显式整数值,以便在重新排序时不会更改它们。 - StriplingWarrior
我认为修改代码以允许那样做并不难。 - Alex Rouillard

0

或者您可以使用常量


在这里使用常量有什么优势? - StriplingWarrior

0

如果问题是“枚举转换比访问字典项更快吗?”,那么其他回答涉及性能的各个方面就有意义了。

但是这里的问题似乎是“当我需要将它们的值存储到数据库表中时,枚举转换会对应用程序性能产生负面影响吗?”。

如果是这样,我不需要运行任何测试来说,将数据存储在数据库表中总是比转换枚举或执行其ToString()慢几个数量级。

在这种情况下,我认为重要的是代码的可读性和可维护性。在简单的情况下,枚举可以干净地完成工作,但我同意其他答案,字典在长期内更加灵活。


0

使用 [Flags] 属性。如果你想要获取两个值之间的共同字段,可能有比给它分配一个新值更好的方法。例如:

[Flags]
public enum MyEnum

{
  statusA = 1,
  statusB = 2,
  both = status1 | status2 //This value = 3 ( 1+2 ) 
}

根据微软的设计准则,"使用二的幂作为标志枚举值,以便可以自由地使用按位或操作进行组合"(来源)。基于此,您在这里不希望进行按位或操作。我认为您可以在这里进行左移操作,对吗?(status1 << status2) - Jeremy Caney

-1

枚举类型在性能方面表现非常优秀,尤其是相比于字典。枚举类型只使用单个字节。但是为什么需要进行强制转换呢?似乎应该在任何地方都使用枚举类型。


8
枚举通常使用4个字节 - 除非您明确声明否则,它们的基本类型默认为int - LukeH
1
“大幅超越”的部分很大程度上取决于您获得枚举后要执行的操作。如果在switch语句中使用它,则枚举将更快。如果调用ToString,那么预填充的字典将更快。请参见我的答案。” - StriplingWarrior

-6
避免使用枚举:应该用从基类派生或实现接口的单例替换枚举。
使用枚举的做法来自于C语言的旧式编程。
你开始使用一个枚举来表示美国各州,然后你需要人口数量、首都等信息,你将需要很多大型开关来获取所有这些信息。

C#中的枚举不是对象。它们并没有真正从基类派生或实现接口。它们只是类型安全的常量。如果正确使用,它们可以很有用,但如果误解了它们,它们就会变得危险。 - StriplingWarrior
我知道枚举不是对象。这就是我说它们无用的原因。无论如何,我已经清楚地表达了我的答案。 - onof

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接