Лабораторна робота 3

Успадкування та поліморфізм

1 Завдання на лабораторну роботу

1.1 Індивідуальне завдання

Розширити програму, яка була створена у попередній лабораторній роботі, ієрархією сутностей, які зберігаються у масиві, відповідно до наведеної таблиці:

Базовий клас Похідний клас Похідний клас
Студент Бюджетний студент Контрактний студент
Навчальний предмет Предмет з іспитом Предмет із заліком
Частина міста Район міста Історична частина міста
Населенний пункт Місто Село
Учасник спортивної секції Учасник Керівник
Учасник футбольного клубу Гравець Тренер
Учасник музичного гурту Учасник Керівник
Збірка пісень Альбом Збірка нот
Музичний твір Пісня Інструментальний твір
Приміщення Кімната Службове приміщення
Твір Оповідання Повість
Твір Живопис Графіка
Об'єкт метрополітену Станція Депо
Залізничний об'єкт Станція Зупинка
Твір Роман П'єса

Для всіх класів також необхідно перекрити метод ToString().

Передбачити методи додавання й видалення сутності з масиву.

Реалізувати функції збереження даних у текстовому файлі та завантаження даних текстового файлу. Відтворити завдання попередніх лабораторних робіт.

Передбачити перехоплення можливих винятків.

1.2 Корені рівняння

Реалізувати програму, що дозволяє знайти усі корені деякого рівняння на заданому інтервалі. Алгоритм знаходження коренів полягає в послідовному переборі з певним кроком точок інтервалу, знаходженні інтервалів, на яких функція змінює знак і виведенні середніх арифметичних початків та кінців інтервалів – коренів рівняння.

Реалізувати два підходи – через використання абстрактних класів і через використання інтерфейсів.

1.3 Робота з текстовими файлами

Розробити програму, яка здійснює копіювання з одного файлу в інший файл рядків, довжина яких не перевищує введеного значення.

2 Методичні вказівки

2.1 Успадкування

Успадкування – це процес створення похідних класів від базових. Об'єкти похідних класів неявно містять усі поля базового класу, включаючи закриті, не зважаючи на те, що методи похідного класу не мають доступу до закритих елементів базового. Крім того, успадковуються всі відкриті властивості та методи. Елементи базового класу з модифікатором protected (захищені) доступні з похідних класів.

Завдяки успадкуванню та створенню ієрархій класів можна істотно скоротити кількість фрагментів присутнього в різних класах схожого коду, що було б порушенням одного з фундаментальних принципів програмування, принципу DRY ("Don't Repeat Yourself"). Окрім того, успадкування дозволяє реалізувати один із принципів SOLID – Принцип відкритості/закритості (Open / Closed Principle, OCP), згідно з яким класи повинні бути відкритими для розширення, але закритими для змін.

На відміну від C++, C# дозволяє тільки одиничне успадкування класів. Успадкування завжди відкрите (похідний клас не може обмежити доступ до відкритих елементів базового класу). Успадкування має такий синтаксис:

class Rectangle : Shape 
{
    // тіло класу
}

Усі класи безпосередньо чи опосередковано походять від класу System.Object.

Конструктори не успадковуються – для кожного класу ієрархії повинен бути реалізований свій власний конструктор. Перед виконанням конструктора похідного класу викликається конструктор базового класу і конструктори об'єктів, які створюються під час опису класу.

Ключове слово base використовують для доступу до елементів базового класу з похідного класу, зокрема:

  • для виклику перекритого методу базового класу;
  • для передачі параметрів конструктора базового класу.

Наприклад:

class BaseClass
{
    int i, j;

    public BaseClass(int i, int j) 
    {
        this.i = i;
        this.j = j;
    }
}

class DerivedClass : BaseClass 
{
    int k;

public DerivedClass() : base(0, 0)
{
k = 0;
} public DerivedClass(int i, int j, int k) : base(i, j) { this.k = k; } }

Класи можуть бути визначені з модифікатором sealed. Такі класи не можуть використовуватися як базові. Методи з модифікатором sealed не можуть бути перекриті.

Як і в інших мовах об'єктно-орієнтованого програмування, існує неявне приведення типу посилання на похідний клас до посилання на базовий, але не навпаки.

BaseClass b = new DerivedClass();
DerivedClass d = new DerivedClass();
b = d; // ОК
d = b; // Помилка компіляції!

Для того, щоб привести посилання на базовий клас до посилання на похідний клас, може бути використане явне перетворення. У випадку, якщо таке перетворення неможливе, генерується виняткова ситуація System.InvalidCastException. Працювати з винятковими ситуаціями не завжди зручно. Існує спеціальна форма приведення типів з перевіркою можливості такого приведення – операція as:

