[Performance][C#]ToString V.S Enum.GetName

[Performance][C#]ToString V.S Enum.GetName

這幾天筆者抽空看了一下程式中有Boxing與UnBoxing的地方,因為想要解決程式中列舉部分處理會有Boxing的問題,而注意到了將列舉值直接ToString與Enum.GetName的不同。兩種寫法有著效能上的差異,因此筆者用下面這樣的範例程式測試了一下兩者所需耗費的時間:


using System;
using System.Diagnostics;
using System.Linq;
namespace ConsoleApplication24
{
	enum MyEnum
	{
		EnumItem1
	}

	class Program
	{
		static void Main(string[] args)
		{
			var count = 1000000;
			Console.WriteLine("ToString: {0} ms", DoTest(count, () =>
			{
				var temp = MyEnum.EnumItem1.ToString();
			}).ToString());

			Console.WriteLine("Enum.GetName: {0} ms", DoTest(count, () =>
			{ 
				var temp = Enum.GetName(typeof(MyEnum), MyEnum.EnumItem1);
			}).ToString());
		}

		static long DoTest(int count, Action action)
		{
			var sw = Stopwatch.StartNew();
			for(int i = 0;i<count;++i)
			{
				action();
			}
			return sw.ElapsedMilliseconds;
		}
	}
}

 

可以看到如下的運行結果。跑1000000次運算,ToString會需耗時1239ms,而Enum.GetName只需耗時465ms。

image

 

這邊筆者也有將兩者的耗時做了一張長條圖,可以很清楚的看到Enum.GetName確實運行起來有著較佳的效能。

image

 

那這樣的效能差距是怎樣出來的呢?看一下BCL在列舉值ToString時所做的處理動作就可以知道了,它會額外的判斷列舉是否有標記FlagAttribute,若有的話將導到Enum.InternalFlagsFormat去處理,若無的話則用Enum.GetName。


/// <summary>Converts the value of this instance to its equivalent string representation.</summary>
/// <returns>The string representation of the value of this instance.</returns>
/// <filterpriority>2</filterpriority>
[__DynamicallyInvokable]
public override string ToString()
{
	return Enum.InternalFormat((RuntimeType)base.GetType(), this.GetValue());
}

private static string InternalFormat(RuntimeType eT, object value)
{
	if (eT.IsDefined(typeof(FlagsAttribute), false))
	{
		return Enum.InternalFlagsFormat(eT, value);
	}
	string name = Enum.GetName(eT, value);
	if (name == null)
	{
		return value.ToString();
	}
	return name;
}

 

所以若是列舉沒有附加FlagAttribute,其實它內部還是叫用Enum.GetName去做。那麼兩種寫法到底有什麼樣的差異呢?又為何ToString要特別去看FlagAttribute呢?這邊我們直接來看個簡易的範例就可以理解了。


using System;
using System.Diagnostics;
using System.Linq;
namespace ConsoleApplication24
{
	[Flags]
	enum MyEnum
	{
		EnumItem1 = 1,
		EnumItem2 = 2,
		EnumItem3 = 1		
	}

	class Program
	{
		static void Main(string[] args)
		{
			Console.WriteLine(MyEnum.EnumItem1.ToString());
			Console.WriteLine(MyEnum.EnumItem2.ToString());
			Console.WriteLine(MyEnum.EnumItem3.ToString());
			Console.WriteLine((MyEnum.EnumItem1 | MyEnum.EnumItem2).ToString());

			Console.WriteLine();
			Console.WriteLine(Enum.GetName(typeof(MyEnum), MyEnum.EnumItem1));
			Console.WriteLine(Enum.GetName(typeof(MyEnum), MyEnum.EnumItem2));
			Console.WriteLine(Enum.GetName(typeof(MyEnum), MyEnum.EnumItem3));
			Console.WriteLine(Enum.GetName(typeof(MyEnum), MyEnum.EnumItem1 | MyEnum.EnumItem2));
		}
	}
}

 

上面的程式運行後可得到像下面這樣的運行結果:

image

 

很明顯的在列舉沒有附加上FlagAttribute,且沒有做過or運算時,兩者運行起來的效果是一樣的。對應到相同數值的列舉值不論用ToString還是Enum.GetName都會錯亂,像是範例中的EnumItem1與EnumItem3其值都是1,用ToString或是Enum.GetName帶入EnumItem3都會得到EnumItem1。但是若是列舉有附加FlagAttribute且做了or運算,那就只有ToString可以正常運作,這也就是BCL內特別做處理的部分。

 

這篇稍稍紀錄一下兩者處理方式的差異,知道兩者的差異後,我們可以依照不同的狀況給予最適合的處理方式。