Лабораторна робота 4
Узагальнене програмування
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Реалізувати індивідуальне завдання попередньої лабораторної роботи, замінивши використання масивів використанням
списків (клас List
). Замість роботи з текстовими файлами реалізувати запис та читання через механізм
XML-серіалізації / десеріалізації. Відтворити завдання попередніх лабораторних робіт.
Для всіх класів, які представляють сутності предметної області, перевизначити методи Equals()
і GetHashCode()
.
Усі класи, крім класу який містить функцію Main()
, розташувати в окремій бібліотеці. Класи, які реалізують
різні аспекти функціональності, розташувати в окремих файлах.
В консольному застосунку підключити створену бібліотеку та здійснити тестування всіх функцій. Передбачити перехоплення можливих винятків.
1.2 Створення бібліотеки узагальнених функцій для роботи з масивами та списками
Реалізувати статичний клас та реалізувати статичні узагальнені методи, які реалізують таку функціональність:
- обмін місцями двох груп елементів
- обмін місцями усіх пар сусідніх елементів (з парним і непарним індексом)
- вставлення у масив (список) іншого масиву (списку) елементів у вказане місце
- заміна групи елементів іншим масивом (списком) елементів
Реалізувати наведені функції для масивів і для списків.
1.3 Створення власного контейнера
Створити узагальнений клас для представлення одновимірного масиву, індекс елементів якого змінюється від
визначеного значення From
до значення To
включно. Ці значення можуть бути як
додатними, так і від'ємними. Клас повинен мати такі елементи:
- закрите поле – "звичайний" масив (список);
- індексатор;
- властивості (для читання)
From
таTo
(доцільно зберігати тількиFrom
, аTo
– обчислювати); - конструктор з параметрами
From
таTo
для створення порожнього масиву; - конструктор з параметром
From
та параметром типу масиву (з атрибутомparams
); - перевантажений оператор перетворення у рядок (
operator string
); - метод надання ітератора, який забезпечить можливість обходу елементів за допомогою
foreach
; - метод додавання нового елемента;
- метод видалення останнього елемента.
У функції Main()
слід здійснити тестування усіх створених елементів класу.
Необхідно реалізувати два варіанти – на базі масиву та на базі списку.
1.4 Робота з множиною
Увести кількість елементів майбутньої множини цілих чисел та діапазон чисел. Сформувати цю множину з випадкових значень. Вивести елементи множини, відсортовані за збільшенням.
1.5 Робота з асоціативним масивом
Увести речення та вивести усі різні літери, які входять у речення та їх кількість входження.
1.6 Використання типу даних dynamic (додаткове завдання)
Створити клас «Простий дріб». Реалізувати перевантаження для операцій +
, -
,
*
та /
,
зокрема, ділення на ціле число. Створити статичний клас, який надає функції для отримання середнього арифметичного та
добутку елементів масиву. Застосувати тип dynamic
. Здійснити тестування функцій,
використовуючи як масив елементів типу double
, так і масив простих дробів.
1.7 Створення "гнучкого" масиву (додаткове завдання)
Створити узагальнений клас для представлення одновимірного масиву, який автоматично розширюється, коли
користувач звертається до відсутнього елементу. Наприклад, якщо створено порожній масив a
,
перше звернення (для читання або запису) до елементу a[n]
забезпечить розширення масиву так,
щоб він містив n + 1
елемент з індексами від 0 до n
-го включно. Якщо певні
елементи вже існували, вони зберігаються та масив доповнюється новими елементами. Якщо елемент з індексом
вже існує, здійснюється звичайне звернення.
Створити конструктор без параметрів, індексатор, властивість, яка повертає індекс останнього елемента,
метод надання ітератора, а також перекрити функцію ToString()
. Здійснити тестування усіх
можливостей класу.
2 Методичні вказівки
2.1 Створення та використання узагальнень
Узагальнене програмування (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> { }
Можна також визначати узагальнені перевантажені операції.
2.2 Стандартні узагальнені класи
2.2.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.2.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
на будь-яку іншу реалізацію інтерфейсу
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.2.3 Використання стандартних методів для роботи зі списками
Перед тим, як створювати свої власні узагальнені класи та методи, слід поцікавитися, чи є відповідні стандартні
засоби. Для роботи зі списками існує велика кількість
методів, наприклад, таких, як Resize()
(зміна розміру
зі зберіганням існуючих елементів), Reverse()
(зміна порядку елементів на протилежний), Sort()
(сортування) та багато
інших. Роботу найбільш вживаних методів для роботи зі списком можна продемонструвати на прикладі:
List<int> a = new() { 1, 2, 3, 4 }; a.Reverse(); Console.WriteLine(string.Join(" ", a)); // 4 3 2 1 a.Sort(); Console.WriteLine(string.Join(" ", a)); // 1 2 3 4 Console.WriteLine(a.Sum()); // 10 Console.WriteLine(a.Average()); // 2.5 Console.WriteLine(a.Min()); // 1 Console.WriteLine(a.Max()); // 4 a.Remove(3); Console.WriteLine(string.Join(" ", a)); // 1 2 4 a = a.Concat(new List<int> { 11, 12, 2 }).ToList(); Console.WriteLine(string.Join(" ", a)); // 1 2 4 11 12 2 Console.WriteLine(a.Contains(3)); // False Console.WriteLine(a.Contains(4)); // True a = a.Distinct().ToList(); Console.WriteLine(string.Join(" ", a)); // 1 2 4 11 12 Console.WriteLine(a.FindIndex(x => x == 1)); // 0 Console.WriteLine(a.ToString());
2.2.4 Робота з множинами
Множина – це колекція, що не містить однакових елементів.
Клас HashSet
використовує так звані хеш-коди для ідентифікації елемента. Хеш-коди забезпечують
швидкий доступ до даних по деякому ключу. Механізм одержання хеш-кодів забезпечує їх майже повну унікальність.
Усі об'єкти C# можуть генерувати хеш-коди. Хеш-код – це послідовність бітів фіксованої довжини. Для
кожного об'єкта ця послідовність вважається унікальною. Клас SortedSet
використовує двійкове дерево
для збереження елементів і гарантує їх певний порядок.
Метод Add()
додає елемент до множини і повертає true
якщо елемент раніше був
відсутній. В іншому випадку елемент не додається, а метод Add()
повертає false
.
Метод Remove()
видаляє зазначений елемент множини, якщо такий є. Усі елементи множини видаляються
за допомогою методу Clear()
. Наприклад:
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:
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.2.5 Асоціативні масиви
Асоціативні масиви можуть зберігати пари об'єктів (посилань на об'єкти). Асоціативні масиви теж є
узагальненими типами. Асоціативні масиви у C# представлені узагальненим інтерфейсом IDictionary
,
який реалізовано, класами Dictionary
і SortedDictionary
. Останній клас вимагає
впорядкованого за ключем зберігання пар. Ключі, на відміну від значень, не можуть повторюватися.
Кожне значення (об'єкт), яке зберігається в асоціативному масиві, пов'язується з певним значенням іншого
об'єкту (ключа). Метод Add(key, value)
додає значення (value
) та асоціює з ним ключ
(key
). Якщо асоціативний масив раніше містив пари з зазначеним ключем, нове значення заміняє старе.
Для перевірки знаходження ключа та значення застосовуються методи ContainsKey()
та ContainsValue()
.
Обійти асоціативний масив можна за допомогою ітерації по елементах типу KeyValuePair
з
властивостями Key
та Value
. Крім того, звертатися як до наявних, так і до відсутніх
елементів можна через індексатор. Видалити елемент за ключем можна за допомогою метода Remove()
.
Ключ і значення можуть бути як різних, так і однакових типів. Наприклад:
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.3 Перевірка еквівалентності та хеш-коди
Для коректної ідентифікації та порівняння об'єктів, зокрема, для належної роботи Dictionary
або HashSet
для
об'єктів, які зберігаються в колекціях, доцільно перевизначити методи Equals()
і GetHashCode()
класу System.Object
.
Перевірка на еквівалентність типів-значень здійснюється за допомогою операції порівняння (==
). Для
деяких стандартних класів, таких як System.String
, окрім Equals()
перевизначено операцію
порівняння для перевірки рядків на еквівалентність. Але в більшості випадків для перевірки еквівалентності двох об'єктів
слід реалізувати метод Equals()
, оскільки звичайне порівняння застосовується до посилань, а не до об'єктів.
Метод Equals()
повинен повертати true
, якщо два об'єкти еквівалентні,
та false
у протилежному випадку. Типова реалізація передбачає такі кроки:
- перевірка посилань (чи вони збігаються);
- перевірка об'єкта, який ми порівнюємо, на значення
null
; - перевірка типу, наприклад, за допомогою
GetType()
; - перевірка значень полів.
Завдяки механізму зіставлення зі зразком (pattern matching) реалізацію Equals()
можна спростити. Припустимо
є клас Person
, у якому можна навести типову реалізацію Equals()
:
public class Person { public string FirstName { get; set; } = ""; public string LastName { get; set; } = ""; public override bool Equals(object? obj) { return obj is Person person && FirstName == person.FirstName && LastName == person.LastName; } }
Якщо obj
дорівнює null
, а також якщо об'єкт хибного типу, операція is
повертає false
.
Перевірку посилань можна не виконувати.
У парі з Equals()
зазвичай перевантажується метод GetHashCode()
, який повертає ціле
число, яке представляє хеш-код об'єкта – унікальне значення, яке повинно бути однаковим для еквівалентних об'єктів,
а також бажано, щоб воно було різним для різних об'єктів. Останнє правило достатньо важко виконати. Хеш-коди використовуються
в хеш-таблицях та інших структурах даних для швидкого пошуку об'єктів. Якщо метод Equals()
перевизначений
для порівняння об'єктів за значеннями, також потрібно перевизначити GetHashCode()
, щоб об'єкти з однаковими значеннями
мали однаковий хеш-код.
У нашому випадку можна запропонувати таку реалізацію методу GetHashCode()
:
public override int GetHashCode() { return HashCode.Combine(FirstName, LastName); }
Метод Combine()
, реалізований для різної кількості аргументів (від 1 до 8), дозволяє скомбінувати
хеш-коди оптимальним способом.
2.4 Створення власних контейнерних типів
Попри велику кількість стандартних контейнерних класів, іноді виникає потреба у створені власних
контейнерів. Це можуть бути, наприклад, складні дерева, більш гнучкі списки, спеціалізовані колекції елементів
тощо. Найчастіше такі контейнери створюють на базі наявних типів (колекцій або масивів). Додавши індексатори
можна забезпечити отримання окремих елементів. Але іноді також є зручним проходження послідовних контейнерів у
циклі 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.5 Тип dynamic
Контекстне ключове слово dynamic
було введено в C# 4.0. Спочатку тип
dynamic
був
розроблений для роботи з COM-об'єктами. Тип dynamic
дозволяє створювати змінні,
типи яких
можна визначити під час виконання, на відміну від ключового слова var
, яке
дозволяє ідентифікувати
тип під час компіляції. Отже, змінна може змінити свій фактичний тип:
dynamic x = 1; Console.WriteLine(x.GetType().Name); // Int32 x = "A"; Console.WriteLine(x.GetType().Name); // String x = 2.5; Console.WriteLine(x.GetType().Name); // Double
Примітка: метод GetType()
повертає об'єкт класу System.Type
, який інкапсулює
інформацію
про тип змінної, зокрема її ім'я (властивість Name
).
Наведений нижче приклад показує реалізацію парадигми узагальненого програмування на основі типу dynamic
.
Метод Sum()
можна застосувати до аргументів різних типів (int
,
double
, string
),
оскільки ці типи дозволяють застосовувати оператор +
:
class Program { static dynamic Sum(dynamic x, dynamic y) { return x + y; } static void Main(string[] args) { int i1 = 1; int i2 = 2; Console.WriteLine(Sum(i1, i2)); // 3 double d1 = 1.1; double d2 = 2.2; Console.WriteLine(Sum(d1, d2)); // 3.3 string s1 = "dot"; string s2 = "net"; Console.WriteLine(Sum(s1, s2)); // dotnet } }
Ми також можемо створити клас (наприклад, Complex
), який перевантажує операцію +
.
Об'єкти цього
класу також можуть бути використані як фактичні аргументи:
public class Complex { public double A { get; set; } public double B { get; set; } public static Complex operator+(Complex c1, Complex c2) { return new Complex { A = c1.A + c2.A, B = c1.B + c2.B }; } public override string ToString() { return A + " " + B + "i"; } } . . . Complex c1 = new Complex { A = 1, B = 2 }; Complex c2 = new Complex { A = 3, B = 4 }; Console.WriteLine(Sum(c1, c2));
Ми можемо спробувати надіслати аргументи типів, які не підтримують операцію +
. У цьому випадку
компілятор не відображає повідомлення про помилки:
object o1 = new object(); object o2 = new object(); Console.WriteLine(Sum(o1, o2));
Але під час виконання ми отримаємо виняток:
Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: Operator '+' cannot be applied to operands of type 'object' and 'object'
Недоліками динамічних типів є деяке зниження ефективності програми та додаткові труднощі зневадження. Можливі помилки можна знайти лише під час виконання, але не під час компіляції.
Можна створити масив елементів типу dynamic
. Насправді елементи можуть бути різних типів:
dynamic[] arr = new dynamic[3]; arr[0] = 1; arr[1] = 1.2; arr[2] = "a"; //arr[2] = new object(); dynamic sum = 0; foreach (dynamic d in arr) { sum += d; } Console.WriteLine(sum); // 2.2a
2.6 Використання атрибутів
Атрибути C# використовуються для додавання метаданих до елементів коду, таких як класи, методи та властивості. Завдяки атрибутам до елемента коду можна додати інформацію, яка не може бути визначена засобами мови C#. Обробка атрибутів здійснюється під час компіляції. Атрибут розташовують у квадратних дужках перед елементом коду, до якого цей атрибут відноситься. Існує декілька стандартних атрибутів, які завжди можна використовувати в програмному коді.
Як приклад стандартного атрибута можна навести атрибут [Obsolete]
, яким помічаються застарілі методи,
використання яких є небажаним. Після імені атрибута в дужках вказується параметр – рядок, який інформує про
те, що цей метод застарілий і що саме рекомендовано вживати замість цього методу:
class Program { [Obsolete("This method is deprecated, use Good instead.")] static void Bad() { } static void Good() { } static void Main(string[] args) { Bad(); // попередження: виклик небажаного методу } }
Можна взагалі заборонити використання застарілих методів вказавши другим параметром true
, що
означає генерацію помилки під час виклику:
class Program { [Obsolete("This method is deprecated, use Good instead.", true)] static void Bad() { } static void Good() { } static void Main(string[] args) { Bad(); // синтаксична помилка } }
Можна також використовувати такі атрибути, як [Serializable]
, [DefaultValue]
, [MaxLength]
, [MinLength]
тощо.
Для створення власного атрибута необхідно описати клас, похідний від System.Attribute
. Клас може містити
поля, властивості, методи тощо. Параметри конструкторів класу вказуються під час застосування атрибутів у програмному
коді.
2.7 Робота з 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.8 Використання серіалізації
Серіалізація – це процес збереження (передачі) стану об'єкта в потоці (файлі), зокрема, для передачі через комп'ютерні мережі. Послідовність даних, що зберігається, містить всю інформацію, необхідну для реконструкції (або десеріалізації) стану об'єкта з метою подальшого використання. Цю інформацію можна зберігати в різних форматах. Найбільш стисла серіалізація здійснюється у двійковому форматі. Але найкращим підходом, який забезпечує необхідну наочність, є серіалізація в 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#:
[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.9 Створення та використання бібліотек класів
Поняття бібліотеки є загальним для різних мов програмування, програмних платформ та програмного забезпечення в цілому. Бібліотека – це набір ресурсів, що використовуються під час розробки або виконання програм. Конкретний набір ресурсів залежить від того, для чого саме вживають бібліотеку.
На стадії розробки користуються бібліотеками, які надають класи, об'єкти, функції, іноді також зміні та інші дані. Такі бібліотеки надаються у вигляді вихідного коду або скомпільованого коду. Різні програми можуть використовувати ці ресурси статично (код бібліотечних функцій та інших ресурсів включається в один файл, що виконується) або динамічно (спеціальний файл з кодом повинен бути присутнім під час виконання програми). В останньому випадку одна й та ж бібліотека може бути використана одночасно в декількох застосунках.
Платформа .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 Робота з множиною
У наведеному нижче прикладі вводиться речення та виводяться всі різні літери речення (не враховуючи роздільників) в алфавітному порядку:
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
гарантує алфавітний
порядок слів (ключів).
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 LabFourthArray { // Масив, елементи якого індексуються з одиниці 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 Обчислення сум елементів масивів різних типів
Припустимо, необхідно створити функцію для обчислення суми елементів масивів різних типів. Можна побудувати таку
функцію із застосуванням типу dynamic
. Програма буде такою:
namespace DynamicSum { class Program { public static dynamic Sum(dynamic arr) { dynamic result = arr[0]; for (int i = 1; i < arr.Length; i++) { result += arr[i]; } return result; } static void Main(string[] args) { int[] ints = { 1, 2, 3 }; Console.WriteLine(Sum(ints)); string[] strings = { "a", "b", "c" }; Console.WriteLine(Sum(strings)); } } }
Результат роботи програми буде таким:
6 abc
Як видно з наведеного коду, суму можна знаходити для всіх об'єктів, для яких визначено операцію +
.
3.6 Робота зі списком публікацій та XML-серіалізацією
Припустимо, ми вирішили в програмі обробки даних про публікації на книжковій полиці, яка була розглянута в попередніх лабораторних роботах, замість масивів застосувати списки. Крім того, доцільно розширити програму функціями для збереження даних у XML-форматі та завантаження даних з XML-документа. Робота з XML-документами базуватиметься на механізмах серіалізації.
Оскільки передбачається багатофункціональне використання створених програмних засобів, усі класи, які описують сутності предметної області, слід розмістити в окремій бібліотеці. Це дозволить спочатку створити консольний застосунок для тестування класів, а потім використовувати бібліотеку для створення програми графічного інтерфейсу користувача, або з іншою метою, наприклад, для створення Web-застосунку.
Створюємо рішення Bookshelf з консольним застосунком BookshelfApp
. До рішення додаємо новий
проєкт – бібліотеку класів. Додаємо новий проєкт (Add | New
Project...) – Class Library. Вводимо ім'я бібліотеки: BookshelfLib
. Створений
файл (Class1.cs
)
слід перейменувати у Publications.cs
. Це слід зробити за допомогою вікна Properties
(Властивості), обравши попередньо файл Class1.cs
у вікні Solution Explorer.
Для більш зручної роботи з бібліотекою окремі групи класів, пов'язані з різними функціями (представлення сутностей, отримання подання у вигляді рядків, пошук і сортування, робота з текстовим файлом) можна розташувати в окремих файлах.
У новій версії програми зберігання авторів у книжках і публікацій на полиці здійснюватиметься у списках замість масивів.
Це спрощує деякі дії, наприклад, пов'язані з додаванням і видаленням елементів. До всіх класів додаємо перевантажені
методи З урахуванням подальшої XML-серіалізації до раніше спроєктованих програмних сутностей треба додати деякі
атрибути, зокрема, перед властивостями для використання XML-атрибутів замість окремих тегів. Код файлу Publications.cs
буде
таким:
// Publications.cs using System.Xml.Serialization; namespace BookshelfLib { /// <summary> /// Представляє автора книги на книжковій полиці /// </summary> public class Author { [System.Xml.Serialization.XmlAttributeAttribute()] public string Name { get; set; } = ""; [System.Xml.Serialization.XmlAttributeAttribute()] public string Surname { get; set; } = ""; public Author() { } public Author(string name, string surname) { Name = name; Surname = surname; } /// <summary> /// Надає представлення рядком даних про автора /// </summary> /// <returns>рядок, який представляє автора книги</returns> public override string ToString() { return StringRepresentations.ToString(this); } /// <summary> /// Визначає, чи дорівнює вказаний об'єкт поточному об'єкту /// </summary> /// <param name="obj">Об'єкт для порівняння з поточним об'єктом</param> /// <returns>true, якщо вказаний об'єкт дорівнює поточному; в іншому випадку false.</returns> public override bool Equals(object? obj) { return obj is Author author && Name == author.Name && Surname == author.Surname; } /// <summary> /// Генерує хеш-код об'єкта /// </summary> /// <returns>хеш-код об'єкта</returns> public override int GetHashCode() { return HashCode.Combine(Name, Surname); } } /// <summary> /// Представляє окреме видання (книгу, журнал тощо) /// </summary> [XmlInclude(typeof(Book))] [XmlInclude(typeof(Magazine))] public abstract class Publication { [System.Xml.Serialization.XmlAttributeAttribute()] public string Title { get; set; } = ""; [System.Xml.Serialization.XmlAttributeAttribute()] public int Year { get; set; } /// <summary> /// Конвертує дані про публікацію в рядок текстового файлу /// </summary> /// <returns>рядок, готовий для запису в текстовий файл</returns> abstract public string ToFileData(); /// <summary> /// Створює об'єкт, дані про який зчитані з рядка текстового файлу /// </summary> /// <param name="data">рядок даних про публікацію, прочитаний з текстового файлу</param> /// <returns>Об'єкт, дані якого прочитані з рядка</returns> abstract public Publication FromFileData(string data); /// <summary> /// Визначає, чи дорівнює вказаний об'єкт поточному об'єкту /// </summary> /// <param name="obj">Об'єкт для порівняння з поточним об'єктом</param> /// <returns>true, якщо вказаний об'єкт дорівнює поточному; в іншому випадку false.</returns> public override bool Equals(object? obj) { return obj is Publication publication && Title == publication.Title && Year == publication.Year; } /// <summary> /// Генерує хеш-код об'єкта /// </summary> /// <returns>хеш-код об'єкта</returns> public override int GetHashCode() { return HashCode.Combine(Title, Year); } } /// <summary> /// Представляє книгу на книжковій полиці /// </summary> public class Book : Publication { public List<Author> Authors { get; set; } = new(); public Book() { } public Book(string title, int year) { Title = title; Year = year; } /// <summary> /// Надає представлення рядком даних про книжку /// </summary> /// <returns>рядок, який представляє дані про книгу</returns> public override string ToString() { return StringRepresentations.ToString(this); } /// <summary> /// Конвертує дані про книгу в рядок текстового файлу /// </summary> /// <returns>рядок, готовий для запису в текстовий файл</returns> public override string ToFileData() { return FileData.ToFileData(this); } /// <summary> /// Створює об'єкт, дані про який зчитані з рядка текстового файлу /// </summary> /// <param name="data">рядок даних про публікацію, прочитаний з текстового файлу</param> /// <returns>Об'єкт, дані якого прочитані з рядка</returns> public override Publication FromFileData(string data) { return FileData.BookFromFileData(data); } /// <summary> /// Створює й додає автора до масиву авторів /// </summary> /// <param name="name">ім'я автора</param> /// <param name="surname">прізвище автора</param> public void AddAuthor(string name, string surname) { Authors.Add(new Author(name, surname)); } /// <summary> /// Видаляє дані про автора /// </summary> /// <param name="author">автор, дані про якого треба знайти й видалити</param> public void RemoveAuthor(string name, string surname) { Authors.RemoveAll(author => author.Name == name && author.Surname == surname); } /// <summary> /// Визначає, чи дорівнює вказаний об'єкт поточному об'єкту /// </summary> /// <param name="obj">Об'єкт для порівняння з поточним об'єктом</param> /// <returns>true, якщо вказаний об'єкт дорівнює поточному; в іншому випадку false.</returns> public override bool Equals(object? obj) { return obj is Book book && base.Equals(book) && Enumerable.SequenceEqual(Authors, book.Authors); } /// <summary> /// Генерує хеш-код об'єкта /// </summary> /// <returns>хеш-код об'єкта</returns> public override int GetHashCode() { return HashCode.Combine(Title, Year, Authors); } } /// <summary> /// Представляє журнал на полиці /// </summary> public class Magazine : Publication { [System.Xml.Serialization.XmlAttributeAttribute()] public int Volume { get; set; } [System.Xml.Serialization.XmlAttributeAttribute()] public int Number { get; set; } /// <summary> /// Надає представлення рядком даних про журнал /// </summary> /// <returns>рядок, який представляє дані про журнал</returns> public override string ToString() { return StringRepresentations.ToString(this); } /// <summary> /// Конвертує дані про журнал в рядок текстового файлу /// </summary> /// <returns>рядок, готовий для запису в текстовий файл</returns> public override string ToFileData() { return FileData.ToFileData(this); } /// <summary> /// Створює об'єкт, дані про який зчитані з рядка текстового файлу /// </summary> /// <param name="data">рядок даних про публікацію, прочитаний з текстового файлу</param> /// <returns>Об'єкт, дані якого прочитані з рядка</returns> public override Publication FromFileData(string data) { return FileData.MagazineFromFileData(data); } /// <summary> /// Визначає, чи дорівнює вказаний об'єкт поточному об'єкту /// </summary> /// <param name="obj">Об'єкт для порівняння з поточним об'єктом</param> /// <returns>true, якщо вказаний об'єкт дорівнює поточному; в іншому випадку false.</returns> public override bool Equals(object? obj) { return obj is Magazine magazine && base.Equals(magazine) && Volume == magazine.Volume && Number == magazine.Number; } /// <summary> /// Генерує хеш-код об'єкта /// </summary> /// <returns>хеш-код об'єкта</returns> public override int GetHashCode() { return HashCode.Combine(Title, Year, Volume, Number); } } /// <summary> /// Книжкова полиця /// </summary> public class Bookshelf { /// <summary> /// Список посилань на публікації /// </summary> public List<Publication> Publications { get; set; } = new(); /// <summary> /// Індексатор, який дозволяє отримувати видання за індексом /// </summary> /// <param name="index">індекс видання</param> /// <returns>видання з відповідним індексом</returns> public Publication this[int index] { get => Publications[index]; set => Publications[index] = value; } /// <summary> /// Конструктор /// </summary> public Bookshelf() { } /// <summary> /// Конструктор /// </summary> /// <param name="publications">відкритий масив видань</param> public Bookshelf(params Publication[] publications) { Publications.AddRange(publications); } /// <summary> /// Додає нову публікацію до книжкової полиці /// </summary> /// <param name="publication">публікація, яка додається до книжкової полиці</param> public void AddPublication(Publication publication) { Publications.Add(publication); } /// <summary> /// Видаляє публікацію зі вказаною назвою /// </summary> /// <param name="title">назва публікації, яку треба знайти й видалити</param> public void Remove(string title) { Publications.RemoveAll(publication => publication.Title == title); } /// <summary> /// Надає представлення рядком даних про книжкову полицю /// </summary> /// <returns>рядок, який представляє дані про книжкову полицю</returns> public override string ToString() { return StringRepresentations.ToString(this); } /// <summary> /// Визначає, чи дорівнює вказаний об'єкт поточному об'єкту /// </summary> /// <param name="obj">Об'єкт для порівняння з поточним об'єктом</param> /// <returns>true, якщо вказаний об'єкт дорівнює поточному; в іншому випадку false.</returns> public override bool Equals(object? obj) { return obj is Bookshelf bookshelf && Enumerable.SequenceEqual(Publications, bookshelf.Publications); } /// <summary> /// Генерує хеш-код об'єкта /// </summary> /// <returns>хеш-код об'єкта</returns> public override int GetHashCode() { return Publications.GetHashCode(); } } }
Як видно з наведеного коду, в окремих атрибутах перелічені можливі похідні типи класу Publication
.
На жаль, таке перелічення є порушенням принципу інверсії залежностей (Dependency Inversion Principle), але це є
вимогою технології XML-серіалізації.
У файлі ToString.cs
ми розташуємо клас StringRepresentations
з методами отримання представлення різних сутностей
рядками:
// ToString.cs using System.Text; namespace BookshelfLib { /// <summary> /// Статичний клас, який дозволяє отримувати представлення /// у вигляді рядків різних об'єктів застосунку /// </summary> public static class StringRepresentations { /// <summary> /// Надає представлення рядком даних про автора /// </summary> /// <param name="author">автор книги</param> /// <returns>рядок, який представляє автора книги</returns> public static string ToString(Author author) { return author.Name + " " + author.Surname; } /// <summary> /// Надає представлення рядком даних про книжку /// </summary> /// <param name="book">книга</param> /// <returns>рядок, який представляє дані про книгу</returns> public static string ToString(Book book) { if (book == null) { return ""; } string result = string.Format("Книга. Назва: \"{0}\". Рiк видання: {1}", book.Title, book.Year); result += " Автор(и):\n"; for (int i = 0; i < book.Authors.Count; i++) { result += string.Format(" {0}", book.Authors[i]); result += (i < book.Authors.Count - 1 ? "," : "") + "\n"; } return result; } /// <summary> /// Надає представлення рядком даних про журнал /// </summary> /// <param name="magazine">журнал</param> /// <returns>рядок, який представляє дані про журнал</returns> public static string ToString(Magazine magazine) { if (magazine == null) { return ""; } return string.Format("Журнал. Назва: \"{0}\". Рiк видання: {1}. Том: {2}. Номер: {3}", magazine.Title, magazine.Year, magazine.Volume, magazine.Number); } /// <summary> /// Надає представлення рядком даних про книжкову полицю /// </summary> /// <param name="bookshelf">книжкова полиця</param> /// <returns>рядок, який представляє дані про книжкову полицю</returns> public static string ToString(Bookshelf bookshelf) { StringBuilder result = new (""); foreach (Publication publication in bookshelf.Publications) { result.Append(publication + "\n"); } return result.ToString(); } } }
Файл BookshelfProcessor.cs
міститиме клас BookshelfProcessor
з методами, які здійснюють пошук і сортування:
// BookshelfProcessor.cs namespace BookshelfLib { /// <summary> /// Надає методи для пошуку і сортування видань на полиці /// </summary> public class BookshelfProcessor { /// <summary> /// Шукає визначену послідовність символів у назвах видань /// </summary> /// <param name="bookshelf">книжкова полиця</param> /// <param name="characters">послідовність символів, яку треба відшукати</param> /// <returns>масив видань, у назви яких входить визначена послідовність</returns> public static List<Publication> ContainsCharacters(Bookshelf bookshelf, string characters) { return bookshelf.Publications.FindAll(publication => publication.Title.Contains(characters)); } /// <summary> /// Здійснює сортування видань за алфавітом назв без урахування регістру /// </summary> /// <param name="bookshelf">книжкова полиця</param> public static void SortByTitles(Bookshelf bookshelf) { bookshelf.Publications.Sort((b1, b2) => string.Compare(b1.Title.ToUpper(), b2.Title.ToUpper())); } } }
Створені раніше класи, які забезпечують читання з текстових файлів і запис у текстові файли, розташуємо у файлі
TxtFiles.cs
:
// TxtFiles.cs using System.Text; /// <summary> /// Простір імен, який охоплює класи для представлення книжкової полиці /// </summary> namespace BookshelfLib { /// <summary> /// Надає методи для конвертування даних об'єктів в рядки текстового файлу й навпаки /// </summary> public static class FileData { /// <summary> /// Конвертує дані про книгу в рядок текстового файлу /// </summary> /// <param name="book">книга, дані про яку конвертуються в рядок</param> /// <returns>рядок, готовий для запису в текстовий файл</returns> public static string ToFileData(Book book) { string s = string.Format("{0}\t{1}\t{2}", book.GetType().ToString(), book.Title, book.Year); StringBuilder sb = new(s); foreach (Author author in book.Authors) { sb.Append("\t" + author); } return sb.ToString(); } /// <summary> /// Конвертує дані про журнал в рядок текстового файлу /// </summary> /// <param name="magazine">журнал, дані про який конвертуються в рядок</param> /// <returns>рядок, готовий для запису в текстовий файл</returns> public static string ToFileData(Magazine magazine) { return string.Format("{0}\t{1}\t{2}\t{3}\t{4}", magazine.GetType().ToString(), magazine.Title, magazine.Year, magazine.Volume, magazine.Number); } /// <summary> /// Створює об'єкт, дані про який зчитані з рядка текстового файлу /// </summary> /// <param name="data">рядок даних про книгу, прочитаний з текстового файлу</param> /// /// <returns>Об'єкт, дані якого прочитані з рядка</returns> public static Book BookFromFileData(string data) { string[] parts = data.Split('\t'); Book book = new(title: parts[1], year: int.Parse(parts[2])); foreach (string author in parts[3..^0]) { string[] authorData = author.Split(' '); book.AddAuthor(name: authorData[0], surname: authorData[1]); } return book; } /// <summary> /// Створює об'єкт, дані про який зчитані з рядка текстового файлу /// </summary> /// <param name="data">рядок даних про журнал, прочитаний з текстового файлу</param> /// /// <returns>Об'єкт, дані якого прочитані з рядка</returns> public static Magazine MagazineFromFileData(string data) { string[] parts = data.Split('\t'); Magazine magazine = new() { Title = parts[1], Year = int.Parse(parts[2]), Volume = int.Parse(parts[3]), Number = int.Parse(parts[4]) }; return magazine; } } /// <summary> /// Надає методи для читання з файлу й запису в файл /// </summary> public static class FileUtils { /// <summary> /// Записує дані про книжкову полицю в текстовий файл /// </summary> /// <param name="bookshelf">посилання на книжкову полицю</param> /// <param name="fileName">ім'я файлу</param> public static void WriteToFile(Bookshelf bookshelf, string fileName) { using StreamWriter writer = new(fileName); foreach (Publication publication in bookshelf.Publications) { writer.WriteLine(publication.ToFileData()); } } /// <summary> /// Читає дані про книжкову полицю з текстового файлу /// </summary> /// <param name="fileName">ім'я файлу</param> /// <returns>Об'єкт - книжкова полиця</returns> public static Bookshelf ReadFromFile(string fileName) { System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly(); Bookshelf bookshelf = new(); using StreamReader reader = new(fileName); string? s; while ((s = reader.ReadLine()) != null) { string typeName = s.Split()[0]; if (assembly.CreateInstance(typeName) is Publication publication) { bookshelf.AddPublication(publication.FromFileData(s)); } } return bookshelf; } } }
Окремий файл створюємо для класу XMLHandle
(XMLHandle.cs
). Методи цього класу забезпечуватимуть
серіалізацію і десеріалізацію даних про книжкову полицю:
// XMLHandle.cs using System.Xml.Serialization; namespace BookshelfLib { /// <summary> /// Надає методи для читання з XML-документу й запису в XML-документ /// </summary> public static class XMLHandle { /// <summary> /// Читає дані про публікації за допомогою механізму серіалізації /// </summary> /// <param name="fileName">ім'я файлу</param> /// <returns>посилання на нову книжкову полицю</returns> public static Bookshelf ReadFromFile(string fileName) { XmlSerializer deserializer = new(typeof(Bookshelf)); using TextReader textReader = new StreamReader(fileName); var data = deserializer.Deserialize(textReader); if (data == null) { return new(); } Bookshelf bookshelf = data as Bookshelf ?? new (); return bookshelf; } /// <summary> /// Записує публікації за допомогою механізму серіалізації /// </summary> /// <param name="bookshelf">посилання на книжкову полицю</param> /// <param name="fileName">ім'я файлу</param> public static void WriteToFile(Bookshelf bookshelf, string fileName) { XmlSerializer serializer = new(typeof(Bookshelf)); using TextWriter textWriter = new StreamWriter(fileName); serializer.Serialize(textWriter, bookshelf); } } }
Тепер до консольного застосунку (проєкт BookshelfApp
) через контекстне меню додаємо посилання (Add
| Project Reference)
на попередньо створену бібліотеку. Слід здійснити перевірку роботи всіх функцій. Код файлу Program.cs
буде
таким:
// Program.cs namespace BookshelfApp; using BookshelfLib; /// <summary> /// Консольний застосунок для демонстрації роботи з виданнями на книжковій полиці /// </summary> class Program { /// <summary> /// Готує тестові дані для демонстрації роботи з виданнями на книжковій полиці /// </summary> /// <returns>Книжкова полиця з доданими виданнями</returns> public static Bookshelf CreateBookshelf() { return new Bookshelf( new Book(@"The UML User Guide", 1999) { Authors = new() { new Author("Grady", "Booch"), new Author("James", "Rumbaugh"), new Author("Ivar", "Jacobson") } }, new Book(@"Об'єктно-орієнтоване моделювання програмних систем", 2007) { Authors = new() { new Author("Iгор", "Дудзяний") } }, new Book(@"Thinking in Java", 2005) { Authors = new() { new Author("Bruce", "Eckel") } }, new Book(@"Програмування мовою С# 7.0: навчальний посібник", 2017) { Authors = new() { new Author("Ігор", "Коноваленко"), new Author("Павло", "Марущак"), new Author("Володимир", "Савків") } }, new Book(@"C# 9.0 in a Nutshell: The Definitive Reference", 2021) { Authors = new() { new Author("Joseph", "Albahari") } }, new Magazine() { Title = @"The Journal of Object Technology", Year = 2024, Volume = 23, Number = 3 } ); } /// <summary> /// Демонструє роботу функцій пошуку та сортування видань /// </summary> /// <param name="bookshelf">книжкова полиця, для якої здійснюється демонстрація роботи</param> public static void HandleBookshelf(Bookshelf bookshelf) { Console.WriteLine("\nПочатковий стан:"); Console.WriteLine(bookshelf); Console.WriteLine("\nНазви, які містять \"The\""); var result = BookshelfProcessor.ContainsCharacters(bookshelf, "The"); foreach (var publication in result) { Console.WriteLine(publication.Title); } //Console.WriteLine(result.ToArray().Length > 0 ? string.Join("\n", result) : "No"); Console.WriteLine("\nЗа алфавітом без урахування регістру:"); BookshelfProcessor.SortByTitles(bookshelf); Console.WriteLine(bookshelf); } /// <summary> /// Виводить дані про виняток /// </summary> /// <param name="ex">виняток, для якого виводяться дані</param> internal static void ShowException(Exception ex) { Console.WriteLine("------------Виняток:------------"); Console.WriteLine(ex.GetType()); Console.WriteLine("-------------Зміст:-------------"); Console.WriteLine(ex.Message); Console.WriteLine("--------Трасування стеку:-------"); Console.WriteLine(ex.StackTrace); } /// <summary> /// Демонструє роботу з файлом, а також методи додавання й видалення авторів і публікацій /// </summary> /// <param name="fileName">ім'я файлу</param> public static void AdditionalProcessing(string fileName) { try { Console.WriteLine("Читаємо з файлу :" + fileName); Bookshelf bookshelf = FileUtils.ReadFromFile(fileName); Console.WriteLine(bookshelf); Console.WriteLine("Додаємо та видаляємо автора:"); if (bookshelf[0] is Book book) { book.AddAuthor("Elon", "Musk"); Console.WriteLine(bookshelf[0]); book.RemoveAuthor("Elon", "Musk"); Console.WriteLine(bookshelf[0]); } Console.WriteLine("Видаляємо книжку про Java"); bookshelf.Remove("Thinking in Java"); Console.WriteLine(bookshelf); } catch (IOException ex) { Console.WriteLine("Помилка читання з файлу " + fileName); ShowException(ex); } } /// <summary> /// Демонструє роботу з XML-документом /// </summary> /// <param name="fileName">ім'я файлу</param> /// <returns></returns> public static void XMLProcessing(Bookshelf bookshelf, string fileName) { try { XMLHandle.WriteToFile(bookshelf, "publications.xml"); Console.WriteLine("Читаємо з XML-файлу :" + fileName); bookshelf = XMLHandle.ReadFromFile(fileName); Console.WriteLine(bookshelf); } catch (IOException ex) { Console.WriteLine("Помилка роботи з файлом " + fileName); ShowException(ex); } catch (InvalidOperationException ex) { Console.WriteLine("Помилка роботи з XML"); ShowException(ex); } } /// <summary> /// Стартова точка консольного застосунку /// </summary> static void Main() { Console.OutputEncoding = System.Text.Encoding.UTF8; Bookshelf bookshelf = CreateBookshelf(); HandleBookshelf(bookshelf); FileUtils.WriteToFile(bookshelf, "publications.txt"); AdditionalProcessing("books.txt"); // Немає такого файлу AdditionalProcessing("publications.txt"); XMLProcessing(bookshelf, "publications.xml"); } }
Проєкт BookshelfApp
слід встановити як стартовий (позиція Set as Startup Project контекстного меню).
Після завершення програми у теці bin\Debug або bin\Release проєкту (залежно від способу
завантаження програми на виконання) явиться XML-документ publications.xml
, який
матиме такий вміст:
<?xml version="1.0" encoding="utf-8"?> <Bookshelf xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Publications> <Publication xsi:type="Book" Title="C# 9.0 in a Nutshell: The Definitive Reference" Year="2021"> <Authors> <Author Name="Joseph" Surname="Albahari" /> </Authors> </Publication> <Publication xsi:type="Magazine" Title="The Journal of Object Technology" Year="2024" Volume="23" Number="3" /> <Publication xsi:type="Book" Title="The UML User Guide" Year="1999"> <Authors> <Author Name="Grady" Surname="Booch" /> <Author Name="James" Surname="Rumbaugh" /> <Author Name="Ivar" Surname="Jacobson" /> </Authors> </Publication> <Publication xsi:type="Book" Title="Thinking in Java" Year="2005"> <Authors> <Author Name="Bruce" Surname="Eckel" /> </Authors> </Publication> <Publication xsi:type="Book" Title="Об'єктно-орієнтоване моделювання програмних систем" Year="2007"> <Authors> <Author Name="Iгор" Surname="Дудзяний" /> </Authors> </Publication> <Publication xsi:type="Book" Title="Програмування мовою С# 7.0: навчальний посібник" Year="2017"> <Authors> <Author Name="Ігор" Surname="Коноваленко" /> <Author Name="Павло" Surname="Марущак" /> <Author Name="Володимир" Surname="Савків" /> </Authors> </Publication> </Publications> </Bookshelf>
4 Вправи для контролю
- Описати класи Студент та Академічна група (з полем – масивом студентів). Створити об'єкти, здійснити їх серіалізацію в XML-документ та десеріалізацію.
- Описати класи Автор та Книга. Створити об'єкти, здійснити їх серіалізацію в XML-документ та десеріалізацію.
- Описати класи Країна та Столиця. Створити об'єкти, здійснити їх серіалізацію в XML-документ та десеріалізацію.
- Реалізувати статичну узагальнену функцію копіювання першого елемента масиву в останню позицію. Здійснити тестування функції на двох масивах різних типів.
- Реалізувати статичну узагальнену функцію видалення елементів масиву з непарними індексами. Здійснити тестування функції на двох масивах різних типів.
- Реалізувати статичну узагальнену функцію заміни порядку елементів списку на протилежний. Здійснити тестування функції на двох списках різних типів.
- Реалізувати статичну узагальнену функцію обміну місцями елемента масиву з індексом 0 та останнього елемента. Здійснити тестування функції на двох масивах різних типів.
- Реалізувати статичну узагальнену функцію обміну місцями елемента списку з індексом 0 та останнього елемента. Здійснити тестування функції на двох списках різних типів.
- Реалізувати статичну узагальнену функцію обміну місцями елемента списку з індексом 1 та передостаннього елемента. Здійснити тестування функції на двох списках різних типів.
- Реалізувати статичну узагальнену функцію визначення кількості разів входження певного елемента у список. Здійснити тестування функції на двох списках різних типів.
- Реалізувати статичну узагальнену функцію циклічного зсуву списку на задану кількість елементів. Здійснити тестування функції на двох списках різних типів.
- Реалізувати статичну узагальнену функцію пошуку індексу елемента, починаючи з якого деякий список повністю входить в інший. Здійснити тестування функції на двох списках різних типів.
- Увести кількість елементів майбутньої множини дійсних чисел та діапазон чисел. Сформувати цю множину з випадкових значень. Вивести елементи множини за зменшенням.
- Заповнити множину цілих випадковими додатними парними значеннями (не більше визначеного числа). Вивести результат.
- Увести слово та вивести всі різні літери слова в алфавітному порядку.
- Увести речення та обчислити кількість різних літер, з яких речення складається. Не враховувати пропусків та розділових знаків.
- Увести речення та обчислити кількість різних слів у реченні.
- Увести речення та вивести всі різні слова речення в алфавітному порядку.
- Представити дані про користувачів у вигляді асоціативного масиву (ім'я / пароль) з припущенням, що всі імена користувачів різні. Вивести дані про користувачів з довжиною пароля понад 6 символів.
- Створити клас "Речення" з ітератором, який переміщується окремими словами.
- Створити клас "Число" з ітератором, який переміщується по окремих цифрах.
5 Контрольні запитання
- Що таке узагальнене програмування?
- Коли доцільно створювати узагальнені типи?
- Які синтаксичні елементи можуть бути узагальненими?
- Чим відрізняється використання узагальнень у C# та шаблонів C++?
- Як визначити обмеження на тип параметра?
- Які контейнерні класи реалізовані у .NET та яке їх призначення?
- Як з масиву отримати список?
- Коли доцільніше використовувати
List
у порівнянні зLinkedList
? - Коли доцільніше використовувати
LinkedList
у порівнянні зList
? - Як здійснюється доступ до окремих елементів списку?
- Як здійснюється сортування списків?
- Чим множина відрізняється від списку?
- Яким вимогам повинен задовольняти об'єкт, щоб список або масив таких об'єктів можна було сортувати без визначення ознаки сортування?
- Чим множина відрізняється від асоціативного масиву?
- Наведіть приклади використання асоціативних масивів.
- У чому відмінності класів
Dictionary
іSortedDictionary
? - Коли необхідно створювати власні контейнери?
- Які елементи слід реалізувати під час створення власного контейнера?
- Що таке ітератор?
- Для чого використовують ключове слово
yield
? - Чим відрізняється визначення
відvar dynamic
? - Як реалізувати узагальнений підхід за допомогою типу
dynamic
? - У чому полягають особливості об'єктної моделі документа в порівнянні з подіє-орієнтованою моделлю документа?
- Що таке серіалізація і для чого її використовують?
- У чому є переваги та недоліки XML-серіалізації?
- Як керувати способом зберігання даних під час XML-серіалізації?
- Що таке бібліотеки і як вони можуть бути використані у програмуванні?
- Що таке динамічна бібліотека?
- Як підключити бібліотеку класів?
- Як створити бібліотеку класів?