BaseClass b1 = new DerivedClass();
BaseClass b2 = new BaseClass();
DerivedClass d1 = b1 as DerivedClass;   // OK
DerivedClass d2 = b2 as DerivedClass; // null

Операція is повертає true, якщо перетворення можливе, і false у протилежному випадку:

BaseClass b1 = new DerivedClass();
BaseClass b2 = new BaseClass();
if (b1 is DerivedClass)
{
    DerivedClass d1 = (DerivedClass) b1; // OK
}
if (b2 is DerivedClass)
{
    DerivedClass d2 = (DerivedClass) b2; // не виконується
}

Конструкції as та is запозичені з Delphi Pascal.

З успадкуванням пов'язаний один із принципів SOLID – принцип заміщення Лісков (Liskov substitution principle, LSP), згідно з яким об'єкти можуть бути заміненими їхніми нащадками без зміни коду. Під час успадкування не слід обмежувати функціональність базових класів. Наприклад, клас "Квадрат" не слід створювати як похідний від класу "Прямокутник", оскільки фактично це звуження типу, а не його розширення. Там, де у програмі потрібен будь-який прямокутник, не можна застосувати квадрат, оскільки не можна окремо змінювати висоту й ширину такого прямокутника. Більш коректні похідні класи від класу "Прямокутник" – "Зафарбований прямокутник", "Прямокутник із закругленими кутами", "Прямокутник з текстом" тощо.

З принципом LSP не узгоджується закрите та захищене успадкування – різновиди успадкування, введені в C++ і відсутні в інших мовах об'єктно-орієнтованого програмування.

2.2 Поліморфізм. Інтерфейси

2.2.1 Поліморфізм часу виконання

Поліморфізм часу виконання – це властивість класів, згідно з якою поведінка об'єктів класу може визначатися не на етапі компіляції, а на етапі виконання. Поняття поліморфізму та поліморфних класів загалом спільні в усіх мовах об'єктно-орієнтованого програмування, зокрема, в Java та C#.

Усі класи C# є поліморфними, оскільки вони походять від поліморфного класу System.Object. Як і в інших мовах об'єктно-орієнтованого програмування, поліморфізм у C# реалізований через механізм віртуальних методів. Як і в C++, перед заголовком віртуального методу необхідно вказати модифікатор virtual у базовому класі і модифікатори override у похідних класах. Якщо ми хочемо перекрити віртуальний метод, обірвавши ланцюжок поліморфізму, необхідно використовувати ключове слово new:

class Shape
{
    public virtual void Draw()
    {
        . . .
    }
}

class Circle : Shape 
{
    public override void Draw()
    {
        . . .
    }
}

class Rectangle : Shape 
{
    public new void Draw()
    {
        . . .
    }
}

Найчастіше виникає необхідність у перекритті віртуальних методів класу System.Object. Наприклад, для того, щоб отримати представлення об'єкта у вигляді рядка, необхідно для класу визначити метод ToString(), який повертає необхідний рядок. Таке представлення можна вживати з будь-якою метою, наприклад, виводити усі дані про об'єкт за допомогою функції Console.WriteLine():

  class MyClass
  {
      int k;
      double x;

      public MyClass(int k, double x)
      {
          this.k = k;
          this.x = x;
      }

      public override string ToString()
      {
          return k + " " + x;
      }

      static void Main(string[] args)
      {
          MyClass mc = new MyClass(1, 2.5);
          Console.WriteLine(mc);
      }
  }    

2.2.2 Абстрактні класи

Іноді класи створюють для представлення абстрактних концепцій, а не для створення екземплярів. Такі концепції можуть бути представлені абстрактними класами. У C# для цього використовується ключове слово abstract перед визначенням класу.

abstract class SomeConcept 
{
    . . .
}

Абстрактний клас може містити абстрактні методи, такі, для яких не приводиться реалізація. Такі методи не мають тіла функції. Їхнє оголошення аналогічне оголошенню функцій-елементів у С++, але оголошенню повинне передувати ключове слово abstract. У такому випадку мається на увазі, що метод – віртуальний, але вказувати слово virtual не можна. Методи, що перевантажують абстрактний метод, у похідних класах повинні мати модифікатор override.

