Попытка присвоить -10 свойству ob.myprop

Console.WriteLine(

Переменной экземпляра prop. Оно позволяет

присваивать ей только положительные числа. */

publicintmyprop {

get {

return prop;

}

set {

if(value>= 0) prop = value;

}}

}

// Демонстрируем использование свойства,

classPropertyDemo {

public static void Main() {

SimpPropob = new SimpProp ();

Console.WriteLine("Исходноезначениеob.myprop: " +

ob.myprop);

ob.myprop = 100; // Присваиваемзначение.

Console.WriteLine("Значениеob.myprop: " + ob.myprop);

// Переменной prop невозможно присвоить

// отрицательное значение.

"Попытка присвоить -10 свойству ob.myprop");

ob.myprop = -10;

Console.WriteLine("Значениеob.myprop: " + ob.myprop);

}

}

Результаты выполнения этой программы выглядят так:

Исходное значение ob.myprop: 0

Значение ob.myprop: 100

Значение ob.myprop: 100

Можно создавать свойства, предназначенные только для чтения (определив лишь get-аксессор) либо только для записи (определив лишь set-аксессор).

На использование свойств налагаются ограничения. Во-первых, поскольку в свойстве не определяется область памяти, его нельзя передавать методу в качестве ref- или out-параметра. Во-вторых, свойство нельзя перегружать. (Но при необходимости вы можете иметь два различных свойства, которые используют одну и ту же базовую переменную, но к такой организации свойств прибегают нечасто.) Наконец, свойство не должно изменять состояние базовой переменной привызове get-аксессора, хотя несоблюдение этого правила компилятор обнаружить не всостоянии. Другими словами, get-операция должна быть максимально простой.


 

Лекция 5. Иерархии классов

5.1 Наследование

Наследование — один из трех фундаментальных принципов объектно-ориентированного программирования, именно благодаря ему возможно создание иерархических классификаций. Используя наследование, создают общийкласс, который определяет характеристики, присущие множеству связанных элементов. Этот класс затем может быть унаследован другими, узкоспециализированнымиклассами с добавлением в каждый из них своих, уникальных особенностей.

В языке С# класс, который наследуется, называется базовым. Класс, который наследует базовый класс, называется производным. Следовательно, производный класс —это специализированная версия базового класса. В производный класс, наследующийвсе переменные, методы, свойства, операторы и индексаторы, определенные в базовом классе, могут быть добавлены уникальные элементы.

5.1.1 Основы наследования

С# поддерживает наследование, позволяя в объявление класса встраивать другойкласс. Это реализуется посредством задания базового класса при объявлении производного. Рассмотрим класс TwoDShape, в котором определяются атрибуты "обобщенной" двумерной геометрической фигуры (например,квадрата, прямоугольника, треугольника и т.д.).

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

// Простая иерархия классов.

usingSystem;

// Классдвумерныхобъектов,

classTwoDShape {

public double width;

public double height;

public void showDim() {

Console.WriteLine("Ширинаивысотаравны " +

width + " и " + height);

}}

// Класс Triangle выводится из класса TwoDShape.

class Triangle : TwoDShape {

public string style; // Типтреугольника.

// Метод возвращает площадь треугольника,

publicdoublearea() {

returnwidth * height / 2;

}

// Отображаем тип треугольника,

public void showStyle() {

Console.WriteLine("Треугольник " + style);

}}

class Shapes {

public static void Main() {

Triangle t1 = newTriangle();

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());

}}

Вот результаты работы этой программы.

Информация о tl:

Треугольник равнобедренный

Ширина и высота равны 4 и 4

Площадь равна 8

Информация о t2:

Треугольник прямоугольный

Ширина и высота равны 8 и 12

Площадь равна 4

 

В классе Triangle создается специфический тип объекта класса TwoDShape, в данном случае треугольник. Класс Triangle содержит все элементы класса TwoDShape и, кроме того, поле style, метод area() и метод showStyle(). В переменной style хранится описание типа треугольника, метод area () вычисляет и возвращает его площадь, а метод showstyle() отображает данные о типе треугольника.

