Шаг 143.
Язык программирования C#. Начала
Наследование. Объектные переменные базовых классов

    На этом шаге мы рассмотрим особенности организации доступа к таким переменным.

    Ранее практически каждый раз при работе с объектами мы ссылку на объект записывали в объектную переменную того же класса. Механизм наследования вносит в это строгое правило некоторое разнообразие. Дело в том, что объектная переменная базового класса может ссылаться на объект производного. Допустим, имеется базовый класс Base, на основе которого наследованием создается класс MyClass. Если мы создадим на основе класса MyClass объект, то ссылку на этот объект можно записать не только в объектную переменную класса MyClass, но и в объектную переменную базового класса Base. Правда, здесь есть одно очень важное ограничение: через объектную переменную базового класса можно получить доступ только к тем членам производного класса, которые объявлены в базовом классе. В нашем случае это означает, что через объектную переменную класса Base можно получить доступ только к членам объекта производного класса MyClass, объявленным в классе Base.


Здесь уместно вспомнить про класс Object из пространства имен System (используют также псевдоним object для инструкции System.Object), который находится в вершине иерархии наследования всех классов (включая и создаваемые нами). Это обстоятельство позволяет ссылаться объектным переменным класса Object на объекты самых разных классов.

    Даже несмотря на упомянутое ограничение, данная особенность объектных переменных базовых классов является фундаментально важной и проявляется и используется в самых разных (часто неожиданных) ситуациях. Как небольшую иллюстрацию рассмотрим приведенную ниже программу.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace pr143_1
{
    // Базовый класс: 
    class Base {
        // Целочисленное поле: 
        public int code;
        // Открытый метод: 
        public void show() {
            Console.WriteLine("Поле code: " + code);
        }
        // Конструктор с целочисленным аргументом: 
        public Base(int n){ 
            code = n;
        }
        // Конструктор создания копии: 
        public Base(Base obj){ 
            code = obj.code;
        }
    }

    // Производный класс: 
    class MyClass: Base { 
        // Символьное поле: 
        public char symb;
        // Открытый метод: 
        public void display() {
            Console.WriteLine("Поле symb: " + symb);
        }
        // Конструктор с двумя аргументами: 
        public MyClass(int n, char s): base(n){ 
            symb = s;
        }
        // Конструктор создания копии:
        public MyClass(MyClass obj): base(obj) { 
            symb = obj.symb;
        }
    }

    // Класс с главным методом: 
    class Program
    {
        // Главный метод:
        static void Main()
        {
            // Создание объекта производного класса:
            MyClass A = new MyClass(100, 'A');
            // Объектная переменная базового класса:
            Base obj;
            // Объектной переменной базового класса присваивается
            // ссылка на объект производного класса:
            obj = A;
            Console.WriteLine("Используем переменную obj:");
            // Вызов метода через объектную переменную 
            // базового класса: 
            obj.show();
            // Использовано явное приведение типа:
            ((MyClass)obj).display();
            // Обращение к полю через объектную переменную
            // базового класса:
            obj.code = 200;
            // Использовано явное приведение типа:
            ((MyClass)obj).symb = 'В';
            Console.WriteLine("Используем переменную А:");
            // Вызов методов через объектную переменную 
            // производного класса:
            A.show();
            A.display();
            // Создание копии объекта: 
            MyClass B = new MyClass(A);
            // Изменение значений полей исходного объекта:
            A.code = 0;
            A.symb = 'O';
            Console.WriteLine("Используем переменную В:");
            // Проверка значений полей объекта-копии:
            B.show();
            B.display();
            // Задержка:
            Console.ReadLine();
        }
    }
}
Архив проекта можно взять здесь.

    Ниже представлен результат выполнения программы.


