Weird bugs when using default interface implementations

What happens in short

  • wrong callstack
  • wrong generic types compilation
  • unity editor crashes when compiling my code

My setup

In my current project I use commands as data-only objects which are bind to strategies. I’ve define multiple strategies interfaces with default implementation, some of them inherit another, so I can use strategies with less responsibilities as strategies with higher responsibilities, for example I can define simple class DrawCardStrategy : ICommandStrategy<DrawCardCommand> and use it as IAsyncCommandStrategy<DrawCardCommand>.

Here you can inspect how my interfaces defined:
public interface ICommandStrategy { }

    public interface ICommandStrategyBox : ICommandStrategy
    {
        public void ExecuteBox(ICommandBox command);
    }

    public interface ICommandStrategyBox<out TOut> : ICommandStrategyBox
    {
        public TOut ExecuteOutBox(ICommandBox command);

        void ICommandStrategyBox.ExecuteBox(ICommandBox command)
            => _ = ExecuteOutBox(command);
    }

    public interface ICommandStrategy<in TCommand> :
        ICommandStrategyBox,
        IAsyncCommandStrategy<TCommand>
    {
        public void Execute(TCommand command);

        // default behavior for boxed execution is to cast and execute
        void ICommandStrategyBox.ExecuteBox(ICommandBox command) 
            => Execute((TCommand)command);

        UniTask IAsyncCommandStrategy<TCommand>.ExecuteAsync(TCommand command)
        {
            Execute(command);
            return UniTask.CompletedTask;
        }
    }

    public interface ICommandStrategy<in TCommand, TOut> :
        ICommandStrategy<TCommand>,
        ICommandStrategyBox<TOut>,
        IAsyncCommandStrategy<TCommand, TOut>
    {
        public TOut ExecuteOut(TCommand command);
        
        // if it can execute and return value then it can just execute with same method
        void ICommandStrategy<TCommand>.Execute(TCommand command)
            => Execute(command);

        void ICommandStrategyBox.ExecuteBox(ICommandBox command)
            => Execute((TCommand)command);

        // default behavior for boxed execution is to cast and execute
        TOut ICommandStrategyBox<TOut>.ExecuteOutBox(ICommandBox command)
            => ExecuteOut((TCommand)command);

        UniTask IAsyncCommandStrategy<TCommand>.ExecuteAsync(TCommand command)
        {
            Execute(command);
            return UniTask.CompletedTask;
        }

        UniTask<TOut> IAsyncCommandStrategy<TCommand, TOut>.ExecuteAsyncOut(TCommand command)
            => ExecuteOut(command).ToUniTaskResult();
    }

    public interface IAsyncCommandStrategyBox : ICommandStrategy
    {
        public UniTask ExecuteAsyncBox(ICommandBox command);
    }
    
    public interface IAsyncCommandStrategyBox<TOut> : ICommandStrategy
    {
        public UniTask<TOut> ExecuteAsyncOutBox(ICommandBox command);
    }

    public interface IAsyncCommandStrategy<in TCommand> : IAsyncCommandStrategyBox
    {
        public UniTask ExecuteAsync(TCommand command);

        UniTask IAsyncCommandStrategyBox.ExecuteAsyncBox(ICommandBox command)
            => ExecuteAsync((TCommand)command);
    }
    
    public interface IAsyncCommandStrategy<in TCommand, TOut> :
        IAsyncCommandStrategy<TCommand>,
        IAsyncCommandStrategyBox<TOut>
    {
        public UniTask<TOut> ExecuteAsyncOut(TCommand command);

        UniTask IAsyncCommandStrategy<TCommand>.ExecuteAsync(TCommand command)
            => ExecuteAsyncOut(command);

        UniTask<TOut> IAsyncCommandStrategyBox<TOut>.ExecuteAsyncOutBox(ICommandBox command)
            => ExecuteAsyncOut((TCommand)command);
    }

Problems in details

Wrong callstack

That was first problem I was surprised to see. Long story short unity invoke wrong method while should be invoke another. The reason was private method inside one of the interfaces. As I fat as I can tell it is fine thing in c# default interface implementation according to docs Default interface methods - C# feature specifications | Microsoft Learn. However removing just one private method solved my problem.

