6

C#中没有对变体类型(又名标记联合、区分联合)的直接支持。但是,可以使用访问者模式,该模式可以通过双重调度进行区分,并保证在编译时处理所有情况。但是实施起来很繁琐。我想知道是否有更轻松的方法来获得:某种具有区分机制的变体,可以保证在 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;
        }
    }

}
4

5 回答 5

4

您是否正在寻找类似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,并在需要时将它们作为参数传递,正如上面的评论中所指出的那样。例如,您可以编写某种穷人的模式匹配,将此方法添加到 class 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);
}

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

于 2013-10-18T15:38:40.283 回答
2

No way. There is not a concept like using visitor pattern at compile time, because implementation of your visitor pattern runs at runtime through instantiating your classes with use of polymorphism, double-dispatching, on object instances at runtime. Double-dispatching can run only on real object instances at run time, it is not related with compile time. Additionally the "Discrimination Mechanism" must run on your objects and if you are talking about objects, you are at runtime..

于 2013-10-21T08:35:39.213 回答
1

我发现了几篇文章可能对您有所帮助:

在 C# 中:http ://siliconcoding.wordpress.com/2012/10/26/either_in_csharp/

歧视工会(一):http ://www.drdobbs.com/cpp/discriminated-unions-i/184403821

歧视工会(二):http ://www.drdobbs.com/cpp/discriminated-unions-ii/184403828

于 2013-10-20T17:22:50.260 回答
0

所以我最终使用了一堆代表而不是访问者界面。这是这里的一些人之前建议的一种方法的变体。显然,它为我节省了一节课,省去了手动关闭的麻烦,并且最终我必须比以前与访客打交道的次数少得多。只要正确实现 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;
            }
        );
    }
于 2013-10-22T16:19:16.777 回答
-1

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

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

使用系统;使用 System.Collections;

class MainApp { static void Main() { // 设置结构 ObjectStructure o = new ObjectStructure(); 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();
}

}

// "Visitor" 抽象类 Visitor { public abstract void VisitConcreteElementA(ConcreteElementAconcreteElementA); 公共抽象无效访问ConcreteElementB(ConcreteElementBconcreteElementB);}

// "ConcreteVisitor1" 类 ConcreteVisitor1 : Visitor { public override void VisitConcreteElementA( ConcreteElementA concreteElementA) { Console.WriteLine("{0} 被 {1} 访问", concreteElementA.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 : Visitor { public override void VisitConcreteElementA( ConcreteElementA concreteElementA) { Console.WriteLine("{0} 被 {1} 访问", concreteElementA.GetType().Name, this.GetType().Name) ; }

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

}

// "元素" 抽象类 Element { public abstract void Accept(Visitor visitor); }

// "ConcreteElementA" 类 ConcreteElementA : Element { public override void Accept(Visitor visitor) { visitor.VisitConcreteElementA(this); }

public void OperationA()
{
}

}

// "ConcreteElementB" 类 ConcreteElementB : Element { public override void Accept(Visitor visitor) { visitor.VisitConcreteElementB(this); }

public void OperationB()
{
}

}

// "ObjectStructure" 类 ObjectStructure { private ArrayList elements = 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);
  }
}

}

于 2013-10-24T12:27:23.070 回答