Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Generics lower bound constraint "where MySubClass : T" (java's "super")

I'm wondering if C# has an equivalent to Java's <X super MySubClass> generic constraint.

To specify an upper bound, one can use class Foo<T> where T : MySuperClass { ... }, but how can we specify a lower bound on the generic parameter?


There are some ways to get similar results, but I haven't found something perfect yet:

  1. Use a second generic parameter — but the caller could specify a subclass of the actual lower bound.

    public class Foo<T, TLowerBound>
        where TLowerBound : MySubClass
        where TLowerBound : T
    {
        ...
    }
    
  2. This is sometime used on extension methods so the extension method's parameter U is constrained to be a superclass of the class' parameter T.

    public static class Extensions {
        public static void Method<T, U>(this Foo<T> self, U someU) where T : U {
            self.ValueOfTypeT = someU;
        }
    }
    
  3. Use variance on interfaces, but I'm not sure if this can be used to specify a lower bound on a generic parameter.

like image 222
Suzanne Soy Avatar asked Nov 02 '22 05:11

Suzanne Soy


1 Answers

I have just faced the same issue. Option 2 (using extension methods) works quite well until you need the method with the lower bound to be virtual (and thus dispatched based on the dynamic type of the object). If you need that, here is a viable solution using Option 3 (variance on interfaces, plus the well known visitor pattern).

In order to achieve an equivalent of

public class A<T> // an argument to the generic method
{
}

public class B<S>
{
    public virtual R Fun<T>(A<T> arg) where S : T // illegal in C#/CLR
    {
        ...
    }
}

public class C<S> : B<S>
{
    public override R Fun<T>(A<T> arg)
    {
    }
}

you do the following. First, you define an interface for the operation to be performed (we will be using visitor pattern here, and thus there has to be a separate method for each type overriding Fun):

public interface IFun<in T>
{
    R Fun<S>(B<S> self) where S : T;
    R Fun<S>(C<S> self) where S : T;
}

Note, that the generic parameter T is only used as a constraint, and therefore the interface can be contravariant with respect to it. We now use this, letting B and C be "visited" by the operation:

public class B<S>
{
    public virtual R Perform(IFun<S> fun)
    // contravariant, any IFun<T> with S : T will be accepted
    {
        return fun.Fun(this);
    }
}

public class C<S> : B<S>
{
    public override R Perform(IFun<S> fun)
    {
        return fun.Fun(this);
    }
}

In order to actually perform the operation with an argument A<T>, you wrap it in a struct/class implementing the interface:

public struct TheFun<T> : IFun<T>
{
    public A<T> arg;

    R IFun<T>.Fun<S>(B<S> self)
    {
        ... body of B<S>.Fun(A<T> arg) ...
    }

    R IFun<T>.Fun<S>(C<S> self)
    {
        ... body of C<S>.Fun(A<T> arg) ...
    }
}

To close up, you introduce an extension method, like in Option 2:

public static class Extensions
{
    public static R Fun<S,T>(this B<S> self, A<T> arg) where S : T
    {
        return self.Perform(new TheFun<T> { arg = arg });
    }
}

Done. It works, without a single cast or type check. The main drawbacks are:

  • it is quite convoluted (though the code size is longer from the intended by only a constant factor) and might make people reading your code hate you
  • the implementations have been moved from B and C to TheFun, and thus any required members of B and C have to be made accessible there
like image 158
Grzegorz Herman Avatar answered Nov 12 '22 20:11

Grzegorz Herman