Если один класс наследует другой, то имя базового класса указывается после имени производного, причем имена классов разделяются двоеточием. ВС# синтаксис наследования класса очень прост для запоминания и использования.

Поскольку класс Triangle включает все члены базового класса, TwoDShape, он может обращаться к членам width и height внутри метода агеа(). Кроме того, внутри метода Main() объекты t1 и t2 могут прямо ссылаться на члены width и height, как если бы они были частью класса Triangle. Включение класса TwoDShape в класс Triangle схематически показано на рис. 2.

 

Рисунок 2

 

 

Общая форма объявления класса, который наследует базовый класс, имеет такой вид:

class имя__производного_класса : имя_базового_класса {

// тело класса

}

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

ВС# (в отличие от C++) не поддерживается наследование нескольких базовых классов в одном производном классе.

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

Вот, например, как из базового класса TwoDShape можно вывести производный класс, который инкапсулирует прямоугольники:

// Класс прямоугольников Rectangle, производный

//от класса TwoDShape.

classRectangle :TwoDShape {

// Метод возвращает значение true, если

// прямоугольник является квадратом,

publicboolisSquare() {

if(width == height) returntrue;

returnfalse;

}

// Метод возвращает значение площади прямоугольника,

public double area() {

return width * height;

}}

Класс Rectangle включает класс TwoDShape и добавляет метод isSquare(), который определяет, является ли прямоугольник квадратом, и метод area(), вычисляющий площадь прямоугольника.

 

 

5.1.2 Доступ к членам класса и наследование

Наследование класса не отменяет ограничения, связанные с закрытым доступом. Таким образом, несмотря на то, что производный класс включает все члены базового класса, он не может получить доступ к тем из НИХ, которые объявлены закрытыми.

С#-программисты обычно предоставляют доступ к закрытым членам класса посредством открытых методов или путем превращения их в свойства.

Во многих случаях используют защищённые члены классов. Защищенным является член, который открыт для своей иерархии классов, но закрыт вне этой иерархии.

Защищенный член создается с помощью модификатора доступа protected. При объявлении protected-члена он, по сути, является закрытым, но с одним исключением. Это исключение вступает в силу, когда защищенный член наследуется. В этом случае защищенный член базового класса становится защищенным членом производного класса, а следовательно, и доступным для производного класса. Таким образом, используя модификатор доступа protected, можно создавать закрытые (для "внешнего мира") члены класса, но вместе с тем они будут наследоваться с возможностью доступа со стороны производных классов.

// Демонстрация использования защищенных членов класса.

usingSystem;

class В {

protectedint i, j; // Закрыт внутри класса В,

//но доступен для класса D.

public void set(int a, int b) {

i = a;

j = b;

}

public void show() {

Console.WriteLine(i + " " + j) ;

}}

classD : В {

int k; // Закрытыйчлен.

// Класс D получаетдоступ к членам i и j класса В.

public void setk() {

k = i * j ;

}

public void showk() {

Console.WriteLine(k) ;

}}

