有没有办法在C#中强类型化整数ID值?
我最近一直在玩Haskell,并且在应用于ID值时可以立即看到其强类型的优势,例如,您永远不会想要使用 PersonId 代替产品编号
有没有一种很好的方法来创建可用于表示给定类型的ID的 Id 类/结构?
我有以下想法,但不幸的是,在许多级别上都不合法。您不能拥有抽象结构,并且不会继承隐式/显式转换运算符。
public abstract struct Id
{
int _value;
public Id(int value)
{
_value = value;
}
// define implicit Id to int conversion operator:
public static implicit operator int(Id id)
{
return _value;
}
// define explicit int to Id conversion operator:
public static explicit operator Id(int value)
{
return new Id(value);
}
public bool Equals(object obj)
{
if(GetType() == obj.GetType())
{
Id other = (Id)obj;
return other._value == _value;
}
return false;
}
public int GetHashCode()
{
return _value.GetHashCode();
}
}
struct PersonId : Id { public PersonId(int value) : base(value) {} }
struct ProductId : Id { public ProductId(int value) : base(value) {} }
有没有有效的方法来执行类似的操作?我们怎样才能证明整数ID类型不会在大型应用程序中混淆?
答案 0 :(得分:19)
public interface IId { }
public struct Id<T>: IId {
private readonly int _value;
public Id(int value) {
this._value = value;
}
public static explicit operator int(Id<T> id) {
return id._value;
}
public static explicit operator Id<T>(int value) {
return new Id<T>(value);
}
}
public struct Person { } // Dummy type for person identifiers: Id<Person>
public struct Product { } // Dummy type for product identifiers: Id<Product>
现在您可以使用Id<Person>
和Id<Product>
类型。 Person
和Product
类型可以是结构或类。您甚至可以使用由id标识的实际类型,在这种情况下,您不需要任何虚拟类型。
public sealed class Person {
private readonly Id<Person> _id;
private readonly string _lastName;
private readonly string _firstName;
// rest of the implementation...
}
显式运算符重载允许在id类型和基础id值之间安全轻松地进行转换。使用旧接口时,您可能希望将转换为整数更改为隐式,甚至更好,以使用正确类型的版本重载旧接口。当传统接口来自第三方且无法直接更改或过载时,可以使用扩展方法。
public interface ILegacy {
public bool Remove(int user);
}
public static class LegacyExtensions {
public static bool Remove(this ILegacy @this, Id<Person> user) {
return @this.Remove((int)user);
}
}
修改:按照smartcaveman的建议添加了IId
界面。
编辑:在考虑了Alejandro的建议后,将两个运营商更改为显式,并添加了如何处理旧版界面的部分。
答案 1 :(得分:2)
根据我的经验,在这种情况下操作员重载通常不能很好地工作并且可能导致各种问题。此外,如果您为类型int
提供隐式强制转换运算符,您确定如下语句:
SomeCompany.ID = SomePerson.ID
仍会被编译器捕获为无效?或者编译器可能只使用您的转换运算符,从而通过...?
进行无效赋值不太优雅的解决方案涉及定义您自己的值对象类型(作为class
)并通过Value
属性访问实际ID:
sealed class PersonId
{
readonly int value;
public int Value { get { return value; } }
public PersonId(int value)
{
// (you might want to validate 'value' here.)
this.value = value;
}
// override these in order to get value type semantics:
public override bool Equals(object other) { … }
public override int GetHashCode() { … }
}
现在,无论何时编写person.Id
,如果您确实需要原始整数,都需要编写person.Id.Value
。如果你真的试图将原始整数值的访问权限减少到尽可能少的地方,那就更好了,例如将实体持久化到DB(或从中加载)的地方。
P.S。:在上面的代码中,我真的想让PersonId
成为struct
,因为它是一个值对象。但是,struct
有一个问题,它是一个由编译器自动提供的无参数构造函数。这意味着您的类型的用户可以绕过所有构造函数(可能会进行验证),并且您可能在构造之后立即得到无效对象。因此,尝试使PersonId
与struct
尽可能相似:声明sealed
,覆盖Equals
和GetHashCode
,并且不提供无参数构造函数。
答案 2 :(得分:1)
我知道答案迟到了(实际建议),但我想到了这个问题,并且想出了一个可能有用的想法。
基本上,我们的想法是为每种类型创建一个标识工厂,返回实际id实例的接口。
<ListBox x:Name="listBox" Grid.Column="0" ItemsSource="{Binding Path=someDict.Keys}"/>
<ComboBox x:Name="comboBox" Grid.Column="1" VerticalAlignment="Top"
ItemsSource="{Binding ElementName=comboBox, Path=SelectedItem}"/>
它有点工作,它不能用于ORM,除非它可以访问内部public interface IPersonId
{
int Value { get; }
}
public static class PersonId
{
public static IPersonId Create(int id)
{
return new Id(id);
}
internal struct Id : IPersonId
{
public Id(int value)
{
this.Value = value;
}
public int Value { get; }
/* Implement equality comparer here */
}
}
,所以除非这是一个问题,否则这个想法应该是安全的。
这是一个粗略的草稿,我自己没有彻底测试过,但到目前为止我得到了id的值类型,并且无参数struct
构造函数没有问题。
如果您愿意,解决方案也可以是通用的(来自其他海报的灵感),如下:
struct
然后你就可以public interface IId<T>
{
int Value { get; }
}
public static class Id
{
public static IId<T> Create<T>(int value)
{
return new IdImpl<T>(value);
}
internal struct IdImpl<T> : IId<T>
{
public IdImpl(int value)
{
this.Value = value;
}
public int Value { get; }
/* Implement equality comparer here */
}
}
。
就个人而言,我并不是这类仿制药的忠实粉丝,但我想这真的是一个品味问题。它实际上只是一种约束类型兼容性的方式,在我看来应该是明确的。 但是,优点是你只有一个地方可以进行相等比较,这很好而且很干。
它绝对不是完美的,在大多数(如果不是大多数)用例中,它将被完全过度设计,只是为了解决Id.Create<Person>(42)
s具有默认无参数构造函数的事实。
此外,由于显而易见的原因,无法对通用变体进行验证检查,并且基础id值被约束为struct
(当然可以更改)。
答案 3 :(得分:1)
您可以创建一个抽象基类,您可以从中派生强类型标识符对象。
public abstract class TypedGuid
{
protected TypedGuid(Guid value)
{
Value = value;
}
public Guid Value { get; }
}
然后你继承了:
public class ProductId : TypedGuid
{
public ProductId (Guid value) : base(value)
{ }
}
在抽象基类中,您还可以实现IEquatable
,IFormattable
和IComparable
接口。您还可以覆盖GetHashCode
和ToString
方法。
public abstract class TypedGuid
{
protected TypedGuid(Guid value)
{
Value = value;
}
public bool HasValue => !Value.Equals(Guid.Empty);
public Guid Value { get; }
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
}
您还可以在类型化对象上提供静态方法,以方便地创建具有空值的实例。
public class ProductId : TypedGuid
{
public static ProductId Empty => new ProductId(Guid.Empty);
public static ProductId New => new ProductId(Guid.NewGuid());
public ProductId(Guid value) : base(value)
{
}
}
您还可以添加隐式运算符以允许隐式转换,但这可能会破坏标识符强类型的稳健性和目的。
答案 4 :(得分:-2)
我不确定为什么你希望PersonID
成为一个类,PersonID将是类型为int
的人的属性。
Person.PersonID \\<<int
很久以前,我确实看到了这样一个漂亮的模式:
interface BusinessObject<TKey,TEntity> //TKey as in ID
{
TKey ID;
TEntity SearchById(TKey id);
}
所以你可以这样做:
class Person : BusinessObject<Person, int> {}
OR
class FileRecord : BusinessObject<FileRecord, Guid> {}
不管它是否有所帮助,但它只是浮现在脑海中。