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

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

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

Проблема в том, что 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));
    }
}

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

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

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

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

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

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

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

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

Заранее спасибо.

4 ответа

Решение

но не должно ли это быть действительным "логически"?

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

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

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

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

Вы могли бы сделать Firearm универсальный тип патронов использует:

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

Затем:

public class ShellWeapon : Firearm<AmmoBox>

Это может или не может быть полезным способом ведения дел, но это по крайней мере стоит рассмотреть.

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

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

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 методы знают как свой тип, так и тип оружия, в которое они перезаряжаются.

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

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);

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

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

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

Другие вопросы по тегам