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

Обновлен: Июль 2008

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

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

  2. Базовые классы могут определять и реализовывать виртуальныеметоды, а производные классы могут переопределять их. Это означает, что они предоставляют свои собственные определение и реализацию. Во время выполнения, когда клиентский код вызывает метод, среда 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.

Общие сведения о полиморфизме

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

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

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

  • наследовать самый близкий метод базового класса без его переопределения,

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

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

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. Ключевое слово 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.

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

Виртуальные члены остаются виртуальными без каких-либо ограничений в отношении количества классов, объявленных между виртуальным членом и классом, в котором он был изначально объявлен. Если в классе A объявляется виртуальный член и класс B является производным от класса A, а класс C является производным от класса B, то класс C наследует виртуальный член и обеспечивает возможность его переопределения независимо от того, было ли в классе B объявлено переопределение этого члена. Ниже приведен пример.

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

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

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

В предыдущем примере метод DoWork больше не является виртуальным для любого производного от класса C класса. Он остается виртуальным для экземпляров класса C, даже если они приведены к типу B или A. Закрытые методы можно замещать в производных классах с помощью ключевого слова new, как показано в следующем примере:

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

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

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

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

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.

ms173152.alert_note(ru-ru,VS.90).gifПримечание.

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

Содержание

См. также

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

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

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

Ссылки

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

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

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

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

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

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

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

Журнал изменений

Дата

Журнал изменений

Причина

Июль 2008

Добавлены содержимое и новые примеры.

Улучшение информации.