web-dev-qa-db-ja.com

T:structおよびT:classの一般的な制約

次のケースを区別したいと思います。

  1. プレーンな値タイプ(例:int
  2. Null許容の値型(例_int?_)
  3. 参照型(例:string)-オプションで、これが上記の(1)または(2)にマッピングされるかどうかは気にしません

私は次のコードを思いつきました。これはケース(1)と(2)でうまく機能します:

_static void Foo<T>(T a) where T : struct { } // 1

static void Foo<T>(T? a) where T : struct { } // 2
_

ただし、このようなケース(3)を検出しようとすると、コンパイルされません。

_static void Foo<T>(T a) where T : class { } // 3
_

エラーメッセージはタイプ 'X'は、同じパラメータータイプで 'Foo'と呼ばれるメンバーを既に定義していますです。まあ、どういうわけか、私は_where T : struct_と_where T : class_を区別できません。

3番目の関数(3)を削除すると、次のコードもコンパイルされません。

_int x = 1;
int? y = 2;
string z = "a";

Foo (x); // OK, calls (1)
Foo (y); // OK, calls (2)
Foo (z); // error: the type 'string' must be a non-nullable value type ...
_

どうすればFoo(z)をコンパイルして、上記の関数の1つ(または、考えていない別の制約を持つ3つ目の関数)にマッピングできますか?

48
Pierre Arnaud

制約は署名の一部ではありませんが、パラメーターは含まれます。また、パラメーターの制約は、オーバーロードの解決中に強制されます。

それでは、パラメーターに制約を設定しましょう。 ugいですが、動作します。

class RequireStruct<T> where T : struct { }
class RequireClass<T> where T : class { }

static void Foo<T>(T a, RequireStruct<T> ignore = null) where T : struct { } // 1
static void Foo<T>(T? a) where T : struct { } // 2
static void Foo<T>(T a, RequireClass<T> ignore = null) where T : class { } // 3

(6年遅れたほうがいいですか?)

49
Alcaro

残念ながら、制約のみに基づいて呼び出すメソッドのタイプを区別することはできません。

そのため、代わりに別のクラスまたは別の名前でメソッドを定義する必要があります。

Marnix's answer についてのコメントに加えて、ちょっとしたリフレクションを使用することで目的を達成できます。

以下の例では、制約のないFoo<T>メソッドはリフレクションを使用して、適切な制約付きメソッド(FooWithStruct<T>またはFooWithClass<T>のいずれか)への呼び出しを排除します。パフォーマンス上の理由から、Foo<T>メソッドが呼び出されるたびに単純なリフレクションを使用するのではなく、厳密に型指定されたデリゲートを作成してキャッシュします。

int x = 42;
MyClass.Foo(x);    // displays "Non-Nullable Struct"

int? y = 123;
MyClass.Foo(y);    // displays "Nullable Struct"

string z = "Test";
MyClass.Foo(z);    // displays "Class"

// ...

public static class MyClass
{
    public static void Foo<T>(T? a) where T : struct
    {
        Console.WriteLine("Nullable Struct");
    }

    public static void Foo<T>(T a)
    {
        Type t = typeof(T);

        Delegate action;
        if (!FooDelegateCache.TryGetValue(t, out action))
        {
            MethodInfo mi = t.IsValueType ? FooWithStructInfo : FooWithClassInfo;
            action = Delegate.CreateDelegate(typeof(Action<T>), mi.MakeGenericMethod(t));
            FooDelegateCache.Add(t, action);
        }
        ((Action<T>)action)(a);
    }

    private static void FooWithStruct<T>(T a) where T : struct
    {
        Console.WriteLine("Non-Nullable Struct");
    }

    private static void FooWithClass<T>(T a) where T : class
    {
        Console.WriteLine("Class");
    }

    private static readonly MethodInfo FooWithStructInfo = typeof(MyClass).GetMethod("FooWithStruct", BindingFlags.NonPublic | BindingFlags.Static);
    private static readonly MethodInfo FooWithClassInfo = typeof(MyClass).GetMethod("FooWithClass", BindingFlags.NonPublic | BindingFlags.Static);
    private static readonly Dictionary<Type, Delegate> FooDelegateCache = new Dictionary<Type, Delegate>();
}

この例はスレッドセーフではないことに注意してください。スレッドセーフが必要な場合は、すべてのアクセスに何らかのロックをかける必要があります)キャッシュ辞書、または-.NET4をターゲットにできる場合 ConcurrentDictionary<K,V> を代わりに使用します。)

10
LukeH

最初のメソッドで構造体制約を削除します。値の型とクラスを区別する必要がある場合は、引数の型を使用して区別できます。

      static void Foo( T? a ) where T : struct
      {
         // nullable stuff here
      }

      static void Foo( T a )
      {
         if( a is ValueType )
         {
            // ValueType stuff here
         }
         else
         {
            // class stuff
         }
      }
5

LukeHへのコメントを増幅し、(オブジェクトインスタンスの型とは異なる)型パラメーターに基づいてさまざまなアクションを呼び出すためにReflectionを使用する必要がある場合に役立つパターンは、次のようなプライベートジェネリックスタティッククラスを作成することです(これ正確なコードはテストされていませんが、私は以前にこの種のことをしました):

静的クラスFooInvoker <T> 
 {
 public Action <Foo> theAction = configureAction; 
 void ActionForOneKindOfThing <TT>(TT param)where TT:thatKindOfThing、 T 
 {
 ... 
} 
 void ActionForAnotherKindOfThing <TT>(TT param)where TT:thatOtherKindOfThing、T 
 {
 。] ... 
} 
 void configureAction(T param)
 {
 ... Tの種類を決定し、 `theAction`を
 ...上記のメソッドのいずれか。 ... 
 theAction(param); 
} 
} 

TTがそのメソッドの制約に準拠していないときに、ActionForOneKindOfThing<TT>(TT param)のデリゲートを作成しようとすると、Reflectionは例外をスローすることに注意してください。システムは、デリゲートが作成されたときにTTの型を検証したため、さらに型チェックすることなくtheActionを安全に呼び出すことができます。また、外部コードが以下を行う場合にも注意してください。

 FooInvoker <T> .theAction(param); 

最初の呼び出しにのみReflectionが必要です。後続の呼び出しは、単にデリゲートを直接呼び出します。

2
supercat

ジェネリックパラメーターが不要で、コンパイル時にこれら3つのケースを区別したい場合は、次のコードを使用できます。

static void Foo(object a) { } // reference type
static void Foo<T>(T? a) where T : struct { } // nullable
static void Foo(ValueType a) { } // valuetype
1

ありがたいことに、この種のいじくりは、C#バージョン7.3以降では必要ありません。

C#7.3の新機能 を参照してください。あまり明確ではありませんが、オーバーロードの解決中に「where」引数をある程度使用するようになりました。

オーバーロード解決にあいまいなケースが少なくなりました

Visual Studioプロジェクトの C#バージョンの選択 も参照してください。

以下との衝突がまだ見られます

Foo(x);
...
static void Foo<T>(T a) where T : class { } // 3
static void Foo<T>(T a) where T : struct { } // 3

しかし、正しく解決します

Foo(x);
...
static void Foo<T>(T a, bool b = false) where T : class { } // 3
static void Foo<T>(T a) where T : struct { } // 3
0
Sprotty