Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
Глава 11 Наследование 278 Часть I. Язык C# аследование — один из трех фундаментальных принципов объектно- ориентированного программирования, поскольку именно благодаря ему возможно создание иерархических классификаций. Используя наследование, можно создать общий класс, который определяет характеристики, присущие множеству связанных элементов. Этот класс затем может быть унаследован другими, узкоспециализированными классами с добавлением в каждый из них своих, уникальных особенностей. В языке C# класс, который наследуется, называется базовым. Класс, который наследует базовый класс, называется производным. Следовательно, производный класс — это специализированная версия базового класса. В производный класс, наследующий все переменные, методы, свойства, операторы и индексаторы, определенные в базовом классе, могут быть добавлены уникальные элементы. Основы наследования C# поддерживает наследование, позволяя в объявление класса встраивать другой класс. Это реализуется посредством задания базового класса при объявлении производного. Лучше всего начать с примера. Рассмотрим класс TwoDShape , в котором определяются атрибуты “обобщенной” двумерной геометрической фигуры (например, квадрата, прямоугольника, треугольника и т.д.). // Класс двумерных объектов. class TwoDShape { public double width; public double height; public void showDim() { Console.WriteLine("Ширина и высота равны " + width + " и " + height); } } Класс TwoDShape можно использовать в качестве базового (т.е. как стартовую площадку) для классов, которые описывают специфические типы двумерных объектов. Например, в следующей программе класс TwoDShape используется для выведения класса Triangle . Обратите внимание на то, как объявляется класс Triangle // Простая иерархия классов. using System; // Класс двумерных объектов. class TwoDShape { public double width; public double height; public void showDim() { Console.WriteLine("Ширина и высота равны " + width + " и " + height); } } // Класс Triangle выводится из класса TwoDShape. class Triangle : TwoDShape { public string style; // Тип треугольника. Н Глава 11. Наследование 279 // Метод возвращает площадь треугольника. public double area() { return width * height / 2; } // Отображаем тип треугольника. public void showStyle() { Console.WriteLine("Треугольник " + style); } } class Shapes { public static void Main() { Triangle t1 = new Triangle(); Triangle t2 = new Triangle(); t1.width = 4.0; t1.height = 4.0; t1.style = "равнобедренный"; t2.width = 8.0; t2.height = 12.0; t2.style = "прямоугольный"; Console.WriteLine("Информация о t1: "); t1.showStyle(); t1.showDim(); Console.WriteLine("Площадь равна " + t1.area()); Console.WriteLine(); Console.WriteLine("Информация о t2: "); t2.showStyle(); t2.showDim(); Console.WriteLine("Площадь равна " + t2.area()); } } Вот результаты работы этой программы. Информация о t1: Треугольник равнобедренный Ширина и высота равны 4 и 4 Площадь равна 8 Информация о t2: Треугольник прямоугольный Ширина и высота равны 8 и 12 Площадь равна 48 В классе Triangle создается специфический тип объекта класса TwoDShape , в данном случае треугольник. Класс Triangle содержит все элементы класса TwoDShape и, кроме того, поле style , метод area() и метод showStyle() . В переменной style хранится описание типа треугольника, метод area() вычисляет и возвращает его площадь, а метод showStyle() отображает данные о типе треугольника. Ниже приведен синтаксис, который используется в объявлении класса Triangle , чтобы сделать его производным от класса TwoDShape class Triangle : TwoDShape { 280 Часть I. Язык C# Этот синтаксис можно обобщить. Если один класс наследует другой, то имя базового класса указывается после имени производного, причем имена классов разделяются двоеточием. В C# синтаксис наследования класса очень прост для запоминания и использования. Поскольку класс Triangle включает все члены базового класса, TwoDShape , он может обращаться к членам width и height внутри метода area() . Кроме того, внутри метода Main() объекты t1 и t2 могут прямо ссылаться на члены width и height , как если бы они были частью класса Triangle . Включение класса TwoDShape в класс Triangle схематически показано на рис. 11.1. Рис. 11.1. Схематическое представление класса Triangle Несмотря на то что класс TwoDShape является базовым для класса Triangle , это совершенно независимый и автономный класс. То, что его использует в качестве базового производный класс (классы), не означает невозможность использования его самого. Например, следующий фрагмент кода абсолютно легален: TwoDShape shape = new TwoDShape(); shape.width = 10; shape.height = 20; shape.showDim(); Безусловно, объект класса TwoDShape “ничего не знает” и не имеет права доступа к классу, производному от TwoDShape Общая форма объявления класса, который наследует базовый класс, имеет такой вид: class имя_производного_класса : имя_базового_класса { // тело класса } Для создаваемого производного класса можно указать только один базовый класс. В C# (в отличие от C++) не поддерживается наследование нескольких базовых классов в одном производном классе. Этот факт необходимо учитывать при переводе C++-кода на C#. Однако можно создать иерархию наследования, в которой один производный класс становится базовым для другого производного класса. И конечно же, ни один класс не может быть базовым (ни прямо, ни косвенно) для самого себя. Основное достоинство наследования состоит в том, что, создав базовый класс, который определяет общие атрибуты для множества объектов, его можно использовать для создания любого числа более специализированных производных классов. В каждом производном классе можно затем точно “настроить” собственную классификацию. Вот, например, как из базового класса TwoDShape можно вывести производный класс, который инкапсулирует прямоугольники: width height showDim() style area showStyle() Triangle TwoDShape Глава 11. Наследование 281 // Класс прямоугольников Rectangle, производный //от класса TwoDShape. class Rectangle : TwoDShape { // Метод возвращает значение true, если // прямоугольник является квадратом. public bool isSquare() { if(width == height) return true; return false; } // Метод возвращает значение площади прямоугольника. public double area() { return width * height; } } Класс Rectangle включает класс TwoDShape и добавляет метод isSquare() , который определяет, является ли прямоугольник квадратом, и метод area() , вычисляющий площадь прямоугольника. Доступ к членам класса и наследование Как разъяснялось в главе 8, члены класса часто объявляются закрытыми, чтобы предотвратить несанкционированное использование и внесение изменений. Наследование класса не отменяет ограничения, связанные с закрытым доступом. Таким образом, несмотря на то, что производный класс включает все члены базового класса, он не может получить доступ к тем из них, которые объявлены закрытыми. Например, как показано в следующем коде, если члены width и height являются private -членами в классе TwoDShape , то класс Triangle не сможет получить к ним доступ, // Доступ к закрытым членам не наследуется. // Этот пример не скомпилируется. using System; // Класс двумерных объектов. class TwoDShape { double width; // Теперь это private-член. double height; // Теперь это private-член. public void showDim() { Console.WriteLine("Ширина и высота равны " + width + " и " + height); } } // Класс Triangle выводится из класса TwoDShape. class Triangle : TwoDShape { public string style; // Тип треугольника. // Метод возвращает значение площади треугольника. public double area() { return width * height /2; // Ошибка, нельзя получить // прямой доступ к закрытым // членам. } 282 Часть I. Язык C# // Отображаем тип треугольника. public void showStyle() { Console.WriteLine("Треугольник " + style); } } Класс Triangle не скомпилируется, поскольку ссылка на члены width и height внутри метода area() вызовет ошибку нарушения прав доступа. Поскольку width и height — закрытые члены, они доступны только для членов их собственного класса. На производные классы эта доступность не распространяется. Совет Закрытый член класса остается закрытым в рамках этого класса. К нему нельзя получить доступ из кода, определенного вне этого класса, включая производные классы. На первый взгляд может показаться, что невозможность доступа к закрытым членам базового класса со стороны производного — серьезное ограничение. Однако это не так, поскольку в C# предусмотрены возможности решения этой проблемы. Одна из них — protected -члены, о которых пойдет речь в следующем разделе. Вторая возможность — использование открытых свойств и методов, позволяющих получить доступ к закрытым данным. Как было показано в предыдущих главах, C#-программисты обычно предоставляют доступ к закрытым членам класса посредством открытых методов или путем превращения их в свойства. Перед вами — новая версия класса TwoDShape , в котором бывшие члены width и height стали свойствами. // Использование свойств для записи и чтения закрытых // членов класса. using System; // Класс двумерных объектов. class TwoDShape { double pri_width; // Теперь это private-член. double pri_height; // Теперь это private-член. // Свойства width и height. public double width { get { return pri_width; } set { pri_width = value; } } public double height { get { return pri_height; } set { pri_height = value; } } public void showDim() { Console.WriteLine("Ширина и высота равны " + width + " и " + height); } } // Класс треугольников - производный от класса TwoDShape. class Triangle : TwoDShape { public string style; // Тип треугольника. // Метод возвращает значение площади треугольника. public double area() { return width * height / 2; Глава 11. Наследование 283 } // Отображаем тип треугольника. public void showStyle() { Console.WriteLine("Треугольник " + style); } } class Shapes2 { public static void Main() { Triangle t1 = new Triangle(); Triangle t2 = new Triangle(); t1.width = 4.0; t1.height = 4.0; t1.style = "равнобедренный"; t2.width = 8.0; t2.height = 12.0; t2.style = "прямоугольный"; Console.WriteLine("Информация о t1: "); t1.showStyle(); t1.showDim(); Console.WriteLine("Площадь равна " + t1.area()); Console.WriteLine(); Console.WriteLine("Информация о t2: "); t2.showStyle(); t2.showDim(); Console.WriteLine("Площадь равна " + t2.area()); } } Базовый и производный классы иногда называют суперклассом и подклассом. Эти термины пришли из программирования на языке Java. Суперкласс в Java — это базовый класс в C#. Подкласс в Java — это производный класс в C#. Вероятно, вам приходилось слышать эти термины, но мы будем придерживаться стандартных C#-терминов. В C++ также используются термины “базовый класс/производный класс”. Использование защищенного доступа Как упоминалось выше, закрытый член базового класса недоступен для производного класса. Казалось бы, это означает, что, если производный класс должен иметь доступ к члену базового класса, его нужно сделать открытым. При этом придется смириться с тем, что открытый член будет доступным для любого другого кода, что иногда нежелательно. К счастью, таких ситуации можно избежать, поскольку C# позволяет создавать защищенные члены. Защищенным является член, который открыт для своей иерархии классов, но закрыт вне этой иерархии. Защищенный член создается с помощью модификатора доступа protected . При объявлении protected -члена он по сути является закрытым, но с одним исключением. Это исключение вступает в силу, когда защищенный член наследуется. В этом случае защищенный член базового класса становится защищенным членом производного класса, а следовательно, и доступным для производного класса. Таким образом, используя модификатор доступа protected , можно создавать закрытые (для “внешнего 284 Часть I. Язык C# мира”) члены класса, но вместе с тем они будут наследоваться с возможностью доступа со стороны производных классов. Рассмотрим простой пример использования защищенных членов класса. // Демонстрация использования защищенных членов класса. using System; class В { protected int i, j; // Закрыт внутри класса В, //но доступен для класса D. public void set(int a, int b) { i = a; j = b; } public void show() { Console.WriteLine(i + " " + j); } } class D : В { int k; // Закрытый член. // Класс D получает доступ к членам i и j класса В. public void setk() { k = i * j; } public void showk() { Console.WriteLine(k); } } class ProtectedDemo { public static void Main() { D ob = new D(); ob.set(2, 3); // OK, так как D "видит" В-члены i и j. ob.show(); // OK, так как D "видит" В-члены i и j. ob.setk(); // OK, так как это часть самого класса D. ob.showk(); // OK, так как это часть самого класса D. } } Поскольку в этом примере класс в наследуется классом D и члены i и j объявлены защищенными в классе в (т.е. с использованием модификатора доступа protected ), метод setk() может получить к ним доступ. Если бы члены i и j были объявлены в классе в закрытыми, класс D не имел бы к ним права доступа, и программа не скомпилировалась бы. Подобно модификаторам public и private модификатор protected остается со своим членом независимо от реализуемого количества уровней наследования. Следовательно, при использовании производного класса в качестве базового для создания другого производного класса любой защищенный член исходного базового класса, который наследуется первым производным классом, также наследуется в статусе защищенного и вторым производным классом. Глава 11. Наследование 285 Конструкторы и наследование В иерархии классов как базовые, так и производные классы могут иметь собственные конструкторы. При этом возникает важный вопрос: какой конструктор отвечает за создание объекта производного класса? Конструктор базового или конструктор производного класса, или оба одновременно? Ответ таков: конструктор базового класса создает часть объекта, соответствующую базовому классу, а конструктор производного класса — часть объекта, соответствующую производному классу. И это вполне логично, потому что базовый класс “не видит” или не имеет доступа к элементам производного класса. Поэтому их конструкции должны быть раздельными. В предыдущих примерах классы опирались на конструкторы по умолчанию, создаваемые автоматически средствами C#, и поэтому мы не сталкивались с подобной проблемой. Но на практике большинство классов имеет конструкторы, и вы должны знать, как справляться с подобной ситуацией. Если конструктор определяется только в производном классе, процесс создания объекта несложен: просто создается объект производного класса. Часть объекта, соответствующая базовому классу, создается автоматически с помощью конструктора по умолчанию. Например, рассмотрим переработанную версию класса Triangle , в которой определяется конструктор. Здесь член style объявлен private -членом, поскольку теперь он устанавливается конструктором. // Добавление конструктора в класс Triangle. using System; // Класс двумерных объектов. class TwoDShape { double pri_width; // Закрытый член. double pri_height; // Закрытый член. // Свойства width и height. public double width { get { return pri_width; } set { pri_width = value; } } public double height { get { return pri_height; } set { pri_height = value; } } public void showDim() { Console.WriteLine("Ширина и высота равны " + width + " и " + height); } } // Класс треугольников - производный от класса TwoDShape. class Triangle : TwoDShape { string style; // Закрытый член. // Конструктор. public Triangle(string s, double w, double h) { width = w; // Инициализирует член базового класса. height = h; // Инициализирует член базового класса. 286 Часть I. Язык C# style = s; // Инициализирует член своего класса. } // Метод возвращает значение площади треугольника. public double area() { return width * height / 2; } // Отображаем тип треугольника. public void showStyle() { Console.WriteLine("Треугольник " + style); } } class Shapes3 { public static void Main() { Triangle t1 = new Triangle("равнобедренный", 4.0, 4.0); Triangle t2 = new Triangle("прямоугольный", 8.0, 12.0); Console. WriteLine("Информация о t1: "); t1.showStyle(); t1.showDim(); Console.WriteLine("Площадь равна " + t1.area()); Console.WriteLine(); Console.WriteLine("Информация о t2: "); t2.showStyle(); t2.showDim(); Console.WriteLine("Площадь равна " + t2.area()); } } Здесь конструктор класса Triangle инициализирует наследуемые им члены класса TwoDShape , а также собственное поле style Если конструкторы определены и в базовом, и в производном классе, процесс создания объектов несколько усложняется, поскольку должны выполниться конструкторы обоих классов. В этом случае необходимо использовать еще одно ключевое слово C# base , которое имеет два назначения: вызвать конструктор базового класса и получить доступ к члену базового класса, который скрыт “за” членом производного класса. Сначала рассмотрим первое назначение слова base |