Как создать объект абстрактного класса c
Перейти к содержимому

Как создать объект абстрактного класса c

  • автор:

NEWOBJ.ru → Введение в ООП с примерами на C# →

Теперь положим, для чтения текущего значения curVal с устройства используется метод Readout . Где нам его следует разместить: в базовом классе или в производных классах? Если мы его разместим в производных классах, то мы не сможем его вызвать через экземпляр базового класса. Если же мы разместим его в базовом классе – сможем ли мы привести общую реализацию? Очевидно, что нет, так как в этом и причина создания нескольких производных классов – они имеют разную реализацию загрузки данных с устройств.

Какой возможности языка программирования нам не хватает? Мы бы хотели при вызове некоторого метода, объявленного в базовом классе, вызывать реализацию этого метода из производного класса, к типу которого реально относится объект. Конечно, мы можем переадресовать вызов метода базового класса к производному классу, однако, это создает зависимость базового класса от производных, что всегда приводит к серьезным проблемам 47 :

 // АНТИШАБЛОН class Sensor < /* . */ public void Readout() < if (this is TemperatureSensor) < ((TemperatureSensor)this).ReadoutTemperature48(); > else if (this is PressureSensor) < ((PressureSensor)this).ReadoutPressure(); >else < throw new NotImplementedException(); >> > 

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

 abstract class Sensor < /* . */ public abstract void Readout(); > class TemperatureSensor : Sensor < /* . */ public override void Readout() < // Считывает значение по датчику температуры из базы и сохраняет в curVal. Console.WriteLine (‘TemperatureSensor.Readout’); curVal = 1; >> class PressureSensor : Sensor < /* . */ public override void Readout() < // Считывает значение по датчику давления из базы и сохраняет в curVal. Console.WriteLine (‘PressureSensor.Readout’); curVal = 2; >> Sensors[] sensors = new Sensors[2]; sensors[0] = new TemperatureSensor(“T-101”); sensors[1] = new PressureSensor (“P-202”); // Использование для массива Sensor[] sensors: for (int i = 0; i < sensors.Length; i++) < Sensor s = sensors[i]; // Сначала актуализируем значение из базы данных. s.Readout(); // Потом выводим его. Console.WriteLine ($”= ”); > 

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

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

В приведенном коде в первой итерации цикла ( i == 0 ) переменная s типа Sensor обозначает объект реального типа TemperatureSensor , поэтому при вызове метода s.Readout будет вызван код этого метода из класса TemperatureSensor . В следующей же итерации цикла ( i == 1 ) переменная s типа Sensor обозначает объект другого реального типа – PressureSensor , поэтому при вызове метода s.Readout будет вызван код этого метода из класса PressureSensor .

 > TemperatureSensor > T-101 = 1 > PressureSensor > P-202 = 2 

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

Абстрактный метод (abstract) – метод, объявленный без реализации (тела); реализация метода может быть приведена в любом из подклассов; класс, в котором объявлен хотя бы один абстрактный метод или который наследует, но не реализует, хотя бы один абстрактный метод, является абстрактным классом; не допускается создание экземпляров абстрактного класса 50 .

Переопределение метода (overriding) – определение (с телом или без) в производном классе метода с сигнатурой, совпадающей с сигнатурой метода, определенного в базовом классе.

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

Продемонстрируем это следующим примером:

 // Объявлен абстрактный метод void M(). abstract class A < public abstract void M(); >// Метод базового класса не переопределяется, // поэтому класс B также абстрактный. abstract class B : A < >// Класс C переопределяет абстрактный метод класса A. class C : B < public override void M() < Console.WriteLine (“C”); >> // Класс D переопределеяет метод класса C. // Для экземлпяров класса D будет использоваться именно эта реализация // (ближайшая по цепочке наследования), а не из класса C. class D : C < public override void M() < Console.WriteLine (“D”); >> // Класс E переопределяет метод класса D как абстрактный, // таким образом, «скрывает» переопределение из класса D. abstract class E : D < public abstract override void M(); >// Класс F переопределяет метод класса E. class F : E < public override void M() < Console.WriteLine (“F”); >> // Класс G не переопределяет метод М(), // но так как в базовом классе F этот метод переопределен как конкретный, // то класс G – конкретный класс и при вызове для его экземпляра метода M // будет вызвана реализация из класса F (ближайшего по цепочке наследования). class G : F < >// Использование: A c = new C(); c.M(); // Вывод: С A d = new D(); d.M(); // Вывод: D A f = new F(); f.M(); // Вывод: F ((D)f).M(); // Вывод: F A g = new G(); g.M() // Вывод: F 

