class Test { public static void Main() { Engineer engineer = new Engineer("Hank", 21.20F); Console.WriteLine("Name is: {0}", engineer.TypeName()); } }
Класс Engineer будет служить базовым классом. Он содержит закрытое поле name и защищенное поле billingRate. Модификатор protected
предоставляет такой же доступ к полю, как и private, но к
защищенным полям имеют доступ все производные классы. Поэтому protected
используется для предоставления подклассам доступа к данному полю.
При использовании модификатора protected подклассы оказываются
зависимыми от внутренней реализации наследуемого класса, поэтому этот
модификатор следует использовать только при необходимости. В приведенном
примере поле billingRate не
может быть переименовано, поскольку подклассы имеют к нему доступ. Часто
наилучшим решением оказывается использование защищенных свойств.
Класс Engineer также имеет функцию-член, которая может использоваться
для вычисления нагрузки с учетом количества рабочих часов.
Простое наследование
Инженер-строитель - это разновидность инженера, поэтому класс
CivilEngineer можно сделать производным от класса Engineer:
class CivilEngineer: Engineer { public CivilEngineer(string name, float billingRate) : base(name, billingRate) { }
// new function, because it's different than the // base version public new float CalculateCharge(float hours) { if (hours < 1.0F) hours = 1.0F; // minimum charge. return(hours * billingRate); }
// new function, because it's different than the // base version public new string TypeName() { return("Civil Engineer"); } }
class Test { public static void Main() { Engineer e = new Engineer("George", 15.50F); CivilEngineer c = new CivilEngineer("Sir John", 40F); Console.WriteLine("{0} charge = {1}", e.TypeName(), e.CalculateCharge(2F)); Console.WriteLine("{0} charge = {1}", c.TypeName(), c.CalculateCharge(0.75F)); } }
Поскольку класс CivilEngineer является производным от класса Engineer,
он наследует все поля класса Engineer (хотя и не может обратиться к
закрытому полю name). Кроме
того, CivilEngineer также наследует функцию-член CalculateCharge().
Конструкторы нельзя наследовать, поэтому в подклассе CivilEngineer
определен свой конструктор. Конструктор класса CivilEngineer не должен
делать ничего особенного, поэтому он просто вызывает конструктор класса
Engineer, используя ключевое слово base. Если бы вызов
конструктора базового класса был опущен, компилятор вызвал бы
конструктор базового класса без параметров.
Для инженера-строителя нагрузка вычисляется иначе, поэтому в классе
CivilEngineer реализована новая версия метода CalculateCharge().
При запуске этой программы будут выведены следующие строки:
Engineer Charge = 31
Civil Engineer Charge = 40
Массивы объектов Engineer
Когда речь идет о нескольких работниках, можно работать с отдельными
объектами. Но для больших компаний удобнее составлять массивы объектов.
Поскольку класс CivilEngineer является производным от класса Engineer,
массив для объектов типа Engineer может содержать объекты обоих типов. В
следующем примере приведена функция Main(), которая создает массив
объектов:
class Test { public static void Main() { // create an array of engineers Engineer[] earray = new Engineer[2]; earray[0] = new Engineer("George", 15.50F); earray[1] = new CivilEngineer("Sir John", 40F); Console.WriteLine("{0} charge = {1}", earray[0].TypeName(), earray[0].CalculateCharge(2F)); Console.WriteLine("{0} charge = {1}", earray[1].TypeName(), earray[1].CalculateCharge(0.75F)); } }
При запуске этой программы будут выведены следующие строки:
Engineer Charge = 31
Engineer Charge = 30
Мы получили неверный результат.
Когда объекты были помещены в массив, тот факт, что второй объект в
действительности принадлежит классу CivilEngineer, а не Engineer, был
проигнорирован. Поскольку был объявлен массив для объектов класса
Engineer, при вызове метода CalculateCharge() вызывается версия метода,
которая объявлена в классе Engineer.
Чтобы избежать ошибок, необходимо правильно определять тип объекта. Для
этого в классе Engineer можно завести поле, содержащее информацию о том,
к какому типу принадлежит объект. Перепишем класс с использованием
перечисления в качестве поля, обозначающего тип объекта:
using System; enum EngineerTypeEnum { Engineer, CivilEngineer }
class Engineer { public Engineer(string name, float billingRate) { this.name = name; this.billingRate = billingRate; type = EngineerTypeEnum.Engineer; }
public float CalculateCharge(float hours) { if (type == EngineerTypeEnum.CivilEngineer) { CivilEngineer c = (CivilEngineer) this; return(c.CalculateCharge(hours)); } else if (type == EngineerTypeEnum.Engineer) return(hours * billingRate); return(0F); }
public string TypeName() { if (type == EngineerTypeEnum.CivilEngineer) { CivilEngineer c = (CivilEngineer) this; return(c.TypeName()); } else if (type == EngineerTypeEnum.Engineer) return("Engineer"); return("No Type Matched"); }
class CivilEngineer: Engineer { public CivilEngineer(string name, float billingRate) : base(name, billingRate) { type = EngineerTypeEnum.CivilEngineer; }
public new float CalculateCharge(float hours) { if (hours < 1.0F) hours = 1.0F; // minimum charge. return(hours * billingRate); }
public new string TypeName() { return("Civil Engineer"); } }
class Test { public static void Main() { Engineer[] earray = new Engineer[2]; earray[0] = new Engineer("George", 15.50F); earray[1] = new CivilEngineer("Sir John", 40F); Console.WriteLine("{0} charge = {1}", earray[0].TypeName(), earray[0].CalculateCharge(2F)); Console.WriteLine("{0} charge = {1}", earray[1].TypeName(), earray[1].CalculateCharge(0.75F)); } }
Проверяя поле type, методы
класса Engineer могут определить действительный тип объекта и вызвать
соответствующую функцию.
Программа выведет следующие строки:
Engineer Charge = 31
Civil Engineer Charge = 40
К сожалению, базовый класс теперь стал гораздо более сложным. Каждая
функция, которую интересует тип объекта, будет содержать код,
перебирающий все возможные типы, чтобы вызвать подходящую функцию. При
этом прибавится много дополнительного кода, и этот способ становится
неприемлемым, если существует, например, более 50 видов инженеров.
Еще более неудобным является тот факт, что при использовании этого
способа базовый класс должен знать имена всех подклассов. Если позже
будет необходимо добавить новый тип инженера, придется изменить и
базовый класс. Если пользователь, не имеющий доступа к базовому классу,
попытается прибавить новый тип инженера, программа не будет работать
вовсе.
Виртуальные функции
Для решения описанных выше проблем, в объектно-ориентированных языках
предусмотрена возможность определения виртуальных функций. Виртуальная
функция отличается тем, что при ее вызове компилятор должен проверить
реальный тип объекта (а не только тип ссылки) и вызвать функцию,
соответствующую этому типу.
Используя виртуальные функции, можно переписать предыдущий пример
следующим образом:
using System; class Engineer { public Engineer(string name, float billingRate) { this.name = name; this.billingRate = billingRate; }
// function now virtual virtual public float CalculateCharge(float hours) { return(hours * billingRate); }
// function now virtual virtual public string TypeName() { return("Engineer"); }
class CivilEngineer: Engineer { public CivilEngineer(string name, float billingRate) : base(name, billingRate) { }
// overrides function in Engineer override public float CalculateCharge(float hours) { if (hours < 1.0F) hours = 1.0F; // minimum charge. return(hours * billingRate); }
// overrides function in Engineer override public string TypeName() { return("Civil Engineer"); } }
class Test { public static void Main() { Engineer[] earray = new Engineer[2]; earray[0] = new Engineer("George", 15.50F); earray[1] = new CivilEngineer("Sir John", 40F); Console.WriteLine("{0} charge = {1}", earray[0].TypeName(), earray[0].CalculateCharge(2F)); Console.WriteLine("{0} charge = {1}", earray[1].TypeName(), earray[1].CalculateCharge(0.75F)); } }
Функции CalculateCharge() и TypeName() теперь объявлены с ключевым
словом virtual в базовом классе. И это - все, что должен знать
базовый класс. Он не должен ничего знать о производных типах за
исключением того, что любой подкласс может создать свою реализацию
CalculateCharge() и TypeName(), если возникнет такая необходимость. В
подклассе функции объявляются с ключевым словом override. Это
означает, что они переопределяют функцию, которая была объявлена в
базовом классе. Если ключевое слово override будет опущено,
компилятор предположит, что данная функция не имеет отношения к функции
базового класса, и виртуальная диспетчеризация не будет функционировать.
При запуске этой программы будут выведены следующие строки:
Engineer Charge = 31
Civil Engineer Charge = 40
Когда компилятор сталкивается с вызовом TypeName() или
CalculateCharge(), он находит определение функции и обращает внимания,
что функция является виртуальной. Вместо генерации кода, непосредственно
вызывающего функцию, компилятор записывает код диспетчеризации, который
будет проверять реальный тип объекта во время выполнения, и вызывать
функцию, соответствующую реальному типу объекта, а не типу ссылки. Это
позволяет вызывать правильную функцию, даже если класс еще не был
реализован на момент компиляции вызывающей программы.
Виртуальная диспетчеризация приводит к небольшому снижению
эффективности, поэтому не следует использовать виртуальные функции без
необходимости. JIT (Just-In-Time компилятор) может, однако, обратить
внимание, что у класса, для которого вызывалась функция, нет никаких
подклассов и преобразовать код виртуальной диспетчеризации в прямой
вызов функции.
Абстрактные классы
Подход, который мы использовали до сих пор, имеет небольшой недостаток.
Новый класс не обязан реализовывать функцию TypeName(), так как он может
наследовать ее реализацию от класса Engineer. Это может привести к тому,
что с новым производным классом будет ассоциироваться неправильное
название типа. Например, мы можем добавить класс ChemicalEngineer:
using System; class Engineer { public Engineer(string name, float billingRate) { this.name = name; this.billingRate = billingRate; }
virtual public float CalculateCharge(float hours) { return(hours * billingRate); }
virtual public string TypeName() { return("Engineer"); }
class ChemicalEngineer: Engineer { public ChemicalEngineer(string name, float billingRate) : base(name, billingRate) { }
// overrides mistakenly omitted }
class Test { public static void Main() { Engineer[] earray = new Engineer[2]; earray[0] = new Engineer("George", 15.50F); earray[1] = new ChemicalEngineer("Dr. Curie", 45.50F); Console.WriteLine("{0} charge = {1}", earray[0].TypeName(), earray[0].CalculateCharge(2F)); Console.WriteLine("{0} charge = {1}", earray[1].TypeName(), earray[1].CalculateCharge(0.75F)); } }
Класс ChemicalEngineer наследует от класса Engineer функцию
CalculateCharge(), которая может быть будет работать правильно, но
ChemicalEngineer также наследует функцию TypeName(), которая определенно
будет работать неправильно. Поэтому необходимо вынудить ChemicalEngineer
создать свою реализацию функции TypeName().
Этого можно добиться, сделав класс Engineer абстрактным. В абстрактном
классе Engineer нужно пометить функцию TypeName() как абстрактную. Это
означает, что все подклассы будут обязаны создать реализацию функции
TypeName().
Создание абстрактного класса означает, что от него планируется
производить подклассы. В абстрактном классе отсутствует "требуемая"
функциональность, поэтому он не может быть инициализирован. Это, в
частности, означает, что объекты класса Engineer не могут быть созданы.
Абстрактные классы ведут себя как обычные классы за исключением
функций-членов, помеченных как абстрактные.
using System; abstract class Engineer { public Engineer(string name, float billingRate) { this.name = name; this.billingRate = billingRate; }
virtual public float CalculateCharge(float hours) { return(hours * billingRate); }
class CivilEngineer: Engineer { public CivilEngineer(string name, float billingRate) : base(name, billingRate) { }
override public float CalculateCharge(float hours) { if (hours < 1.0F) hours = 1.0F; // minimum charge. return(hours * billingRate); }
override public string TypeName() { return("Civil Engineer"); } }
class ChemicalEngineer: Engineer { public ChemicalEngineer(string name, float billingRate) : base(name, billingRate) { }
override public string TypeName() { return("Chemical Engineer"); } }
class Test { public static void Main() { Engineer[] earray = new Engineer[2]; earray[0] = new CivilEngineer("Sir John", 40.0F); earray[1] = new ChemicalEngineer("Dr. Curie", 45.0F); Console.WriteLine("{0} charge = {1}", earray[0].TypeName(), earray[0].CalculateCharge(2F)); Console.WriteLine("{0} charge = {1}", earray[1].TypeName(), earray[1].CalculateCharge(0.75F)); } }
Класс Engineer изменился в результате добавления ключевого слова abstract перед определением класса.
Ключевое слово abstract
указывает, что класс является абстрактным (то есть, имеет одну или более
абстрактных функций). Ключевое слово abstract
также было добавлено перед объявлением виртуальной функции TypeName().
Реализация класса CivilEngineer осталось прежней, но теперь компилятор
проверит, присутствует ли реализация функция TypeName() и в классе
CivilEngineer, и в классе ChemicalEngineer.
Ключевое слово sealed
Ключевое слово sealed запрещает использовать класс в качестве
базового. Это бывает особенно полезно для предотвращения
непреднамеренного наследования.
// error sealed class MyClass { MyClass() {} }
class MyNewClass : MyClass { }
Компилятор выдаст сообщение об ошибке, поскольку класс MyClass объявлен
как sealed и от него нельзя производить подклассы.