Наприклад, абстрактний клас Shape (геометрична фігура) реалізує поля і методи, що можуть бути використані різними похідними класами. До таких полів можна, наприклад, віднести поточну позицію і метод переміщення екраном MoveTo(). У класі Shape також оголошені абстрактні методи, такі як Draw(), що повинні бути реалізовані у всіх похідних класах, але по-різному. Усталена реалізація не має сенсу. Наприклад:

abstract class Shape 
{
    int x, y;
    . . .
    public void MoveTo(int newX, int newY) 
    {
        . . .
        Draw();
    }
    public abstract void Draw();
}

Конкретні класи, створені від Shape, такі як Circle чи Rectangle, визначають реалізацію методу Draw().

class Circle : Shape 
{
    public override void Draw()
    {
        . . .
    }
}

class Rectangle : Shape 
{
    public override void Draw()
    {
        . . .
    }
}

Від абстрактного класу не вимагають обов'язкову наявність абстрактних методів. Але кожен клас, у якому є хоч один абстрактний метод, чи хоча б один абстрактний метод базового класу не був визначений, повинен бути оголошений як абстрактний (з використанням ключового слова abstract).

Абстрактні класи можуть містити абстрактні властивості, для таких властивостей не задається код доступу, а тільки позначається необхідність завдання такого коду в нащадках. Наприклад:

abstract class Shape
{
    public abstract double Area 
    {
        get;
    }
}

...

class Rectangle : Shape 
{
    double width, height;
    public Rectangle(double width, double height)
    { 
        this.width = width;
        this.height = height;
    }
    public override double Area   
    {
        get    
        {
            return width * height;
        }  
    }
}

Примітка: не слід плутати абстрактні властивості з автоматичними, які мають схожий синтаксис.

Проєктуючи ієрархії класів, слід дотримуватися принципу інверсії залежностей (Dependency Inversion Principle, DIP), одного з принципів SOLID.

  • Модулі верхнього рівня не мають залежати від модулів нижнього рівня.
  • Абстракції не мають залежати від деталей, деталі повинні залежати від абстракцій.

Під час проєктування базових типів не слід враховувати специфіку похідних типів, тим більше не обмежувати набір можливих похідних сутностей, фактично додаючи небажані залежності. Винятком з цього правила може бути створення спеціальних кооперацій класів в межах одного модуля (складання), коли використання базових типів за межами модуля не передбачено. Тоді фактично базові й похідні типи знаходяться на одному рівні абстракції.

2.2.3 Інтерфейси

У C# використовується поняття інтерфейсів. Інтерфейс схожий на абстрактний клас, що містить методи та властивості. Ці елементи усталено абстрактні (якщо не наведено їхньої реалізації):

interface Int1 {
    void F();
    int G(int x);
    int P { get; set; }
}

Насправді інтерфейс не є типом даних і фактично тільки декларує певну поведінку, яку повинен забезпечити клас, який реалізує інтерфейс.

Кожен клас може бути створений тільки від одного базового класу, але при цьому реалізовувати один чи кілька інтерфейсів. Клас, що реалізує інтерфейс, повинен забезпечити реалізацію всіх методів, оголошених в інтерфейсі. У іншому разі такий клас буде абстрактним і має бути оголошений зі специфікатором abstract.

Інтерфейси, що реалізуються класом, перелічуються через кому там же, де вказується базовий клас. Базовий клас повинен завжди бути першим у цьому списку. Для того, щоб розрізняти базові класи від інтерфейсів, імена інтерфейсів рекомендується починати з великої літери I.

Методи та властивості, визначені в інтерфейсі, є усталено абстрактними й відкритими (цього явно не пишуть). У класі, який реалізує інтерфейс, такі методи повинні бути оголошені як public:

interface ISomeFuncs {
    void F();
    int G(int x);
}

class SomeClass : ISomeFuncs {
    public void F()
    {

    }

    public int G(int x) 
    {
        return x;
    }
}

Клас може реалізувати кілька інтерфейсів:

interface IFirst 
{
    void F();
    int G(int x);
}

interface ISecond
{
    void H(int z);
}

class AnotherClass : IFirst, ISecond 
{
    public void F() 
    {
  
    }

    public int G(int x) 
    {
        return x;
    }

    public void H(int z) 
    {
  
    }
}

Можна створювати похідні інтерфейси. До інтерфейсів можна застосовувати множинне успадкування:

interface IFirst 
{
    void F();
    int G(int x);
}

interface ISecond
{
    void H(int z);
}

interface IDerived : IFirst, ISecond
{

}

Інтерфейс може містити оголошення властивостей. Також можна визначити константи в інтерфейсах.

