6

So I'm just hacking around with a state machine type I was working on and mostly wanting to just try out the Activator.CreateInstance method to see what it was like, and I ran into a problem where I cant seem to use the where clause as I would think. I apologize ahead of time if I am just an idiot and everyone laughs me out of here. So I have 2 small classes.

public class TransitionContainer<TTransition, TStateTo> :
    ITransitionContainer<TTransition, TStateTo>
    where TTransition : ITransition
    where TStateTo : IState
{
    public TransitionContainer()
    {
        StateTo = typeof(TStateTo);
        Transition = Activator.CreateInstance<TTransition>();
    }

    public Type StateTo { get; private set; }

    public TTransition Transition { get; private set; }
}

as well as

  public class StateContainer<T> : IStateContainer<T> where T : IState
{
    private Dictionary<Type, TransitionContainer<ITransition, IState>> _transitions =
        new Dictionary<Type, TransitionContainer<ITransition, IState>>();

    public StateContainer()
    {
        State = Activator.CreateInstance<T>();
    }

    public T State { get; private set; }

    public int TransitionCount
    {
        get { return _transitions.Count; }
    }


    public void AddTransition<TTransition, TStateTo>() where TTransition : ITransition, new()
        where TStateTo : IState, new()
    {
        var transitionContainer= new TransitionContainer<TTransition, TStateTo>();

        _transitions.Add(typeof(TTransition), transitionContainer);
    }

So on the line _transitions.Add(typeof(TTransition), transitionContainer); I receive a cannot convert TransitionContainer<TTransition,TStateTo> expression to type TransitionContainer<ITransition,IState> error.

If I change the generic parameters to

var transitionContainer= new TransitionContainer<ITransition, IState>();

it works fine, but I wanted to use inherited types that are new() so I could be sure I could instantiate them.

Again I apologize if I'm doing something incredibly wrong, I was just kind of ran into a brick wall and my googling led me in no good direction. I didnt include any of the other interfaces or classes as they didn't seem to be part of the problem, but if there needed I can attach them. Thanks for any help!

Travis Scott
  • 95
  • 1
  • 6

2 Answers2

4

This issue happens because:

  1. ITransitionContainer is not a covariant interface over its type arguments.
  2. AddTransition method generic arguments are not constrained to be reference types.
  3. _transitions is not a dictionary with ITransitionContainer values, so without changing it to Dictionary<Type, ITransitionContainer<ITransition, IState>> we still won't be able to add even properly resticted covariant transtions.

Simplified example

Consider the following simplified case:

public interface ITransition
{

}

public class SomeTransition : ITransition
{

}

public interface ITest<TTransition>
    where TTransition : ITransition
{
    TTransition Value { get; }
}


public class SomeTest<TTransition> : ITest<TTransition>
    where TTransition : ITransition
{
    public TTransition Value
    {
        get
        {
            throw new NotImplementedException();
        }
    }
}

It will fail in both

public static void Do<TTransition>()
    where TTransition : ITransition
{
    ITest<ITransition> item = new SomeTest<TTransition>();
}

and

ITest<ITransition> item = new SomeTest<SomeTransition>();

If you make ITest covariant

public interface ITest<out TTransition>

, then it will fail only in generic method. Because here TTransition can be a struct and co/(contra)variance doesn't work with value types:

public static void Do<TTransition>()
    where TTransition : ITransition
{
    ITest<ITransition> item = new SomeTest<TTransition>();
}

But if you make that method constrained to only reference types, then it will work in both cases:

public static void Do<TTransition>()
    where TTransition : class, ITransition
{
    ITest<ITransition> item = new SomeTest<TTransition>();
}

Apply the same principle(out and class) to your two generic arguments and it will do the job.


Full solution for your specific case:

public interface IState
{    }

public interface ITransition
{    }

// !!!!! - Here we add out specifier
public interface ITransitionContainer<out TTransition, out TStateTo>
    where TTransition : ITransition
    where TStateTo : IState
{
    Type StateTo
    {
        get;
    }

    TTransition Transition
    {
        get;
    }
}

public interface IStateContainer<T> where T : IState
{
    T State
    {
        get;
    }
}


public class TransitionContainer<TTransition, TStateTo> : ITransitionContainer<TTransition, TStateTo>
    where TTransition : ITransition
    where TStateTo : IState
{
    public TransitionContainer()
    {
        StateTo = typeof(TStateTo);
        Transition = Activator.CreateInstance<TTransition>();
    }

    public Type StateTo { get; private set; }

    public TTransition Transition { get; private set; }
}


public class StateContainer<T> : IStateContainer<T> where T : IState
{
    private Dictionary<Type, ITransitionContainer<ITransition, IState>> _transitions =
        new Dictionary<Type, ITransitionContainer<ITransition, IState>>();