То есть, еще раз: при вызове метода класса, в частности, при вызове абстрактного метода, всегда вызывается «самая частная» («самая производная») реализация в иерархии наследования для реального типа объекта, а не для типа переменной, через которую мы вызываем метод. На следующем рисунке показано, что при вызове метода M() для переменной g типа A будет вызвана реализация из класса F , так как она ближе всего в иерархии наследования к реальному типу объекта – G .

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

 // Данные отдельного измерения. class Data < // Время измерения. public DateTime Time; // Измеренное значение. public float Value; >abstract class Sensor < /* . */ // Теперь это неабстрактный метод базового класса. public void Readout() < // Вызываем из метода базового класса абстрактный метод, // который будет «доопределен» в производном классе. Data[] data = ReadoutRaw(); // Сохраняем последнее значение. if (data.Length >0) < curValue = data[data.Lenght – 1]; >> public abstract Data[] ReadoutRaw(); > class TemperatureSensor : Sensor < /* . */ public override Data[] ReadoutRaw () < /* . */ >> class PressureSensor : Sensor < /* . */ public override Data[] ReadoutRaw () < /* . */ >> 

Логика в приведенном коде следующая: мы получаем массив последних зарегистрированных датчиком значений, далее, в базовом классе сохраняем самое последнее из числа полученных как текущее значение. При этом мы вызываем абстрактный метод ReadoutRaw() класса Sensor из конкретного метода Readout() того же класса Sensor . Вспомним, что, вызывая метод класса и другого метода того же класса, мы, фактически, вызываем этот метод для объекта this этого класса. Таким образом, нет никакой разницы между вызовом абстрактного метода класса извне или из конкретного метода того же класса – в обоих случаях будет вызвана реализация метода для реального типа объекта:

 Sensor sensor = new TemperatureSensor(); // Вызов абстрактного метода приводит к выполненению реализации // для реального типа объекта, // здесь – к выполнению метода из класса TemperatureSensor Data[] data = sensor.ReadoutRow(); abstract class Sensor < public void Readout() < // Эквивалентно: Data[] data = this.ReadoutRaw(); Data[] data = ReadoutRaw(); /* . */ > public abstract Data[] ReadoutRaw(); /* . */ > 

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

 class A < protected abstract string M(); public A() < // Использование абстрактных методов в конструкторе – плохая практика! N(); >public void N () < Console.WriteLine (M()); >> class B < private string m = “1”; public B() < m = “2”; >protected override string M() < return m; >> B b = new B(); // Вывод: 1 b.N(); // Вывод: 2 

Объясните, почему при создании объекта при вызове метода N выводится значение 1, а при явном вызове метода N – 2.

Абстрактный методы – крайне важный механизм объектно-ориентированного программирования; убедитесь, что вы вполне разобрались с рассмотренными в параграфе примерами, прежде чем продолжать изучение.

§ 38. Виртуальные методы. В примере с датчиками из предыдущего параграфа метод GetCurVal имеет общую для всех производных классов сигнатуру и реализацию (это обычный, «конкретный» метод), а метод Readout имеет общую для всех производных классов сигнатуру, но реализация определяется в каждом из классов отдельно (абстрактный метод). Однако вполне можно представить, что семантика некоторых методов будет таковой, что у них или (первая ситуация) будет общая реализация, но она будет общей только для некоторых производных классов, или (вторая ситуация) для некоторых производных классов потребуется дополнить общую реализацию некоторым кодом. Рассмотрим такие ситуации.

Логика работы метода Readout() вполне устраивает нас для датчиков, считывающих значение с периодичностью раз в несколько секунд. Но, положим, некоторый датчик измеряет скорость потока жидкости в трубе и физически фиксирует значение несколько раз в секунду. В этом случае, последнее из измеренных за интервал значение будет не очень информативно, а большая часть измерений будет пропускаться и не отображаться пользователю вовсе. Мы хотели бы для повышения информативности выводить среднее арифметическое полученных за некоторый период, положим, за прошедшие 3 секунды, значений. Таким образом, реализация метода Readout нас устраивает для производных классов TemperatureSensor и PressureSensor , но для класса VelocityHDSensor нам требуется другая реализация, что соответствует описанной в начале параграфа первой ситуации.

Предположим теперь, наши классы датчиков также хранят единицу измерения. Логично сделать абстрактный метод GetUnits и реализовать его в каждом производном классе. Также, положим, нам требуется метод GetCurValString , возвращающий текущее значение в отформатированном виде как число и единицу измерения. Мы можем разместить этот метод в базовом классе:

 abstract class Sensor < public abstract string GetUnits(); public string GetCurValString() < return curVal + “ “ + GetUnit(); >> class PressureSensor : Sensor < public override string GetUnits() < return “МПа”; >> 