В С# існує додаткова можливість визначення явної реалізації інтерфейсів, що запобігає конфліктам імен у класах, які реалізують декілька інтерфейсів. У класі, який реалізує інтерфейс, ім'я функції, визначеної в інтерфейсі, складається з двох частин.

тип Ім'я_інтерфейсу.ім'я_методу() 
{ 
    ...// реалізація
}

Для таких методів не вказується рівень доступу. Методи класу, які явно реалізують інтерфейс, можуть викликатися тільки через ім'я посилання на інтерфейс, а не на клас, який його реалізує.

Починаючи C# 8 можна визначати усталену реалізацію методів інтерфейсу. Мета усталеної реалізації – додавати нові методи до раніше визначених інтерфейсів без модифікації класів, що реалізували попередні версії інтерфейсів. Метод можна реалізувати без будь-яких модифікаторів. Також можна визначити статичні методи:

public interface IGreetings
{
    void Hello()
    {
        Console.WriteLine("Hello world!");
    }
    static void HelloStatic()
    {
        Console.WriteLine("Hello as well!");
    }
}

Нестатичні методи можна викликати лише через посилання на інтерфейс (не через посилання на клас, який реалізує інтерфейс):

class Greetings : IGreetings
{
    public void TestHello()
    {
        IGreetings greetings = this;
        greetings.Hello();
    }
}

class Program
{
    static void Main(string[] args)
    {
        new Greetings().TestHello();
    }
}

Статичні методи можна викликати через ім'я інтерфейсу:

class Program
{
    static void Main(string[] args)
    {
        IGreetings.HelloStatic();
    }
}

Методи з усталеною реалізацією можна перевизначити в похідних інтерфейсах. Такі методи також можна оголосити як абстрактні:

public interface IMyGreetings : IGreetings
{
    void IGreetings.Hello()
    {
        Console.WriteLine("Hello to me!");
    }
}

public interface IAbstractGreetings : IGreetings
{
    abstract void IGreetings.Hello();
}

Тепер цей метод повинен бути реалізований у класах, що реалізують інтерфейс IAbstractGreetings.

Під час проєктування ієрархій типів є спокуса створювати великі "універсальні" інтерфейси, які описують майже всю функціональність складної системи. Проєктуючи такі інтерфейси слід подумати про труднощі, пов'язані з їхньою реалізацією. Крім того, як це провокує створення небажаних взаємних залежностей і зв'язків. Один із принципів SOLID – Принцип розділення інтерфейсу (Interface Segregation Principle, ISP) визначає доцільність створення великої кількості спеціалізованих інтерфейсів замість одного універсального, що підвищує гнучкість програмних компонентів, видаляє непотрібні залежності та спрощує реалізацію.

2.2.4 Використання стандартних інтерфейсів

Існує велика кількість стандартних інтерфейсів. Деякі з них інтегровані в синтаксис C#. Наприклад, інтерфейс IDisposable оголошує метод Dispose(), мета якої – виконати певну завершальну роботу, наприклад, закрити файли, звільнити інші ресурси тощо. Для того, щоб цей метод був гарантовано викликаний, об'єкт, який реалізує інтерфейс IDisposable можна створити у конструкції using:

using (X x = new X()) 
{
    // робота з об'єктом x
}
// виклик x.Dispose()

Як зазначалося раніше, для масивів числових значень сортування здійснюється за збільшенням. Для класів і структур упорядкування визначається функцією 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);
        }
    }
}

Сортування списків (List) здійснюється аналогічно. Списки будуть розглянуті в наступній лабораторній роботі.

Деякі інші стандартні інтерфейси будуть розглянуті нижче в контексті їхнього застосування.

2.2.5 Успадкування та поліморфізм під час роботи з записами, структурами та кортежами

Записи підтримують успадкування. Наприклад:

public record PopulatedRegion
{
    public string Name { get; init; } = "";
    public double Area { get; init; }
    public int Population { get; init; }
}

public record Country : PopulatedRegion
{
    public string Capital { get; init; } = "";
}

Усі структури є явними нащадками типу System.ValueType, що є нащадком System.Object. Разом з тим, структури не підтримують механізму явного успадкування, хоча і можуть реалізовувати інтерфейси. У структурі можна перевизначити методи класу System.Object, наприклад, такі як ToString(). Перевизначення здійснюється з використанням модифікатора override, що в інших випадках для структур заборонене.

Явне використання успадкування та поліморфізму для кортежів не підтримується.

2.3 Зіставлення зі зразком

Концепція зіставлення зі зразком (Pattern matching) є розвитком ідеї реалізації алгоритмів з розгалуженням. На загальному рівні зіставлення зі зразком передбачає виконання програмного коду залежно від збігу значення, яке досліджується, з певним зразком. Залежно від можливостей мови програмування це може бути

  • константа,
  • предикат,
  • тип даних,
  • інша конструкція мови програмування.

Традиційні засоби порівняння значень змінних з константами в твердженнях if та switch є найпростішими формами зіставлення зі зразком. Коли у мові C# йдеться про зіставлення зі зразком, мають на увазі додані починаючи з версії C# 7.0 конструкції перевірки типів об'єктів з одночасним створенням посилання на змінну відповідного типу. Наприклад, у нас є змінна:

object obj = "Текст";

Десь у програмі слід перевірити чи справді ця змінна посилається на рядок, та виконати певні дії. До введення нових конструкцій, пов'язаних із зіставлення зі зразком слід було здійснювати явне перетворення типів:

if (obj is string)
{
    string s = (string)obj;
    Console.WriteLine(s.Length);
}

Починаючи з версії C# 7.0 можна використовувати більш компактну конструкцію:

if (obj is string s)
{
    Console.WriteLine(s.Length);
}

Але найцікавіша новація - це використання перевірки типів у конструкції switch(). Наприклад:

switch (obj)
{
    case string s:
        Console.WriteLine(s.Length);
        break;
    case int i:
        Console.WriteLine(i + 1);
        break;
    default:
        Console.WriteLine("Хибний тип");
        break;
}

У виразах зіставлення зі зразком можна використовувати конструкцію when для визначення додаткової умови, наприклад:

switch (obj)
{
    case string s when s.Length == 0:
        Console.WriteLine("Порожній рядок");
        break;
    case string s:
        Console.WriteLine(s);
        break;
    default:
        Console.WriteLine("Хибний тип");
        break;
}

2.4 Рефлексія

Відображення або рефлексія (reflection) – це механізм, який дозволяє програмі відстежувати й модифікувати власну структуру та поведінку під час виконання. Інформація про типи, яка може бути отримана через механізм рефлексії, міститься в метаданих складання. Мова C# надає класи Assembly, MemberInfo, MethodInfo, PropertyInfo, FieldInfo та інші типи простору імен System.Reflection.

Під час виконання дані про тип можна отримати, вказавши рядок з повною назвою типу (включаючи простір імен і вкладені простори імен):

int k = 100;
Type type = Type.GetType("System.Int32");
MemberInfo[] members = type.GetMembers();
foreach(MemberInfo member in members)
{
    Console.WriteLine(member);
}

Результатом роботи програми буде відносно великий перелік полів, методів (включаючи статичні) і властивостей, визначених у структурі System.Int32 і її базових типах.

Для отримання інформації про тип можна також створити змінну.

int k = 100;
Type type = k.GetType();
Console.WriteLine(type); // System.Int32

Можна окремо отримати інформацію про методи, поля і властивості:

FieldInfo[] fields = type.GetFields();
MethodInfo[] methods = type.GetMethods();
PropertyInfo[] properties = type.GetProperties();

За допомогою рефлексії можна створювати об'єкти типів, імена яких визначаються рядком. Можна викликати методи, працювати з полями та властивостями через імена, визначені під час виконання програми. Наприклад, так можна створити екземпляр визначеного класу (MySpace.MyClass) і завантажити викликати його метод (MyFunc):

// Створюємо екземпляр (об'єкт) класу:
object o = assembly.CreateInstance("MySpace.MyClass");
// Отримуємо інформацію про тип:
Type t = o.GetType();
// Отримуємо інформацію про метод з указаним ім'ям: 
MethodInfo mi = t.GetMethod("MyFunc");
// Викликаємо метод з параметром x:
object result = mi.Invoke(o, new object[] { x });;

Засоби рефлексії C# дозволяють нам, окрім публічних даних, властивостей і методів, отримувати також інформацію про приватні елементи типів. Наприклад, так можна отримати інформацію про всі поля деякого типу:

FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

Оскільки через механізми рефлексії можна не тільки отримувати інформацію, а також змінювати значення полів, викликати методи тощо, рефлексія фактично дозволяє нам обходити обмеження інкапсуляції.

2.5 Обробка виняткових ситуацій

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

Для генерації виняткової ситуації використовується оператор throw. Після ключового слова throw повинен бути розташований об'єкт класу System.Exception чи класів, похідних від нього. Такі похідні класи відбивають специфіку конкретної програми.

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.6 Початкові відомості про роботу з файлами

