我有一个原型类TypedString<T>
,试图“强类型”(可疑意义)某个类别的字符串。它使用curiously recurring template pattern (CRTP)的C#-analogue。
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)。
string GetUserRootDirectory(Username user)
{
if (user == null)
throw new ArgumentNullException("user");
return Path.Combine(UsersDirectory, user.ToString());
}
我不必担心这里的用户字符串格式化 - 我已经知道它的类型是正确的。
IEnumerable<Username> GetFriends(Username user)
{
//...
}
在这里,调用者只知道基于类型返回的内容。 IEnumerable<string>
需要阅读方法或文档的详细信息。更糟糕的是,如果有人要更改GetFriends
的实现,以致它引入了一个错误并产生无效的用户名字符串,那么该错误可能会无声地传播给该方法的调用者并造成各种破坏。这个类型很好的版本可以防止这种情况。
System.Uri
是.NET中一个类的示例,它只包含一个具有大量格式约束的字符串和用于访问其有用部分的辅助属性/方法。所以这是一种证据表明这种方法并不完全疯狂。
我想这种事情以前已经完成了。我已经看到了这种方法的好处,不需要再说服自己了。
我可能缺少一个缺点吗?是否有办法可以在以后再次咬我?
答案 0 :(得分:5)
我并没有从根本上反对这种方法(并且知道/使用CRTP的荣誉,这可能非常有用)。该方法允许元数据包裹在单个值周围,这可能是一件非常好的事情。它也是可扩展的;您可以在不破坏接口的情况下向该类型添加其他数据。
我不喜欢你当前的实现似乎很大程度上依赖于基于异常的流程这一事实。这可能非常适合某些事情或真正特殊的情况。但是,如果用户试图选择有效的用户名,他们可能会在此过程中抛出数十个例外。
当然,您可以向界面添加无异常验证。您还必须问自己想要生活验证规则的位置(这始终是一项挑战,尤其是在分布式应用程序中)。
说到“分发”:考虑将这些类型作为WCF数据合同的一部分实施的含义。忽略数据契约通常应该暴露简单DTO的事实,你也有代理类的问题,它将维护你的类型的属性,而不是它的实现。
当然,您可以通过将父程序集放在客户端和服务器上来缓解这种情况。在某些情况下,这是完全合适的。在其他情况下,不那么重要。假设您的某个字符串的验证需要调用数据库。这很可能不适合在客户端/服务器位置。
听起来你正在寻求一致的格式化。这是一个有价值的目标,适用于URI和用户名之类的东西。对于更复杂的字符串,这可能是一个挑战。我已经研究过产品,即使是“简单”的字符串也可以根据上下文以多种不同的方式进行格式化。在这种情况下,专用(可能是可重复使用的)格式化程序可能更合适。
同样,非常具体情况。
更糟糕的是,如果有人要改变GetFriends的实现 这样它会引入一个bug并产生无效的用户名字符串, 该错误可能会无声地传播给方法的调用者并造成破坏 各种各样的破坏。
IEnumerable<Username> GetFriends(Username user) { }
我可以看到这个论点。我想到了一些事情:
GetUserNamesOfFriends()
附注:在与人/用户打交道时,不可变ID可能更有用(人们喜欢更改用户名)。
System.Uri是.NET中一个类的例子 包装具有大量格式约束的字符串 辅助属性/访问它的有用部分的方法。所以那是 有证据表明这种方法并不完全疯狂。
那里没有争论,BCL中有很多这样的例子。
ASP.Net MVC对字符串使用了类似的范例。如果值为IMvcHtmlString
,则将其视为受信任且未再次编码。如果没有,则进行编码。
答案 1 :(得分:2)
您已经为可以从字符串中解析的内容的对象表示定义了基类。使基类中的所有成员都是虚拟的,除了它看起来很好。您可以考虑稍后管理序列化,区分大小写等。
此类对象表示用于基类库,例如System.Uri:
Uri uri = new Uri("ftp://myUrl/%2E%2E/%2E%2E");
Console.WriteLine(uri.AbsoluteUri);
Console.WriteLine(uri.PathAndQuery);
使用这个基类,可以轻松实现对部件的轻松访问(如使用System.Uri),强类型成员,验证等。我看到的唯一缺点是c#中不允许多重继承,但你可能无论如何都不需要继承任何其他类。
答案 2 :(得分: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",但我自己从未使用过阿达。在C#中,你能做的最好的是一个包装器,这样不太方便。
答案 3 :(得分:0)
我会推荐另一种设计。
定义一个描述解析规则的简单接口(字符串语法):
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>();
}