1

問題の説明

アイテムごとに異なる可能性がある一般的なFoo<T>要素のコレクションを保存しようとしています。また、任意のを受け入れることができるTような関数もあります。上記のリストの各要素でこの関数を呼び出すことができるはずです。これらはすべて関数の有効なパラメーターであるためですが、このアイデアを C# コンパイラーに表現することはできないようです。DoSomething<T>(Foo<T>)Foo<T>T

私が知る限り、問題は、C# ではFoo<T>binding なしで書くことができないため、そのようなリストを実際に表現できないことですT。私が望むのは、Java のワイルドカード メカニズム ( Foo<?>) のようなものです。このワイルドカード型が存在する疑似 C# では、次のようになります。

class Foo<T> {
    // ...
}

static class Functions {
    public static void DoSomething<T>(Foo<T> foo) {
        // ...
    }

    public static void DoSomething(List<Foo<?>> list) {
        foreach(Foo<?> item in list)
            DoSomething(item);
    }
}

このパターンは Java では有効ですが、C# で同じことを行うにはどうすればよいでしょうか? 以下の回答に投稿する解決策を見つけるために少し実験しましたが、もっと良い方法があるはずだと感じています。

注:私はすでにこの問題を実際のニーズに「十分に」解決しており、それを回避する方法(dynamicタイプを使用するなど)を知っていますが、放棄しないより簡単な解決策があるかどうかを本当に知りたいです静的タイプの安全性。

object以下で提案されているように、または非ジェネリックなスーパータイプを使用するだけでは、 Foo<T>. ただし、これについては何も知らなくても、これは理にかなっていTます。たとえば、 を使用して、ある場所からFoo<T>a を取得し、別の場所から a を取得して呼び出すと、コンパイラはすべての型が適切に機能することを認識します。List<T> listT valuelist.Add(value)

動機

なぜこのようなものが必要なのかと尋ねられたので、ほとんどの開発者の日常的な経験に少し近い例を作成しています。ユーザーが特定の型の値を操作できるようにする一連の UI コンポーネントを作成しているとします。

public interface IUiComponent<T> {
    T Value { get; set; }
}

public class TextBox : IUiComponent<string> {
    public string Value { get; set; }
}

public class DatePicker : IUiComponent<DateTime> {
    public DateTime Value { get; set; }
}

Value プロパティとは別に、コンポーネントにはもちろん他の多くのメンバー (OnChangeイベントなど) があります。

次に、元に戻すシステムを追加しましょう。このために UI 要素自体を変更する必要はありません。関連するすべてのデータに既にアクセスできるためです。OnChangeイベントを接続するだけで、ユーザーが UI コンポーネントを変更するたびに、それぞれの値が保存されますIUiComponent<T>(無駄ですが、物事をシンプルにしましょう)。値を保存するには、フォームでStack<T>for eachを使用IUiComponent<T>します。これらのリストには、IUiComponent<T>as キーを使用してアクセスします。リストの保存方法の詳細は省略します (これが重要だと思われる場合は、実装を提供します)。

public class UndoEnabledForm {
    public Stack<T> GetUndoStack<T>(IUiComponent<T> component) {
        // Implementation left as an exercise to the reader :P
    }

    // Undo for ONE element. Note that this works and is typesafe,
    // even though we don't know anything about T...
    private void Undo<T>(IUiComponent<T> component) {
        component.Value = GetHistory(component).Pop();
    }
    
    // ...but how do we implement undoing ALL components?
    // Using Pseudo-C# once more:
    public void Undo(List<IUiComponent<?>> components) {
        foreach(IUiComponent<?> component in components)
            Undo(component);
    }
}

Undo<T>()すべてのIUiComponents を (名前で)直接呼び出すことで、すべてを元に戻すことができます。

public void Undo(List<IUiComponent<?>> components) {
    Undo(m_TextBox);
    Undo(m_DatePicker);
    // ...
}

ただし、コンポーネントを追加/削除すると、コード内のもう 1 つの場所に触れる必要があるため、これは避けたいと思います。数十のフィールドと、すべてのコンポーネントで実行したい機能 (たとえば、すべての値をデータベースに書き込んで再度取得する) がある場合、これは多くの重複になる可能性があります。

サンプルコード