Приведенный код вполне решает наши задачи, однако, допустим, для класса VelocityHDSensor , который определяет текущее значение как среднее арифметическое, мы хотели бы дополнить метод GetCurValString , выводя в конце строки в скобках текст “avg”. То есть, если для класса PressureSensor метод вернет, например, “1,2 МПа”, то для класса VelocityHDSensor мы бы хотели получить «0,4 м/с (avg)». Это пример второй ситуации, обозначенной в начале параграфа.

Как мы можем решить эти две задачи?

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

 // АНТИШАБЛОН abstract class Sensor < public string GetCurValString() < return GetCurValStringExt (curVal + “ “ + GetUnit()); > public abstract string GetUnits(); public abstract string GetCurValStringExt (string b); > class TemperatureSensor : Sensor < public string GetCurValStringExt (string b) < return b; >> class PressureSensor : Sensor < public string GetCurValStringExt (string b) < return b; >> class VelocityHDSensor : Sensor < public override string GetCurValStringExt (string b) < return b + “ (avg)”; >> 

Сложность восприятия кода повысилась, также мы были вынуждены создать в классах TemperatureSensor и PressureSensor методы, не выполняющие никаких действий, а только возвращающие значение, вычисленное в базовом классе. Однако основная проблема этого решения в другом: чтобы расширить поведение базового класса для целей нового производного класса мы вынуждены вносить изменения и в базовый класс, и во все другие уже реализованные и стабильные производные классы, что в значительной мере обесценивает механизм наследования.

Внимательный читатель может предложить еще одно решение, подходящее для обоих ситуаций, используя только механизм абстрактных и обычных методов. В предыдущем параграфе мы показывали, что абстрактный метод может быть перегружен несколько раз в иерархии наследования. Соответственно, мы можем определить дополнительный базовый класс SensorBase , в котором определить абстрактные методы Readout и GetCurValString , далее, сначала переопределить их в классе Sensor , а потом, при необходимости, переопределить их в некоторых из производных классов. Проанализируем код:

 // Также АНТИШАБЛОН, но существенно лучше предыдущего решения. abstract class SensorBase < public abstract void Readout(); public abstract string GetCurValString(); >abstract class Sensor : SensorBase < public override void Readout() < Data[] data = ReadoutRaw(); if (data.Length >0) < curValue = data[data.Lenght – 1]; >> public override string GetCurValString() < return curVal + “ “ + GetUnit(); >> class TemperatureSensor : Sensor < // Не переопределяем методы Readout и GetCurValString, // будут использоваться реализации из класса Sensor, // дополнительного кода в настоящем классе TemperatureSensor не потребуется. >class PressureSensor : Sensor < // Не переопределяем методы Readout и GetCurValString, // будут использоваться реализации из класса Sensor, // дополнительного кода в настоящем классе PressureSensor не потребуется. >class VelocityHDSensor : Sensor < public override void Readout() < Data[] data = ReadoutRaw(); // Рассчитываем среднее значение за последние 3 сек // и сохраняем его в curVal. >public override string GetCurValString() < return curVal + “ “ + GetUnit() + “ (avg)”; >> 

Приведенное решение, во-первых, подходит и для первой, и для второй ситуации. Во-вторых, не требует изменения кода существующих производных классов при переопределении методов Readout и GetCurValString в новом производном. Однако остается две проблемы. Во-первых, переопределенные методы производного класса целиком заменяют методы базового класса. Так, при выводе отформатированной строки мы дублируем логику метода базового класса. Конкретно в этом случае она крайне проста, однако даже здесь, если, к примеру, потребуется для всех классов изменить форматирование и выводить единицу измерения в скобках, нам потребуется вносить изменение в двух местах: в базовом классе Sensor и в производном классе VelocityHDSensor . Во-вторых, мы вынуждены были объявить дополнительный базовый класс, хотя никакой семантики в нем нет: если датчики температуры, давления и скорости – это «частное» по отношению к датчику – «общему», то подобной семантической связи между Sensor и SensorBase нет, это лишь синтаксическая уловка. Как следствие, код становится сложнее для восприятия.

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

 abstract class Sensor < // Помечая обычный, неабстрактный метод, ключевым словом virtual, // мы разрешаем переопределить его в производных классах, // как если бы он был абстрактным. public virtual void Readout() < Data[] data = ReadoutRaw(); if (data.Length >0) < curValue = data[data.Lenght – 1]; >> public virtual string GetCurValString() < return curVal + “ “ + GetUnit(); >> class TemperatureSensor : Sensor < // Не переопределяем методы Readout и GetCurValString, // будут использоваться реализации из класса Sensor, // дополнительного кода в настоящем классе TemperatureSensor не потребуется. >class PressureSensor : Sensor < // Не переопределяем методы Readout и GetCurValString, // будут использоваться реализации из класса Sensor, // дополнительного кода в настоящем классе PressureSensor не потребуется. >class VelocityHDSensor : Sensor < public override void Readout() < Data[] data = ReadoutRaw(); // Рассчитываем среднее значение за последние 3 сек // и сохраняем его в curVal. >public override string GetCurValString() < // Ипользуя ключевое слово base, // мы вызываем реализацию метода GetCurValString // из базового класса Sensor. // Использование имени GetCurValString без base // приведет к рекурсивному вызову этого метода. return base.GetCurValString() + “ (avg)”; > > 

Виртуальный метод (virtual) – метод, который может быть переопределен в производных классах.

Обратим внимание, что аналогично абстрактному методу, виртуальный метод может, но не должен быть переопределен.

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

 Sensor sensor = new VelocityHDSensor(); Console.WriteLine (sensor.GetCurValString()); 

Будет вызвана самая последняя реализация в цепочке наследования для реального объекта, то есть реализация из класса VelocityHDSensor , далее, в коде этого класса мы используем переменную base , вызывая реализацию того же самого метода, но определенную в базовом классе. При этом используется та же логика: вызывается самая последняя (самая «частная») реализация в цепочке наследования, но не до реального объекта, а до ближайшего базового класса этого реального объекта. В нашем случае «цепочки» уже не будет, останется только класс Sensor :

Следующий рисунок демонстрирует поведение в аналогичной ситуации при многоуровневом наследовании:

Обратим внимание на особую логику вызова метода базового класса через ключевое слово base : хотя мы можем представить, что base это переменная-указатель на текущий объект, то есть base == (BaseClass)this , такое представление не вполне верно. Первая причина: при вызове абстрактных и виртуальных методов через переменную base вызываемая реализация определяется, как если бы реальный тип объекта был типом базового класса. Иначе мы просто не смогли бы вызвать в виртуальном методе производного класса реализацию из базового класса. Вторая причина синтаксическая – в C# мы не можем использовать base , как обычную переменную, в частности, не можем преобразовывать ее к другим типам или сравнивать с другими переменными, это ключевое слово применяется только для вызова методов и обращения к полям базового класса. Рассмотрим следующий пример:

 class A < public virtual void M() < Console.WriteLine (“A.M”); >> class B : A < public override void M() < Console.WriteLine (“B.M”); >public void Test() < // Далее в комментариях указывается результат выполнения // при вызове метода следующим образом: // B obj = new C(); // obj.Test(); // Вывод: C.M // (Так как реальный тип объекта this – С.) this.M(); // Вывод: C.M // (Так как реальный тип объекта this – С.) ((A)this).M(); // Вывод: A.M. // Виртуальные и абстрактные методы обрабатываются, // как если бы реальный тип объекта base был A. base.M(); // Следующие операции с ключевым словом base недопустимы и // приведут к ошибке компиляции. // ((B)base).M(); // Console.WriteLine ((A) this == base); >> class C : B < public override void M() < Console.WriteLine (“C.M”); >> 

В заключение, рассмотрим еще один пример:

 сlass A < public virtual M () < Trace.WriteLine (“A.M”); >> abstract class B: A < public abstract override M () < Trace.WriteLine (“B.M”); >class C : B < >class D : B < public override M () < Trace.WriteLine (“D.M”); >// Объясните результат выполнения следующего кода: D d = new D(); d.M(); // Вывод: D.M ((B)d).M(); // Вывод: D.M ((A)d).M(); // Вывод: D.M C c = new C(); c.M(); // Вывод: B.M ((B)c).M(); // Вывод: B.M ((A)c).M(); // Вывод: B.M 

Отметим, что абстрактные методы являются частным случаем виртуальных методов. Можно сказать, что абстрактный метод – это виртуальный метод без реализации. Поэтому, например, в С++, абстрактные методы также называются чистыми виртуальными методами (pure virtual).

Большинство современных языков программирования для обозначения рассмотренных понятий виртуальных и абстрактных методов используют именно термины «виртуальный» и «абстрактный». Также обратим внимание на то, что в C# мы должны явно помечать метод как виртуальный ключевым словом virtual, однако это не является общим правилом. Например, в Java все методы по умолчанию являются виртуальными.

Мы уже отмечали крайнюю важность механизма абстрактных методов. То же самое верно и для виртуальных методов. Можно сказать, что механизм абстрактных и виртуальных методов – ядро инструментария объектно-ориентированного программирования.