2.6.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.6.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 і зчитуємо дані з бінарного файлу. Об'єкт зі прочитаними даними виводимо на екран.

Слід пам'ятати, що робота з бінарними файлами не передбачає перегляду та редагування файлів поза спеціальною програмою, яка була створена для читання і запису даних.

3 Приклади програм

3.1 Ієрархія об'єктів реального світу

Припустимо, необхідно розробити ієрархію класів "Регіон" – "Населений район" – "Країна". Окремі класи цієї ієрархії можуть стати базовими для інших класів (наприклад "Незаселений острів", "Національний парк", "Адміністративний район", "Автономна республіка" і т.д.). Ієрархію класів можна доповнити класами "Місто" і "Острів". Доцільно в кожен клас додати конструктор, який ініціалізує усі поля. Можна також створити масив посилань на різні об'єкти ієрархії та для кожного об'єкта вивести на екран рядок даних про нього.

Для того, щоб одержати рядкове представлення об'єкта, необхідно перекрити метод ToString(). Можна запропонувати таку ієрархію класів:

namespace HierarchyTest
{
    // ієрархія класів
    class Region {
        public string Name { get; set; }
        public double Area { get; set; }

        public Region(string name, double area) 
        {
            Name = name;
            Area = area;
        }

        public override string ToString() 
        {
            return Name + ".\nТериторія " + Area + " кв.км.\n";
        }
  
    }

    class PopulatedRegion : Region {
        public int Population { get; set; }

        public PopulatedRegion(string name, double area, int population) 
                : base(name, area)
        {
            Population = population;
        }

        public int Density() {
            return (int) (Population / Area);
        }

        public override string ToString()
        {
            return base.ToString() + 
                "Населення " + Population + " чол.\n" +
                "Щiльнiсть населення " + Density() + " чол/кв.км.\n";
        }

    }

    class Country : PopulatedRegion 
    {
        public string Capital { get; set; }

        public Country(string name, double area, int population, string capital)
                : base(name, area, population) 
        {
            Capital = capital;
        }

        public override string ToString()
        {
            return "Країна " + base.ToString() + "Столиця " + Capital + "\n";
        }

    }

    class City : PopulatedRegion 
    {
        public int Boroughs { get; set; } // Кількість районів

        public City(string name, double area, int population, int boroughs) :
              base(name, area, population) 
        {
          Boroughs = boroughs;
        }

        public override string ToString()
        {
            return "Мiсто " + base.ToString() + "Районiв - " + Boroughs + "\n";
        }
  
    }

    class Island : PopulatedRegion 
    {
        public string Sea { get; set; }

        public Island(string name, double area, int population, string sea) :
              base(name, area, population) 
        {
          Sea = sea;
        }

        public override string ToString()
        {
            return "Острiв " + base.ToString() + "Море -    " + Sea + "\n";
        }  
    }

    class Program
    {
        static void Main(string[] args)
        {
            Region[] a = { new City("Київ", 839, 2679000, 10),
                           new Country("Україна", 603700, 46294000, "Київ"),
                           new City("Харкiв", 310, 1461000, 9),
                           new Island("Змiїний", 0.2, 30, "Чорне") };
            foreach (Region region in a)
            {
                System.Console.WriteLine(region);
            }
        }
    }
}

3.2 Знаходження мінімуму методом дихотомії

Припустимо, необхідно створити універсальний клас для знаходження методом дихотомії мінімуму будь-якої функції f(x). Алгоритм знаходження мінімуму на певному інтервалі [a, b] з точністю h полягає в такому:

  • визначається середина інтервалу (x)
  • обчислюються значення f(x - h) та f(x + h)
  • якщо f(x - h) > f(x + h), початок інтервалу переноситься в x, в іншому випадку туди переноситься кінець інтервалу
  • якщо довжина нового інтервалу менша, ніж h, процес завершується, в іншому випадку – повторюється для нового інтервалу

Слід зазначити, що цей алгоритм працює тільки для випадку, коли на інтервалі один мінімум.

Можна запропонувати два підходи: через використання абстрактних класів і через використання інтерфейсів.

Перший варіант

Створюємо новий клас – AbstractMinimum, який містить абстрактний метод F() і метод знаходження мінімуму – Solve(). У похідному класі цей метод перекривається.

using System;

namespace LabThird
{
    public abstract class AbstractMinimum {
        abstract public double F(double x);

        public double Solve(double a, double b, double h)
        {
            double x = (a + b) / 2;
            while (Math.Abs(b - a) > h) {
                if (F(x - h) > F(x + h))
                {
                    a = x;
                }
                else
                {
                    b = x;
                }
                x = (a + b) / 2;
            }
            return x;
        }
    }

