强类型字符串

本文关键字:字符串 强类型 | 更新日期: 2023-09-27 18:16:43

设置

我有一个原型类TypedString<T>,它试图"强类型"(可疑的含义)某个类别的字符串。它使用类似于c#的奇特循环模板模式(CRTP)。

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的实现,从而引入了一个错误并产生无效的用户名字符串,那么该错误可能会无声地传播给该方法的调用者,并造成各种破坏。这个简洁的版本防止了这种情况的发生。

场景3

System.Uri是。net中一个类的例子,它只是包装了一个字符串,这个字符串有大量的格式化约束和辅助属性/方法来访问它的有用部分。因此,这是证明这种方法并非完全疯狂的一个证据。

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

是否有我可能错过的缺点?有没有一种方法可以让这件事以后反咬我一口呢?

强类型字符串

总体思路

我并不从根本上反对这种方法(知道/使用CRTP是值得称赞的,它可能非常有用)。这种方法允许将元数据包装在单个值周围,这是一件非常好的事情。它也是可扩展的;您可以在不破坏接口的情况下向类型添加额外的数据。

我不喜欢你当前的实现似乎严重依赖于基于异常的流。这可能非常适合某些事情或在真正特殊的情况下。然而,如果用户试图选择一个有效的用户名,他们可能会在这样做的过程中抛出几十个异常。

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

说到"分布":考虑一下将这些类型实现为WCF数据契约的一部分的含义。忽略数据契约通常应该公开简单dto这一事实,您还会遇到代理类的问题,代理类将维护您的类型的属性,而不是其实现。

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

<标题> "场景1"

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

再次强调,这是非常具体的情况。

<标题> "场景2

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

IEnumerable<Username> GetFriends(Username user) { }

我可以看到这个论点。我想到了一些事情:

  • 更好的方法名称:GetUserNamesOfFriends()
  • 单元/集成测试
  • 假设这些用户名在创建/修改时被验证。如果这是你自己的API,你为什么不相信它给你的东西呢?

旁注:当处理人员/用户时,不可变的ID可能更有用(人们喜欢更改用户名)。

<标题> "场景3"

系统。Uri是。net中一个类的例子,它所做的仅仅是包装具有大量格式约束的字符串用于访问有用部分的助手属性/方法。这是有一个证据表明,这种方法并非完全疯狂。

这里没有参数,BCL中有很多这样的例子。

<标题>最终想法h1> li>将一个值包装成一个更复杂的类型,这样它就可以用更丰富的元数据来描述/操作。
  • 将验证集中在一个地方是一件好事,但要确保你选择了正确的地方。
  • 当逻辑驻留在传递的类型中时,跨越序列化边界会带来挑战。
  • 如果您主要关注于信任输入,则可以使用一个简单的包装器类,让被调用方知道它正在接收已验证的数据。验证在哪里/如何发生并不重要。
  • ASP。Net MVC对字符串使用类似的范例。如果值为IMvcHtmlString,则将其视为可信值,不再对其进行编码。如果没有,则对其进行编码。

    您已经为可以从字符串解析的对象表示定义了一个基类。使基类中的所有成员都是虚的,除了看起来很好之外。以后可以考虑管理序列化、区分大小写等。

    这种对象表示用于基类库中,例如System。Uri:

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

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

    我能想到两个缺点:

    1)维护开发人员可能会感到惊讶。他们也可能只是决定使用CLR类型,然后你的代码库被分割成代码,在一些地方使用string username,在其他地方使用Username username

    2)你的代码可能会因为调用new Username(str)username.Value而变得混乱。这可能看起来不像现在,但第20次你输入username.StartsWith("a"),并不得不等待智能感知告诉你,有些东西是错误的,然后考虑它,然后改正为username.Value.StartsWith("a"),你可能会生气。

    我相信你真正想要的是Ada所说的"约束子类型",但我自己从未使用过Ada。在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>();
        }