Полиморфизм (Руководство по программированию на C#)

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

  • Во время выполнения объекты производного класса могут обрабатываться как объекты базового класса в таких местах, как параметры метода и коллекции или массивы. Когда это происходит, объявленный тип объекта перестает соответствовать своему типу во время выполнения.

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

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

  1. Создать иерархию классов, в которой каждый отдельный класс фигур является производным из общего базового класса.

  2. Применить виртуальный метод для вызова соответствующего метода на любой производный класс через единый вызов в метод базового класса.

Для начала создайте базовый класс с именем Shape и производные классы, например Rectangle, Circle и Triangle. Присвойте классу Shape виртуальный метод с именем Draw и переопределите его в каждом производном классе для рисования конкретной фигуры, которую этот класс представляет. Создайте объект List<Shape> и добавьте в него круг, треугольник и прямоугольник. Для обновления поверхности рисования используйте цикл foreach, чтобы выполнить итерацию списка и вызвать метод Draw на каждом объекте Shape в списке. Несмотря на то, что каждый объект в списке имеет объявленный тип Shape, вызывать будет тип во время выполнения (переопределенная версия метода в каждом производном классе).

public class Shape
{
    // A few example members 
    public int X { get; private set; }
    public int Y { get; private set; }
    public int Height { get; set; }
    public int Width { get; set; }

    // Virtual method 
    public virtual void Draw()
    {
        Console.WriteLine("Performing base class drawing tasks");
    }
}

class Circle : Shape
{
    public override void Draw()
    {
        // Code to draw a circle...
        Console.WriteLine("Drawing a circle");
        base.Draw();
    }
}
class Rectangle : Shape
{
    public override void Draw()
    {
        // Code to draw a rectangle...
        Console.WriteLine("Drawing a rectangle");
        base.Draw();
    }
}
class Triangle : Shape
{
    public override void Draw()
    {
        // Code to draw a triangle...
        Console.WriteLine("Drawing a triangle");
        base.Draw();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Polymorphism at work #1: a Rectangle, Triangle and Circle 
        // can all be used whereever a Shape is expected. No cast is 
        // required because an implicit conversion exists from a derived  
        // class to its base class.
        System.Collections.Generic.List<Shape> shapes = new System.Collections.Generic.List<Shape>();
        shapes.Add(new Rectangle());
        shapes.Add(new Triangle());
        shapes.Add(new Circle());

        // Polymorphism at work #2: the virtual method Draw is 
        // invoked on each of the derived classes, not the base class. 
        foreach (Shape s in shapes)
        {
            s.Draw();
        }

        // Keep the console open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }

}

/* Output:
    Drawing a rectangle
    Performing base class drawing tasks
    Drawing a triangle
    Performing base class drawing tasks
    Drawing a circle
    Performing base class drawing tasks
 */

В C# каждый тип является полиморфным, так как все типы, включая пользовательские, наследуют Object.

Обзор полиморфизма

Виртуальные члены

Если производный класс наследуется из базового, он получает все методы, поля, свойства и события базового класса. Разработчик производного класса может выбрать следующее:

  • переопределение виртуальных членов в базовом классе;

  • наследование метода ближайшего базового класса без переопределения;

  • определение новой, невиртуальной реализации тех членов, которые скрывают реализации базового класса.

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

public class BaseClass
{
    public virtual void DoWork() { }
    public virtual int WorkProperty
    {
        get { return 0; }
    }
}
public class DerivedClass : BaseClass
{
    public override void DoWork() { }
    public override int WorkProperty
    {
        get { return 0; }
    }
}

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

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork();  // Also calls the new method.

Виртуальные методы и свойства позволяют производным классам расширять базовый класс без необходимости использовать реализацию базового класса метода. Для получения дополнительной информации см. Практическое руководство. Управление версиями с помощью ключевых слов "Override" и "New" (Руководство по программированию в C#). Еще одну возможность определения метода или набора методов, реализация которых оставлена производным классам, дает интерфейс. Для получения дополнительной информации см. Интерфейсы (Руководство по программированию в C#).

Сокрытие членов базового класса новыми членами

Если вам нужно, чтобы производный член имел такое же имя, как и член в базовом классе, но вы не хотите, чтобы он участвовал в виртуальном вызове, используйте новое ключевое слово. Ключевое слово new вставляется перед типом возвращаемого значения замещаемого члена класса. Примером является следующий код:

public class BaseClass
{
    public void DoWork() { WorkField++; }
    public int WorkField;
    public int WorkProperty
    {
        get { return 0; }
    }
}

public class DerivedClass : BaseClass
{
    public new void DoWork() { WorkField++; }
    public new int WorkField;
    public new int WorkProperty
    {
        get { return 0; }
    }
}

Доступ к скрытым членам базового класса можно по-прежнему осуществлять из клиентского кода приведением экземпляра производного класса к экземпляру базового класса. Например:

DerivedClass B = new DerivedClass();
B.DoWork();  // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork();  // Calls the old method.

Защита виртуальных членов от переопределения производными классами

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

public class A
{
    public virtual void DoWork() { }
}
public class B : A
{
    public override void DoWork() { }
}

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

public class C : B
{
    public sealed override void DoWork() { }
}

В предыдущем примере метод DoWork более не является виртуальным ни для одного класса, произведенного из класса С. Он по-прежнему виртуален для экземпляров класса С, даже если они приводятся к типу В или типу А. Запечатанные методы можно заменить производными классами с помощью ключевого слова new, как показано в следующем примере.

public class D : C
{
    public new void DoWork() { }
}

В этом случае, если DoWork вызывается на D с помощью переменной типа D, вызывается новый DoWork. Если переменная типа C, B или A используется для доступа к экземпляру D, вызов DoWork будет выполняться по правилам виртуального наследования и направлять эти вызовы на реализацию DoWork на классе C.

Доступ к виртуальным членам базового класса из производных классов

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

public class Base
{
    public virtual void DoWork() {/*...*/ }
}
public class Derived : Base
{
    public override void DoWork()
    {
        //Perform Derived's work here 
        //... 
        // Call DoWork on base class 
        base.DoWork();
    }
}

Для получения дополнительной информации см. базу.

Примечание

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

Содержание

См. также

Ссылки

Наследование (Руководство по программированию на C#)

Абстрактные и запечатанные классы и члены классов (Руководство по программированию на C#)

Методы (Руководство по программированию на C#)

События (Руководство по программированию в C#)

Свойства (Руководство по программированию в C#)

Индексаторы (Руководство по программированию в C#)

Типы (Руководство по программированию на C#)

Основные понятия

Руководство по программированию на C#

Руководство по программированию на C#