Файл: 1_C#_Введение_в_программирование_Уч_пособ_(Марченко-2005).pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 11.12.2025

Просмотров: 1392

Скачиваний: 3

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

public static explicit operator Point2D(Point3D p3d)

{

Point2D p2d = new Point2D(); p2d.x = p3d.x;

p2d.y = p3d.y; return p2d;

}

}

// Тестовый класс. Здесь всё происходит. class TestClass

{

static void Main(string[] args)

{

Point2D p2d = new Point2D(125,125); Point3D p3d; // Сейчас это только ссылка!

//Этой ссылке присваивается значение в результате

//НЕЯВНОГО преобразования значения типа Point2D к типу Point3D p3d = p2d;

//Изменили значения полей объекта. p3d.x = p3d.x*2;

p3d.y = p3d.y*2;

p3d.z = 125; // Главное - появилась новая информация,

//которая неизбежно будет потеряна в случае присвоения значения типа Point3D

//значению типа Point2D. Ключевое слово explicit в объявлении соответствующего

//метода преобразования приводит к тому, что программист всякий раз вынужден

//повторять, что он в курсе возможных последствий.

p2d = (Point2D)p3d;

}

}

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

Наследование является одним из принципов ООП.

Основы классификации и реализация механизмов повторного использования и модификации кода. Базовый класс задаёт общие признаки и общее поведение для классов-наследников.

Общие (наиболее общие) свойства и методы наследуются от базового класса, в дополнение к которым добавляются и определяются НОВЫЕ свойства и методы.

Таким образом, прежде всего, наследование реализует механизмы расширения базового класса.

Реализация принципов наследования на примере.

using System; namespace Inheritance_1

{

public class A

{

public int val1_A;

public void fun1_A (String str)

{

Console.WriteLine(“A’s fun1_A:” + str);

}

}

public class B:A

{

public int val1_B;

public void fun1_B (String str)

{

Console.WriteLine(“B’s fun1_B:” + str);

}

}

class Class1

{

static void Main(string[] args)

68

{

B b0 = new B();

//От имени объекта b0 вызвана собственная функция fun1_B. b0.fun1_B(“from B”);

//От имени объекта b0 вызвана унаследованная от класса A функция fun1_A. b0.fun1_A(“from B”);

}

}

}

Наследование и проблемы доступа

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

Добавляем в базовый класс частные члены:

public class A

{

public int val1_A = 0;

public void fun1_A (String str)

{

Console.WriteLine(“A’s fun1_A:” + str); this.fun2_A(“private function from A:”);

}

//При определении переменных

//в C# ничего не происходит без конструктора и оператора new.

//Даже если они и не присутствуют явным образом в коде. private int val2_A = 0;

private void fun2_A (String str)

{

Console.WriteLine(str + “A’s fun2_A:” + val2_A.ToString());

}

}

Иобъект-представитель класса B в принципе НЕ может получить доступ к частным данным-членам и функциям-членам класса A. Косвенное влияние на такие данные-члены и функции-члены лишь через публичные функции класса A.

Следует иметь в виду ещё одно важное обстоятельство.

Если упорядочить все (известные) спецификаторы доступа C# по степени их открытости:

Максимальная степень открытости

 

Минимальная степень открытости

public

private

то наследуемый класс не может иметь более открытый спецификатор доступа, чем его предок.

protected class A

{

:::::::::

}

public class B:A // Неправильное наследование.

{

:::::::::

}

Используем ещё один спецификатор доступа – protected. Этот спецификатор обеспечивает открытый доступ к членам базового класса, но только для производного класса!

public class A

{

:::::::::

protected int val3_A = 0;

}

public class B:A

69


{

:::::::::

public void fun1_B (String str)

{

:::::::::

this.val3_A = 125;

}

}

static void Main(string[] args)

{

:::::::::

//b0.val3_A = 125; // Это член класса закрыт для внешнего использования!

}

Защищённые члены базового класса доступны для ВСЕХ прямых и косвенных наследников данного класса.

И ещё несколько важных замечаний относительно использования спецификаторов доступа:

в C# структуры НЕ поддерживают наследования. Поэтому спецификатор доступа protected в объявлении данных-членов и функций-членов структур НЕ ПРИМЕНЯЕТСЯ, спецификаторы доступа действуют и в рамках пространства имён (поэтому и классы в нашем пространстве имён были объявлены со спецификаторами доступа public). Но в пространстве имён явным образом можно использовать лишь один спецификатор – спецификатор public, либо не использовать никаких

спецификаторов.

Явное обращение к конструктору базового класса

Продолжаем совершенствовать наши классы A и B. Очередная задача – выяснить способы передачи управления конструктору базового класса при создании объекта-представителя производного класса.

using System;

/*private*/ class X

{

}

/*public*/ class A

