在C#中没有直接支持
variant types(也称为标记的联合,有区别的联合).然而,可以使用
visitor pattern,通过双重调度实现歧视,并保证在编译时解决所有情况.然而,实施起来很繁琐.我想知道是否有更容易获得的方法:某种具有歧视机制的变体可以保证在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; } } }
解决方法
你在寻找
Boost Variants的东西吗?如果是这样,我认为不可能直接移植,因为C模板语言和C#泛型有些不同.此外,boost :: variant使用访问者模式.无论如何,如果你愿意,你可以写类似的东西.例如(请注意,此代码仅是概念证明),您可以为访问者和变体定义两种通用类型:
public interface VariantVisitor<T,U> { void Visit(T item); void Visit(U item); } public class Variant<T,U> { public T Item1 { get; private set; } private bool _item1Set; public U Item2 { get; private set; } private bool _item2Set; public Variant() { } public void Set(T item) { this.Item1 = item; _item1Set = true; _item2Set = false; } public void Set(U item) { this.Item2 = item; _item1Set = false; _item2Set = true; } public void ApplyVisitor(VariantVisitor<T,U> visitor) { if (_item1Set) { visitor.Visit(this.Item1); } else if (_item2Set) { visitor.Visit(this.Item2); } else { throw new InvalidOperationException("Variant not set"); } } }
你可以使用这样的类型:
private static object _result; internal class TimesTwoVisitor : VariantVisitor<int,string> { public void Visit(int item) { _result = item * 2; } public void Visit(string item) { _result = item + item; } } [Test] public void TestVisitVariant() { var visitor = new TimesTwoVisitor(); var v = new Variant<int,string>(); v.Set(10); v.ApplyVisitor(visitor); Assert.AreEqual(20,_result); v.Set("test"); v.ApplyVisitor(visitor); Assert.AreEqual("testtest",_result); var v2 = new Variant<double,DateTime>(); v2.Set(10.5); //v2.ApplyVisitor(visitor); // Argument 1: cannot convert from 'TestCS.TestVariant.TimesTwoVisitor' to 'TestCS.TestVariant.VariantVisitor<double,System.DateTime>' }
这样,编译器可以验证您是否将正确的访问者传递给正确的变体,并且VariantVisitor接口强制您为变体的所有类型实现Visit方法.显然,您还可以使用两个以上的参数定义变体:
public interface VariantVisitor<T,U,V> ... public interface VariantVisitor<T,V,W> ... public class Variant<T,V> ... public class Variant<T,W> ...
但我个人不喜欢这种方法,我宁愿将Visit方法转换为lambdas,并在需要时将它们作为参数传递,如上面的评论中所指出的那样.例如,您可以编写某种穷人的模式匹配,将此方法添加到类Variant< T,U>:
public R Match<R>(Func<T,R> f1,Func<U,R> f2) { if (_item1Set) { return f1(this.Item1); } else if (_item2Set) { return f2(this.Item2); } else { throw new InvalidOperationException("Variant not set"); } }
并像这样使用它:
[Test] public void TestMatch() { var v = new Variant<int,string>(); v.Set(10); var r1 = v.Match( i => i * 2,s => s.Length); Assert.AreEqual(20,r1); v.Set("test"); var r2 = v.Match( i => i.ToString(),s => s + s); Assert.AreEqual("testtest",r2); }
但请注意,真正的模式匹配具有更多功能:警卫,详尽检查,脆弱的模式匹配检查等.