作为一个业余爱好项目的思想实验,我一直在想一种方法来确保这种微妙的错误/拼写错误不会发生:
public void MyMethod(int useCaseId)
{
// Do something with the useCaseId
}
public void SomeOtherMethod()
{
int userId = 12;
int useCaseId = 15;
MyMethod(userId); // Ooops! Used the wrong value!
}
这个错误很难发现,因为没有编译时错误,而且你甚至不一定会在运行时遇到异常。你只会得到“意想不到的结果”。
为了以简单的方式解决这个问题,我尝试使用空枚举定义。有效地使用户 ID 成为一种数据类型(无需达到类或结构的程度):
public enum UseCaseId { // Empty… }
public enum UserId { // Empty… }
public void MyMethod(UseCaseId useCaseId)
{
// Do something with the useCaseId
}
public void SomeOtherMethod()
{
UserId userId = (UserId)12;
UseCaseId useCaseId = (UseCaseId)15;
MyMethod(userId); // Compile error!!
}
你觉得怎么样?
如果您要麻烦地创建新类型来保存
UserId
和 UseCaseId
,您几乎可以轻松地将它们制作成简单的类,并使用 int
中的隐式转换运算符来为您提供所需的语法:
public class UserId
{
public int Id { get; private set; }
public UserId(int id)
{
id_ = id;
}
public static implicit operator UserId(int id)
{
return new UserId(id);
}
public static void Main()
{
UserId id1 = new UserId(1);
UserId id2 = 2;
}
}
这样,您就可以获得类型安全性,而不必在代码中使用强制转换。
如果是 Haskell 并且我想这样做,我可能会这样做:
data UserId = UserId Int
data UseCaseId = UseCaseId Int
这样,函数将接受
UserId
而不是 Int,并且创建 UserId
始终是显式的,例如:
doSomething (UserId 12) (UseCaseId 15)
这类似于 Niall C. 的创建类型来包装 Int 的解决方案。然而,如果每个类型不需要 10 行来实现就好了。
我想做类似的事情有一段时间了,你的帖子促使我尝试以下操作:
public class Id<T> {
private readonly int _Id;
private Id(int id) {
_Id = id;
}
public static implicit operator int(Id<T> id) {
return id._Id;
}
public static implicit operator Id<T>(int id) {
return new Id<T>(id);
}
}
我可以按如下方式使用
public void MyMethod(Id<UseCase> useCaseId)
{
// Do something with the useCaseId
}
public void SomeOtherMethod()
{
Id<User> userId = 12;
Id<UseCase> useCaseId = 15;
MyMethod(userId); // Compile error!!
}
我认为传递这种类型的 Id 对象比传递整个域对象更好,因为它使调用者和被调用者之间的契约更加明确。如果您只传递 id,您就知道被调用者没有访问对象上的任何其他属性。
我个人觉得没必要说实话。
开发人员必须正确实现逻辑,不能依赖编译时错误来解决此类错误。
游戏迟到了,但是 FWIW ... codeplex 上的这个项目定义了几个“强类型”标量,如角度、方位角、距离、纬度、经度、弧度等。实际上,每个都是一个带有单个标量成员和多个方法/属性/构造函数来“正确”操作它。除了它们是值类型而不是引用类型这一事实之外,与将它们都作为一个类没有太大区别。虽然我没有使用过该框架,但我可以看到可能使这些类型成为一等公民的价值。
不知道这最终是否是一个好主意,但能够编写这样的代码(类似于您的原始示例)并确保类型安全(和值安全)似乎确实很有用:
Angle a = new Angle(45); //45 degrees
SomeObject o = new SomeObject();
o.Rotate(a); //Ensure that only Angle can be sent to Rotate
或
Angle a = (Angle)45.0;
或
Radians r = Math.PI/2.0;
Angle a = (Angle)r;
如果您的域有很多具有值语义的标量“类型”并且可能有许多这些类型的实例,那么这种模式似乎最有用。将每个模型建模为具有单个标量的结构可以提供非常紧凑的表示(与使每个模型成为完整的类相比)。虽然实现这种级别的抽象(而不是仅仅使用“裸”标量来表示域值)和离散性可能有点痛苦,但最终的 API 似乎更容易“正确”使用。
我宁愿验证 MyMethod 中的 argument 并提出 适当的例外情况 错误条件。
public void MyMethod(int useCaseId)
{
if(!IsValidUseCaseId(useCaseId))
{
throw new ArgumentException("Invalid useCaseId.");
}
// Do something with the useCaseId
}
public bool IsValidUseCaseId(int useCaseId)
{
//perform validation here, and return True or False based on your condition.
}
public void SomeOtherMethod()
{
int userId = 12;
int useCaseId = 15;
MyMethod(userId); // Ooops! Used the wrong value!
}