Главная страница
Навигация по странице:

  • Ссылки на базовый класс и объекты производных классов

  • Виртуальные методы и их переопределение

  • Зачем переопределять методы

  • Применение виртуальных методов

  • Использование абстрактных классов

  • Метод

  • Приведение к объектному типу и восстановление значения

  • Использование класса object в качестве обобщенного типа данных

  • Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией


    Скачать 5.05 Mb.
    НазваниеСправочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
    АнкорC #.pdf
    Дата08.12.2017
    Размер5.05 Mb.
    Формат файлаpdf
    Имя файлаC #.pdf
    ТипСправочник
    #10795
    страница19 из 52
    1   ...   15   16   17   18   19   20   21   22   ...   52
    297
    }
    // Создаем класс, производный от B. class C : B { public C() {
    Console.WriteLine("Создание класса C.");
    }
    } class OrderOfConstruction { public static void Main() {
    C c = new C();
    }
    }
    Вот результаты, сгенерированные программой:
    Создание класса А.
    Создание класса В.
    Создание класса С.
    Как видите, конструкторы вызываются в порядке выведения классов.
    И в этом есть логика. Поскольку базовый класс “ничего не знает” о производном, то действия по инициализации, которые он должен выполнить, никак не связаны с существованием производного класса. Более того, они (действия) могут быть необходимы как обязательное условие (предпосылка) инициализации, выполняемой производным классом в форме вызова его конструктора. Потому-то конструктор базового класса выполняется первым.
    Ссылки на базовый класс и объекты
    производных классов
    Как вы знаете, C# — строго типизированный язык. За исключением стандартного и автоматического преобразований, которые применяются к простым типам, совместимость типов строго соблюдается. Следовательно, ссылочная переменная одного “классового” типа обычно не может ссылаться на объект другого “классового” типа. Рассмотрим, например, следующую программу:
    // Эта программа не скомпилируется. class X { int a; public X(int i) { a = i;
    }
    } class Y { int a; public Y(int i) { a = i;
    }
    } class IncompatibleRef { public static void Main() {
    X x = new X(10);
    X x2;

    298
    Часть I. Язык C#
    Y y = new Y(5); x2 = x; // OK, обе переменные имеют одинаковый тип. x2 = y; // Ошибка, здесь переменные разного типа.
    }
    }
    Несмотря на то что здесь классы
    X
    и
    Y
    физически представляют собой одно и то же, невозможно присвоить объект класса
    Y
    ссылочной переменной типа
    X
    , поскольку они имеют разные типы. В общем случае ссылочная переменная может ссылаться только на объекты своего типа.
    Однако существует важное исключение из C#-требования строгой совместимости типов. Ссылочную переменную базового класса можно присвоить ссылке на объект любого класса, выведенного из этого базового класса. Рассмотрим пример.
    // Ссылка на базовый класс может указывать на
    // объект производного класса. using System; class X { public int a; public X(int i) { a = i;
    }
    } class Y : X { public int b; public Y(int i, int j) : base(j) { b = i;
    }
    } class BaseRef { public static void Main() {
    X x = new X(10);
    X x2;
    Y y = new Y(5, 6); x2 = x; // OK, обе переменные имеют одинаковый тип.
    Console.WriteLine("x2.a: " + x2.a); x2 = y; // Все равно ok, поскольку класс Y
    // выведен из класса X.
    Console.WriteLine("x2.a: " + x2.a);
    //
    Х-ссылки "знают" только о членах класса X. x2.a = 19; // ОК
    // x2.b = 27; // Ошибка, в классе X нет члена b.
    }
    }
    На этот раз класс
    Y
    — производный от класса
    X
    , поэтому допустимо ссылке x2
    присвоить ссылку на объект класса
    Y

    Глава 11. Наследование
    299
    Важно понимать, что именно тип ссылочной переменной (а не тип объекта, на который она ссылается) определяет, какие члены могут быть доступны. Другими словами, когда ссылка на производный класс присваивается ссылочной переменной базового класса, вы получаете доступ только к тем частям объекта, которые определены базовым классом.
    Вот почему ссылка x2
    не может получить доступ к члену b
    класса
    Y
    даже при условии, что она указывает на объект класса
    Y
    . И это вполне логично, поскольку базовый класс “не имеет понятия” о том, что добавил в свой состав производный класс. Поэтому последняя строка программы представлена как комментарий.
    И хотя последний абзац может показаться несколько “эзотерическим”, он имеет ряд важных практических приложений. Одно из них описано в этом разделе, а другое — ниже в этой главе при рассмотрении виртуальных методов.
    Важность присвоения ссылок на производный класс ссылочным переменным базового класса ощущается в случае, когда в иерархии классов вызываются конструкторы.
    Как вы знаете, считается нормальным определить для класса конструктор, который в качестве параметра принимает объект своего класса. Это позволяет классу создать копию объекта. Классы, выведенные из такого класса, могут из этого факта извлечь определенную пользу. Рассмотрим, например, следующие версии классов
    TwoDShape и
    Triangle
    , В оба класса добавлены конструкторы, которые в качестве параметра принимают объект.
    // Передача ссылки на производный класс
    // ссылке на базовый класс. using System; class TwoDShape { double pri_width; // Закрытый член. double pri_height; // Закрытый член.
    //
    Конструктор по умолчанию. public TwoDShape() { width = height =0.0;
    }
    //
    Конструктор класса TwoDShape. public TwoDShape(double w, double h) { width = w; height = h;
    }
    //
    Создаем объект, в котором ширина равна высоте. public TwoDShape(double x) { width = height = x;
    }
    //
    Создаем объект из объекта. public TwoDShape(TwoDShape ob) { width = ob.width; height = ob.height;
    }
    //
    Свойства width и height. public double width { get { return pri_width; } set { pri_width = value; }
    }

    300
    Часть I. Язык C# 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() { style = "null";
    }
    //
    Конструктор класса Triangle. public Triangle(string s, double w, double h) : base(w, h) { style = s;
    }
    //
    Создаем равнобедренный треугольник. public Triangle(double x) : base(x) { style = "равнобедренный";
    }
    //
    Создаем объект из объекта. public Triangle(Triangle ob) : base(ob) { style = ob.style;
    }
    //
    Метод возвращает площадь треугольника. public double area() { return width * height / 2;
    }
    //
    Метод отображает тип треугольника. public void showStyle() {
    Console.WriteLine("Треугольник " + style);
    }
    } class Shapes7 { public static void Main() {
    Triangle t1 = new Triangle("прямоугольный", 8.0, 12.0);
    //
    Создаем копию объекта t1.
    Triangle t2 = new Triangle(t1);
    Console.WriteLine("Информация о t1: "); t1.showStyle(); t1.showDim();

    Глава 11. Наследование
    301
    Console.WriteLine("Площадь равна " + t1.area());
    Console.WriteLine();
    Console.WriteLine("Информация о t2: "); t2.showStyle(); t2.showDim();
    Console.WriteLine("Площадь равна " + t2.area());
    }
    }
    В этой программе объект t2
    создается из объекта t1
    и является идентичным ему.
    Вот результаты выполнения этой программы:
    Информация о t1:
    Треугольник прямоугольный
    Ширина и высота равны 8 и 12
    Площадь равна 48
    Информация о t2:
    Треугольник прямоугольный
    Ширина и высота равны 8 и 12
    Площадь равна 4 8
    Обратите внимание на этот конструктор класса
    Triangle
    :
    // Создаем объект из объекта. public Triangle(Triangle ob) : base(ob) { style = ob.style;
    }
    Он принимает объект типа
    Triangle и передает его (посредством base
    -механизма) этому конструктору класса
    TwoDShape
    :
    // Создаем объект из объекта. public TwoDShape(TwoDShape ob) { width = ob.width; height = ob.height;
    }
    Ключевым моментом здесь является то, что конструктор
    TwoDShape()
    ожидает объект класса
    TwoDShape
    . Однако конструктор
    Triangle()
    передает ему объект класса
    Triangle
    . Как разъяснялось выше, такой “номер проходит” благодаря тому, что ссылка на базовый класс может указывать на объект производного класса. Следовательно, вполне допустимо передать конструктору
    TwoDShape()
    ссылку на объект класса, выведенного из класса
    TwoDShape
    . Поскольку конструктор
    TwoDShape()
    инициализирует только те части объекта производного класса, которые являются членами класса
    TwoDShape
    , не имеет значения, что объект может содержать и другие члены, добавленные производным классом.
    Виртуальные методы и их переопределение
    Виртуальным называется метод, объявляемый с помощью ключевого слова virtual в базовом классе и переопределяемый в одном или нескольких производных классах. Таким образом, каждый производный класс может иметь собственную версию виртуального метода. Виртуальные методы представляют интерес с такой позиции: что произойдет, если виртуальный метод будет вызван посредством ссылки на базовый класс. Какую именно версию метода нужно вызвать, C# определяет по типу

    302
    Часть I. Язык C# объекта, на который указывает эта ссылка, причем решение принимается динамически, во
    время выполнения программы. Следовательно, если имеются ссылки на различные объекты, будут выполняться различные версии виртуального метода. Другими словами, именно тип объекта, на который указывает ссылка (а не тип ссылки) определяет, какая версия виртуального метода будет выполнена. Таким образом, если базовый класс содержит виртуальный метод и из этого класса выведены производные классы, то при наличии ссылки на различные типы объектов (посредством ссылки на базовый класс) будут выполняться различные версии этого виртуального метода.
    Чтобы объявить метод в базовом классе виртуальным, его объявление необходимо предварить ключевым словом virtual
    . При переопределении виртуального метода в производном классе используется модификатор override
    . Итак, процесс переопределения виртуального метода в производном классе иногда называется замещением метода (method overriding). При переопределении метода сигнатуры типа у виртуального и метода- заменителя должны совпадать. Кроме того, виртуальный метод нельзя определять как статический (с использованием слова static
    ) или абстрактный (с использованием слова abstract
    , о котором пойдет речь ниже в этой главе).
    Переопределение виртуального метода формирует базу для одной из самых мощных концепций C#: динамической диспетчеризации методов. Динамическая диспетчеризация методов — это механизм вызова переопределенного метода во время выполнения программы, а не в период компиляции. Именно благодаря механизму диспетчеризации методов в C# реализуется динамический полиморфизм.
    Рассмотрим пример, который иллюстрирует виртуальные методы и их переопределение.
    // Демонстрация виртуального метода. using System; class Base {
    //
    Создаем виртуальный метод в базовом классе. public virtual void who() {
    Console.WriteLine("Метод who() в классе Base.");
    }
    } class Derived1 : Base {
    //
    Переопределяем метод who() в производном классе. public override void who() {
    Console.WriteLine("Метод who() в классе Derived1");
    }
    } class Derived2 : Base {
    //
    Снова переопределяем метод who()
    // в другом производном классе. public override void who() {
    Console.WriteLine("Метод who() в классе Derived2");
    }
    } class OverrideDemo { public static void Main() {
    Base baseOb = new Base();
    Derived1 dOb1 = new Derived1();
    Derived2 dOb2 = new Derived2();

    Глава 11. Наследование
    303
    Base baseRef; // Ссылка на базовый класс. baseRef = baseOb; baseRef.who(); baseRef = dOb1; baseRef.who(); baseRef = dOb2; baseRef.who();
    }
    }
    Вот результаты выполнения этой программы:
    Метод who() в классе Base.
    Метод who() в классе Derived1
    Метол who() в классе Derived2
    В программе создается базовый класс
    Base и два производных класса —
    Derived1
    и
    Derived2
    . В классе
    Base объявляется метод с именем who()
    , а производные классы его переопределяют. В методе
    Main()
    объявляются объекты типа
    Base
    ,
    Derived1
    и
    Derived2
    , а также ссылка baseRef типа
    Base
    . Затем программа поочередно присваивает ссылку на объект каждого типа ссылке baseRef и использует эту ссылку для вызова метода who()
    . Как показывают результаты выполнения этой программы, нужная для выполнения версия определяется типом объекта, адресуемого в момент вызова, а не
    “классовым” типом ссылки baseRef
    Виртуальный метод переопределять необязательно. Если производный класс не предоставляет собственную версию виртуального метода, используется версия, определенная в базовом классе. Вот пример:
    /* Если виртуальный метод не переопределен в производном классе, используется метод базового класса. */ using System; class Base {
    // Создаем виртуальный метод в базовом классе. public virtual void who() {
    Console.WriteLine("Метод who() в классе Base");
    }
    } class Derived1 : Base {
    //
    Переопределяем метод who() в производном классе. public override void who() {
    Console.WriteLine("Метод who() в классе Derived1");
    }
    } class Derived2 : Base {
    //
    Этот класс не переопределяет метод who().
    } class NoOverrideDemo { public static void Main() {
    Base baseOb = new Base();
    Derived1 dOb1 = new Derived1();

    304
    Часть I. Язык C#
    Derived2 dOb2 = new Derived2();
    Base baseRef; // Ссылка на базовый класс. baseRef = baseOb; baseRef.who(); baseRef = dOb1; baseRef.who(); baseRef = dOb2; baseRef.who();
    //
    Вызывает метод who() класса Base.
    }
    }
    Вот результаты выполнения этой программы:
    Метод who() в классе Base
    Метод who() в классе Derived1
    Метод who() в классе Base
    Здесь класс
    Derived2
    не переопределяет метод who()
    . Поэтому при вызове метода who()
    для объекта класса
    Derived2
    выполняется метод who()
    , определенный в классе
    Base
    Если производный класс не переопределяет виртуальный метод в случае многоуровневой иерархии, то будет выполнен первый переопределенный метод, который обнаружится при просмотре иерархической лестницы в направлении снизу вверх.
    Рассмотрим пример.
    /* Если производный класс не переопределяет виртуальный метод в случае многоуровневой иерархии, будет выполнен первый переопределенный метод, который обнаружится при просмотре иерархической лестницы в направлении снизу вверх. */ using System; class Base {
    //
    Создаем виртуальный метод в базовом классе. public virtual void who() {
    Console.WriteLine("Метод who() в классе Base");
    }
    } class Derived1 : Base {
    //
    Переопределяем метод who() в производном классе. public override void who() {
    Console.WriteLine("Метод who() в классе Derived1");
    }
    } class Derived2 : Derived1 {
    //
    Этот класс не переопределяет метод who().
    } class Derived3 : Derived2 {
    //
    Этот класс также не переопределяет метод who().
    } class NoOverrideDemo2 {

    Глава 11. Наследование
    305 public static void Main() {
    Derived3 dOb = new Derived3();
    Base baseRef; // Ссылка на базовый класс. baseRef = dOb; baseRef.who();
    //
    Вызывает метод who()
    //из класса Derived1.
    }
    }
    Результаты выполнения этой программы таковы:
    Метод who()
    в классе
    Derived1
    Здесь класс
    Derived3
    наследует класс
    Derived2
    , который наследует класс
    Derived1
    , который в свою очередь наследует класс
    Base
    . Как подтверждают результаты выполнения этой программы, поскольку метод who()
    не переопределяется ни в классе
    Derived3
    , ни в классе
    Derived2
    , но переопределяется в классе
    Derived1
    , то именно эта версия метода who()
    (из класса
    Derived1
    ) и выполняется, так как она является первой обнаруженной в иерархии классов.
    Еще одно замечание. Свойства также можно модифицировать с помощью ключевого слова virtual
    , а затем переопределять с помощью ключевого слова override
    Зачем переопределять методы
    Переопределение методов позволяет C# поддерживать динамический полиморфизм.
    Без полиморфизма объектно-ориентированное программирование невозможно, поскольку он позволяет исходному классу определять общие методы, которыми будут пользоваться все производные классы, и в которых при этом можно будет задать собственную реализацию некоторых или всех этих методов. Переопределенные методы представляют собой еще один способ реализации в C# аспекта полиморфизма, который можно выразить как “один интерфейс — много методов”.
    Ключ (вернее, его первый “поворот”) к успешному применению полиморфизма лежит в понимании того, что базовые и производные классы образуют иерархию, которая развивается в сторону более узкой специализации. При корректном использовании базовый класс предоставляет производному классу все элементы “пригодными к употреблению”, т.е. для прямого их использования. Кроме того, он определяет методы, которые производный класс должен реализовать самостоятельно. Это делает определение производными классами собственных методов более гибким, по-прежнему оставляя в силе требование согласующегося интерфейса. Таким образом, сочетая наследование с возможностью переопределения (замещения) методов, в базовом классе можно определить общую форму методов, которые будут использованы производными классами.
    Применение виртуальных методов
    Чтобы лучше почувствовать силу виртуальных методов, применим их к классу
    TwoDShape
    . В предыдущих примерах каждый класс, выведенный из класса
    TwoDShape
    , определяет метод с именем area()
    . Это наводит нас на мысль о том, не лучше ли сделать метод вычисления площади фигуры area()
    виртуальным в классе
    TwoDShape
    , получив возможность переопределить его в производных классах таким образом, чтобы он вычислял площадь согласно типу конкретной геометрической фигуры, которую инкапсулирует класс.
    Эта мысль и реализована в следующей программе. Для удобства в класс
    TwoDShape вводится свойство name
    , которое упрощает демонстрацию этих классов.

    306
    Часть I. Язык C#
    // Использование виртуальных методов и полиморфизма. using System; class TwoDShape { double pri_width; // Закрытый член. double pri_height; // Закрытый член. string pri_name; // Закрытый член.
    //
    Конструктор по умолчанию. public TwoDShape() { width = height = 0.0; name = "null";
    }
    //
    Конструктор с параметрами. public TwoDShape(double w, double h, string n) { width = w; height = h; name = n;
    }
    //
    Создаем объект, у которого ширина равна высоте. public TwoDShape(double x, string n) { width = height = x; name = n;
    }
    //
    Создаем объект из объекта. public TwoDShape(TwoDShape ob) { width = ob.width; height = ob.height; name = ob.name;
    }
    //
    Свойства width, height и name. public double width { get { return pri_width; } set { pri_width = value; }
    } public double height { get { return pri_height; } set { pri_height = value; }
    } public string name { get { return pri_name; } set { pri_name = value; }
    } public void showDim() {
    Console.WriteLine("Ширина и высота равны " + width
    +
    " и " + height);
    } public virtual double area() {
    Console.WriteLine(

    Глава 11. Наследование
    307
    "Метод area() необходимо переопределить."); return 0.0;
    }
    }
    // Класс треугольников, производный от класса TwoDShape. class Triangle : TwoDShape { string style; // Закрытый член.
    //
    Конструктор по умолчанию. public Triangle() { style = "null";
    }
    //
    Конструктор с параметрами. public Triangle(string s, double w, double h) : base(w, h,
    "треугольник") { style = s;
    }
    //
    Создаем равнобедренный треугольник. public Triangle(double x) : base(x, "треугольник") { style = "равнобедренный";
    }
    //
    Создаем объект из объекта. public Triangle(Triangle ob) : base(ob) { style = ob.style;
    }
    //
    Переопределяем метод area() для класса Triangle. public override double area() { return width * height / 2;
    }
    //
    Метод отображает тип треугольника. public void showStyle() {
    Console.WriteLine("Треугольник " + style);
    }
    }
    // Класс прямоугольников, производный от класса TwoDShape. class Rectangle : TwoDShape {
    //
    Конструктор с параметрами. public Rectangle(double w, double h) : base(w, h,
    "прямоугольник"){ }
    //
    Создаем квадрат. public Rectangle(double x) : base(x,
    "прямоугольник") { }
    //
    Создаем объект из объекта. public Rectangle(Rectangle ob) : base(ob) { }
    //
    Метод возвращает true, если прямоугольник - квадрат. public bool isSquare() { if(width == height) return true; return false;

    308
    Часть I. Язык C#
    }
    //
    Переопределяем метод area() для класса Rectangle. public override double area() { return width * height;
    }
    } class DynShapes { public static void Main() {
    TwoDShape[] shapes = new TwoDShape[5]; shapes[0] = new Triangle("прямоугольный", 8.0, 12.0); shapes[1] = new Rectangle(10); shapes[2] = new Rectangle(10, 4); shapes[3] = new Triangle(7.0); shapes[4] = new TwoDShape(10, 20,
    "заготовка для фигуры"); for(int i=0; i < shapes.Length; i++) {
    Console.WriteLine("Объектом является " + shapes[i].name);
    Console.WriteLine("Площадь равна " + shapes[i].area());
    Console.WriteLine();
    }
    }
    }
    При выполнении программа генерирует следующие результаты:
    Объектом является треугольник
    Площадь равна 48
    Объектов является прямоугольник
    Площадь равна 100
    Объектом является прямоугольник
    Площадь равна 40
    Объектом является треугольник
    Площадь равна 24,5
    Объектом является заготовка для фигуры
    Метод area() необходимо переопределить.
    Площадь равна 0
    Рассмотрим программу подробнее. Во-первых, метод area()
    объявляется в классе
    TwoDShape с использованием ключевого слова virtual и переопределяется в классах
    Triangle и
    Rectangle
    . В классе
    TwoDShape метод area()
    представляет собой своего рода “заглушку”, которая просто информирует пользователя о том, что в производном классе этот метод необходимо переопределить. Каждое переопределение метода area()
    реализует вариант вычисления площади, соответствующий типу объекта, инкапсулируемому производным классом. Таким образом, если бы вы реализовали класс эллипсов, то метод area()
    в этом классе вычислял бы площадь эллипса.
    В предыдущей программе проиллюстрирован еще один важный момент. Обратите внимание на то, что в методе
    Main()
    член shapes объявляется как массив объектов

    Глава 11. Наследование
    309 типа
    TwoDShape
    . Однако элементам этого массива присваиваются ссылки на объекты классов
    Triangle
    ,
    Rectangle и
    TwoDShape
    . Это вполне допустимо, поскольку ссылка на базовый класс может указывать на объект производного класса. Затем программа в цикле опрашивает массив shapes
    , отображая информацию о каждом объекте. Несмотря на простоту, этот цикл иллюстрирует силу как наследования, так и переопределения методов.
    Конкретный тип объекта, хранимый в ссылочной переменной базового класса, определяется во время выполнения программы, что позволяет принять соответствующие меры, т.е. выполнить действия, соответствующие объекту данного типа. Если объект выведен из класса
    TwoDShape
    , его площадь можно узнать посредством вызова метода area()
    Интерфейс для выполнения этой операции одинаков для всех производных классов, независимо от типа используемой фигуры.
    Использование абстрактных классов
    Иногда полезно создать базовый класс, определяющий только своего рода “пустой бланк”, который унаследуют все производные классы, причем каждый из них заполнит этот
    “бланк” собственной информацией. Такой класс определяет “суть” методов, которые производные классы должны реализовать, но сам при этом не обеспечивает реализации одного или нескольких методов. Подобная ситуация может возникнуть, когда базовый класс попросту не в состоянии реализовать метод. Этот случай был проиллюстрирован версией класса
    TwoDShape
    (из предыдущей программы), в которой определение метода area()
    представляло собой “заглушку”, поскольку в нем площадь фигуры не вычислялась и, естественно, не отображалась.
    В будущем, создавая собственные библиотеки классов, вы убедитесь, что отсутствие у метода четкого определения в контексте своего (базового) класса, не является чем-то необычным. Описанную ситуацию можно обработать двумя способами. Один из них, который продемонстрирован в предыдущем примере, — вывод предупреждающего сообщения. И хотя такой подход может быть полезным в определенных обстоятельствах
    (например, при отладке программы), все же он не соответствует уровню профессионального программирования. Существует и другой способ. Наша цель — заставить производные классы переопределить методы, которые в базовом классе не имеют никакого смысла. Рассмотрим класс
    Triangle
    . Им нельзя пользоваться, если не определен метод area()
    . Необходимо иметь средство, благодаря которому производный класс обязательно переопределит все необходимые методы. Этим средством в C# является
    абстрактный метод.
    Абстрактный метод создается с помощью модификатора типа abstract
    Абстрактный метод не содержит тела и, следовательно, не реализуется базовым классом.
    Поэтому производный класс должен его переопределить, поскольку он не может использовать версию, предложенную в базовом классе. Нетрудно догадаться, что абстрактный метод автоматически является виртуальным, поэтому и нет необходимости в использовании модификатора virtual
    . Более того, совместное использование модификаторов virtual и abstract считается ошибкой.
    Для объявления абстрактного метода используйте следующий формат записи. abstract
    тип
    имя
    (
    список_параметров
    );
    Как видите, тело абстрактного метода отсутствует. Модификатор abstract можно использовать только применительно к обычным, а не к static
    -методам. Свойства также могут быть абстрактными.
    Класс, содержащий один или несколько абстрактных методов, также должен быть объявлен как абстрактный с помощью спецификатора abstract
    , который ставится перед объявлением class
    . Поскольку абстрактный класс нереализуем в полном объеме,

    310
    Часть I. Язык C# невозможно создать его экземпляры, или объекты. Таким образом, попытка создать объект абстрактного класса с помощью оператора new приведет к возникновению ошибки времени компиляции.
    Если производный класс выводится из абстрактного, он может реализовать все абстрактные методы базового класса. В противном случае такой производный класс также должен быть определен как абстрактный. Таким образом, атрибут abstract наследуется до тех пор, пока реализация класса не будет полностью достигнута.
    Используя абстрактный класс, можно усовершенствовать определение класса
    TwoDShape
    . Поскольку для не определенной заранее двумерной фигуры понятие площади не имеет смысла, в следующей версии предыдущей программы метод area()
    в классе
    TwoDShape объявляется как абстрактный, как, впрочем, и сам класс
    TwoDShape
    Безусловно, это означает, что все классы, выведенные из
    TwoDShape
    , должны переопределить метод area()
    // Создание абстрактного класса. using System; abstract class TwoDShape { double pri_width; // Закрытый член. double pri_height; // Закрытый член. string pri_name; // Закрытый член.
    //
    Конструктор по умолчанию. public TwoDShape() { width = height = 0.0; name = "null";
    }
    //
    Конструктор с параметрами. public TwoDShape(double w, double h, string n) { width = w; height = h; name = n;
    }
    //
    Создаем объект, у которого ширина равна высоте. public TwoDShape(double x, string n) { width = height = x; name = n;
    }
    //
    Создаем объект из объекта. public TwoDShape(TwoDShape ob) { width = ob.width; height = ob.height; name = ob.name;
    }
    //
    Свойства width, height и name. public double width { get { return pri_width; } set { pri_width = value; }
    } public double height { get { return pri_height; } set { pri_height = value; }

    Глава 11. Наследование
    311
    } public string name { get { return pri_name; } set { pri_name = value; }
    } public void showDim() {
    Console.WriteLine("Ширина и высота равны " + width
    +
    " и " + height);
    }
    //
    Теперь метод area() абстрактный. public abstract double area();
    }
    // Класс треугольников, производный от класса TwoDShape. class Triangle : TwoDShape { string style; // Закрытый член.
    //
    Конструктор по умолчанию. public Triangle() { style = "null";
    }
    //
    Конструктор с параметрами. public Triangle(string s, double w, double h) : base(w, h,
    "triangle")
    { style = s;
    }
    //
    Создаем равнобедренный треугольник. public Triangle(double x) : base(x, "треугольник") { style = "равнобедренный";
    }
    //
    Создаем объект из объекта. public Triangle(Triangle ob) : base(ob) { style = ob.style;
    }
    //
    Переопределяем метод area() для класса Triangle. public override double area() { return width * height / 2;
    }
    //
    Отображаем тип треугольника. public void showStyle() {
    Console.WriteLine("Треугольник " + style);
    }
    }
    // Класс прямоугольников, производный от класса TwoDShape. class Rectangle : TwoDShape {
    //
    Конструктор с параметрами. public Rectangle(double w, double h) : base(w, h, "прямоугольник"){ }
    //
    Создаем квадрат.

    312
    Часть I. Язык C# public Rectangle(double x) : base(x,
    "прямоугольник") { }
    //
    Создаем объект из объекта. public Rectangle(Rectangle ob) : base(ob) { }
    //
    Метод возвращает значение true, если
    // прямоугольник является квадратом. public bool isSquare() { if(width == height) return true; return false;
    }
    //
    Переопределяем метод area() для класса Rectangle. public override double area() { return width * height;
    }
    } class AbsShape { public static void Main() {
    TwoDShape[] shapes = new TwoDShape[4]; shapes[0] = new Triangle("прямоугольный", 8.0, 12.0); shapes[1] = new Rectangle(10); shapes[2] = new Rectangle(10, 4); shapes[3] = new Triangle(7.0); for(int i=0; i < shapes.Length; i++) {
    Console.WriteLine("Объектом является " + shapes[i].name);
    Console.WriteLine("Площадь равна " + shapes[i].area());
    Console.WriteLine();
    }
    }
    }
    Как продемонстрировано этой программой, все производные классы должны или переопределить метод area()
    , или также объявить себя абстрактными. Чтобы убедиться в этом, попробуйте создать производный класс, который не переопределяет метод area()
    Вы тут же (т.е. во время компиляции) получите сообщение об ошибке. Конечно, мы можем создать объектную ссылку типа
    TwoDShape
    , что и делается в программе. Однако теперь нельзя объявить объект типа
    TwoDShape
    . Поэтому в методе
    Main()
    размер массива shapes сокращен до 4, и больше не создается “заготовка для фигуры” в виде объекта класса
    TwoDShape
    Обратите также внимание на то, что класс
    TwoDShape по-прежнему включает метод showDim()
    , объявления которого не коснулся модификатор abstract
    . Ведь вполне допустимо для абстрактного класса содержать конкретные (а не только абстрактные) методы, которые производный класс может использовать “как есть”. И только методы, объявленные с использованием ключевого слова abstract
    , должны переопределяться производными классами.

    Глава 11. Наследование
    313
    Использование ключевого слова sealed для
    предотвращения наследования
    Каким бы мощным и полезным ни был механизм наследования, все же иногда необходимо его отключать. Например, у вас может быть класс, который инкапсулирует последовательность действий при инициализации такого специализированного устройства, как медицинский монитор. В этом случае необходимо запретить пользователям изменять характер инициализации этого монитора, чтобы исключить возможную некорректность этой процедуры. Специально для подобных ситуаций в C# предусмотрена возможность предотвращения наследования класса с помощью ключевого слова sealed
    Чтобы запретить наследование класса, предварите его объявление ключевым словом sealed
    . Нетрудно догадаться, что нельзя объявлять класс одновременно с помощью двух модификаторов — abstract и sealed
    , поскольку абстрактный класс сам по себе
    “полуфабрикат” и его полная реализация возможна только в следующих “поколениях”, т.е. после создания производных классов.
    Рассмотрим пример sealed
    -класса. sealed class А {
    // ...
    }
    // Следующий класс создать невозможно. class В : А { // ОШИБКА! Класс А не может иметь наследников.
    // ...
    }
    Класс
    B
    не может быть производным от класса
    А
    , так как последний объявлен sealed
    -классом.
    Класс object
    В C# определен специальный класс с именем object
    , который является неявным базовым классом всех других классов и типов (включая типы значений). Другими словами, все остальные типы выводятся из класса object
    . Это означает, что ссылочная переменная типа object может указывать на объект любого типа. Кроме того, поскольку C#-массивы реализованы как классы, переменная типа object также может ссылаться на любой массив. Строго говоря, C#-имя object
    — еще одно имя для класса
    System.Object
    , который является частью библиотеки классов .NET Framework.
    Класс object определяет методы, перечисленные в табл. 11.1. Эти методы доступны для каждого объекта.
    Таблица 11.1. Методы класса object
    Метод
    Назначение
    public virtual bool Equals( object
    ob
    )
    Определяет, является ли вызывающий объект таким же, как объект, адресуемый ссылкой
    ob
    public static bool Equals( object
    ob1
    , object
    ob2
    )
    Определяет, является ли объект
    оb1
    таким же, как объект
    оb2

    314
    Часть I. Язык C#
    Окончание табл. 11.1
    Метод
    Назначение
    protected Finalize()
    Выполняет завершающие действия перед процессом сбора мусора. В C# метод
    Finalize()
    доступен через деструктор public virtual int GetHashCode()
    Возвращает хеш-код, связанный с вызывающим объектом public Type GetType()
    Получает тип объекта во время выполнения программы protected object
    MemberwiseClone()
    Выполняет "поверхностное копирование" объекта, т.е. копируются члены, но не объекты, на которые ссылаются эти члены public static bool
    ReferenceEquals( object
    оb1
    , object
    ob2
    )
    Определяет, ссылаются ли объекты
    оb1
    и
    оb2
    на один и тот же объект public virtual string ToString()
    Возвращает строку, которая описывает объект
    Назначение некоторых из перечисленных выше методов требует дополнительных разъяснений. По умолчанию метод
    Equals(object)
    определяет, ссылаются ли вызывающий объект и объект, адресуемый аргументом, на один и тот же объект, т.е. метод определяет, являются ли эти две ссылки одинаковыми. Метод возвращает значение true, если объекты совпадают, и false
    — в противном случае. Этот метод можно переопределить в создаваемых им классах. Это позволит уточнить, что означает равенство для вашего класса. Например, вы можете так определить метод
    Equals(object)
    , чтобы он сравнивал содержимое двух объектов (и давал ответ на вопрос, равны ли они). Метод
    Equals(object, object)
    для получения результата вызывает метод
    Equals(object)
    Метод
    GetHashCode()
    возвращает хеш-код, связанный с вызывающим объектом.
    Этот хеш-код можно использовать с любым алгоритмом, который применяет хеширование как средство доступа к объектам, хранимым в памяти,
    Как упоминалось в главе 9, при перегрузке оператора “
    ==
    ” необходимо переопределить методы
    Equals(object)
    и
    GetHashCode()
    , поскольку функции оператора “
    ==
    ” и метода
    Equals(object)
    , как правило, должны быть идентичными.
    Переопределив метод
    Equals(object)
    , необходимо переопределить и метод
    GetHashCode()
    , чтобы они были совместимы.
    Метод
    ToString()
    возвращает строку, содержащую описание объекта, для которого вызывается этот метод. Кроме того, метод
    ToString() автоматически вызывается при выводе объекта с помощью метода
    WriteLine()
    . Метод
    ToString()
    переопределяется во многих классах, что позволяет подобрать описание специально для типов объектов, которые они создают. Вот пример:
    // Демонстрация использования метода ToString(). using System; class MyClass { static int count = 0; int id; public MyClass() { id = count; count++;
    } public override string ToString() { return
    "Объект класса MyClass #" + id;

    Глава 11. Наследование
    315
    }
    } class Test { public static void Main() {
    MyClass ob1 = new MyClass();
    MyClass ob2 = new MyClass();
    MyClass ob3 = new MyClass();
    Console.WriteLine(ob1);
    Console.WriteLine(ob2);
    Console.WriteLine(ob3);
    }
    }
    Эта программа генерирует следующие результаты:
    Объект класса MyClass #0
    Объект класса MyClass #1
    Объект класса MyClass #2
    Приведение к объектному типу и восстановление значения
    Как упоминалось выше, все C#-типы, включая типы значений, выведены из класса object
    . Следовательно, ссылку типа object можно использовать в качестве ссылки на любой другой тип, включая типы значений. Если ссылку типа object заставляют указывать на значение нессылочного типа, этот процесс называют приведением к
    объектному типу (boxing). В результате этого процесса значение нессылочного типа должно храниться подобно объекту, или экземпляру класса. Другими словами,
    “необъектное” значение помешается в объектную оболочку. Такой “необъектный” объект можно затем использовать подобно любому другому объекту. В любом случае приведение к объектному типу происходит автоматически. Для этого достаточно присвоить значение ссылке на объект класса object
    . Все остальное доделает C#.
    Восстановление значения из “объектного образа” (unboxing) — это по сути процесс извлечения значения из объекта. Это действие выполняется с помощью операции приведения типа, т.е. приведения ссылки на объект класса object к значению желаемого типа.
    Рассмотрим простой пример, который иллюстрирует приведение значения к объектному типу и его восстановление.
    // Простой пример "объективизации" и "дезобъективизации". using System; class BoxingDemo { public static void Main() { int x; object obj; x = 10; obj = x; // "Превращаем" x в объект. int y = (int)obj; // Обратное "превращение"
    // объекта obj в int-значение.
    Console.WriteLine(y);
    }
    }

    316
    Часть I. Язык C#
    Эта программа отображает значение 10. Обратите внимание на то, что значение переменной x
    приводится к объектному типу простым ее присваиванием переменной obj
    , которая является ссылкой на объект типа object
    . Целочисленное значение, хранимое в ссылочной переменной obj
    , извлекается с помощью операции приведения к типу int
    А вот еще один (более интересный) пример приведения значения к объектному типу.
    На этот раз int
    -значение передается в качестве аргумента методу sqr()
    , который использует параметр типа object
    // Приведение значений к объектному типу возможно
    // при передаче значений методам. using System; class BoxingDemo { public static void Main() { int x; x = 10;
    Console.WriteLine("Значение x равно: " + x);
    //
    Переменная x автоматически приводится
    // к объектному типу при передаче методу sqr(). x = BoxingDemo.sqr(x);
    Console.WriteLine("Значение x в квадрате равно: " + x);
    } static int sqr(object o) { return(int)o * (int)o;
    }
    }
    При выполнении этой программы получаются такие результаты:
    Значение x равно: 10
    Значение x в квадрате равно: 100
    Здесь значение x
    при передаче методу sqr()
    автоматически приводится к объектному типу.
    Приведение к объектному типу и обратный процесс делает C#-систему типов полностью унифицированной. Все типы выводятся из класса object
    . Ссылке типа object можно присвоить ссылку на любой тип. В процессе приведения значения к объектному типу и его восстановления из объекта автоматически обрабатываются все детали, соответствующие нессылочным типам. Более того, поскольку все типы выведены из класса object
    , все они имеют доступ к методам этого класса. Рассмотрим, например, следующую маленькую, но удивительную программу.
    // "Объективизация" позволяет вызывать методы
    // класса object для значений нессылочного типа! using System; class MethOnValue { public static void Main() {
    Console.WriteLine(10.ToString());
    }
    }

    Глава 11. Наследование
    317
    Эта программа отображает значение 10. Дело в том, что метод
    ToString()
    возвращает строковое представление объекта, для которого он вызывается. В данном случае строковое представление числа 10 выглядит как 10!
    Использование класса object в качестве обобщенного типа данных
    С учетом того, что object
    — базовый класс для всех остальных C#-типов и что приведение значения к объектному типу и его восстановление из объекта происходят автоматически, класс object можно использовать в качестве обобщенного типа данных.
    Например, рассмотрим следующую программу, которая создает массив объектов класса object
    , а затем присваивает его элементам данные различных типов.
    // Использование класса object для создания
    // массива обобщенного типа. using System; class GenericDemo { public static void Main() { object[] ga = new object[10];
    //
    Сохраняем int-значения. for(int i=0; i < 3; i++) ga[i]
    = i;
    //
    Сохраняем double-значения. for(int i=3; i < 6; i++) ga[i] = (double) i / 2;
    //
    Сохраняем две строки, bool- и char-значения. ga[6] = "Массив обобщенного типа"; ga[7] = true; ga[8] = 'X'; ga[9] = "Конец"; for(int i = 0; i < ga.Length; i++)
    Console.WriteLine("ga[" + i + "]: " + ga[i] + " ");
    }
    }
    Вот результаты выполнения этой программы: ga[0]: 0 ga[1]: 1 ga[2]: 2 ga[3]: 1,5 ga[4]: 2 ga[5]: 2,5 ga[6]: Массив обобщенного типа ga[7]: True ga[8]: X ga[9]: Конец
    Как видно по результатам выполнения программы, ссылку на объект класса object можно использовать в качестве ссылки на данные других типов. Таким образом,

    318
    Часть I. Язык C# массив типа object в этой программе может хранить значения любого типа. Это говорит о том, что object
    -массив по сути представляет собой обобщенный список. Развивая эту идею, нетрудно создать класс стека, который бы хранил ссылки на объекты класса object
    Это позволило бы стеку сохранять данные любого типа.
    Несмотря на эффективность такого обобщенного типа, как класс object
    , в некоторых ситуациях, было бы ошибкой думать, что класс object следует использовать как способ обойти строгий C#-контроль соответствия типов. Другими словами, для хранения int
    -значения используйте int
    -переменную, для хранения string
    -значения — string
    -ссылку и т.д. Оставьте обобщенную природу класса object для особых ситуаций.

    Полный справочник по
    1   ...   15   16   17   18   19   20   21   22   ...   52


    написать администратору сайта