6

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 Variantに沿ったものをお探しですか? もしそうなら、C++ テンプレート言語と C# ジェネリックは多少異なるため、直接移植は可能ではないと思います。またboost::variant、ビジターパターンを使用しています。とにかく、必要に応じて、似たようなものを書くことができます。たとえば (このコードは概念実証にすぎないことに注意してください)、訪問者とバリアントに対して 2 つのジェネリック型を定義できます。

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明らかに、3 つ以上のパラメーターを持つバリアントを定義することもできます。

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、上記のコメントで指摘されているように、メソッドをラムダに変換し、必要に応じてパラメーターとして渡したいと考えています。たとえば、このメソッドを 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

とんでもない。コンパイル時にビジター パターンを使用するような概念はありません。ビジター パターンの実装は、実行時にオブジェクト インスタンスでポリモーフィズム、二重ディスパッチを使用してクラスをインスタンス化することによって実行されるためです。二重ディスパッチは、実行時に実際のオブジェクト インスタンスでのみ実行でき、コンパイル時間とは関係ありません。さらに、「識別メカニズム」はオブジェクトで実行する必要があり、オブジェクトについて話している場合は実行時に..

于 2013-10-21T08:35:39.213 に答える
1

あなたに役立つかもしれないいくつかの記事を見つけました:

C# のいずれか: http://siliconcoding.wordpress.com/2012/10/26/either_in_csharp/

差別組合 (I): http://www.drdobbs.com/cpp/discriminated-unions-i/184403821

差別組合 (II): 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 に答える