Toxantron
Toxantron

Reputation: 2398

Generics covariance in constraint

I am currently having trouble with generics in C# and I have a solution, but I am looking for a cleaner one. I have generally a pretty good C# knowledge, so I am not sure, if what I want exists.

I have a generic base class, with nested constraints

public abstract class BaseParam { }

public abstract class BaseProcess<TParam> : IProcess
   where TParam : BaseParam
{
    public TParam Parameter { get; set; }
}

public abstract class BaseTask<TProc, TParam>
   where TProc : BaseProcess<TParam>, new()
   where TParam : BaseParam, new()
{
    public IProcess Create()
    {
        var proc = new TProc();
        proc.Param = new TParam();
        return proc;
    }
}

Now I could create a couple of derivations and this worked for a while

public class SomeParam : BaseParam { }

public class SomeProcess : BaseProcess<SomeParam> { }

public class SomeTask : BaseTask<SomeProcess, SomeParam> { }

The problem occurs when I start mixing the inheritance levels like so

public class SuperParam : SomeParam { }

// The next line won't compile
public class SuperTask : BaseTask<SomeProcess, SuperParam> { }

I know it does not compile because Covariance only exists on interfaces and Foo<A> and Foo<B> are technically two different classes. However the code inside would work just fine. What I want is to write a "covariant constraint" like:

public abstract class BaseTask<TProc, TParam>
   where TProc : BaseProcess<TProcParam>
   where TProcParam : BaseParam
   where TParam : TProcParam
{
}

I tried using ISomeProc<TParam> because interfaces are covariant, but that only works for assignments, not lower bounds. Functional Workaround is declaring an additional type argument, but that is not pretty enough:

public abstract class BaseTask<TProc, TProcParam, TParam>
   where TProc : BaseProcess<TProcParam>
   where TProcParam : BaseParam
   where TParam : TProcParam
{
}

CompilerError:

The type 'SomeProcess' cannot be used as type parameter 'TProc' in the
generic type or method 'BaseTask<TProc,TParam>'. There is no implicit
reference conversion from 'SomeProcess' to 'BaseProc<SuperParam>'.  

Edit: I understand some more explanation is required. Within the class I don't need properties of SuperParam, only BaseParam, but it should be an instance of SuperParam. However we read the SuperTask definition with reflection and create an editor. And that editor must know what object it is supposed to create.

Question: Is there any way to design the constraints of BaseTask so that it respects covariance without adding the extra parameter TProcParam? I could come up with workarounds, but I feel like there is an easier way.

Upvotes: 1

Views: 625

Answers (2)

Toxantron
Toxantron

Reputation: 2398

Based on the comments and further inspection I noticed two things:

  1. The example is incomplete, which distracted from the problem at hand.
  2. What I want to do is impossible for the compiler to understand

The solution I will go with for now is the a combination of the workaround and an effort to keep the default behavior clean:

// Use workaround on BaseTask
public abstract class BaseTask<TProc, TProcParam, TParam>
   where TProc : BaseProcess<TProcParam>
   where TProcParam : BaseParam
   where TParam : TProcParam
{
}

// Generic shortcut overload
public abstract class BaseTask<TProc, TParam> : BaseTask<TProc, TParam, TParam>
   where TProc : BaseProcess<TProcParam>
   where TParam : TProcParam
{
}

This way I can do both:

public class SomeTask : BaseTask<SomeProcess, SomeParam> { }

public class SuperTask : BaseTask<SomeProcess, SomeParam, SuperParam> { }

Upvotes: 0

Arturo Menchaca
Arturo Menchaca

Reputation: 15982

A solution for your problem is highly dependent on how are implemented your classes, but if you can convert BaseProcess<> to a co-variant interface IBaseProcess<>:

public interface IBaseProcess<out TParam>
   where TParam : BaseParam
{
}

And declare BaseTask as:

public abstract class BaseTask<TProc, TParam>
    where TProc : IBaseProcess<BaseParam>
    where TParam : BaseParam
{
}

Then, SomeProcess as:

public class SomeProcess : IBaseProcess<SomeParam> { }

You would be able to declare SuperTask as you want:

public class SuperTask : BaseTask<SomeProcess, SuperParam> { }

Upvotes: 1

Related Questions