Что такое виртуальный метод
Перейти к содержимому

Что такое виртуальный метод

  • автор:

Что такое виртуальный метод

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

Те методы и свойства, которые мы хотим сделать доступными для переопределения, в базовом классе помечается модификатором 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.

Урок #20 – Виртуальные методы

Урок #20 – Виртуальные методы

Виртуальные методы – свойство языка C# за счет которого мы получаем возможность переопределения родительского функционала. За урок мы научимся использовать ключевые слова «virtual» и «override», а также познакомимся с ними на практике.

Видеоурок

Полиморфизм является одной из концепций объектно ориентированного программирования. Благодаря полиморфизму мы можем переопределять методы родительского класса в классах наследниках.

Зачем нужен полиморфизм?

Предположим что у нас есть один большой класс «Транспорт». В нём прописаны методы:

  • вывод всей информации;
  • установка полей класса;
  • запуск двигателя (на данный момент выводит лишь текст про запуск двигателя).

На основе класса мы создаем два класса наследника: «Car» и «Airplane». В каждом из классов-наследников мы будем иметь доступ ко всем методам из класса «Транспорт».

Мы явно понимаем, что метод «запуск двигателя» должен иметь разную реализацию у двух классов. Как мы можем заменить методы:

  1. Можем создать в каждом классе новые методы, что будут релевантны конкретному классу. Из минусов то, что каждый метод будет иметь новое название и нам сложно будет запомнить все названия методов для всех классов;
  2. Можем создать переопределение методов. Для этого необходимо прописать такое же имя как в главном классе и далее прописать новое содержимое для метода. Получается явный плюс, так как теперь повсюду используется одно имя и в зависимости от разных классов будет вызываться разный метод, но под одним и тем же именем.

В ООП вы можете переопределять неограниченное количество методов и указывать для них новые свойства и действия.

Реализация программы

Пример реализации виртуальных методов приведен ниже:

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 месяца

Виртуальные методы, свойства и индексаторы

Полиморфизм предоставляет подклассу способ определения собственной версии метода, определенного в его базовом классе, с использованием процесса, который называется . Чтобы пересмотреть текущий дизайн, нужно понять значение ключевых слов 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(); >> > 

Виртуальные методы, свойства и индексаторы в C#

Давайте рассмотрим данный пример более подробно. В базовом классе Font инкапсулируется виртуальный метод FontInfo (), возвращающий информацию о шрифте. В производном классе FontColor данный метод переопределяется с помощью ключевого слова override, поэтому при создании экземпляра данного класса и вызова метода FontInfo() в исходную информацию возвращается помимо первоначальных данных еще и цвет шрифта. Затем данный метод вновь переопределяется в классе GradientColorFont, унаследованном от класса FontColor. Обратите внимание, что здесь переопределяется не исходный метод базового класса Font, а уже переопределенный метод класса FontColor. В этом и заключается принцип динамического полиморфизма!

Так же обратите внимание, что в данном примере используется виртуальное свойство color, принцип использования которого аналогичен использованию виртуального метода.

Виртуальный метод

Виртуальный метод (виртуальная функция) — в объектно-ориентированном программировании метод (функция) класса, который может быть переопределён в классах-наследниках так, что конкретная реализация метода для вызова будет определяться во время исполнения. Таким образом, программисту необязательно знать точный тип объекта для работы с ним через виртуальные методы: достаточно лишь знать, что объект принадлежит классу или наследнику класса, в котором метод объявлен.

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

Базовый класс может и не предоставлять реализации виртуального метода, а только декларировать его существование. Такие методы без реализации называются «чистыми виртуальными» (перевод англ. pure virtual ) или абстрактными. Класс, содержащий хотя бы один такой метод, тоже будет абстрактным. Объект такого класса создать нельзя (в некоторых языках допускается, но вызов абстрактного метода приведёт к ошибке). Наследники абстрактного класса должны предоставить реализацию для всех его абстрактных методов, иначе они, в свою очередь, будут абстрактными классами.

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

Пример виртуальной функции на C++

Пример на C++, иллюстрирующий отличие виртуальных функций от невиртуальных:

class Ancestor  public: virtual void function1 ()  cout  <"Ancestor::function1()"  ; > void function2 ()  cout  <"Ancestor::function2()"  ; > >; class Descendant : public Ancestor  public: virtual void function1 ()  cout  <"Descendant::function1()"  ; > void function2 ()  cout  <"Descendant::function2()"  ; > >; Descendant* pointer = new Descendant (); Ancestor* pointer_copy = pointer; pointer->function1 (); pointer->function2 (); pointer_copy->function1 (); pointer_copy->function2 (); 

