【问题标题】:Is there a way to have variants in C# besides using the visitor pattern?除了使用访问者模式之外,还有没有办法在 C# 中使用变体?
【发布时间】:2013-10-14 01:40:02
【问题描述】:

在 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;
        }
    }

}

【问题讨论】:

  • 对我来说似乎毫无用处,考虑到许多高级 C# 功能,例如 Action&lt;T&gt; 和泛型。我还是不明白你在追求什么。
  • 我不明白“静态验证”在有区别的联合上下文中是什么意思,或者访问者模式与它有什么关系。当问题具体且与实际代码相关时,StackOverflow 效果最佳。
  • 如果您正在寻找一种能够简化访问者模式“实现”的编程结构,请使用委托。 Action 是最值得注意的,它允许您执行任何类型的“访问”操作。您的“可访问”类(数据结构)需要有一个接受 Action 或 Action 并调用实际执行“访问”的操作的方法
  • @EricLippert,访问者模式在正确实施时,可以保证考虑到变体类型的所有情况。通过说“静态验证”,我的意思是由编译器检查,而不是说动态调度不能保证整体性。
  • 你的观点是,我认为,如果 (1) 你有一组对象都是标记的联合类型,并且 (2) 你正确地编写了一个访问者模式基类来调度虚拟基于标签的方法,然后您可以创建一个自定义该调度的派生类。由于假设基类是正确的访问者,那么每个可能的标签都有一个虚拟方法。 (这会为您提供双重虚拟调度。)您的问题是,是否有一种更简单的方法可以在不编写访问者模式基类的情况下获得类似的保证?

标签: c# variant visitor-pattern


【解决方案1】:

您是否正在寻找类似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, U, V, W>
...

public class Variant<T, U, V>
...
public class Variant<T, U, V, W>
...

但我个人不喜欢这种方法,我宁愿将Visit 方法转换为 lambda,并在需要时将它们作为参数传递,正如上面 cmets 中所指出的那样。例如,您可以编写某种穷人的模式匹配,将此方法添加到类Variant&lt;T, U&gt;

    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);
}

但请注意,真正的模式匹配具有更多功能:防护、穷举检查、脆弱模式匹配检查等。

【讨论】:

    【解决方案2】:

    没办法。没有像在编译时使用访问者模式这样的概念,因为访问者模式的实现是在运行时通过在运行时在对象实例上使用多态性、双重调度来实例化您的类来运行的。双重调度只能在运行时运行在真实对象实例上,它与编译时无关。此外,“判别机制”必须在您的对象上运行,如果您在谈论对象,那么您就是在运行时..

    【讨论】:

      【解决方案3】:

      【讨论】:

        【解决方案4】:

        所以我最终使用了一堆委托而不是访问者界面。这是这里的一些人之前建议的一种方法的变体。显然,它为我节省了一节课,省去了手动关闭的麻烦,并且最终我必须比以前与访客打交道的次数少得多。只要正确实现 GetProcessed 方法,就可以保证详尽无遗(考虑所有情况)。唯一的问题是 C# 有“void”(缺少结果值)的东西,这是由表示没有值的名义类型 Nothing 来解决的。

        // This is a variant type. At each single time it can 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 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 any possible case the variant can hold to a processing
            // function that turns the value of that case into some result.
            // Using delegates instead of an interface saves us a lot of typing!
            TResult GetProcessed<TResult>(
                Func<Cat, TResult> processCat,
                Func<Fish, TResult> processFish
            );
        }
        
        // 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>(
                Func<Cat, TResult> processCat,
                Func<Fish, TResult> processFish
            ) {
                // for this particular case (being a cat) we pick the processCat delegate
                return 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>(
                Func<Cat, TResult> processCat,
                Func<Fish, TResult> processFish
            ) {
                // for this particular case (being a fish) we pick the processFish method
                return processFish(this);
            }
        }
        
        public static class AnimalPainter
        {
            // Now, in order to process a variant, in this case we stil want to
            // add an animal to a picture, we don't need a visitor anymore.
            // All the painting logic stays within the same method.
            // Which is:
            // 1. Much less typing.
            // 2. More readable.
            // 3. Easier to maintain.
            public static void AddAnimalToPicture(Picture picture, ISomeAnimal animal)
            {
                animal.GetProcessed<Nothing>(
                    cat =>
                    {
                        picture.AddBackground(new SomeHouse());
                        picture.Add(cat.Body);
                        picture.Add(cat.Head);
                        picture.Add(cat.Tail);
                        picture.AddAll(cat.Legs);
                        return Nothing.AtAll;
                    },
                    fish =>
                    {
                        picture.AddBackground(new SomeUnderwater());
                        picture.Add(fish.Body);
                        picture.Add(fish.Tail);
                        picture.Add(fish.Head);
                        return Nothing.AtAll;
                    }
                );
            }
        

        【讨论】:

          【解决方案5】:

          表示要对对象结构的元素执行的操作。 Visitor 允许您定义新操作,而无需更改其操作的元素的类。

          此结构代码演示了访问者模式,其中对象遍历对象结构并对该结构中的每个节点执行相同的操作。不同的访问者对象定义了不同的操作。

          使用系统; 使用 System.Collections;

          类 MainApp { 静态无效主要() { // 设置结构 对象结构 o = 新对象结构(); o.Attach(new ConcreteElementA()); o.Attach(new ConcreteElementB());

            // Create visitor objects 
            ConcreteVisitor1 v1 = new ConcreteVisitor1();
            ConcreteVisitor2 v2 = new ConcreteVisitor2();
          
            // Structure accepting visitors 
            o.Accept(v1);
            o.Accept(v2);
          
            // Wait for user 
            Console.Read();
          }
          

          }

          // "访客" 抽象类访客 { 公共抽象无效访问ConcreteElementA( 具体元素A 具体元素A); 公共抽象无效 VisitConcreteElementB( 具体元素B具体元素B); }

          // "ConcreteVisitor1" 类 ConcreteVisitor1 : 访客 { 公共覆盖无效 VisitConcreteElementA( 具体元素A 具体元素A) { Console.WriteLine("{1} 访问了{0}", 具体元素A.GetType().Name, this.GetType().Name); }

          public override void VisitConcreteElementB(
            ConcreteElementB concreteElementB)
          {
            Console.WriteLine("{0} visited by {1}",
              concreteElementB.GetType().Name, this.GetType().Name);
          }
          

          }

          // "ConcreteVisitor2" 类 ConcreteVisitor2 : 访客 { 公共覆盖无效 VisitConcreteElementA( 具体元素A 具体元素A) { Console.WriteLine("{1} 访问了{0}", 具体元素A.GetType().Name, this.GetType().Name); }

          public override void VisitConcreteElementB(
            ConcreteElementB concreteElementB)
          {
            Console.WriteLine("{0} visited by {1}",
              concreteElementB.GetType().Name, this.GetType().Name);
          }
          

          }

          // “元素” 抽象类元素 { 公共摘要无效接受(访问者访问者); }

          // "ConcreteElementA" 类 ConcreteElementA : 元素 { 公共覆盖无效接受(访问者访问者) { 访客.VisitConcreteElementA(this); }

          public void OperationA()
          {
          }
          

          }

          // "ConcreteElementB" 类 ConcreteElementB : 元素 { 公共覆盖无效接受(访问者访问者) { 访客.VisitConcreteElementB(this); }

          public void OperationB()
          {
          }
          

          }

          // "对象结构" 类 ObjectStructure { 私有 ArrayList 元素 = new ArrayList();

          public void Attach(Element element)
          {
            elements.Add(element);
          }
          
          public void Detach(Element element)
          {
            elements.Remove(element);
          }
          
          public void Accept(Visitor visitor)
          {
            foreach (Element e in elements)
            {
              e.Accept(visitor);
            }
          }
          

          }

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-03-05
            • 1970-01-01
            • 2018-12-19
            相关资源
            最近更新 更多