【问题标题】:Strongly Typed String强类型字符串
【发布时间】:2013-05-30 05:40:54
【问题描述】:

设置

我有一个原型类TypedString<T>,它试图“强类型”(可疑的含义)某个类别的字符串。它使用 curiously recurring template pattern (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&lt;T&gt; 类现在可用于在整个项目中定义一堆不同的“字符串类别”时消除代码重复。该类的一个简单用法示例是定义一个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 类型的表达式或变量,这是保证 正确(或为空)。

场景 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&lt;string&gt; 需要阅读方法或文档的详细信息。更糟糕的是,如果有人要更改 GetFriends 的实现,从而引入错误并产生无效的用户名字符串,则该错误可能会默默地传播给方法的调用者并造成各种破坏。这个类型很好的版本可以防止这种情况发生。

场景 3

System.Uri 是 .NET 中的一个类示例,它只不过是包装了一个字符串,该字符串具有大量格式约束和用于访问有用部分的辅助属性/方法。所以这是一个证据,证明这种方法并不完全疯狂。

问题

我想这种事情以前已经做过了。我已经看到了这种方法的好处,不需要再说服自己了。

我可能会遗漏什么缺点吗?
以后有什么办法可以反过来咬我吗?

【问题讨论】:

  • 对我来说,这只是一个不同名称的面向对象编程。如果UserName 编写正确,TypedString&lt;UserName&gt;UserName 类在功能上可以相同——TypedString 只是为您提供了一个强制继承模式。
  • @TimothyShields:在一般意义上同意 Meters 示例。但是,如果您正在编写单位转换程序或科学软件(月球着陆器模块!?-:)),那么像 Meters 类之类的东西将非常有用和有保证。它会阻止某人在需要 Meters 的情况下使用 Miles 实例。或者更好的是,通过一个小的操作员重载它可以自动将英里转换为米。关键是,根据需要使用(或滥用)数据类型。
  • “我想这种事情以前已经做过了。我已经看到了这种方法的好处,不需要再说服自己了。” - 我没有看到在不同的“类”周围喷射字符串验证代码的好处,这些“类”除了验证字符串之外真的没有其他行为。如果我的任务是维护这种废话,我会很生气地发现这些字符串验证没有在序列化/反序列化边界处处理。
  • 我相信首选的命名法是“字符串类型”
  • @TimothyShields - 我没有投反对票。我不认为这是一个坏问题。我只是不认为这种方法有用。 YMMV。

标签: c# string design-patterns types


【解决方案1】:

一般想法

我从根本上并不反对这种方法(并且对了解/使用 CRTP 表示敬意,这可能非常有用)。该方法允许将元数据包装在单个值周围,这可能是一件非常好的事情。它也是可扩展的;您可以在不破坏接口的情况下向类型添加其他数据。

我不喜欢您当前的实现似乎严重依赖基于异常的流程这一事实。这可能非常适合某些事情或真正特殊的情况。但是,如果用户尝试选择有效的用户名,他们可能会在此过程中引发数十个异常。

当然,您可以向接口添加无异常验证。您还必须问自己希望验证规则在哪里存在(这始终是一个挑战,尤其是在分布式应用程序中)。

WCF

谈到“分发”:考虑将此类类型实现为 WCF 数据协定的一部分的含义。忽略数据协定通常应该公开简单 DTO 的事实,您还会遇到代理类的问题,它会维护您的类型的属性,而不是它的实现。

当然,您可以通过将父程序集放在客户端和服务器上来缓解这种情况。在某些情况下,这是完全合适的。在其他情况下,则更少。假设您的一个字符串的验证需要调用数据库。这很可能不适合在客户端/服务器位置都有。

“场景一”

听起来您正在寻求一致的格式。这是一个有价值的目标,并且非常适合 URI 和用户名之类的东西。对于更复杂的字符串,这可能是一个挑战。我曾开发过一些产品,即使是“简单”的字符串也可以根据上下文以多种不同的方式进行格式化。在这种情况下,专用(并且可能是可重复使用的)格式化程序可能更合适。

再次,非常具体的情况。

“场景二”

更糟糕的是,如果有人要更改 GetFriends 的实现 这样它会引入错误并产生无效的用户名字符串, 该错误可能会悄悄地传播给方法的调用者并造成 各种破坏。

IEnumerable<Username> GetFriends(Username user) { }