Рис.1. Результат выполнения программы

    В этой программе мы описываем базовый класс Base. У него есть открытое целочисленное поле code, метод show(), который при вызове отображает в консольном окне значение целочисленного поля, а также в классе есть две версии конструктора. Есть конструктор с одним целочисленным аргументом, и еще имеется конструктор создания копии, аргументом которому передается объект класса Base.


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

    В нашем примере поле code создаваемого объекта получает такое же значение, какое есть у поля code объекта, переданного аргументом конструктору (команда

   code = obj.code;
в теле конструктора, а через obj обозначен его аргумент). При этом объекты физически разные.

    На основе класса Base путем наследования создается класс MyClass. В этом классе дополнительно описывается символьное поле symb, метод display(), отображающий в консольном окне значение символьного поля, а также две версии конструктора. В соответствии с описанными версиями объект класса MyClass можно создавать, передав два аргумента конструктору (целое число и символ) или передав аргументом уже существующий объект класса MyClass (конструктор создания копии). Если при создании объекта класса MyClass используются два аргумента, то первый, целочисленный, аргумент передается конструктору базового класса, а второй, символьный, аргумент определяет значение символьного поля. С конструктором создания копии все намного интереснее. Аргумент конструктора obj описан как такой, что является объектной ссылкой класса MyClass. И эта объектная ссылка передается конструктору базового класса (инструкция

  base(obj) 
в описании конструктора класса MyClass). Здесь имеется в виду версия конструктора базового класса, у которой аргументом является объект. Но дело в том, что в классе Base описан конструктор с аргументом, являющимся объектной переменной класса Base. А здесь мы по факту передаем аргументом объектную переменную класса MyClass. Тем не менее это возможно, и ошибки нет. Почему? Ответ базируется на возможности для объектных переменных базового класса ссылаться на объекты производного класса. Когда вызывается конструктор базового класса, то он "ожидает получить" ссылку на объект класса Base. Такая ссылка - аргумент конструктора. Чтобы ее запомнить, создается техническая объектная переменная класса Base, в которую будет копироваться значение аргумента.


Напомним, что аргументы в методы и конструкторы передаются по значению: на самом деле для переменной, переданной аргументом, создается техническая копия, и все операции выполняются именно с этой копией.

    Другими словами, есть специальная "рабочая", автоматически создаваемая переменная, в которую копируется аргумент конструктора. Данная переменная относится к классу Base, а передается аргументом переменная класса MyClass. Получается, что в объектную переменную класса Base копируется значение переменной класса MyClass. Но это можно делать, поскольку класс Base является базовым для класса MyClass.

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

  symb = obj.symb;
в теле конструктора производного класса значение присваивается символьному полю создаваемого объекта.

    В главном методе программы командой

  MyClass A = new MyClass(100, 'A'); 
создается объект производного класса MyClass. Также мы объявляем объектную переменную obj базового класса Base. После выполнения команды
  obj = A;
и переменная obj, и переменная А ссылаются на один и гот же объект. Но если через переменную А есть доступ к полям code и symb, а также методам show() и display(), то через переменную obj имеется доступ только к тем полям и методам, которые объявлены в классе Base: это поле code и метод show(). Поэтому команды
  obj.show()
и
  obj.code = 200;
являются вполне корректными. А вот если мы хотим через переменную obj получить доступ к полю symb и методу display(), то приходится выполнять явное приведение типа - то есть фактически явно указать, что объект, на который ссылается переменная obj, является объектом класса MyClass. Пример такого подхода дают команды
  ((MyClass)obj).display();
и
  ((MyClass)obj).symb = 'В';

    После присваивания с использованием переменной obj новых значений полям code и symb, используя переменную А, убеждаемся, что переменные obj и А действительно ссылаются на один и тот же объект.

    Командой

  MyClass B = new MyClass(A); 
объект В создается на основе объекта А (так мы проверяем работу конструктора создания копии производного класса). На момент создания объекта В значения его полей такие же, как и у объекта А, но объекты физически разные. Чтобы убедиться в этом, командами
  А.code = 0; 
и
  A.symb = 'O'; 
изменяем значения полей исходного объекта (на основе которого создавалась копия), а с помощью команд
  В.show();
и
  В.display();
убеждаемся, что значения полей объекта В остались такими же, как были у объекта А на момент создания объекта В.

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




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