Избегайте явного приведения типов при переопределении унаследованных методов

У меня есть базовый абстрактный класс, который также реализует конкретный интерфейс.

public interface IMovable<TEntity, T>
    where TEntity: class
    where T: struct
{
    TEntity Move(IMover<T> moverProvider);
}

public abstract class Animal : IMovable<Animal, int>
{
    ...

    public virtual Animal Move(IMover<int> moverProvider)
    {
        // performs movement using provided mover
    }
}

Затем я унаследовал классы, некоторые из которых должны переопределить методы реализации интерфейса базового класса.

public class Snake : Animal
{
    ...

    public override Animal Move(IMover<int> moverProvider)
    {
        // perform different movement
    }
}

Мои методы интерфейса возвращают один и тот же экземпляр объекта после его перемещения, поэтому я могу использовать цепочку или делать что-то непосредственно в инструкции return без использования дополнительных переменных.

// I don't want this if methods would be void typed
var s = GetMySnake();
s.Move(provider);
return s;

// I don't want this either if at all possible
return (Snake)GetMySnake().Move(provider);

// I simply want this
return GetMySnake().Move(provider);

Вопрос

Как вы можете видеть в моем примере, мои переопределения в дочернем классе возвращают тип базового класса вместо запуска класса. Это может потребовать от меня отдать результаты, которых я бы хотел избежать.

Как я могу определить свой интерфейс и реализации, чтобы мои переопределения вернули фактический тип исполняемого экземпляра?

public Snake Move(IMover<int> moverProvider) {}

Ответ 1

Я предлагаю изменить возвращаемый тип метода интерфейса на void и перемещать поведение цепочки в метод расширения, где вы можете получить реальный тип цели, например.

public interface IMovable<TEntity, T>
    where TEntity : class
    where T : struct
{
    void MoveTo(IMover<T> moverProvider);
}

public abstract class Animal : IMovable<Animal, int>
{
    public virtual void MoveTo(IMover<int> mover) { }
}

public static class AnimalExtensions
{
    public static TAnimal Move<TAnimal>(this TAnimal animal, IMover<int> mover) where TAnimal : Animal, IMovable<TAnimal, int>
    {
        animal.MoveTo(mover);
        return animal;
    }
}

Обратите внимание: вы можете сделать расширение Move более универсальным, если вам нужно применить его в более общем плане:

public static TEntity Move<TEntity, T>(this TEntity entity, IMover<T> mover) where TEntity : IMovable<TEntity, T> where T : struct
{
    entity.MoveTo(mover);
    return entity;
}

Ответ 2

Вы можете преобразовать Animal в общий тип, который принимает конкретный тип как параметр типа:

public abstract class Animal<T> : IMovable<T, int> where T:Animal<T>        
{


    public virtual T Move(IMover<int> moverProvider)
    {
    ...
    }
}

public class Snake : Animal<Snake>
{


    public override Snake Move(IMover<int> moverProvider)
    {
    ...
    }
}

Ответ 3

Как насчет:

public virtual T Move<T>(IMover<int> moverProvider) where T : Animal
{
    // performs movement using provided mover
}

Ответ 4

Иногда вам нужно иметь текущий тип как возвращаемое значение метода, и он должен изменяться в производных классах. Я бы избегал этого шаблона, потому что это приведет к странному поведению и необычному синтаксису (если ваша модель становится сложной), но попробуйте (как правило, потому что для очень маленьких иерархий это выглядит довольно просто):

abstract class Animal<TConcrete> : IMovable<TConcrete, int>
where TConcrete : Animal<T>
{
    public virtual T Move(IMover<int> moverProvider) {
        return (T)this; // Cast to Animal<T> to T isn't implicit
    }
}

sealed class Snake : Animal<Snake>
{
    public virtual Snake Move(IMover<int> moverProvider) {
        return this;
    }
}

Почему это плохо? Вы можете ответить самим себе, когда вам нужно объявить общую переменную типа Animal<TConcrete> (на практике это останавливает вас, чтобы иметь переменную с этим базовым классом).

Что бы я сделал, это сделать это требование ясным (с классом или методом расширения - в этом случае с использованием другого имени):

abstract class Animal : IMovable<Animal, int>
{
    // Please note that this implementation is explicit
    Animal IMovable<Animal, int>.Move(IMover<int> moverProvider) {
        return MoveThisAnimal(moverProvider);
    }

    protected virtual Animal MoveThisAnimal(IMover<int> moverProvider) {
        // Peform moving
        return this;
    }
}

class Snake : Animal
{
    public Snake Move(IMover<int> moverProvider) {
        return (Snake)MoveThisAnimal(moverProvider);
    }

    protected override Animal MoveThisAnimal(IMover<int> moverProvider) {
        // Peform custom snake moving
        return this;
    }
}

Ответ 5

Это беспорядочно, но, введя неосновный базовый интерфейс, метод расширения может дать желаемый результат. Он также может быть упрощен (чтобы удалить вторую реализацию явного интерфейса), если вы не заботитесь об экспонировании "MoveFunc" вызывающим абонентам:

public interface IMovable
{
    IMovable MoveFunc();
}

public interface IMovable<TEntity, T> : IMovable
    where TEntity : IMovable
{
    new TEntity MoveFunc();
}

public abstract class Animal : IMovable<Animal, int>
{
    protected virtual Animal MoveFunc()
    {
        // performs movement using provided mover
        Debug.WriteLine("Animal");
    }

    Animal IMovable<Animal, int>.MoveFunc()
    {            
        return MoveFunc();
    }

    IMovable IMovable.MoveFunc()
    {
        return ((IMovable<Animal, int>)this).MoveFunc();
    }
}

public class Snake : Animal
{
    protected override Animal MoveFunc()
    {
         // performs movement using provided mover
         Debug.WriteLine("Snake");
    }
}

public static class IMovableExtensions
{
    public static TOut Move<TOut>(this TOut entity) where TOut : IMovable
    {
        return (TOut)entity.MoveFunc();
    }

}

...

Snake snake = new Snake();

Snake moved = snake.Move(); // "Snake"

Animal animal = snake;

animal.Move() // "Snake"