В этом примере класс Ancestor определяет две функции, одну из них виртуальную, другую — нет. Класс Descendant переопределяет обе функции. Однако, казалось бы одинаковое обращение к функциям даёт разные результаты. На выводе программа даст следующее:

Descendant::function1() Descendant::function2() Descendant::function1() Ancestor::function2()

То есть, в случае виртуальной функции, для определения реализации функции используется информация о типе объекта и вызывается «правильная» реализация, независимо от типа указателя. При вызове невиртуальной функции, компилятор руководствуется типом указателя или ссылки, поэтому вызываются две разные реализации function2() , несмотря на то, что используется один и тот же объект.

Следует отметить, что в С++ можно, при необходимости, указать конкретную реализацию виртуальной функции, фактически вызывая её невиртуально:

pointer->Ancestor::function1 (); 

для нашего примера выведет Ancestor::function1(), игнорируя тип объекта.

Пример виртуальной функции в Delphi

Язык Object Pascal, использующийся в Delphi, тоже поддерживает полиморфизм. Рассмотрим пример:

Объявим два класса. Предка (Ancestor):

TAncestor = class private protected public procedure VirtualProcedure; virtual; procedure StaticProcedure; end; 

и его потомка (Descendant):

TDescendant = class(TAncestor) private protected public procedure VirtualProcedure; override; procedure StaticProcedure; end; 

Как видно в классе предке объявлена виртуальная функция — VirtualProcedure . Чтобы воспользоваться достоинствами полиморфизма, её нужно перекрыть в потомке.

Реализация выглядит следующим образом:

 < TAncestor >procedure TAncestor.StaticProcedure; begin ShowMessage('Ancestor static procedure.'); end; procedure TAncestor.VirtualProcedure; begin ShowMessage('Ancestor virtual procedure.'); end; 
 < TDescendant >procedure TDescendant.StaticProcedure; begin ShowMessage('Descendant static procedure.'); end; procedure TDescendant.VirtualProcedure; begin ShowMessage('Descendant override procedure.'); end; 

Посмотрим как это работает:

procedure TForm2.BitBtn1Click(Sender: TObject); var MyObject1: TAncestor; MyObject2: TAncestor; begin MyObject1 := TAncestor.Create; MyObject2 := TDescendant.Create; try MyObject1.StaticProcedure; MyObject1.VirtualProcedure; MyObject2.StaticProcedure; MyObject2.VirtualProcedure; finally MyObject1.Free; MyObject2.Free; end; end; 

Заметьте, что в разделе var мы объявили два объекта MyObject1 и MyObject2 типа TAncestor . А при создании MyObject1 создали как TAncestor , а MyObject2 как TDescendant . Вот что мы увидим при нажатии на кнопку BitBtn1 :

  1. Ancestor static procedure.
  2. Ancestor virtual procedure.
  3. Ancestor static procedure.
  4. Descendant override procedure.

Для MyObject1 все понятно, просто вызвались указанные процедуры. А вот для MyObject2 это не так.

Вызов MyObject2.StaticProcedure; привел к появлению «Ancestor static procedure.». Ведь мы объявили MyObject2: TAncestor , поэтому и была вызвана процедура StaticProcedure; класса TAncestor .

А вот вызов MyObject2.VirtualProcedure; привел к вызову VirtualProcedure; реализованной в потомке( TDescendant ). Это произошло потому, что MyObject2 был создан не как TAncestor , а как TDescendant : MyObject2 := TDescendant.Create; . И виртуальный метод VirtualProcdure был перекрыт.

В Delphi полиморфизм реализован с помощью так называемой виртуальной таблицы методов (или VMT).

Достаточно часто виртуальные методы забывают перекрыть с помощью ключевого слова override . Это приводит к закрытию метода. В этом случае замещения методов в VMT не произойдет и требуемая функциональность не будет получена.

Эта ошибка отслеживается компилятором, который выдаёт соответствующее предупреждение.

Вызов метода предка из перекрытого метода

Бывает необходимо вызвать метод предка в перекрытом методе.

Объявим два класса. Предка(Ancestor):

TAncestor = class private protected public procedure VirtualProcedure; virtual; end; 

и его потомка (Descendant):

TDescendant = class(TAncestor) private protected public procedure VirtualProcedure; override; end; 

Обращение к методу предка реализуется с помощью ключевого слова «inherited»

procedure TDescendant.VirtualProcedure; begin inherited; end; 

