强类型的Guid为通用结构

时间:2018-12-12 17:50:04

标签: c# generics struct guid

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

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,因为存在空检查,更少的内存占用空间,没有垃圾收集器开销等...

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

4 个答案:

答案 0 :(得分: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();
    }
}

现在您完成了。您有Id<App>Id<Payment>类型,而不是AppIdPaymentId,但是仍然无法为Id<App>或{{1}分配Id<Payment> }。

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

PaymentId

以此类推。

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

第四,请注意using AppId = MyNamespace.Whatever.Id<MyNamespace.Whatever.App> 仍然为您提供了一个“空guid”标识符,因此您尝试阻止该行为实际上是行不通的;仍然有可能创建一个。解决这个问题的方法真的不是很好。

答案 1 :(得分:5)

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

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

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

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

答案 2 :(得分:0)

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

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);

实现对称。

答案 3 :(得分:-9)

您也许可以使用具有其他编程语言的子类。