Можно ли переопределить метод с производным параметром вместо базового?
Я застрял в этой ситуации, когда:
- У меня есть абстрактный класс
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
, но внутренне проверяет, является ли она доступной для поиска или записывается - в зависимости от ее требований.