强类型Guid作为通用结构

Tom*_*bes 39 c# generics struct guid

我已经在代码中制作了两次相同的bug,如下所示:

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);
Run Code Online (Sandbox Code Playgroud)

好的,我想防止这些错误,所以我创建了强类型的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();
    }
}
Run Code Online (Sandbox Code Playgroud)

还有另一个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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

你有一些想法如何使用struct而不重复代码?

Eri*_*ert 45

首先,这是一个非常好的主意.简要说一下:

我希望C#能够更容易地围绕整数,字符串,ID等创建廉价的类型包装器.作为程序员,我们非常"快乐"和"整体快乐"; 很多东西都表示为字符串和整数,可以在类型系统中跟踪更多信息; 我们不希望将客户名称分配给客户地址.前段时间我写了一系列关于在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();
    }
}
Run Code Online (Sandbox Code Playgroud)

现在你已经完成了.你有类型的Id<App>Id<Payment>,而不是AppIdPaymentId,但你仍然不能分配Id<App>Id<Payment>Guid.

此外,如果您喜欢使用AppId,PaymentId然后在文件的顶部,您可以说

using AppId = MyNamespace.Whatever.Id<MyNamespace.Whatever.App>
Run Code Online (Sandbox Code Playgroud)

等等.

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

第四,要注意default(Id<App>)仍然会给你一个"空guid"标识符,所以你试图阻止它实际上不起作用; 它仍然可以创建一个.这并不是一个好方法.

  • 一个有趣的小信息:在此代码中以"T"的方式使用的类型在函数的上下文中称为[*幻像类型*](https://wiki.haskell.org/Phantom_type)编程:它用作类型参数,但从不存在类型为"T"的值. (3认同)
  • @BurnsBA:如果你这样做会伤害,不要这样做!:-) (2认同)
  • 您认为转换为类型定义的 Guid 或公开 GUID 的公共只读属性有用吗?这对于需要“原始”Guid 的持久性框架的接口非常有用。 (2认同)
  • @dasblinkenlight:这似乎是一个合理的选择.也许明确转换到Guid和从Guid转换是有用的. (2认同)

nvo*_*igt 5

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

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

在Visual Studio中,您可以为此使用T4模板。您基本上只编写一次类,然后有一个模板,说“我希望该类用于App,Payment,Account ...”,Visual Studio会为您分别生成一个源代码文件。

这样,您只有一个来源(T4模板),如果您在类中发现错误,则可以在其中进行更改,并且该错误将传播到所有标识符,而您无需考虑更改所有标识符。