§ 39. Класс object. В большинстве языков программирования есть специальный тип, который может хранить указатель на объект любого типа. Такой тип условно можно назвать базовым по отношению к любому пользовательскому или встроенному типу в том смысле, что преобразование между ним и другими типами выполняется по правилам преобразования между базовым и производным типом, рассмотренными в главе 3.2. В C# таким типом является тип object . В этом параграфе мы коротко рассмотрим несколько его ключевых и часто используемых возможностей.

Первая возможность, связанная с типом object – использование виртуальных методов, определённых в этом типе. Мы ограничимся рассмотрением только одного такого метода:

 public virtual string ToString() < /* . */ >

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

 namespace Geometry < class Point < private float x; private float y; // Используем переопределение, // хотя явно класс Point не наследуется ни от какого другого. // Неявное наследование от базового класса object. public override string ToString() < return $”X = , Y = ”; > > > // Без переопределения: Console.WriteLine (p.ToString()); // Вывод: Geometry.Point // С переопределением: Console.WriteLine (p.ToString()); // Вывод: X = 1, Y = 2 

Вторая возможность, связанная с типом object – упаковка и распаковка значимых типов. Ранее мы уже обсуждали отличия значимых и ссылочных типов данных в C#. В некоторых языках мы можем использовать любой класс или как значимый, или как ссылочный. Например, так работает C++ 52 . В некоторых языках эта логика скрывается, и тип фиксировано или всегда является значимым, или всегда является ссылочным. Так работает C#. Однако тот факт, что тип object является ссылочным и базовым по отношению ко всем другим типам, говорит нам, что мы можем преобразовать значимый тип, например, int к типу object :

 int a = 123; object o = a; int b = (int) o; 

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

В случае преобразования переменной значимого типа в object и обратно (на рисунке – слева) выполняется создание новой ссылочной переменной и копирование в нее значения исходной. В случае ссылочных переменных (на рисунке – справа), исходные данные остаются без изменений, меняются только ячейки для хранения адреса.

Преобразование переменной значимого типа в ссылочную переменную типа object называется упаковкой (boxing). Обратное преобразование называется распаковкой (unboxing).

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

 int n = 123; // string.Format (string format, params object[] args) string message = string.Format (“Обработано строк”, n); Console.WriteLine (message); // Обработано 123 строк 

Мы вернемся к вопросу упаковки и распаковки в главе 3.6 «Обобщенное программирование».

Вопросы и задания

Дайте определения следующим терминам, а также сопоставьте русские и английские термины: абстрактный метод, абстрактный класс, виртуальный метод, чистый абстрактный метод, переопределенный метод, упаковка, распаковка; abstract, virtual, pure virtual, override, boxing, unboxing.

Зачем в абстрактном классе нужен конструктор, если экземпляр все равно никогда не создается?
Можно ли при переопределении метода изменить его видимость?

Напишите метод, создающий массив long[] , содержащий миллион элементов и заполняющий его числами по порядку. Напишите аналогичный метод, создающий массив object[] , содержащий миллион элементов и заполняющий его числами типа long по порядку. Сравните время выполнения методов с помощью класса Stopwatch и объясните результат.

** Изучите назначение виртуальных методов Equals и GetHash класса object .

* В объектно-ориентированных языках существует возможность запретить наследование от заданного класса. Так в C#, мы можем пометить класс ключевым словом sealed (запечатанный). Чем можно объяснить, что в практике программирования, это ключевое слово значительно чаще используется в Java чем в C#?

47. Мы вернемся к вопросу о недопустимости зависимости базовых классов от производных в § 43.

48. Отметим, что мы не можем объявить метод с одинаковой сигнатурой и базовом, и в производном классе (будет ошибка компиляции), не используя специальные механизмы переопределения, которые мы будем рассматривать далее. Поэтому в этом примере мы используем разные имена методов в производных классах (ReadoutTemperature, ReadoutPressure вместо Readout).

49. Не следует путать с абстрактными типами данных. Это не связанные термины. Как мы уже говорили в главе 2.1, термин «абстрактные типы данных» сегодня употребляется не так широко и обозначает типы данных, моделирующие в программе некоторые абстракции. Слово «абстрактные» в терминах «абстрактные методы» и «абстрактные классы» используется в несколько ином значения, обозначая, что методы или классы не вполне реализованы, в отличие от обычных, конкретных, методов и классов.

50. Заметим, что ключевым словом abstract может быть помечен и класс без абстрактных методов. В этом случае также запрещается создание экземпляров этого класса (но допускается создание экземпляров неабстрактных производных классов).

