优化 Enum.ToString()






4.80/5 (46投票s)
一个泛型的 Enum 版本,提供更快的格式化速度
引言
虽然对于大多数项目来说这不是一个问题,但有些开发者可能会注意到,仅仅是将枚举值转换为字符串就需要花费大量处理器时间。如果您开发一个高负载的服务器应用程序,并且每秒需要处理成千上万次枚举名称的获取,那么这一点尤为重要。本文介绍的类可以为任何枚举解决这个问题。
请注意,`Enum.ToString()` 方法经常会被隐式调用,例如在以下语句中
"Paint it " + Color.Black
Console.Write("Paint it {0}", Color.Black)
问题
为了找到枚举值的字符串表示,.NET Framework 使用了 Reflection,众所周知,这是一项非常耗时的操作。此外,它还对所有枚举使用通用的方法,检查它是否使用了 `FlagsAttribute` 等等。由于每次调用 `ToString` 都会分配新的字符串,因此还会导致过多的内存消耗。
最简单快捷的解决方案是声明一个字符串数组,其中包含枚举中所有值的字符串表示。例如,对于简单的枚举
enum Color
{
Red,
Green,
Blue
}
数组看起来会像这样
private static readonly string[] colorNames = new[] {"Red", "Green", "Blue"};
为了将 `Color` 值转换为字符串,我们只需将枚举值转换为 `int`,然后通过该索引获取数组值
string myColor = colorNames[(int) color];
标准的 `Enum.ToString()` 实现会在值未定义时返回一个数字。所以我们的代码也应该进行数组边界检查。
int i = (int)value;
string myColor = i >= 0 && i < colorNames.Length ? colorNames[i] : i.ToString();
实际上,您不需要手动输入数组项,您可以直接将 `Enum.GetNames()` 方法的结果赋值给它
private static readonly string[] colorNames = Enum.GetNames(typeof(Color));
虽然这种方法确实是最快的,但它也有一些限制
- 它只能应用于非常简单的枚举。对于“稀疏”枚举或带有 `FlagsAttribute` 属性的枚举,它将不起作用。如果您想提高稀疏枚举的性能,应该使用字典而不是数组(这会稍慢一些)。将标志转换为字符串是一项相当复杂的操作。
- 它需要为每个要格式化的枚举进行额外的编码。
另一种流行的解决方案是使用 `switch` 语句。它也表现良好,并且支持任何类型的枚举,但它需要更多的手动编码,并且需要保持枚举和格式化方法同步。
解决方案
目标是创建一个通用方法,它能够像 `Enum.ToString()` 一样工作,但效率更高。
为了通用化该方法并使其对任何枚举类型都尽可能快,我利用了泛型的一个有趣特性:泛型类型的静态构造函数会在每次使用实际类型作为类型参数时调用一次,并且泛型类型的每个特化都会使用自己的一组静态字段。因此,我们可以声明一个泛型的 `Enum` 类型,并将其静态成员中保存执行枚举格式化所需的所有信息。我们还可以应用策略设计模式来为每个枚举类型选择最佳策略。这是类的声明
/// <summary>
/// Helper class for enum types
/// </summary>
/// <typeparam name="T">Must be enum type (declared using
/// the <c>enum</c> keyword)</typeparam>
public static class Enum<t> where T : struct, IConvertible
(希望 .NET 有一个枚举的约束。)
在我的静态类中,我考虑了三种枚举类型
- 简单 - 当命名常量占据从 0 到 N 的所有连续值时。
- 稀疏 - 当命名常量不连续,或者不从零开始时(基本上是任何带有初始值的枚举)。
- 带标志 - 用 `FlagsAttribute` 属性标记。我使用字符串数组来存储简单枚举的名称,使用 `Dictionary<int,string>` 来存储稀疏枚举的名称,并使用字典+无符号整数数组来存储带标志的枚举。
看一下静态构造函数
static Enum()
{
Type type = typeof(T);
if (!type.IsEnum)
throw new ArgumentException("Generic Enum type works only with enums");
string[] names = Enum.GetNames(type);
var values = (T[])Enum.GetValues(type);
if (type.GetCustomAttributes(typeof(FlagsAttribute), false).Length > 0)
{
Converter = new FlagsEnumConverter(names, values);
}
else
{
if (values.Where((t, i) => Convert.ToInt32(t) != i).Any())
{
Converter = new DictionaryEnumConverter(names, values);
}
if (Converter == null)
Converter = new ArrayEnumConverter(names);
}
}
类 `ArrayEnumConverter`、`DictionaryEnumConverter` 和 `FlagsEnumConverter` 是嵌套类,它们都继承自同一个抽象类 `EnumConverter`。`Converter` 是一个类型为 `EnumConverter` 的静态字段。
为了性能,`Enum` 类不仅支持按枚举值获取枚举名称,还支持按相应的整数值获取。相应的方法只是调用已保存的 `EnumConverter` 实例的方法。
/// <summary>
/// Converts enum value to string
/// </summary>
/// <param name="value">Enum value converted to int</param>
/// <returns>If <paramref name="value"/> is defined,
/// the enum member name; otherwise the string representation
/// of the <paramref name="value"/>.
/// If <see cref="FlagsAttribute"/> is applied,
/// can return comma-separated list of values</returns>
public static string ToString(int value)
{
return Converter.ToStringInternal(value);
}
/// <summary>
/// Converts enum value to string
/// </summary>
/// <param name="value">Enum value</param>
/// <returns>If <paramref name="value"/> is defined,
/// the enum member name; otherwise the string representation
/// of the <paramref name="value"/>.
/// If <see cref="FlagsAttribute"/> is applied,
/// can return comma-separated list of values</returns>
public static string ToString(T value)
{
return Converter.ToStringInternal(value.ToInt32(null));
}
使用这些方法几乎和使用标准实现一样简单(尽管它不会被隐式调用)。
string myColor = Enum<Color>.ToString(Color.Blue);
//or
string myColor = Enum<Color>.ToString((int)Color.Blue);
//or
string myColor = Enum<Color>.ToString(2);
关于解析呢?
显然,可以使用相同的方法来解析枚举值。更快的解析方法是使用内部 `Dictionary<string,int>` 来通过枚举名称快速查找值。但由于这不是本文的目标,我在附带的源代码中使用了与 `ToString()` 方法相同的集合,并简单地进行遍历。我还添加了限制将任意整数值解析为未声明枚举值的功能,这在标准实现中是不支持的。它还包含 `TryParse` 方法,因此如果您使用 .NET 2.0,可能会发现它们很有用。
public static T Parse(string value, bool ignoreCase = false, bool parseNumeric = true)
{
return (T) Enum.ToObject(typeof(T),
Converter.ParseInternal(value, ignoreCase, parseNumeric));
}
public static bool TryParse(string value, bool ignoreCase,
bool parseNumeric, out T result)
{
int ir;
bool b = Converter.TryParseInternal(value, ignoreCase, parseNumeric, out ir);
result = (T) Enum.ToObject(typeof(T), ir);
return b;
}
public static bool TryParse(string value, bool ignoreCase, out T result)
{
int ir;
bool b = Converter.TryParseInternal(value, ignoreCase, true, out ir);
result = (T)Enum.ToObject(typeof(T), ir);
return b;
}
public static bool TryParse(string value, out T result)
{
int ir;
bool b = Converter.TryParseInternal(value, false, true, out ir);
result = (T)Enum.ToObject(typeof(T), ir);
return b;
}
因此,`EnumConverter` 类声明如下
abstract class EnumConverter
{
public abstract string ToStringInternal(int value);
public abstract int ParseInternal(string value, bool ignoreCase, bool parseNumber);
public abstract bool TryParseInternal(string value,
bool ignoreCase, bool parseNumber, out int result);
}
性能
我写了一个简单的性能测试来比较我的类和标准的 .NET 实现的性能。在测试中,我使用了这三个枚举
enum Simple
{
Zero,
One,
Two,
Three,
Four,
Five
}
enum Sparse
{
MinusOne = -1,
One = 1,
Three = 3,
Four,
Five,
Hundred = 100,
}
[Flags]
enum Flagged
{
None = 0,
One = 1,
Two = 2,
Both = 3,
Four = 4,
All = 7
}
在测试中,我只是将不同的方法实现调用了 1,000,000 次,并将结果输出到控制台。
以下是将 `Simple.Four` 值转换为字符串的五种不同方法的测试结果
Operation Time Ratio Result
Simple.Four.ToString() 2059 1,00 Four
Enum<Simple>.ToString(Simple.Four) 343 6,00 Four
Enum<Simple>.ToString(4) 26 79,19 Four
SimpleToStringUsingSwitch(Simple.Four) 13 158,38 Four
SimpleToStringUsingArray(Simple.Four) 16 128,69 Four
结果表明,手动方法在性能上仍然是最好的,但我们接受整数的重载紧随其后,并且比标准版本快了近 80 倍!接受枚举的重载快了 6 倍,这也非常好。
结果列显示每种方法都产生相同的结果。
其他参数值和其他枚举类型的测试结果
Operation Time Ratio Result
((Simple) 404).ToString() 2096 1,00 404
Enum<Simple>.ToString((Simple) 404) 618 3,39 404
Enum<Simple>.ToString(404) 285 7,35 404
Sparse.Four.ToString() 2007 1,00 Four
Enum<Sparse>.ToString(Sparse.Four) 382 5,25 Four
Enum<Sparse>.ToString(4) 94 21,35 Four
((Sparse) 404).ToString() 2162 1,00 404
Enum<Sparse>.ToString((Sparse) 404) 688 3,14 404
Enum<Sparse>.ToString(404) 342 6,32 404
Flagged.Four.ToString() 5011 1,00 Four
Enum<Flagged>.ToString(Flagged.Four) 388 12,91 Four
Enum<Flagged>.ToString(4) 99 50,62 Four
((Flagged) 404).ToString() 5585 1,00 404
Enum<Flagged>.ToString((Flagged) 404) 1138 4,91 404
Enum<Flagged>.ToString(404) 800 6,98 404
(Flagged.Two|Flagged.Four).ToString() 5301 1,00 Two, Four
Enum<Flagged>.ToString(Flagged.Two|Flagged.Four) 1302 4,07 Two, Four
Enum<Flagged>.ToString(6) 949 5,59 Two, Four
Flagged.All.ToString() 5021 1,00 All
Enum<Flagged>.ToString(Flagged.All) 398 12,62 All
Enum<Flagged>.ToString(7) 99 50,72 All
我没有对解析方法进行太多优化,但在大多数情况下,它们的性能也比标准实现要好。
Operation Time Ratio Result
Enum.Parse(typeof(Simple), "Four") 1191 1,00 Four
Enum<Simple>.Parse("Four") 670 1,78 Four
Enum.Parse(typeof(Simple), "4") 1193 1,00 Four
Enum<Simple>.Parse("4") 791 1,51 Four
Enum.Parse(typeof(Sparse), "Four") 1126 1,00 Four
Enum<Sparse>.Parse("Four") 799 1,41 Four
Enum.Parse(typeof(Sparse), "4") 1184 1,00 Four
Enum<Sparse>.Parse("4") 806 1,47 Four
Enum.Parse(typeof(Flagged), "Four") 1184 1,00 Four
Enum<Flagged>.Parse("Four") 1114 1,06 Four
Enum.Parse(typeof(Flagged), "4") 1229 1,00 Four
Enum<Flagged>.Parse("4") 948 1,30 Four
Enum.Parse(typeof(Flagged), "Two,Four") 1517 1,00 Two, Four
Enum<Flagged>.Parse("Two,Four") 1835 0,83 Two, Four
尽管该类内部使用 `Int32`,但它与除 `Int64` 和 `UInt64` 之外的任何底层类型都能很好地工作。这是一个限制,可以很容易地克服,但个人从未需要使用如此大的枚举,所以我没有实现它。
源代码
在附件中,您将找到泛型 Enum 类的源代码和性能测试。非常欢迎对实现有任何想法。