Классы и объекты C#: виртуальные методы и свойства
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
При использовании механизма наследовании в C# достаточно часто появляется необходимость изменить в классе-наследнике функционал метода, который был унаследован от предка (базового класса). Для этого, в C# класс-наследник может переопределять методы и свойства базового класса. Те методы или свойства, которые мы хотим сделать доступными для переопределения, в базовом классе помечается модификатором virtual . Такие методы и свойства обычно называют виртуальными.
Переопределение методов в C#
Чтобы переопределить метод в классе-наследнике, этот метод определяется с в классе модификатором override . В отличие от перегрузки, переопределенный метод в классе-наследнике должен иметь тот же набор параметров, что и виртуальный метод в базовом классе. Например, рассмотрим следующие классы:
class Person < public string Name < get; set; >public Person(string name) < Name = name; >public virtual void Display() < 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 . Чтобы сделать метод Display доступным для переопределения, этот метод определен с модификатором virtual . Поэтому мы можем переопределить этот метод (а можем и не переопределять — всё зависит от наших потребностей).
Допустим, что нас устраивает реализация метода из базового класса. В таком случае, объекты Employee будут использовать реализацию метода Display из класса Person :
Person p1 = new Person("Вася"); p1.Display(); // вызов метода Display из класса Person Employee e1 = new Employee("Билл", "Microsoft"); e1.Display(); // вызов метода Display из класса Person
В консоли мы увидим следующее:
Если же нас не устраивает функционал метода Display в родителе, то мы можем переопределить виртуальный метод. Для этого в классе-наследнике определяется метод с модификатором override , который имеет то же самое имя и набор параметров:
class Employee : Person < public string Company < get; set; >public Employee(string name, string company): base(name) < Company = company; >public override void Display() < Console.WriteLine($"работает в "); > >
Person p1 = new Person("Bill"); p1.Display(); // вызов метода Display из класса Person Employee p2 = new Employee("Tom", "Microsoft"); p2.Display(); // вызов метода Display из класса Employee
В консоли будут следующие строки:
Tom работает в Microsoft
Виртуальные методы базового класса определяют интерфейс всей иерархии классов. Это значит, что в любом производном классе, который не является прямым наследником от базового класса, можно переопределить виртуальные методы. Например, мы можем определить класс Manager , который будет производным от Employee , и в нем также переопределить метод Display .
При переопределении виртуальных методов в C# необходимо учитывать следующие ограничения:
- Виртуальный и переопределенный методы должны иметь один и тот же модификатор доступа. Если виртуальный метод определен с помощью модификатора public , то и переопредленный метод также должен иметь модификатор public .
- Нельзя переопределить или объявить виртуальным статический метод.
Переопределение свойств в C#
В C# можно переопределять как методы, так и свойства базовых классов:
class Credit < public virtual decimal Sum < get; set; >> class LongCredit : Credit < private decimal sum; //переопределенное свойство public override decimal Sum < get < return sum; >set < if(value >1000) < sum = value; >> > > class Program < static void Main(string[] args) < LongCredit credit = new LongCredit < Sum = 6000 >; credit.Sum = 490; Console.WriteLine(credit.Sum); > >
Ключевое слово base
С использованием ключевого слова base мы можем обращаться к членам базового класса. В нашем случае вызов base.Display(); позволяет обратиться к методу Display() в классе Person :
class Employee : Person < public string Company < get; set; >public Employee(string name, string company) :base(name) < Company = company; >public override void Display() < base.Display(); Console.WriteLine($"работает в "); > >
Запрет переопределения методов
Бывает необходимо также запретить переопределение методов и свойств в классах-наследниках. В этом случае методы и свойства необходимо объявлять с модификатором sealed :
class Employee : Person < public string Company < get; set; >public Employee(string name, string company): base(name) < Company = company; >//ни один наследник Employee не сможет переопределить этот метод public override sealed void Display() < Console.WriteLine($"работает в "); > >
При создании методов с модификатором sealed следует учитывать, что ключевое слово sealed применяется в паре с override , то есть только в переопределяемых методах.
Итого
Сегодня мы познакомились с такой замечательной возможностью языка C# как переопределение методов и свойств. Узнали как определять в классах виртуальные методы, переопределять методы и свойства в наследниках, а также обращаться к членам и методам базовых классов, используя ключевое слово base и запрещать переопределять методы с использованием ключевого слова sealed .
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Лекция. Виртуальные функции и полиморфизм
Для примеров будем использовать классы из предыдущей лекции. Следует напомнить, что у нас был класс Person, от которого был унаследован класс Student. Рассмотрим следующий пример:
Student s; Person &p = s; s.name(); //Student::name() p.name(); //Person::name()
В 3-й строке вызовется метод класса Student, т.к. s является объектом этого класса. Однако, в строке 4 вызовется метод name базового класса Person, хотя по логике следовало бы тоже ожидать вызов name() класса Student — ведь p — это ссылка на объект производного класса.
Возможность вызова методов производного класса через ссылку или указатель на базовый класс осуществляется с помощью механизма виртуальных функций. Чтобы при вызове p.name() вызвался метод класса Student, реализуем классы следующим образом:
struct Person < virtual string name() const; >; struct Student: Person < string name() const; >;
Перед методом name класса Person мы указали ключевое слово virtual, которое указывает, что метод является виртуальным. Теперь при вызове p.name() произойдет вызов метода класса Student, несмотря на то, что мы его вызываем через ссылку на базовый класс Person. Аналогичная ситуация и с указателями:
Student s; Person *p = &s ; p->name(); //вызовется Student::name(); Person n; p = &n; p->name(); //вызовется Person::name()
Если с некоторого класса в иерархии наследования метод стал виртуальным, то во всех производных от него классах он будет виртуальным, вне зависимости от того, указано ли ключевое слово virtual в классах наследниках.
Механизм виртуальных функций реализует полиморфизм времени выполнения: какой виртуальный метод вызовется будет известно только во время выполнения программы.
В качестве следующего примера можно рассмотреть класс TextFile, от которого наследуются два класса: GZippedTextFile и BZippedTextFile. Базовый класс имеет два метода: name(), возвращающий имя файла, и read(), считывающий данные из файла. В этом случае виртуальным имеет смысл сделать только метод read, т.к. у каждого типа сжатого файла будет свой способ считывания данных:
struct TextFile < string name() const; virtual string read(size_t count); //. >; struct GZippedTextFile : TextFile < string read(size_t count); //. >struct BZippedTextFile : TextFile < string read(size_t count); //. >
Перекрытие методов
Рассмотрим класс A, у которого имеется метод f(int), и класс B, унаследованный от A, у которого есть метод f(long):
struct A < void f(int); >; struct B : A < void f(long); >;
В следующем коде:
B b; b.f(1);
произойдет вызов метода f(long) класса B, несмотря на то, что у родительского класса A есть более подходящий метод f(int). Оказывается, что метод f(int) родительского класса A перекрылся. Для того, чтобы в примере вызвался метод f(int), следует добавить строку using A::f; в определении класса B:
struct B : A < using A::f; void f(long); >;
Абстрактные классы и чистые виртуальные функции
Расширим пример текстового файла. Предположим, что нам нужно сделать для класса TextFile базовый класс File, от которого будет унаследован еще один класс RTFFile. Однако, в такой ситуации неизвестно как реализовать метод read() класса File, т.к. класс File не реализует поведение какого-то конкретного типа файлов, а представляет интерфейс для работы с различными файлами. В этом случае, метод read(. ) этого класса нужно сделать чистым виртуальным, дописав «= 0» после его сигнатуры:
struct File < virtual string read(size_t count) = 0; >;
Это означает, что метод read(. ) должен быть определен в классах наследниках. Теперь класс File стал абстрактным, и его экземпляры невозможно создать. Но зато можно работать через указатель на абстрактный класс с объектами производных классов, например, так:
File *f = new TextFile("text.txt"); //различные действия с файлом text.txt delete f; f = new RTFFile("rich_text.rtf"); //различные действия с файлом rich_text.rtf delete f;
Следует отметить, что в любой иерархии классов деструктор всегда должен быть виртуальным. Рассмотрим пример, поясняющий важность этого факта:
struct Person < public: ~Person() <>private: string name; //. >; struct Student : Person < public: Student() < someData = new Data(); >~Student() < delete someData; >//. private: Data *someData; >; //. Student *s = new Student(); //. delete s; //вызовется деструктор класса Student, память по указателю someData освободится Person *p = new Student(); //. delete p; /*вызовется деструктор класса Person, а не Student, т.к. он не является виртуальным, несмотря на то, что на самом деле объект - экземпляр Student. В этом случае произойдет утечка памяти, т.к. память по указателю someData не освободится */
Деструктор можно также сделать чистым виртуальным, но при этом его тело нужно определить снаружи класса.
Таблица виртуальных функций (Virtual Function Table)
Для каждого класса, содержащего виртуальные методы, или унаследованного от класса с виртуальными методами, создается таблица виртуальных функций. Эта таблица предназначена для вызова нужных реализаций виртуальных методов во время исполнения программы. При создании экземпляра класса, указатель на VFT этого класса помещается в самое начало созданного объекта.
Как известно, конструирование объекта происходит поэтапно и начинается созданием объекта самого первого класса в иерархии наследования. Во время этого процесса перед вызовом конструктора каждого класса указатель на VFT устанавливается равным указателю на VFT текущего конструируемого класса. Например, у нас есть 3 класса: A, B, C (B наследуется от A, C наследуется от B). При создании экземпляра С, произойдут 3 последовательных вызова конструкторов: сначала A(), затем B(), и в конце C(). Перед вызовом конструктора A() указатель на VFT будет указывать на таблицу класса A, перед вызовом B() он станет указывать на таблицу класса B() и т.д. Аналогичная ситуация при вызове деструкторов, только указатель будет менятся от таблицы самого младшего класса к самому старшему.
Из этого факта следует правило: в конструкторах и деструкторах нельзя вызывать виртуальные методы. Посмотрим, что произойдет, если нарушить это правило:
struct A < A() < f(); >virtual void f() < cout >; struct B : A < B() < f(); >virtual void f() < cout >; struct C : B < C() < f(); >virtual void f() < cout >; //. C c; //создание объекта класса C
В этом примере на экран будет выведено:
A::f() B::f() C::f()
Модификаторы доступа
- public — доступ для всех
- protected — доступ только для самого класса и его наследников
- private — доступ только для самого класса
struct B : public A ; struct C : protected A ; struct D : private A ;
Если модификатор не указан, то по умолчанию для структур наследование public, для классов private.
public-наследование позволяет установить между классами отношение ЯВЛЯЕТСЯ. Т.е., если класс B открыто унаследован от A, то объект класса B ЯВЛЯЕТСЯ объектом класса A, но не наоборот.
private наследование выражает отношение РЕАЛИЗОВАНО_ПОСРЕДСТВОМ. Если класс B закрыто унаследован от A, то можно говорить, что объект класса B реализован посредством объекта класса A. В большинстве случаев закрытое наследование можно заменить агрегацией.
Следует отметить тот факт, что закрытый(private) виртуальный метод базового класса нельзя вызвать из наследника, но без проблем можно переопределить.
Ковариантность
Пусть имеется класс A, и класс B — его наследник. Тогда возможно определить классы C и D, с виртуальным методом f() следующим образом:
struct A ; struct B : A ; struct C < virtual A * f(); >; struct D : C
Такая возможность в C++ называет ковариантностью по типу возвращаемого значения (return type covariance).
Виртуальные методы, свойства и индексаторы
Полиморфизм предоставляет подклассу способ определения собственной версии метода, определенного в его базовом классе, с использованием процесса, который называется . Чтобы пересмотреть текущий дизайн, нужно понять значение ключевых слов 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, принцип использования которого аналогичен использованию виртуального метода.
Урок #20 – Виртуальные методы
Виртуальные методы – свойство языка C# за счет которого мы получаем возможность переопределения родительского функционала. За урок мы научимся использовать ключевые слова «virtual» и «override», а также познакомимся с ними на практике.
Видеоурок
Полиморфизм является одной из концепций объектно ориентированного программирования. Благодаря полиморфизму мы можем переопределять методы родительского класса в классах наследниках.
Зачем нужен полиморфизм?
Предположим что у нас есть один большой класс «Транспорт». В нём прописаны методы:
- вывод всей информации;
- установка полей класса;
- запуск двигателя (на данный момент выводит лишь текст про запуск двигателя).
На основе класса мы создаем два класса наследника: «Car» и «Airplane». В каждом из классов-наследников мы будем иметь доступ ко всем методам из класса «Транспорт».
Мы явно понимаем, что метод «запуск двигателя» должен иметь разную реализацию у двух классов. Как мы можем заменить методы:
- Можем создать в каждом классе новые методы, что будут релевантны конкретному классу. Из минусов то, что каждый метод будет иметь новое название и нам сложно будет запомнить все названия методов для всех классов;
- Можем создать переопределение методов. Для этого необходимо прописать такое же имя как в главном классе и далее прописать новое содержимое для метода. Получается явный плюс, так как теперь повсюду используется одно имя и в зависимости от разных классов будет вызываться разный метод, но под одним и тем же именем.
В ООП вы можете переопределять неограниченное количество методов и указывать для них новые свойства и действия.
Реализация программы
Пример реализации виртуальных методов приведен ниже:
using System; namespace ProjectOne < class Shape < public virtual void saysSomething () < Console.Write("No! "); >> class Square : Shape < public override void saysSomething () < base.saysSomething (); Console.Write("But I will say something!"); >> class MainClass < public static void Main(String[] args) < Square test = new Square(); test.saysSomething (); // Будет выведено - "No! But I will say something!" >> >
Для создания подобных методов необходимо прописать в главном классе ключевое слово virtual перед типом данных, а в классах наследнике слово override .
Основной класс
using System; namespace project < class Program < static void Main() < Robot bot = new Robot("Bot", 800, new byte[] ); bot.printValues(); Killer killer = new Killer("Killer", 1000, new byte[] , 100); killer.printValues(); killer.Lazer(); Robot bot1 = new Robot("Bot"); bot1.Weight = -100; > > >
Посмотреть остальной код можно после подписки на проект!
Задание к уроку
Необходимо оформить подписку на проект, чтобы получить доступ ко всем домашним заданиям
Большое задание по курсу
Вам необходимо оформить подписку на сайте, чтобы иметь доступ ко всем большим заданиям. В задание входит методика решения, а также готовый проект с ответом к заданию.
PS: подобные задания доступны при подписке от 1 месяца