Введение в ООП с примерами на C#. Часть третья. Практические аспекты использования полиморфизма

Рассказывает Akhil Mittal


Введение

Раньше в этой серии мы говорили о полиморфизме и наследовании.

В этой статье мы опять будем говорить о полиморфизме, но в этот раз сосредоточимся именно на практических нюансах, а не на теории. Если вы овладеете технологией, описанной в этой статье, то считайте, что изучили 50% ООП.

Ключевые слова New и Override в C#

Для начала создадим новое консольное приложение и два класса в нём:

public class ClassA
{
    public void AAA()
    {
        Console.WriteLine("ClassA AAA");
    }

    public void BBB()
    {
        Console.WriteLine("ClassA BBB");
    }

    public void CCC()
    {
        Console.WriteLine("ClassA CCC");
    }
}
public class ClassB : ClassA
{
    public void AAA()
    {
        Console.WriteLine("ClassB AAA");
    }

    public void BBB()
    {
        Console.WriteLine("ClassB BBB");
    }

    public void CCC()
    {
        Console.WriteLine("ClassB CCC");
    }
}

Мы видим, что эти классы содержат три метода с попарно одинаковыми именами. Теперь выполним следующий код из Program.cs:

/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
    private static void Main(string[] args)
    {
        ClassA x = new ClassA();
        ClassB y=new ClassB();
        ClassA z=new ClassB();

        x.AAA(); x.BBB(); x.CCC();
                Console.WriteLine("");
        y.AAA(); y.BBB();y.CCC();
                Console.WriteLine("");
        z.AAA(); z.BBB(); z.CCC();
    }
}

Жмём F5, т.е. выполняем код, и что мы видим?

ClassA AAA
ClassA BBB
ClassA CCC

ClassB AAA
ClassB BBB
ClassB CCC

ClassA AAA
ClassA BBB
ClassA CCC

Но кроме вывода, мы получили ещё и три предупреждения от компилятора:

‘InheritanceAndPolymorphism.ClassB.AAA()’ hides inherited member
‘InheritanceAndPolymorphism.ClassA.AAA()’. Use the new keyword if hiding was intended.

‘InheritanceAndPolymorphism.ClassB.BBB()’ hides inherited member
‘InheritanceAndPolymorphism.ClassA.BBB()’. Use the new keyword if hiding was intended.

‘InheritanceAndPolymorphism.ClassB.CCC()’ hides inherited member
‘InheritanceAndPolymorphism.ClassA.CCC()’. Use the new keyword if hiding was intended.

Что нужно запомнить: мы можем записать в переменную класса-родителя объект наследника, но не наоборот.

ClassA — родитель ClassB. То есть ClassB содержит то, что находится в ClassA и ещё что-то своё. В этом причина правила, которое мы записали выше: класс-родитель не содержит описания всех необходимых полей и методов класса-наследника, поэтому мы не можем использовать ClassA как ClassB.

Теперь посмотрим, что у нас происходит в коде. С x и y всё понятно: они объявлены и инициализированны одним и тем же типом. Рассмотрим подробнее z. Эта переменная типа ClassB, а её значение — объект типа ClassA, хотя в данном контексте нет никакой разницы, какого типа её значение, вывод всегда будет аналогичен выводу от y. Выбор метода по типу ссылки, а не по типу объекта — это стандартное поведение, когда явно не указан приоритет методов, о чём свидетельствуют warning’и. Как же описать требуемое поведение? Здесь нам как раз помогут ключевые слова new и override.

Давайте проведём эксперимент

Добавим к двум методам из ClassB ключевые слова new и override следующим образом:

public class ClassB : ClassA
{
    public override void AAA()
    {
        Console.WriteLine("ClassB AAA");
    }

    public new void BBB()
    {
        Console.WriteLine("ClassB BBB");
    }

    public void CCC()
    {
        Console.WriteLine("ClassB CCC");
    }
}

Если мы сейчас выполним Program.cs, то на выходе получим:

Error: ‘InheritanceAndPolymorphism.ClassB.AAA()’: cannot override inherited member
‘InheritanceAndPolymorphism.ClassA.AAA()’ because it is not marked virtual, abstract, or override

Ошибка возникает из-за того, что поля родителя не помечены ключевым словом virtual. Этот модификатор обозначает, что мы имеем право вызывать метод из дочернего класса или перезаписывать его. Добавим virtual ко всем методам ClassA:

public class ClassA
    {
        public virtual void AAA()
        {
            Console.WriteLine("ClassA AAA");
        }

        public virtual void BBB()
        {
            Console.WriteLine("ClassA BBB");
        }

        public virtual void CCC()
        {
            Console.WriteLine("ClassA CCC");
        }
    }

И снова запустим Program.cs:

ClassB AAA
ClassB BBB
ClassB CCC

ClassA AAA
ClassA BBB
ClassA CCC

ClassB AAA
ClassA BBB
ClassA CCC

Очевидно, что метод дочернего класса вызвался только там, где стоял модификатор override. В свзяи с чем делаем вывод: override значит, что помеченный метод — новая версия родительского и должен использоваться вместо него. И, напротив, new обозначает, что метод, хоть и случайно имеет такое же имя, является абсолютно по сути абсолютно другим, а значит, в нашем примере должен выполняться метод родительского класса. Если мы не пишем никакого модификатора, мы подразумеваем именно new.
Разберём подробнее логику C#. Когда вызывается метод какого-то объекта по ссылке, то в первую очередь он смотрит на тип ссылки. Если в этом классе обнаружен модификатор virtual, он начинает искать среди дочерних классов тип объекта, и, если встречает new, запускает последний override метод, который встретил (либо метод типа ссылки). Возможно, это не слишком понятно, обратимся к более сложному примеру.

Эксперимент с тремя классами

/// <summary>
/// ClassA, acting as a base class
/// </summary>
public class ClassA
{
    public  void AAA()
    {
        Console.WriteLine("ClassA AAA");
    }

    public virtual void BBB()
    {
        Console.WriteLine("ClassA BBB");
    }

    public virtual void CCC()
    {
        Console.WriteLine("ClassA CCC");
    }
}

/// <summary>
/// Class B, acting as a derived class
/// </summary>
public class ClassB : ClassA
{
    public virtual void AAA()
    {
        Console.WriteLine("ClassB AAA");
    }

    public new void BBB()
    {
        Console.WriteLine("ClassB BBB");
    }

    public override void CCC()
    {
        Console.WriteLine("ClassB CCC");
    }
}

/// <summary>
/// Class C, acting as a derived class
/// </summary>
public class ClassC : ClassB
{
    public override void AAA()
    {
        Console.WriteLine("ClassC AAA");
    }

    public void CCC()
    {
        Console.WriteLine("ClassC CCC");
    }
}
public class Program
{
    private static void Main(string[] args)
    {
        ClassA y = new ClassB();
        ClassA x = new ClassC();
        ClassB z = new ClassC();

        y.AAA(); y.BBB(); y.CCC();
        Console.WriteLine("");
        x.AAA(); x.BBB(); x.CCC();
        Console.WriteLine("");
        z.AAA(); z.BBB(); z.CCC();

        Console.ReadKey();
    }
}

Результатом такого эксперимента станет:

ClassA AAA
ClassA BBB
ClassB CCC

ClassA AAA
ClassA BBB
ClassB CCC

ClassC AAA
ClassB BBB
ClassB CCC

В первом случае мы имеем дело с типом ссылки ClassA и типом объекта ClassB. Компилятор действует вполне очевидно:

Во втором случае тип объекта у нас уже ClassC. Поскольку он наследуется от ClassA не напрямую, а через ClassB, наша диаграмма будет уже несколько сложнее.

В третьем случае мы имеем дело снова с двумя классами, ClassA мы просто игнорируем. Если учесть это, то вывод будет очевиден, но всё же вот схема:

