Generic constraints, where T : struct and where T : class

65,575

Solution 1

Constraints are not part of the signature, but parameters are. And constraints in parameters are enforced during overload resolution.

So let's put the constraint in a parameter. It's ugly, but it works.

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

(better six years late than never?)

Solution 2

You cannot differentiate the type of method to call based only on the constraints, unfortunately.

So you need to define a method in a different class or with a different name instead.

Solution 3

Further to your comment on Marnix's answer, you can achieve what you want by using a bit of reflection.

In the example below, the unconstrained Foo<T> method uses reflection to farm out calls to the appropriate constrained method - either FooWithStruct<T> or FooWithClass<T>. For performance reasons we'll create and cache a strongly-typed delegate rather than using plain reflection every time the Foo<T> method is called.

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>();
}

(Note that this example is not threadsafe. If you require thread-safety then you'll either need to use some sort of locking around all access to the cache dictionary, or -- if you're able to target .NET4 -- use ConcurrentDictionary<K,V> instead.)

Solution 4

Drop the struct contraint on the first method. If you need to differentiate between value types and classes you can use the type of the argument to do so.

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

Solution 5

Amplifying my comment to LukeH, a useful pattern if one will need to use Reflection to invoke different actions based upon a type parameter (as distinct from the type of an object instance) is to create a private generic static class something like the following (this exact code is untested, but I've done this sort of thing before):

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);
  }
}

Note that Reflection will throw an exception if one attempts to create a delegate for ActionForOneKindOfThing<TT>(TT param) when TT does not comply with that method's constraints. Because the system validated the type of TT when the delegate was created, one can safely invoke theAction without further type-checking. Note also that if outside code does:

  FooInvoker<T>.theAction(param);

only the first call will require any Reflection. Subsequent calls will simply invoke the delegate directly.

Share:
65,575
Pierre Arnaud
Author by

Pierre Arnaud

Pierre Arnaud is a long-time software enthousiast who likes to consider himself as a craftsman. As a kid, he wrote programs on a mechanical typewriter, imagining what the computer would do with his BASIC programs. In 1982, he bought his first 8-bit CPU (RCA 1802) which he programmed in binary, by toggling switches. In 1984, he discovered the Smaky at the local school and started experimenting with new programming languages, such as PASCAL, LOGO, PROLOG, etc. On Christmas 1985 he received a Commodore 128D from his parents, on which he developed his first real applications: a drawing program, followed the next year by a text processor. In 1991, he wrote his first implementation a LISP interpreter for the Smaky computer. He had already acquired some experience developing system software, applications and drivers in CALM assembly language for the Smaky OS. A few years later, he gradually switched from assembly language to C++ and contributed a port of GCC and Ghostscript to the Smaky. He also developed the Smaky emulator and the last Smaky hardware (Smaky 400). Now, he is still happy developing software, but mainly in C#... Pierre has a Ph.D. in Computer Science (artificial intelligence) from the EPFL.

Updated on November 16, 2020

Comments

  • Pierre Arnaud
    Pierre Arnaud over 3 years

    I would like to differentiate between following cases:

    1. A plain value type (e.g. int)
    2. A nullable value type (e.g. int?)
    3. A reference type (e.g. string) - optionally, I would not care if this mapped to (1) or (2) above

    I have come up with the following code, which works fine for cases (1) and (2):

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

    However, if I try to detect case (3) like this, it does not compile:

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

    The error message is Type 'X' already defines a member called 'Foo' with the same parameter types. Well, somehow I cannot make a difference between where T : struct and where T : class.

    If I remove the third function (3), the following code does not compile either:

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

    How can I get Foo(z) to compile, mapping it to one of the above functions (or a third one with another constraint, which I have not thought of)?