Можно ли переопределить метод с производным параметром вместо базового?

Я застрял в этой ситуации, когда:

  • У меня есть абстрактный класс Ammo, с AmmoBox и Clip как дети.
  • У меня есть абстрактный класс, называемый Weapon, с Firearm и Melee как дети.
  • Firearm является абстрактным, с ClipWeapon и ShellWeapon как дети.
  • Внутри Firearm есть void Reload(Ammo ammo);

Проблема заключается в том, что a ClipWeapon может использовать как Clip, так и AmmoBox для перезагрузки:

public override void Reload(Ammo ammo)
{
    if (ammo is Clip)
    {
        SwapClips(ammo as Clip);
    }
    else if (ammo is AmmoBox)
    {
        var ammoBox = ammo as AmmoBox;
        // AddBullets returns how many bullets has left from its parameter
        ammoBox.Set(clip.AddBullets(ammoBox.nBullets));
    }
}

Но a ShellWeapon может использовать только AmmoBox для перезагрузки. Я мог бы сделать это:

public override void Reload(Ammo ammo)
{
    if (ammo is AmmoBox)
    {
        // reload...
    }
}

Но это плохо, потому что, несмотря на то, что я проверяю, что это тип AmmoBox, снаружи, он выглядит как ShellWeapon может принимать Clip, так как a Clip также Ammo.

Или, я мог бы удалить Reload из Firearm и поместить его как ClipWeapon и ShellWeapon с конкретными параметрами, которые мне нужны, но при этом я потеряю преимущества полиморфизма, что не то, что я хотите.

Не было бы это оптимальным, если бы я мог переопределить Reload внутри ShellWeapon следующим образом:

public override void Reload(AmmoBox ammoBox)
{
   // reload ... 
}

Конечно, я попробовал, и это не сработало, я получил сообщение об ошибке, что подпись должна соответствовать или что-то в этом роде, но разве это не должно быть "логически"? так как AmmoBox есть Ammo?

Как мне обойти это? И вообще, мой дизайн правильный? (Обратите внимание, что я использовал интерфейсы IClipWeapon и IShellWeapon, но у меня возникли проблемы, поэтому я перешел на использование классов)

Спасибо заранее.

Ответ 1

но разве это не должно быть "логически"?

Нет. В вашем интерфейсе говорится, что вызывающий может передать любой Ammo - там, где вы его ограничиваете, требуя AmmoBox, что более конкретно.

Что бы вы ожидали, если бы кто-то написал:

Firearm firearm = new ShellWeapon();
firearm.Reload(new Ammo());

? Это должен быть полностью действительный код - так вы хотите, чтобы он взорвался во время выполнения? Половина статической типизации - это избежать этой проблемы.

Вы можете сделать Firearm generic в типе боеприпасов:

public abstract class Firearm<TAmmo> : Weapon where TAmmo : Ammo
{
    public abstract void Reload(TAmmo ammo);
}

Тогда:

public class ShellWeapon : Firearm<AmmoBox>

Это может быть или не быть полезным способом делать что-то, но, по крайней мере, стоит подумать.

Ответ 2

Проблема, с которой вы боретесь, связана с необходимостью разговора с другой версией, основанной на типах боезапасов и оружия. По сути, действие перезагрузки должно быть "виртуальным" по отношению к двум, а не одному объекту. Эта проблема называется двойная отправка.

Один из способов решения этой проблемы - создать конструкцию, похожую на посетителя:

abstract class Ammo {
    public virtual void AddToShellWeapon(ShellWeapon weapon) {
        throw new ApplicationException("Ammo cannot be added to shell weapon.");
    }
    public virtual void AddToClipWeapon(ClipWeapon weapon) {
        throw new ApplicationException("Ammo cannot be added to clip weapon.");
    }
}
class AmmoBox : Ammo {
    public override void AddToShellWeapon(ShellWeapon weapon) {
        ...
    }
    public override void AddToClipWeapon(ClipWeapon weapon) {
        ...
    }
}
class Clip : Ammo {
    public override void AddToClipWeapon(ClipWeapon weapon) {
        ...
    }
}
abstract class Weapon {
    public abstract void Reload(Ammo ammo);
}
class ShellWeapon : Weapon {
    public void Reload(Ammo ammo) {
        ammo.AddToShellWeapon(this);
    }
}
class ClipWeapon : Weapon {
    public void Reload(Ammo ammo) {
        ammo.AddToClipWeapon(this);
    }
}

"Магия" происходит в реализациях Reload подклассов оружия: вместо того, чтобы решать, какие боеприпасы они получают, они позволяют самим боеприпасам выполнять "вторую ногу" двойной отправки и вызывать любой метод потому что их методы AddTo...Weapon знают как их собственный тип, так и тип оружия, в которое они перезаряжаются.

Ответ 3

Вы можете использовать композицию с расширениями интерфейса вместо множественного наследования:

class Ammo {}
class Clip : Ammo {}
class AmmoBox : Ammo {}

class Firearm {}
interface IClipReloadable {}
interface IAmmoBoxReloadable {}

class ClipWeapon : Firearm, IClipReloadable, IAmmoBoxReloadable {}
class AmmoBoxWeapon : Firearm, IAmmoBoxReloadable {}

static class IClipReloadExtension {
    public static void Reload(this IClipReloadable firearm, Clip ammo) {}
}

static class IAmmoBoxReloadExtension {
    public static void Reload(this IAmmoBoxReloadable firearm, AmmoBox ammo) {}
}

Итак, у вас будет 2 определения метода Reload() с Clip и AmmoBox в качестве аргументов в ClipWeapon и только 1 метод Reload() в классе AmmoBoxWeapon с аргументом AmmoBox.

var ammoBox = new AmmoBox();
var clip = new Clip();

var clipWeapon = new ClipWeapon();
clipWeapon.Reload(ammoBox);
clipWeapon.Reload(clip);

var ammoBoxWeapon = new AmmoBoxWeapon();
ammoBoxWeapon.Reload(ammoBox);

И если вы попробуете передать клип на AmmoBoxWeapon.Reload, вы получите сообщение об ошибке:

ammoBoxWeapon.Reload(clip); // <- ERROR at compile time

Ответ 4

Я думаю, что отлично проверить, прошел ли Ammo допустимый тип. Аналогичная ситуация имеет место, когда функция принимает Stream, но внутренне проверяет, является ли она доступной для поиска или записывается - в зависимости от ее требований.