これは、ソリューションの開発/チェックに使用できる小さなコードです。タスクは、いくつかのPair<T>-objects をある種のコレクション オブジェクトに入れ、次に、このコレクション オブジェクトを受け入れてそれぞれのFirstandSecondフィールドを交換する関数を呼び出すことですPair<T>( を使用Application.Swap())。理想的には、キャストやリフレクションを使用しないでください。Pair<T>-class をまったく変更せずにそれを行うことができれば、ボーナスポイント:)

class Pair<T> {
    public T First, Second;

    public override string ToString() {
        return String.Format("({0},{1})", First, Second);
    }    
}

static class Application {
    static void Swap<T>(Pair<T> pair) {
        T temp = pair.First;
        pair.First = pair.Second;
        pair.Second = temp;
    }

    static void Main() {
        Pair<int> pair1 = new Pair<int> { First = 1, Second = 2 };
        Pair<string> pair2 = new Pair<string> { First = "first", Second = "second" };
        // imagine more pairs here

        // Silly solution
        Swap(pair1);
        Swap(pair2);

        // Check result
        Console.WriteLine(pair1);
        Console.WriteLine(pair2);
        Console.ReadLine();
    }
}
4

4 に答える 4

1

として呼び出したい関数を呼び出すインターフェイスを定義することをお勧めしますDoSomething<T>(T param)。最も単純な形式:

public interface IDoSomething
  { void DoSomething<T>(T param); }

次に基本型を定義しますElementThatCanDoSomething

abstract public class ElementThatCanDoSomething
  { abstract public void DoIt(IDoSomething action); }

および一般的な具象型:

public class ElementThatCanDoSomething><T>
{
  T data;
  ElementThatCanDoSomething(T dat) { data = dat; }

  override public void DoIt(IDoSomething action)
    { action.DoIt<T>(data); }
}

これで、任意の型のコンパイル時 T の要素を構築し、その要素をジェネリック メソッドに渡して、型を維持することが可能になりましたT(要素が null の場合や、要素が の派生物である場合でもT)。上記の正確な実装はそれほど有用ではありませんが、多くの便利な方法で簡単に拡張できます。たとえば、型Tがインターフェイスと具体的な型に一般的な制約を持っている場合、要素は、そのパラメーターの型にそれらの制約があるメソッドに渡すことができます (そうでなければ、リフレクションを使用しても非常に困難です)。パススルー パラメーターを受け入れることができるインターフェイスおよび呼び出し元メソッドのバージョンを追加することも役立つ場合があります。

public interface IDoSomething<TX1>
{ void DoSomething<T>(T param, ref TX1 xparam1); }

... and within the ElementThatCanToSomething

  abstract public void DoIt<TX1>(IDoSomething<TX1> action, ref TX1 xparam1);

... and within the ElementThatCanToSomething<T>

  override public void DoIt<TX1>(IDoSomething<TX1> action, ref TX1 xparam1)
    { action.DoIt<T>(data, ref xparam1); }

パターンは、任意の数のパススルー パラメータに簡単に拡張できます。

于 2012-12-14T23:03:10.943 に答える
1

EDIT 2:オーバーホールされた質問の場合、アプローチは基本的に以前に提案したものと同じです。ここで私はそれをあなたのシナリオに適応させ、それが機能する理由についてよりよくコメントしています(さらに、値の型に関する不幸な「落とし穴」...)

// note how IPair<T> is covariant with T (the "out" keyword)
public interface IPair<out T> {
     T First {get;}
     T Second {get;}
}

// I get no bonus points... I've had to touch Pair to add the interface
// note that you can't make classes covariant or contravariant, so I 
// could not just declare Pair<out T> but had to do it through the interface
public class Pair<T> : IPair<T> {
    public T First {get; set;}
    public T Second {get; set;}

    // overriding ToString is not strictly needed... 
    // it's just to "prettify" the output of Console.WriteLine
    public override string ToString() {
        return String.Format("({0},{1})", First, Second); 
    }    
}

public static class Application {
    // Swap now works with IPairs, but is fully generic, type safe
    // and contains no casts      
    public static IPair<T> Swap<T>(IPair<T> pair) {
        return new Pair<T>{First=pair.Second, Second=pair.First};       
    }

    // as IPair is immutable, it can only swapped in place by 
    // creating a new one and assigning it to a ref
    public static void SwapInPlace<T>(ref IPair<T> pair) {
        pair = new Pair<T>{First=pair.Second, Second=pair.First};
    }

    // now SwapAll works, but only with Array, not with List 
    // (my understanding is that while the Array's indexer returns
    // a reference to the actual element, List's indexer only returns
    // a copy of its value, so it can't be switched in place
    public static void SwapAll(IPair<object>[] pairs) {
        for(int i=0; i < pairs.Length; i++) {
           SwapInPlace(ref pairs[i]);
        }
    }
}

それは多かれ少なかれそれです...今、あなたmainはできる:

var pairs = new IPair<object>[] {
    new Pair<string>{First="a", Second="b"},
    new Pair<Uri> {
               First=new Uri("http://www.site1.com"), 
               Second=new Uri("http://www.site2.com")},     
    new Pair<object>{First=1, Second=2}     
};

Application.SwapAll(pairs);
foreach(var p in pairs) Console.WriteLine(p.ToString());

出力:

(b,a)
(http://www.site2.com/,http://www.site1.com/)
(2,1)

したがって、配列はPairs (まあ、IPairs) のみを含むことができるため、タイプ セーフです。唯一の落とし穴は、値の型です。ご覧のとおり、配列の最後の要素を、好きなようにではPair<object>なくとして宣言Pair<int>する必要がありました。これは、共分散/反分散が値の型で機能しないためintですobject

=========

編集1(古い、以下のコメントを理解するための参照としてそこに残しておいてください):コンテナに作用する必要があるときのために、両方の非ジェネリックマーカーインターフェースを持つことができます(ただし、「ラップされた」タイプは気にしません) と、型情報が必要な場合のための共変ジェネリック型。

何かのようなもの:

interface IFoo {}
interface IFoo<out T> : IFoo {
    T Value {get;}
}

class Foo<T> : IFoo<T> {
    readonly T _value;
    public Foo(T value) {this._value=value;}
    public T Value {get {return _value;}}
}

次の単純なクラス階層があるとします。

public class Person 
{
    public virtual string Name {get {return "anonymous";}}
}

public class Paolo : Person 
{
    public override string Name {get {return "Paolo";}}
}

任意の関数 (ラップ aIFooを気にしない場合) または具体的に( 気にする場合) のいずれかで機能する関数を使用できます。FooPersonIFoo<Person>

static class Functions 
{
    // this is where you would do DoSomethingWithContainer(IFoo<?> foo)
    // with hypothetical java-like wildcards 
    public static void DoSomethingWithContainer(IFoo foo) 
    {
        Console.WriteLine(foo.GetType().ToString());
    }

    public static void DoSomethingWithGenericContainer<T>(IFoo<T> el) 
    {
        Console.WriteLine(el.Value.GetType().ToString());
    }

    public static void DoSomethingWithContent(IFoo<Person> el) 
    {
        Console.WriteLine(el.Value.Name);
    }

}

次のように使用できます。

    // note that IFoo can be covariant, but Foo can't,
    // so we need a List<IFoo  
    var lst = new List<IFoo<Person>>
    {   
        new Foo<Person>(new Person()),
        new Foo<Paolo>(new Paolo())
    };


    foreach(var p in lst) Functions.DoSomethingWithContainer(p);    
    foreach(var p in lst) Functions.DoSomethingWithGenericContainer<Person>(p);
    foreach(var p in lst) Functions.DoSomethingWithContent(p);
// OUTPUT (LinqPad)
// UserQuery+Foo`1[UserQuery+Person]
// UserQuery+Foo`1[UserQuery+Paolo]
// UserQuery+Person
// UserQuery+Paolo
// anonymous
// Paolo

出力で注目すべき点の 1 つは、IFoo のみを受け取った関数でさえ、完全な型情報を保持して出力していたことです。これは、Java では型消去によって失われていたはずです。

于 2012-12-14T13:00:48.260 に答える
0

C# では、 のFoo基本型として使用するのリストを作成する必要があるようですFoo<T>。ただし、そこから簡単に戻ることはできませんFoo<T>

Foo私が見つけた解決策の 1 つは、 for each functionに抽象メソッドを追加し、を呼び出しSomeFn<T>(Foo<T>)て実装することです。ただし、これは、 で新しい (外部) 関数を定義するたびに、転送関数を に追加する必要があることを意味します。実際には、その関数について知る必要はありませんFoo<T>SomeFn(this)Foo<T>Foo

abstract class Foo {
    public abstract void DoSomething();
}

class Foo<T> : Foo {
    public override void DoSomething() {
        Functions.DoSomething(this);
    }
    // ...
}

static class Functions {
    public static void DoSomething<T>(Foo<T> foo) {
        // ...
    }

    public static void DoSomething(List<Foo> list) {
        foreach(Foo item in list)
            item.DoSomething();
    }
}

設計の観点から見たわずかにクリーンなソリューションは、上記のアプローチをある程度一般化し、特定の汎用関数との間の結合を切断するビジター パターンのようですFooが、それによって全体がさらに冗長で複雑になります。

interface IFooVisitor {
    void Visit<T>(Foo<T> foo);
}

class DoSomethingFooVisitor : IFooVisitor {
    public void Visit<T>(Foo<T> foo) {
        // ...
    }
}

abstract class Foo {
    public abstract void Accept(IFooVisitor foo);
}

class Foo<T> : Foo {
    public override void Accept(IFooVisitor foo) {
        foo.Visit(this);
    }
    // ...
}

static class Functions {
    public static void DoSomething(List<Foo> list) {
        IFooVisitor visitor = new DoSomethingFooVisitor();
        foreach (Foo item in list)
            item.Accept(visitor);
    }
}

ビジターを作成する方が簡単であれば、これはほとんど良い解決策です。C# は明らかに一般的なデリゲート/ラムダを許可していないため、ビジターをインラインで指定してクロージャーを利用することはできません。型はFoo、Visitor パターンを実装することによって、このスキームを明示的にサポートする必要もあります。

于 2012-12-14T12:43:56.030 に答える
0

これがまだ興味深いと思うかもしれない人のために、元のタイプにまったく触れないという「ボーナス要件」も満たす、私が思いつくことができる最善の解決策を次に示します。これは基本的に、Foo<T>コンテナに を直接格納するのではなく、 を呼び出すデリゲートを格納するIFooVisitorというひねりを加えた Visitor パターンFoo<T>です。T実際にはデリゲートの型の一部ではないため、それらのリストを簡単に作成できることに注目してください。

// The original type, unmodified
class Pair<T> {
    public T First, Second;
}

// Interface for any Action on a Pair<T>
interface IPairVisitor {
    void Visit<T>(Pair<T> pair);
}

class PairSwapVisitor : IPairVisitor {
    public void Visit<T>(Pair<T> pair) {
        Application.Swap(pair);
    }
}

class PairPrintVisitor : IPairVisitor {
    public void Visit<T>(Pair<T> pair) {
        Console.WriteLine("Pair<{0}>: ({1},{2})", typeof(T), pair.First, pair.Second);
    }
}

// General interface for a container that follows the Visitor pattern
interface IVisitableContainer<T> {
    void Accept(T visitor);
}

// The implementation of our Pair-Container
class VisitablePairList : IVisitableContainer<IPairVisitor> {
    private List<Action<IPairVisitor>> m_visitables = new List<Action<IPairVisitor>>();

    public void Add<T>(Pair<T> pair) {
        m_visitables.Add(visitor => visitor.Visit(pair));
    }

    public void Accept(IPairVisitor visitor) {
        foreach (Action<IPairVisitor> visitable in m_visitables)
            visitable(visitor);
    }
}

static class Application {
    public static void Swap<T>(Pair<T> pair) {
        T temp = pair.First;
        pair.First = pair.Second;
        pair.Second = temp;
    }

    static void Main() {
        VisitablePairList list = new VisitablePairList();
        list.Add(new Pair<int> { First = 1, Second = 2 });
        list.Add(new Pair<string> { First = "first", Second = "second" });

        list.Accept(new PairSwapVisitor());
        list.Accept(new PairPrintVisitor());
        Console.ReadLine();
    }
}

出力:

Pair<System.Int32>: (2,1)
Pair<System.String>: (second,first)
于 2012-12-18T17:32:07.527 に答える