Важным замечанием будет, что следующий код:

internal class A
{
    public virtual void X()
    {
    }
}

internal class B : A
{
    public new void X()
    {
    }
}

internal class C : B
{
    public override void X()
    {
    }
}

Выдаст ошибку:

Error: ‘InheritanceAndPolymorphism.C.X()’: cannot override inherited member
‘InheritanceAndPolymorphism.B.X()’ because it is not marked virtual, abstract, or override

Ведь из-за того, что в B метод помечен как new, он не наследует свойство virtual, а значит не может быть перезаписан с помощью override в C. Правильным вариантом было бы добавить к описанию метода в B ключевое слово virtual или изменить в C override на new, в зависимости от требуемого поведения.

Ключевое слово base

Теперь, когда мы разобрались с самой сложной частью, стоит напомнить про возможность вызова методов родительского класса из дочернего. Простой пример:

/// <summary>
    /// Class A
    /// </summary>
    public class ClassA
    {
        public virtual void XXX()
        {
            Console.WriteLine("ClassA XXX");
        }
    }

    /// <summary>
    /// ClassB
    /// </summary>
    public class ClassB:ClassA 
    {
        public override void XXX()
        {
            base.XXX();
            Console.WriteLine("ClassB XXX");
        }
    }

    /// <summary>
    /// Class C
    /// </summary>
    public class ClassC : ClassB
    {
        public override void XXX()
        {
            base.XXX();
            Console.WriteLine("ClassC XXX");
        }
    }

   /// <summary>
    /// Program: used to execute the method.
    /// Contains Main method.
    /// </summary>
    public class Program
    {
        private static void Main(string[] args)
        {
            ClassA a = new ClassB();
            a.XXX();
            Console.WriteLine("");
            ClassB b = new ClassC();
            b.XXX();
            Console.ReadKey();
        }
    }

Даст нам вывод:

ClassA XXX
ClassB XXX

ClassA XXX
ClassB XXX
ClassC XXX

В первом случае выполняется метод ClassB, который через base вызывает метод из ClassA. Во втором — XXX() из ClassC, который обращается к ClassB, а тот, в свою очередь, к ClassA.

Немного рекурсии

/// <summary>
    /// Class A
    /// </summary>
    public class ClassA
    {
        public virtual void XXX()
        {
            Console.WriteLine("ClassA XXX");
        }
    }

    /// <summary>
    /// ClassB
    /// </summary>
    public class ClassB:ClassA 
    {
        public override void XXX()
        {
            ((ClassA)this).XXX();
            Console.WriteLine("ClassB XXX");
        }
    }

   
    /// <summary>
    /// Program: used to execute the method.
    /// Contains Main method.
    /// </summary>
    public class Program
    {
        private static void Main(string[] args)
        {
            ClassA a = new ClassB();
            a.XXX();
           
        }
    }

В этом примере вызов ClassB.XXX() всегда будет приводить к созданию нового объекта типа ClassB в ссылке ClassA. Очевидно, что по такой ссылке снова будет вызван ClassB.XXX() и т.д. В данном случае выводом будет ошибка:

Error: {Cannot evaluate expression because the current thread is in a stack overflow state.}

В заключение

Подведём итоги:

  • В C# мы можем записать в переменную класса-родителя объект наследника, но не наоборот;
  • Модификатор override используется, чтобы указать на то, что должен вызваться метод именно дочернего класса;
  • Чтобы использовать модификаторы override и new, метод родительского класса должен быть помечен ключевым словом virtual;
  • Когда вызывается метод какого-то объекта по ссылке, то С# в первую очередь смотрит на тип ссылки. Если в этом классе обнаружен модификатор virtual, он начинает искать среди дочерних классов тип объекта, и, если встречает new, зупускает последний override метод, который встретил (либо метод типа ссылки).

Источник: «Diving in OOP (Day 3): Polymorphism and Inheritance (Dynamic Binding/Run Time Polymorphism)»