开发者

Implementing phantom types in C#

I'm looking to use "phantom types" to implement type-safe identifiers. There's a开发者_JS百科 question here about doing this in F#.

I'd like to do this in C#. How?

I've got a solution (which has problems), so I'll post it as a possible answer to see if anyone can improve it.


Why not make it a sealed class with its constructor private?

public sealed class Id<TDiscriminator>
{
    private Id() { }

    //some static methods
}


I've come up with the following:

struct Id<TDiscriminator>
{
    private readonly Guid _id;

    private Id(Guid id)
    {
        _id = id;
    }

    public Guid Value
    {
        get { return _id; }
    }

    public static Id<TDiscriminator> NewId()
    {
        return From(Guid.NewGuid());
    }

    public static Id<TDiscriminator> From(Guid id)
    {
        return new Id<TDiscriminator>(id);
    }

    public static readonly Id<TDiscriminator> Empty = From(Guid.Empty);

    // Equality operators ellided...
}

...which I can use as follows:

class Order { /* empty */ }
class Customer { /* empty */ }

void Foo()
{
    var orderId = Id<Order>.NewId();
    var customerId = Id<Customer>.NewId();

    // This doesn't compile. GOOD.
    bool same = (orderId == customerId);
}

I don't particularly want concrete classes for the discriminator, because I don't want anyone instantiating them.

I could get around that by using an interface or an abstract class. Unfortunately, these can still be derived from and instantiated.

C# won't let you use a static class as a type argument. I can't say that I'm totally happy with the answers to that question, because the answers basically say "just because".


How about?

public sealed class Order
{
    private Order() {}
}

public static sealed class Id<T>
{
    // ...
}

I think that's exactly what you say. No one (except some special cases) can construct it and no one can inherit from it.


Well, as far as I could understand, you would like to provide a mechanism for distinguishing different types by a custom identifier object. I think you are almost near a working solution. In .NET when having a generic class, each substitution of the generic argument (or each unique combination of the generic arguments, if more than one) creates a unique type in the runtime. In your code Id<Order> and Id<Customer> are two distinct types. The NewId() method returns an instance of Id<Order> for the orderId and Id<Customer> for the customerId variables. The two types do not implement the == operator and therefore cannot be compared. Moreover, such comparison would be difficult to implement, since you cannot determine all possible uses of the Id<TDsicriminator> - you cannot guess what type will the TDsicriminator be substituted with.

1

A fast and simple solution will be to do this:

class Order { /* skipped */ }
class Customer { /* skipped */ }

void Foo()
{
    var orderId = Id<Order>.NewId();
    var customerId = Id<Customer>.NewId();

    bool sameIds = (orderId.Value == customerId.Value); // true
    bool sameObjects = orderId.Equals(customerId); // false
}

Since the Value properties are both of the Guid type, comparison is possible.

2

If you need however, to implement the == operator, or some sort of equality comparisons for instances of Id<TDisciminator>, the approach will be different. What comes up to my mind is the following:

public abstract class IdBase
{
    public abstract Guid Value { get; protected set; }

    public static bool operator == (IdBase left, IdBase right)
    {
        return left.Value == right.Value;
    }
}

public sealed class Id<TDiscriminator> : IdBase
{
   // your implementation here, just remember the override keyword for the Value property
}

Many people would not recommend the second approach though, since different implementations of IdBase may happen to have the same Value property (if you used the constructor that passes an existing ID). For instance:

var guid = Guid.NewGuid();
var customerID = Id<Customer>.From(guid);
var orderID = Id<Order>.From(guid);

Here (customerID == orderID) will then return true which is probably not what you want.

Shortly, in such a case, two different types will count as equal, which is a big logical mistake, so I'd stick to the first approach.

If you need Id<Customer>.Value to always be different than Id<Order>.Value, because of the different generic arguments (Customer is different than Order), then the following approach will work:

public sealed class Id<in TDiscriminator>
{
    private static readonly Guid _idStatic = Guid.NewGuid();

    private Id()
    {
    }

    public Guid Value
    {
        get { return _idStatic; }
    }
}

Notice the in keyword used here. This is applicable for .NET 4.0 where generics can be covariant and ensures that your class uses contravariant generics. (see http://msdn.microsoft.com/en-us/library/dd469487.aspx). In the above code, the _idStatic field will have a unique value for every different type supplied as a generic argument.

I hope this info is helpful.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