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