classProtectedDemo {

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 остается со своим членом независимо от реализуемого количества уровней наследования. Следовательно, при использовании производного класса в качестве базового для создания другого производного класса любой защищенный член исходного базового класса, который наследуется первым производным классом, также наследуется в статусе защищенного и вторым производным классом.

5.1.3 Конструкторы и наследование

В иерархии классов как базовые, так и производные классы могут иметь собственные конструкторы. При этом возникает важный вопрос: какой конструктор отвечает за создание объекта производного класса? Конструктор базового класса создает часть объекта, соответствующую базовому классу, а конструктор производного класса — часть объекта, соответствующую производному классу.

Если конструктор определяется только в производном классе, процесс создания объекта несложен: просто создается объект производного класса. Часть объекта, соответствующая базовому классу, создается автоматически с помощью конструктора поумолчанию.

Если конструкторы определены и в базовом, и в производном классе, процесс создания объектов несколько усложняется, поскольку должны выполниться конструкторы обоих классов. В этом случае необходимо использовать еще одно ключевое словоС#base, которое имеет два назначения: вызвать конструктор базового класса и получить доступ к члену базового класса, который скрыт "за" членом производного класса.

Производный класс может вызывать конструктор, определенный в его базовом классе, используя расширенную форму объявления конструктора производного класса и ключевое слово base. Формат расширенного объявления таков:

конструктор_производиого_класса(список_параметров) :

base (список_аргументов) {

// тело конструктора

}

Здесь с помощью элемента список_аргументовзадаются аргументы, необходимые конструктору в базовом классе.

При задании производным классом bаsе-"метода" вызывается конструктор непосредственного базового класса. Таким образом, ключевое слово base всегда отсылает к базовому классу, стоящему в иерархии классов непосредственно над вызывающим классом. Это справедливо и для многоуровневой иерархии. Чтобы передать аргументы конструктору базового класса, достаточно указать их в качестве аргументов "метода" base(). При отсутствии ключевого слова base автоматически вызывается конструктор базового класса, действующий по умолчанию.

Производный класс может определить член, имя которого совпадает с именем члена базового класса. В этом случае член базового класса становится скрытым в производном классе. Существует вторая форма использования ключевого слова base, которая действует подобно ссылке this , за исключением того, что ссылка base всегда указывает на базовый класс производного класса, в котором она используется. В этом случае формат ее записи такой: base.член. Здесь в качестве элемента член можно указывать либо метод, либо переменную экземпляра. Эта форма ссылки base наиболее применима в тех случаях, когда имя члена в производном классе скрывает член с таким же именем в базовом классе.

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

Виртуальным называется метод, объявляемый с помощью ключевого слова virtual в базовом классе и переопределяемый в одном или нескольких производных классах. Таким образом, каждый производный класс может иметь собственную версию виртуального метода. Виртуальные методы представляют интерес с такой позиции: что произойдет, если виртуальный метод будет вызван посредством ссылки на базовый класс. Какую именно версию метода нужно вызвать, С# определяет по типу объекта, на который указываетэта ссылка, причем решение принимается динамически, во времявыполнения программы. Следовательно, если имеются ссылки на различныеобъекты, будут выполняться различныеверсии виртуального метода. Другими словами, именно тип объекта, на который указывает ссылка (а не тип ссылки) определяет, какая версия виртуального метода будет выполнена. Таким образом, если базовый класс содержит виртуальный метод и из этого класса выведены производные классы, то при наличии ссылки на различные типы объектов (посредством ссылки на базовый класс) будут выполняться различные версии этого виртуального метода.

Чтобы объявить метод в базовом классе виртуальным, его объявление необходимо предварить ключевым словом virtual . При переопределении виртуального метода в производном классе используется модификатор override. Итак, процесс переопределения виртуального метода в производном классе иногда называется замещениемметода(methodoverriding). При переопределении метода сигнатуры типа у виртуального и метода-заменителя должны совпадать. Кроме того, виртуальный метод нельзя определять как статический (с использованием слова static) или абстрактный (с использованием слова abstract).

Переопределение виртуального метода формирует базу для одной из самых мощных концепций С#: динамической диспетчеризации методов. Динамическая диспетчеризация методов — это механизм вызова переопределенного метода во время выполнения программы, а не в период компиляции. Именно благодаря механизму диспетчеризации методов в С# реализуется динамический полиморфизм.

// Демонстрация виртуального метода.

usingSystem;

classBase {

// Создаем виртуальный метод в базовом классе,

public virtual void who () {

Console.WriteLine("Метод who() вклассе Base.");

}}

classDerived1 :Base {

// Переопределяем метод who() в производном классе,

public override void who() {

Console.WriteLine("Метод who() вклассе Derived1")

}}

classDerived2 :Base {

// Сновапереопределяемметодwho()

// в другом производном классе,

publicoverridevoidwho() {

Console.WriteLine("Метод who() вклассе Derived2

}}

class OverrideDemo {

public static void Main() {

Base baseOb = new Base();

Derived1dOb1 = newDerived1()

Derived2 dOb2 = new Derived2()

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.

Виртуальный метод переопределять необязательно. Если производный класс не предоставляет собственную версию виртуального метода, используется версия, определенная в базовом классе. Если производный класс не переопределяет виртуальный метод в случае многоуровневой иерархии, то будет выполнен первый переопределенный метод, который обнаружится при просмотре иерархической лестницы в направлении снизу вверх.

Свойства также можно модифицировать с помощью ключевого слова virtual, а затем переопределять с помощью ключевого слова override.

 

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

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

Абстрактный метод создается с помощью модификатора типа abstract. Абстрактный метод не содержит тела и, следовательно, не реализуется базовым классом. Поэтому производный класс должен его переопределить, поскольку он не может использовать версию, предложенную в базовом классе. Нетрудно догадаться, что абстрактный метод автоматически является виртуальным, поэтому и нет необходимости в использовании модификатора virtual. Более того, совместное использование модификаторов virtual и abstract считается ошибкой.

Для объявления абстрактного метода используйте следующий формат записи.

abstract ТИП ИМЯ(список_параметров) ;

Тело абстрактного метода отсутствует. Модификатор abstract можно использовать только применительно к обычным, а не к static-методам. Свойства также могут быть абстрактными.

Класс, содержащий один или несколько абстрактных методов, также должен быть объявлен как абстрактный с помощью спецификатора abstract, который ставится перед объявлением class. Поскольку абстрактный класс нереализуем в полном объеме, невозможно создать его экземпляры, или объекты. Таким образом, попытка создать объект абстрактного класса с помощью оператора 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; }

}

public string name {

get { return pri__name; }set { pri_name = value; }

}

public void showDim() {

Console.WriteLine("Ширинаивысотаравны " +

width + " и " + height);

}

// Теперьметодаrеа() абстрактный,

publicabstractdoublearea();

}

// Класс треугольников, производный от класса 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;

}