    public StateContainer()
    {
        State = Activator.CreateInstance<T>();
    }

    public T State { get; private set; }

    public int TransitionCount
    {
        get { return _transitions.Count; }
    }

    public void AddTransition<TTransition, TStateTo>()
        // !!!!!! - Here we add class constraints
        where TTransition : class, ITransition, new()
        where TStateTo : class, IState, new()
    {
        var transitionContainer = new TransitionContainer<TTransition, TStateTo>();

        _transitions.Add(typeof(TTransition), transitionContainer);
    }
}
Community
  • 1
  • 1
Eugene Podskal
  • 10,270
  • 5
  • 31
  • 53
  • So should he add `where` and `out` to the definition of `interface ITransitionContainer` and `class` to the `AddTransition` method? I tried but it still doesn't work. – Andrew Jun 26 '16 at 08:25
  • Hey thanks for the answer. Just as Andrew said though I tried and still received the same error. I've been looking through the stack overflow links and docs you've linked so I'll see what I can do! – Travis Scott Jun 26 '16 at 19:06
  • @TravisScott I've given a so called **full solution** that compiles and executes fine for me under .NET 4.6.1 in VS2015 - doesn't that one work for you? Perhaps you can create a [Minimal Complete Verifiable Example](http://stackoverflow.com/help/mcve) so that we can be sure that there is nothing missing that could influence the result? – Eugene Podskal Jun 26 '16 at 19:17
  • Sorry @EugenePodskal that was my fault, I got it now. I thought I did go through it, but I needed to switch the Dictionary from TransitionContainer to ITransitionContainer. Noticed it when I made a new project and was writing as little I could. Sorry should have followed more closely, the Minimal Complete Verifiable Example worked perfectly I suppose. :P Thanks for all your help!! – Travis Scott Jun 27 '16 at 00:21
  • as a note though, I am getting a `System.ArrayTypeMismatchException : Source array type cannot be assigned to destination array type.` when `_transitions.Add(typeof(TTransition), transitionContainer);` runs. I'll see what I can do – Travis Scott Jun 27 '16 at 00:45
  • @TravisScott About interface in place of concrete class - ehm, that's actually was a my fault - I forgot to mention that dictionary should use interfaces - otherwise variance won't work. And about your ArrayTypeMismatchException you should probably ask another question, just with [Minimal Complete Verifiable Example](http://stackoverflow.com/help/mcve). – Eugene Podskal Jun 27 '16 at 07:57
2

That fails because generics are not covariant. The problem can be seen here:

TransitionContainer<ITransition, IState> value = new TransitionContainer<TTransition, TStateTo>();

That gives you the same error. You also get this error with something as simple as:

List<IComparable> list = new List<DateTime>();

Visual Studio tells you (basically) that:

Cannot implicitly convert type 'List<System.DateTime>' to 'List<System.IComparable>'

What you need to do is convert the object. You could create a Convert method that returns a TransitionContainer<ITransition, IState> and then use .Add(typeof(TTransition), transitionContainer.Convert()) (or whatever you name it).

But the most painless option is to create an implicit conversion for your TransitionContainer<TTransition, TStateTo> object by adding this static method:

public static implicit operator TransitionContainer<ITransition, IState>(TransitionContainer<TTransition, TStateTo> value)
{
    return new TransitionContainer<ITransition, IState>() { StateTo = value.StateTo, Transition = value.Transition };
}

And that's it. :)

Of course, you will have to copy everything needed for it to work, in this case it seems these two objects are enough.

Andrew
  • 7,602
  • 2
  • 34
  • 42