Общие ограничения, где T: структура и где T: класс - PullRequest
48 голосов
/ 04 июня 2010

Я хотел бы различать следующие случаи:

  1. Тип простого значения (например, int)
  2. Тип значения обнуляемый (например, 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

Однако, если я попытаюсь обнаружить case (3) следующим образом, он не скомпилируется:

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

Сообщение об ошибке: Тип «X» уже определяет элемент с именем «Foo» с такими же типами параметров . Ну, как-то я не могу сделать разницу между where T : struct и where T : class.

Если я удалю третью функцию (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) для компиляции, сопоставив его с одной из вышеперечисленных функций (или с третьей с другим ограничением, о котором я не думал)?

Ответы [ 7 ]

49 голосов
/ 21 апреля 2016

Ограничения не являются частью подписи, но параметры являются. И ограничения в параметрах применяются во время разрешения перегрузки.

Итак, давайте установим ограничение в параметре. Это некрасиво, но работает.

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

(лучше на шесть лет позже, чем никогда?)

20 голосов
/ 04 июня 2010

К сожалению, вы не можете различить тип метода для вызова, основываясь только на ограничениях.

Таким образом, вам нужно определить метод в другом классе или с другим именем.

10 голосов
/ 04 июня 2010

В дополнение к вашему комментарию на Ответ Марникса , вы можете достичь того, чего хотите, используя немного размышлений.

В приведенном ниже примере неограниченный метод 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>.)

5 голосов
/ 04 июня 2010

Отбросьте структурное противоречие по первому методу. Если вам необходимо различать типы значений и классы, вы можете использовать тип аргумента для этого.

      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
         }
      }
2 голосов
/ 26 февраля 2012

Усиление моего комментария к LukeH, полезный шаблон, если нужно использовать Reflection для вызова различных действий, основанных на параметре типа (в отличие от типа экземпляра объекта), - это создание частного универсального статического класса, который выглядит как следующее (этот точный код не проверен, но я делал подобные вещи раньше):

static class 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)
  {
    ... Determine which kind of thing T is, and set `theAction` to one of the
    ... above methods.  Then end with ...
    theAction(param);
  }
}

Обратите внимание, что Reflection выдаст исключение, если попытаться создать делегат для ActionForOneKindOfThing<TT>(TT param), когда TT не соответствует ограничениям этого метода. Поскольку система проверяла тип TT при создании делегата, можно безопасно вызывать theAction без дальнейшей проверки типа. Обратите внимание также, что если внешний код делает:

  FooInvoker<T>.theAction(param);

Только первый звонок потребует отражения. Последующие вызовы просто вызовут делегата напрямую.

1 голос
/ 13 декабря 2015

Если вам не нужны общие параметры и вы просто хотите различать эти 3 случая во время компиляции, вы можете использовать следующий код.

static void Foo(object a) { } // reference type
static void Foo<T>(T? a) where T : struct { } // nullable
static void Foo(ValueType a) { } // valuetype
0 голосов
/ 14 ноября 2018

К счастью, этот вид возни требует меньше от C # версии 7.3

См. Что нового в C # 7.3 - Это не очень явно, но теперь кажется, что во время разрешения перегрузки в некоторой степени используются аргументы 'where'

Разрешение перегрузки теперь имеет меньше неоднозначных случаев

Также см. Выбор версии C # в вашем проекте Visual Studio

Он по-прежнему будет видеть столкновения со следующим

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
...