简体   繁体   中英

Type-safe discriminated unions in C#, or: How to limit the number of implementations of an interface?

First, sorry for the lengthy post. Basically, my question is this:

I'm trying to reproduce the following F# discriminated union type in C#:

type Relation =
     | LessThan of obj * obj
     | EqualTo of obj * obj
     | GreaterThan of obj * obj

Can anyone suggest a simpler interface-based solution than the following?


interface IRelation // concrete types represent ◊ in the expression "Subject ◊ Object"
{
    object Subject { get; }
    object Object  { get; }
}

struct LessThanRelation    : IRelation { … }
struct EqualToRelation     : IRelation { … }
struct GreaterThanRelation : IRelation { … }

All my algorithms recognise these three relation types, and these only, so I need to prevent any further implementations of IRelation by third parties (ie other assemblies).

Footnote: To some, it might occur that if I just got my interface and algorithms right in terms of object orientation / polymorphism, it shouldn't matter that an third-party implementation is injected into my algorithm methods, as long as the interface is implemented correctly. This is a valid critique. But let's just assume that for the moment that I'm favouring a more functional-programming style over strict object-orientation in this case.

My best idea so far is to declare all above types as internal (ie. they will never be seen directly by outsiders) and create a proxy type Relation , which will be the only visible type to third parties:

public struct Relation  // constructors etc. are omitted here for brevity's sake
{
    public RelationType Type { get { … /* concrete type of value -> enum value */ } }

    public Relation Subject  { get { return value.Subject; } }
    public Relation Object   { get { return value.Object;  } }

    internal readonly IRelation value;
}

public enum RelationType
{
    LessThan,
    EqualTo,
    GreaterThan
}

All is well so far, but it gets more elaborate…

  • … if I expose factory methods for the concrete relation types:

     public Relation CreateLessThanRelation(…) { return new Relation { value = new LessThanRelation { … } }; } 
  • … whenever I expose an algorithm working on relation types, because I must map from/to the proxy type:

     public … ExposedAlgorithm(this IEnumerable<Relation> relations) { // forward unwrapped IRelation objects to an internal algorithm method: return InternalAlgorithm(from relation in relations select relation.value); } 

Limiting the interface implementations means it isn't really acting as an interface (which should accept any implementation (substitution), such as decorators) - so I can't recommend that.

Also, note that with a small exception of generics, treating a struct as an interface leads to boxing.

So that leaves one interesting case; an abstract class with a private constructor, and a known number of implementations as nested types , which means that they have access to the private constructor.

Now you control the subtypes, boxing isn't an issue (as it is a class), and there is less expectation of substitution.

I think your general approach is going in the right direction, but it looks like you can simplify your code using abstract classes:

public abstract class Relation
{
    internal Relation(object subject, object obj)
    {
        Subject = subject;
        Object = obj;
    }
    public object Subject { get; private set; }
    public object Object { get; private set; }
}

public sealed class LessThanRelation : Relation
{
    public LessThanRelation(object subject, object obj) : base(subject, obj) { }
}

public sealed class EqualToRelation : Relation
{
    public EqualToRelation(object subject, object obj) : base(subject, obj) { }
}

public sealed class GreaterThanRelation : Relation
{
    public GreaterThanRelation(object subject, object obj) : base(subject, obj) { }
}

Outside assemblies can see all the members of the Relation class except the internal constructor -- which, from the outside, makes it appear that the class has no constructors defined, so its not possible for third-party assemblies to define their own implementations.

I would go with the idea based on enum . In fact, I would use that solution in F# too. Since you always have only two arguments, you don't really need discriminated union:

// Note: with numbers assigned to cases, this becomes enum
type RelationType =      
  | LessThan = 1
  | EqualTo = 2
  | GreaterThan = 3

// Single-case union (could be record, depending on your style)
type Relation = 
  | BinaryRelation of RelationType * obj * obj

In general, if you want to encode discriminated union in C# then it is probably the best option to use abstract base class and then inherited class for each cases (with additional fields). Since you're not planing to extend it by adding new subclasses, you can define tag enum that lists all the possible subtypes (so that you can easily implement "pattern matching" by switch on the tag).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM