强类型整数

5
作为一个对业余项目进行思考的想法实验,我一直在考虑一种方式来确保不会出现这种微妙的错误/打印错误:
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!
}

这个 bug 很难被发现,因为没有编译时错误,并且你甚至在运行时也不一定会收到异常。你只会得到“意外的结果”。
为了简单地解决这个问题,我尝试使用空枚举定义。实际上是将用户 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!!
}

你觉得怎样?


1
为什么?你增加了相当大的复杂性,是为了防止拼写错误?我认为为了避免某人使用错误变量而编写代码有点过头了。 - cthom06
在第二次阅读您的问题后,我意识到您似乎想对arg-name进行限制。这是过度设计了。为什么有人会想要这样做呢?类型不应该完成这项工作吗? - this. __curious_geek
@thiscuriousgeek 类型并不能胜任这个工作,因为UserID和UseCaseID是同一类型(int)。这个实验的重点是看是否有一种轻量级的方式来防止意外地将错误的值传递给方法。例如,假设您的ASP.NET MVC控制器操作获取ViewModel的“UserID”属性并将其传递给“UseCaseService.CloseUseCase(int useCaseID)”方法。在这个例子中,服务类将关闭错误的用例。 - Neil Barnwell
4
如果我是你,我不会将我的方法设计成UseCaseService.CloseUseCase(int useCaseID),而是会将其命名为UseCaseService.CloseUseCase(UseCase useCaseObj),这样可以解决这个问题。 - this. __curious_geek
@thiscuriousgeek 是的,说得好。对于我这个人为的例子来说,那可能是我最终会做的事情。我想的是当你有主键值时,不想从数据存储中加载对象,只是因为你想将它传递给只需要ID的东西。 - Neil Barnwell
显示剩余4条评论
6个回答

6
如果您正在创建新类型来保存UserIdUseCaseId,您几乎可以轻松将它们制作成简单的类,并使用隐式转换运算符从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;
    }
}

那样,您就可以获得类型安全性,而不必在代码中添加大量的强制转换。

+1. 对我来说,这个解决方案通过使用比 OP 代码更有意义。虽然 OP 代码非常简短,但对于使用那些参数的类的用户来说会很困惑。 - Merlyn Morgan-Graham
@Niall C:你可以将其作为“TypedInteger”基类,并每次需要新的int类型时创建一个空的派生类。如果你愿意,你甚至可以将其作为模板,并为任何类提供此功能... - Merlyn Morgan-Graham
@jdv,Niall C:是的,我同意。如果你能让隐式转换反过来进行,我认为你会成功的。 - Merlyn Morgan-Graham
1
@this:我建议将其命名为ExplicitlyTyped<T>,并将运算符更改为public static implicit operator T(ExplicitlyTyped<T> value)。将UserIdUseCaseId作为派生类,只包含一个构造函数(用于转发到基类构造函数)。这将解决隐式转换调用问题(编译器接受MyMethod(5)),以及跨兼容值问题。 - Merlyn Morgan-Graham
1
@Merylyn:我同意你的观点。我只是在问题和讨论的背景下发表了评论。 - this. __curious_geek
显示剩余10条评论

2

我一直想做类似的事情,你的帖子促使我尝试以下操作:

 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,您就知道被调用者不会访问对象上的任何其他属性。


2
如果是 Haskell,我想要做到这一点,我可能会像这样做:
data UserId    = UserId    Int
data UseCaseId = UseCaseId Int

这样,函数将接受一个UserId而不是一个整数,创建UserId始终是显式的,例如:

doSomething (UserId 12) (UseCaseId 15)

这类似于 Niall C. 创建一个封装 Int 的类型的解决方案。不过,如果每个类型都要花费 10 行来实现,那就太麻烦了。


1
晚来一步,但 FWIW ... this project on codeplex 定义了几个“强类型”标量,如 Angle、Azimuth、Distance、Latitude、Longitude、Radian 等。实际上,每个标量都是一个结构体,具有单个标量成员和多个方法/属性/构造函数来“正确”操作它。除了这些是值类型而不是引用类型之外,它们与将每个标量作为类的区别并不大。虽然我没有使用过这个框架,但我可以看到可能使这些类型成为一等公民的价值。
不知道这最终是不是一个好主意,但似乎有用能够编写像这样的代码(类似于您的原始示例),确保类型安全性(和值安全性):
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似乎更容易“正确”使用。


1

我个人认为这是不必要的,说实话。
开发者需要正确实现逻辑,不能依靠编译时错误来检测此类错误。


即使编译器可以检查逻辑的这一部分,即使这是强类型的关键点,也几乎不可能。 接近 -1 。 - Joey Adams
如果你使用的是动态语言,这就有意义了。如果你已经摆脱了静态类型语言的头痛,为什么不让它为你捕捉错误呢? - kyoryu

0
我宁愿在我的方法中进行参数验证,并在出现错误条件时引发适当的异常。
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!
}

2
当然,但是如果UserID和UseCaseID都是int类型,如何验证传入的是UserID而不是UseCaseID呢?也就是说,你的IsValidUseCaseId()会是什么样子的呢? - Neil Barnwell
为什么不使用类型,在编译时捕获错误而不是运行时?这难道不是强类型语言的主要优势之一吗? - kyoryu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接