Search code examples
c#genericscasting.net-7.0

Cast this to generic type without tricking compiler


Consider this simplified class hierarchy:

public interface IProcessor
{
  Task<TProcessor> Run<TProcessor>() where TProcessor : IProcessor;
}


public abstract class Processor : IProcessor
{

  public async Task<TProcessor> Run<TProcessor>() where TProcessor : IProcessor
  {
    await DoWork();
    return (TProcessor)(object)this;  // works, but yuck
    //return (TProcessor)this;        // error: "Cannot convert type 'Processor' to 'TProcessor'"
  }

  protected abstract Task DoWork();

}


public sealed class FooProcessor : Processor
{
  protected override Task DoWork() => Task.CompletedTask;
}

The (TProcessor)(object)this trick works, but is an ugly compiler hack.

Can I modify this code to rely on compile-time checks instead?


Solution

  • A run time error is possible if you have another class inheriting from Processor, say BarProcessor. Then BarProcessor p = new FooProcessor().Run<BarProcessor>(); will compile, but will fail at run time.

    C# does not support a "self" type like this (see for example https://github.com/dotnet/csharplang/issues/5413 ), but the code can be improved by moving the type parameter from the method to the type. This restricts the potential for mistakes to the class implementations and reduces the need to specify type parameters at the call site.

    For example:

    public interface IProcessor<TProcessor> where TProcessor : IProcessor<TProcessor>
    {
        Task<TProcessor> Run();
    }
    
    public abstract class Processor<TProcessor> : IProcessor<TProcessor>
        where TProcessor : Processor<TProcessor>
    {
        public async Task<TProcessor> Run()
        {
          await DoWork();
          return (TProcessor)this;
        }
    
        protected abstract Task DoWork();
    }
    
    public sealed class FooProcessor : Processor<FooProcessor>
    {
        protected override Task DoWork() => Task.CompletedTask;
    }
    
    public sealed class BarProcessor : Processor<BarProcessor>
    {
        protected override Task DoWork() => Task.CompletedTask;
    }
    

    Now callers of Run cannot specify a wrong type parameter (since there isn't any), but FooProcessor can still erroneously inherit from Processor<BarProcessor>. Also, the interface IProcessor<TProcessor> is now clearly only useful as a constraint (where T: IProcessor<T>), not as a type.

    Note that FooProcessor and BarProcessor are sealed. Allowing inheritance from a Processor<TProcessor> subclass will require that subclass to be generic, like public abstract class SpecialProcessor<TProcessor> : Processor<TProcessor> where TProcessor : SpecialProcessor<TProcessor>.