什麼是.NET的強型別字串(Strongly typed string)?

czwy發表於2024-11-29

.NET中,強型別字串(Strongly typed string)並不是一個官方的概念,是指使用特定的結構來表示某種型別字串資料的編碼實踐。類似於列舉,可以提供編譯時檢查型別,減少執行時錯誤,以及更好的可讀性和維護性。相比於列舉,具有更好的擴充套件性以及更強的約束性。

列舉

列舉提供了一種便捷的方法來使用相關常數集並將常數值與名稱相關聯,具有型別安全、可讀性高以及編譯時檢查等優點。但是列舉型別不能定義任何方法、屬性或事件,只能透過擴充套件方法功能模擬向列舉型別新增方法。
儘管列舉提供了編譯時檢查,但對輸入值的約束是有限的。例如,下面這個列舉有四個值,預設情況下是int型別。取值範圍為0 ~ 3。

public enum Roles {
    Author,
    Editor,
    Administrator,
    SalesRepresentative
}

然後,有一個方法接受這個列舉型別的引數:

public string DoSomething(Roles role) {
    return role.ToString();
}

許多開發人員可能不會檢查傳入值是否為實際有效的列舉值。任何int型別都可以轉換,可能出現下邊這種程式碼:

var result = myObject.DoSomething((Roles)10);

輸出的結果是 “10”,如果後續程式碼中有基於這個列舉的分支語句或者條件判斷,將產生錯誤的結果。對於這種情況,強型別字串是一個不錯的選擇。

強型別字串(Strongly typed string)

強型別字串要宣告成帶有字串建構函式的不可變值型別(struct),即要在該型別上用 readonly 修飾符,併為其實現 IEquatable<T> 介面。要覆寫強型別字串的 ToString() 方法,以返回隱式的字串值。並將已知的強型別字串透過靜態只讀屬性宣告到該型別上。
為了讓強型別字串在通用程式碼的語言結構上看起來更像字串或者列舉,需要為強型別字串覆寫相等運算子。
以下就是 .NET 原始碼中加密雜湊演算法的名稱強型別字串HashAlgorithmName的程式碼

using System.Diagnostics.CodeAnalysis;

namespace System.Security.Cryptography
{
    
    public readonly struct HashAlgorithmName : IEquatable<HashAlgorithmName>
    {
        public static HashAlgorithmName MD5 { get { return new HashAlgorithmName("MD5"); } }

        public static HashAlgorithmName SHA1 { get { return new HashAlgorithmName("SHA1"); } }

        public static HashAlgorithmName SHA256 { get { return new HashAlgorithmName("SHA256"); } }

        public static HashAlgorithmName SHA384 { get { return new HashAlgorithmName("SHA384"); } }

        public static HashAlgorithmName SHA512 { get { return new HashAlgorithmName("SHA512"); } }

        public static HashAlgorithmName SHA3_256 => new HashAlgorithmName("SHA3-256");

        public static HashAlgorithmName SHA3_384 => new HashAlgorithmName("SHA3-384");

        public static HashAlgorithmName SHA3_512 => new HashAlgorithmName("SHA3-512");

        private readonly string? _name;

        public HashAlgorithmName(string? name)
        {
            // Note: No validation because we have to deal with default(HashAlgorithmName) regardless.
            _name = name;
        }

        public string? Name
        {
            get { return _name; }
        }

        public override string ToString()
        {
            return _name ?? string.Empty;
        }

        public override bool Equals([NotNullWhen(true)] object? obj)
        {
            return obj is HashAlgorithmName && Equals((HashAlgorithmName)obj);
        }

        public bool Equals(HashAlgorithmName other)
        {
            // NOTE: intentionally ordinal and case sensitive, matches CNG.
            return _name == other._name;
        }

        public override int GetHashCode()
        {
            return _name == null ? 0 : _name.GetHashCode();
        }

        public static bool operator ==(HashAlgorithmName left, HashAlgorithmName right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(HashAlgorithmName left, HashAlgorithmName right)
        {
            return !(left == right);
        }

        //其他擴充套件功能
        public static bool TryFromOid(string oidValue, out HashAlgorithmName value)
        {
            ArgumentNullException.ThrowIfNull(oidValue);

            switch (oidValue)
            {
                case Oids.Md5:
                    value = MD5;
                    return true;
                case Oids.Sha1:
                    value = SHA1;
                    return true;
                case Oids.Sha256:
                    value = SHA256;
                    return true;
                case Oids.Sha384:
                    value = SHA384;
                    return true;
                case Oids.Sha512:
                    value = SHA512;
                    return true;
                case Oids.Sha3_256:
                    value = SHA3_256;
                    return true;
                case Oids.Sha3_384:
                    value = SHA3_384;
                    return true;
                case Oids.Sha3_512:
                    value = SHA3_512;
                    return true;
                default:
                    value = default;
                    return false;
            }
        }

        public static HashAlgorithmName FromOid(string oidValue)
        {
            if (TryFromOid(oidValue, out HashAlgorithmName value))
            {
                return value;
            }

            throw new CryptographicException(SR.Format(SR.Cryptography_InvalidHashAlgorithmOid, oidValue));
        }
    }
}

這段程式碼更好地約束了加密雜湊演算法名稱的輸入,同時還擴充套件了其他功能。但比列舉繁瑣不少。
根據《框架設計指南》建議:當基類支援一組固定的輸入引數,但是派生類需要支援更多的引數時,建議使用強型別字串;當僅由密封型別使用時,只需要使用預定義的值,列舉將是更好的選擇。
此外,列舉通常定義的是封閉的選項集,對於作業系統版本這種開放集合,也建議使用強型別字串。控制元件庫 HandyControl 中的 SystemVersionInfo正是這樣的例子。

參考

Enum Alternatives in C# | Blog
使用列舉類(而不是列舉型別) - .NET | Microsoft Learn

相關文章