Стоит помнить, что в Delphi деструктор должен быть обязательно перекрытым — «override» — и содержать вызов деструктора предка

TDescendant = class(TAncestor) private protected public destructor Destroy; override; end; 
destructor TDescendant. Destroy; begin inherited; end; 

В языке C++ не нужно вызывать конструктор и деструктор предка, деструктор должен быть виртуальным. Деструкторы предков вызовутся автоматически. Чтобы вызвать метод предка, нужно явно вызвать метод:

class Ancestor  public: virtual void function1 ()  printf("Ancestor::function1"); > >; class Descendant : public Ancestor  public: virtual void function1 ()  printf("Descendant::function1"); Ancestor::function1(); // здесь будет напечатано "Ancestor::function1" > >; 

Для вызова конструктора предка нужно указать конструктор:

class Descendant : public Ancestor  public: Descendant(): Ancestor()> >; 

Еще один пример

class A  public: virtual int function ()  return 1; > int get()  return this->function(); > >; class B: public A  public: int function()  return 2; > >; #include int main()  A a; B b; std::cout  get()  ::endl; // 2 return 0; > 

Несмотря на то, что в классе B отсутствует метод get(), его можно позаимствовать у класса A, при этом результат работы этого метода вернет вычисления для B::function()!

См. также

  • Объектно-ориентированное программирование
  • Полиморфизм (программирование)
  • Абстрактный класс

Ссылки

  • C++ FAQ Lite: Виртуальные функции в C++ (англ.)
  • Метод (информатика)

Wikimedia Foundation . 2010 .

Полезное

Смотреть что такое «Виртуальный метод» в других словарях:

  • виртуальный метод доступа к памяти — — [http://www.iks media.ru/glossary/index.html?glossid=2400324] Тематики электросвязь, основные понятия EN virtual storage access methodVSAM … Справочник технического переводчика
  • виртуальный телекоммуникационный метод доступа — Программа IBM, постоянно хранящаяся в памяти на сервере и управляющая обменом информацией и потоками данных в сети SNA. [http://www.morepc.ru/dict/] Тематики информационные технологии в целом EN Virtual Telecommunications Access MethodVTAM … Справочник технического переводчика
  • Метод контурных токов — Метод контурных токов метод сокращения размерности системы уравнений, описывающей электрическую цепь. Содержание 1 Основные принципы 2 Построение системы контуров … Википедия
  • Виртуальный собеседник — В этой статье не хватает ссылок на источники информации. Информация должна быть проверяема, иначе она может быть поставлена под сомнение и удалена. Вы можете … Википедия
  • Абстрактный метод — Эту статью следует викифицировать. Пожалуйста, оформите её согласно правилам оформления статей. Абстрактный метод (или чистый виртуальный метод (pure virtual method часто неверно переводится как чисто виртуальный метод)) в … Википедия
  • Фабричный метод (шаблон проектирования) — Шаблон проектирования Фабричный метод Factory Method Тип: порождающий Описан в Design Patterns Да Фабричный метод (англ. Factory Method) порождающий шаблон проектирования, предоставляющий подклассам интерфейс для созда … Википедия
  • Фабричный метод — (англ. Factory Method) порождающий шаблон проектирования, предоставляющий подклассам интерфейс для создания экземпляров некоторого класса. В момент создания наследники могут определить, какой класс инстанциировать. Иными словами, Фабрика… … Википедия
  • интеллектуальный постоянный виртуальный канал — Метод организации связи с использованием гибридного логического соединения, создаваемого на основе сочетания двух типов виртуальных каналов постоянных (PVC) и коммутируемых (SVC). Администратор сети производит настройку соединений, однако… … Справочник технического переводчика
  • Виртуальная функция — Виртуальный метод (виртуальная функция) в объектно ориентированном программировании метод (функция) класса, который может быть переопределён в классах наследниках так, что конкретная реализация метода для вызова будет определяться во время… … Википедия
  • Сравнение C Sharp и Java — Правильный заголовок этой статьи Сравнение C# и Java. Он показан некорректно из за технических ограничений. Сравнения языков программирования Общее сравнение Основной синтаксис Основные инструкции Массивы Ассоциативные массивы Операции со… … Википедия
  • Обратная связь: Техподдержка, Реклама на сайте
  • �� Путешествия

Экспорт словарей на сайты, сделанные на PHP,
WordPress, MODx.

  • Пометить текст и поделитьсяИскать в этом же словареИскать синонимы
  • Искать во всех словарях
  • Искать в переводах
  • Искать в ИнтернетеИскать в этой же категории

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *