Лабораторна робота 3
Узагальнене програмування
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Розширити програму, яка була створена у попередніх лабораторних роботах, шляхом перетворення класу-сутності на узагальнений, що дозволить використовувати для додаткових даних як рядок, так і окремо створену структуру або клас. Додати до функціональності класу-групи функції сортування за такими ознаками:
Клас-сутність | Перша ознака сортування | Друга ознака сортування |
---|---|---|
Студент | За алфавітом прізвища | За роком народження |
Факультет | За алфавітом назви | За кількістю студентів |
Спеціальність | За алфавітом назви в зворотному порядку | За кількістю студентів |
Предмет | За алфавітом назви | За кількістю незадовільних оцінок |
Район | За алфавітом назви в зворотному порядку | За кількістю мешканців |
Місто | За алфавітом назви | За кількістю мешканців |
Учасник | За алфавітом прізвища | За віком |
Гравець | За алфавітом імені | За стажем |
Учасник | За алфавітом прізвища | За стажем |
Альбом | За алфавітом назви | За роком створення |
Пісня | За алфавітом назви в зворотному порядку | За рейтингом |
Кімната | За алфавітом назви | За площею |
Оповідання | За алфавітом назви | За роком створення |
Твір | За алфавітом назви | За роком створення |
Роман | За алфавітом назви в зворотному порядку | За роком створення |
Реалізувати функції збереження даних у XML-форматі та завантаження даних з XML-документу. Для роботи з XML-документами використовувати серіалізацію та десеріалізацію. Для зберігання сутностей у групі замість масиву використати список. Відтворити завдання попередніх лабораторних робіт.
Усі класи, крім класу який містить функцію Main()
, розташувати в окремій бібліотеці. В консольному застосунку підключити створену бібліотеку та здійснити тестування всіх функцій. Реалізувати тестування трьох варіантів:
- з представленням додаткових даних у вигляді рядку
- з застосуванням спеціальної структури
- з застосуванням окремо створеного класу.
Передбачити перехоплення можливих винятків.
1.2 Робота з текстовими файлами
Розробити програму, яка здійснює копіювання з одного файлу в інший рядків, довжина яких не перевищує введеного значення.
1.3 Реалізація серіалізації та десеріалізації
Описати класи Студент та Академічна група (з масивом студентів у якості поля). Створити об'єкти, здійснити їх серіалізацію в XML-документ та десеріалізацію.
1.4 Створення бібліотеки узагальнених функцій для роботи з масивами та списками
Реалізувати статичний клас та реалізувати статичні узагальнені методи, які реалізують таку функціональність:
- обмін місцями двох груп елементів
- обмін місцями усіх пар сусідніх елементів (з парним і непарним індексом)
- вставлення у масив (список) іншого масиву (списку) елементів у вказане місце
- заміна групи елементів іншим масивом (списком) елементів
Реалізувати наведені функції для масивів і для списків.
1.5 Створення власного контейнеру
Створити узагальнений клас для представлення одновимірного масиву, індекс елементів якого змінюється від визначеного значення From
до значення To
включно. Ці значення можуть бути як додатними, так і від'ємними. Клас повинен мати такі елементи:
- закрите поле – "звичайний" масив (список);
- індексатор;
- властивості (для читання)
From
таTo
(доцільно зберігати тількиFrom
, аTo
– обчислювати); - конструктор з параметрами
From
таTo
для створення порожнього масиву; - конструктор з параметром
From
та параметром типу масиву (з атрибутомparams
); - перевантажений оператор перетворення у рядок (
operator string
); - метод надання ітератору, який забезпечить можливість обходу елементів за допомогою
foreach
. - метод додавання нового елементу;
- метод видалення останнього елементу.
У функції Main()
слід здійснити тестування усіх створених елементів класу.
Необхідно реалізувати два варіанти – на базі масиву та на базі списку.
1.6 Робота з множиною
Увести кількість елементів майбутньої множини цілих чисел та діапазон чисел. Сформувати цю множину з випадкових значень. Вивести елементи множини, відсортовані за збільшенням.
1.7 Робота з асоціативним масивом
Увести речення та вивести усі різні літери, які входять у речення та їх кількість входження.
1.8 Створення "гнучкого" масиву (додаткове завдання)
Створити узагальнений клас для представлення одновимірного масиву, який автоматично розширюється, коли користувач звертається до неіснуючого елементу. Наприклад, якщо створено порожній масив a
, перше звернення (для читання або запису) до елементу a[n]
забезпечить розширення масиву так, щоб він містив n + 1
елемент з індексами від 0 до n
-го включно. Якщо певні елементи вже існували, вони зберігаються та масив доповнюється новими елементами. Якщо елемент з індексом вже існує, здійснюється звичайне звернення.
Створити конструктор без параметрів, індексатор, властивість, яка повертає індекс останнього елемента, метод надання ітератору, а також перекрити функцію ToString()
. Здійснити тестування усіх можливостей класу.
2 Методичні вказівки
2.1 Обробка виняткових ситуацій
Використання механізму обробки виняткових ситуацій є дуже важливою складовою частиною практики програмування на всіх сучасних об'єктно-орієнтованих мовах. Об'єкти-винятки дозволяють програмісту відокремити точки виникнення помилок часу виконання від коду, де ці помилки повинні оброблятися. Виняткова ситуація являє собою подію, що виникає під час виконання програми та порушує нормальне виконання інструкцій коду.
Для генерації виняткової ситуації використовується оператор throw
. Після ключового слова throw
повинен бути розташований об'єкт класу System.Exceptіon
чи класів, похідних від нього. Такі похідні класи відбивають специфіку конкретної програми.
class SpecificException : Exception { }
Клас System.Exception
містить ряд властивостей, за допомогою яких можна одержати доступ до інформації про виняткову ситуацію, зокрема:
Message
– текстовий опис помилки, що задається як параметр конструктора під час створення об'єкта-винятку;Source
– ім'я об'єкта чи застосунку, що згенерувало помилку;-
StackTrace
– послідовність викликів, що привели до виникнення помилки.
У більшості випадків об'єкт-виняток створюється в місці генерації виняткової ситуації за допомогою оператора new
, однак іноді об'єкт-виняток створюється заздалегідь. Типове твердження throw
може виглядати так:
void F() { . . . if (/* помилка */) throw new SpecificException(); . . . }
У заголовку функції не специфікуються типи винятків, які генеруються цією функцією. У наведеному нижче прикладі функція Reciprocal()
генерує виняткову ситуацію у випадку ділення на нуль.
class DivisionByZero : Exception { } class Test { public double Reciprocal(double x) { if (x == 0) { throw new DivisionByZero(); } return 1 / x; } }
На відміну від C++, C# не допускає створення винятків примітивних типів. Дозволені тільки об'єкти класів, похідних від Exception
.
У блоці try
розміщують код, що може генерувати виняткову ситуацію:
double x, y; . . . try { y = Reciprocal(x); }
Після блоку try
повинен випливати один чи кілька оброблювачів (блоків catch
). Кожен такий оброблювач відповідає визначеному типу винятку. Блок catch
без дужок обробляє всі інші виняткові ситуації:
catch (DivisionByZero d) { // обробка виняткової ситуації } catch (SpecificException) { // обробка виняткової ситуації } catch { // обробка виняткової ситуації }
Як видно з приклада, у заголовку блоку catch
можна опускати ідентифікатор об'єкта-винятку, якщо важливий
тільки тип.
Класи винятків утворять ієрархію. При зіставленні типів винятків, оброблювач базового типу сприймає також винятки всіх створених від нього типів. Звідси випливає, що оброблювачі похідних типів варто розміщувати до оброблювачів базових типів.
У деяких випадках оброблювач виняткових ситуацій не може цілком обробити виняток і повинен передати його зовнішньому оброблювачу. Це можна зробити за допомогою оператора throw
:
catch (SomeEx ex) { // локальна обробка виняткової ситуації throw (ex); // повторна генерація }
Якщо в заголовку оброблювача не визначений ідентифікатор, то можна використовувати throw
без виразу:
catch (Exception) { // локальна обробка виняткової ситуації throw; }
Після останнього блоку catch
можна розмістити блок finally
. Цей код завжди виконується незалежно від того, виникла виняткова ситуація чи ні.
try { OpenFile(); // інші дії } catch (FileError f) { // обробка виняткової ситуації } catch (Exception ex) { // обробка виняткової ситуації } finally { CloseFile(); }
В .NET визначені стандартні особливі ситуації – класи, що теж є нащадками Exceptіon
. Один із найчастіше виникаючих стандартних винятків – System.NullReferenceException
, який генерується при спробі звертатися до елементів класу через посилання, яке дорівнює null
. Виняток System.IndexOutOfRangeException
генерується, коли відбувається вихід за межі масиву.
Внутрішні виняткові ситуації .NET сигналізують про серйозну проблему під час виконання програми і можуть виникнути при виконанні будь-якого оператора. До них відносяться ExecutionEngineException
(внутрішня помилка CLR), StackOverflowException
(переповнення стеку), OutOfMemoryException
(брак оперативної пам'яті). Зазвичай такі винятки не перехоплюються.
Починаючи з версії 6 мови C#, можна додавати так звані фільтри винятків (exception filters) в конструкції catch
. Вирази-фільтри після ключового слова when
визначають, в якому випадку слід виконувати блок catch
. Якщо цей вираз істиний, блок catch
виконується. В іншому випадку catch
пропускається. Наприклад:
static void SomeFunc(int k) { if (k == 1) { throw new Exception("First case"); } if (k == 2) { throw new Exception("Second case"); } throw new Exception("Other case"); } static void Main(string[] args) { int n = int.Parse(Console.ReadLine()); try { SomeFunc(n); } catch (Exception ex) when (ex.Message.Contains("First")) // фільтр винятку { Console.WriteLine("Our case!"); } catch { Console.WriteLine("Something else"); } }
Якщо цей вираз вставити в код блоку catch
, це означає, що виняток обробляється і повторно генерується. Використання фільтрів винятків передбачає, що вираз взагалі не обробляється. Фільтри винятків також можна використовувати для зневадження.
2.2 Початкові відомості про роботу з файлами
2.2.1 Робота з текстовими файлами
Як практично всі універсальні мови програмування C# надає засоби роботи з файлами та іншими потоками. Ці засоби описані у просторі імен System.IO
. Класи цього простору імен пропонують низку методів для створення таких потоків, читання, запису, тощо. Потоки, призначені для роботи з текстовою інформацією, мають назву потоків символів. Базовими класами для роботи з потоками символів є TextReader
та TextWriter
. Похідні класи StreamWriter
та StreamReader
,
а також похідні від них, забезпечують роботу з текстовими файлами.
Наведена нижче програма здійснює читання з текстового файлу рядків та запис їх в інший текстовий файл.
using System; using System.IO; namespace LabThird { class Program { static void Main(string[] args) { using (StreamReader reader = new StreamReader("From.txt", Encoding.Default)) { using (StreamWriter writer = new StreamWriter("To.txt")) { string s; while ((s = reader.ReadLine()) != null) { writer.WriteLine(s); } } } } } }
Завдяки вживанню оператора using
()
файли автоматично закриваються, оскільки викликаються методи Dispose()
, які в свою чергу викликають методи Close()
. Файл From.txt повинен до початку роботи програми знаходитися у теці bin\Debug або bin\Release проекту (залежно від способу завантаження програми на виконання).
Існує функція ReadToEnd()
, яка дозволяє прочитати весь файл до кінця та занести весь вміст в один рядок. Використання цієї функції дозволить скоротити попередню програму:
using System; using System.IO; namespace LabThird { class Program { static void Main(string[] args) { using (StreamReader reader = new StreamReader("From.txt", Encoding.Default)) { using (StreamWriter writer = new StreamWriter("To.txt")) { string s = reader.ReadToEnd(); writer.Write(s); } } } } }
Для роботи з двійковими потоками використовують класи BinaryReader
і BinaryWriter
. Також існують так звані потоки в пам'яті – StringReader
і StringWriter
, які дозволяють використовувати рядки як потоки введення та виведення.
2.2.2 Робота з бінарними файлами
Засоби .NET включають класи для зручної роботи з бінарними файлами. Клас BinaryWriter
надає функції для
запису в файл даних різних вбудованих типів-значень, а також масивів байтів і масивів символів. Клас BinaryReader
надає
методи для читання даних усіх цих типів з бінарного файлу. Для того, щоб створити відповідні потоки, спочатку треба створити
файлові потоки (FileStream
). Якщо fileName
– це рядок, який містить ім'я файлу, потоки створюють так:
FileStream outputStream = new(fileName, FileMode.Create); FileStream inputStream = new(fileName, FileMode.Open);
Далі ці потоки використовують для створення об'єктів BinaryWriter
і BinaryReader
:
BinaryWriter writer = new BinaryWriter(outputStream); BinaryReader reader = new BinaryReader(inputStream);
Існує низка методів Write()
класу BinaryWriter
, призначених для запису даних різних типів.
Об'єкт класу BinaryReader
може прочитати ці дані за допомогою методів ReadInt16()
, ReadInt32()
,
ReadInt64()
, ReadDouble()
, ReadDecimal()
тощо. Найкращий
варіант запису рядків – перетворити
рядок у масив символів. Перед записом масиву в файлі доцільно записати його довжину.
Так виглядатиме запис рядка s
:
writer.Write(s.Length); writer.Write(s.ToCharArray());
Під час читання з файлу слід спочатку прочитати довжину, а потім символи з масиву, які було збережено:
int len = reader.ReadInt32(); s = new String(reader.ReadChars(len));
Спеціальні типи можна зберігати, наприклад у вигляді рядка. Якщо треба зберігти дату та час (об'єкт класу DateTime
),
найкращий варіант – отримати
подання у вигляді довгого цілого числа функцією ToBinary()
. Потім дату і час можна відтворити у відповідному
об'єкті, прочитавши з файлу число типу long
і скориставшись статичною функцією DateTime.FromBinary()
.
В наведеному нижче прикладі здійснюється запис даних
різних типів, які містяться в об'єкті типу Employee
:
namespace BinaryFilesDemo { public class Employee { public int Id { get; set; } public string Name { get; set; } = ""; public string Surname { get; set; } = ""; public DateTime DateOfBirth { get; set; } public decimal Salary { get; set; } public override string ToString() { string s = "Id:\t\t" + Id + "\nName:\t\t" + Name + "\nSurname:\t" + Surname + "\nDate of Birth\t" + DateOfBirth.ToShortDateString() + "\nSalary\t\t" + Salary; return s; } public void WriteToFile(string fileName) { using (FileStream fs = new(fileName, FileMode.Create)) { using (BinaryWriter writer = new(fs)) { writer.Write(Id); writer.Write(Name.Length); writer.Write(Name.ToCharArray()); writer.Write(Surname.Length); writer.Write(Surname.ToCharArray()); writer.Write(DateOfBirth.ToBinary()); writer.Write(Salary); } } } public void ReadFromFile(string fileName) { using (FileStream fs = new(fileName, FileMode.Open)) { using (BinaryReader reader = new(fs)) { Id = reader.ReadInt32(); int len = reader.ReadInt32(); Name = new String(reader.ReadChars(len)); len = reader.ReadInt32(); Surname = new String(reader.ReadChars(len)); DateOfBirth = DateTime.FromBinary(reader.ReadInt64()); Salary = reader.ReadDecimal(); } } } } class Program { static void Main(string[] args) { Employee employee = new() { Id = 1, Name = "John", Surname = "Smith", DateOfBirth = DateTime.Parse("1989/12/31"), Salary = 1000 }; Console.WriteLine(employee); employee.WriteToFile("employee.bin"); employee = new(); Console.WriteLine(employee); employee.ReadFromFile("employee.bin"); Console.WriteLine(employee); } } }
У програмі спочатку створюється об'єкт типу Employee
. Потім ми виводимо дані створеного об'єкта на екран
і записуємо в бінарний файл. Далі створюємо новий порожній об'єкт типу Employee
і зчитуємо дані з бінарного
файлу. Об'єкт зі считаними даними виводимо на екран.
Слід пам'ятати, що робота з бінарними файлами не передбачає перегляду та редагування файлів поза спеціальною програмою, яка була створена для читання і запису даних.
2.3 Робота з XML-документами
Розширювана мова розмічування XML (eXtensible Markup Language) – це незалежний від платформи метод структурування інформації. Оскільки XML відокремлює зміст документу від його структури, його успішно використовують для обміну інформацією. Наприклад, XML можна використовувати для передачі даних між програмою та базами даних, або між базами даних, що мають різні формати.
Орієнтація на використання XML є однією з найбільш істотних властивостей платформи .NET. Зокрема, XML-документи застосовують для опису конфігурації складання, генерації та зберігання документації, серіалізації, внутрішнього представлення даних компонентами роботи з базами даних, опису елементів графічного інтерфейсу користувача, передачі даних у Web-сервісах тощо.
Файли формату XML – це завжди текстові файли. Синтаксис мови XML багато в чому схожий на синтаксис мови HTML, яка застосовується для розмічування текстів, що публікуються в Internet. Мова XML також може бути безпосередньо застосована для розмітки текстів.
Незалежно від мови програмування та програмної платформи, існує два стандартних підходи до роботи з XML-документами в програмах:
- Подіє-орієнтована модель документу (Simple API for XML, SAX), орієнтована на потік даних та обробку подій, зв'язаних з різними тегами;
- Об'єктна модель документу (Document Object Model, DOM), яка дозволяє створити в пам'яті колекцію вузлів, організованих в ієрархію.
Подіє-орієнтований підхід не дозволяє розроблювачу змінювати дані у вихідному документі. У разі потреби корегування частини даних, документ потрібно цілком оновити. На відміну від його DOM забезпечує API, що дозволяє розроблювачу додавати видаляти або вузли в будь-якій точці дерева в застосунку.
Обидва підходи використовують поняття парсера. Парсер (parser) – це програмний застосунок, призначений для того, щоб аналізувати документ шляхом розділення його на лексеми (tokens). Парсер може ініціювати події (як у SAX), або будувати в пам'яті дерево даних.
Об'єктна модель документу (Document Object Model, DOM) – це представлення XML-документу в пам'яті у вигляді ієрархічної структури. DOM дозволяє як читати і зберігати, так і змінювати XML-документ. DOM – це стандартний спосіб представлення XML-даних у пам'яті, хоча власне XML-дані зберігаються послідовно в файлі чи для передачі до іншого об'єкта.
Ієрархія класів, що підтримують DOM, включає в якості підкорня XmlNode
, що є базовим об'єктом дерева DOM. Клас XmlDocument
, що розширює XmlNode
, підтримує методи виконання операцій над документом у цілому, наприклад для завантаження його в пам'ять чи збереження XML у файлі. Крім того, XmlDocument
надає засоби для перегляду і керування вузлами у всьому XML-документі.
При зчитуванні XML-документа в пам'ять у вигляді дерева вузлів тип вузла визначається під час його створення. XML DOM має кілька типів вузлів:
- Document (клас
XmlDocument
) - DocumentFragment (клас
XmlDocumentFragment
) - DocumentType (клас
XmlDocumentType
) - EntityReference (клас
XmlEntityReference
) - Element (клас
XmlElement
) - Attr (клас
XmlAttribute
) - ProcessingInstruction (клас
XmlProcessingInstruction
) - Comment (клас
XmlComment
) - Text (клас
XmlText
) - CDATASection (клас
XmlCDataSection
) - Entity (клас
XmlEntity
) - Notation (клас
XmlNotation
)
Коли XML-документ знаходиться в пам'яті, він представлений у вигляді дерева. Для програмування є ієрархія об'єктів для доступу до вузлів дерева.
Клас XmlReader
також дозволяє читати XML, однак він надає односпрямований доступ тільки для читання. Це означає відсутність можливості редагування документа за допомогою XmlReader
.
2.4 Використання серіалізації
Серіалізація – це процес збереження (передачі) стану об'єкта в потоці (файлі), зокрема, для передачі через комп'ютерні мережі. Послідовність даних, що зберігається, містить всю інформацію, необхідну для реконструкції (або десеріалізації) стану об'єкта з метою подальшого використання. Цю інформацію можна зберігати в різних форматах. Найбільш стисла серіалізація здійснюється у двійковому форматі. Але найкращим підходом, який забезпечує необхідну наочність, є серіалізація в XML-файл. Автоматично зберігаються значення відкритих властивостей (полів).
Для збереження об'єктів у документі XML використовують клас XmlSerializer
. Щоб використовувати цей тип, а також файлові потоки, потрібно додати директиви:
using System.Xml.Serialization; using System.IO;
Припустимо, описано клас Student
:
public class Student { public string Name { get; set; } public string Surname { get; set; } public string[] Grades { get; set; } }
Створюємо об'єкт у програмному коді:
Student student = new Student() { Name = "Frodo", Surname = "Baggins", Grades = new string[] { "B", "D", "C" } };
Для того, щоб здійснити серіалізацію, необхідно створити об'єкт типу XmlSerializer
:
XmlSerializer serializer = new XmlSerializer(typeof(Student)); using (TextWriter textWriter = new StreamWriter("Frodo.xml")) { serializer.Serialize(textWriter, student); }
Результатом серіалізації буде XML-файл з таким вмістом:
<?xml version="1.0" encoding="utf-8"?> <Student xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Name>Frodo</Name> <Surname>Baggins</Surname> <Grades> <string>B</string> <string>D</string> <string>C</string> </Grades> </Student>
Як видно зі вмісту файлу, кожній властивості відповідає окремий тег, масиву (або списку) – складений тег.
Десеріалізація виконується таким чином:
XmlSerializer deserializer = new XmlSerializer(typeof(Student)); using (TextReader textReader = new StreamReader("Frodo.xml")) { student = (Student)deserializer.Deserialize(textReader); }
Іноді більш зручним є використання XML-атрибутів для властивостей замість окремих тегів. Досягти цього можна завдяки спеціальному атрибуту мови C# (використання атрибутів C# буде розглянуто пізніше):
[System.Xml.Serialization.XmlAttributeAttribute()]
Цей атрибут розміщують безпосередньо перед відповідними властивостями:
public class Student { [System.Xml.Serialization.XmlAttributeAttribute()] public string Name { get; set; } [System.Xml.Serialization.XmlAttributeAttribute()] public string Surname { get; set; } public string[] Grades { get; set; } }
Тепер XML-файл матиме такий вміст:
<?xml version="1.0" encoding="utf-8"?> <Student xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="Frodo" Surname="Baggins"> <Grades> <string>B</string> <string>D</string> <string>C</string> </Grades> </Student>
2.5 Створення та використання узагальнень
Узагальнене програмування (generic programming) це парадигма програмування, яка полягає у відокремленні даних, структур даних та алгоритмів обробки даних. Узагальнені структури даних дозволяють зберігати колекції даних різних типів, при чому механізми зберігання не залежать від типів даних об'єктів. Узагальнені алгоритми, в свою чергу, повинні бути побудовані так, щоб вони не залежали ні від типів даних, ні від конкретних типів структур, які ці дані зберігають. Узагальнене програмування фокусується на пошуку спільності серед подібних реалізацій контейнерних класів та алгоритмів та створенні типів та функцій більш високого рівня абстракції.
Мови, які не підтримують узагальнень, розв'язують цю проблему шляхом використання вказівників на будь-яку змінну (у C),
або посилань на спільний базовий клас (як у Delphi Pascal, а також у першій версії C#). Такий підхід не є зручним
та безпечним з точки зору типів. Наприклад, якщо у першій версії C# необхідно було створити клас для зберігання пари об'єктів
одного типу, треба було б створити два посилання на клас object
:
public class Pair { object First, Second; public Pair(object first, object second) { First = first; Second = second; } }
Оскільки клас object
є базовим для усіх типів, новий клас можна, наприклад, застосувати для зберігання пари рядків:
Pair p = new Pair("Прізвище", "Ім\'я");
Такий підхід має певні недоліки:
- Для читання об'єктів необхідно застосувати явне перетворення типів:
string s = (string) p.First; // Замість string s = p.First;
- Немає впевненості, що у парі зберігаються об'єкти саме того типу, який нас цікавить:
int i = (int) p.Second; // Помилка часу виконання
- Не можна гарантувати, що обидва поля будуть одного типу:
Pair p1 = new Pair("Прізвище", 2); // Жодного повідомлення про помилку
Крім того, зберігання типів-значень у такому підході пов'язана з автоматичним упакуванням, що не є досить ефективним. Можна, звичайно, створити декілька класів для зберігання пари даних, наприклад IntegerPair
, FloatPair
, StringPair
тощо. Такий підхід – громіздкий, та незручний. Він також ускладнює пошук та виправлення помилок.
Інша проблема – створення функцій для роботи з масивами різних типів. Багато типових алгоритмів (наприклад, обміняти два певних елемента місцями, змінити порядок елементів на протилежний) не залежать від типів елементів масиву. Як і у випадку створення класу, можна використовувати object як тип елементів. Наприклад:
public static void SwapElements(object[] arr, int i, int j) { object e = arr[i]; arr[i] = arr[j]; arr[j] = e; }
Знову ті ж самі проблеми: відсутність можливості контролю типів під час компіляції, "безглузде" перетворення типів, тощо. Створення окремих функцій для різних типів – громіздке, пов'язане з помилками, для всіх типів – взагалі неможливе.
Починаючи з версії 2, мова C# дозволяє створювати та використовувати узагальнення – синтаксичну конструкцію, що включає в себе параметри класу, структури або функції, які містять додаткову інформацію про типи елементів та інших даних. Ці параметри беруть у кутові дужки та розділяють комами. В найчастішому випадку список містить один параметр.
Узагальнення надають можливість створення та використання синтаксичних конструкцій, безпечних з точки зору типів. Типи,
опис яких містить такі параметри, мають назву узагальнених. Під час створення об'єкта узагальненого типу у кутових дужках
вказують імена реальних типів. C# дозволяє використовувати будь-які типи, оскільки всі вони походять від System.Object
.
Наведемо приклад використання узагальнень.
public class Pair<T> { public T First { get; set; } public T Second { get; set; } public Pair(T first, T second) { First = first; Second = second; } } class Program { static void Main(string[] args) { Pair<string> p = new Pair<string>("Прізвище", "Ім\'я"); string s = p.First; // Отримуємо рядок без приведення типів Pair<int> p1 = new Pair<int>(1, 2); // Можна використовувати цілі константи int i = p1.Second; // Отримуємо ціле значення без приведення типів } }
Узагальнені класи не можуть містити функцію Main()
.
Якщо ми намагаємось додати до пари дані різних типів, компілятор згенерує помилку. Помилковою є також спроба явно перетворити тип:
Pair<string> p = new Pair<string>("1", "2"); int i = (int) p.Second; // Помилка компіляції
Тип даних з параметром у кутових дужках (наприклад, Pair<string>
) має назву параметризованого типу.
Окрім узагальнених класів і структур, можна створювати узагальнені функції всередині як узагальнених, так і звичайних (неузагальнених) класів (структур):
public class ArrayPrinter { public static void PrintArray<T>(T[] a) { foreach (T x in a) { Console.Write("{0, 10}", x); } Console.WriteLine(); } static void Main(string[] args) { string[] sa = {"First", "Second", "Third"}; PrintArray(sa); int[] ia = {1, 2, 4, 8}; PrintArray(ia); } }
Рекомендованими іменами формальних параметрів є імена, які починаються з великої літери T
.
Узагальнення може мати два і більше параметрів. У наведеному нижче прикладі пара може містити об'єкти різних типів:
public class PairOfDifferentObjects<TFirst, TSecond> { public TFirst First { get; set; } public TSecond Second { get; set; } public PairOfDifferentObjects(TFirst first, TSecond second) { First = first; Second = second; } } class Program { static void Main(string[] args) { PairOfDifferentObjects<int, String> p = new PairOfDifferentObjects<int, String>(1000, "thousand"); PairOfDifferentObjects<int, int> p1 = new PairOfDifferentObjects<int, int>(1, 2); //... } }
Узагальнений тип вважають безпосередньо або опосереднено похідним від класу object
. Іноді такого припущення замало для створення корисних класів та функцій. Наприклад, ми не можемо викликати жодного методу, окрім методів, описаних у класі object
, навіть не можемо створити об'єкт за допомогою операції new
, оскільки не впевнені, що тип надає конструктор без параметрів. Для того, щоб розширити функціональність узагальнених класів та методів, для опису узагальнень використовують так звані обмеження. Після узагальнення розміщують ключове слово where
, а далі можна вказувати про наявність конструктора без параметрів, базовий клас, інтерфейси, які реалізує тип. Наприклад:
class A<T> where T : new() // тип Т повинен мати конструктор без параметрів { public T Data = new T(); } interface Int1 { void doSomething(); } class B<T> where T : Int1 // тип Т повинен реалізовувати вказаний інтерфейс { T data; public void f() { data.doSomething(); } } class C<T> where T : class // тип Т повинен бути типом-посиланням { } class D<T> where T : struct // тип Т повинен бути типом-значенням { }
Від узагальнених класів шляхом спадкування можна створювати як узагальнені, так і неузагальнені класи. Якщо створюється неузагальнений клас, необхідно вказати конкретний тип для параметра-узагальнення. Наприклад:
class X<T> { } class Y<T> : X<T> { } class Z : X<int> { }
Можна також визначати узагальнені перевантажені операції.
Також можна серіалізувати об'єкти узагальнених класів. У такому випадку для імен тегів (атрибутів) серіалізатор автоматично обирає Ім'яВластивостіOfІм'яТипу
. Наприклад, для властивості з ім'ям SomeProperty
класу SomeClass<T>
, якщо у створеному об'єкті тип T
спеціалізовано як string
, отримаємо таке ім'я тегу (атрибуту): SomePropertyOfString
.
2.6 Стандартні узагальнені класи та методи
2.6.1 Загальний огляд
Контейнерний клас – це клас, об'єкти якого надають можливість зберігання інших об'єктів або посилань. Найпростіший контейнер – це звичайний масив. Функціональність масивів не є достатньою для багатьох задач, тому сучасні мови і платформи програмування надають різноманітні можливості для створення контейнерів.
C# надає варіанти створення контейнерів як із застосуванням узагальнень (простір імен System.Collections.Generic
), так і без них (простір імен System.Collections
). Варіант без узагальнень вважається застарілим і небажаним. Далі розглядатимуться саме узагальнені контейнери.
Усі стандартні контейнери реалізують інтерфейс IEnumerable
. У наведеній нижче таблиці наведені деякі узагальнені інтерфейси та стандартні узагальнені класи, які реалізують ці інтерфейси:
Інтерфейс | Опис | Стандартні класи, які реалізують інтерфейс |
---|---|---|
ICollection<T> |
узагальнена колекція | Stack<T> (стек), Queue<T> (черга), LinkedList<T> (зв'язаний список), а також усі класи, перелічені нижче |
IList<T> |
список | List<T> (список, побудований з використанням внутрішнього масиву)
|
ISet<T> |
множина | HashSet<T> (множина, побудована на хеш-таблиці)SortedSet<T> (відсортована множина) |
IDictionary<K,V> |
словник (асоціативний масив) | Dictionary<K,V> (словник, побудований на хеш-таблиці)SortedDictionary<K,V> (словник, відсортований за ключами) |
Усі класи надають велику кількість статичних і нестатичних функцій для роботи з послідовностями елементів. Контейнери дозволяють зберігати як посилання, так і безпосередньо елементи типу int
, double
, тощо.
Клас Stack
дозволяє створювати структуру даних, організовану за принципом "останній зайшов – перший вийшов" ("last in first out", LIFO). Клас Queue
надає структуру даних, організовану за принципом "перший зайшов – перший вийшов" ("first in first out", FIFO).
2.6.2 Робота зі списками
Інтерфейс IList
описує упорядковану колекцію (послідовність). Узагальнений клас List
, який
реалізує інтерфейс IList
, представляє список, створений за допомогою масиву (аналогічний класу vector
у
C++). Як і в масивах, доступ до елементів може здійснюватися за індексом (через операцію []
). На відміну
від масивів, розмір списків може динамічно змінюватися. Властивість Count
повертає число елементів, які містяться
в списку. Як і елементи масивів, елементи списків пронумеровані з нуля.
Нижче наведені деякі методи узагальненого класу List
:
Метод | Опис |
---|---|
Add() |
Додає об'єкт в кінець списку |
AddRange() |
Додає елементи зазначеної колекції в кінець списку |
Clear() |
Видаляє всі елементи зі списку |
Contains() |
Визначає, чи входить елемент до складу списку |
CopyTo() |
Копіює список або його частину в масив |
IndexOf() |
Повертає індекс першого входження значення в списку або в його частині |
Insert() |
Додає елемент в список в позицію з вказаним індексом |
InsertRange() |
Додає елементи колекції в список в позицію з вказаним індексом |
Remove() |
Видаляє перше входження зазначеного об'єкта зі списку |
RemoveAt() |
Видаляє зі списку елемент з вказаним індексом |
RemoveRange() |
Видаляє діапазон елементів зі списку |
Reverse() |
Змінює порядок елементів у списку або в його частині на зворотний |
Sort() |
Сортує елементи в списку або в його частині |
ToArray() |
Копіює елементи списку в новий масив |
Створити порожній список об'єктів деякого типу (SomeType
) можна за допомогою усталеного конструктора:
IList<SomeType> list1 = new List<SomeType>();
Можна також одразу описати посилання на List
:
List<SomeType> list1 = new List<SomeType>();
Другий варіант є менш бажаним, оскільки в такому випадку знижується гнучкість програми. Перший варіант дозволить легко замінити реалізацію списку List
на будь-яку іншу реалізацію інтерфейсу IList
, яка більше відповідає вимогам конкретної задачі. У другому випадку є спокуса викликати методи, специфічні для List
, тому перехід на іншу реалізацію буде ускладнено. Для локальних змінних можна вживати опис за допомогою ключового слова var
. Наприклад, останній опис міг би бути таким:
var list1 = new List<SomeType>();
За допомогою спеціального конструктору можна створити список з існуючого масиву:
int[] a = { 1, 2, 3 }; IList<int> list2 = new List<int>(a);
Можна створити новий список із використанням існуючого. Новий список містить копії елементів. Наприклад:
var list3 = new List<int>(list2);
Списки, як і масиви, можна створювати з використанням ініціалізаторів:
IList<int> list4 = new List<int> { 11, 12, 13 };
Створивши список, у нього можна додавати елементи за допомогою функції Add()
. Метод Add()
з одним аргументом додає елемент у кінець списку:
IList<string> list5 = new List<string>(); list5.Add("abc"); list5.Add("def"); list5.Add("xyz");
До списку можна додати всі елементи іншого списку (або іншої колекції) за допомогою функції Concat()
. Результатом буде нова колекція (об'єкт типу IEnumerable<>
):
var result = list3.Concat(list4); // 1 2 3 11 12 13
У списках зберігати у списках елементи типу int
, double
тощо.
Як і масиви, списки можна обходити за допомогою конструкції foreach
:
foreach (int i in list3) { Console.WriteLine(i); }
У тих випадках, коли частіше, ніж вибір довільного елемента, застосовують операції додавання і видалення елементів у довільних місцях, доцільно використовувати клас LinkedList<>
, що зберігає об'єкти за допомогою зв'язаного списку.
2.6.3 Використання стандартних узагальнених функцій
Перед тим, як створювати свої власні узагальнені класи та методи, слід поцікавитися, чи є відповідні стандартні засоби. Для роботи з масивами існує стандартний клас System.Array
, який надає велику кількість статичних узагальнених функцій для роботи з масивами, наприклад, таких, як Resize()
(зміна розміру зі зберіганням існуючих елементів), Reverse()
(зміна порядку елементів на протилежний), Copy()
(копіювання одного масиву в інший, або в той самий з іншої позиції), Sort()
(сортування) та багато інших. Роботу цих функцій можна продемонструвати на прикладі:
using System; using System.Collections.Generic; namespace Arrays { class Program { public static void Print<TElem>(IList<TElem> arr) { foreach (TElem elem in arr) { System.Console.Write("{0, 6}", elem); } System.Console.WriteLine(); } static void Main(string[] args) { int[] a = { 1, 2, 3, 4 }; Array.Resize(ref a, 5); Print(a); // 1 2 3 4 0 Array.Reverse(a); Print(a); // 0 4 3 2 1 Console.WriteLine(Array.IndexOf(a, 4)); // 1 Array.Sort(a); // 0 1 2 3 4 Print(a); int[] b = new int[2]; Array.Copy(a, b, b.Length); Print(b); // 0 1 Array.Copy(a, 2, b, 0, b.Length); Print(b); // 2 3 } } }
З наведеного приклада видно, що клас System.Array
реалізує інтерфейс IList
. Тому його екземпляр може бути використаний як фактичний параметр функції з параметром типу IList
.
У протилежність масивам, відповідні методи для списків – нестатичні:
static void Main(string[] args) { List<int> a = new List<int> { 1, 2, 3, 4 }; a.Add(0); Print(a); // 1 2 3 4 0 a.Reverse(); Print(a); // 0 4 3 2 1 Console.WriteLine(a.IndexOf(4)); // 1 a.Sort(); Print(a); // 0 1 2 3 4 int[] b = new int[5]; a.CopyTo(b, 0); // копіювання у масив, починаючи зі вказаної позиції Print(b); // 0 1 2 3 4 }
Для масивів і списків числових значень сортування здійснюється за збільшенням. Для класів і структур упорядкування визначається функцією CompareTo()
інтерфейсу IComparable
. Цей метод повинен повернути від'ємне значення (наприклад, -1), якщо об'єкт, для якого викликаний метод, менше об'єкта o, нульове значення, якщо об'єкти рівні, і додатне значення в протилежному випадку. Для того, щоб масив або список можна було відсортувати за усталеним порядком, елементи масиву (списку) повинні бути об'єктами, для яких реалізовано інтерфейс IComparable
. Можна самостійно створити клас, що реалізує інтерфейс IComparable
. Наприклад, масив прямокутників сортується за площею:
class Rectangle : IComparable<Rectangle> { double width, height; public Rectangle(double width, double height) { this.width = width; this.height = height; } public double Area() { return width * height; } public int CompareTo(Rectangle rect) { return Area().CompareTo(rect.Area()); } public override String ToString() { return "[" + width + ", " + height + ", area = " + Area() + "]"; } } class Program { static void Main(string[] args) { Rectangle[] a = {new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4)}; Array.Sort(a); foreach (Rectangle rect in a) { Console.WriteLine(rect); } } }
Для типів, які не реалізують інтерфейс IComparable
, спроба здійснити усталене сортування призводить до генерації винятку InvalidOperationException
.
Якщо ми не хочемо (чи не можемо) визначити функцію CompareTo()
, можна створити окремий клас, що реалізує інтерфейс IComparer
. Посилання на об'єкт такого класу передаються в якості в другого параметру функції Sort()
для масивів (або першого параметру для списків). Інтерфейс IComparer
містить опис методу Compare()
з двома параметрами. Функція повинна повернути від'ємне число, якщо перший об'єкт під час сортування необхідно вважати меншим, чим інший, значення 0, якщо об'єкти еквівалентні, і додатне число в протилежному випадку.
class Rectangle { double width, height; public Rectangle(double width, double height) { this.width = width; this.height = height; } public double Area() { return width * height; } public override String ToString() { return "[" + width + ", " + height + ", area = " + Area() + "]"; } } class CompareByArea : IComparer<Rectangle> { public int Compare(Rectangle r1, Rectangle r2) { return r1.Area().CompareTo(r2.Area()); } } class Program { static void Main(string[] args) { Rectangle[] a = { new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4) }; Array.Sort(a, new CompareByArea()); foreach (Rectangle rect in a) { Console.WriteLine(rect); } } }
Сортування списків здійснюється аналогічно.
2.6.4 Робота з множинами
Множина – це колекція, що не містить однакових елементів.
Клас HashSet
використовує так звані хеш-коди для ідентифікації елемента. Хеш-коди забезпечують швидкий доступ до даних по деякому ключу. Механізм одержання хеш-кодів забезпечує їх майже повну унікальність. Усі об'єкти C# можуть генерувати хеш-коди. Хеш-код – це послідовність бітів фіксованої довжини. Для кожного об'єкта ця послідовність вважається унікальною. Клас SortedSet
використовує двійкове дерево для збереження елементів і гарантує їх певний порядок.
Метод Add()
додає елемент до множини і повертає true
якщо елемент раніше був відсутній. В іншому випадку елемент не додається, а метод Add()
повертає false
. Метод Remove()
видаляє зазначений елемент множини, якщо такий є. Усі елементи множини видаляються за допомогою методу Clear()
. Наприклад:
using System; using System.Collections.Generic; namespace SetDemo { class Program { static void Main(string[] args) { SortedSet<String> set = new SortedSet<string>(); Console.WriteLine(set.Add("one")); // True Console.WriteLine(set.Add("two")); // True Console.WriteLine(set.Add("three")); // True Console.WriteLine(set.Add("one")); // False foreach (string s in set) { Console.Write("{0} ", s); // one three two } Console.WriteLine(); set.Remove("three"); foreach (string s in set) { Console.Write("{0} ", s); // one two } Console.WriteLine(); set.Clear(); } } }
Як видно з наведеного приклада, однакові елементи не можна додати двічі. Елементи автоматично записуються в порядку збільшення.
До елементів можна звертатися за індексом. Метод Contains()
повертає true
, якщо множина містить зазначений елемент. Властивість Count
повертає кількість елементів. Крім того, для відсортованих множин є властивості, які повертають мінімальне і максимальне значення (Min
та Max
). Як і для списків, за допомогою методу CopyTo()
можна скопіювати елементи множини в одновимірний масив.
У наведеному нижче прикладі до множини цілих чисел додається десять випадкових значень у діапазоні від -9 до 9:
using System; using System.Collections.Generic; namespace SetOfIntegers { class Program { static void Main(string[] args) { SortedSet<int> set = new SortedSet<int>(); Random random = new Random(); for (int i = 0; i < 10; i++) { int k = random.Next() % 10; set.Add(k); } foreach (int k in set) { Console.Write("{0} ", k); ; } } } }
Результуюча множина як правило містить менш, ніж 10 чисел, оскільки окремі значення можуть повторюватися. Оскільки ми використовуємо SortedSet
, числа зберігаються та виводяться в упорядкованому (за зростанням) вигляді. Для того, щоб додати саме десять різних чисел, програму можна модифікувати, наприклад із застосуванням циклу while
замість for
:
while (set.Count < 10) { . . . }
Оскільки множина може містити тільки різні елементи, її можна використати для підрахунку різних слів, літер, цифр тощо – створюється множина та викликається властивість Count
. Застосовуючи SortedSet
, можна виводити слова та літери в алфавітному порядку.
2.6.5 Асоціативні масиви
Асоціативні масиви можуть зберігати пари об'єктів (посилань на об'єкти). Асоціативні масиви теж є узагальненими типами. Асоціативні масиви у C# представлені узагальненим інтерфейсом IDictionary
, який реалізовано, класами Dictionary
і SortedDictionary
. Останній клас вимагає впорядкованого за ключем зберігання пар. Ключі, на відміну від значень, не можуть повторюватися.
Кожне значення (об'єкт), яке зберігається в асоціативному масиві, пов'язується з певним значенням іншого об'єкту (ключа). Метод Add(key, value)
додає значення (value
) та асоціює з ним ключ (key
). Якщо асоціативний масив раніше містив пари з зазначеним ключем, нове значення заміняє старе. Для перевірки знаходження ключа та значення застосовуються методи ContainsKey()
та ContainsValue()
. Обійти асоціативний масив можна за допомогою ітерації по елементам типу KeyValuePair
з властивостями Key
та Value
. Крім того, звертатися як до існуючих, так і до відсутніх елементів можна через індексатор. Видалити елемент за ключем можна за допомогою метода Remove()
.
Ключ і значення можуть бути як різних, так і однакових типів. Наприклад:
using System; using System.Collections.Generic; namespace DictionaryTest { class Program { static void Main(string[] args) { SortedDictionary<string, string> dictionary = new SortedDictionary<string, string>(); dictionary.Add("sky", "небо"); dictionary.Add("house", "дiм"); dictionary.Add("white", "бiлий"); dictionary.Add("dog", "собака"); dictionary["dog"] = "собака"; // аналогічно dictionary.Add("dog", "собака"); foreach (var pair in dictionary) // pair типу KeyValuePair { Console.WriteLine("{0}\t {1}", pair.Key, pair.Value); // виведення за абеткою } Console.WriteLine(dictionary.ContainsValue("небо")); // True Console.WriteLine(dictionary.ContainsKey("city")); // False dictionary.Remove("dog"); dictionary["house"] = "будинок"; foreach (var pair in dictionary) Console.WriteLine("{0}\t {1}", pair.Key, pair.Value); } } } }
Властивості Keys
та Values
відповідно повертають колекції ключів та значень.
Версія 6 C# пропонує новий спосіб ініціалізації словників – спеціальну форму ініціалізатора індексу. Наприклад:
Dictionary<string, string> countries = new Dictionary<string, string> { ["France"] = "Paris", ["Germany"] = "Berlin", ["Ukraine"] = "Kyiv" }; foreach (var pair in countries) { Console.WriteLine("{0}\t {1}", pair.Key, pair.Value); }
2.7 Створення власних контейнерних типів
Незважаючи на велику кількість стандартних контейнерних класів, іноді виникає потреба у створені власних контейнерів. Це можуть бути, наприклад, складні дерева, більш гнучкі списки, спеціалізовані колекції елементів тощо. Найчастіше такі контейнери створюють на базі існуючих типів (колекцій або масивів). Додавши індексатори можна забезпечити отримання окремих елементів. Але іноді також є зручним проходження послідовних контейнерів у циклі foreach
. Застосування таких циклів можливе, якщо клас-контейнер надає так званий ітератор – допоміжний об'єкт, який забезпечує проходження по елементам колекції.
Для того, щоб контейнер надав ітератор, відповідний клас повинен реалізувати функцію GetEnumerator()
. Для генерації ітератору використовують спеціальне ключове слово yield
. Твердження yield return
повертає один елемент колекції і переміщує поточну позицію на наступний елемент. У наведеному нижче прикладі ітератор обходить окремі літери слова:
using System; using System.Collections.Generic; namespace LabThird { class Letters { string word; public Letters(string word) { this.word = word; } public IEnumerator<char> GetEnumerator() { foreach (char c in word) { yield return c; } } } class Program { static void Main(string[] args) { Letters letters = new Letters("Hello!"); foreach (char c in letters) { Console.WriteLine(c); } } } }
Після запуску програми отримаємо такий результат:
H e l l o !
2.8 Створення та використання бібліотек класів
Поняття бібліотеки є загальним для різних мов програмування, програмних платформ та програмного забезпечення в цілому. Бібліотека – це набір ресурсів, що використовуються під час розробки або виконання програм. Конкретний набір ресурсів залежить від того, для чого саме вживають бібліотеку.
На стадії розробки користуються бібліотеками, які надають класи, об'єкти, функції, іноді також зміні та інші дані. Такі бібліотеки надаються у вигляді вихідного коду або скомпільованого коду. Різні програми можуть використовувати ці ресурси статично (код бібліотечних функцій та інших ресурсів включається в один файл, що виконується) або динамічно (спеціальний файл з кодом повинен бути присутнім під час виконання програми). В останньому випадку одна й та ж бібліотека може бути використана одночасно в декількох застосунках.
Платформа .NET підтримує механізм динамічних бібліотек. Стандартні класи згруповані у складання та скомпільовані у бібліотеки динамічного компонування (Dynamic-link library). Такі файли зазвичай мають розширення dll
.
Під час створення нового застосунку в середовищі MS Visual Studio до проекту автоматично підключаються найбільш вживані стандартні бібліотеки .NET.
Іноді виникає необхідність у створені власних бібліотек класів. Такі бібліотеки групуватимуть простори імен та класи, які потім можуть бути застосовані в інших проектах. У найпростішому випадку ці бібліотеки будуть застосовані у проектах поточного рішення. Для того, щоб додати нову бібліотеку до існуючого рішення, у вікні Solution Explorer обираємо гілку Solution та через контекстне меню додаємо новий проект (Add | New Project...) – Class Library. У правій частині вікна слід вибрати шаблон Class Library (.NET Core). Далі вводимо ім'я бібліотеки. Створений файл (Class1.cs) доцільно перейменувати відповідно до змісту класів, які будуть додані до бібліотеки. Якщо тепер ми хочемо вживати класи з нової бібліотеки, її підключення слід здійснити вручну для кожного проекту, де вона необхідна (Add | Project Reference...).
3 Приклади програм
3.1 Створення бібліотеки узагальнених методів
Незважаючи на величезну кількість функцій для роботи з масивами, які надає клас System.Array
, деякі корисні функції там відсутні. Наприклад, можна було б додати функції обміну місцями елементів, уставлення елемента на вказане місце, а також виведення елементів масиву на консоль. Створюємо простір імен Arrays
та додаємо в нього статичний клас ArrayUtils
. Відповідні функції реалізуємо як узагальнені статичні методи.
namespace Arrays { public static class ArrayUtils { public static void SwapElements<TElem>(TElem[] arr, int i, int j) { // Копіювання одного з елементів у тимчасову комірку: TElem elem = arr[i]; arr[i] = arr[j]; arr[j] = elem; } public static void Insert<TElem>(ref TElem[] arr, int index, TElem elem) { // Зміна розміру масиву: System.Array.Resize(ref arr, arr.Length + 1); // Зсув елементів уперед шляхом копіювання: System.Array.Copy(arr, index, arr, index + 1, arr.Length - index - 1); // Встановлення нового значення: arr[index] = elem; } public static void Print<TElem>(TElem[] arr) { foreach (TElem elem in arr) { System.Console.Write("{0, 6}", elem); } System.Console.WriteLine(); } } class Program { static void Main(string[] args) { int[] a = { 1, 2, 3, 4 }; ArrayUtils.SwapElements(a, 2, 3); ArrayUtils.Print(a); // 1 2 4 3 ArrayUtils.Insert(ref a, 2, 11); ArrayUtils.Print(a); // 1 2 11 4 3 string[] b = { "one", "two", "three" }; ArrayUtils.SwapElements(b, 2, 0); ArrayUtils.Print(b); // three two one ArrayUtils.Insert(ref b, 3, "zero"); ArrayUtils.Print(b); // three two one zero } } }
Функція Insert<TElem>()
отримує параметр типу масиву як посилання, оскільки в ній змінюється не порядок елементів, а створюється новий масив (тобто змінюється значення посилання).
3.2 Робота з множиною
У наведеному нижче прикладі вводиться речення та виводяться всі різні літери речення (не враховуючи роздільників) в алфавітному порядку:
using System; using System.Collections.Generic; namespace Sentence { class Program { static void Main(string[] args) { string sentence = Console.ReadLine(); // Створюємо множину роздільників: HashSet<char> delimiters = new HashSet<char>() {' ', '.', ',', ':', ';', '?', '!', '-', '(', ')', '\"'}; // Створюємо множину літер: SortedSet<char> letters = new SortedSet<char>(); // Додаємо всі літери крім роздільників: for (int i = 0; i < sentence.Length; i++) { if (!delimiters.Contains(sentence[i])) { letters.Add(sentence[i]); } } foreach(char c in letters) { Console.Write("{0} ", c); } } } }
3.3 Робота з асоціативним масивом
У наведеному нижче прикладі обчислюється кількість входжень різних слів у речення. Слова і відповідні кількості зберігаються в асоціативному масиві. Використання класу SortedDictionary
гарантує алфавітний порядок слів (ключів).
using System; using System.Collections.Generic; namespace WordsCounter { class Program { static void Main(string[] args) { SortedDictionary<string, int> d = new SortedDictionary<string, int>(); string s = "the first men on the moon"; string[] arr = s.Split(); foreach (string word in arr) { int count = d.ContainsKey(word) ? d[word] : 0; d[word] = count + 1; } foreach (var pair in d) { Console.WriteLine(pair.Key + "\t" + pair.Value); } } } }
3.4 Створення власного контейнера
Припустимо, необхідно створити клас для представлення масиву, індекс елементів якого змінюється від одиниці до кількості елементів включно. Це є типовим для таких мов, як Паскаль та Бейсик. Крім того, функціональність масиву можна розширити перекриттям функції ToString()
, додавання нового елементу та видалення останнього.
Такий клас, звичайно, буде узагальненим. Полем класу буде звичайний масив. Для того, щоб можна було обходити елементи нового контейнеру за допомогою конструкції foreach
, необхідно визначити ітератор – функцію GetEnumerator()
. Вихідний код програми буде таким:
using System; using System.Collections.Generic; namespace LabThirdArray { // Масив, елементи якого індексуються з одиниці public class ArrayFromOne<TElem> { private TElem[] arr = { }; // Індексатор public TElem this[int index] { get { return arr[index - 1]; } set { arr[index - 1] = value; } } // Кількість об'єктів у масиві public int Length { get { return arr.Length; } } // Створюємо порожній масив визначеної довжини public ArrayFromOne(int maxIndex) { arr = new TElem[maxIndex]; } // Створюємо об'єкт зі "звичайного" масиву, або списку елементів public ArrayFromOne(params TElem[] arr) { this.arr = new TElem[arr.Length]; Array.Copy(arr, this.arr, arr.Length); } // Визначаємо ітератор для проходження за допомогою foreach public IEnumerator<TElem> GetEnumerator() { foreach (TElem x in arr) { yield return x; } } // Елементи виводяться через кому, весь список – у квадратних дужках public override string ToString() { string s = "["; foreach (TElem elem in arr) { s += elem + ", "; } // Видаляємо останній пропуск та кому: return s.Substring(0, s.Length - 2) + "]"; } // Додаємо новий елемент у кінець масиву public void Add(TElem elem) { Array.Resize(ref arr, Length + 1); this[Length] = elem; } // Видаляємо останній елемент public void RemoveLast() { if (arr.Length > 0) { Array.Resize(ref arr, Length - 1); } } } class Program { static void Main(string[] args) { ArrayFromOne<int> a = new ArrayFromOne<int>(1, 2, 3); for (int i = 1; i <= a.Length; i++) { a[i]++; } Console.WriteLine(a); a.Add(8); Console.WriteLine(a); a.RemoveLast(); foreach (int x in a) { Console.WriteLine(x); } ArrayFromOne<string> b = new ArrayFromOne<string>(1); b[1] = "one"; b.Add("two"); Console.WriteLine(b); } } }
Можна також реалізувати такий клас, полем якого буде не масив, а список. Частина коду залишиться незмінною. У заголовку конструктора, який ініціалізує об'єкт масивом, можна описати аргумент з атрибутом params
.
3.5 Створення ієрархії узагальнених класів
Припустимо, необхідно розширити програму обробки даних про книжки на книжній полиці функціями для збереження даних у XML-форматі та завантаження даних з XML-документу. Робота з XML-документами базуватиметься на механізмах серіалізації.
У прикладах двох попередніх лабораторних робіт були запропоновані різні рішення для представлення даних про автора – у вигляді рядка та окремою структурою. Можна також запропонувати інші варіанти (структури або класи інших типів тощо). Для того, щоб об'єднати всі можливі реалізації, в якості типу автора можна застосувати узагальнення. Використання узагальнень дозволить відкласти рішення про представлення даних про автора – потім залежно від необхідності для представлення даних можна буде використовувати спеціальний клас, структуру, стандартний рядок, або щось інше.
Оскільки передбачається багатофункціональне використання створених програмних засобів, усі класи, які описують сутності предметної області, слід розмістити в окремій бібліотеці. Це дозволить спочатку створити консольний застосунок для тестування класів, а потім використовувати бібліотеку для створення програми графічного інтерфейсу користувача, або з іншою метою, наприклад, для створення Web-застосунку.
Спочатку створимо бібліотеку та консольний тест. Створюємо нове рішення (Bookshelf) з консольним застосунком. Потім до рішення додаємо новий проект – бібліотеку класів. Додаємо новий проект (Add | New Project...) – Class Library. Вводимо ім'я бібліотеки: BookshelfLib
. У єдиному вихідному файлі бібліотеки міститимуться класи, які описують сутності предметної області. Створений файл (Class1.cs) слід перейменувати у BookshelfClasses.cs. Це слід зробити за допомогою вікна Properties (Властивості), обравши попередньо файл Class1.cs у вікні Solution Explorer.
Далі вручну додаємо такий текстУ бібліотеці класів розташовуємо такий код:
using System; using System.Collections.Generic; using System.Xml.Serialization; using System.IO; namespace BookshelfLib { // Структура для опису автора public struct Author { [System.Xml.Serialization.XmlAttributeAttribute()] public string Surname { get; set; } [System.Xml.Serialization.XmlAttributeAttribute()] public string Name { get; set; } // Перевизначення еквівалентності public override bool Equals(object? obj) { if (obj == null) { return false; } Author author = (Author)obj; return author.Surname == Surname && author.Name == Name; } // Визначення представлення у вигляді рядку: public override string ToString() { return Name + " " + Surname; } // Визначається у парі з Equals() public override int GetHashCode() { return base.GetHashCode(); } public static bool operator ==(Author left, Author right) { return left.Equals(right); } public static bool operator !=(Author left, Author right) { return !(left == right); } } // Книжка public class Book<TAuthor> { [System.Xml.Serialization.XmlAttributeAttribute()] public string Title { get; set; } [System.Xml.Serialization.XmlAttributeAttribute()] public int Year { get; set; } public List<TAuthor> Authors { get; set; } // Конструктори public Book() { Title = ""; Authors = new List<TAuthor>(); } public Book(string title, int year, params TAuthor[] authors) { Title = title; Year = year; Authors = new List<TAuthor>(authors); } // Визначення представлення у вигляді рядку override public string ToString() { string s = string.Format("Назва: \"{0}\". Рiк видання: {1}", Title, Year); s += "\n" + " Автор(и):"; for (int i = 0; i < Authors.Count; i++) { s += string.Format(" {0}", Authors[i]); if (i < Authors.Count - 1) { s += ","; } else { s += "\n"; } } return s; } // Перевизначення еквівалентності public override bool Equals(object? obj) { if (obj is Book<TAuthor> b) { if (b.Authors.Count != Authors.Count) { return false; } for (int i = 0; i < Authors.Count; i++) { if (!b.Authors[i].Equals(Authors[i])) { return false; } } return b.Title == Title && b.Year == Year; } return false; } // Визначається у парі з Equals() public override int GetHashCode() { return base.GetHashCode(); } } // Книжкова полиця public class Bookshelf<TAuthor> { public List<Book<TAuthor>> Books { get; set; } // Конструктор public Bookshelf(params Book<TAuthor>[] books) { Books = new List<Book<TAuthor>>(books); } // Індексатор public Book<TAuthor> this[int index] { get { return Books[index]; } set { Books[index] = value; } } // Визначення представлення у вигляді рядку override public string ToString() { string result = ""; foreach (Book<TAuthor> book in Books) { result += book; } return result; } // Пошук книжки з певною послідовністю літер public List<Book<TAuthor>> ContainsCharacters(string characters) { List<Book<TAuthor>> found = new(); foreach (Book<TAuthor> book in Books) { if (book.Title.Contains(characters)) { // Додаємо новий елемент до списку: found.Add(book); } } return found; } // Додавання книжки public void Add(Book<TAuthor> book) { Books.Add(book); } // Видалення книжки зі вказаними даними public void Remove(Book<TAuthor> book) { Books.Remove(book); } // Читання книжок за допомогою механізму десеріалізації public bool ReadBooks(string fileName) { XmlSerializer deserializer = new(typeof(List<Book<TAuthor>>)); using TextReader textReader = new StreamReader(fileName); var data = deserializer.Deserialize(textReader); if (data == null) { return false; } Books = (List<Book<TAuthor>>)data; return true; } // Запис книжок за допомогою механізму серіалізації public void WriteBooks(string fileName) { XmlSerializer serializer = new(typeof(List<Book<TAuthor>>)); using TextWriter textWriter = new StreamWriter(fileName); serializer.Serialize(textWriter, Books); } // Вкладений клас для порівняння книжок за алфавітом назв class CompareByTitle : IComparer<Book<TAuthor>> { public int Compare(Book<TAuthor>? b1, Book<TAuthor>? b2) { if (b1 == null || b2 == null) { return 0; } return string.Compare(b1.Title, b2.Title); } } // Вкладений клас для порівняння книжок за кількістю авторів class CompareByAuthorsCount : IComparer<Book<TAuthor>> { public int Compare(Book<TAuthor>? b1, Book<TAuthor>? b2) { if (b1 == null || b2 == null) { return 0; } return b1.Authors.Count < b2.Authors.Count ? -1 : (b1.Authors.Count == b2.Authors.Count ? 0 : 1); } } // Сортування книжок за алфавітом назв public void SortByTitle() { Books.Sort(new CompareByTitle()); } // Сортування книжок за кількістю авторів public void SortByAuthorsCount() { Books.Sort(new CompareByAuthorsCount()); } // Перевантажений оператор додавання книжки public static Bookshelf<TAuthor> operator +(Bookshelf<TAuthor> bookshelf, Book<TAuthor> book) { Bookshelf<TAuthor> newShelf = new() { Books = bookshelf.Books }; newShelf.Add(book); return newShelf; } // Перевантажений оператор видалення книжки public static Bookshelf<TAuthor> operator -(Bookshelf<TAuthor> bookshelf, Book<TAuthor> book) { Bookshelf<TAuthor> newShelf = new() { Books = bookshelf.Books }; newShelf.Remove(book); return newShelf; } } // Книжкова полиця з назвою public class TitledBookshelf<TAuthor> : Bookshelf<TAuthor> { public string Title { get; set; } // Конструктор з параметрами public TitledBookshelf(string title, params Book<TAuthor>[] books) : base(books) { Title = title; } // Визначення представлення у вигляді рядку override public string ToString() { return Title + "\n" + base.ToString(); } // Перевантажений оператор додавання книжки public static TitledBookshelf<TAuthor> operator +(TitledBookshelf<TAuthor> titled, Book<TAuthor> book) { TitledBookshelf<TAuthor> newShelf = new(titled.Title) { Books = titled.Books }; newShelf.Add(book); return newShelf; } // Перевантажений оператор видалення книжки public static TitledBookshelf<TAuthor> operator -(TitledBookshelf<TAuthor> titled, Book<TAuthor> book) { TitledBookshelf<TAuthor> newShelf = new(titled.Title) { Books = titled.Books }; newShelf.Remove(book); return newShelf; } } }
Тепер можна додати проект (BookshelfApp
) до рішення Bookshelf
. Далі до проекту BookshelfApp
через кгнтекстне меню додаємо посилання (Add | Project Reference)
на попередньо створену бібліотеку. Слід здійснити тестування усіх функцій. Замість автоматично згенерованого коду розміщуємо
такий:
using BookshelfLib; namespace BookshelfApp { class Program { static void Main() { // Створюємо порожню полицю: Bookshelf<Author> bookshelf = new(); // Додаємо книжки bookshelf += new Book<Author>("The UML User Guide", 1999, new Author() { Name = "Grady", Surname = "Booch" }, new Author() { Name = "James", Surname = "Rumbaugh" }, new Author() { Name = "Ivar", Surname = "Jacobson" }); bookshelf += new Book<Author>(@"Об'єктно-орiєнтоване моделювання програмних систем", 2007, new Author() { Name = "Iгор", Surname = "Дудзяний" }); bookshelf += new Book<Author>("Thinking in Java", 2005, new Author() { Name = "Bruce", Surname = "Eckel" }); // Виводимо дані на екран: Console.WriteLine(bookshelf); Console.WriteLine(); // Шукаємо книжки з певною послідовністю літер: Console.WriteLine("Уведiть послiдовнiсть лiтер:"); string sequence = Console.ReadLine() ?? ""; Bookshelf<Author> newBookshelf = new() { Books = bookshelf.ContainsCharacters(sequence) }; // Виводимо результат на екран: Console.WriteLine("Знайденi книжки:"); Console.WriteLine(newBookshelf); Console.WriteLine(); try { // Зберігаємо дані про книжки: bookshelf.WriteBooks("Bookshelf.xml"); // Здійснюємо сортування за назвами та зберігаємо у файлі: bookshelf.SortByTitle(); Console.WriteLine("За назвами:"); Console.WriteLine(bookshelf); Console.WriteLine(); bookshelf.WriteBooks("ByTitle.xml"); // Здійснюємо сортування за кількістю авторів та зберігаємо у файлі: bookshelf.SortByAuthorsCount(); Console.WriteLine("За кiлькiстю авторiв:"); Console.WriteLine(bookshelf); Console.WriteLine(); bookshelf.WriteBooks("ByAuthorsCount.xml"); // Відтворюємо першу полицю в початковому варіанті bookshelf.ReadBooks("Bookshelf.xml"); Console.WriteLine("Початковий стан:"); Console.WriteLine(bookshelf); Console.WriteLine(); // Видаляємо книжку про Java Book<Author> javaBook = bookshelf[2]; // індексатор bookshelf -= javaBook; Console.WriteLine("Пiсля видалення книжки:"); Console.WriteLine(bookshelf); Console.WriteLine(); // Створюємо нову полицю. Для зберігання даних про автора використовуємо рядок TitledBookshelf<string> titledBookshelf = new("Java"); titledBookshelf += new Book<string>("Thinking in Java", 2005, "Bruce Eckel"); Console.WriteLine("Полиця з книжками з мови Java:"); Console.WriteLine(titledBookshelf); titledBookshelf.WriteBooks("JavaBooks.xml"); } catch (Exception ex) { Console.WriteLine("------------Виняток:------------"); Console.WriteLine(ex.GetType()); Console.WriteLine("-------------Змсiт:-------------"); Console.WriteLine(ex.Message); Console.WriteLine("-------Трасування стеку:-------"); Console.WriteLine(ex.StackTrace); } } } }
Новий проект слід встановити як стартовий (позиція Set as Startup Project контекстного меню).
Після завершення програми у теці bin\Debug або bin\Release проекту (залежно від способу завантаження програми на виконання) повинні з'явитися три XML-документа. Перший з них (Bookshelf.xml) матиме такий вміст:
<?xml version="1.0" encoding="utf-8"?> <ArrayOfBookOfAuthor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <BookOfAuthor Title="The UML User Guide" Year="1999"> <Authors> <Author Surname="Booch" Name="Grady" /> <Author Surname="Rumbaugh" Name="James" /> <Author Surname="Jacobson" Name="Ivar" /> </Authors> </BookOfAuthor> <BookOfAuthor Title="Об'єктно-орiєнтоване моделювання програмних систем" Year="2007"> <Authors> <Author Surname="Дудзяний" Name="Iгор" /> </Authors> </BookOfAuthor> <BookOfAuthor Title="Thinking in Java" Year="2005"> <Authors> <Author Surname="Eckel" Name="Bruce" /> </Authors> </BookOfAuthor> </ArrayOfBookOfAuthor>
Інші XML-документи відрізнятимуться порядком розташування книжок.
4 Вправи для контролю
- Прочитати з текстового файлу всі рядки, занести у список, та записати в інший текстовий файл у зворотному порядку.
- Прочитати з текстового файлу всі рядки, занести у список, та записати в інший текстовий файл рядки з парними номерами.
- Описати класи Автор та Книга. Створити об'єкти, здійснити їх серіалізацію в XML-документ та десеріалізацію.
- Описати класи Країна та Столиця. Створити об'єкти, здійснити їх серіалізацію в XML-документ та десеріалізацію.
- Реалізувати статичну узагальнену функцію копіювання першого елемента масиву в останню позицію. Здійснити тестування функції на двох масивах різних типів.
- Реалізувати статичну узагальнену функцію видалення елементів масиву з непарними індексами. Здійснити тестування функції на двох масивах різних типів.
- Реалізувати статичну узагальнену функцію заміни порядку елементів списку на протилежний. Здійснити тестування функції на двох списках різних типів.
- Реалізувати статичну узагальнену функцію обміну місцями елемента масиву з індексом 0 та останнього елемента. Здійснити тестування функції на двох масивах різних типів.
- Реалізувати статичну узагальнену функцію обміну місцями елемента списку з індексом 0 та останнього елемента. Здійснити тестування функції на двох списках різних типів.
- Реалізувати статичну узагальнену функцію обміну місцями елемента списку з індексом 1 та передостаннього елемента. Здійснити тестування функції на двох списках різних типів.
- Реалізувати статичну узагальнену функцію визначення кількості разів входження певного елемента у список. Здійснити тестування функції на двох списках різних типів.
- Реалізувати статичну узагальнену функцію циклічного зсуву списку на задану кількість елементів. Здійснити тестування функції на двох списках різних типів.
- Реалізувати статичну узагальнену функцію пошуку індексу елемента, починаючи з якого деякий список повністю входить в інший. Здійснити тестування функції на двох списках різних типів.
- Увести кількість елементів майбутньої множини дійсних чисел та діапазон чисел. Сформувати цю множину з випадкових значень. Вивести елементи множини у порядку зменшення.
- Заповнити множину цілих випадковими додатними парними значеннями (не більше визначеного числа). Вивести результат.
- Увести слово та вивести всі різні літери слова в алфавітному порядку.
- Увести речення та обчислити кількість різних літер, з яких речення складається. Не враховувати пропусків та розділових знаків.
- Увести речення та обчислити кількість різних слів у реченні.
- Увести речення та вивести всі різні слова речення в алфавітному порядку.
- Представити дані про користувачів у вигляді асоціативного масиву (ім'я / пароль) з припущенням, що всі імена користувачів різні. Вивести дані про користувачів з довжиною пароля більше 6 символів.
- Створити клас "Речення" з ітератором, який переміщується по окремим словам.
- Створити клас "Число" з ітератором, який переміщується по окремим цифрам.
5 Контрольні запитання
- Для чого призначений механізм винятків?
- Чим відрізняється механізм обробки винятків від відповідного механізму мови C++?
- Як створити об'єкт-виняток?
- Чи можна використовувати основний результат функції, якщо відбулася генерація винятку?
- Як отримати текст повідомлення про виняток у C#?
- Як отримати результат трасування стеку в C#?
- Як визначити список виняткових ситуацій функції?
- Чи можна викликати функцію, яка генерує виняткову ситуацію, без перевірки цього винятку? Якою буде реакція системи?
- Які засоби .NET надає для роботи з текстовими файлами?
- Чи можна записати весь текст файлу в один рядок однією функцією?
- У чому полягають особливості об'єктної моделі документу в порівнянні з подіє-орієнтованою моделлю документу?
- Що таке серіалізація і для чого її використовують?
- У чому є переваги та недоліки XML-серіалізації?
- Як керувати способом зберігання даних під час XML-серіалізації?
- Що таке узагальнене програмування?
- Коли доцільно створювати узагальнені типи?
- Які синтаксичні елементи можуть бути узагальненими?
- Чим відрізняється використання узагальнень у C# та шаблонів C++?
- Як визначити обмеження на тип параметру?
- Які контейнерні класи реалізовані у .NET та яке їх призначення?
- Як з масиву отримати список?
- Коли доцільніше використовувати
List
у порівнянні зLinkedList
? - Коли доцільніше використовувати
LinkedList
у порівнянні зList
? - Як здійснюється доступ до окремих елементів списку?
- Як здійснюється сортування списків?
- Чим множина відрізняється від списку?
- Яким вимогам повинен задовольняти об'єкт, щоб список або масив таких об'єктів можна було сортувати без визначення ознаки сортування?
- Чим множина відрізняється від асоціативного масиву?
- Наведіть приклади використання асоціативних масивів.
- У чому відміни класів
Dictionary
іSortedDictionary
? - Коли необхідно створювати власні контейнери?
- Які елементи слід реалізувати під час створення власного контейнеру?
- Що таке ітератор?
- Для чого використовують ключове слово
yield
? - Що таке бібліотеки і як вони можуть бути використані у програмуванні?
- Що таке динамічна бібліотека?
- Як підключити бібліотеку класів?
- Як створити бібліотеку класів?