51. Термин «переопределение» используется, даже если в базовом классе метод только объявлен (приведена сигнатура без тела). Строго говоря, исходный английский термин override переводится как «замещение», то есть мы замещаем метод из базового класса (только объявленный или определенный с реализацией), новым методом (также только объявленным или определённым с реализацией). А термины «объявленный» и «определенный» используются непоследовательно.

52. Строго говоря, в С++ нет ссылочных типов, вместо этого мы создаем переменную указатель и присваиваем ей адрес объекта. Но эта схема в первом приближении полностью аналогична ссылочным типам C#. C++: Point* p = new Point(); C#: Point p = new Point(); Однако в C++ мы можем объявить переменную класса и как значимую: Point p; (аналогия: int x;). В этом случае память будет выделена в стеке метода и p будет обозначать не ячейку с адресом объекта, а сам объект.

&copy Тимофей Усов, 2019—2020.

Классы и объекты C#: абстрактные классы

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Иногда бывает необходимо создавать классы, экземпляры которого нельзя создавать. Такие классы называются абстрактными. В каком случае нам может потребоваться абстрактный класс? Например, в случае, когда набор каких-либо сущностей относятся к одной и той же области знаний, но могут иметь различные реализации одних и тех же свойств и методов. Для примера, представим себе такую задачу — нам необходимо разработать систему учёта сотрудников в учебном заведении. Студент и преподаватель — это два человека у которых может быть имя, фамилия. При этом у преподавателя есть звание, должность, какие-то свои характеристики, например, стаж работы. У студента тоже могут быть свои, характерные только для него свойства, например, курс на котором он учится, группа, средний балл по всем изучаемым дисциплинам и т.д. Таким образом, исходя из этих данных, можно выделить следующие важные для нас сущности — преподаватель и студент. Абстрактный класс в этом случае — человек, то есть этот класс будет чем-то общим между студентом и преподавателем. Теперь попробуем реализовать нашу задачу в коде C#.

Абстрактный класс

Создадим наш первый абстрактный класс, который будет представлять в программе какого-либо человека:

abstract class Person < public string Name < get; set; >public string Family < get; set; >public Person(string name, string family) < Name = name; Family = family; >public string Display() < return $""; > >

У класса определено два свойства: имя ( Name ) и фамилия ( Family ), а также конструктор и метод Diasplay , возвращающий строку, содержащую фамилию и имя человека. Ключевое слово abstract говорит нам о том, что класс является абстрактным и мы не можем воспользоваться конструктором абстрактного класса напрямую (создать объект абстрактного класса). Например, вот такой вызов

Person person = new Person("Вася", "Пупкин");

приведет к ошибке

Ошибка CS0144 Не удается создать экземпляр абстрактного типа или интерфейса «Person»

Теперь используем ключевую возможность ООП — наследование и создадим производный от Person класс студента.

class Student: Person < string Group < get; set; >public Student(string name, string family, string group) : base(name, family) < Group = group; >>

Здесь мы уже определили свой конструктор для класса и добавили для класса свое свойство — Group (название группы, в которой учится студент). Теперь, мы можем создать экземпляр этого класса и, например, воспользоваться методом Diasplay :

Person student = new Student("Вася", "Пупкин", "ГВН-105"); Student student1 = new Student("Ваня", "Иванов", "ГВН-105"); Console.WriteLine(student.Display()); Console.WriteLine(student1.Display());

Обратите внимание, что в первой строке мы объявили переменную student как Person (наш абстрактный класс), однако создали объект класса-потомка ( Student ) и по факту в нашей переменной лежит именно объект класса Student . Во втором случае мы использовали уже класс-потомок для описания типа переменной. Теперь, в классе Student переопределим метод Display следующим образом:

public string Display(bool withGroup) < if (withGroup) return string.Concat(new string[] < $"", Display() >); else return Display(); >

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

Console.WriteLine(student.Display()); //метод из абстрактного класса Console.WriteLine(student1.Display(true));

Соответственно, класс преподавателя можно сделать вот таким:

class Teacher: Person < public int Seniority < get; set; >public Teacher(string name, string family, int seniority) : base(name, family) < Seniority = seniority; >>

Опять же, несмотря на то, что классы Student и Teacher — это вполне самостоятельные классы, описывающие разные сущности, в программе мы можем объявить их, используя абстрактный класс Person .

Использование методов наследника и предка

Используя наши разработанные классы, попробуйте сделать вот такой вызов метода Display() :

Person student = new Student("Вася", "Пупкин", "ГВН-105"); Console.WriteLine(student.Display(true));

Несмотря на то, что метод Diaplay() у класса Student переопределен, мы получим следующую ошибку:

Ошибка CS1501 Ни одна из перегрузок метода «Display» не принимает 1 аргументов.

Это произошло по тому, что для определения переменной student мы использовали класс Person у которого действительно нет метода Diaplay , который бы принимал хотя бы один аргумент. Чтобы воспользоваться методом наследника мы должны указать компилятору, что у нас лежит по факту в переменной student :

string res = ((Student)student).Display(true); Console.WriteLine(res);

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

string res = student.Display(); Console.WriteLine(res);

Абстрактные члены класса

Абстрактные классы в C# моuen также иметь абстрактные члены классов, которые, также как и класс, определяются с помощью ключевого слова abstract и, при этом, не имеют никакого функционала. Абстрактными могут быть:

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

  1. абстрактные члены классов не должны иметь модификатор private .
  2. производный класс обязан переопределить и реализовать все абстрактные методы и свойства, которые имеются в базовом абстрактном классе. При переопределении в производном классе такой метод или свойство объявляются с модификатором override .
  3. если класс имеет хотя бы один абстрактный член, то этот класс должен быть определен как абстрактный .

В качестве примера, сделаем в классе Person абстрактным метод Display . Как только вы пометите метод ключевым словом abstract, компилятор сразу сообщит вам следующее:

Ошибка CS0500 «Person.Display()» не может объявить тело, потому что помечен как abstract.

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

abstract class Person

Теперь для наших классов Student и Teacher появится сообщение от C#:

Ошибка CS0534 «Student» не реализует наследуемый абстрактный член «Person.Display()».

Переопределим и реализуем абстрактный метод в наших классах:

class Student: Person < public override string Display() < return $" "; > >
class Teacher: Person < public override string Display() < return $" Стаж: "; > >

теперь метод Display() переопределен (не путать с перегружен) и можно собрать нашу программу и проверить её работоспособность.

Отказ от реализации абстрактных членов

Несмотря на то, что в производных классах мы обязаны переопределить все абстрактные свойства и методы, мы можем этого не делать, но, при этом, производный класс также должен быть абстрактным. Например,

abstract class Anybody : Person < //для такого класса переопределять метод Display() не требуется >

Итого

В C# мы можем создавать некие общие для набора сущностей классы, которые называются абстрактными. Абстрактные классы помечаются ключевым словом abstract и объекты такого класса не могут быть созданы с использованием конструктора абстрактного класса. В абстрактном классе могут быть абстрактные члены (методы, свойства, индексаторы, события). При этом, производный от абстрактного класс должен переопределять все абстрактные члены, если производный класс не является также абстрактным.

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Как создать объект абстрактного класса c

Иногда возникает необходимость определить класс, который не предполагает создания конкретных объектов. Например, класс фигуры. В реальности есть конкретные фигуры: квадрат, прямоугольник, треугольник, круг и так далее. Однако абстрактной фигуры самой по себе не существует. В то же время может потребоваться определить для всех фигур какой-то общий класс, который будет содержать общую для всех функциональность. И для описания подобных сущностей используются абстрактные классы.

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

Что такое чистые виртуальные функции (pure virtual functions)? Это функции, которые не имеют определения. Цель подобных функций — просто определить функционал без реализации, а реализацию определят производные классы. Чтобы определить виртуальную функцию как чистую, ее объявление завершается значением «=0». Например, определим абстрактный класс, который представляет геометрическую фигуру:

class Shape < public: virtual double getSquare() const = 0; // площадь фигуры virtual double getPerimeter() const = 0; // периметр фигуры >;

Класс Shape является абстрактным, потому что он содержит как минимум одну чистую виртуальную функцию. А в данном случае даже две таких функции — для вычисления площади и периметра фигуры. И ни одна из функций не имеет никакой реализации. В данном случае обе функции являются константными, но это необязательно. Гловно, чтобы любой производный класс от Shape должен будет предоставить для этих функций свою реализацию.

При этом мы не можем создать объект абстрактного класса:

Shape shape<>;

Для применения абстрактного класса определим следующую программу:

#include class Shape < public: virtual double getSquare() const = 0; // площадь фигуры virtual double getPerimeter() const = 0; // периметр фигуры >; class Rectangle : public Shape // класс прямоугольника < public: Rectangle(double w, double h) : width(w), height(h) < >double getSquare() const override < return width * height; >double getPerimeter() const override < return width * 2 + height * 2; >private: double width; // ширина double height; // высота >; class Circle : public Shape // круг < public: Circle(double r) : radius(r) < >double getSquare() const override < return radius * radius * 3.14; >double getPerimeter() const override < return 2 * 3.14 * radius; >private: double radius; // радиус круга >; int main() < Rectangle rect; Circle circle; std::cout

