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.