提问人:Tomas Kubes 提问时间:12/13/2018 最后编辑:Tomas Kubes 更新时间:6/22/2022 访问量:2323
强类型 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();
}
}
这些结构几乎相同,但有很多重复的代码。不是吗?
除了使用类而不是结构之外,我无法想出任何优雅的方法来解决它。我宁愿使用struct,因为空检查,更少的内存占用,没有垃圾收集器开销等......
您知道如何在不复制代码的情况下使用结构吗?
答:
首先,这是一个非常好的主意。简单来说:
我希望 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>
AppId
PaymentId
Id<App>
Id<Payment>
Guid
另外,如果您喜欢使用 ,然后在文件顶部,您可以说AppId
PaymentId
using AppId = MyNamespace.Whatever.Id<MyNamespace.Whatever.App>
等等。
第三,您的类型可能需要更多功能;我认为这还没有完成。例如,您可能需要相等,以便您可以检查两个 ID 是否相同。
第四,请注意,这仍然会给你一个“空的 guid”标识符,所以你试图阻止它实际上不起作用;仍然可以创建一个。没有一个很好的方法可以解决这个问题。default(Id<App>)
评论
AppId
Id
AppId
Id<AppId>
我们做同样的事情,效果很好。
是的,这需要大量的复制和粘贴,但这正是代码生成的目的。
在 Visual Studio 中,可以使用 T4 模板来实现此目的。你基本上只写了一次你的类,然后有一个模板,你说“我想要这个类用于应用程序、支付、帐户,...Visual Studio 将为每个文件生成一个源代码文件。
这样一来,你就有了单一的源(T4 模板),如果你在类中发现一个错误,你可以在其中进行更改,它将传播到你的所有标识符,而你不必考虑更改所有标识符。
这有一个很好的副作用。您可以为添加设置以下重载:
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);
实现了对称性。
这很容易实现。record
public readonly record struct UserId(Guid Id)
{
public override string ToString() => Id.ToString();
public static implicit operator Guid(UserId userId) => userId.Id;
}
隐式运算符允许我们在适用的情况下将强类型用作常规。UserId
Guid
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) { }
评论