Здесь определены два класса-наследника от абстрактного класса Shape — Rectangle (прямоугольник) и Circle (круг). При создании классов-наследников все они должны либо определить для чистых виртуальных функций конкретную реализацию, либо повторить объявление чистой виртуальной функции. Во втором случае производные классы также будут абстрактными.

В данном же случае и Circle, и Rectangle являются конкретными классами и реализуют все виртуальные функции.

Консольный вывод программы:

Rectangle square: 1500 Rectangle perimeter: 160 Circle square: 2826 Circle perimeter: 188.4

Стоит отметить, что абстрактный класс может определять и обычные функции и переменные, может иметь несколько конструкторов, но при этом нельзя создавать объекты этого абстрактного класса. Например:

#include class Shape < public: Shape(int x, int y): x, y <> virtual double getSquare() const = 0; // площадь фигуры virtual double getPerimeter() const = 0; // периметр фигуры void printCoords() const < std::cout private: int x; int y; >; class Rectangle : public Shape // класс прямоугольника < public: Rectangle(int x, int y, double w, double h) : Shape, width(w), height(h) < >double getSquare() const override < return width * height; >double getPerimeter() const override < return width * 2 + height * 2; >private: double width; // ширина double height; // высота >; class Circle : public Shape // круг < public: Circle(int x, int y, double r) : Shape, radius(r) < >double getSquare() const override < return radius * radius * 3.14; >double getPerimeter() const override < return 2 * 3.14 * radius; >private: double radius; // радиус круга >; int main() < Rectangle rect; rect.printCoords(); // X: 0 Y: 0 Circle circle; circle.printCoords(); // X: 10 Y: 20 >

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

Абстрактные классы и интерфейсы: 7‑я часть гайда по ООП

Узнайте истинную мощь наследования и полиморфизма! Раскрываем секреты абстрактных классов и интерфейсов.

Евгений Кучерявый

Евгений Кучерявый

Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.

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

Все статьи про ООП
  • Что такое классы и объекты.
  • Особенности работы с объектами.
  • Модификаторы доступа, инкапсуляция.
  • Полиморфизм и перегрузка методов.
  • Полиморфизм.
  • Наследование и ещё немного полиморфизма.
  • Абстрактные классы и интерфейсы.
  • Практикум.

Что такое абстракция в ООП

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

Абстракции часто встречаются в повседневной жизни. Например, когда мы набираем и отправляем сообщения в мессенджере, то работаем лишь с клавиатурой и кнопкой «Отправить». Мы не задумываемся о версии приложения, о том, какую кодировку использует операционная система, сколько весит наше сообщение и т.д.

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

Похожая ситуация в объектно-ориентированном программировании, только там мы имеем дело с абстракцией данных и методов. Например, чтобы рассчитать зависимость количества потребляемого топлива от веса автомобиля, нам достаточно использовать атрибуты «вес» и «номер» (чтобы различать автомобили) и метод «ехать» (для моделирования пробега).

Также можно использовать индексаторы и события (это тема для отдельной статьи). Теперь рассмотрим применение этого интерфейса.

class NPC : Character, IInteractive < public NPC(string name, int x, int y) :base(name, x, y) < >public override int Y < get < return this.y; > > public override void ShowPosition() < Console.WriteLine($"[, ]"); > public void Interact(Player p) < Console.WriteLine($" interacting with "); > >

В отличие от абстрактных методов, методы интерфейса не нужно реализовывать с ключевым словом override.

Также есть одна особенность: метод, реализация которого находится внутри интерфейса, не может использовать этот метод — класс нужно привести к интерфейсу. Для примера добавим в класс Player следующий метод:

public void Interact(IInteractive obj) < if(obj.IsInteractive()) < obj.Interact(this); > >

В качестве параметра в этот метод можно передавать любой класс, который использует интерфейс IInteractive.

Player p = new Player("Gamer", 5, 10); NPC npc = new NPC("Cube", 10, 10); p.Interact(npc); 

Это очень удобно в разработке игр, в которых взаимодействовать можно с самыми разными объектами — от NPC до предметов.

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

Домашние задание

Создайте игру, в которой будут использоваться абстрактные классы Character и Item, а также интерфейсы IInteractive, ITalkable, IMovable. Методы и свойства придумайте, исходя из названий.

Заключение

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

Больше интересного про код в нашем телеграм-канале. Подписывайтесь!

Читайте также:

  • Не Windows единой: как писать кроссплатформенные приложения с GUI на C#
  • Лицензии BSD и MIT: чем они различаются и в каких проектах их используют
  • Перечисления в C#: как правильно использовать enum

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

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