Wrong generic types compilation

That appeared yesterday and surprised me even more. The problem was that before today strategies interfaces had property public Type HandledType { get; } with default implementation in all generic strategies as public Type HandledType => typeof(TCommand). I have GetFirstCommand<T> and GetFirstStrategy<T> in my project to complete DRY. GetFirstStrategy<ICard> was return GetFirstCommand<GetFirstCommand<ICard>> from HandledType property instead of GetFirstCommand<ICard>. Strategy is implemented as simple as public class GetFirstStrategy<T> : ICommandStrategy<GetFirstCommand<T>, T> and command is implemeted as inheritor of base abstract class public class GetFirstCommand<TElement> : AEnumerableMediatorCommand<TElement> { } (this base abstract class shares same data for certain commands like “GetFirstCommand” / “GetLastCommand” / etc). The problem was resolved by removing HandledType property and binding command type manually during DI installation (which is how it should be done from the start).

Code
public class GetFirstCommand<TElement> : AEnumerableMediatorCommand<TElement> { }
    public class GetLastCommand<TElement> : AEnumerableMediatorCommand<TElement> { }
    public class GetRandomCommand<TElement> : AEnumerableMediatorCommand<TElement> { }
    public class GetSelectedCommand<TElement> : AEnumerableMediatorAsyncCommand<TElement> { }

    public class GetFirstStrategy<T> : ICommandStrategy<GetFirstCommand<T>, T>
    {
        public T ExecuteOut(GetFirstCommand<T> command) => command.Get().First();
    }
    public class GetLastStrategy<T> : ICommandStrategy<GetLastCommand<T>, T>
    {
        public T ExecuteOut(GetLastCommand<T> command) => command.Get().Last();
    }
    public class GetRandomStrategy<T> : ICommandStrategy<GetRandomCommand<T>, T>
    {
        [Inject] private IRandomProvider _rnd;
        
        public T ExecuteOut(GetRandomCommand<T> command) => command.Get().ToArray().GetRandom(_rnd);
    }
    public class GetSelectedStrategy<T> : IAsyncCommandStrategy<GetSelectedCommand<T>, T>
    {
        [Inject] private ISelector<T> _selector;
        
        public UniTask<T> ExecuteAsyncOut(GetSelectedCommand<T> command) 
            => _selector.RequestSelect(command.Get());
    }

    public abstract class AEnumerableMediator<T> : IProvider<IEnumerable<T>>
    {
        private IEnumerable<T> _source;
        
        public void Feed(IEnumerable<T> value) => _source = value;
        public IEnumerable<T> Get() => _source;
    }

    public abstract class AEnumerableMediatorCommand<T> : AEnumerableMediator<T>, IConsumerCommand<IEnumerable<T>, T> { }
    public abstract class AEnumerableMediatorAsyncCommand<T> : AEnumerableMediator<T>, IConsumerAsyncCommand<IEnumerable<T>, T> { }

Editor crashes when compiling code

This last problem happen just now and I can’t fix it anyway rather then just not use this interfaces inheritance hierarchy. I have abstract class command / strategy for filtration logic. It works like I have some concrete filter command which can obtain IEnumerable<T> and be executed with corresponding strategy with return type of IEnumerable<T>.

Code
[Serializable, AddTypeMenu("Base/Enumerable/Filter")]
    public abstract class AFilterCommand<T> : IConsumerCommand<IEnumerable<T>, IEnumerable<T>>, IProvider<IEnumerable<T>>
    {
        private IEnumerable<T> _source;

        public void Feed(IEnumerable<T> value) => _source = value;
        public IEnumerable<T> Get() => _source;
    }

    public abstract class AFilterStrategy<TCommand, T> : IAsyncCommandStrategy<TCommand, IEnumerable<T>>
        where TCommand : AFilterCommand<T>
    {
        public IEnumerable<T> ExecuteOut(TCommand command)
            => Filter(command, command.Get());

        protected abstract IEnumerable<T> Filter(TCommand command, IEnumerable<T> collection);
        
        public UniTask<IEnumerable<T>> ExecuteAsyncOut(TCommand command) 
            => Filter(command, command.Get()).ToUniTaskResult();
    }

