作为通用结构的强类型 Guid

Strongly typed Guid as generic struct

我已经在代码中犯了两次相同的错误,如下所示:

void Foo(Guid appId, Guid accountId, Guid paymentId, Guid whateverId)
{
...
}

Guid appId = ....;
Guid accountId = ...;
Guid paymentId = ...;
Guid whateverId =....;

//BUG - parameters are swapped - but compiler compiles it
Foo(appId, paymentId, accountId, whateverId);

好的,我想防止这些错误,所以我创建了强类型的 GUID:

[ImmutableObject(true)]
public struct AppId
{
    private readonly Guid _value;

    public AppId(string value)
    {            
        var val = Guid.Parse(value);
        CheckValue(val);
        _value = val;
    }      

    public AppId(Guid value)
    {
        CheckValue(value);
        _value = value;           
    }

    private static void CheckValue(Guid value)
    {
        if(value == Guid.Empty)
            throw new ArgumentException("Guid value cannot be empty", nameof(value));
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

还有一个用于 PaymentId:

[ImmutableObject(true)]
public struct PaymentId
{
    private readonly Guid _value;

    public PaymentId(string value)
    {            
        var val = Guid.Parse(value);
        CheckValue(val);
        _value = val;
    }      

    public PaymentId(Guid value)
    {
        CheckValue(value);
        _value = value;           
    }

    private static void CheckValue(Guid value)
    {
        if(value == Guid.Empty)
            throw new ArgumentException("Guid value cannot be empty", nameof(value));
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

这些结构几乎相同,有很多重复代码。不是吗?

除了使用 class 而不是 struct 之外,我想不出任何优雅的方法来解决它。我宁愿使用 struct,因为 null 检查、更少的内存占用、没有垃圾收集器开销等...

您知道如何在不重复代码的情况下使用 struct 吗?

首先,这是一个非常好的主意。顺便说一句:

我希望 C# 可以更轻松地围绕整数、字符串、id 等创建便宜的类型化包装器。作为程序员,我们非常"string happy"和"integer happy";许多东西都表示为字符串和整数,它们可以在类型系统中跟踪更多信息;我们不想将客户名称分配给客户地址。前段时间我写了一系列关于在 OCaml 中编写虚拟机的博客文章(从未完成!),我做过的最好的事情之一是用表明其用途的类型包装虚拟机中的每个整数。这防止了很多错误! OCaml 使得创建小型包装器类型变得非常容易; C# 没有。

其次,我不会太担心重复代码。它主要是一个简单的复制粘贴,你不太可能编辑太多代码或犯错误。 花时间解决实际问题。复制粘贴一点代码没什么大不了的。

如果你确实想避免复制粘贴代码,那么我建议使用这样的泛型:

struct App {}
struct Payment {}

public struct Id<T>
{
    private readonly Guid _value;
    public Id(string value)
    {            
        var val = Guid.Parse(value);
        CheckValue(val);
        _value = val;
    }

    public Id(Guid value)
    {
        CheckValue(value);
        _value = value;           
    }

    private static void CheckValue(Guid value)
    {
        if(value == Guid.Empty)
            throw new ArgumentException("Guid value cannot be empty", nameof(value));
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

现在你已经完成了。您有类型 Id<App>Id<Payment> 而不是 AppIdPaymentId,但您仍然无法将 Id<App> 分配给 Id<Payment>Guid.

此外,如果您喜欢使用 AppIdPaymentId,那么您可以在文件顶部说

using AppId = MyNamespace.Whatever.Id<MyNamespace.Whatever.App>

等等。

第三,您的类型可能需要更多功能;我想这还没有完成。例如,您可能需要相等性,以便检查两个 ID 是否相同。

第四,请注意 default(Id<App>) 仍然给你一个 "empty guid" 标识符,所以你试图阻止它实际上不起作用;仍然可以创建一个。确实没有很好的解决方法。

您可以使用不同编程语言的子类化。

我们也这样做,效果很好。

是的,需要大量的复制和粘贴,但这正是代码生成的目的。

在 Visual Studio 中,您可以为此使用 T4 模板。你基本上写了一次 class 然后有一个模板,你说 "I want this class for App, Payment, Account,..." 和 Visual Studio 将为你生成一个源代码文件。

这样你就有了一个单一的来源(T4 模板),如果你在你的 classes 中发现一个错误,你可以在其中进行更改,它会传播到你所有的标识符,而无需考虑更改全部。

这有一个很好的副作用。您可以为添加添加这些重载:

void Add(Account account);
void Add(Payment payment);

但是您不能对 get 进行重载:

Account Get(Guid id);
Payment Get(Guid id);

我一直不喜欢这种不对称。你必须做:

Account GetAccount(Guid id);
Payment GetPayment(Guid id);

通过上述方法,这是可能的:

Account Get(Id<Account> id);
Payment Get(Id<Payment> id);

实现对称。