{

public A(){val2_A = 0; val3_A = 0;}

//К этому конструктору также можно обратиться из производного класса. protected A(int key):this() {val1_A = key;}

//А вот этот конструктор предназначен исключительно

//для внутреннего использования.

private A(int key1,int key2,int key3){val1_A = key1; val2_A = key2; val3_A = key3;}

public int val1_A = 0;

public void fun1_A (String str)

{

Console.WriteLine(“A’s fun1_A:” + str); this.fun2_A(“private function from A:”); fun3_A();

}

private void fun2_A (String str)

{

Console.WriteLine(str + “A’s fun2_A:” + val2_A.ToString());

}

protected int val3_A; private void fun3_A ()

{

A a = new A(1,2,3);

a.fun2_A(“Это наше внутреннее дело!”);

}

}

70


/*public*/ class B:A

{

public B():base(){val1_B = 0;}

public B(int key):base(key){val1_B = key;}

}

class Class1

{

static void Main(string[] args)

{

B b0 = new B(125);

}

}

Таким образом, в программе для создания объектов можно применять конструкторы трёх степеней защиты:

public – при создании объектов в рамках данного пространства имён, в методах любого класса-члена данного пространства имён,

protected – при создании объектов в рамках производного класса, в том числе при построении объектов производного класса, а также для внутреннего использования классом-владельцем данного конструктора,

private – применяется исключительно для внутреннего использования классомвладельцем данного конструктора.

Кто строит БАЗОВЫЙ ЭЛЕМЕНТ

Теперь (в рамках того же приложения) построим два новых класса.

class X

{

}

class Y:X

{

}

// Это уже в Main() Y y = new Y();

Вся работа по созданию объекта-представителя класса Y при явном отсутствии конструкторов по умолчанию возлагается на КОНСТРУКТОРЫ УМОЛЧАНИЯ – те самые, которые самостоятельно строит транслятор. Особых проблем не будет, если в производном классе явным образом начать объявлять конструкторы.

class X

{

}

class Y:X

public Y(int key){} public Y(){}

}

// Это уже в Main() Y y0 = new Y();

Y y1 = new Y(125);

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

Проблемы возникнут, если в базовом классе попытаться объявить вариант конструктора с параметрами:

class X

{

public X(int key){}

}

71

class Y:X

public Y(int key){} // Нет конструктора умолчания базового класса! public Y(){} // Нет конструктора умолчания базового класса!

}

// Это уже в Main() Y y0 = new Y();

Y y1 = new Y(125);

И здесь транслятор начнёт обижаться на конструкторы производного класса, требуя ЯВНОГО объявления конструктора базового класса БЕЗ параметров. Если вспомнить, что при ЛЮБОМ вмешательстве в дело построения конструкторов транслятор снимает с себя всю ответственность, причина негодования транслятора становится очевидной. Возможны два варианта решения проблемы:

явным образом заставить работать новый конструктор базового класса,

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

class X

{

public X(){} public X(int key){}

}

class Y:X

public Y(int key){} public Y():base(125){}

}

// Это уже в Main() Y y0 = new Y();

Y y1 = new Y(125);

Переопределение членов базового класса

При объявлении членов производного класса в C# разрешено использовать те же

самые

имена,

которые

использовались при объявлении членов базового класса.

Это касается как методов, так и данных-членов объявляемых классов.

считаются

В

этом

случае

соответствующие

члены

базового

класса

переопределёнными.

 

 

 

 

 

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

При переопределении в производном классе наследуемого члена его объявление в производном классе должно предваряться модификатором new. Если этот модификатор в объявлении производного класса опустить - ничего страшного не произойдёт. Транслятор всего лишь выдаст предупреждение об ожидаемом спецификаторе new в точке объявления переопределяемого члена класса.

Дело в том, что переопределённые члены базового класса при “взгляде” на производный класс “извне” не видны. В производном классе они замещаются переопределёнными членами и непосредственный доступ к этим членам возможен только из функций-членов базового и производного классов. При этом для обращения к переопределённому члену из метода производного класса используется ключевое слово base.

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

using System; namespace Inheritance_2

{

class X

{

public X(){} public X(int key){}

72


public int q0; public int q; public void fun0()

{

Console.WriteLine(“class X, function fun0()”); q = 125;

}

public void fun1()

{

Console.WriteLine(“class X, function fun1()”);

}

}

class Y:X

{

public Y(int key){} public Y():base(125){}

new public int q; // Если опустить модификатор new -

new public void fun1() // появится предупреждение об ожидаемом new...

{

base.fun1(); // Обращение к переопределённым членам базового класса. base.q = 125;

Console.WriteLine(“class Y, function fun1()”);

}

static void Main(string[] args)

{

Y y0 = new Y();

Y y1 = new Y(125);

// А извне переопределённые члены базового класса не видны. y0.fun1();

y0.q = 100; y0.fun0(); y0.q0 = 125;

}

}

}

Объявление класса может содержать вложенное объявление класса. C# допускает и такие объявления:

class X

{

public class XX

{

}

}

Так вот одновременное включение явного объявления класса или структуры с одним и тем же именем в базовый и производный классы также требует явной спецификации с помощью модификатора new:

class X

{

public class XX

{

}

}

class Y:X

{

new public class XX

{

}

}

73