I have the only implementation of those for filtering cards by theirs suit.

Code
[Serializable, AddTypeMenu("Base/Enumerable/Filter/Filter by suit")]
public class FilterBySuitCommand : AFilterCommand<ICard>
{
    public FilterBySuitCommand(ECardSuit suit) => Suit = suit;

    [field: SerializeField] public ECardSuit Suit { get; private set; }
}
    
public class FilterBySuitStrategy : AFilterStrategy<FilterBySuitCommand, ICard>
{
    protected override IEnumerable<ICard> Filter(FilterBySuitCommand command, IEnumerable<ICard> collection) 
        => collection.Where(card => card.Suit == command.Suit);
}

The problem is: when AFilterStrategy<TCommand, T> inherits IAsyncCommandStrategy<TCommand, IEnumerable<T>> nothing happens, all works just fine. But when it inherits ICommandStrategy<TCommand, IEnumerable<T>> instead unity just crashes. Also it crashed just every time I try to open project after then.

More details

Two last problems gone if ICommandStrategy<TCommand, TOut> doesn’t inherit ICommandStrategy<TCommand>. Maybe it could help on investigate the problem.

Conclusion

I understand that it is a very specific case containing hell of interface inheritance with default implementations of each other, but it seems that something compiles wrong when all of this melt with abstract classes.

Further investigations

It seems I have some complex combination of what is going wrong, so there are multiple conditions for that problem to exist

  • ICommandStrategy<TCommand, TOut> should inherit ICommandStrategy<TCommand>
  • Strategy class implementation should inherit generic class with EXACTLY 2 generic parameters which implement ICommandStrategy<TCommand, TOut> with EXACTLY the same parameters (order doesn’t matter), like abstract class AFoo<TCommand, T> : ICommandStrategy<TCommand, T>. If some of that two generic parameters not used in generic interface, all works fine. I use public abstract class AFilterStrategy<TCommand, T> : ICommandStrategy<TCommand, IEnumerable<T>> to generalize filtration strategies so they obtain IEnumerable<T> and return filtered of the same type. This leads to unity crashes constantly when trying to compile, BUT using only one generic parameter doesn’t crash anything, like public abstract class AFilterStrategy<TCommand> : ICommandStrategy<TCommand, IEnumerable<ICard>> where I explicitly define that that strategy is for ICard collections filtration.

One another case which crashes unity is: when create new CompareStrategy<int, int> unity crashes but compilation goes well (unity 6000.0.23f1)

Code
public struct CompareCommand<T1, T2> : ICommand<bool>
        where T1 : IComparable<T2>
    {
        public T1 A { get; }
        public T2 B { get; }
        public ECompare Mode { get; }

        public CompareCommand(T1 a, T2 b, ECompare mode)
        {
            A = a;
            B = b;
            Mode = mode;
        }
    }

    // creating an instance of this crashing unity
    public class CompareStrategy<T1, T2> : ICommandStrategy<CompareCommand<T1, T2>, bool> 
        where T1 : IComparable<T2>
    {
        public bool ExecuteOut(CompareCommand<T1, T2> command)
        {
            var result = command.A.CompareTo(command.B);
            return command.Mode switch
            {
                ECompare.Equal => result == 0,
                ECompare.Less => result < 0,
                ECompare.Greater => result > 0,
                ECompare.NotEqual => result != 0,
                ECompare.LessOrEqual => result <= 0,
                ECompare.GreaterOrEqual => result >= 0,
                _ => throw new ArgumentOutOfRangeException()
            };
        }
    }

If it’s this one, I also ran into it earlier this year:

public interface IValueProvider<TValue> : IValueProvider
{
    new TValue Value { get; }
    object IValueProvider.Value => Value; // <- this made Unity 2022.3.17f1 crash
}

It’s since been fixed for Unity 2023.3 and Unity 7 already, but looks like Unity 6 is not on the list yet.

If you think you’ve found a different bug, you should file a bug report using the menu item Help > Report a Bug... in the Editor.

1 Like