    class SpecificMinimum : AbstractMinimum 
    {
         public override double F(double x)
        {
            return x * x - x;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            SpecificMinimum sm = new SpecificMinimum();
            Console.WriteLine(sm.Solve(0, 3, 0.000001));
        }
    }
}

Другий варіант

Описуємо інтерфейс для представлення функції. Клас Solver реалізує статичний метод для знаходження мінімуму функції. Клас, який реалізує інтерфейс, містить конкретну реалізацію функції F().

using System;

namespace LabThird
{
    public interface IFunction
    {
        double F(double x);
    }

    public class Solver
    {
        public static double Solve(double a, double b, double h, IFunction func)
        {
            double x = (a + b) / 2;
            while (Math.Abs(b - a) > h)
            {
                if (func.F(x - h) > func.F(x + h))
                {
                    a = x;
                }
                else
                {
                    b = x;
                }
                x = (a + b) / 2;
            }
            return x;
        }
    }

    class MyFunc : IFunction {
        public double F(double x) {
            return x * x - x;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Solver.Solve(0, 3, 0.000001, new MyFunc()));
        }
    }
}

3.3 Ієрархія друкованих видань

Припустимо, необхідно доробити попередньо створену програму, яка описує книжкову полицю, додавши ієрархію друкованих видань, які можуть бути розташовані на полиці. Наприклад, окремі типи друкованих видань – книга й журнал. Крім того, слід передбачити методи додавання й видалення друкованих видань та авторів з масивів. Замість перевантаженої операції приведення до типу string ми реалізуємо перевизначення методу ToString().

Також можна реалізувати функції збереження даних у текстовому файлі та завантаження даних текстового файлу.

До файлу Books.cs додаємо класи Publication, Magazine, FileData та FileUtils. Крім того, вносимо необхідні зміни до класів, які були створені раніше. Отримаємо такий код файлу Books.cs:

using System.Text;
/// <summary>
/// Простір імен, який охоплює класи для представлення книжкової полиці
/// </summary>
namespace Bookshelf
{
    /// <summary>
    /// Представляє автора книги на книжковій полиці
    /// </summary>
    public class Author
    {
        public string Name { get; set; } = "";
        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>
    public abstract class Publication
    {
        public string Title { get; set; } = "";
        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>
    public class Book : Publication
    {
        public Author[] Authors { get; set; } = { };
        
        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 = Authors.Append(new Author(name, surname)).ToArray();
        }

        /// <summary>
        /// Видаляє дані про автора
        /// </summary>
        /// <param name="name">ім'я автора, дані про якого треба знайти й видалити</param>
        /// <param name="surname">прізвище автора, дані про якого треба знайти й видалити</param>
        public void RemoveAuthor(string name, string surname)
        {
            Authors = Authors.Where(author => author.Name != name ||
                author.Surname != surname).ToArray();
        }

    }

    /// <summary>
    /// Представляє журнал на полиці
    /// </summary>
    public class Magazine : Publication
    {
        public int Volume { get; set; }
        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>
    public class Bookshelf
    {
        /// <summary>
        /// Масив посилань на публікації
        /// </summary>
        public Publication[] Publications { get; set; } = { };

        /// <summary>
        /// Індексатор, який дозволяє отримувати видання за індексом
        /// </summary>
        /// <param name="index">індекс видання</param>
        /// <returns>видання з відповідним індексом</returns>
        public Publication this[int index]
        {
            get => Publications[index];
            set => Publications[index] = value;
        }

        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="publications">відкритий масив видань</param>
        public Bookshelf(params Publication[] publications)
        {
            Publications = publications;
        }

        /// <summary>
        /// Додає нову публікацію до книжкової полиці
        /// </summary>
        /// <param name="publication">публікація, яка додається до книжкової полиці</param>
        public void AddPublication(Publication publication)
        {
            Publications = Publications.Append(publication).ToArray();
        }

        /// <summary>
        /// Видаляє публікацію зі вказаною назвою
        /// </summary>
        /// <param name="title">назва публікації, яку треба знайти й видалити</param>
        public void Remove(string title)
        {
            Publications = Publications.Where(publication => publication.Title != title).ToArray();
        }

        /// <summary>
        /// Надає представлення рядком даних про книжкову полицю
        /// </summary>
        /// <returns>рядок, який представляє дані про книжкову полицю</returns>
        public override string ToString()
        {   
            return StringRepresentations.ToString(this);
        }
    }

