强类型的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();
}
}

这些结构几乎相同,存在大量代码重复。不是吗?

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

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

首先,这是一个非常好的主意。旁白:

我希望C#能更容易地创建围绕整数、字符串、id等的廉价类型包装器。作为程序员,我们非常"字符串快乐"one_answers"整数快乐";很多东西被表示为字符串和整数,它们可以在类型系统中跟踪更多的信息;我们不想将客户名称分配给客户地址。不久前,我写了一系列关于用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>)仍然给你一个"空guid"标识符,所以你试图阻止它实际上并不起作用;创建一个仍然是可能的。真的没有什么好办法解决这个问题。

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

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

在Visual Studio中,您可以使用T4模板进行此操作。你基本上只写一次你的类,然后有一个模板,你说"我想要这个类用于应用程序、支付、帐户…",Visual Studio会为每个类生成一个源代码文件。

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

这使用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) { }

这有很好的副作用。您可以将这些过载添加到:

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

然而,你不能有过载获取:

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

实现对称性。

最新更新