强类型 Guid 作为泛型结构

Strongly typed Guid as generic struct

提问人:Tomas Kubes 提问时间:12/13/2018 最后编辑:Tomas Kubes 更新时间:6/22/2022 访问量:2323

问:

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

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

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

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

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

C# 泛型 结构 GUID

评论

1赞 Bogdan Beda 12/13/2018
有趣的需求。我有一个问题。撇开代码重复不谈,当你想创建其中一个结构时,你必须用一些东西来初始化它们。你如何确保它们被正确的东西初始化?如..var appIdStruct=new AppId(paymentId) 不应编译
0赞 Tomas Kubes 12/13/2018
我不能保证。我无法检查一切。即便如此,错误的空间也会减少一些。
2赞 Bogdan Beda 12/13/2018
从这个角度来看,我认为 bug 的空间是一样的,你只是将源代码移动到不同的地方。此外,你正在使事情复杂化,从长远来看,这实际上会增加这个空间。你打算分配很多这样的吗?他们会在记忆中长寿吗?
0赞 Tomas Kubes 12/13/2018
不多,但 null 检查也是结构的好理由。
0赞 hazimdikenli 1/2/2019
你有没有考虑过创建一个像 FooParams 这样的 Params 类并将其传递给函数,然后如果你真的使用有意义的变量名称,错误赋值会很突出,并且很容易像 fooParams.PaymentId = accountId;

答:

44赞 Eric Lippert 12/13/2018 #1

首先,这是一个非常好的主意。简单来说:

我希望 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();
    }
}

现在你完成了。您有类型 和 而不是 和 ,但仍然无法将 to 或 赋值。Id<App>Id<Payment>AppIdPaymentIdId<App>Id<Payment>Guid

另外,如果您喜欢使用 ,然后在文件顶部,您可以说AppIdPaymentId

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

等等。

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

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

评论

2赞 Sergey Kalinichenko 12/13/2018
您认为为类型定义到 Guid 的转换,或者公开 Guid 的公共只读属性是否有用?这对于连接持久性框架很有用,其中需要“原始”Guid。
2赞 Eric Lippert 12/13/2018
@dasblinkenlight:这似乎是一个合理的选择。也许与 Guid 之间的显式转换会很有用。
3赞 Lii 12/17/2018
一个有趣的小信息:在函数式编程的上下文中,以此代码中的方式使用的类型称为幻像类型:它用作类型参数,但从未有过类型的值。TT
1赞 Lii 4/3/2019
@SolomonUcko:我认为在大多数情况下,这是一个更好的设计。也就是说,要有一个特定的类扩展。或者将这两种方法结合起来并进行扩展。AppIdIdAppIdId<AppId>
1赞 Eric Lippert 4/3/2019
@Lii:虽然这是一个合理的想法,但它的问题在于,你只能获得引用类型的扩展,这会导致收集压力和过多的内存分配。阅读原发帖人问题的最后一段;出于这些原因,他们明确表示他们想要一个基于结构的解决方案。
5赞 nvoigt 12/19/2018 #2

我们做同样的事情,效果很好。

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

在 Visual Studio 中,可以使用 T4 模板来实现此目的。你基本上只写了一次你的类,然后有一个模板,你说“我想要这个类用于应用程序、支付、帐户,...Visual Studio 将为每个文件生成一个源代码文件。

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

0赞 Michiel de Wolde 4/10/2021 #3

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

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

实现了对称性。

2赞 l33t 6/22/2022 #4

这很容易实现。record

public readonly record struct UserId(Guid Id)
{
    public override string ToString() => Id.ToString();
    public static implicit operator Guid(UserId userId) => userId.Id;
}

隐式运算符允许我们在适用的情况下将强类型用作常规。UserIdGuid

var id = Guid.NewGuid();
GuidTypeImportant(id);                 // ERROR
GuidTypeImportant(new UserId(id));     // OK
DontCareAboutGuidType(new UserId(id)); // OK
DontCareAboutGuidType(id);             // OK

void GuidTypeImportant(UserId id) { }
void DontCareAboutGuidType(Guid id) { }

评论

0赞 Bellarmine Head 4/14/2023
这无疑是自 C# 10 问世以来的正确答案。