    /// <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.Length; i++)
            {
                result += string.Format("      {0}", book.Authors[i]);
                result += (i < book.Authors.Length - 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();
        }

    }

    /// <summary>
    /// Надає методи для пошуку і сортування видань на полиці
    /// </summary>
    public static class BookHandle
    {
        /// <summary>
        /// Шукає визначену послідовність символів у назвах видань
        /// </summary>
        /// <param name="bookshelf">книжкова полиця</param>
        /// <param name="characters">послідовність символів, яку треба відшукати</param>
        /// <returns>масив видань, у назви яких входить визначена послідовність</returns>
        public static Publication[] ContainsCharacters(Bookshelf bookshelf, string characters)
        {
            return Array.FindAll(
                bookshelf.Publications, publication => publication.Title.Contains(characters));
        }

        /// <summary>
        /// Здійснює сортування видань за алфавітом назв без урахування регістру
        /// </summary>
        /// <param name="bookshelf">книжкова полиця</param>
        public static void SortByTitles(Bookshelf bookshelf)
        {
            Array.Sort(bookshelf.Publications,
                       (b1, b2) => string.Compare(b1.Title.ToUpper(), b2.Title.ToUpper()));
        }
    }

    /// <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;
        }
    }
}

До файлу Program.cs додаємо нову функцію AdditionalProcessing(). Отримаємо такий код:

namespace Bookshelf;

/// <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 = BookHandle.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За алфавітом без урахування регістру:");
        BookHandle.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>
    /// Стартова точка консольного застосунку
    /// </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");
    }
}

4 Вправи для контролю

  1. Створити ієрархію класів Книга та Підручник. Реалізувати конструктори та методи доступу. Перекрити метод ToString(). У функції Main() створити масив, який містить елементи різних типів. Вивести елементи на екран.
  2. Створити ієрархію класів Кінофільм і Серіал. Реалізувати конструктори та методи доступу. Перекрити метод ToString(). У функції Main() створити масив, який містить елементи різних типів. Вивести елементи на екран.
  3. Створити ієрархію класів Місто та Столиця. Реалізувати конструктори та методи доступу. Перекрити метод ToString(). У функції Main() створити масив, який містить елементи різних типів. Вивести елементи на екран.
  4. Створити ієрархію класів "Домашня тварина" та "Кішка". Перекрити метод ToString(). У функції Main()створити масив, який містить елементи різних типів. Вивести елементи на екран.
  5. Створити ієрархію класів "Планета" та "Супутник". Перекрити метод ToString(). У функції Main() створити масив, який містить елементи різних типів. Вивести елементи на екран.
  6. Прочитати з текстового файлу всі рядки, занести у масив, та записати в інший текстовий файл у зворотному порядку.
  7. Прочитати з текстового файлу всі рядки, занести у масив, та записати в інший текстовий файл рядки з парними номерами.

5 Контрольні запитання

  1. Для чого застосовують успадкування класів?
  2. Яка різниця між множинним та одиничним успадкуванням?
  3. Які елементи базового класу не успадковуються?
  4. Як здійснити ініціалізацію базового класу?
  5. Де і для чого можна застосовувати ключове слово base?
  6. Як перекрити метод з модифікатором sealed?
  7. Чи можна неявно приводити посилання на базовий клас до посилання на похідний клас?
  8. Які можливості надає використання поліморфізму?
  9. Чим віртуальний метод відрізняється від невіртуального?
  10. Як здійснюється опис та перекриття віртуальних методів?
  11. Яка реакція компілятора на відсутність override або new перед перекритим віртуальним методом?
  12. Як описати абстрактні методи та класи? Чи можуть абстрактні класи містити неабстрактні методи?
  13. У чому перевага інтерфейсів у порівнянні з абстрактними класами?
  14. Як описати та реалізувати інтерфейс?
  15. Навіщо здійснюється явна реалізація інтерфейсів?
  16. У чому полягають особливості методів, які явно реалізують інтерфейси?
  17. Як описати й викликати метод інтерфейсу з усталеною реалізацією?
  18. У чому полягає ідея механізму зіставлення зі зразком?
  19. Для чого призначений механізм винятків?
  20. Як створити об'єкт-виняток?
  21. Як отримати текст повідомлення про виняток у C#?
  22. Як отримати результат трасування стека в C#?
  23. Чи можна викликати функцію, яка генерує виняткову ситуацію, без перевірки цього винятку? Якою буде реакція системи?
  24. Які засоби .NET надає для роботи з текстовими файлами?

 

up