使用语义类型进行强类型检查。






4.93/5 (18投票s)
使用语义原生类型实现更强的类型。
引言
常用类型语言的一个缺点是丢失了语义信息。例如
string zipcode = "12565";
string state = "NY";
WhatAmI(zipcode);
WhatAmI(state);
void WhatAmI(string str)
{
Console.WriteLine("I have no idea what " + str + " is.");
}
这说明程序不知道zipcode
和state
的语义含义——它仅仅是程序员为了方便使用而添加的标签,希望用正确的值填充。
本文的目标是创建一种方法来实现不可变的语义类型,这些类型(通常)包装原生类型,提供更强的参数类型检查,并且以一种易于定义语义类型和易于使用语义类型的方式来实现。
源代码
实现语义类型很简单——只需复制和粘贴“**幕后**”部分中描述的接口和基类,然后开始实现你自己的具体语义类型。
使用语义类型的“更强类型”
下面描述的实现允许我们改写成这样
Zipcode z = Zipcode.SetValue("12565");
State s = State.SetValue("NY");
WhatAmI(z);
WhatAmI(s);
// Now we can pass a semantic type rather than "string"
static void WhatAmI(Zipcode z)
{
Console.WriteLine("Zipcode = " + z.Value);
}
static void WhatAmI(State z)
{
Console.WriteLine("State = " + z.Value);
}
现在,由于使用语义类型代替了原生类型,我们有了“更强”的类型检查。这种方法的另一个好处是语义类型是不可变的——每次调用SetValue
都会实例化一个新的语义实例。因此,在多线程应用程序中使用语义类型非常有利——换句话说,语义类型实现了类似于函数式编程的功能。当然,这种不可变性很容易被破坏,但这并不推荐!
幕后
语义类型声明背后的实现涉及几个接口和一个抽象基类。
/// <summary>
/// Topmost abstraction.
/// </summary>
public interface ISemanticType
{
}
public interface ISemanticType<T>
{
T Value { get; }
}
/// <summary>
/// Enforces a semantic type of type T with a setter.
/// </summary>
/// <typeparam name="T">The native type.</typeparam>
public abstract class SemanticType<T> : ISemanticType
{
public T virtual Value { get; protected set; }
}
/// <summary>
/// Abstract native semantic type. Implements the native type T and the setter/getter.
/// This abstraction implements an immutable native type due to the fact that the setter
/// always returns a new concrete instance.
/// </summary>
/// <typeparam name="R">The concrete instance.</typeparam>
/// <typeparam name="T">The native type backing the concrete instance.</typeparam>
public abstract class NativeSemanticType<R, T> : SemanticType<T>
where R : ISemanticType<T>, new()
{
public T Value { get { return val; } }
protected T val;
public static R SetValue(T val)
{
R ret = new R();
ret.Value = val;
return ret;
}
}
接口ISemanticType
只是在类型信息不可用时的一种方便之举。
接口ISemanticType<T>
是另一种方便之举——这允许我们在不知道语义类型的情况下传递语义类型的实例。换句话说,它允许我们通过传递非语义接口实例来打破本文的重点,但这有时是必要的。
抽象类SemanticType<T>
实现了一个不可变的Value
属性。我们需要一个protected
的setter,以便可以使用static
工厂方法实例化具体的语义类型,但我们不希望程序员在设置值后更改它。
抽象类NativeSemanticType<R, T>
是魔法发生的地方。
- 此类派生自
SemanticType<T>
,允许它访问基类的受保护Value
setter。 - 该类采用
R
,这是派生自NativeSemanticType<R, T>
的具体语义类型的泛型参数。这才是真正有趣的部分——一个类,它接受本身派生自该类的泛型类型。
关于最后一点,编译器对R
的类型非常挑剔。为了使
ret.Value = val;
能够工作,ret
(类型为R
)必须能够访问受保护的Value
setter。为此,R
必须是NativeSemanticType<R, T>
类型——它不能是(虽然看起来它应该是)SemanticType<T>
类型。
实现具体的语义类型
我们可以很容易地实现具体的语义类型。在前面使用的例子中,实现是
public class Zipcode : NativeSemanticType<Zipcode, string> { }
public class State : NativeSemanticType<State, string> { }
唯一需要注意的是,基类必须将具体类指定为泛型参数R
,以便基类的SetValue
函数知道要实例化什么类型。因为这是一个static
“factory
”方法,所以我们真的无法避免这种小尴尬(至少我还没找到如何避免它)。
第二个泛型参数是底层原生类型。当然,这实际上不必是原生类型——它也可以是任何其他类。
语义类型的额外好处
这里有一些考虑使用语义类型的理由
验证
关于具体语义类型的另一个巧妙之处在于,类型实现可以覆盖值setter并执行检查。例如
public class Zipcode : NativeSemanticType<Zipcode, string>
{
public override string Value
{
get
{
return base.Value;
}
protected set
{
if (value.Length != 5)
{
throw new ApplicationException("Zipcode must have length of 5.");
}
base.Value = value;
}
}
}
这
Zipcode fail = Zipcode.SetValue("123");
现在会抛出异常。
安全
通过使用上面说明的概念,您可以确保底层值是安全的,无论是加密的、哈希的,还是信用卡,信用卡数字都被屏蔽等。
语义计算/分布式语义计算
当然,如果您想全力以赴,语义类型也非常适合多线程和分布式计算,正如我在这里所写。
结论
虽然以语义方式进行编码似乎很奇怪,但您可能会发现这是一种有用的技术,可以提供更强的参数类型检查,尤其是在处理具有许多相同原生类型的函数调用或类属性时。在设置语义值期间还可以提供值检查和其他行为的能力是一个额外的优势。