Модификатор new используется при переопределении общедоступных объявлений и защищённых объявлений в производном классе для явного указания факта переопределения.

Наследование и new модификатор

Этот модификатор используется при объявлении класса-наследника в том случае, если надо явным образом скрыть факт наследования объявляемого члена класса.

public class MyBaseC

{

public int x;

public void Invoke();

}

Объявление члена класса Invoke в наследуемом классе скрывает метод Invoke в базовом классе:

public class MyDerivedC : MyBaseC

{

new public void Invoke();

}

А вот данное-член x не было скрыто в производном классе соответствующим членом, следовательно, остаётся доступным в производном классе.

Сокрытие имени члена базового класса в производном классе возможно в одной из следующих форм:

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

Внедрённый в класс или структуру метод скрывает в базовом классе свойства, поля, типы, обозначенные тем же самым идентификатором. Также в базовом классе скрываются одноимённые методы с той же самой сигнатурой.

Объявляемые в производном классе индексаторы, скрывают одноименные индексаторы в базовом классе с аналогичной сигнатурой.

В производном классе одновременное использование члена базового класса и его переопределённого потомка является ошибкой.

Полное квалифицированное имя. Примеры использования

В этом примере базовый и BC и производный DC классы используют одно и то же имя для обозначения объявляемого в обоих классах члена типа int. new модификатор подчеркивает факт недоступности члена x базового класса в производном классе. Однако из производного класса всё-таки можно обратиться к переопределённому полю базового класса с использованием полного квалифицированного имени.

using System; public class BC

{

public static int x = 55; public static int y = 22;

}

public class DC : BC

{

new public static int x = 100; // Переопределили члена базового класса public static void Main()

{

//Доступ к переопределённому члену x: Console.WriteLine(x);

//Доступ к члену базового класса x: Console.WriteLine(BC.x);

//Доступ к члену y: Console.WriteLine(y);

}

}

Впроизводном классе DC переопределяется вложенный класс C. В рамках производного класса легко можно создать объект-представитель вложенного

74


переопределённого класса C. Для создания аналогичного объекта-представителя базового класса, необходимо использовать полное квалифицированное имя.

using System; public class BC

{

public class C

{

public int x = 200; public int y;

}

}

public class DC:BC

{

new public class C // Вложенный класс базового класса скрывается

{

public int x = 100; public int y; public int z;

}

public static void Main()

{

//Из производного класса виден переопределённый вложенный класс: C s1 = new C();

//Полное имя используется для доступа к вложенному в базовый класс: BC.C s2 = new BC.C();

Console.WriteLine(s1.x);

Console.WriteLine(s2.x);

}

}

Прекращение наследования. sealed спецификатор

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

Если при разработке класса возникла ситуация, при котором дальнейшее совершенствование и переопределение возможностей класса в деле решения специфических задач окажется нецелесообразным (сколько можно заниматься переопределением функций форматирования), класс может быть закрыт для дальнейшего наследования. Закрытие класса обеспечивается спецификатором sealed. При этом закрываться для наследования может как класс целиком, так и отдельные его члены.

sealed class X

{

}

class Y:X // Наследование от класса X невозможно

{

}

А так закрывается для переопределения функция-член класса:

class X

{

sealed public void f0()

{

}

}

class Y:X

{

public void f0(){} // Наследование (переопределение) f0, объявленной в X запрещено!

75

}

Абстрактные функции и абстрактные классы

При реализации принципа наследования базовый класс воплощает НАИБОЛЕЕ ОБЩИЕ черты разрабатываемого семейства классов. Поэтому на этапе разработки базового класса часто бывает достаточно лишь обозначить множество функций, которые будут определять основные черты поведения объектов-представителей производных классов.

Если базовый класс объявляется исходя из следующих предпосылок:

базовый класс используется исключительно как основа для объявления классовнаследников,

базовый класс никогда не будет использован для определения объектов,

часть функций-членов (возможно, все) базового класса в обязательном порядке

переопределяется в производных классах, то определение таких функций даже в самых общих чертах (заголовок функции и

тело, содержащее вариант оператора return) в базовом классе лишено всякого смысла.

class X

{

public int f0()

{

//Если в соответствии с замыслом разработчика, этот код ВСЕГДА будет недоступен,

//то зачем он в принципе нужен?

return 0;

}

}

class Y:X

{

new public void f0()

{

::::::::::

// Здесь размещается код переопределённой функции.

::::::::::

return 0;

}

}

Такой код никому не нужен и C# позволяет избегать таких странных конструкций. Вместо переопределяемой “заглушки” можно использовать объявление абстрактной функции.

Синтаксис объявления абстрактной функции предполагает использование ключевого слова abstract и полное отсутствие тела. Объявление абстрактной функции завершается точкой с запятой.

Класс, содержащий вхождения абстрактных (хотя бы одной!) функций также должен содержать в заголовке объявления спецификатор abstract.

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

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

abstract class X // Абстрактный класс с одной абстрактной функцией.

{

public abstract int f0();

}

class Y:X

{

//Переопределение абстрактной функции должно

//содержать спецификатор override.

76