There is no direct support for variant types (aka tagged unions, discriminated unions) in C#. However one can go with a visitor pattern that enables discrimination via double-dispatching and guarantees that all cases are addressed at the compile time. However it's tedious to implement. I wonder if there is more effortless way to get: some sort of variants with a discrimination mechanism that guarantees that all cases of a union are addressed at the compile time in C#?
// This is a variant type. At each single time it can only hold one case (a value)
// from a predefined set of cases. All classes that implement this interface
// consitute the set of the valid cases of the variant. So at each time a variant can
// be an instance of one of the classes that implement this interface. In order to
// add a new case to the variant there must be another class that implements
// this interface.
public interface ISomeAnimal
{
// This method introduces the currently held case to whoever uses/processes
// the variant. By processing we mean that the case is turned into a resulting
// value represented by the generic type TResult.
TResult GetProcessed<TResult>(ISomeAnimalProcessor<TResult> processor);
}
// This is the awkward part, the visitor that is required every time we want to
// to process the variant. For each possible case this processor has a corresponding
// method that turns that case to a resulting value.
public interface ISomeAnimalProcessor<TResult>
{
TResult ProcessCat(Cat cat);
TResult ProcessFish(Fish fish);
}
// A case that represents a cat from the ISomeAnimal variant.
public class Cat : ISomeAnimal
{
public CatsHead Head { get; set; }
public CatsBody Body { get; set; }
public CatsTail Tail { get; set; }
public IEnumerable<CatsLeg> Legs { get; set; }
public TResult GetProcessed<TResult>(ISomeAnimalProcessor<TResult> processor)
{
// a processor has a method for each case of a variant, for this
// particular case (being a cat) we always pick the ProcessCat method
return processor.ProcessCat(this);
}
}
// A case that represents a fish from the ISomeAnimal variant.
public class Fish : ISomeAnimal
{
public FishHead Head { get; set; }
public FishBody Body { get; set; }
public FishTail Tail { get; set; }
public TResult GetProcessed<TResult>(ISomeAnimalProcessor<TResult> processor)
{
// a processor has a method for each case of a variant, for this
// particular case (being a fish) we always pick the ProcessCat method
return processor.ProcessFish(this);
}
}
public static class AnimalPainter
{
// Now, in order to process a variant, in this case we want to
// paint a picture of whatever animal it prepresents, we have to
// create a new implementation of ISomeAnimalProcessor interface
// and put the painting logic in it.
public static void AddAnimalToPicture(Picture picture, ISomeAnimal animal)
{
var animalToPictureAdder = new AnimalToPictureAdder(picture);
animal.GetProcessed(animalToPictureAdder);
}
// Making a new visitor every time you need to process a variant:
// 1. Requires a lot of typing.
// 2. Bloats the type system.
// 3. Makes the code harder to maintain.
// 4. Makes the code less readable.
private class AnimalToPictureAdder : ISomeAnimalProcessor<Nothing>
{
private Picture picture;
public AnimalToPictureAdder(Picture picture)
{
this.picture = picture;
}
public Nothing ProcessCat(Cat cat)
{
this.picture.AddBackground(new SomeHouse());
this.picture.Add(cat.Body);
this.picture.Add(cat.Head);
this.picture.Add(cat.Tail);
this.picture.AddAll(cat.Legs);
return Nothing.AtAll;
}
public Nothing ProcessFish(Fish fish)
{
this.picture.AddBackground(new SomeUnderwater());
this.picture.Add(fish.Body);
this.picture.Add(fish.Tail);
this.picture.Add(fish.Head);
return Nothing.AtAll;
}
}
}
Action<T>
and generics. I still don't understand what you're after. – Commercialize