簡體   English   中英

強類型字符串

[英]Strongly Typed String

那個設定

我有一個原型類TypedString<T> ,試圖“強類型”(可疑意義)某個類別的字符串。 它使用了奇怪的重復模板模式(CRTP)的C# 模式

class TypedString<T>

public abstract class TypedString<T>
    : IComparable<T>
    , IEquatable<T>
    where T : TypedString<T>
{
    public string Value { get; private set; }

    protected virtual StringComparison ComparisonType
    {
        get { return StringComparison.Ordinal; }
    }

    protected TypedString(string value)
    {
        if (value == null)
            throw new ArgumentNullException("value");
        this.Value = Parse(value);
    }

    //May throw FormatException
    protected virtual string Parse(string value)
    {
        return value;
    }

    public int CompareTo(T other)
    {
        return string.Compare(this.Value, other.Value, ComparisonType);
    }

    public bool Equals(T other)
    {
        return string.Equals(this.Value, other.Value, ComparisonType);
    }

    public override bool Equals(object obj)
    {
        return obj is T && Equals(obj as T);
    }

    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }

    public override string ToString()
    {
        return Value;
    }
}

在整個項目中定義一堆不同的“字符串類別”時,現在可以使用TypedString<T>類來消除代碼重復。 此類的一個簡單用法示例是定義Username類:

class Username (示例)

public class Username : TypedString<Username>
{
    public Username(string value)
        : base(value)
    {
    }

    protected override string Parse(string value)
    {
        if (!value.Any())
            throw new FormatException("Username must contain at least one character.");
        if (!value.All(char.IsLetterOrDigit))
            throw new FormatException("Username may only contain letters and digits.");
        return value;
    }
}

現在,我可以在整個項目中使用Username類,而不必檢查用戶名是否格式正確 - 如果我有一個Username類型的表達式或變量,則保證它是正確的(或null)。

場景1

string GetUserRootDirectory(Username user)
{
    if (user == null)
        throw new ArgumentNullException("user");
    return Path.Combine(UsersDirectory, user.ToString());
}

我不必擔心這里的用戶字符串格式化 - 我已經知道它的類型性質是正確的。

情景2

IEnumerable<Username> GetFriends(Username user)
{
    //...
}

在這里,調用者只根據類型知道返回的內容。 IEnumerable<string>需要讀入方法或文檔的詳細信息。 更糟糕的是,如果有人要更改GetFriends的實現, GetFriends它引入了一個錯誤並產生無效的用戶名字符串,那么該錯誤可能會無聲地傳播給該方法的調用者並造成各種破壞。 這個類型很好的版本阻止了這一點。

場景3

System.Uri是.NET中一個類的一個例子,它只包含一個具有大量格式約束的字符串和輔助屬性/方法來訪問它的有用部分。 所以這是一種證據表明這種方法並不完全瘋狂。

問題

我想這種事情以前已經完成了。 我已經看到了這種方法的好處,不需要再說服自己了。

我可能會缺少一個缺點嗎?
有沒有辦法可以在以后再次咬我?

一般思考

我並沒有從根本上反對這種方法(並且知道/使用CRTP的榮譽,這可能非常有用)。 該方法允許元數據包裹在單個值周圍,這可能是一件非常好的事情。 它也是可擴展的; 您可以在不破壞接口的情況下向該類型添加其他數據。

我不喜歡你當前的實現似乎很大程度上依賴於基於異常的流程這一事實。 這可能非常適合某些事情或真正特殊的情況。 但是,如果用戶試圖選擇有效的用戶名,他們可能會在此過程中拋出數十個異常。

當然,您可以向界面添加無異常驗證。 您還必須問自己,您希望驗證規則存在於何處(這始終是一項挑戰,尤其是在分布式應用程序中)。

WCF

說到“分發”:考慮將這些類型作為WCF數據合同的一部分實施的含義。 忽略數據契約通常應該暴露簡單DTO的事實,你也有代理類的問題,它將維護你的類型的屬性,而不是它的實現。

當然,您可以通過將父程序集放在客戶端和服務器上來緩解這種情況。 在某些情況下,這是完全合適的。 在其他情況下,不那么重要。 假設您的某個字符串的驗證需要調用數據庫。 這很可能不適合在客戶端/服務器位置。

“場景1”

聽起來你正在尋求一致的格式化。 這是一個有價值的目標,適用於URI和用戶名之類的東西。 對於更復雜的字符串,這可能是一個挑戰。 我已經研究過產品,即使是“簡單”的字符串也可以根據上下文以多種不同的方式進行格式化。 在這種情況下,專用(可能是可重復使用的)格式化程序可能更合適。

同樣,非常具體情況。

“場景2”

更糟糕的是,如果有人要更改GetFriends的實現,以致它引入了一個錯誤並產生無效的用戶名字符串,那么該錯誤可能會無聲地傳播給該方法的調用者並造成各種破壞。

IEnumerable<Username> GetFriends(Username user) { }

我可以看到這個論點。 我想到了一些事情:

  • 一個更好的方法名稱: GetUserNamesOfFriends()
  • 單元/集成測試
  • 據推測,這些用戶名在創建/修改時會得到驗證。 如果這是您自己的API,為什么您不相信它給你的東西?

旁注:在處理人/用戶時,不可變ID可能更有用(人們喜歡更改用戶名)。

“情景3”

System.Uri是.NET中一個類的一個例子,它只包含一個具有大量格式約束的字符串和輔助屬性/方法來訪問它的有用部分。 所以這是一種證據表明這種方法並不完全瘋狂。

沒有爭論,BCL中有很多這樣的例子。

最后的想法

  • 將值包裝到更復雜的類型中沒有任何問題,因此可以使用更豐富的元數據來描述/操作它。
  • 在一個地方集中驗證是一件好事,但請確保選擇正確的地方。
  • 當邏輯駐留在傳遞的類型中時,跨越序列化邊界會帶來挑戰。
  • 如果您主要關注信任輸入,則可以使用一個簡單的包裝類,讓被調用者知道它正在接收已經過驗證的數據。 無論在何處/如何進行驗證都無關緊要。

ASP.Net MVC對字符串使用了類似的范例。 如果值為IMvcHtmlString ,則將其視為受信任且不再編碼。 如果沒有,則進行編碼。

以下是我能想到的兩個缺點:

1)維護開發人員可能會感到意外。 他們也可能只是決定使用CLR類型,然后你的代碼被分成使用代碼string username在一些地方和Username username在其他國家。

2)調用new Username(str)username.Value可能會使代碼混亂。 現在看起來似乎不太多,但是第20次輸入username.StartsWith("a")並且必須等待IntelliSense告訴你出現了問題,然后再考慮它然后將其更正為username.Value.StartsWith("a")你可能會生氣。

我相信你真正想要的是Ada所謂的“受限制的亞型” ,但我自己從未使用過Ada。 在C#中,你能做的最好的是一個包裝器,不太方便。

您已經為可以從字符串中解析的內容的對象表示定義了基類。 使基類中的所有成員都是虛擬的,除了它看起來很好。 您可以考慮稍后管理序列化,區分大小寫等。

這種對象表示在基類庫中使用,例如System.Uri

Uri uri = new Uri("ftp://myUrl/%2E%2E/%2E%2E");
Console.WriteLine(uri.AbsoluteUri);
Console.WriteLine(uri.PathAndQuery);

使用這個基類,可以很容易地實現對部件的輕松訪問(比如使用System.Uri),強類型成員,驗證等。我看到的唯一缺點是c#中不允許多重繼承,但你可能不需要無論如何繼承任何其他類。

我會推薦另一種設計。

定義描述解析規則的簡單接口(字符串語法):

internal interface IParseRule
{
    bool Parse(string input, out string errorMessage);
}

定義用戶名(以及您擁有的其他規則)的解析規則:

internal class UserName : IParseRule
{
    public bool Parse(string input, out string errorMessage)
    {
        // TODO: Do your checks here
        if (string.IsNullOrWhiteSpace(input))
        {
            errorMessage = "User name cannot be empty or consist of white space only.";
            return false;
        }
        else
        {
            errorMessage = null;
            return true;
        }
    }
}

然后添加一些使用該接口的擴展方法:

internal static class ParseRule
{
    public static bool IsValid<TRule>(this string input, bool throwError = false) where TRule : IParseRule, new()
    {
        string errorMessage;
        IParseRule rule = new TRule();

        if (rule.Parse(input, out errorMessage))
        {
            return true;
        }
        else if (throwError)
        {
            throw new FormatException(errorMessage);
        }
        else
        {
            return false;
        }
    }

    public static void CheckArg<TRule>(this string input, string paramName) where TRule : IParseRule, new()
    {
        string errorMessage;
        IParseRule rule = new TRule();

        if (!rule.Parse(input, out errorMessage))
        {
            throw new ArgumentException(errorMessage, paramName);
        }
    }

    [Conditional("DEBUG")]
    public static void DebugAssert<TRule>(this string input) where TRule : IParseRule, new()
    {
        string errorMessage;
        IParseRule rule = new TRule();
        Debug.Assert(rule.Parse(input, out errorMessage), "Malformed input: " + errorMessage);
    }
}

您現在可以編寫用於驗證字符串語法的干凈代碼:

    public void PublicApiMethod(string name)
    {
        name.CheckArg<UserName>("name");

        // TODO: Do stuff...
    }

    internal void InternalMethod(string name)
    {
        name.DebugAssert<UserName>();

        // TODO: Do stuff...
    }

    internal bool ValidateInput(string name, string email)
    {
        return name.IsValid<UserName>() && email.IsValid<Email>();
    }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM