Как изменить тип унаследованного метода?

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

    public abstract class Character
    {
        public abstract void Attack(Character t);
    }

    public class A :Character
    {
            public override void Attack(Character t){}

            public void Attack(A x)
            {
             /*instructions*/
            }

            public void Attack(B y)
            {
             /*instructions*/
            }

            public void Attack(C z)
            {
             /*instructions*/
            }
    }

И так далее для классов B и C.

Я бы также предпочел избежать этого:

    public abstract class Character
    {
        public abstract void Attack(Character c);
    }

    public class A :Character
    {
            public override void Attack(Character t)
            {
                A x = t as A
                if (x != null)
                {
                    /*instructions*/
                }

                B y = t as B
                if (y != null)
                {
                    /*instructions*/
                }

                C z = t as C
                if (z != null)
                {
                    /*instructions*/
                }
            }
    }

Надеюсь, мой вопрос достаточно ясен, несмотря на мой английский.

Если A, B и C являются производными от Character, вы столкнетесь с проблемами, связанными с отправкой C# по отдельности. Скорее всего, вы смотрите на реализацию паттерна Посетитель, чтобы выполнить то, что вы хотите.

Jonathon Chase 13.06.2018 23:20

Мне не имеет смысла помещать инструкции для всех 3 классов в A.

paparazzo 13.06.2018 23:25

Я написал серию статей об этой проблеме; вам, вероятно, будет интересно. ericlippert.com/2015/04/27/wizards-and-warriors-part-one. (Прочтите все; ваша конкретная проблема начинается в части 3.) Комментарий Джонатона Чейза верен; проблема, с которой вы столкнулись, заключается в том, что C# отправляется однократно. Я считаю, что вам не следует в первую очередь пытаться представлять правила игры в системе типов! Создайте класс, представляющий правила игры, и поместите правила в что.

Eric Lippert 13.06.2018 23:52
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
3
152
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Поскольку вы знаете, что у вас есть только 3 разных типа, почему бы вам просто не создать 3 абстрактных метода для каждого типа в вашем классе Charackter?

    public abstract class Character
        {
            public abstract void Attack(A a);
            public abstract void Attack(B a);
            public abstract void Attack(C a);
        }

Каждому классу нужно будет переопределить все.

paparazzo 14.06.2018 00:32

@paparazzo, я думал, это то, что он хотел. у каждого персонажа есть функция для каждого другого персонажа

Bodo Baumstamm 15.06.2018 17:28

Зачем вам 3 класса, если все делают все? Это был не мой голос против.

paparazzo 15.06.2018 17:35

Вы можете реализовать эту рассылку с помощью небольшой "магии" от dynamic:

abstract class Character {
    public void Attack(Character c) {
        ((dynamic)this).DoAttack((dynamic)c);
    }
}
class A : Character {
    public void DoAttack(A a) { Console.WriteLine("A attacks A"); }
    public void DoAttack(B b) { Console.WriteLine("A attacks B"); }
    public void DoAttack(C c) { Console.WriteLine("A attacks C"); }
}
class B : Character {
    public void DoAttack(A a) { Console.WriteLine("B attacks A"); }
    public void DoAttack(B b) { Console.WriteLine("B attacks B"); }
    public void DoAttack(C c) { Console.WriteLine("B attacks C"); }
}
class C : Character {
    public void DoAttack(A a) { Console.WriteLine("C attacks A"); }
    public void DoAttack(B b) { Console.WriteLine("C attacks B"); }
    public void DoAttack(C c) { Console.WriteLine("C attacks C"); }
}

Обратите внимание на преобразование this и c в dynamic, это то, что позволяет среде выполнения находить соответствующее переопределение DoAttack, не полагаясь на структуру наследования.

Демо.

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

Недостатком этого подхода является то, что он не имеет статической типизации, что означает, что он будет компилироваться даже при отсутствии метода, который обрабатывает желаемое взаимодействие. Вы можете смягчить эту проблему, предоставив реализацию DoAttack(Character c) "по умолчанию" в самом классе Character.

Если вы хотите избежать решения динамической диспетчеризации, представленного dasblinkenlight, вы можете сделать это, реализовав шаблон Visitor. Нам нужно добавить новый метод в ваш абстрактный класс.

public abstract class Character
{
    public int HP {get;set;}
    public abstract void Attack(Character t);
    public abstract void Accept(ICharacterVisitor visitor);
}

А также создадим наш интерфейс ICharacterVisitor.

public interface ICharacterVisitor {
    void Visit(A a);
    void Visit(B b);
    void Visit(C c);
}

Давайте продолжим и определим некоторые правила для атаки как персонаж A в терминах ICharacterVisitor.

public class AttackVisitorA : ICharacterVisitor
{
    private int _baseDamage;
    public AttackVisitorA(int baseDamage){
        _baseDamage = baseDamage;
    }
    public void Visit(A a)
    {
        // A does normal damage to A.
        a.HP -= _baseDamage;
    }

    public void Visit(B b)
    {
        // A does double damage to B.
        b.HP -= (_baseDamage * 2);
    }

    public void Visit(C c)
    {
        // A does half damage to C.
        c.HP -= (_baseDamage / 2);
    }
}

Теперь мы можем реализовать наши символы A, B и C. Я оставил реализацию атаки на B и C в качестве упражнения для читателя.

public class A : Character
{
    public override void Accept(ICharacterVisitor visitor)
    {
        visitor.Visit(this);
    }

    public override void Attack(Character t)
    {
        var damage = 15;
        t.Accept(new AttackVisitorA(damage));
    }
}

public class B : Character
{
    public override void Accept(ICharacterVisitor visitor)
    {
        visitor.Visit(this);
    }

    public override void Attack(Character t)
    {
        throw new NotImplementedException();
    }
}

public class C : Character
{
    public override void Accept(ICharacterVisitor visitor)
    {
        visitor.Visit(this);
    }

    public override void Attack(Character t)
    {
        throw new NotImplementedException();
    }
}

Теперь мы можем довольно просто проводить атаки персонажей на других персонажей:

void Main()
{
    var a = new A { HP = 100 };
    var b = new B { HP = 100 };
    var c = new C { HP = 100 };
    a.Attack(a); // stop hitting yourself
    a.Attack(b);
    a.Attack(c);
    Console.WriteLine(a.HP); // 85
    Console.WriteLine(b.HP); // 70
    Console.WriteLine(c.HP); // 93
}

Логика эффективно отделена от иерархии объектов. Вы можете реализовать DefaultAttackVisitor, который проходит через стандартные правила, или иметь AttackVisitor для каждого типа персонажа, или, возможно, некоторые более сложные правила, основанные на оружии или заклинаниях. В любом случае у вас получился довольно чистый и легко заменяемый набор логики для разрешения атак между классами.

Реализация посетителя вместо использования Dynamic Dispatch предоставит вам немного больше безопасности типов во время компиляции. Если в какой-то момент вам нужно будет добавить новый тип символа, D, вы не сможете скомпилировать, не убедившись, что ваши реализации посетителя были обновлены для включения логики для D, как только вы реализуете метод D Accept.

При динамической отправке ваш метод провала будет задействован для любых вновь добавленных типов символов, если атакующий тип не имеет, например, добавленной перегрузки Attack(D d).

Тем не менее, динамическая отправка требует гораздо меньших затрат на реализацию, но вы отказываетесь от некоторой безопасности типов во время компиляции. Вам нужно будет оценить, какой компромисс более важен для вашего варианта использования.

Просто используйте уже встроенный в C# typeof(), вот пример кода.

public class Program
{
    public static void Main()
    {
        A a = new A();
        B b = new B();

        a.Attack(b);
        b.Attack(a);
        Console.WriteLine(typeof(A));
        Console.WriteLine(typeof(B));

        Console.WriteLine(typeof(A) == a.GetType());
        Console.WriteLine(typeof(B) == a.GetType());
        Console.ReadLine();
    }

    public abstract class Character
    {
        public abstract void Attack(Character c);
    }

    public class A : Character
    {
        public override void Attack(Character t)
        {
            if (t.GetType() == typeof(A))
            {
                Console.WriteLine("A attacked type A");
                return;
            }

            if (t.GetType() == typeof(B))
            {
                Console.WriteLine("A attacked type B");
                return;
            }

            if (t.GetType() == typeof(C))
            {
                Console.WriteLine("A attacked type C");
                return;
            }

        }
    }

    public class B : Character
    {
        public override void Attack(Character t)
        {
            if (t.GetType() == typeof(A))
            {
                Console.WriteLine("B attacked type A");
                return;
            }

            if (t.GetType() == typeof(B))
            {
                Console.WriteLine("B attacked type B");
                return;
            }

            if (t.GetType() == typeof(C))
            {
                Console.WriteLine("B attacked type C");
                return;
            }


        }

    }
    public class C : Character
    {
        public override void Attack(Character t)
        {
            if (t.GetType() == typeof(A))
            {
                Console.WriteLine("C attacked type A");
                return;
            }

            if (t.GetType() == typeof(B))
            {
                Console.WriteLine("C attacked type B");
                return;
            }

            if (t.GetType() == typeof(C))
            {
                Console.WriteLine("C attacked type C");
                return;
            }

        }
    }


}

Я считаю, что OP старался избежать именно такой реализации.

Jonathon Chase 13.06.2018 23:57

@JonathonChase Я думаю, из вопроса, и я могу ошибаться, что OP искал способ реализовать одно переопределение для каждого класса, которое могло бы делать разные вещи в зависимости от класса, который подвергался атаке, и злоумышленника. Скажем, если класс C атакует B, он нанесет 50 повреждений, но если A атакует B, он нанесет только 25 и так далее. По крайней мере, так я интерпретировал это после того, как прочитал и увидел желаемый тип кода из второго примера.

thatguy 14.06.2018 00:03
Ответ принят как подходящий

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

Я просто рассмотрю наиболее распространенную процедуру и напишу ее как подходящую для всех классов, унаследованных от абстрактного класса. Затем я могу указать методы для тех, у кого есть конкретный способ работы с атакой. Итак, предположим, что когда персонаж класса A имеет один и тот же способ атаковать символ класса A или B, но атакует по-разному символы типа C. Используя полиморфизм, нам просто нужно сделать:

public abstract class Character
{
    public abstract void Attack(Character t);
}

public class A :Character
{
        public override void Attack(Character t)
        {
         /*instructions for the attacking of a character of type A or B*/
        }

        public void Attack(C z)
        {
         /*instructions for the attacking of a type C character*/
        }
}

Общий метод (тот, который принимает символьный параметр) будет использоваться для любого объекта символьного типа (или наследуемого от символа), кроме того, если это объект / символ типа C; в этом случае он будет отправлен методу Attack, который принимает объект / символ типа C в качестве параметра.

И если все типы деревьев атакованы по-разному, мы можем просто произвольно выбрать один из них для реализации, следуя сигнатуре абстрактного метода:

public abstract class Character
{
    public abstract void Attack(Character t);
}

public class A :Character
{
        public override void Attack(Character t)
        {
         /*instructions for the attacking of a character of type A*/
        }

        public void Attack(B y)
        {
         /*instructions for the attacking of a type B character*/
        }

        public void Attack(C z)
        {
         /*instructions for the attacking of a type C character*/
        }
}

Ну, я не знаю, слишком ли это приглушает, но это работает. Использование dynamic, интерфейса или универсальных типов, вероятно, хорошо, но на самом деле здесь особо нечего было делать.

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

Любому читателю-новичку, который я бы порекомендовал взглянуть на элегантное решение близкой проблемы (которая действительно существует), представленное в разделе 3 статьи, написанной Эриком Липпертом в его блоге, которую можно найти по этой ссылке: https://ericlippert.com/2015/04/27/wizards-and-warriors-part-one/

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