// Переопределяемметодаrеа() длякласса 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;

}

// Переопределяемметодаrеа() длякласса 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();

}}}

 

5.4 Использование ключевого слова sealed для предотвращения наследования

В С# предусмрена возможность предотвращения наследования класса с помощью ключевого слова sealed. Чтобы запретить наследование класса, предварите его объявление ключевым словом sealed. Рассмотрим пример sealed-класса.

sealedclassA {

//…

}

// Следующий класс создать невозможно.

class В : А { // ОШИБКА! Класс А не может иметь //наследников.

//…

}

Класс В не может быть производным от класса А, так как последний объявлен sealed-классом.

 

5.5 Класс object

В С# определен специальный класс с именем object, который является неявнымбазовым классом всех других классов и типов (включая типы значений). Другими словами, все остальные типы выводятся из класса object. Это означает, что ссылочная переменная типа object может указывать на объект любого типа. Кроме того, посколькуС#-массивы реализованы как классы, переменная типа object также может ссылатьсяна любой массив. Строго говоря, С#-имя object — еще одно имя для классаSystem.Object, который является частью библиотеки классов .NETFramework.

Класс object определяет методы, перечисленные в табл. Эти методы доступны для каждого объекта.

 

public virtual bool Equals (objectob) Определяет, является ли вызывающий объест таким же, как обьект, адресуемый ссылкой ob
public static bool Equals (objectob1,object ob2) Определяет, является ли объект ob1 таким же, как объект ob2
protected Finalize() Выполняет завершающие действия перед процессом сбора мусора. В С# метод Finalize () доступен через деструктор
public virtualint GetHashCode () Возвращает хеш-код, связанный с вызывающим объектом
public Type GetType() Получает тип объекта во время выполнения программы
protected objectMemberwiseClone () Выполняет "поверхностное копирование" объекта, т.е. копируются члены, но не объекты, на которые ссылаются эти члены
public static boolReferenceEquals (object ob1,object ob2) Определяет, ссылаются ли объекты оb1 и оb2 на один и тот же объект
public virtual string ToString() Возвращает строку, которая описывает объект