我可以看到这个论点。想到了几件事:

  • 更好的方法名:GetUserNamesOfFriends()
  • 单元/集成测试
  • 大概是这些用户名在创建/修改时经过验证。如果这是您自己的 API,您为什么不相信它为您提供的功能?

旁注:在与人/用户打交道时,不可变 ID 可能更有用(人们喜欢更改用户名)。

“场景三”

System.Uri 是 .NET 中的一个类示例,它的功能仅 包装一个具有大量格式约束的字符串,并且 用于访问有用部分的辅助属性/方法。所以那是 一个证据表明这种方法并不完全疯狂。

没有争论,BCL中有很多这样的例子。

最后的想法

  • 将值包装到更复杂的类型中并没有错,以便可以使用更丰富的元数据对其进行描述/操作。
  • 将验证集中在一个地方是一件好事,但请确保选择正确的地方。
  • 当逻辑位于所传递的类型中时,跨越序列化边界可能会带来挑战。
  • 如果您主要专注于信任输入,您可以使用一个简单的包装类,让被调用者知道它正在接收已验证的数据。验证发生的地点/方式无关紧要。

ASP.Net MVC 对字符串使用类似的范例。如果值为IMvcHtmlString,则将其视为受信任且不会再次编码。如果不是,则对其进行编码。

【讨论】:

  • 关于GetFriends 名称:它完全是为我的问题而发明的——签名比名称更重要——我的应用程序与“朋友”没有任何关系。哈哈 :) -- 关于“场景 2”并信任我自己的 API:如果您在任何地方使用string,只需一个人在一个位置忘记反序列化/解析“特殊”字符串以正确格式化/转换/规范化该字符串以使错误以静默方式渗入系统的其余部分。但是,Username 示例类型保证由于该类型的性质而不会发生。
  • 非常好的答案,它是荣誉复选标记的候选者。 ;)
  • 谢谢。只要您始终如一并与使用您的代码的其他人分享知识,我当然不会反对这种范式。我经常将值封装在各种包装器中。
【解决方案2】:

以下是我能想到的两个缺点:

1) 维护开发人员可能会感到意外。他们也可能只是决定使用 CLR 类型,然后您的代码库被拆分为在某些地方使用 string username 和在其他地方使用 Username username 的代码。

2) 您的代码可能会因调用new Username(str)username.Value 而变得混乱。现在看起来可能不多,但是第 20 次输入 username.StartsWith("a") 并且必须等待 IntelliSense 告诉您有问题,然后考虑它,然后将其更正为 username.Value.StartsWith("a"),您可能会生气。

我相信你真正想要的是Ada calls "constrained subtypes",但我自己从未使用过 Ada。在 C# 中,你能做的最好的就是一个包装器,这不太方便。

【讨论】:

    【解决方案3】:

    您已经为可以从字符串中解析出来的东西的对象表示定义了一个基类。将基类中的所有成员设为虚拟,除此之外它看起来还不错。您可以考虑稍后管理序列化、区分大小写等。

    在基类库中使用这样的对象表示,例如System.Uri

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

    使用这个基类很容易实现轻松访问部件(如 System.Uri)、强类型成员、验证等。我看到的唯一缺点是在 c# 中不允许多重继承,但您可能反正不需要继承任何其他类。

    【讨论】:

    • 您的System.Uri 类示例非常有说服力,并且(如果它还没有在.NET 中!)它实际上将是这个TypedString&lt;T&gt; 应用程序的一个很好的示例。另外,将基础的所有方法都设为虚拟是个好点。
    • IHtmlString 是另一个很好的 BCL 示例,因为它只不过是一个已知为 HTML 编码的字符串。
    【解决方案4】:

    我会推荐另一种设计。

    定义一个描述解析规则的简单接口(字符串语法):

    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>();
        }
    

    【讨论】:

    • 这与我的问题中的“场景 2”有什么关系?我所说的“保证”似乎在这里丢失了? - 这种方法还要求将这些“强类型”字符串中的任何一个作为输入的每个方法都记住每个字符串都有一个CheckArg/DebugAssert
    • 这个想法是在参数进入您的 API 时检查它们。在内部,您使用调试条件代码断言您的规则得到满足。通过这种方式,您可以最大限度地减少解析需求,获得干净的代码,并且您会尽早发现错误,因为它们会导致调试断言失败。
    猜你喜欢
    • 2011-10-06
    • 2011-01-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-06
    • 2017-08-25
    • 2016-01-05
    相关资源
    最近更新 更多