Шаг 140.
Язык программирования C#. Начала
Наследование. Наследование и уровни доступа

    На этом шаге мы рассмотрим использование спецификаторов доступа при организации наследования.

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

    Итак, при описании члена класса могут использоваться следующие спецификаторы уровня доступа: public, private, protected и internal. Есть еще вариант, что член класса будет описан вообще без спецификатора уровня доступа. Разберем, чем чревата каждая из ситуаций.


Член класса может быть описан только с одним спецификатором уровня доступа. Исключение составляет комбинация protected internal. Член класса, описанный с комбинацией ключевых слов 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();
создается объект А класса 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();
создаем объект В класса Bravo. Через этот объект мы можем напрямую обращаться к четырем методам и свойствам number и symbol. Поле symb является защищенным, поэтому вне пределов кода класса Bravo оно не доступно. А поле num вообще не наследуется (в указанном выше смысле). Сначала командами
  B.setNum(200);
и
  B.setSymb('B');
полям присваиваются значения, а проверяем мы эти значения с помощью выражений В.getNum() и В.getSymb(). Но есть и другой путь для получения доступа к полям: с помощью свойств. Так, командами
  B.number = 300;
и
  B.symbol = 'C';
записываем в поля новые значения, а с помощью инструкций В.number и В.symbol проверяем результат.

    На следующем шаге мы рассмотрим наследование и конструкторы.




Предыдущий шаг Содержание Следующий шаг