public static string GetMd5Hash(string input) { using (MD5 md5Hash = MD5.Create()) { // Convert the input string to a byte array and compute the hash. byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input)); // Create a new Stringbuilder to collect the bytes // and create a string. StringBuilder sBuilder = new StringBuilder(); // Loop through each byte of the hashed data // and format each one as a hexadecimal string. for (int i = 0; i < data.Length; i++) { sBuilder.Append(data[i].ToString("x2")); } // Return the hexadecimal string. return sBuilder.ToString(); } }
這是一段 MSDN 官方的 MD5 示例,例子很簡單且很容易理解。但是,這個例子也有很多的問題,首先上例至少創建了 3 個臨時緩存區!且每次執行 GetMd5Hash 都會創建一個 MD5 實例,並在方法執行完成后釋放它。這些都造成了很大的系統資源浪費和增加了 GC 的壓力。
鑒於官方給的 Demo 並不優秀,且網上也沒有給出很好使用方式,這里我就拿出我多年使用的 MD5 打開方式,這個方法同時支持 SHA1,SHA256 等,即支持 System.Security.Cryptography 命名空間下的 HashAlgorithm(哈希算法) 實現。也同時支持 .Net Framework 2.0 之后的所有 .Net 平台。
先說明,這個文章是基於 System.Security.Cryptography 命名空間的實現,不是自己寫一個 MD5 算法哦。
現在我們開始,首先我們先定義一個輔助類:
using System; using System.Reflection; using System.Runtime.CompilerServices; using System.Security.Cryptography; static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm { /// <summary> /// 線程靜態變量。 /// 即:這個變量在每個線程中都是唯一的。 /// 再結合泛型類實現:該變量在不同泛型或不同的線程下的值都是不一樣的。 /// 這樣做的目的是為了避開多線程問題。
/// 關於垃圾回收:當 .NET 線程被釋放時,程序中的所有線程靜態變量都會被回收,GC 回收時同時將釋放資源,所以不必擔心釋放問題,GC 會幫助我們的。
/// 這里描述的 .NET 線程釋放不是指 .NET 線程回收至線程池。很多時候 .NET 的線程在程序關閉之前都不會真正釋放,而是在線程池中繼續駐留。
/// 線程唯一真的能避免多線程問題嗎?答:多個線程所用存儲空間都不一樣,那么臟值就不可能存在,如果這都能出現多線程問題,我直播吃....豬紅(本人極其厭惡吃豬紅🌚)。 /// </summary>h [ThreadStatic] static THashAlgorithm instance; public static THashAlgorithm Instance => instance ?? Create(); // C# 語法糖,低版本可以改為 { get { return instance != null ? instance : Create(); } } /// <summary> /// 尋找 THashAlgorithm 類型下的 Create 靜態方法,並執行它。 /// 如果沒找到,則執行 Activator.CreateInstance 調用構造方法創建實例。 /// 如果 Activator.CreateInstance 方法執行失敗,它會拋出異常。 /// </summary> [MethodImpl(MethodImplOptions.NoInlining)] static THashAlgorithm Create() { var createMethod = typeof(THashAlgorithm).GetMethod( nameof(HashAlgorithm.Create), // 這段代碼同 "Create",低版本 C# 可以替換掉 BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, Type.DefaultBinder, Type.EmptyTypes, null); if (createMethod != null) { instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { }); } else { instance = Activator.CreateInstance<THashAlgorithm>(); } return instance; } }
該輔助類幫助我們避開多線程問題,且幫助我們創建指定的 HashAlgorithm 實例。
這里說明一下,HashAlgorithm.ComputeHash (同 MD5.ComputeHash) 方法絕對不是線程安全的!大家使用它的時候必須要注意,在未線程同步下調用同一實例的 ComputeHash 方法得到的結果是錯誤的!
關於線程唯一和泛型唯一:
還記得老師教我們的時候強調靜態變量就是唯一的,可是現在就突然出現了兩個反例,與之對立,這讓初學者一下子難以接受,其實這也很容易理解的:
首先 [ThreadStatic] 特性我們可以理解為將字段封裝為了 ThreadLocal<T>,它在內部區分線程,然后返回不同的值。
然后泛型唯一,舉例:我們都知道 List<int> 和 List<string> 它們不是一個類型!那么它們的字段 List<int>.thread_field 和 List<string>.thread_field 也同理不是一個字段,那么它們的值當然也不是同一個啦。
接下來我們再定義實現類:
public static class HashAlgorithmHelper {
{
{
sBuilder.Append(item.ToString("x2"));
}
}
}
到這里我們入門級的 MD5 打開方式就完成了,使用方法:HashAlgorithmHelper.ComputeHash<MD5>("Hello World!")。
我們來先測試一下:
static void Main(string[] args) { Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!")); Console.WriteLine(GetMd5Hash("Hello World!")); while (true) { var stopwatch = Stopwatch.StartNew(); for (int i = 0; i < 1000000; i++) { HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"); } Console.WriteLine(stopwatch.ElapsedMilliseconds); stopwatch = Stopwatch.StartNew(); for (int i = 0; i < 1000000; i++) { GetMd5Hash("Hello World!"); } Console.WriteLine(stopwatch.ElapsedMilliseconds); } }
輸出結果:
可以看出我們的性能已經超官方 Demo 近一倍了。
接下來我們將進入進階級打開方式,我們現在需要自己寫一個簡單的 byte[] To string 方法,我們先打開 C# 項目的 “允許不安全代碼” 選項。
在解決方案中右鍵項目->屬性->生成->勾選“允許不安全代碼”。
然后我們在 HashAlgorithmHelper 類中定義新的 ToString 方法。
static string ToString(byte[] bytes) { unsafe { const int byte_len = 2; // 表示一個 byte 的字符長度。 var str = new string('\0', byte_len * bytes.Length); // 創建一個指定長度的空字符串。 fixed(char* pStr = str) { var pStr2 = pStr; // fixed pStr 是只讀的,所以我們定義一個變量。 foreach (var item in bytes) { *pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2; *pStr2 = Digitals[item & 15/* byte low */]; ++pStr2; } } return str; } }
然后我們修改 ComputeHash 方法為如下:
{
var bytes = Encoding.UTF8.GetBytes(input);
}
現在我們再測試就會發現已經比官方 Demo 快 4 倍了!現在這個 MD5 打開方式已經適合絕大多數人了,如果您不喜歡不安全代碼,也可以用數組代替,效率只差一丟丟而已,該方式我會在下方給出完整代碼。
接下來我們使用 .Net Core 以最優的方式打開,我們修改 HashAlgorithmHelper 為如下:(這里就不再支持 .Net Framework 了)
public static class HashAlgorithmHelper { static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
// 在這個函數里要用到的 bytes 成員 ReadOnlySpan<byte> 與 byte[] 的一致,所以我們只需要修改參數類型即可。 static string ToString(ReadOnlySpan<byte> bytes) { unsafe { const int byte_len = 2; // 表示一個 byte 的字符長度。 var str = new string('\0', byte_len * bytes.Length); fixed(char* pStr = str) { var pStr2 = pStr; // fixed pStr 是只讀的,所以我們定義一個變量。 foreach (var item in bytes) { *pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2; *pStr2 = Digitals[item & 15/* byte low */]; ++pStr2; } } return str; } } public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm { var instance = THashAlgorithmInstances<THashAlgorithm>.Instance; // 避免二次取值,微微提高效率(自我感覺)。 var encoding = Encoding.UTF8;
// 我們在這里聲明一個足量的 byte 數組,足以容下字符串的 utf-8 字節碼和 hash 值的字節碼。 var bytes = new byte[Encoding.UTF8.GetMaxByteCount(Math.Max(input.Length, instance.HashSize / 2))]; var bytesCount = encoding.GetBytes(input, bytes); var source = new ReadOnlySpan<byte>(bytes, 0, bytesCount); // source: utf-8 bytes region. var destination = new Span<byte>(bytes, bytesCount, bytes.Length - bytesCount); // destination: buffer region. if (bytes.Length - bytesCount > instance.HashSize && instance.TryComputeHash(source, destination, out var bytesWritten)) { return ToString(destination.Slice(0, bytesWritten)); } else {
// 通常情況下這里就很有可能拋出異常了,但是我們封裝工具方法必須有一個原則,我們盡量不要自行拋出異常。
// 用戶的參數執行到這里我們依然調用 HashAlgorithm.ComputeHash,由它內部拋出異常。這樣可以避免很多問題和歧義。 return ToString(instance.ComputeHash(bytes, 0, bytesCount)); } } }
我們再次測試,結果如下:
現在我們已經超官方示例達 5 倍了!這就是最終版本了。
最后附上各個版本實現的完整代碼:
Core 2.1+ 包含不安全代碼版本:
using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm { /// <summary> /// 線程靜態變量。 /// 即:這個變量在每個線程中都是唯一的。 /// 再結合泛型類實現了該變量在不同泛型或不同的線程先的變量都是唯一的。 /// 這樣做的目的是為了避開多線程問題。 /// </summary> [ThreadStatic] static THashAlgorithm instance; public static THashAlgorithm Instance => instance ?? Create(); // C# 語法糖,低版本可以改為 { get { return instance != null ? instance : Create(); } } /// <summary> /// 尋找 THashAlgorithm 類型下的 Create 靜態方法,並執行它。 /// 如果沒找到,則執行 Activator.CreateInstance 調用構造方法創建實例。 /// 如果 Activator.CreateInstance 方法執行失敗,它會拋出異常。 /// </summary> [MethodImpl(MethodImplOptions.NoInlining)] static THashAlgorithm Create() { var createMethod = typeof(THashAlgorithm).GetMethod( nameof(HashAlgorithm.Create), // 這段代碼同 "Create",低版本 C# 可以替換掉 BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, Type.DefaultBinder, Type.EmptyTypes, null); if (createMethod != null) { instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { }); } else { instance = Activator.CreateInstance<THashAlgorithm>(); } return instance; } } public static class HashAlgorithmHelper { static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; static string ToString(ReadOnlySpan<byte> bytes) { unsafe { const int byte_len = 2; // 表示一個 byte 的字符長度。 var str = new string('\0', byte_len * bytes.Length); fixed(char* pStr = str) { var pStr2 = pStr; // fixed pStr 是只讀的,所以我們定義一個變量。 foreach (var item in bytes) { *pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2; *pStr2 = Digitals[item & 15/* byte low */]; ++pStr2; } } return str; } } public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm { var instance = THashAlgorithmInstances<THashAlgorithm>.Instance; var encoding = Encoding.UTF8; var bytes = new byte[Encoding.UTF8.GetMaxByteCount(Math.Max(input.Length, instance.HashSize / 2))]; var bytesCount = encoding.GetBytes(input, bytes); var source = new ReadOnlySpan<byte>(bytes, 0, bytesCount); // source: utf-8 bytes region. var destination = new Span<byte>(bytes, bytesCount, bytes.Length - bytesCount); // destination: buffer region. if (destination.Length > 0 && instance.TryComputeHash(source, destination, out var bytesWritten)) { return ToString(destination.Slice(0, bytesWritten)); } else { return ToString(instance.ComputeHash(bytes, 0, bytesCount)); } } } class Program { public static string GetMd5Hash(string input) { using (MD5 md5Hash = MD5.Create()) { // Convert the input string to a byte array and compute the hash. byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input)); // Create a new Stringbuilder to collect the bytes // and create a string. StringBuilder sBuilder = new StringBuilder(); // Loop through each byte of the hashed data // and format each one as a hexadecimal string. for (int i = 0; i < data.Length; i++) { sBuilder.Append(data[i].ToString("x2")); } // Return the hexadecimal string. return sBuilder.ToString(); } } static void Main(string[] args) { Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!")); Console.WriteLine(GetMd5Hash("Hello World!")); while (true) { var stopwatch = Stopwatch.StartNew(); for (int i = 0; i < 1000000; i++) { HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"); } Console.WriteLine(stopwatch.ElapsedMilliseconds); stopwatch = Stopwatch.StartNew(); for (int i = 0; i < 1000000; i++) { GetMd5Hash("Hello World!"); } Console.WriteLine(stopwatch.ElapsedMilliseconds); } } }
包含不安全代碼的通用版本:
using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm { /// <summary> /// 線程靜態變量。 /// 即:這個變量在每個線程中都是唯一的。 /// 再結合泛型類實現了該變量在不同泛型或不同的線程先的變量都是唯一的。 /// 這樣做的目的是為了避開多線程問題。 /// </summary> [ThreadStatic] static THashAlgorithm instance; public static THashAlgorithm Instance => instance ?? Create(); // C# 語法糖,低版本可以改為 { get { return instance != null ? instance : Create(); } } /// <summary> /// 尋找 THashAlgorithm 類型下的 Create 靜態方法,並執行它。 /// 如果沒找到,則執行 Activator.CreateInstance 調用構造方法創建實例。 /// 如果 Activator.CreateInstance 方法執行失敗,它會拋出異常。 /// </summary> [MethodImpl(MethodImplOptions.NoInlining)] static THashAlgorithm Create() { var createMethod = typeof(THashAlgorithm).GetMethod( nameof(HashAlgorithm.Create), // 這段代碼同 "Create",低版本 C# 可以替換掉 BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, Type.DefaultBinder, Type.EmptyTypes, null); if (createMethod != null) { instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { }); } else { instance = Activator.CreateInstance<THashAlgorithm>(); } return instance; } } public static class HashAlgorithmHelper { static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; static string ToString(byte[] bytes) { unsafe { const int byte_len = 2; // 表示一個 byte 的字符長度。 var str = new string('\0', byte_len * bytes.Length); fixed(char* pStr = str) { var pStr2 = pStr; // fixed pStr 是只讀的,所以我們定義一個變量。 foreach (var item in bytes) { *pStr2 = Digitals[item >> 4/* byte high */]; ++pStr2; *pStr2 = Digitals[item & 15/* byte low */]; ++pStr2; } } return str; } } public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm { var bytes = Encoding.UTF8.GetBytes(input); return ToString(THashAlgorithmInstances<THashAlgorithm>.Instance.ComputeHash(bytes)); } } class Program { public static string GetMd5Hash(string input) { using (MD5 md5Hash = MD5.Create()) { // Convert the input string to a byte array and compute the hash. byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input)); // Create a new Stringbuilder to collect the bytes // and create a string. StringBuilder sBuilder = new StringBuilder(); // Loop through each byte of the hashed data // and format each one as a hexadecimal string. for (int i = 0; i < data.Length; i++) { sBuilder.Append(data[i].ToString("x2")); } // Return the hexadecimal string. return sBuilder.ToString(); } } static void Main(string[] args) { Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!")); Console.WriteLine(GetMd5Hash("Hello World!")); while (true) { var stopwatch = Stopwatch.StartNew(); for (int i = 0; i < 1000000; i++) { HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"); } Console.WriteLine(stopwatch.ElapsedMilliseconds); stopwatch = Stopwatch.StartNew(); for (int i = 0; i < 1000000; i++) { GetMd5Hash("Hello World!"); } Console.WriteLine(stopwatch.ElapsedMilliseconds); } } }
using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; static class THashAlgorithmInstances<THashAlgorithm> where THashAlgorithm : HashAlgorithm { /// <summary> /// 線程靜態變量。 /// 即:這個變量在每個線程中都是唯一的。 /// 再結合泛型類實現了該變量在不同泛型或不同的線程先的變量都是唯一的。 /// 這樣做的目的是為了避開多線程問題。 /// </summary> [ThreadStatic] static THashAlgorithm instance; public static THashAlgorithm Instance => instance ?? Create(); // C# 語法糖,低版本可以改為 { get { return instance != null ? instance : Create(); } } /// <summary> /// 尋找 THashAlgorithm 類型下的 Create 靜態方法,並執行它。 /// 如果沒找到,則執行 Activator.CreateInstance 調用構造方法創建實例。 /// 如果 Activator.CreateInstance 方法執行失敗,它會拋出異常。 /// </summary> [MethodImpl(MethodImplOptions.NoInlining)] static THashAlgorithm Create() { var createMethod = typeof(THashAlgorithm).GetMethod( nameof(HashAlgorithm.Create), // 這段代碼同 "Create",低版本 C# 可以替換掉 BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, Type.DefaultBinder, Type.EmptyTypes, null); if (createMethod != null) { instance = (THashAlgorithm)createMethod.Invoke(null, new object[] { }); } else { instance = Activator.CreateInstance<THashAlgorithm>(); } return instance; } } public static class HashAlgorithmHelper { static readonly char[] Digitals = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; static string ToString(byte[] bytes) { const int byte_len = 2; // 表示一個 byte 的字符長度。 var chars = new char[byte_len * bytes.Length]; var index = 0; foreach (var item in bytes) { chars[index] = Digitals[item >> 4/* byte high */]; ++index; chars[index] = Digitals[item & 15/* byte low */]; ++index; } return new string(chars); } public static string ComputeHash<THashAlgorithm>(string input) where THashAlgorithm : HashAlgorithm { var bytes = Encoding.UTF8.GetBytes(input); return ToString(THashAlgorithmInstances<THashAlgorithm>.Instance.ComputeHash(bytes)); } } class Program { public static string GetMd5Hash(string input) { using (MD5 md5Hash = MD5.Create()) { // Convert the input string to a byte array and compute the hash. byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input)); // Create a new Stringbuilder to collect the bytes // and create a string. StringBuilder sBuilder = new StringBuilder(); // Loop through each byte of the hashed data // and format each one as a hexadecimal string. for (int i = 0; i < data.Length; i++) { sBuilder.Append(data[i].ToString("x2")); } // Return the hexadecimal string. return sBuilder.ToString(); } } static void Main(string[] args) { Console.WriteLine(HashAlgorithmHelper.ComputeHash<MD5>("Hello World!")); Console.WriteLine(GetMd5Hash("Hello World!")); while (true) { var stopwatch = Stopwatch.StartNew(); for (int i = 0; i < 1000000; i++) { HashAlgorithmHelper.ComputeHash<MD5>("Hello World!"); } Console.WriteLine(stopwatch.ElapsedMilliseconds); stopwatch = Stopwatch.StartNew(); for (int i = 0; i < 1000000; i++) { GetMd5Hash("Hello World!"); } Console.WriteLine(stopwatch.ElapsedMilliseconds); } } }
不包含不安全代碼通用版本的性能:(性能依然極佳,建議使用此版本)
注:測試結果僅來自個人電腦,不同平台或硬件可能會有差異!
感謝閱讀!