Теория и задания по Си-Шарп (КФУ). Учебное пособие казань 2017 2 удк 681 06 ббк 32. 973 Печатается по постановлению Редакционноиздательского совета
![]()
|
ГЛАВА 10. НАСЛЕДОВАНИЕ В С# Наследование – это свойство объектно-ориентированной системы наследовать данные и функциональность базового класса. Можно в класс- потомок к методам и полям родительского класса добавить необходимые поля и методы. Класс-потомок может замещать методы родительского класса. Надо помнить, при изменении родительского класса, класс-потомок может оказаться не рабочим. Наследование от класса называется расширением базового класса. Если класс А наследует от класса В, то класс А называется потомком, а В – предком. Синтаксически это пишется следующим образом: class A:B {…} Класс-потомок наследует все элементы базового класса, кроме конструктора и деструктора. Все public элементы базового класса остаются неявно publiс в потомке, private элементы, хоть и наследуются, но доступны только для объектов базового класса. Класс-потомок не может быть более доступным, чем базовый класс. class Example { private class NestedBase { } public class NestedDerived: NestedBase { } // Ошибка } Класс-потомок имеет доступ ко всем protected полям и методам родительского класса, класс не являющийся потомком доступа к protected членам не имеет. class Token { protected string name; } class CommentToken:Token { public string Name() { return name; // Доступ разрешен } } 77 class CommentToken: Token { void Fails(Token t) { Console.WriteLine(t.name); // Ошибка при компиляции } } Для вызова конструктора базового класса из класса-потомка используется ключевое слово base. Вызов конструктора базового класса пишется после заголовка конструктора класса-потомка через двоеточие: A(список параметров): base(параметры для передачи в конструктор базового класса) { //тело конструктора у класса потомка } Ключевое слово base обращается к базовому классу. Для конструктора по умолчанию вызов базового конструктора производится по умолчанию, то есть его можно не прописывать. Для конструкторов не по умолчанию необходимо явно вызывать конструктор базового класса. class Token { protected Token(string name) { ... } } class CommentToken: Token { public CommentToken(string name) { ... } // ошибка при компиляции } В примере выше получим ошибку при компиляции, так как будет вызываться конструктор по умолчанию, но он отсутствует в классе Token. Для правильной работы необходимо записать конструктор в следующем виде: public CommentToken(string name) : base(name) { ... } Если конструктор базового класса private, то нельзя создавать конструктор класса потомка. class NonDerivable { private NonDerivable(){ } 78 } class Impossible: NonDerivable { public Impossible() { } // Ошибка при компиляции } В классе-потомке можно переопределять методы базового класса, если они для этого предназначены. Виртуальные методы можно полиморфно переопределять в классах-потомках. В С# по тому, содержит ли базовый класс виртуальные методы, можно определить, был ли этот класс разработан для наследования. Не виртуальный метод имеет только одно определение, одинаковое для всех потомков. Для объявления виртуального метода используется ключевое слово virtual. Виртуальный метод должен содержать тело в базовом классе. Нельзя объявлять виртуальными статичные и private методы. Статичные методы не могут быть виртуальными, так как полиморфизм – это свойство, относящееся к объектам, а не к классам. Перегруженные методы могут создаваться только для виртуальных методов. Для задания перегруженных методов используется ключевое слово override. В базовом классе метод объявляется виртуальным virtual, в базовом классе есть тело у этого метода, в классе-потомке этот метод определяется перегруженным override и содержит свое тело – таким образом задается перегруженный метод. class Token { public virtual string Name() {…} } class CommentToken : Token { public override string Name() {…} } Можно перегружать только абсолютно идентичные методы. Должны совпадать: имя метода, тип возвращаемого значения, список параметров, уровень доступа. Перегруженный метод должен быть virtual или override. Нельзя объявлять перегруженный метод одновременно виртуальным, т.е. override virtual – нельзя. Нельзя, чтобы перегруженные методы были статичными или private. Можно скрыть наследуемый метод в иерархии классов, заменив его новым идентичным методом при помощи ключевого слова new. Метод 79 родительского класса не будет наследоваться потомком, и заменится на новый идентичный метод. class Token { public int LineNumber() {…} } class CommentToken : Token { new public int LineNumber() {…} } Ключевое слово new прячет как виртуальные, так и невиртуальные методы, разрешает проблему совпадения имен, скрывает методы с одинаковой сигнатурой. В C# можно объявить класс ненаследуемым, при помощи ключевого слова sealed. public sealed class String {..} public class MyStr:String; // ошибка – от класса String наследовать нельзя Использование интерфейсов Интерфейс – синтаксический и семантический шаблон, которого все классы-наследники должны придерживаться. Интерфейс говорит, что он умеет делать, классы определяют, как они это делают. Интерфейс представляет собой класс без какого-либо кода. Все интерфейсы по умолчанию public, модификатор доступа у интерфейсов не используется. У методов также не используется модификатор доступа, по умолчанию методы public. У методов в интерфейсе не должно быть тела, только заголовки методов. interface IToken { public int LineNumber( ){ … }; // Ошибка при компиляции: 1. Модификатор доступа у метода public //2. Есть тело метода } С# позволяет наследовать от одного класса и множества интерфейсов. Интерфейс может наследовать от многих интерфейсов. interface IToken { ... } interface IVisitable { ... } 80 interface IVisitableToken: IVisitable, IToken { ... } class Token: IVisitableToken { ... } Класс может быть более доступным, чем интерфейс class Example { private interface INested { } public class Nested: INested { } // Разрешено } Класс должен определить все методы всех интерфейсов, от которых он наследует как напрямую, так и косвенно. Метод интерфейса, определяемый классом должен быть идентичен, то есть должны совпадать параметр доступа, имя, возвращаемое значение и список параметров. Интерфейсные методы, реализуемые в классе, могут быть объявлены как virtual. В этом случае классы наследники могут перегружать эти методы в дальнейшем. Другой способ реализации интерфейсных методов – явная реализация. При явной реализации необходимо указать полное имя метода: Имя_интерфейса.имя_метода. При явном определении метод не может быть виртуальным, должен отсутствовать модификатор доступа. При вызове метода к нему нет прямого доступа, только через интерфейс. class Token: IToken, IVisitable { string IToken.Name( ) { } private void Example( ) { Name( ); // Ошибка при компиляции ((IToken)this).Name( ); // Правильно } } Явная реализация позволяет: • исключить определение интерфейса из класса, если он не интересен пользователям класса. • обеспечивать классу несколько определений различных методов интерфейсов одинаковой сигнатуры. 81 interface IArtist { void Draw(); } interface ICowboy { void Draw(); } class ArtisticCowboy: IArtist, ICowboy { void IArtist.Draw() {…} void ICowbowt.Draw() {…} } Использование абстрактных классов Абстрактные классы используются для частичной реализации классов, которые могут быть полностью реализованы в конкретных классах-потомках. Абстрактный класс объявляется с помощью ключевого слова abstract. Правила создания абстрактного класса совпадают с правилами создания обычных классов. Однако, в абстрактных классах можно объявлять абстрактные методы. Нельзя создавать объекты абстрактного класса. Абстрактный класс может являться наследником неабстрактного класса. Все методы интерфейса, определяемого абстрактным классом, должны быть определены в абстрактном классе. И абстрактные классы, и интерфейсы предназначены для наследования. Однако класс может наследовать только от одного абстрактного класса. Только абстрактные классы могут иметь абстрактные методы. У абстрактного метода отсутствует тело метода. Абстрактные методы – виртуальные, переопределенные абстрактные методы у классов-потомков будут override. Абстрактные методы могут переопределять virtual и override методы. class Token { public virtual string Name( ) { ... } } 82 abstract class Force: Token { public abstract override string Name( ); } Вопросы к разделу 1. В чем отличие между public, private и protected полями? 2. Как переопределить метод базового класса у класса-потомка? 3. Что такое абстрактный класс? 4. Что такое интерфейс? В чем отличие интерфейса от абстрактного класса? 5. Допустимо ли множественное наследование? Лабораторная работа Задания на наследование, определение и использование интерфейсов, абстрактных классов, виртуальные методы. Время, необходимое на выполнение задания 60 мин. Упражнение 10.1. Создать интерфейс ICipher, который определяет методы поддержки шифрования строк. В интерфейсе объявляются два метода encode() и decode(), которые используются для шифрования и дешифрования строк, соответственно. Создать класс ACipher, реализующий интерфейс ICipher. Класс шифрует строку посредством сдвига каждого символа на одну «алфавитную» позицию выше. Например, в результате такого сдвига буква А становится буквой Б. Создать класс BCipher, реализующий интерфейс ICipher. Класс шифрует строку, выполняя замену каждой буквы, стоящей в алфавите на i-й позиции, на букву того же регистра, расположенную в алфавите на i-й позиции с конца алфавита. Например, буква В заменяется на букву Э. Написать программу, демонстрирующую функционирование классов. Домашнее задание 10.1 Создать класс Figure для работы с геометрическими фигурами. В качестве полей класса задаются цвет фигуры, состояние «видимое/невидимое». Реализовать операции: передвижение геометрической фигуры по горизонтали, по вертикали, изменение цвета, опрос состояния (видимый/невидимый). Метод вывода на экран должен выводить состояние всех полей объекта. 83 Создать класс Point (точка) как потомок геометрической фигуры. Создать класс Circle (окружность) как потомок точки. В класс Circle добавить метод, который вычисляет площадь окружности. Создать класс Rectangle (прямоугольник) как потомок точки, реализовать метод вычисления площади прямоугольника. Точка, окружность, прямоугольник должны поддерживать методы передвижения по горизонтали и вертикали, изменения цвета. Подумать, какие методы можно объявить в интерфейсе, нужно ли объявлять абстрактный класс, какие методы и поля будут в абстрактном классе, какие методы будут виртуальными, какие перегруженными. 84 ГЛАВА 11. АГРЕГАЦИИ, ПРОСТРАНСТВА ИМЕН, СБОРКИ И МОДУЛИ Использование внутренних (internal) классов, методов и данных Модификаторы доступа определяют возможность доступа к элементам класса, таким как методы и свойства. При разработке класса необходимо явно указывать модификатор доступа у каждого члена класса. Модификаторы доступа: • Public – элементы доступны всюду внутри области видимости; • Protected – элементы доступны внутри класса и у всех потомков класса; • Private – элементы доступны только внутри класса; • Internal – элементы доступны внутри одной сборки Microsoft.NET (одного исполняемого файла или одного .dll файла), из других сборок доступ запрещён; • Protected internal – элементы доступны внутри классов-потомков и во всех классах внутри одной сборки. Public – слишком открытый доступ, private – слишком закрытый, protected – открыт только для потомков класса. Для создания промежуточного типа доступа был создан внутренний (internal) доступ. Типы доступа public и private – логические, то есть не зависят от физического размещения класса. Для internal классов или элементов размещение определяет область доступа: если класс находится в исполняемом файле, то он доступен для всех классов файла; если класс относится к конкретной сборке, то доступ разрешен только внутри данной сборки. В исполняемом файле может быть несколько сборок, тогда доступ к internal классу из другой сборки будет запрещен. Когда класс определен как internal, доступ к нему имеют все классы из сборки. Классы и элементы protected internal доступны всей сборке, а также всем потомкам текущего класса. Ограничения на использование модификаторов доступа: 1. Вне классов и пространств имен нельзя объявлять классы protected и private, эти типы доступа можно использовать только внутри какого-либо класса. Для элементов внутри класса модификатором доступа по 85 умолчанию будет private. В С# есть возможность внутри одного класса объявлять другой класс. 2. При объявлении типа в глобальной области видимости можно использовать модификаторы доступа public и internal. В глобальной области видимости или в пространстве имен модификатор доступа по умолчанию будет internal. Использование агрегаций Агрегации используются для группировки объектов вместе в иерархию объектов, которую можно неоднократно использовать. Агрегации определяют связь целое/часть между объектами, не классами. В одном случае в этой связи время жизни «целого» и «части» могут быть не связаны. В этом случае агрегация называется агрегацией по ссылке. В случае, когда время жизни «целого» и «части» связаны друг с другом, агрегация называется агрегацией по значению. В агрегации «целое» – это всего лишь класс, который используется для группировки «частей»-классов в единое целое, т.е. «целый» класс реально не существует. Например, что такое компьютер? Это всего лишь имя, которое используется для описания конкретных частей: CPU, монитор, клавиатура и т.д. Через это имя можно получить доступ к методам, которые работают с составными частями. Сравнение агрегаций и наследования: • Агрегации определяют связь на уровне объектов, наследование – на уровне классов. В случае агрегации у целого объекта может быть несколько объектов частей одного типа. Например, у агрегации компьютер может быть несколько мониторов, несколько процессоров. В случае наследования речь идет не о конкретных объектах, а о классах. В этом случае компьютер и монитор – это просто два разных класса, никак не связанные друг с другом. • В агрегации при изменении методов «частей» методы «целого» автоматически не меняются. В наследовании же при изменении методов у предков, меняются и потомки. • В агрегации в объект «целого» можно добавлять и удалять объекты «части» без каких-либо ограничений. В главном объекте сохраняются 86 ссылки на части, при необходимости они могу изменяться, можно создавать новые, удалять старые. В механизме наследования все статично: класс потомок всегда будет оставаться потомком. Фабрики классов Иногда бывает необходимо запретить создание объекта класса напрямую, разрешить создавать объекты только определенным объектам некоторого другого класса. Например, самостоятельно нельзя открыть счет в банке. Чтобы открыть счет, нужно пойти в банк и банковский работник открывает счет. В программировании, пусть есть два класса Bank и BankAccount. Открывать счет в банке – создавать объект класс BankAccount можно только внутри банка, вызвав внутри класса Bank нужный конструктор: public class Bank { public BankAccount OpenAccount( ) { BankAccount opened = new BankAccount( ); accounts[opened.Number( )] = opened; return opened; } private Hashtable accounts = new Hashtable( ); } public class BankAccount { internal BankAccount( ) { ... } public long Number( ) { ... } public void Deposit(decimal amount) { ... } } В примере для хранения всех открытых в банке счетов внутри класса Bank предусмотрено поле accounts в виде хэш-таблицы. Каждый открытый банковский счет сохраняется в этой хэш-таблице. В данном случае класс Bank будет являться фабрикой. Для того чтобы конструктор класса BankAccount был доступен внутри класса Bank, его объявили с модификатором доступа internal. 87 Пространства имен Рассмотрим пример public class Bank { public class Account { public void Deposit(decimal amount) { balance += amount; } private decimal balance; } public Account OpenAccount( ) { ... } } В этом примере четыре области видимости: • глобальная область – в ней располагается класс Bank; • область класса Bank – в ней класс Account и метод OpenAccount; • область класса Account – в ней метод Deposit и поле balance; • область метода Deposit – в ней объявляется параметр amount. Если объект или метод находится вне области видимости или его имя скрыто, то вызвать его можно только при помощи полного пути. class Top { public void M(){…} } class Bottom:Top { new public void M() { M(); // рекурсия base.M(); //обращение к скрытому методу } 88 } При обращении к элементам базового класса используется ключевое слово base. Точно так же и для полей класса: public struct Point { public Point(int x, int y) { this.x = x; this.y = y; } private int x,y; } В сложных проектах, в которых используется большое количество классов, когда над проектом работает несколько программистов, возможна ситуация, что в одной области видимости окажутся два класса с одинаковым именем. В таком случае следует использовать пространства имен (namespaces). Пространства имен разделяют большой проект на несколько подсистем, в каждой подсистеме – своя глобальная область видимости. Рекомендуется внутри одного пространства имен размещать логически связанные друг с другом элементы. namespace Outer { namespace Inner { class Widget{…} } } В С# можно записать предыдущий класс короче. namespace Outer.Inner { class Widget{…} } Для того чтобы использовать класс из другого пространства имен, необходимо использовать полное его имя или подключить соответствующее пространство имен, используя директиву using. Например, для обращения к классу Widget из предыдущего примера из другого пространства имен можно написать 89 Outer.Inner.Widget w; //полный путь к классу Widget или using Outer.Inner; //подключаем пространство имен Widget w; //объявляем объект класса Widget без указания полного пути к классу. Директива using должна располагаться перед объявлением каких-либо элементов либо в глобальной области видимости, либо внутри пространства имен. Директива разрешает доступ по имени к классам только данного пространства имен, то есть доступ к классам вложенных пространств имен по короткому имени запрещен. namespace Microsoft.PowerPoint { public class Widget { ... } } namespace VendorB { using Microsoft; // но не Microsoft.PowerPoint class SpecialWidget: Widget { ... } // ошибка на этапе компиляции } Кроме того, если подключить при помощи директивы два пространства имен с несколькими классами с одним именем, то при вызове этого класса получим ошибку компиляции. namespace Test { using VendoreA; // содержит класс Widget using VendoreB; // также содержит класс Widget // Здесь ошибки нет class Application { static void Main() { Widget w = new Widget(); // ошибка при компиляции } } } 90 Ошибка возникает, так как компилятор не знает, какой класс нужен. Надо явно прописать необходимое пространство имен, например VendoreA.Widget w = new VendoreA.Widget(); Директиву using можно использовать для объявления коротких имён классов напрямую. using Widget = VendoreA.SuiteB.Widget В этом случае можно сразу обращаться по короткому имени к данному классу. Все ограничения на использование директивы остаются. Директиву using можно использовать в обеих её возможностях одновременно, единственное, что надо помнить, что действие директивы начинается с первого элемента пространства имен в которой она объявлена, то есть друг на друга директивы не действуют, и следующая конструкция даст ошибку: using System; using TheConsole = Console; // ошибка, хотя Console класс из System Правильно будет: using System; using TheConsole = System.Console; Модули и сборки В С# присутствует возможность компилировать исходные .cs файлы в управляемый модуль – промежуточный язык MSIL, который содержит метаданные для описания модуля. Для этого в опциях компилятору надо задать параметр /target:module имя_файла.cs. При выполнении программы управляемый модуль преобразовывается в машинный код. При компиляции исходный файл компилируется в управляемый модуль с расширением .netmodule. Команда командной строки: csc /target:module Bank.cs На выходе получим файл Bank.netmodule. Выполняемый файл может запускать модули только в составе какой-либо сборки. В сборке физически размещается группа взаимодействующих классов. Классы, находящиеся в одной сборке, имеют доступ к internal элементам друг друга. Для классов из других сборок доступ к этим элементам закрыт. Сборка – это многократно-используемый, безопасный, и самоописываемый модуль, содержащий типы и ресурсы с поддержкой версий; 91 это первичный стандартный блок приложения .NET. Сборка состоит из двух логических частей: • наборов типов и ресурсов, которые формируют некоторый логический модуль из функциональных возможностей, • метаданные, которые описывают, как эти элементы связаны и от чего зависят их правильная работа. Метаданные, которые описывают сборку, называют манифестом. В манифесте содержится следующая информация: • Уникальность. Уникальность сборки включает в себя простое текстовое название, номер версии и дополнительный открытый ключ, который гарантирует уникальность названия и защищает от нежелательного использования. • Содержание. Сборка содержит типы и ресурсы. Манифест перечисляет названия всех типов и ресурсов, которые видимы извне сборки. Содержит информацию о том, где они могут быть найдены во время трансляции. • Ссылки. Каждая сборка явно описывает другие сборки, от которых она зависит. В простейшем случае, сборка состоит из одного файла, который содержит код, ресурсы, метаданные и манифест. В более общем случае, сборка состоит из нескольких файлов, тогда сборка существует как автономный файл или содержится в одном из файлов PE, которые содержат типы и ресурсы. Соответствующие команды компилятора: сsc /target:library /out:Bank.dll Bank.cs Account.cs сsc /t:library /addmodule:Account.netmodule /out:Bank.dll Bank.cs Каждая сборка имеет свой уникальный номер версии. Две сборки, которые отличаются только номером, для исполняющей среды различны. Номер версии состоит из четырех частей: старшая версия . младшая версия . номер компиляции . редакция Например, номер 1.5.12540.0 означает, что старшая версия 1, младшая 5, номер компиляции 12540 и редакция 0. Пространства имен – это логический механизм компиляции, он обеспечивает структуру имен сущностей исходного кода. При выполнении программы пространства имен не рассматриваются. Сборки – это физический 92 механизм выполнения, обеспечивающий структуру компонент при исполнении программы. Можно разместить классы одного пространства имен в разных сборках, также в одной сборке могут быть классы разных пространств имен. В сборку можно подключать внешние модули, тогда в сборке записывается именованная ссылка на управляемый модуль. Вопросы к разделу 1. Пусть есть два cs файла. Файл alpha.cs содержит класс Alpha c internal методом Method. Файл beta.cs содержит класс Beta c internal методом с тем же названием Method. Может ли Alpha.Method вызвать Beta.Method, и наоборот? 2. Агрегации это связь объектов или классов? 3. Откомпилируется ли следующий код без ошибок? namespace Outer.Inner { class Wibble{} } namespace Test { using Outer.Inner; class SpecialWible:Inner.Wible{} } Лабораторная работа Модификатор доступа internal. Задание на агрегацию, пространство имен. Время, необходимое на выполнение задания 60 мин. Упражнение 11.1 Создать новый класс, который будет являться фабрикой объектов класса банковский счет. Изменить модификатор доступа у конструкторов класса банковский счет на internal. Добавить в фабричный класс перегруженные методы создания счета CreateAccount, которые бы вызывали конструктор класса банковский счет и возвращали номер созданного счета. Использовать хеш-таблицу для хранения всех объектов банковских счетов в фабричном классе. В фабричном классе предусмотреть метод закрытия счета, 93 который удаляет счет из хеш-таблицы (методу в качестве параметра передается номер банковского счета). Использовать утилиту ILDASM для просмотра структуры классов. Упражнение 11.2 Разбить созданные классы, связанные с банковским счетом, и тестовый пример в разные исходные файлы. Разместить классы в одно пространство имен и создать сборку. Подключить сборку к проекту и откомпилировать тестовый пример со сборкой. Получить исполняемый файл, проверить с помощью утилиты ILDASM, что тестовый пример ссылается на сборку и не содержит в себе классов, связанный с банковским счетом. Домашнее задание 11.1 Для реализованного класса из домашнего задания 7.1 создать новый класс Creator, который будет являться фабрикой объектов класса здания. Для этого изменить модификатор доступа к конструкторам класса, в новый созданный класс добавить перегруженные фабричные методы CreateBuild для создания объектов класса здания. В классе Creator все методы сделать статическими, конструктор класса сделать закрытым. Для хранения объектов класса здания в классе Creator использовать хеш-таблицу. Предусмотреть возможность удаления объекта здания по его уникальному номеру из хеш-таблицы. Создать тестовый пример, работающий с созданными классами. Домашнее задание 11.2 Разбить созданные классы (здания, фабричный класс) и тестовый пример в разные исходные файлы. Разместить классы в одном пространстве имен. Создать сборку (DLL), включающие эти классы. Подключить сборку к проекту и откомпилировать тестовый пример со сборкой. Получить исполняемый файл, проверить с помощью утилиты ILDASM, что тестовый пример ссылается на сборку и не содержит в себе классов здания и Creator. |