Что означает модификатор virtual
При наследовании нередко возникает необходимость изменить в классе-наследнике функционал метода, который был унаследован от базового класса. В этом случае класс-наследник может переопределять методы и свойства базового класса.
Те методы и свойства, которые мы хотим сделать доступными для переопределения, в базовом классе помечается модификатором virtual . Такие методы и свойства называют виртуальными.
А чтобы переопределить метод в классе-наследнике, этот метод определяется с модификатором override . Переопределенный метод в классе-наследнике должен иметь тот же набор параметров, что и виртуальный метод в базовом классе.
Например, рассмотрим следующие классы:
class Person < public string Name < get; set; >public Person(string name) < Name = name; >public virtual void Print() < Console.WriteLine(Name); >> class Employee : Person < public string Company < get; set; >public Employee(string name, string company) : base(name) < Company = company; >>
Здесь класс Person представляет человека. Класс Employee наследуется от Person и представляет сотруднника предприятия. Этот класс кроме унаследованного свойства Name имеет еще одно свойство — Company.
Чтобы сделать метод Print доступным для переопределения, этот метод определен с модификатором virtual . Поэтому мы можем переопределить этот метод, но можем и не переопределять. Допустим, нас устраивает реализация метода из базового класса. В этом случае объекты Employee будут использовать реализацию метода Print из класса Person:
Person bob = new Person("Bob"); bob.Print(); // вызов метода Print из класса Person Employee tom = new Employee("Tom", "Microsoft"); tom.Print(); // вызов метода Print из класса Person
Bob Tom
Но также можем переопределить виртуальный метод. Для этого в классе-наследнике определяется метод с модификатором override , который имеет то же самое имя и набор параметров:
class Employee : Person < public string Company < get; set; >public Employee(string name, string company) : base(name) < Company = company; >public override void Print() < Console.WriteLine($"работает в "); > >
Возьмем те же самые объекты:
Person bob = new Person("Bob"); bob.Print(); // вызов метода Print из класса Person Employee tom = new Employee("Tom", "Microsoft"); tom.Print(); // вызов метода Print из класса Employee
Bob Tom работает в Microsoft
Виртуальные методы базового класса определяют интерфейс всей иерархии, то есть в любом производном классе, который не является прямым наследником от базового класса, можно переопределить виртуальные методы. Например, мы можем определить класс Manager, который будет производным от Employee, и в нем также переопределить метод Print.
При переопределении виртуальных методов следует учитывать ряд ограничений:
- Виртуальный и переопределенный методы должны иметь один и тот же модификатор доступа. То есть если виртуальный метод определен с помощью модификатора public, то и переопредленный метод также должен иметь модификатор public.
- Нельзя переопределить или объявить виртуальным статический метод.
Ключевое слово base
Кроме конструкторов, мы можем обратиться с помощью ключевого слова base к другим членам базового класса. В нашем случае вызов base.Print(); будет обращением к методу Print() в классе Person:
class Employee : Person < public string Company < get; set; >public Employee(string name, string company) :base(name) < Company = company; >public override void Print() < base.Print(); Console.WriteLine($"работает в "); > >
Переопределение свойств
Также как и методы, можно переопределять свойства:
class Person < int age = 1; public virtual int Age < get =>age; set < if(value >0 && value < 110) age = value; >> public string Name < get; set; >public Person(string name) < Name = name; >public virtual void Print() => Console.WriteLine(Name); > class Employee : Person < public override int Age < get =>base.Age; set < if (value >17 && value < 110) base.Age = value; >> public string Company < get; set; >public Employee(string name, string company) : base(name) < Company = company; base.Age = 18; // возраст для работников по умолчанию >>
В данном случае в классе Person определено виртуальное свойство Age, которое устанавливает значение, если оно больше 0 и меньше 110. В классе Employee это свойство переопределено — возраст работника должен быть не меньше 18.
Person bob = new Person("Bob"); Console.WriteLine(bob.Age); // 1 Employee tom = new Employee("Tom", "Microsoft"); Console.WriteLine(tom.Age); // 18 tom.Age = 22; Console.WriteLine(tom.Age); // 22 tom.Age = 12; Console.WriteLine(tom.Age); // 22
Запрет переопределения методов
Также можно запретить переопределение методов и свойств. В этом случае их надо объявлять с модификатором sealed :
class Employee : Person < public string Company < get; set; >public Employee(string name, string company) : base(name) < Company = company; >public override sealed void Print() < Console.WriteLine($"работает в "); > >
При создании методов с модификатором sealed надо учитывать, что sealed применяется в паре с override, то есть только в переопределяемых методах.
И в этом случае мы не сможем переопределить метод Print в классе, унаследованном от Employee.
Зачем вообще нужен модификатор virtual в c#?
Ну в общем такой вопрос, зачем нужен модификатор virtual? В чем заключается его особенность, необходимость в использовании? Почему нельзя просто переопределять не виртуальные методы? Спасибо заранее!
Отслеживать
задан 25 фев 2016 в 14:46
EmielRegis EmielRegis
33 5 5 бронзовых знаков
«Вот! Вот вопрос. Вы же наповал меня бьёте этим вопросом.» Братья Вайнеры
– user176262
25 фев 2016 в 14:48
25 фев 2016 в 15:13
2 ответа 2
Сортировка: Сброс на вариант по умолчанию
Для реализации полиморфизма. Допустим мы имеем два класса:
public class Parent < public void Func() < . >> public class Child : Parent < public void Func() < . >>
И имеем использование
Paren obj = new Child(); obj.Func();
Если мы не имеем модификатора virtual (в языке), то мы ставим перед компилятором неразрешимую задачу — какую из функций (Parent.Func() или Child.Func()) использовать. При наличие же такого модификатора в языке его наличие перед определением функции укажет компилятору использовать вариант из наследника (полиморфизм), а отсутствие вариант из родительского класса.
Но почему бы не использовать всегда какой то один вариант? Тогда virtual не нужен. Но тогда для доступа ко второму варианту придется использовать какие то дополнительные средства, например приведение типов, ссылки и т.д. Либо существенно ограничивать разработчиков в средствах выражения.
Виртуальные методы, свойства и индексаторы
Полиморфизм предоставляет подклассу способ определения собственной версии метода, определенного в его базовом классе, с использованием процесса, который называется . Чтобы пересмотреть текущий дизайн, нужно понять значение ключевых слов virtual и override.
Виртуальным называется такой метод, который объявляется как virtual в базовом классе. Виртуальный метод отличается тем, что он может быть переопределен в одном или нескольких производных классах. Следовательно, у каждого производного класса может быть свой вариант виртуального метода. Кроме того, виртуальные методы интересны тем, что именно происходит при их вызове по ссылке на базовый класс. В этом случае средствами языка C# определяется именно тот вариант виртуального метода, который следует вызывать, исходя из типа объекта, к которому происходит обращение по ссылке, причем это делается во время выполнения. Поэтому при ссылке на разные типы объектов выполняются разные варианты виртуального метода. Иными словами, вариант выполняемого виртуального метода выбирается по типу объекта, а не по типу ссылки на этот объект.
Так, если базовый класс содержит виртуальный метод и от него получены производные классы, то при обращении к разным типам объектов по ссылке на базовый класс выполняются разные варианты этого виртуального метода.
Метод объявляется как виртуальный в базовом классе с помощью ключевого слова virtual, указываемого перед его именем. Когда же виртуальный метод переопределяется в производном классе, то для этого используется модификатор override. А сам процесс повторного определения виртуального метода в производном классе называется переопределением метода. При переопределении метода — имя, возвращаемый тип и сигнатура переопределяющего метода должны быть точно такими же, как и у того виртуального метода, который переопределяется. Кроме того, виртуальный метод не может быть объявлен как static или abstract.
Переопределение метода служит основанием для воплощения одного из самых эффективных в C# принципов: динамической диспетчеризации методов, которая представляет собой механизм разрешения вызова во время выполнения, а не компиляции. Значение динамической диспетчеризации методов состоит в том, что именно благодаря ей в C# реализуется динамический полиморфизм.
Если при наличии многоуровневой иерархии виртуальный метод не переопределяется в производном классе, то выполняется ближайший его вариант, обнаруживаемый вверх по иерархии.
И еще одно замечание: свойства также подлежат модификации ключевым словом virtual и переопределению ключевым словом override. Это же относится и к индексаторам.
Давайте рассмотрим пример использования виртуальных методов, свойств и индексаторов:
// Реализуем класс содержащий информацию о шрифтах // и использующий виртуальные методы, свойства и индексаторы using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 < // Базовый класс class Font < string TypeFont; short FontSize; public Font() < TypeFont = "Arial"; FontSize = 12; >public Font(string TypeFont, short FontSize) < this.TypeFont = TypeFont; this.FontSize = FontSize; >public string typeFont < get < return TypeFont; >set < TypeFont = value; >> public short fontSize < get < return FontSize; >set < FontSize = value; >> // Создаем виртуальный метод public virtual string FontInfo(Font obj) < string s = "Информация о шрифте: \n------------------\n\n" + "Тип шрифта: " + typeFont + "\nРазмер шрифта: " + fontSize + "\n"; return s; >> // Производный класс 1 уровня class ColorFont : Font < byte Color; public ColorFont(byte Color, string TypeFont, short FontSize) : base(TypeFont, FontSize) < this.Color = Color; >// Переопределение для виртуального метода public override string FontInfo(Font obj) < // Используется ссылка на метод, определенный в базовом классе Font return base.FontInfo(obj) + "Цвет шрифта: " + Color + "\n"; >// Создадим виртуальное свойство public virtual byte color < set < Color = value; >get < return Color; >> > // Производный класс 2 уровня class GradientColorFont : ColorFont < char TypeGradient; public GradientColorFont(char TypeGradient, byte Color, string TypeFont, short FontSize) : base(Color, TypeFont, FontSize) < this.TypeGradient = TypeGradient; >// Опять переопределяем виртуальный метод public override string FontInfo(Font obj) < // Используется ссылка на метод определенный в производном классе FontColor return base.FontInfo(obj) + "Тип градиента: " + TypeGradient + "\n\n"; >// Переопределим виртуальное свойство public override byte color < get < return base.color; >set < if (value < 10) base.color = 0; else base.color = (byte)(value - 0x0A); >> > // Еще один производный класс 1 уровня class FontStyle : Font < string style; public FontStyle(string style, string TypeFont, short FontSize) : base (TypeFont, FontSize) < this.style = style; >// Данный класс не переопределяет виртуальный метод // поэтому при вызове метода FontInfo () // вызывается метод созданный в базовом классе > class Program < static void Main() < ColorFont font1 = new ColorFont(Color: 0xCF, TypeFont: "MS Trebuchet", FontSize: 16); Console.WriteLine(font1.FontInfo(font1)); GradientColorFont font2 = new GradientColorFont(Color: 0xFF, TypeFont: "Times New Roman", FontSize: 10, TypeGradient: 'R'); Console.WriteLine(font2.FontInfo(font2)); font2.color = 0x2F; Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("Видоизмененный цвет font2"); Console.ForegroundColor = ConsoleColor.Gray; Console.WriteLine(font2.FontInfo(font2)); FontStyle font3 = new FontStyle(style: "oblique", TypeFont: "Calibri", FontSize: 16); Console.WriteLine(font3.FontInfo(font3)); Console.ReadLine(); >> >
Давайте рассмотрим данный пример более подробно. В базовом классе Font инкапсулируется виртуальный метод FontInfo (), возвращающий информацию о шрифте. В производном классе FontColor данный метод переопределяется с помощью ключевого слова override, поэтому при создании экземпляра данного класса и вызова метода FontInfo() в исходную информацию возвращается помимо первоначальных данных еще и цвет шрифта. Затем данный метод вновь переопределяется в классе GradientColorFont, унаследованном от класса FontColor. Обратите внимание, что здесь переопределяется не исходный метод базового класса Font, а уже переопределенный метод класса FontColor. В этом и заключается принцип динамического полиморфизма!
Так же обратите внимание, что в данном примере используется виртуальное свойство color, принцип использования которого аналогичен использованию виртуального метода.
Урок №163. Виртуальные функции и Полиморфизм
На предыдущем уроке мы рассматривали ряд примеров, в которых использование указателей или ссылок родительского класса упрощало логику и уменьшало количество кода.
Оглавление:
- Виртуальные функции и Полиморфизм
- Более сложный пример
- Использование ключевого слова virtual
- Типы возврата виртуальных функций
- Не вызывайте виртуальные функции в теле конструкторов или деструкторов
- Недостаток виртуальных функций
- Тест
Виртуальные функции и Полиморфизм
Тем не менее, мы сталкивались с проблемой, когда родительский указатель или ссылка вызывали только родительские методы, а не дочерние. Например:
class Parent
const char * getName ( ) < return "Parent" ; >
class Child : public Parent
const char * getName ( ) < return "Child" ; >
Child child ;
std :: cout << "rParent is a " << rParent . getName ( ) << '\n' ;
rParent is a Parent
Поскольку rParent является ссылкой класса Parent, то вызывается Parent::getName(), хотя фактически мы ссылаемся на часть Parent объекта child .
На этом уроке мы рассмотрим, как можно решить эту проблему с помощью виртуальных функций.
Виртуальная функция в языке С++ — это особый тип функции, которая, при её вызове, выполняет «наиболее» дочерний метод, который существует между родительским и дочерними классами. Это свойство еще известно, как полиморфизм. Дочерний метод вызывается тогда, когда совпадает сигнатура (имя, типы параметров и является ли метод константным) и тип возврата дочернего метода с сигнатурой и типом возврата метода родительского класса. Такие методы называются переопределениями (или «переопределенными методами»).
Чтобы сделать функцию виртуальной, нужно просто указать ключевое слово virtual перед объявлением функции. Например:
class Parent
virtual const char * getName ( ) < return "Parent" ; >// добавили ключевое слово virtual
class Child : public Parent
virtual const char * getName ( ) < return "Child" ; >
Child child ;
std :: cout << "rParent is a " << rParent . getName ( ) << '\n' ;
rParent is a Child
Поскольку rParent является ссылкой на родительскую часть объекта child , то, обычно, при обработке rParent.getName() вызывался бы Parent::getName(). Тем не менее, поскольку Parent::getName() является виртуальной функцией, то компилятор понимает, что нужно посмотреть, есть ли переопределения этого метода в дочерних классах. И компилятор находит Child::getName()!
Рассмотрим пример посложнее:
virtual const char * getName ( ) < return "A" ; >
class B : public A
virtual const char * getName ( ) < return "B" ; >
class C : public B
virtual const char * getName ( ) < return "C" ; >
class D : public C
virtual const char * getName ( ) < return "D" ; >
std :: cout << "rParent is a " << rParent . getName ( ) << '\n' ;
Как вы думаете, какой результат выполнения этой программы?
Рассмотрим всё по порядку:
Сначала создается объект c класса C.
rParent — это ссылка класса A, которой мы указываем ссылаться на часть A объекта c .
Затем вызывается метод rParent.getName() .
Вызов rParent.GetName() приводит к вызову A::getName(). Однако, поскольку A::getName() является виртуальной функцией, то компилятор ищет «наиболее» дочерний метод между A и C. В этом случае — это C::getName().
Обратите внимание, компилятор не будет вызывать D::getName(), поскольку наш исходный объект был класса C, а не класса D, поэтому рассматриваются методы только между классами A и C.
Результат выполнения программы:
Более сложный пример
Рассмотрим класс Animal из предыдущего урока, добавив тестовый код:
class Animal
std :: string m_name ;
// Мы делаем этот конструктор protected так как не хотим, чтобы пользователи имели возможность создавать объекты класса Animal напрямую,
// но хотим, чтобы в дочерних классах доступ был открыт
Animal ( std :: string name )
: m_name ( name )
std :: string getName ( ) < return m_name ; >
const char * speak ( ) < return ". " ; >
class Cat : public Animal
Cat ( std :: string name )
: Animal ( name )
const char * speak ( ) < return "Meow" ; >
class Dog : public Animal
Dog ( std :: string name )
: Animal ( name )
const char * speak ( ) < return "Woof" ; >
void report ( Animal &animal )
std :: cout << animal . getName ( ) << " says " << animal . speak ( ) << '\n' ;
Cat cat ( "Matros" ) ;
Dog dog ( "Barsik" ) ;
report ( cat ) ;
report ( dog ) ;
Результат выполнения программы:
Matros says .
Barsik says .
А теперь рассмотрим тот же класс, но сделав метод speak() виртуальным:
class Animal
std :: string m_name ;
// Мы делаем этот конструктор protected так как не хотим, чтобы пользователи имели возможность создавать объекты класса Animal напрямую,
// но хотим, чтобы в дочерних классах доступ был открыт
Animal ( std :: string name )
: m_name ( name )
std :: string getName ( ) < return m_name ; >
virtual const char * speak ( ) < return ". " ; >
class Cat : public Animal
Cat ( std :: string name )
: Animal ( name )
virtual const char * speak ( ) < return "Meow" ; >
class Dog : public Animal
Dog ( std :: string name )
: Animal ( name )
virtual const char * speak ( ) < return "Woof" ; >
void report ( Animal &animal )
std :: cout << animal . getName ( ) << " says " << animal . speak ( ) << '\n' ;
Cat cat ( "Matros" ) ;
Dog dog ( "Barsik" ) ;
report ( cat ) ;
report ( dog ) ;
Результат выполнения программы:
Matros says Meow
Barsik says Woof
При обработке animal.speak() , компилятор видит, что метод Animal::speak() является виртуальной функцией. Когда animal ссылается на часть Animal объекта cat , то компилятор просматривает все классы между Animal и Cat, чтобы найти наиболее дочерний метод speak(). И находит Cat::speak(). В случае, когда animal ссылается на часть Animal объекта dog , компилятор находит Dog::speak().
Обратите внимание, мы не сделали Animal::GetName() виртуальной функцией. Это из-за того, что GetName() никогда не переопределяется ни в одном из дочерних классов, поэтому в этом нет необходимости.
Аналогично со следующим примером с массивом животных:
Cat matros ( «Matros» ) , ivan ( «Ivan» ) , martun ( «Martun» ) ;
Dog barsik ( «Barsik» ) , tolik ( «Tolik» ) , tyzik ( «Tyzik» ) ;
// Создаем массив указателей на наши объекты Cat и Dog
Animal * animals [ ] = < &matros , &barsik , &ivan , &tolik , &martun , &tyzik >;
for ( int iii = 0 ; iii < 6 ; ++ iii )
std :: cout << animals [ iii ] ->getName ( ) << " says " << animals [ iii ] ->speak ( ) << '\n' ;<>
Matros says Meow
Barsik says Woof
Ivan says Meow
Tolik says Woof
Martun says Meow
Tyzik says Woof
Несмотря на то, что эти два примера используют только классы Cat и Dog, любые другие дочерние классы также будут работать с нашей функцией report() и с массивом животных, без внесения дополнительных модификаций! Это, пожалуй, самое большое преимущество виртуальных функций — возможность структурировать код таким образом, чтобы новые дочерние классы автоматически работали со старым кодом, без необходимости внесения изменений со стороны программиста!
Предупреждение: Сигнатура виртуального метода дочернего класса должна полностью соответствовать сигнатуре виртуального метода родительского класса. Если у дочернего метода будет другой тип параметров, нежели у родительского, то вызываться этот метод не будет.
Использование ключевого слова virtual
Если функция отмечена как виртуальная, то все соответствующие переопределения тоже считаются виртуальными, даже если возле них явно не указано ключевое слова virtual. Однако, наличие ключевого слова virtual возле методов дочерних классов послужит полезным напоминанием о том, что эти методы являются виртуальными, а не обычными. Следовательно, полезно указывать ключевое слово virtual возле переопределений в дочерних классах, даже если это не является строго необходимым.
Типы возврата виртуальных функций
Типы возврата виртуальной функции и её переопределений должны совпадать. Рассмотрим следующий пример:
class Parent
virtual int getValue ( ) < return 7 ; >
class Child : public Parent
virtual double getValue ( ) < return 9.68 ; >
В этом случае Child::getValue() не считается подходящим переопределением для Parent::getValue(), так как типы возвратов разные (метод Child::getValue() считается полностью отдельной функцией).
Не вызывайте виртуальные функции в теле конструкторов или деструкторов
Вот еще одна ловушка для новичков. Вы не должны вызывать виртуальные функции в теле конструкторов или деструкторов. Почему?
Помните, что при создании объекта класса Child сначала создается родительская часть этого объекта, а затем уже дочерняя? Если вы будете вызывать виртуальную функцию из конструктора класса Parent при том, что дочерняя часть создаваемого объекта еще не была создана, то вызвать дочерний метод вместо родительского будет невозможно, так как объект child для работы с методом класса Child еще не будет создан. В таких случаях, в языке C++ будет вызываться родительская версия метода.
Аналогичная проблема существует и с деструкторами. Если вы вызываете виртуальную функцию в теле деструктора класса Parent, то всегда будет вызываться метод класса Parent, так как дочерняя часть объекта уже будет уничтожена.
Правило: Никогда не вызывайте виртуальные функции в теле конструкторов или деструкторов.
Недостаток виртуальных функций
«Если всё так хорошо с виртуальными функциями, то почему бы не сделать все методы виртуальными?» — спросите Вы. Ответ: «Это неэффективно!». Обработка и выполнение вызова виртуального метода занимает больше времени, чем обработка и выполнение вызова обычного метода. Кроме того, компилятор также должен выделять один дополнительный указатель для каждого объекта класса, который имеет одну или несколько виртуальных функций.
Тест
Какой результат выполнения следующих программ? Не нужно запускать/выполнять следующий код, вы должны определить результат, без помощи своих IDE.