C# Is Not Assignable Type - Generics

后端 未结 2 958
粉色の甜心
粉色の甜心 2021-01-18 13:42

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

相关标签:
2条回答
  • 2021-01-18 13:54

    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);
        }
    }
    
    0 讨论(0)
  • 2021-01-18 14:06

    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.

    0 讨论(0)
提交回复
热议问题