На этом шаге мы рассмотрим использование спецификаторов доступа при организации наследования.
Как отмечалось в предыдущих шагах, при наследовании из базового класса в производный "перекочевывают" открытые и защищенные члены класса. Пришло время более детально осветить этот вопрос.
Итак, при описании члена класса могут использоваться следующие спецификаторы уровня доступа: public, private, protected и internal. Есть еще вариант, что член класса будет описан вообще без спецификатора уровня доступа. Разберем, чем чревата каждая из ситуаций.
По умолчанию классы доступны в пределах сборки. Для того чтобы класс был доступен в других сборках, его описывают с ключевым словом public. Такой класс называют открытым.
Если класс описан с ключевым словом sealed (указывается перед ключевым словом class в описании класса), то такой класс не может быть базовым - то есть на основе sealed-класса нельзя создать производный класс. Например, класс String, на основе которого реализуются текстовые значения, является sealed-классом.
Мы постараемся разобраться с тем, что происходит при наследовании базового класса, в котором есть защищенные и закрытые члены класса. Для этого рассмотрим следующий пример.
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace pr140_1 { // Базовый класс с закрытым и защищенным полем: class Alpha { // Закрытое целочисленное поле: private int num; // Защищенное символьное поле: protected char symb; // Открытый метод для считывания значения // целочисленного поля: public int getNum(){ return num; } // Открытый метод для считывания значения // символьного поля: public char getSymb() { return symb; } // Открытый метод для присваивания значения // целочисленному полю: public void setNum(int n){ num = n; } // Открытый метод для присваивания значения // символьному полю: public void setSymb(char s){ symb = s; } } // Производный класс: class Bravo: Alpha { // Открытое символьное свойство: public char symbol { // Аксессор для считывания значения свойства: get { // Обращение к защищенному унаследованному полю: return symb; } // Аксессор для присваивания значения свойству: set { // Обращение к защищенному унаследованному полю: symb = value; } } // Открытое целочисленное свойство: public int number { // Аксессор для считывания значения свойства: get { // Вызов открытого унаследованного метода, // возвращающего значение закрытого поля // из базового класса: return getNum(); } // Аксессор для присваивания значения свойству: set { // Вызов открытого унаследованного метода для // присваивания значения закрытому полю // из базового класса: setNum(value); } } } // Класс с главным методом: class Program { static void Main() { // Создание объекта базового класса: Alpha A = new Alpha(); // Вызов метода для присваивания значения // целочисленному полю: A.setNum(100); // Вызов метода для присваивания значения // символьному полю: A.setSymb('A'); // Вызов методов для считывания значений полей: Console.WriteLine("Объект A: {0} и {1}", A.getNum(), A.getSymb()); // Создание объекта производного класса: Bravo B = new Bravo(); // Вызов метода для присваивания значения // целочисленному полю: B.setNum(200); // Вызов метода для присваивания значения // символьному полю: B.setSymb('B'); // Вызов методов для считывания значений полей: Console.WriteLine ("Объект В: {0} и {1}", B.getNum(), B.getSymb()); // Присваивание значения целочисленному свойству: B.number = 300; // Присваивание значения символьному свойству: B.symbol = 'C'; // Считывание значений свойств: Console.WriteLine("Объект В: {0} и {1}", B.number, B.symbol); // Задержка: Console.ReadLine(); } } }
Ниже представлен результат выполнения программы.
Рис.1. Результат выполнения программы
Мы описали класс Alpha, в котором есть закрытое целочисленное поле num, защищенное символьное поле symb и четыре открытых метода. С помощью методов getNum() и getSymb() можно получить значение соответственно целочисленного и символьного поля, а методы setNum() и setSymb() позволяют присвоить значения полям.
В главном методе программы командой
Alpha A = new Alpha();
A.setNum(100);
A.setSymb('A'); .
Для считывания значений полей мы используем выражения
A.getNum()
A.getSymb().
Другого способа обращения к полям объекта нет, поскольку оба поля (и закрытое поле num, и защищенное поле symb) вне пределов кода класса Alpha недоступны. Мы не можем обратиться через объект А напрямую к этим полям. Нужны "посредники", которыми в данном случае выступают открытые методы setNum(), setSymb(), getNum() и getSymb(). То есть в плане доступности поле symb не отличается от поля num. Разница между полями проявляется при наследовании.
Класс Bravo создается путем наследования класса Alpha. Что же "получает в наследство" класс Bravo? Во-первых, это все открытые методы. Во-вторых, в классе Bravo наследуется поле symb, поскольку оно не закрытое, а защищенное. Причем в классе Bravo это поле остается защищенным. То есть мы можем обратиться к полю symb только в теле класса Bravo. Что касается поля num, то оно не наследуется. Причем "не наследуется" следует понимать в том смысле, что в классе Bravo мы не можем обратиться к полю num. Класс Bravo "ничего не знает" о существовании этого поля, но поле в реальности существует. В теле класса Bravo мы можем обратиться к нему с помощью методов getNum() и setNum(), которые являются открытыми и наследуются из класса Alpha. Именно поэтому в описании целочисленного свойства number в классе Bravo для обращения к полю num использованы методы getNum() и setNum(). В описании символьного свойства symbol мы, по аналогии, могли бы использовать методы getSymb() и setSymb() для получения доступа к полю symb. Но пошли другим путем: поскольку поле symb наследуется, то мы воспользовались возможностью непосредственного обращения к нему.
В главном методе программы мы с помощью команды
Bravo B = new Bravo();
B.setNum(200);
B.setSymb('B');
B.number = 300;
B.symbol = 'C';
На следующем шаге мы рассмотрим наследование и конструкторы.