Теория и задания по Си-Шарп (КФУ). Учебное пособие казань 2017 2 удк 681 06 ббк 32. 973 Печатается по постановлению Редакционноиздательского совета
![]()
|
ГЛАВА 7. ОСНОВЫ ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОГРАММИРОВАНИЯ С# – объектно-ориентированный язык. В этой главе изучим терминологию и концепцию ООП. Классы и объекты Ключевым словом в ООП является класс. Все языки программирования могут обращаться с общими данными и методами. Эта возможность помогает избежать дублирования. Главная концепция программирования – не писать один и тот же код дважды. Программы без дублирования лучше и понятнее, так как содержат меньше кода. ООП переводит эту концепцию на новый уровень, он позволяет описывать классы (множества объектов), которые делают общими и структуру, и поведение. Классы не ограничиваются описанием конкретных объектов, они также могут описывать и абстрактные вещи. Объект – это конкретный представитель класса. Его определяет три характеристики: уникальность, поведение и состояние. Уникальность – это характеристика, определяющая отличие одного объекта от другого. Поведение определяет то, чем объект может быть полезен, что он может делать. Поведение объекта классифицирует его. Объекты разных классов отличаются своим поведением. Состояние описывает внутреннюю работу объекта то, что обеспечивает его поведение. Хорошо спроектированный объект оставляет своё состояние недоступным. Нас не интересует, как он это делает, нам важно то, что он умеет это делать. Сравним структуры и классы. В C# структуры могут иметь методы, но желательно избегать этого. Однако в некоторых структурах необходимы операторы. Операторы – стилизованные методы, но они не добавляют поведение, а обеспечивают более краткий синтаксис. Структурные типы – нижний уровень программы, это элементы, из которых строятся более сложные элементы. Переменные структурных типов свободно копируются и используются как поля и атрибуты объектов. Ссылочные типы – верхний уровень программы, они состоят из мелких элементов. Ссылочные типы в основном не могут быть скопированы. 49 Абстракция – это тактика очистки объекта от всех несущественных деталей, оставляя только существенную минимальную форму. Абстракция – важный принцип программирования. Хорошо спроектированный класс содержит минимальный набор методов, полностью описывающий его поведение. Инкапсуляция данных Традиционное процедурное программирование содержит много данных и много процедур. Любая функция имеет доступ к любым данным. Когда программы становятся большими, это создаёт много проблем – при небольших изменениях в коде необходимо следить за всей программой. Другая проблема – это хранение данных отдельно от функций. В ООП эта проблема решается за счет объединения данных и методов, которые работают с этими данными как одно целое. Данные и функции объединены в одну сущность, эта сущность ограничивает капсулу. Получаем две области: снаружи этой капсулы и внутри неё. Элементы, которые доступны извне, называются public, те, которые доступны только внутри класса – private. С# не ограничивает области видимости, любой элемент может быть как public, так и private. Две причины использования инкапсуляции – это контроль использования и минимизация воздействий при изменениях. Можно использовать инкапсуляцию данных и определить поведение для того, чтобы с объектом работали по заданным правилам. Вторая причина вытекает из первой, если данные закрыты от внешнего использования, то их изменение не влияет на использование объекта извне. Большинство данных внутри объектов описывают информацию об индивидуальности объекта. Данные внутри объекта обычно private и доступны только из методов класса. Иногда необязательно хранить информацию внутри каждого объекта. Т.е. может быть информация, одинаковая для всех объектов данного класса. Для этого используются статичные поля, которые принадлежат не конкретному объекту, а всему классу. 50 Статичные методы инкапсулируют статичные данные. Статичные методы существуют на уровне класса, в них нельзя использовать оператор this, но можно обращаться к полям класса, если получить объект класса как параметр. class Time { public static void Reset(Time t) { t.hour = 0; // ОК t.minute = 0; // O К hour = 0; // ошибка при компиляции minute = 0; // ошибка при компиляции } private int hour, minute; } Теперь вернемся к программе Hello world, рассмотрим её со стороны объектно-ориентированного программирования. Ответим на два вопроса: как при выполнении вызывается класс и почему метод Main статичный? Если в файле два класса с методом Main, то точка входа определяется при компиляции. // TwoEntries.cs using System; class EntranceOne { public static void Main( ) { Console.Write("EntranceOne.Main( )"); } } class EntranceTwo { public static void Main( ) { Console.Write("EntranceTwo.Main( )"); } } // Конец файла 51 c:\> csc /main:EntranceOne TwoEntries.cs c:\> twoentries.exe EntranceOne.Main( ) c:\> csc /main:EntranceTwo TwoEntries.cs c:\> twoentries.exe EntranceTwo.Main( ) c:\> Если в файле нет классов с методом Main, то из него нельзя скомпилировать запускаемый файл, только библиотеку dll. // NoEntrance.cs using System; class NoEntrance { public static void NotMain( ) { Console.Write("NoEntrance.NotMain( )"); } } // Конец файла c:\> csc /target:library NoEntrance.cs c:\> dir NoEntrance.dll Почему метод Main должен быть статичным? Так как для вызова нестатичного метода необходимо создать объект, а при вызове Main программа только начинает работу, и никаких объектов ещё нет. Для определения простых классов необходимо выполнить следующую последовательность действий: обозначить ключевым словом class начало класса, определить поля как в структурах, определить методы внутри класса, установить модификаторы доступа для всех полей и методов класса. Модификатор public означает, что “доступ неограничен”, private – “доступ ограничен типом, которому принадлежит”. Если пропустить модификатор, то по умолчанию поле или метод будут private. 52 class BankAccount { public void Deposit(decimal amount) { balance += amount; } private decimal balance; } При объявлении объекта класса, объект не создаётся, необходимо использовать оператор new, при этом все поля инициализируются нулями. При использовании объекта без создания будет ошибка при компиляции. Time now = new Time( ); // пример объявления объекта класса Ключевое слово this неявно указывает на объект вызвавший метод. Например, в следующем примере полю name нельзя присвоить значение, компилятор будет считать его параметром class BankAccount { public void SetName(string name) { name = name; } private string name; } Здесь необходимо было использовать this.name = name; В С# можно выделить пять различных видов типов: class struct interface enum delegate Любой из них можно использовать в классе. Т.е. в классе могут содержаться другие классы. Вложенные классы должны помечаться модификатором доступа, использование вложенных классов переводит имена из глобальной области видимости и пространства имен. Public вложенные классы не имеют ограничений по использованию, полное имя класса можно использовать в любом месте программы. Private вложенный класс виден только из класса, содержащего его. Класс без модификатора по умолчанию является private. 53 Наследование и полиморфизм Наследование – это связи на уровне классов. Новый класс может наследовать существующий класс. Наследование – это мощная связь, так как наследуемый класс наследует все (не private) элементы базового класса. От базового класса может наследоваться любое количество классов. Изменение базового класса, автоматически изменяет все классы-потомки. Классы потомки могут быть одновременно и базовыми классами для других классов. Группа классов, связанных наследованием, формирует структуру, называемую иерархией классов. При движении по иерархии вверх переходим к более общим классам. При движении вниз – к более специализированным классам. Простое наследование – это случай, когда у класса есть только один прямой базовый класс. Множественное наследование, когда у класса есть несколько прямых базовых классов. Множественное наследование создаёт предпосылки к ошибочному использованию наследования. Поэтому С#, как и большинство современных языков программирования, запрещает множественное наследование. Напомним, что наследование, особенно множественное, позволяет рассматривать один объект с разных точек зрения. Полиморфизм с литературной точки зрения означает много форм или много обликов. Это концепция, по которой один и тот же метод, определенный в базовом классе, может быть по-разному определен в разных классах потомках. Появляется новая проблема, как работать методу у объекта базового класса? Есть возможность не определять метод в базовом классе, т.е. оставить тело метода пустым. Такие методы называют операциями. В типичной иерархии классов операции объявляются в базовом классе, а определяются различными путями в различных классах потомках. Базовый класс представляет имя метода в иерархии. В случае, когда метод не определен в базовом классе, то нельзя создавать объекты этого класса, так для этого объекта будет не определен этот метод. Такие классы называются абстрактными. Абстрактные классы и интерфейсы похожи, так как не могут иметь объектов. Отличие между ними в том, что абстрактный класс может содержать 54 определения методов, интерфейсы содержат только операции (имена методов). То есть интерфейсы абстрактнее абстрактных классов. Когда вызываем метод напрямую из объекта, а не из операции базового класса, то метод связывается с вызовом при компиляции – раннее связывание. При вызове метода не напрямую через объект, а через операцию базового типа, он вызывается при выполнении программы – позднее связывание. Гибкость позднего связывания обеспечивается физической и логической ценой. Позднее связывание выполняется дольше, чем раннее. При позднем связывании классы- потомки могут заменять базовые классы. Операции могут быть вызваны из интерфейса, а классы-потомки обеспечат правильное выполнение. Вопросы к разделу 1. Объясните концепцию абстракции, и почему она важна для программной инженерии? 2. Назовите два принципа инкапсуляции. 3. Опишите наследование в контексте ООП. 4. Что такое полиморфизм? Как он связан с ранним и поздним связыванием? 5. Опишите разницу между интерфейсами, абстрактными классами и конкретными классами. Лабораторная работа Задания на классы. Время, необходимое на выполнение задания 45 мин. Упражнение 7.1 Создать класс счет в банке с закрытыми полями: номер счета, баланс, тип банковского счета (использовать перечислимый тип из упр. 3.1). Предусмотреть методы для доступа к данным – заполнения и чтения. Создать объект класса, заполнить его поля и вывести информацию об объекте класса на печать. Упражнение 7.2 Изменить класс счет в банке из упражнения 7.1 таким образом, чтобы номер счета генерировался сам и был уникальным. Для этого надо создать в классе статическую переменную и метод, который увеличивает значение этого переменной. 55 Упражнение 7.3 Добавить в класс счет в банке два метода: снять со счета и положить на счет. Метод снять со счета проверяет, возможно ли снять запрашиваемую сумму, и в случае положительного результата изменяет баланс. Домашнее задание 7.1 Реализовать класс для описания здания (уникальный номер здания, высота, этажность, количество квартир, подъездов). Поля сделать закрытыми, предусмотреть методы для заполнения полей и получения значений полей для печати. Добавить методы вычисления высоты этажа, количества квартир в подъезде, количества квартир на этаже и т.д. Предусмотреть возможность, чтобы уникальный номер здания генерировался программно. Для этого в классе предусмотреть статическое поле, которое бы хранило последний использованный номер здания, и предусмотреть метод, который увеличивал бы значение этого поля. 56 ГЛАВА 8. ИСПОЛЬЗОВАНИЕ ССЫЛОЧНЫХ ТИПОВ ДАННЫХ С# поддерживает типы данных, такие как int, long и bool, эти типы данных называются значимыми (value type). Переменные такого типа непосредственно содержат значение в самой переменной. Кроме этого, есть также ссылочные типы данных (reference type). Переменные такого типа содержат ссылку на некоторую область памяти с данными. К ссылочным типам данных относятся встроенные в платформу .NET такие типы, как: Array, string, классы. Для того чтобы объявить переменную ссылочного типа данных, используется следующий код: coordinate c1; Данная строка только объявляет переменную с1, которая может содержать ссылку на объект типа coordinate. Строка c1=new coordinate(); создает новый объект и возвращает ссылку на него, которую сохраняет в переменной c1. После создания объекта c1 можно получить доступ к полям этого объекта, используя оператор .(точку), например: c1.x=10; c1.y=5; Чтобы освободить ссылочную переменную, ей явно надо присвоить значение null. При попытке доступа к полю не проинициализированной ссылочной переменной может возникнуть ошибка на этапе компиляции или будет сгенерировано исключение во время выполнения программы. Например, если сначала проинициализировали переменную, потом присвоили ей null, а потом попытались получить доступ к полю этого объекта, то в этом случае возникнет исключительная ситуация типа NullReferenceException во время выполнения программы. Операторы проверки равенства (==) и (!=) для ссылочных переменных будут проверять, ссылаются ли две переменные на один и тот же объект – на одну и ту же область памяти. Для типа данных string оператор (==) можно использовать для проверки равенства значений (строк) в двух строковых переменных. Для ссылочных переменных нельзя использовать операторы отношения (>,<.>=,>=). 57 Две ссылочных переменных могут ссылаться на одну и ту же область памяти. Соответственно изменение значения объекта через одну ссылку изменит значение объекта через другую ссылку. Ссылочные переменные можно передавать в качестве параметров в методы. В случае передачи параметра по значению будет создана копия ссылки на тот же объект внутри метода. При передаче по ссылке (ключевое слово ref) используется одна ссылка внутри и снаружи метода. При передаче out параметра используется одна ссылка снаружи и внутри метода, но в отличие от передачи по ссылке в данном случае ссылочная переменная должна быть инициализирована внутри метода. При передаче ссылочной переменной в качестве параметра в метод любым из трех способов, изменение значения объекта внутри метода приведет к изменению значения объекта вне метода. Рассмотрим несколько определенных в .NET ссылочных типов данных. 1. Класс Exception. Объекты этого класса и только они используются в операторе генерирования исключительной ситуации throwи операторе обработки исключительной ситуации catch. Кроме того, в C# есть множество классов, наследников от класса Exception, для разного рода исключений. 2. Класс String – последовательность неизменяемых символов в кодировке Unicode . Все методы, которые работают со строкой и, казалось бы, изменяют ее, на самом деле создают новый объект класса String и возвращают его в качестве результата. Некоторые методы, определенные в классе: string str=”alphabet”; char c=str [5]; //возвращает шестой символ строки; str[3]=’z ’;//приведет к ошибке, т.к. свойство взятие индекса только для чтения; int n=str.Length ; //возвращает длину строки; string str1=str.Insert(2,”ABC ”); //добавляет во вторую позицию строку ABC и результат возвращает в str1. Значение строки str1 будет равно “aABCalphabet ”, значение строки str останется без изменения (str=”alphabet”); string str2=String.Copy(str ); //создается новая строка str2 и в нее копируется значение строки str; 58 string s=String.Concat(“a”,”b”,”c ”); // операция конкатенации эквивалента операции: string s=”a”+”b”+”c”; string s=str.Trim (); //создает новый объект String на основе строки str, из которой удалены специальные символы и пробелы; string s=str.ToUpper (); // переводит строку к верхнему регистру string s=str.ToLower (); //переводит строку к нижнему регистру Для сравнения значения двух строковых переменных между собой можно использовать оператор == и !=. Также можно использовать метод Equals s1.Equals(s 2); //вернет true, если строки совпадают; String.Equals(s1,s 2); //вернет true, если строки совпадают. Метод Compare сравнивает две строки в соответствии с их лексикографическим порядком. Метод возвращает отрицательное значение, если первая строка меньше второй; число ноль – если две строки совпадают; положительное число – если первая строка больше второй. String.Compare(s1,s2); Метод Compare можно также использовать с тремя параметрами. Третий параметр типа bool, если он равен true, то метод не чувствителен к регистру. 3. Класс StringBuilder – класс для хранения строк. В отличии от класса String , любой метод этого класса изменяет саму строку, не создавая новый объект класса. Все классы в С# явным или неявным образом наследуются от класса System.Object (часто вместо полного имени класса используется псевдоним object). У класса Object есть общие методы, которые наследуются всеми классами: • ToString – возвращает строковое представление объекта. Реализация по умолчанию вернет имя типа класса. У каждого класса наследника этот метод можно переопределить. • Equals – определяет, указывают ли ссылки на один и тот же объект. Метод можно перегружать, например, для проверки на равенство значений полей объектов. • Finalize – метод вызывается системой во время выполнения, когда объект становится недоступным. 59 • GetType – позволяет во время выполнения получить информацию о типе. Reflection (рефлексия) Процесс получения информации о типе во время выполнения называется рефлексией. В пространстве имен System.Reflection содержатся классы и интерфейсы, которые позволяют получить информацию о типах, методах и полях. Класс System.Type содержит методы для получения информации об объявлении типа: о конструкторах, полях, методах, событиях и свойствах класса. Для получения информации о типе можно использовать оператор typeof: using System; using System.Reflection; Type t = typeof(byte); Console.WriteLine("Type: {0}", t); Чтобы получить более подробную информацию о классе можно использовать метод GetMethods у объекта класса Type: using System; using System.Reflection; Type t = typeof(string); // Get type information MethodInfo[ ] mi = t.GetMethods( ); foreach (MethodInfo m in mi) { Console.WriteLine("Method: {0}", m); } Оператор typeof можно применять только тогда, когда объект существует на этапе компиляции. Если необходимо информацию о типе получить во время выполнения программы, следует использовать метод GetType класса Object. Пространства имен В платформе .NET Framework реализована большая коллекция классов, которые предоставляют интерфейс к общеязыковой среде исполнения, к операционной системе и сети. Все классы сгруппированы в пространства имен. 60 Пространство имен System.IO содержит классы для работы с операциями ввода/вывода, для работы с файловой системой. Классы File и Directory предоставляют методы для создания, удаления и управления директориями и файлами в файловой системе. Классы StreamReader и StreamWriter позволяют программе получать доступ к содержимому файлов как к потоку битов или символов. Класс FileStream содержит операции чтения и записи в файл, открытия и закрытия файлов в файловой системе, а также методы для изменения других дескрипторов операционной системы для обработки файлов, позволяет задать синхронное или асинхронное выполнение операций чтения и записи. Классы BinaryReader и BinaryWriter позволяет читать и записывать простые типы данных как двоичные значения в заданной кодировке. Пространство имен System.Data содержит классы для работы с данными из различных источников. Класс DataSet предназначен для работы с данными из разных источников. Класс DataSet состоит из таблиц (класс DataTable). Пространство имен System.Data.SqlClient содержит классы, которые предоставляют прямой доступ к базе данных SQL Server. Для доступа к другим реляционным БД используются классы из System.Data.OleDb. Пространство имен System содержит классы для работы со значимыми типами данных и ссылочными типами данных, событиями, делегатами, интерфейсами, атрибутами. Предоставляет методы для преобразования типов данных, управления программой. Пространство имен System.Net представляет простой программный интерфейс для работы с сетью с помощью различных протоколов. Пространство имен System.Windows.Forms – классы для создания графического интерфейса в Windows приложениях. Приведение типов данных Для значимых типов данных различают явное и неявное преобразование. Неявное преобразование происходит тогда, когда переменную одного типа данных пытаются присвоить в переменную другого типа. В С# неявное преобразование разрешается обычно тогда, когда значение может быть преобразовано к другому типу без потери значимости. Например, от int к long: int a=4; long b; 61 b=a; При явном преобразовании типов данных используется оператор cast. Если в процессе преобразования возникли проблемы, генерируется исключительная ситуация: try{ a=checked((int)b); } catch (Exception e){ Console.WriteLint(“Problem in cast”); } Все преобразования между различными базовыми типами данных осуществляются классом System.Convert. Можно преобразовать объект класса потомка к объекту родительского класса и наоборот. Преобразование к родительскому классу возможно всегда: Animal a; Bird b=new Bird(); //класс Animal – класс предок для класса Bird a=b; Можно использовать оператор приведения типов: a=(Animal) b; Преобразование к классу потомка необходимо осуществлять явно: Bird b=(Bird) a; В данном случае в процессе работы программы будет выполнена проверка, действительно ли объект а типа Bird. Если это не так, будет сгенерирована исключительная ситуация типа InvalidCastException. Код, осуществляющий явное приведение типов данных, лучше размещать в блоке try-catch. В С# есть возможность проверить, можно ли преобразовать объект одного типа данных к другому типу данных. Для этого используется оператор is, который возвращает true или false в зависимости от того, возможно ли приведение типов: if (a is Bird) b=(Bird)a; // Приведение типов возможно else Console.WriteLine(“a не является птицей”); Оператор as преобразует объект одного типа данных к другому, если преобразование возможно, и возвращает null, если преобразование не возможно: 62 Bird b=a as Bird; if (b==null) Console.WriteLine(“a не является птицей”); В .NET все ссылочные типы данных наследуют от класса object, поэтому объект любого ссылочного типа данных можно привести к типу данных object. Преобразования типов можно выполнять, работая с интерфейсами: IHashCodeProvider hcp; hcp = (IHashCodeProvider) x; При помощи оператора is можно узнать определяет ли класс интерфейс if (x is IHashCodeProvider) ... Также можно использовать оператор as, вместо оператора преобразования IHashCodeProvider hcp; hcp = x as IHashCodeProvider; C# может переводить структурные типы в ссылочные и наоборот (boxing и unboxing). static void Show(object o) { Console.WriteLine(o.ToString( )); } Show(42); В случае явного преобразования типов: object o = (object) 42; // Box Console.WriteLine(o.ToString( )); Вопросы к разделу 1. Как распределяется память для переменных ссылочного типа? 2. Какое значение присваивают ссылочной переменной, чтобы показать, что она не указывает на объект? Что произойдет, если обратиться к ней как к объекту? 3. Какой класс является базовым для всех классов С#? 4. Объясните разницу между операцией преобразования типа (cast) и оператором as. 63 Лабораторная работа Задания на использование переменных ссылочного типа, передачу их в качестве параметров методам, преобразование типов данных. Время, необходимое на выполнение задания 75 мин. Упражнение 8.1 В класс банковский счет, созданный в упражнениях 7.1- 7.3 добавить метод, который переводит деньги с одного счета на другой. У метода два параметра: ссылка на объект класса банковский счет откуда снимаются деньги, второй параметр – сумма. Упражнение 8.2 Реализовать метод, который в качестве входного параметра принимает строку string, возвращает строку типа string, буквы в которой идут в обратном порядке. Протестировать метод. Упражнение 8.3 Написать программу, которая спрашивает у пользователя имя файла. Если такого файла не существует, то программа выдает пользователю сообщение и заканчивает работу, иначе в выходной файл записывается содержимое исходного файла, но заглавными буквами. Упражнение 8.4 Реализовать метод, который проверяет реализует ли входной параметр метода интерфейс System.IFormattable. Использовать оператор is и as. (Интерфейс IFormattable обеспечивает функциональные возможности форматирования значения объекта в строковое представление.) Домашнее задание 8.1 Работа со строками. Дан текстовый файл, содержащий ФИО и e-mail адрес. Разделителем между ФИО и адресом электронной почты является символ #: Иванов Иван Иванович # iviviv@mail.ru Петров Петр Петрович # petr@mail.ru Сформировать новый файл, содержащий список адресов электронной почты. Предусмотреть метод, выделяющий из строки адрес почты. Методу в качестве параметра передается символьная строка s, e-mail возвращается в той же строке s: public void SearchMail (ref string s). Домашнее задание 8.2 Список песен. В методе Main создать список из четырех песен. В цикле вывести информацию о каждой песне. Сравнить между собой первую и вторую песню в списке. Песня представляет собой класс с методами для заполнения каждого из полей, методом вывода данных о песне на печать, методом, который сравнивает между собой два объекта: class Song{ string name; // название песни 64 string author ; //автор песни Song prev ; //связь с предыдущей песней в списке //метод для заполнения поля name //метод для заполнения поля author //метод для заполнения поля prev //метод для печати названия песни и ее исполнителя public string Title (){… /*возвращ название+исполнитель*/ …} //метод, который сравнивает между собой два объекта-песни: public bool override Equals(object d){…} } |