Лабораторна робота 1

Основи ООП. Базовий синтаксис мови C#

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

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

Створити консольний застосунок C#, який реалізує класи відповідно до завдання, наведеного в таблиці:

Клас представлення групи сутностей Клас-сутність Допоміжний клас
Група Студент Адреса
Інститут Факультет Декан
Факультет Спеціальність Декан
Сесія Предмет Викладач
Місто Район Країна
Область Місто Країна
Спортивна секція Учасник Адреса
Футбольний клуб Гравець Країна
Музичний гурт Учасник Країна
Музичний гурт Альбом Країна
Альбом Пісня Автор тексту
Квартира Кімната Адреса
Збірка оповідань Оповідання Автор
Художник Твір Країна
Письменник Роман Країна

Перелік може бути розширено за узгодженням з викладачем.

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

Дані, які потрібно шукати, будуть вводитися з клавіатури під час виконання. Дані тесту повинні бути підготовлені таким чином, щоб пошук дав більше одного результату.

1.2 Застосування null-сумісних типів

Реалізувати функцію обчислення квадратного кореня з параметрами типу double (аргумент x) і out int (код помилки), яка повертає double? зі значенням квадратного кореня (або null, якщо квадратний корінь не можна відшукати). Код помилки після виклику функції повинен бути 0 (якщо обчислення квадратного кореня було здійснене успішно), або -1, якщо аргумент від'ємний. Здійснити тестування функції для різних значень аргументів.

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

Примітка: не використовувати функцію Math.Sqrt().

1.3 Найбільший спільний дільник

Створити консольний застосунок, у функції Main() якого розташовано статичну локальну функцію (з типом результату void) обчислення найбільшого спільного дільника двох цілих додатних чисел найпростішим алгоритмом Евкліда. Числа повинні передаватися як параметри з атрибутом ref. Як результат роботи функції початкові значення повинні бути замінені значенням найбільшого спільного дільника. Слід викликати створену локальну функцію для різних аргументів і продемонструвати її роботу.

1.4 Клас для подання квадратного рівняння (додаткове завдання)

Створити клас "Квадратне рівняння", корені якого – властивості з доступом для читання. Тип властивостей повинен бути null-сумісним. Додати також індексатор для доступу до коренів за індексом.

1.5 Робота з невирівняним масивом

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

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

2.1 Основи ООП

2.1.1 Парадигми програмування

Парадигма програмування (programming paradigm) – це сукупність ідей і понять, що визначають стиль написання комп'ютерних програм. Це спосіб концептуалізації, що визначає організацію обчислень і структурування роботи, яку виконує комп'ютер.

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

  • Імперативное програмування (imperative programming) описує процес отримання результатів як послідовності інструкцій зміни стану програми.
  • Функціональне (функційне) програмування (functional programming) розглядає процес обчислень як послідовність викликів функцій без збереження стану застосунку.
  • Структурне програмування (structured programming) визначає програмe як сукупність блоків.
  • Процедурне програмування (procedural programming) передбачає функціональний підхід зі збереженням стану програми.
  • Модульне програмування (modular programming) передбачає поділ програми на незалежні логічні та фізичні частини, які можуть самостійно оброблятися.
  • Компонентне програмування (component-based programming) передбачає збереження модульної структури програмного забезпечення при виконанні програми.
  • Об'єктно-орієнтоване програмування (ООП, object-oriented programming) передбачає організацію програми як сукупності об'єктів класів (структур даних, що складаються з даних і функцій), а також визначення їхньої взаємодіі.
  • Прототипне програмування (prototype-based programming) – різновид об'єктно-орієнтованого програмування, реалізованого не через класи, а через успадкування об'єктів шляхом клонування існуючого екземпляра об'єкта (прототипа).
  • Узагальнене програмування (generic programming) полягає в такому описі даних і алгоритмів, які можна застосувати до різних типів даних, не змінюючи цей опис.
  • Подійно-орієнтоване програмування (event-driven programming) – управління обчисленнями визначається через події (асинхронне введення, повідомлення від інших програм або потоків тощо).
  • Метапрограмування (metaprogramming) передбачає створення програм, які створюють інші програми як результат своєї роботи, або програм, які змінюють себе під час виконання.
  • Декларативне програмування (declarative programming) визначає логіку обчислень без опису потоку управління.

Існує також багато інших парадигм програмування – логічне, аспектно-орієнтоване, агентно-орієнтоване тощо.

2.1.2 Методологія імперативного програмування

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

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

Для реалізації «неструктурного» підходу в мові програмування необхідна наявність таких засобів:

  • опис змінних;
  • послідовне виконання тверджень, зокрема присвоєння змінним певних значень;
  • мітки;
  • безумовний перехід (goto);
  • умовний перехід (if...goto).

У мові C# цей підхід реалізується через відповідні синтаксичні конструкції.

2.1.3 Реалізація структурного підходу

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

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

  • послідовне виконання (аналогічне неструктурному програмуванню);
  • розгалуження – умовне твердження (if, if...else) та перемикач (switch);
  • цикли: з передумовою (while), з постумовою (do...while), з параметром (for);
  • програмний блок – одне або кілька тверджень, взятих у "операторні" дужки (наприклад, { і }); блок визначає свою область видимості; всередині блоку можна описувати локальні змінні, константи, типи тощо; блоки можна вкладати один в інший.

Усі необхідні синтаксичні конструкції присутні в мові C#.

2.1.4 Реалізація процедурного підходу

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

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

В мові C# процедурний підхід реалізований через використання статичних функцій, що знаходяться в області видимості класів.

2.1.5 Реалізація модульного підходу

Модульне програмування передбачає розділення програмного коду на окремі модулі, що містять логічно пов'язані елементи (типи, дані, підпрограми). На логічному рівні мови підтримують так звані простори імен. Простір імен (namespace) – іменована частина глобальної області видимості, в якій можуть міститися оголошення та визначення. Простори імен допомагають уникнути конфліктів імен.

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

Для реалізації модульного підходу в C# використовують простори імен, що забезпечують групування коду на логічному рівні. Так звані складання забезпечують фізичне групування.

2.1.6 Причини виникнення та переваги об'єктно-орієнтованого підходу

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

Програмне забезпечення є по своїй суті дуже складним. Складність програмних систем часто перевищує людський інтелектуальний потенціал. Як стверджує один з засновників об'єктно-орієнтованої методології Грейді Буч, ця складність випливає з чотирьох елементів:

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

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

Вперше терміни "об'єкт" і "об'єктно-орієнтований" в сучасному сенсі об'єктно-орієнтованого програмування з'явилися у дослідженні групи штучного інтелекту Массачусетського технологічного інституту в кінці 1950-х – на початку 1960-х років. Поняття "об'єкт" і "екземпляр" з'явилися в глосарію, розробленому Іваном Сазерлендом в 1961 р.і пов'язані з описом роботи світлового пера (Sketchpad).

Першою мовою об'єктно-орієнтованого програмування була мова Simula 67. Ця мова призначалася для програмної реалізації систем імітаційного моделювання (discrete simulation). Мова була створена у Норвегії у 1967 році. Автори цієї мови – Олє-Йохан Даль і Крістен Нуґард.

Перша універсальна мова об’єктно-орієнтованого програмування – Smalltalk. Її широко розповсюджена версія – Smalltalk 80. Автори – Алан Кей та Ден Інгаллс.

2.1.7 Складові об'єктно-орієнтованої методології

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

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

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

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

2.1.8 Основні принципи та концепції об'єктно-орієнтованої парадигми

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

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

Дані об'єкта (поля, fields, іноді – елементи даних) – це змінні, що характеризують стан об'єкта.

Функції об'єкта (методи, methods) – це функції, які мають безпосередній доступ до даних об'єкта. Іноді говорять, що методи визначають поведінку об'єкта. На відміну від звичайних (глобальних) функцій, необхідно спочатку створити об'єкт і викликати метод у контексті цього об'єкта.

Об'єкти характеризуються життєвим циклом. Створення об'єктів передбачає виклик спеціальної функції ініціалізації даних – так званого конструктора. Конструктори викликаються безпосередньо після створення об'єкта в пам'яті. Більшість мов об'єктно-орієнтованого програмування підтримує механізми звільнення ресурсів задіяних у життєвому циклі об'єктів із застосуванням деструкторів. Деструктор – це спеціальна функція, яка викликається безпосередньо перед видаленням об'єкта і звільняє системні ресурси, які були залучені в процесі створення і функціонування об'єкта.

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

Інкапсуляція (приховування даних) – одне з трьох фундаментальних понять об'єктно-орієнтованого програмування. Зміст інкапсуляції полягає в приховуванні від зовнішнього користувача деталей реалізації об'єкта. Доступ до даних (полів) здійснюється через відкриті функції доступу або властивості.

Успадкування – це механізм створення похідних класів від базових. Створення продуктивного класу передбачає розширення шляхом додавання нових полів (атрибутів) і методів. У C++ є так звані закриті та захищені спадщини. Ці форми наслідування дозволяють обмежити доступ до елементів базових класів зовнішнього класу. У більшості мов об'єктно-орієнтованого програмування підтримується тільки відкрите успадкування – елементи при успадкуванні зберігають свою видимість. При цьому закриті елементи успадковуються, але становляться недоступними для безпосереднього звернення у похідних класах.

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

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

Підключення тіла функцій до точки її виклику називається зв'язуванням. Якщо воно відбувається до початку виконання програми, йдеться про раннє зв'язування. Цей тип зв'язку присутня мовам процедурного типу, таким як C або Pascal. Пізнє зв'язування означає, що підключення відбувається під час виконання програм і в об'єктно-орієнтованих мовах залежить від типів об'єктів. Пізнє зв'язування ще називають динамічним. Для реалізації поліморфізму використовується механізм пізнього зв'язування.

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

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

2.2 Платформа .NET та мова програмування C#

2.2.1 Загальні концепції платформи .NET

Платформа .NET – це програмне середовище, яке забезпечує розробку та виконання програм, що спираються на загальномовну інфраструктуру – спільне середовище часу виконання та спільний набір типів. Як і в Java, є можливість створення програм, які можуть бути виконані у різних операційних системах без перекомпіляції. Архітектура .NET спирається на набір стандартів консорціуму WWW, перш за все йдеться про протокол HTTP (він є базовим протоколом для технології Web-сервісів) і мови XML.

У порівнянні з попередніми технологіями, які пропонувала Microsoft, платформа .NET має такі переваги:

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

Реалізація .NET-платформи від Microsoft для MS Windows в попередніх версіях мала назву .NET Framework – набір базових засобів архітектури .NET, що забезпечують розробку і виконання програм для платформи.NET. Існує декілька версій .NET Framework. Версія 3.5 автоматично встановлюється під час інсталяції Windows 7. Версія 4.0 автоматично встановлюється під час інсталяції Windows 10.

Паралельно з розробкою нових версій .NET Framework, Microsoft оприлюднила модульну платформу з відкритим кодом .NET Core для операційних систем Windows, Linux та macOS. Вийшло декілька версій (до 3.1 включно). Паралельно виходили нові версії .NET Framework (до 4.8 включно). Гілки відрізнялися не тільки ліцензійними умовами, але й набором технологій, які вони підтримували. У 2020 році Microsoft знову об'єднала дві гілки у платформі .NET 5.

У 2021 році вийшла версія .NET 6. Передбачено довгострокову підтримку цієї версії (до листопада 2024 року). В листопаді 2022 року вийшла версія .NET 7.

Останній випуск платформи .NET можна завантажити з https://dotnet.microsoft.com/download. Слід вибрати версію для певної операційної системи.

Засоби .NET включають дві основні складові частини:

  • загальне середовище виконання (Common Language Runtіme, CLR);
  • спільна система типів (Common Type System CTS).

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

Компіляція програми з мови високого рівня виконується у два етапи. Спочатку створюється модуль, який містить текст мовою Common Intermediate Language (CIL). Програмний код, керований CLR, називається керованим кодом (managed code) на противагу некерованому (unmanaged code). На відміну від Java, керований код не інтерпретується. Замість цього відбувається компіляція коду, іменована just-in-time (JIT). Під час першого завантаження програми цей код компілюється у машинні інструкції. Під час подальших запусків код одразу виконується.

2.2.2 Спільна система типів

Для того, щоб у проєктах могли здійснювати взаємодію частини, які написані різними мовами програмування, ці частини повинні спиратися на Спільну систему типів (Common Type System, CTS). Це можуть бути типи-значення (value types) і типи-посилання (reference types).

Типи-значення містять свої дані безпосередньо і їхні екземпляри розміщаються або в стеку, або як елементи структур. Такі типи можуть бути вбудованими (реалізованими безпосередньо CLR), створеними користувачем (структури), а також типами-переліченнями.

Типи-посилання містять посилання на адресу в пам'яті, де зберігаються дані. Пам'ять під ці дані виділяється в керованій області (managed heap). Такі типи далі підрозділяються на типи із самоописом (self-descrіbіng types), до яких відносяться класові типи і масиви, типи-вказівники (pointer types) і типи-інтерфейси (interface types).

Усі типи CLR входять в одну ієрархію зі спільним базовим класом System.Object.

Бібліотека класів містить у собі набір класів та інтерфейсів, які можна використовувати для розробки застосунків різної архітектури. Наприклад, для розробки Wіndows-застосунків часто використовують бібліотеку Windows Forms. Бібліотека Web Forms надає компоненти рівня сервера для створення web-застосунків.

2.2.3 Поняття складання

Складання (assembly) – це набір типів (разом з їхніми реалізаціями) і ресурсів, що розроблені для того, щоб працювати разом, і формують логічну одиницю функціональності застосунку.

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

Складання є одиницею контролю версій. Вони також є одиницею розгортання застосунку.

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

На кожному комп'ютері, на якому встановлено CLR, організується т.зв. глобальний кеш складань (global assembly cache, GAC), в якому зберігаються версії складань, призначених для використання різними застосунками.

2.2.4 Основні характеристики мови програмування С#

Попри те, що .NET допускає можливість використання різних мов програмування, найбільш природно підтримка .NET реалізована в мові C#.

С# увібрав у себе все краще з таких мов як C++, Visual Basic, Java і Object Pascal. До особливостей мови можна віднести такі:

  • об'єктна орієнтація;
  • вбудована підтримка типів CTS;
  • строгий контроль типів;
  • підтримка властивостей і подій;
  • підтримка перевантаження операцій та індексаторів;
  • автоматичне збирання сміття;
  • можливість маніпулювати вказівниками і мати безпосередній доступ до пам'яті (у некерованому коді);
  • підтримка атрибутів.

Остання версія мови C# – це 11.0 (підтримується Visual Studio 2022, версією 17.4 і пізніше).

На відміну від С++, у С# немає глобальних змінних, функцій чи процедур. Це зроблено з метою запобігання конфліктів імен. Програма складається з одного чи більше описів типів – класів, інтерфейсів, структур, перелічень або делегатів. Типи об'єднуються в так звані простори імен, які їх логічно групують.

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

Програма може складатися з декількох файлів. Як і в C++, у C# відсутній прямий зв'язок між іменами класів і іменами файлів. За пошук потрібних файлів під час виконання відповідають метадані, включені у складання.

2.3 Створення консольного застосунку C# у середовищі програмування Visual Studio

Для роботи з останньою версією C# слід завантажити останню версію Visual Studio. Після завантаження інсталятора та запуску його слід вибрати, чи потрібно встановлювати певні компоненти. Компоненти об’єднані в групи, так звані робочі завантаження (Workloads). У нашому випадку достатньо вибрати таке завантаження: .NET desktop development. Потім ви можете натиснути кнопку Install. Після встановлення слід зареєструватися за допомогою облікового запису Microsoft. Цей обліковий запис можна створити безплатно.

Зазвичай після завантаження інтеґрованого середовища Visual Studio всередині головного вікна відображається стартова сторінка. Панель Get Started містить список рекомендованих варіантів початку роботи. Можна створити новий проєкт безпосередньо. Якщо скористатися опцією Continue without code, відкриється порожнє вікно середовища. Новий проєкт може бути створений декількома способами:

  • через головне меню (File | New | Project...);
  • шляхом натиснення кнопки New Project стандартної панелі інструментів;
  • клавішною комбінацією Ctrl+Shift+N.

Також можна створити новий проєкт безпосередньо зі стартової сторінки. Це найпростіший спосіб (без відкриття порожнього середовища). Припустимо, розпочато створення нового проєкту одним із перерахованих способів. Тепер на екрані з’являється нове вікно під назвою Create a new project. Необхідно вибрати шаблон для проєкту. На правій панелі вибираємо C# Console Application. Потім з’являється наступне вікно Configure your new project. Далі можна змінити ім'я проекту(Name), теку, в якій буде розташоване рішення (Location) та ім'я рішення (Solution name). Рішення (Solution) – це концептуальний контейнер проєкту або групи логічно зв'язаних проєктів, які мають спільні властивості та налаштування. Проект (Project) включає в себе набір вихідних файлів, а також пов'язаних з ними метаданих, таких як посилання та інструкції по збірці. Проєкт зазвичай продукує один або декілька бінарних файлів як результат компіляції. Якщо рішення передбачає створення застосунку, один (і тільки один) з проєктів може бути позначений як стартовий.

Опцію Place solution and project in the same directory слід вибирати для невеличких проєктів. Але якщо треба додати кілька проєктів до загального рішення, цю опцію вибирати не слід. Далі можна змінити ім'я проєкту (Name), теку, в якій буде розташоване рішення (Location) та ім'я рішення (Solution name). У нашому випадку ім'я проєкту буде Hello, а ім'я рішення - Labs. Після натиснення Next, на сторінці Additional information слід вибрати Target Framwork: .NET 7.0. Також слід вибрати опцію Do not use top-level statements. Після натиснення кнопки Create, Visual Studio автоматично генерує вихідний файл Program.cs у підкаталогу Hello теки Labs. Його текст буде таким:

namespace Hello
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Текст програми містить опис власного простору імен (namespace) – Hello (збігається з назвою проєкту), опис всередині нового простору імен класу Program зі статичною (static) функцією Main(), яка визначає точку початку виконання програми. Програму можна завантажити на виконання, обравши функцію головного меню Debug | Start Without Debugging, або застосувавши клавішну комбінацію Ctrl+F5. Як і очікувалося, у консольному вікні відображається відповідний текст.

Можна обійтися без аргументів функції Main():

namespace Hello
{
    internal class Program
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");
        }
    }
}    

Іноді важливим є так званий код завершення, який програма повертає операційній системі. Значення 0 (нуль) – це успішне завершення, інше ціле число можна трактувати як помилку. Тоді функція Main() може повернути ціле значення (int) замість void:

namespace Hello
{
    internal class Program
    {
        static int Main()
        {
            Console.WriteLine("Hello World!");
            return 0;
        }
    }
}    

Функція Main() та клас Program можуть також мати атрибути public (відкритий).

Останні версії C# (починаючи з 9.0) дозволяють створити дуже просту програму "Hello World", яка міститиме лише один рядок коду. Для отримання такої програми не слід вибирати опцію Do not use top-level statements:

Console.WriteLine("Hello World!");

Слід пам’ятати, що прихований клас із статичним методом Main() створюється автоматично. Файл із таким сирцевим кодом може бути єдиним у проєкті.

Середовище MS Visual Studio надає зручні засоби зневадження програми (debugging). Можна додати точку переривання (F9) у необхідному рядку коду. Якщо тепер скористатися функцією меню Debug | Start Debugging (F5), програма буде завантажена на виконання, але її виконання буде призупинене у точці переривання. Зовнішній вигляд та розташування підвікон середовища дещо зміняться. У закладках Autos та Locals можна переглянути проміжні значення змінних. Зупинити виконання програми можна за допомогою функції Stop Debugging (Shift+F5).

2.4 Базовий синтаксис мови C#

2.4.1 Директиви препроцесора

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

  • умовно пропускати фрагменти коду (#define, #undef, #if, #elif, #else, #endif, використання аналогічне C++);
  • генерувати повідомлення про помилки і попередження (#warning і #error з відповідними рядками помилок);
  • задавати іменовані фрагменти коду для ієрархічного відображення в Visual Studio (#region і #endregion).

Остання директива дозволяє задати іменований фрагмент коду, який можна буде розкривати і ховати з використанням ієрархічного відображення (outlines) у Visual Studio.

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

2.4.2 Коментарі

Усі мови програмування підтримують поняття коментаря. Коментарі – це текст всередині вихідного коду, який не обробляє компілятор. Мова C# підтримує три види коментарів:

  • у стилі C++ (// до кінця рядка)
  • у стилі C (/* */)
  • XML-коментарі, що використовуються для генерації документації (///).

Ідея генерації документації багато в чому аналогічна Java, однак замість HTML-документу генерується XML-документ, з якого, своєю чергою, можна отримати стандартні файли документації (Help).

2.4.3 Ідентифікатори, ключові та зарезервовані слова

Сирцевий код C#, який обробляє компілятор, складається з лексем. Лексема (token) – це послідовність символів, що мають певне сукупне значення. Проміж окремими лексемами розташовують розділювачі – пропуск, табуляція, новий рядок тощо. Лексеми поділяються на такі групи:

  • ключові (зарезервовані) слова
  • ідентифікатори
  • літерали (константи)
  • знаки операцій.

Як C++, мова C# є чуттєвою до регістру (заголовні та малі літери відрізняються). Для репрезентації символів використовується кодування Unicode.

Ключові слова (keywords) – це попередньо визначені зарезервовані імена, які мають спеціальні значення для компілятора. Їх не можна використовувати в програмі як ідентифікатори. Окрім зарезервованих ключових слів, які є такими в будь-якому контексті, існують так звані контекстно-залежні ключові слова. Такі слова не є зарезервованими і набувають статусу ключових лише у певному контексті. Усього є 79 зарезервованих ключових слів. Приклади таких слів – int, double, if, for, class, struct тощо. Приклади контекстно-залежних ключових слів – set, get, var, value тощо.

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

Аналогічні також правила побудови ідентифікаторів C# аналогічні відповідним правилам С++. Доцільно використовувати змістовні імена, які відображають природу об'єкта або функції. Не можна використовувати пропуски всередині ідентифікатора. Тому, якщо необхідно створити ідентифікатор з кількох слів, ці слова пишуть злито, починаючи друге, третє та інші слова з великої літери. Наприклад, можна створити таке ім'я змінної:

thisIsMyVariable

Для змістовних імен доцільно використовувати англійську мнемоніку. За згодою, прийнятою в мові та взагалі в .NET, імена просторів імен, типів (класів, інтерфейсів, перелічень, структур, делегатів), методів, відкритих полів і властивостей пишуться з великої літери, інші імена – тільки з маленької.

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

int i = 11;
double d = 0, x;
float f; 
int j, k;

Локальні змінні можуть бути визначені в будь-якому місці усередині тіла функції, а також у вкладеному блоці. У C# не можна у внутрішньому блоці визначати імена, вже описані в зовнішньому блоці:

{
    int i = 0;
    {
        int j = 1; // Змінна j визначається у внутрішньому блоці
        int i = 2; // Помилка! Змінна i визначена в зовнішньому блоці
    }
}

У C# не можна оголошувати змінні без їхнього створення.

Для ініціалізації змінних, а також у виразах всередині програмного коду, використовують константи (літерали). Приклади таких літералів – 12, 0x22, 3.1416, 'k' або "some text". Дуже часто доцільно створювати так звані іменовані константи. Для цього використовують ключове слово const, яке стосовно до імен змінних означає, що вони не можуть бути змінені, наприклад:

const int h = 0; 
const double pi = 3.14169265;

2.4.4 Типи даних

Кожна змінна або константа має свій тип. Базові типи C# відповідають прийнятій у .NET спільній системі типів. Для імен стандартних типів CTS у C# створені зручні синоніми, що є ключовими словами мови. Як і C++, C# надає можливість користуватись знаковими та беззнаковими цілими типами. Нижче наведено типи CTS і їхні синоніми, які використовуються в C#.

Тип CTS Тип C# Опис
System.Object object загальний базовий тип
System.String string тип рядка
System.SByte sbyte байт зі знаком
System.Byte byte байт без знаку
System.Int16 short 2-байтове число зі знаком
System.UInt16 ushort 2-байтове число без знаку
System.Int32 int 4-байтове число зі знаком
System.UInt32 uint 4-байтове число без знаку
System.Int64 long 8-байтове число зі знаком
System.UInt64 ulong 8-байтове число без знаку
System.Char char 2-байтовий Unicode-символ
System.Single float число з рухомою точкою звичайної точності
System.Double double число з рухомою точкою подвійної точності
System.Boolean bool логічне значення (приймає значення true і false)
System.Decimal decimal тип даних високої точності (16 байт)

У наведеному списку object та string є типами-посиланнями, всі інші – типами-значеннями.

У С# 9.0 додані нові цілі типи – nint (знаковий) та nuint (беззнаковий). Реальний розмір цих типів визначається вже під час виконання і залежить від розрядності системи: на 32-розрядних їх розмір буде 4 байта, а на 64-розрядних, відповідно, 8 байтів.

Константи цілого типу записуються як послідовності десяткових цифр. Усталений тип такої константи – int. Він може бути уточнений додаванням наприкінці константи суфіксів L і l (long), U і u (uint). Цілі константи можуть записуватися у вісімковій системі числення, у цьому випадку першою цифрою повинна бути цифра 0, число може містити тільки цифри 0...7. Цілі константи можна записувати й у шістнадцятковій системі числення, у цьому випадку запис константи починається із символів 0x чи 0X. Для позначення цифр понад 9 використовуються латинські літери a, b, c, d, e та f (великі або маленькі). Наприклад:

int octal = 023; // 19
int hex = 0xEF;  // 239

У C# 7.0 з'явилася можливість визначати також двійкові константи (з використанням префіксів 0B або 0b) та роздільники для великих чисел (з використанням символу підкреслення):

int binary = 0b101011; // 43
long large = 12_345_678_900;

Символи підкреслення також можна розмістити у шістнадцятковій та двійковій константах. Починаючи з C# 7.2, символ підкреслення також можна розміщувати між 0x (або 0b) і безпосередньо числом:

int binary = 0b_10_1011;

Константи типу char беруть в одиночні лапки (апострофи), значення константи задається або знаком з поточного набору символів, або цілою константою, якій передує зворотна коса риска (символ із заданим кодом). Є ряд спеціальних символів, що можуть використовуватись як значення константи типу char (такі подвійні символи називаються керувальними послідовностями, escape sequences):

'\n' - новий рядок,
'\t' - горизонтальна табуляція,
'\r' - переведення на початок рядку, 
'\'' - одиночні лапки (апостроф),
'\"' - подвійні лапки,
'\\' - зворотна коса риска (backslash).

Для зберігання даних символьного типу в пам'яті використовується таблиця Unicode.

Константи дійсних типів можуть записуватись у формі з крапкою або в експонентному форматі і усталено мають тип double. При необхідності тип константи можна уточнити, записавши наприкінці суфікс f чи F для типу float, суфікс d чи D для типу double. Константи типу decimal використовують суфікс M (m). Наприклад:

1.5f    // 1.5  типу float
2.4E-2d // 0.25 типу double
12.5m   // 12.5 типу decimal

У C# у ряді випадків допускається неявне перетворення (приведення) типів. Для арифметичних типів воно допускається, коли перетворення веде до розширення типів.

Числа без десяткової крапки інтерпретуються як цілі (типу int). Константний вираз типу int може бути перетворений у значення будь-якого цілого типу (навіть більш вузького), якщо його значення потрапляє в діапазон для цього типу. Дані типу char можуть бути неявно перетворені до цілих та типів з рухомою крапкою, але не навпаки. Немає неявного перетворення float чи double у decimal. Значення з рухомою крапкою не можна присвоювати цілим змінним.

int   i  = 10;
float f  = i;        // Таке перетворення допускається
long  l  = f;        // Помилка!

Існує також явне перетворення типів:

(тип) вираз

Перетворення до більш вузького типу (дійсного до цілого, числа з подвійною точністю до числа з одинарною точністю) пов'язано з ризиком утрати даних і повинно здійснюватися явно

double d = 1;
long k = (long) d; // Явне перетворення типів

Числа з рухомою крапкою мають тип double. Для приведення їх до більш вузьких типів використовується явне приведення типів:

float f  = 10.5;         // Помилка!
float f1 = (float) 10.5; // Явне приведення типів
float f2 = 10.5f;        // Уточнення типу константи. Помилки немає

Немає перетворення між типом bool та іншими типами.

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

"Це рядок"

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

int k = 1;
double d = 2.5;
Console.WriteLine(k + " " + d); // 1 2.5

Усі типи-посилання можуть отримувати значення null (не посилається на жоден об'єкт). Крім того, існує спеціальний варіант типів-значень – null-сумісні типи (nullable types). Для опису таких типів використовують конструкцію тип?, де тип – один з можливих типів-значень, наприклад int?, double? тощо. Змінні null-сумісних типів можуть отримувати значення, визначені для відповідного типу, плюс значення null. Значення null-сумісних типів не можна неявно перетворювати у відповідні типи-значення, оскільки можна загубити значення null. Тому необхідне явне перетворення.

Використання типів byte, sbyte, short и ushort, може призвести до неочевидних помилок і виправдано тільки у виняткових випадках.

Мова C# підтримує явні вказівники. Використовувати вказівники не рекомендується.

Локальні змінні можуть бути визначені та ініціалізовані так само як і в C++. На відміну від C++, не можна оголосити змінні без визначення. Компілятор не дозволяє використовувати значення непроініціалізованих змінних. Це призводить до помилки компіляції:

int m;
int n = m;    // Помилка!

Починаючи з версії мови 3.0, C# надає можливість створення неявно типізованих локальних змінних. Для опису таких змінних застосовують контекстно-залежне ключове слово var. Такі змінні обов'язково повинні бути проініціалізовані. Тип змінної компілятор визначає відповідно до типу виразу ініціалізації. Наприклад:

var i = 1;
var s = "Hello";

Попри те, що тип не вказано явно, компілятор створює змінну певного типу. Після того, як змінна створена, не можна змінювати її тип:

var k = 1;
k = "Hello"; // Помилка!

Починаючи з C# 7.0, можна створювати посилання на змінні типів-значень. Для цього використовують модифікатор ref::

double x = 1;
ref double z = ref x; // посилання на x
z = 10;
Console.WriteLine(x); // 10

Починаючи з C# 7.3, посилання можуть бути перепризначені для посилання на різні змінні після ініціалізації:

double x = 1;
ref double z = ref x; // посилання на x
double y = 2;
z = ref y;            // посилання на y

2.4.5 Вирази та операції

У C# підтримуються практично всі стандартні арифметичні й логічні операції, а також операції порівняння мови С++. Ці операції мають такий само пріоритет і асоціативність, як і в C++.

На відміну від C++, операція "кома" може бути застосована тільки в заголовках циклів, наприклад:

int i, j;
for (i = 0, j = 0; i < 10; i++, j += 2)
{
    Console.WriteLine(i + " " + j);
}

Починаючи з C# 7.2, можна застосовувати умовну операцію для отримання посилання на тип-значення. Наприклад:

int m, n;
// ...
ref int k = ref (m < n ? ref m : ref n); // посилається на змінну з меншим значенням

Дуже часто умовну операцію необхідно використовувати для перевірки змінних-посилань на нерівність null. Наприклад:

a = b != null ? b : c;    

де a, b та c – посилання. Для подібних перевірок (як для посилань, так і для null-сумісних типів) у C# є спрощена операція:

a = b ?? c;    

тобто a отримає значення b, якщо b – не null, або c у протилежному випадку.

У C# 8.0 введено новий оператор ??=. Значення правого операнда присвоюється лівому операнду, лише якщо значення лівого операнда дорівнює null. Наприклад,

int? m = null;
m ??= 2; // m присвоюється 2
Console.WriteLine(m); // 2
m ??= 3; // m не присвоюється 3
Console.WriteLine(m); // 2

Нижче наводиться перелік операцій у порядку зниження пріоритету.

Операція Позначення
дужки
доступ до елементу
доступ до елементу з перевіркою null
доступ по індексу
постфіксний інкремент і декремент
створення об'єкта
отримання інформації про тип
включення/відключення перевірки переповнення
(x)
x.y
x?.y

a[i]
x++, x--
new
typeof

checked/unchecked
унарні арифметичні операції
логічне заперечення
префіксний інкремент і декремент
приведення типу
-, +
!
++x, --x
(T)x
множення, ділення, одержання залишку *, /, %
бінарне додавання і віднімання +, –
зсув <<, >>
операції відношення
операції перевірки та приведення поліморфних типів
<,>, <=, >=
is, as
перевірка на рівність і нерівність (дорівнює не дорівнює) == !=
побітове чи логічне ТА &
побітове чи логічне XOR ^
побітове чи логічне АБО |
логічне ТА &&
логічне АБО ||
перевірка посилання на значення null ??
умовна операція (тернарна) ? :
присвоєння
лямбда-оператор
op=
=>

Операція sіzeof повертає розмір в байтах для так званих некерованих типів.

Під час виконання математичних операцій можна включати і виключати перевірку на переповнення. Для цього використовуються оператори checked і unchecked. Після відповідного ключового слова використовується вираз чи блок. У випадку checked виникає помилка компіляції (якщо операнд – константа) чи генерується виняткова ситуація System.OverflowExceptіon (якщо операнд – не константа) а у випадку unchecked відбувається неявне звуження даних і ніякої помилки не генерується. Наприклад:

byte i = 255;
byte k = unchecked ((byte) (i + 1)); // k == 0

або

byte i = 255;
byte k; checked { k = ((byte) (i + 1)); // Помилка під час виконання! }

Усталено компілятор розглядає код як unchecked.

2.4.6 Твердження (інструкції). Керування виконанням програми

Твердження (або інструкція, чи оператор. англ. statement) – найменша автономна частина мови програмування. Програма являє собою послідовність інструкцій. Більшість тверджень мови C# аналогічна твердженням C++.

Порожня інструкція складається з однієї крапки з комою.

Інструкція-вираз є повний вираз, який закінчується крапкою з комою. Наприклад:

k = i + j + 1;
Console.WriteLine(k);

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

Інструкції вибору – умовна інструкція та перемикач. Умовна інструкція застосовується в двох видах:

if (вираз-умова)
    інструкція1
else
    інструкція2

або

if (умова)
    інструкція1

Під час виконання цієї інструкції обчислюється вираз-умова і, якщо це істина, то виконується інструкція1 а інакше – інструкція2. На відміну від C++, вираз-умова може бути лише типу bool.

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

switch (вираз)
    блок

Блок має такий вигляд:

{
    case константа_1: інструкції; break;
    case константа-2: інструкції; break;
    ...
    default: інструкції; break;
}

Виконання перемикача полягає в обчисленні виразу керування і переході до групи інструкцій, позначених case-міткою, значення якої дорівнює виразу керування. Якщо такої мітки немає, виконуються інструкції після мітки default (яка може бути відсутня). Важливою відмінністю від С++ є обов'язковість використання break чи goto у кожному варіанті. Наприклад,

switch (i) 
{
    case 1: Console.WriteLine("1");
            break;        // вихід з switch
    case 2: Console.WriteLine("2");
            goto case 3;  // перехід до іншого варіанту
    case 3: Console.WriteLine("2 or 3");
          // помилка компіляції – пропущений оператор переходу!
    default:Console.WriteLine("other values");
            break;
}

Вирази в switch можуть бути не тільки цілими, але й рядками. В цьому випадку константи варіантів (після case) повинні теж бути рядками, наприклад:

case "some text":

Вираз-перемикач (switch expression), уведений в C# 8.0, дозволяє спростити код:

type result = expression switch
{
    value1 => result1,
    value2 => result2,
    ...
    _      => default_result, // підкреслення інтерпретується як усталена гілка
};

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

bool? b = ... // може бути false, true або null

Тепер ми хочемо обчислити k згідно з такою таблицею:

b
k
false
0
true
1
null
-1

Традиційний підхід вимагає такого коду:

int k;
switch (b)
{
    case false:
        k = 0;
        break;
    case true:
        k = 1;
        break;
    case null:
        k = -1;
        break;
}

Тепер це можна реалізувати простіше:

var k = b switch
{
    false => 0,
    true  => 1,
    null  => -1
};

Циклічні конструкції в C# реалізовані так само як аналогічні конструкції в C++.

У C# є твердження goto для переходу на мітку. Єдиний випадок, коли використання goto виправдане, – це переривання декількох вкладених циклів. Наприклад:

int a;
  . . .
double b = 0;  
for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (i + j + a == 0)
        {
            goto label;
        }
        b += 1 / (i + j + a);
    }
}
label:
// інші твердження

Існує додаткова циклічна конструкція foreach для обходу масивів і колекцій.

2.5 Простори імен

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

namespace NewSpace
{
    public class SomeClass
    {
        public void f()
        {
        }
    }
    
    public struct SomeStruct
    {
    
    }
}

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

Як видно з наведеного прикладу, простір імен групує класи (та інші типи). Простори можна вкладати один в інший. Замість створення декількох вкладених просторів можна описувати простори імен з крапками. Наприклад, замість такого опису

namespace First
{
    namespace Second
    {
        namespace Third
        {
        }
    }
}    

можна застосувати більш компактний:

namespace First.Second.Third
{

}    

Версія C# 10 дозволяє визначати простір імен до кінця файлу без зайвих фігурних дужок (file-scoped namespace declaration). Якщо на початку файлу вказати заголовок простору імен, який закінчується крапкою з комою, це означає, що всі визначення до кінця файлу належать до цього простору імен:

namespace SomeSpace;
//
// Увесь код визначений у просторі імен SomeSpace
//

Для підключення всіх описів простору імен можна використовувати директиву using:

using NewSpace;

namespace OtherSpace { 
    public class Test {
        public void g() {
            SomeClass sc = new SomeClass();  // ім'я типу без префіксу імені простору
            sc.f();
        }
    }
}

За допомогою using у C# можна створювати синоніми для просторів імен. Наприклад:

using S = System;
...
    S.Console.WriteLine();

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

using S = System;
...
    S::Console.WriteLine();

У C# визначене контекстно-залежне ключове слово global – синонім глобального простору імен. Наприклад:

global::System.Console.WriteLine();    

Цей синонім застосовують для запобігання конфліктам імен.

У версії C# 10 розширені можливості використання директиви using. Зокрема, можна додати модифікатор global до будь-якої using-директиви. Тоді директива застосовуватиметься до всіх сирцевих файлів у проєкті.

2.6 Класи

2.6.1 Визначення класів

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

class Rectangle 
{
    public double Width;
    public double Height;
    public double Area()
    {
        return Width * Height;
    }
}

На відміну від C++, неабстрактні та нечасткові методи завжди реалізуються всередині визначення класу або структури.

Під час створення об'єкта класу поля ініціалізуються усталеними значеннями. C# допускає ініціалізацію полів початковими значеннями:

class Rectangle
{
    public double Width = 10;
    public double Height = 20;
    public double Area()
    {
        return Width * Height;
    }
}

Сам клас може бути визначений у просторі імен чи в іншому класі.

Поля і методи можуть бути оголошені з ключовим словом statіc. Звертання до таких полів і методів може здійснюватися без створення екземпляра класу. C# дозволяє звертання до таких елементів тільки через ім'я класу. Звертання до статичних елементів через ім'я об'єкта приводить до помилки компіляції. Статичні поля можуть бути ініціалізовані під час опису:

class NewClass 
{
    public static double x = 10;
    public static int    i = 20;
}

Як елементи класу можуть виступати константи. Їх описують за допомогою модифікатора const. Такі константи створюються компілятором і не можуть бути ніде змінені.

Мова C# підтримує закритий (private), захищений (protected), відкритий (public), внутрішній (internal), захищений внутрішній (protected internal) і закритий захищений (private protected) рівні доступу. Внутрішні рівні доступу визначають видимість у межах складання (assembly). Рівень private protected (представлений в C# 7.2) забезпечує доступ для похідних класів, оголошених у тому ж складанні (assembly). Сам клас може бути оголошений як public, інакше він буде доступний лише у межах складання. На відміну від C++, C# вимагає окремої специфікації доступу для кожного елемента (або групи елементів даних, описаних з одним специфікатором типу), при чому усталено передбачається private:

public class TestClass
{
    private int i;
    double x; // private

    public void SetI(int value)
    {
        i = value;
    }
   
    public int GetI() 
    {
        return i;
    }
  
    public void SetX(double value)
    {
        x = value;
    }

    public double GetX()
    {
        return x;
    }
}

Екземпляр класу створюється шляхом застосування операції new до конструктора:

TestClass tc = new TestClass();

У результаті об'єкт створюється в динамічній пам'яті. У даному прикладі tc – ім'я посилання на об'єкт. Змінні типу посилання на об'єкт класу можуть не посилатися ні на який екземпляр класу. У цьому випадку їм доцільно присвоїти значення порожнього посилання – null.

Конструктор являє собою функцію, яка здійснює ініціалізацію об'єкта. Ім'я конструктора збігається з ім'ям класу. Не можна вказувати тип результату конструктора. У класі може бути визначено кілька конструкторів. Якщо жоден конструктор явно не визначений, автоматично створюється усталений конструктор (без параметрів). Такий конструктор ініціалізує усі поля усталеними початковими значеннями.

Ключове слово this використовується як посилання на об'єкт, для якого викликаний метод. Використання this багато в чому аналогічно відповідному вказівнику в C++. Статичні методи не можуть використовувати посилання thіs.

Один конструктор можна викликати з іншого з використанням слова thіs і конструкції, аналогічної списку ініціалізації в C++.

class Rectangle
{
    double width;
    double height;
    public Rectangle(double width, double height) 
    {
        this.width = width;
        this.height = height;
    }
    public Rectangle() : this(10, 20) // виклик іншого конструктора
    {
    }
}

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

Статичний конструктор завжди викликається неявно – до створення екземпляра чи класу використання будь-якого статичного елемента класу. Наведемо приклад статичного конструктора:

class Counter 
{
    static int count; 
    static Counter() 
    { 
        count = 0; 
    }
}

У класі можуть бути одночасно статичний і нестатичний конструктори з однаковим набором параметрів.

Існує варіант нестатичних констант – поля з модифікатором readonly. Значення таких полів можуть задаватися тільки в конструкторі – це так звані константи екземпляра, що можуть бути різними для різних екземплярів того самого класу:

class NewClass 
{
    readonly double x = 10;
    public NewClass(double initX)
    {
        x = initX;
    }
}

У C# є деструктори. Синтаксис їхнього опису цілком аналогічний C++.

class NewClass 
{
    ~NewClass() // деструктор
    {

    }
}

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

using (X x = new X()) 
{
  
}
// виклик Dispose()

Примітка: клас X повинен реалізовувати інтерфейс IDisposable. Інтерфейси будуть розглянуті у наступній темі.

За допомогою using в C# можна створювати синоніми для імен класів:

using AliasToClass = NewSpace.SomeClass;
...
    AliasToClass atc = new AliasToClass(); // всередині функції якогось класу
  

2.6.2 Опис та використання методів

Метод – функція, яка описана всередині класу і має безпосередній доступ до інших елементів класу. На відміну від C++, неабстрактні та нечасткові методи завжди реалізуються всередині визначення класу. У C# немає глобальних функцій. Аналогом глобальної функції є статичний метод класу. Опис статичної функції у найпростішому випадку має таку структуру:

static тип_результату ім'я_функції(список_формальних_параметрів) тіло

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

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

static int Sum(int a, int b)
{
    int c = a + b;
    return c;
} 

Можна взагалі обійтися без змінної с:

static int Sum(int a, int b)
{
    return a + b;
} 

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

int x = 4;
int y = 5;
int z = Sum(a, b);
int t = Sum(1, 3); 

Щоб викликати публічний статичний метод іншого класу, слід використовувати такий синтаксис:

Class_name.Method_name(actual_parameters)

У такій формі викликають стандартні математичні функції. Наприклад

y = Math.Sin(x);

Починаючи з C# 6.0, можна імпортувати статичні елементи з певного класу, наприклад:

using static System.Math;

Тепер можна викликати статичні методи, визначені в класі Math, не використовуючи префіксу Math:

y = Sin(x);

Функція може бути без параметрів:

static int Zero()
{
    return 0;
}

Викликаючи таку функцію, також необхідно використовувати дужки:

Console.WriteLine(Zero());

Інструкція return у тілі функції забезпечує завершення роботи функції. Значення виразу після return стає значенням функції, яке ця функція повертає.

Функція може не повертати ніякого результату. Для позначення цього використовується тип void.

static void Hello()
{
    Console.WriteLine("Hello!");
}

У цьому випадку в тілі функції return може бути відсутнім. Якщо інструкція return присутня, то після неї не повинно бути ні якого виразу. Таку функцію можна викликати тільки окремою інструкцією.

Hello();

Функцію можна викликати з тіла цієї ж функції. Рекурсія – це виклик функції з неї самої безпосередньо або опосередковано.

Усталено параметри передаються до функцій за значенням, тобто значення фактичних параметрів копіюються в пам'ять, відведену для формальних параметрів. Додатковою можливістю C# є передача параметрів типів-значень за посиланням. Існує два способи опису таких параметрів. Перший спосіб припускає, що до виклику методу передані змінні були проініціалізовані. Для передачі параметрів за посиланням використовується ключове слово ref.

public static void Swap(ref int a, ref int b) 
{
    int c = a;
    a = b;
    b = c;
}

Під час виклику такого методу і передачі йому фактичних параметрів також варто використовувати ключове слово ref. Фактичні параметри повинні бути обов'язково ініціалізовані. Наведемо повний приклад використання функції Swap():

using System;

namespace SwapperApp
{
    class Swapper
    {
        public static void Swap(ref int a, ref int b) 
        {
            int c = a;
            a = b;
            b = c;
        }
 
        static void Main(string[] args)
        {
            int x = 1;
            int y = 10;
            Swap(ref x, ref y); 
            Console.WriteLine("x = " + x + " y = " + y); 
        }
    }
}

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

Починаючи з C# 7.0, також можна використовувати модифікатор ref перед функцією повернення типу. У цьому випадку також треба додати модифікатор ref під час виклику функції:

static ref int SecondName(ref int n) 
{
    return ref n;
}

static void Main(string[] args)
{
    int k = 3;
    ref int k2 = ref SecondName(ref k);
    k2 = 4;
    Console.WriteLine(k);
}

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

Припустимо, нам необхідно розв'язати таке рівняння:

|x| – b = 0

Оскільки коренів – два, доцільно використати параметри с атрибутами out:

using System;

namespace EquationWithAbsValue
{
    class Program
    {
        static void SolveEquation(double b, out double x1, out double x2)
        {
            x1 = b;
            x2 = -b;
        }

        static void Main(string[] args)
        {
            double b, x1, x2;
            b = 3;
            SolveEquation(b, out x1, out x2);
            Console.WriteLine("x1 = " + x1 + " x2 = " + x2);
        }
    }
}

Версія 7.0 C# дозволяє визначити необхідні змінні з модифікаторами out всередині виклику методу у списку фактичних аргументів. Після повернення із викликаного методу їх можна використовувати:

...

        static void Main(string[] args)
        {
            double b = 3;
            SolveEquation(b, out double x1, out double x2);
            Console.WriteLine("x1 = " + x1 + " x2 = " + x2);
        }
    }
}

Починаючи з C# 7.2 для передачі параметру за посиланням можна також використовувати модифікатор in. Розміщення ключового слова in перед параметром типу значення дозволяє оголосити параметр, який передається за посиланням, але його значення не можна змінити; це трактується як константа в тілі функції. Наприклад:

static double Sum(in double x, in double y)
{
    return x + y;
}

static void Main(string[] args)
{
    double x = 1, y = 2;
    Console.WriteLine(Sum(in x, in y)); // модифікатор in є обов'язковим
}

Якщо спробувати змінити значення таких параметрів у тілі функції, буде отримано помилку компілятора:

static double Sum(in double x, in double y)
{
    x = 4; // Помилка!
    // ...
}

Використання модифікатора in підвищує ефективність надсилання великих структур даних без копіювання.

Примітка: модифікатор in працює як посилання на константу в C++.

Версія 4.0 мови C# дозволяє описувати функції з усталеними параметрами (default parameters), як і в C++. Такий підхід дозволяє викликати одну функцію з різною кількістю параметрів. Наприклад

static double Sum(double a, double b = 0, double c = 0)
{
    return a + b + c;
}

Цю функцію можна викликати або з одним, або з двома, або з трьома параметрами:

double x = 0.1;
double y = 0.2;
double z = 0.3;
Console.WriteLine(Sum(x));
Console.WriteLine(Sum(x, y));
Console.WriteLine(Sum(x, y, z));

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

static void F(double x, int y = 0, int h) { } // Синтаксична помилка!

Під час виклику можна вказувати імена формальних параметрів (у версії 4.0 мови C#). Вказується ім'я параметру та після двокрапки – його значення. Наприклад, у наведеному нижче фрагменті програми обчислюється вираз y = ax + b:

static double Y(double a, double x, double b)
{
    return a * x + b;
}

static void Main(string[] args)
{
    Console.WriteLine(Y(a: 2, x: 3, b: 4)); // 10
}    

Ця можливість є особливо корисною в поєднанні з усталеними параметрами. Вона дозволяє вказувати лише необхідні параметри. Наприклад:

static double Y(double a = 1, double x = 0, double b = 0)
{
    return a * x + b;
}

static void Main(string[] args)
{
    Console.WriteLine(Y(a: 2, x: 3, b: 4)); // 10
    Console.WriteLine(Y(x: 5, b:11));       // 16
    Console.WriteLine(Y(x: 5));             // 5
    Console.WriteLine(Y());                 // 0
}    

Параметри з іменами повинні бути останніми у списку фактичних параметрів.

Починаючи з C# 7.0 можна створювати локальні функції: методи можна створювати в контексті іншого методу. Локальний метод можна викликати лише з контексту, в якому він оголошений. У C# 8.0 локальні функції також можуть бути статичними. Такі функції не можуть мати доступ до локальних змінних та параметрів. У наведеному нижче прикладі локальна функція sum() не може бути статичною, оскільки вона використовує значення n (локальний аргумент). Функція cube() є статичною, оскільки отримує всю необхідну інформацію зі свого списку аргументів:

static int SumOfCubes(int n)
{
    return sum();
    // локальна функція:
    int sum()
    {
        int result = 0;
        for (int k = 1; k <= n; k++)
        {
            result += cube(k);
        }
        return result;
    }
    // статична локальна функція:
    static int cube(int k)
    {
        return k * k * k;
    }
}

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

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

2.7 Властивості

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

У C# властивість описується разом зі своїм кодом. Після імені властивості міститься блок, у якому виділяються блоки set {} (для запису значення властивості) і get {} (для читання значення властивості). У блоці запису можна використовувати ключове слово value, що має зміст записуваного значення.

Властивості можна описати тільки для читання (відсутній set) чи тільки для запису (відсутній get). У наведеному нижче прикладі в класі Properties визначені властивості FirstPower і SecondPower:

using System;

namespace PropertyTest
{
    class Properties
    {
        private double firstPower; // поле
        public double FirstPower   // властивість для читання і запису
        {
            set
            {
                firstPower = value;
            }
            get
            {
                return firstPower;
            }
        }

        public double SecondPower // властивість тільки для читання
        {
            get
            {
                return firstPower * firstPower;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Properties p = new Properties();
            p.FirstPower = 10;                // запис
            Console.WriteLine(p.SecondPower); // читання
        }
    }
}

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

Починаючи з версії C# 3.0 (2007 рік) до синтаксису мови додані так звані автоматичні властивості:

  public int X { get; set; }

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

  public class Point
  {
      public int X { get; set; } // X = 0
      public int Y { get; set; } // Y = 0
      public Point()
      {
          X = 1;
          Y = 2;
      }
  }

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

  public int Z { get; private set; }    

Примітка: аналогічна можливість додана також для звичайних (неавтоматичних) властивостей, наприклад:

  private int z;
  public int Z
  {
      get { return z; }
      protected set { z = value; }
  }

Починаючи з версії 6.0 мови C#, можна створювати автоматичні властивості лише для читання:

public int X { get; }

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

Версія C# 6.0 також дозволяє ініціалізувати автоматичні властивості, наприклад:

int Initial { get; set; } = 1;

2.8 Робота з масивами

2.8.1 Одновимірні масиви

Масиви в C# є типами-посиланнями. Як і в С++, масиви індексуються починаючи з нуля. Під час опису масиву квадратні дужки ставляться після імені типу, а не імені змінної.

int[] a;

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

int[] a = new int[10];   // масив з 10 елементів типу int
double[] b;
b = new double[20];

У будь-який момент у програмі можна присвоїти масиву новий набір елементів:

a = new int[30];

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

Для визначення розміру масиву можна використовувати будь-які вирази цілого типу. Створення масиву передбачає ініціалізацію його елементів початковими усталеними значеннями. Для цілих та дійсних чисел це 0, для Булевих значень – false.

На відміну від C++, у C# масиви зберігаються разом з кількістю елементів. Кількість елементів масиву завжди можна отримати за допомогою спеціальної властивості Length (тільки для читання):

int[] a = new int[10];
Console.WriteLine(a.Length); // 10

Як і в C++, для звернення до елементу масиву використовують операцію []. Так виглядає типовий цикл для обходу масиву:

for (int i = 0; i < a.Length; i++)
{
    a[i] = 0;
}

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

int[] a1 = new int[3]{ 1, 2, 3 };
// при наявності списку ініціалізації можна не вказувати розмірність:
int[] a2 = new int[]{ 1, 2, 3 };
// можна опустити new:
int[] a3 = { 1, 2, 3 };

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

using System;

namespace ArrayTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Уведiть кiлькiсть елементiв масиву:");
            int size = int.Parse(Console.ReadLine() ?? "0");
            double[] a = new double[size];
            Console.WriteLine("Уведiть елементи масиву:");
            for (int i = 0; i < a.Length; i++)
            {
                a[i] = double.Parse(Console.ReadLine() ?? "0");
            }
            // Робота з масивом
            // ...
        }
    }

}
  

Тип елементів масиву, описаного локально у функції, можна вказувати неявно. Наприклад:

var a = new int[10];           // Масив цілих (int)
var b = new[] { 1.5, 2, 4 };   // Масив дійсних (double)

Неявна ініціалізація не дозволяє створювати масиви з елементами різних типів. Наприклад, такий опис є синтаксичною помилкою:

var c = new[] { 1, 'a', false }; // Помилка: елементи різних типів

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

double[] b = new double[10];
b[100] = 10;   // виняткова ситуація IndexOutOfRangeException

Масив зберігає свою довжину, яку можна отримати за допомогою властивості Length:

for (int i = 0; i < a.Length; i++)
{
    a[i] = i;
}

Для обходу масивів можна використовувати цикл foreach:

foreach (тип змінна in масив)
    тіло циклу

Наприклад:

int[] a = { 1, 2, 3 };
foreach (int x in a) 
{
    Console.WriteLine(x); // x – поточний елемент масиву
}

Не можна змінювати вміст масиву зсередини foreach. Такий цикл використовують тільки для читання елементів.

Оскільки масиви є типам-посиланнями, під час присвоєння масиву іншого масиву привласнюються посилання, дані при цьому не копіюються:

b = a; // b посилається на той самий масив, що і a

Клас System.Array є базовим для всіх масивів. Цей клас надає методи для створення, обробки, пошуку і сортування масивів. Для копіювання масивів можна скористатись статичною функцією Copy класу System.Array. Ця функція реалізована у двох варіантах:

// Копіювання вказаної кількості елементів, починаючи з початкового:
public static void Copy(Array from, Array to, int length);

// Копіювання вказаної кількості елементів, починаючи з fromIndex, у новий масив, починаючи з toIndex:
public static void Copy(Array from, int fromIndex, Array to, int toIndex, int length);

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

// копіювання масиву
int[] a = { 1, 2, 3, 4};
int[] b = new int[4];
Array.Copy(a, b, a.Length); // b містить {1, 2, 3, 4}
// аналогічно Array.Copy(a, 0, b, 0, a.Length); 
a = new int[] { 10, 20, 30, 40};
// копіювання піддіапазону
Array.Copy(a, 1, b, 2, 2);  // b містить {1, 2, 20, 30}    

Статична функція Array.Resize() дозволяє змінити розміри існуючого масиву. Наприклад:

int[] a = { 1, 2, 3, 4 };
Array.Resize(ref a, a.Length + 1);
foreach (int x in a)
{
    Console.Write(x + " "); // 1 2 3 4 0
}

Інші корисні функції класу System.Array будуть розглянуті пізніше.

Версія C# 8.0 представляє нові типи System.Index та System.Range. Ці типи спрощують роботу з індексами масивів. У найпростішому випадку ми можемо використовувати змінні типу Index замість цілих індексів:

int[] arr = { 20, 30, 40, 50 };
Index index = 0;
Console.WriteLine(arr[index]); // 20

Перевага типу Index – в можливості застосування оператора ^. У нашому випадку ^1 означає arr.length - 1, ^2arr.length - 2 і тощо.

index = ^1;
Console.WriteLine(arr[index]); // 50

Примітка: арифметичні операції, а також операції порівняння заборонені для даних типу Index.

Зазвичай використовують анонімні константи типу Index:

Console.WriteLine(arr[^2] + " " + arr[^1]); // 40 50

Тип System.Range представляє піддіапазон послідовності (масиву). Зазвичай змінні типу System.Range ініціалізуються за допомогою операції ..:

Range range = 0..3; // індекси 0, 1 і 2

Діапазон визначає початок і кінець діапазону, включаючи початок і не включаючи кінець діапазону. Тип Index використовується для визначення початку та кінця діапазону. Наприклад, [0..^0] представляє весь діапазон.

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

int[] arr = { 20, 30, 40, 50 }; // вихідний масив
int[] slice = arr[1..^0]; // 30 40 50

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

Range range = 0..3; // індекси 0, 1 і 2
foreach (var item in arr[range])
{
    Console.Write(item + " "); // 20 30 40
}

2.8.2 Багатовимірні масиви

У C# є два види багатомірних масивів – звичайні багатомірні масиви, усі рядки яких однакової довжини, і невирівняні (jagged) масиви, що являють собою масиви масивів.

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

int[,]  с2 = new int [2, 3];      // двовимірний масив
int[,,] с3 = new int [3, 4, 5];   // тривимірний масив

Під час ініціалізації початковими значеннями необхідно вказати вкладені фігурні дужки для кожної розмірності:

int[,] d = new int[2, 3]{{1, 10, 100}, {12, 13, 14}};

Звертання до елементів масиву здійснюється на кшталт аналогічної дії у мові Pascal:

d[1, 2] = 20;

Типовий цикл обходу масиву матиме такий вигляд (метод GetLength(), успадкований від System.Array повертає кількість елементів заданої розмірності):

int[,] arr = {{11, 12}, {21, 22}, {31, 32}};
for (int i = 0; i < arr.GetLength(0); i++)
{
    for (int j = 0; j < arr.GetLength(1); j++)
    {
        Console.Write(arr[i, j] + " ");
    }
    Console.WriteLine();
}

Невирівняні (jagged) масиви – це масиви масивів. Кожна розмірність такого масиву являє собою набір окремих масивів, різні елементи можуть мати різну довжину. Для опису невирівняних масивів потрібно вказувати стільки пар порожніх квадратних дужок, скільки розмірностей у масиву.

Створення невирівняного масиву здійснюється у два етапи:

  • створюється масив посилань на масиви;
  • створюються масиви з необхідною кількістю елементів; посилання на них записують у раніше створений масив.

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

using System;

namespace ArraysTest
{
    class ArrayClass
    {
        static void Main(string[] args)
        {
            int[][] arr = new int[3][];
            arr[0] = new int[] {1};
            arr[1] = new int[] {2, 3};
            arr[2] = new int[] {4, 6, 5};
            int[] sums = {0, 0, 0};
            for (int i = 0; i < arr.Length; i++)
            {
                for (int j = 0; j < arr[i].Length; j++)
                {
                    sums[i] += arr[i][j]; 
                }
            }
            for (int i = 0; i < sums.Length; i++)
            {
                Console.WriteLine(sums[i]);
            }
        }
    }
}

Ініціалізація невирівняних масивів початковими даними може бути здійснена у такий спосіб:

double[][] a = { new double[] { 1 }, new double[] { 2, 3 } };    

2.8.3 Масиви як параметри

Оскільки масиви є типами-посиланнями, вони завжди передаються у функції як параметри посилання:

static void Spoil(int[] a)
{
    a[2] = -100;
}

static void Main(string[] args)
{
    int[] a = { 1, 2, 3 };
    Spoil(a);
    foreach (int elem in a)
    {
        Console.Write(elem + " "); // 1 2 -100
    }
}    

Для опису функцій зі змінним числом параметрів потрібно використовувати спеціальний параметр – масив, перед яким стоїть модифікатор params. Такий параметр повинен йти останнім у списку параметрів методу. Усередині методу робота з таким параметром нічим не відрізняється від роботи зі звичайним масивом. При необхідності як фактичний параметр можна передати масив. У наведеному нижче прикладі функція повертає суму чисел.

public static double Sum(params double[] a)
{
    double result = 0;
    foreach (double x in a)
    {
        result += x;
    }
    return result;
}

static void Main(string[] args)
{
    Console.WriteLine(Sum(1, 2.5));            // 3.5
    Console.WriteLine(Sum(1, 2, 3, 4));        // 10
    double[] b = new double[5] { 1, 1, 1, 1, 1 };
    Console.WriteLine(Sum(b));                 // 5
}

2.9 Індексатори

Спеціальний вид властивостей, так званий індексатор, дозволяє звертатися до об'єкта класу за допомогою операції взяття індексу ([]). Щоб створити індексатор, слід визначити результуючий тип, за яким слідує ключове слово this, а потім визначення індексу в квадратних дужках. Інша частина визначення така ж, як і у визначенні властивості. У наведеному нижче класі індексатор забезпечує доступ до закритого поля:

class HiddenArray
{
    private int[] arr = { 1, 2, 3 };
    public int this[int index]
    {
        get
        {
            return arr[index];
        }
        set
        {
            arr[index] = value;
        }
    }
}

Завдяки індексатору з об'єктом створеного класу можна працювати як з масивом:

HiddenArray hiddenArray = new();
hiddenArray[0] = 4;
int k = hiddenArray[1];

Наведений нижче клас описує точку в просторі, а індексатор використовується для доступу по вимірам (1, 2 чи 3):

using System;

namespace Point3DApp
{
    class Point3D
    {
        double x, y, z;

        public Point3D(double x, double y, double z)
        {
            this.x = x;
            this.y = y;
            this.z = z;
        }
        public double this [int index] // індексатор
        {
            set
            {
                switch (index)
                {
                    case 1: x = value; break;
                    case 2: y = value; break;
                    case 3: z = value; break;
                }
            }
            get
            {
                switch (index)
                {
                    case 1:  return x;
                    case 2:  return y;
                    case 3:  return z;
                // В іншому випадку - максимальне double
                    default: return Double.MaxValue;
                }
            }
        }
    }
	
    class Test
    {
		
        static void Main(string[] args)
        {
            Point3D p3d = new Point3D(2, 3, 4);
            p3d[3] = 5;                   // запис властивості
            for (int i = 1; i <= 4; i++)
            {
                Console.WriteLine(p3d[i]);  // читання властивості
            }
        }
    }
}

Тип індексу не обов'язково повинен бути цілим.

2.10 Рядки

2.10.1 Загальні відомості

Рядки в C# – це екземпляри класу System.String. Об'єкти цього класу містять символи Unicode. Ключове слово string використовують як синонім класу System.String. Рядки є типами-посиланнями.

Об'єкт-рядок може бути створений під час опису посилання шляхом присвоєння йому рядкового літералу:

string s = "Перший рядок";

Константі-рядку може передувати символ @ (et). Це – так звані дослівні (verbatim) рядки. Обробка таких рядків передбачає ігнорування керуючих послідовностей на кшталт \t, \n, \\, \" тощо. Такі константи зручно використовувати для визначення шляхів до файлів, наприклад:

string path = @"c:\Users\Default";

Це еквівалентно "традиційній" формі літералу:

string path = "c:\\Users\\Default";

До окремих символів можна звертатися за допомогою квадратних дужок. Отримати кількість символів можна за допомогою властивості Length.

Нижче наведені методи класу System.String, які використовують найбільш часто.

Метод Аргументи Повертає Опис
CompareTo (string value) int Порівнює поточний рядок з аргументом-рядком. Результат від'ємний, якщо поточний рядок лексикографічно передує рядку-аргументу. Результат дорівнює 0, якщо рядки збігаються і додатний у протилежному випадку
Equals (string value) bool Порівнює поточний рядок з аргументом-рядком. Повертає true, якщо рядки збігаються
IndexOf> (string substring) int Повертає індекс, який відповідає першому входженню підрядка у рядок. Якщо підрядок не входить у рядок – повертає –1
IndexOf (char ch) int Повертає індекс, який відповідає першому входженню символу у рядок. Якщо символ відсутній – повертає –1
Substring (int beginindex, int endindex) string Повертає новий рядок, що є підрядком вихідного рядку
ToLower () string Повертає рядок, усі символи якого переведені в нижній регістр
ToUpper () string Повертає рядок, усі символи якого переведені у верхній регістр

Наведені нижче приклади демонструють використання методів обробки рядків.

string s1 = "Hello World.";
int i = s1.Length;   // i = 12
char c = s1[6]; // c = 'W'
i = s1.IndexOf('e');   // i = 1 (індекс 'e' у "Hello World.")
string s2 = "abcdef".Substring(2, 5); // s2 = "cde"
int k = "AA".CompareTo("AB");         // k = -1      

Мова C# дозволяє зшивати рядки за допомогою оператора +:

string s1 = "first";
string s2 = s1 + " and second";

Якщо один з операндів – рядок, а інший – ні, то цей операнд приводиться до рядкового представлення.

int n = 1;
string sn = "n дорівнює" + n; // "n дорівнює 1"
double d = 1.1;
string sd = d + ""; // "1.1"

Можна також використовувати операцію "+=" для дошивання в кінець рядка.

Можна створювати масиви рядків. Як і для інших типів-посилань, масив зберігає не рядки безпосередньо, а посилання на них. Функція Sort() класу Array реалізована також для рядків. Рядки впорядковуються за алфавітом:

string[] a = { "dd", "ab", "aaa", "aa" };
Array.Sort(a); // aa aaa ab dd     

Вони також не можуть бути змінені після створення.

Обійти усі символи можна також за допомогою циклу foreach. Наприклад:

string s = "First";
for (int i = 0; i < s.Length; i++)
{
    Console.WriteLine(s[i]);
}
foreach (char c in s) // еквівалентний обхід: 
{
    Console.WriteLine(c);
}

Примітка: Починаючи з C# 8.0, рядки, як масиви, підтримують нові функції індексів та діапазонів.

Метод Split() дозволяє отримати масив рядків з окремих слів рядку. Наприклад

string s = "The first sentence";
string[] arr = s.Split();
foreach (string word in arr)
{
    Console.WriteLine(word);
}

Клас System.String надає низку корисних статичних методів для отримання та маніпуляції з рядками. Наприклад, метод Join() дозволяє перетворити на рядки і зшити елементи масиву. Перший параметр функції – розділювач (символ або рядок), другий параметр – масив, елементи якого треба зшити:

int[] arr = { 1, 2, 3 };
string s = string.Join("  ", arr); // розділювач - два пропуски 
WriteLine(s); // "1  2  3"

Метод Concat() зшиває елементи масиву без розділювача:

s = string.Concat(arr); // "123"

Метод Format() забезпечує форматування рядка. Наприклад:

int k = 10;
double b = 2;
string s = string.Format("k = {0}, b = {1}", k, b);

Правила форматування рядків будуть розглянуті нижче.

2.10..2 Модифікація рядка

Екземпляр класу System.String не може бути змінений після створення. Робота деяких методів та операцій зовні нагадує модифікацію об'єкту, однак насправді створюється новий рядок.

string s = "ab"; // У пам'яті один рядок
s = s += "c";    // У пам'яті три рядки: "ab", "c" та "abc". На "abc" посилається s
// Зайві рядки потім будуть видалені збирачем сміття

Існує спеціальний клас StringBuilder, що дозволяє модифікувати вміст рядкового об'єкта. Цей клас описаний в просторі імен System.Text. Створити об'єкт типу StringBuilder можна з існуючого рядка. Після модифікації можна створити новий об'єкт класу String, використовуючи об'єкт класу StringBuilder. Наприклад:

string s = "abc";
StringBuilder sb1 = new StringBuilder(s);    // Виклик конструктора
StringBuilder sb2 = new StringBuilder("cd"); // Виклик конструктора
// модифікація sb1 та sb2
// ...
string s1 = sb1 + "";        // Перетворення типів
string s2 = sb2 + "";        // Перетворення типів

Клас StringBuilder надає низку методів для модифікації вмісту. Це такі методи, як Append(), Remove(), Insert(), Replace() та деякі інші. Розглянемо використання цих функцій на наведеному нижче прикладі:

using System;
using System.Text;

namespace StringBuilderTest
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = "abc";
            StringBuilder sb = new StringBuilder(s);
            sb.Append("d");         // abcd
            sb[0] = 'f';            // fbcd
            sb.Remove(1, 2);        // fd
            sb.Insert(1, "gh");     // fghd
            sb.Replace("h", "mn");  // fgmnd
            Console.WriteLine(sb);
        }
    }
}

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

2.10.3 Інтерполяція рядків

Додаткова функціональність, яка була додана в C# 6, передбачає можливість вбудови виразів у рядок. Ця можливість називається інтерполяцією рядків (string interpolation). Перша частина відповідного виразу – це деякий рядок формату з префіксом $. У межах цього рядка формату можна розташувати вирази, результати яких будуть перетворені у подання у вигляді рядка. Наприклад:

string s = $"{7 - 5} * {1 + 1} = {1 + 3}";  // 2 * 2 = 4

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

d - десяткове (ціле) число
f - дійсне число з фіксованою крапкою
e - експоненційна форма числа
x - шістнадцяткове число

Є також інші символи форматування. Після символів можна додавати цілі значення – ширину поля виведення. Наприклад:

int i = 10;
double x = 2012;
string s = $"i = {i:d8} x = {x:f}, the same: {x:e}";
WriteLine(s); // i = 00000010 x = 2012.00, the same: 2.012000e+003

2.11 Консольне виведення та введення

Для консольного виведення та введення застосовують клас Console простору імен System. Функція Write() виводить указані дані починаючи з поточного місця консольного вікна. У найпростішому випадку – це параметр будь-якого типу. Функція WriteLine() крім того здійснює перехід на новий рядок. Виклик WriteLine() без параметрів забезпечує перехід на новий рядок.

Першим параметром Write() та WriteLine() можна вказати рядок формату виведення. У фігурних дужках вказуються індекси параметрів, що вказані далі. Фактично значення, які виводяться, включаються у рядок у вказаних місцях. Ці значення перелічують через кому. Наприклад, після виконання наведеного фрагмента коду,

int k = 10;
double b = 2;
Console.WriteLine("k = {0}, b = {1}", k, b);    

ми отримаємо такі дані у консольному вікні:

k = 10, b = 2

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

int i = 10;
Console.WriteLine("{0:d8}", i);      // 00000010
Console.WriteLine("{0:x}", i);       // a
double d = 2012;
Console.WriteLine("{0:f6}", d);      // 2012.000000
Console.WriteLine("{0:f} {0:e}", d); // 2012.00 2.012000e+003    

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

Для введення даних застосовують функцію ReadLine() класу Console. Ця функція повертає рядок, який можна перетворити на необхідне число за допомогою статичних функцій Parse, які реалізовані для стандартних типів-значень (int, double тощо). Наприклад:

int i = int.Parse(Console.ReadLine() ?? "0");
double d = double.Parse(Console.ReadLine() ?? "0");

Використання оператору ?? тут настійно рекомендується, оскільки метод readLine() може потенційно повернути null.

Метод TryParse() дозволяє зчитувати значення з деякого рядка (перший параметр) і помістити перетворене значення у задану змінну (другий параметр). Результат типу bool може бути або true (перетворення успішним), або false (перетворення не вдалося). Наприклад:

double z;
if (double.TryParse(ReadLine(), out z))
{
    // Робота зі значенням z
}
else
{
    WriteLine("Wrong number");
}

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

3.1 Робота з перемикачем

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

x
y
1
12
2
14
інше значення
16

Програма (яка базується на C#7) може бути такою:

using System;

namespace Switcher
{
    class Program
    {
    
        static void Main(string[] args)
        {
            int x = int.Parse(Console.ReadLine() ?? "0");
            int y;
            switch (x)
            {
                case 1:  y = 12; break;
                case 2:  y = 14; break;
                default: y = 16; break;
            }
            Console.WriteLine(y);
        }
    }
}

Цю програму можна істотно спростити, використовуючи нові можливості C# 8 (вираз switch) та C# 9 (неявний клас з функцією Main()):

using static System.Console;

int x = int.Parse(ReadLine() ?? "0");
int y = x switch
{
    1 => 12,
    2 => 14,
    _ => 16,
};
WriteLine(y);

3.2 Робота з локальними методами та модифікатором ref

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

using static System.Console;

if (double.TryParse(ReadLine(), out double a) && double.TryParse(ReadLine(), out double b))
{
    replaceWithArithmeticMean();
    WriteLine("a = {0} b = {1}", a, b);
}
else
{
    WriteLine("Wrong number");
}

void replaceWithArithmeticMean()
{
    var c = (a + b) / 2;
    a = b = c;
}

Функція є локальною, оскільки весь код цього файлу є неявним тілом функції Main().

Недоліком цього підходу є можливість модифікувати лише одну пару змінних. Якщо ми хочемо застосувати цю функцію до кількох пар змінних, ми можемо створити статичну локальну функцію з параметрами:

using static System.Console;

if (double.TryParse(ReadLine(), out double a) && double.TryParse(ReadLine(), out double b))
{
    replaceWithArithmeticMean(ref a, ref b);
    WriteLine("a = {0} b = {1}", a, b);
}
else
{
    WriteLine("Wrong number");
}

double c = 3, d = 4;
replaceWithArithmeticMean(ref c, ref d);
WriteLine("c = {0} d = {1}", c, d);

static void replaceWithArithmeticMean(ref double a, ref double b)
{
    var c = (a + b) / 2;
    a = b = c;
}

Цей підхід є більш універсальним.

3.3 Застосування null-сумісних типів і модифікатора out

Припустимо, необхідно розробити функцію обчислення зворотної величини. Ця функція не може бути обчислена, якщо аргумент дорівнює 0. Як тип результату функції можна застосувати null-сумісний тип.

using static System.Console;

namespace ReciprocalTest
{
    class Program
    {
        static double? Reciprocal(double x, out int errorCode)
        {
            errorCode = 0;
            if (x == 0)
            {
                errorCode = -1;
                return null;
            }
            return 1 / x;
        }

        static void Main(string[] args)
        {
            Console.Write("Enter x: ");
            double x = double.Parse(Console.ReadLine() ?? "0");
            double? y = Reciprocal(x, out int errorCode);
            WriteLine(errorCode == 0 ? y : "Error");
        }
    }
}

3.4 Лінійне рівняння

Припустимо, необхідно спроєктувати клас, що представляє лінійне рівняння. Поля цього класу – коефіцієнти a і b, а також корінь x, який необхідно знайти. Можна запропонувати таку програму:

using System;

namespace LinearEquation
{
    public class LinearEquation
    {
        public double A  { get; set; }
        public double B  { get; set; }
        public double? X { get; private set; }

        public LinearEquation()
        {
            A = B = 0;
            X = null;
        }

        public int Solve()
        {
            if (A == 0)
            {
                if (B == 0)
                {
                    return -1; // безмежна кількість розв'язків
                }
                else
                {
                    return 0;  // немає розв'язків
                }
            }
            X = -B / A;
            return 1;      // один розв'язок
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            LinearEquation e = new LinearEquation();
            e.A = double.Parse(Console.ReadLine() ?? "0");
            e.B = double.Parse(Console.ReadLine() ?? "0");
            switch (e.Solve())
            {
                case -1: Console.WriteLine("Безмежна кiлькiсть розв'язкiв"); break;
                case 0:  Console.WriteLine("Немає розв'язкiв"); break;
                case 1:  Console.WriteLine("X = " + e.X); break;
            }
        }
    }
}

3.5 Добуток чисел, які вводяться з клавіатури

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

using System;

namespace Product
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] a = { };
            int k;
            do
            {
                k = int.Parse(Console.ReadLine() ?? "0");
                if (k == 0)
                {
                    break;
                }
                Array.Resize(ref a, a.Length + 1);
                a[a.Length - 1] = k;
            }
            while (true);
            int product = 1;
            foreach (int x in a)
            {
                product *= x;
            }
            Console.WriteLine(product);
        }
    }
}

3.6 Робота з двовимірними та невирівняними масивами

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

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

using System;

namespace JaggedArray
{
    class Program
    {
        static void Main(string[] args)
        {
            double[,] a = {{1.5, 0, -1},
                           {-12, -3, 0},
                           {7, 10, -11},
                           {1, 2, 3.5}};
            double[][] b = new double[a.GetLength(0)][];
            for (int i = 0; i < a.GetLength(0); i++)
            {
                int count = 0;
                for (int j = 0; j < a.GetLength(1); j++)
                {
                    if (a[i, j] > 0)
                    {
                        count++;
                    }
                }
                b[i] = new double[count];
            }
            for (int i = 0; i < a.GetLength(0); i++)
            {
                int k = 0;
                for (int j = 0; j < a.GetLength(1); j++)
                {
                    if (a[i, j] > 0)
                    {
                        b[i][k++] = a[i, j];
                    }
                }
            }
            for (int i = 0; i < b.Length; i++)
            {
                for (int j = 0; j < b[i].Length; j++)
                {
                    Console.Write(b[i][j] + " ");
                }
                Console.WriteLine();
            }
        }
    }
}

3.7 Сума цифр

Для знаходження суми цифр цілого числа можна скористатись його представленням у вигляді рядка:

using System;

namespace SumOfDigits
{
    class Program
    {
        static void Main(string[] args)
        {
            String n = Console.ReadLine() ?? "";
            int sum = 0;
            for (int i = 0; i < n.Length; i++)
            {
                sum += int.Parse(n[i] + "");
            }
            Console.WriteLine(sum);
        }
    }
}

3.8 Видалення зайвих пропусків

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

using System;

namespace SpaceRemover
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = Console.ReadLine() ?? "";
            while (s.IndexOf("  ") >= 0)
            {
                s = s.Replace("  ", " ");
            }
            Console.WriteLine(s);
        }
    }
}

Якщо, наприклад ввести такий рядок,

To    be  or not to                be

одержимо результат:

To be or not to be

3.9 Обробка даних про книги на книжковій полиці

Припустимо, у середовищі MS Visual Studio необхідно розробити консольну програму, в якій обробляються дані про книги на книжковій полиці. Інформація про окрему книгу складається з назви (string), року видання (int) та списку авторів. Для опису автора можна поки скористатися типом рядку (string). Необхідно реалізувати функцію пошуку книг, у назві яких є певна послідовність символів.

У середовищі MS Visual Studio створюємо новий проєкт – консольний застосунок (New Project | Console App) з ім'ям (Project Name) LabFirst. Для рішення обираємо ім'я (Solution name) Bookshelf. Отримуємо такий вихідний текст, який міститься у файлі Program.cs:

using System;

namespace LabFirst
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}

Нові класи додаватимемо до простору імен LabFirst. Це можна зробити вручну, або за допомогою функції Insert Snippet... контекстного меню. Для реалізації останнього підходу необхідно:

  • додати порожній рядок всередині простору імен LabFirst, перед описом класу Program
  • встановити курсор на новому порожньому рядку
  • обрати у контекстному меню Snippet | Insert Snippet... | Visual C# | class

Після перелічених дій на вказаному місці з'явиться порожній клас з ім'ям MyClass. Це ім'я слід змінити на Book. До нового класу додаємо публічні властивості Title, Year та Authors. Можна також видалити зайві using-директиви. Отримаємо такий код:

using System;

namespace LabFirst
{
    public class Book
    {
        public string Title { get; set; }
        public int Year { get; set; }
        public string[] Authors { get; set; }
    } 

    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}

Після опису властивостей слід додати конструктор. Його параметри відповідають переліченим властивостям. Для передачі даних про авторів можна застосувати масив з атрибутом params. На жаль, Visual Studio не дозволяє згенерувати такий конструктор автоматично. Код, створений вручну, матиме такий вигляд:

    public Book(string title, int year, params string[] authors)
    {
        Title = title;
        Year = year;
        Authors = authors;
    }    

Придасться також метод для виведення на консоль даних про книгу. Він матиме такий вигляд:

    public void Print()
    {
        Console.WriteLine("Назва: \"{0}\". Рiк видання: {1}", Title, Year);
        Console.WriteLine("   Автор(и):"); 
        for(int i = 0; i < Authors.Length; i++)
        {
            Console.Write("      {0}", Authors[i]);
            if (i < Authors.Length - 1)
            {
                Console.WriteLine(",");
            }
            else
            {
                Console.WriteLine("");
            }
        }
    }

У тілі цього методу в окремому рядку виводиться назва та рік видання, а потім у окремих рядках автори. Якщо автор у списку – не останній, до рядку додається кома.

Наступним буде клас Bookshelf. Він містить властивість Books – масив книжок. Одночасно можна додати відповідний конструктор:

public class Bookshelf
{
    public Book[] Books { get; set; }
    
    public Bookshelf(params Book[] books)
    {
        Books = books;
    }
}

Клас повинен надавати методи для друку вмісту полиці та для пошуку книг із визначеною послідовністю літер у назві:

    public void Print()
    {
        Console.WriteLine("----------Книжки:----------");
        foreach (Book book in Books)
        {
            book.Print();
        }
    }

    public Book[] ContainsCharacters(string characters)
    {
        Book[] found = new Book[0];
        foreach (Book book in Books)
        {
            if (book.Title.Contains(characters)) 
            {
                // Додаємо новий елемент до масиву:
                Array.Resize(ref found, found.Length + 1);
                found[found.Length - 1] = book;
            }
        }
        return found;
    }    

В останньому методі спочатку створюємо порожній масив, потім у циклі перевіряємо наявність послідовності літер у назвах книжок. Якщо послідовність знайдена, змінюємо довжину масиву на одиницю та заносимо у новий елемент посилання на знайдену книжку. Тепер можна здійснити тестування створених методів у функції Main() класу Program:

    static void Main(string[] args)
    {
        // Створюємо полицю з трьома книжками:
        Bookshelf bookshelf = new Bookshelf(
            new Book("The UML User Guide", 1999, "Grady Booch", "James Rumbaugh", "Ivar Jacobson"),
            new Book(@"Об'єктно-орiєнтоване моделювання програмних систем", 2007, "Дудзяний I.М"),
            new Book("Thinking in Java", 2005, "Bruce Eckel")
        );
        // Виводимо дані на екран:
        bookshelf.Print();
        Console.WriteLine("Уведiть послiдовнiсть лiтер:");
        string sequence = Console.ReadLine() ?? "";
        // Шукаємо книжки з певною послідовністю літер:
        Bookshelf newBookshelf = new Bookshelf(bookshelf.ContainsCharacters(sequence));
        // Виводимо результат на екран:
        newBookshelf.Print();
    }

Весь текст програми матиме такий вигляд:

using System;

namespace LabFirst
{
    public class Book
    {
        public string Title { get; set; }
        public int Year { get; set; }
        public string[] Authors { get; set; }

        public Book(string title, int year, params string[] authors)
        {
            Title = title;
            Year = year;
            Authors = authors;
        }

        public void Print()
        {
            Console.WriteLine("Назва: \"{0}\". Рiк видання: {1}", Title, Year);
            Console.WriteLine("   Автор(и):"); 
            for(int i = 0; i < Authors.Length; i++)
            {
                Console.Write("      {0}", Authors[i]);
                if (i < Authors.Length - 1)
                {
                    Console.WriteLine(",");
                }
                else
                {
                    Console.WriteLine("");
                }
            }
        }
    }

    public class Bookshelf
    {
        public Book[] Books { get; set; }

        public Bookshelf(params Book[] books)
        {
            Books = books;
        }

        public void Print()
        {
            Console.WriteLine("----------Книжки:----------");
            foreach (Book book in Books)
            {
                book.Print();
            }
        }

        public Book[] ContainsCharacters(string characters)
        {
            Book[] found = new Book[0];
            foreach (Book book in Books)
            {
                if (book.Title.Contains(characters))
                {
                    // Додаємо новий елемент до масиву:
                    Array.Resize(ref found, found.Length + 1);
                    found[found.Length - 1] = book;
                }
            }
            return found;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Створюємо полицю з трьома книжками:
            Bookshelf bookshelf = new Bookshelf(
                new Book("The UML User Guide", 1999, "Grady Booch", "James Rumbaugh", "Ivar Jacobson"),
                new Book(@"Об'єктно-орiєнтоване моделювання програмних систем", 2007, "Дудзяний I.М"),
                new Book("Thinking in Java", 2005, "Bruce Eckel")
            );
            // Виводимо дані на екран:
            bookshelf.Print();
            Console.WriteLine("Уведiть послiдовнiсть лiтер:");
            string sequence = Console.ReadLine() ?? "";
            // Шукаємо книжки з певною послідовністю літер:
            Bookshelf newBookshelf = new Bookshelf(bookshelf.ContainsCharacters(sequence));
            // Виводимо результат на екран:
            newBookshelf.Print();
        }
    }
}

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

  1. Створити класи з конструкторами і властивостями для опису студента та його оцінок.
  2. Створити класи з конструкторами і властивостями для опису книжкового магазина та книжок.
  3. Створити клас з конструктором для опису точки в тривимірному просторі.
  4. Створити клас з конструктором для опису товару (зберігаються назва та ціна).
  5. Створити клас з конструктором для опису користувача (зберігаються ім'я та пароль).
  6. Створити функцію з двома параметрами-посиланнями, яка збільшує один параметр на 1 та зменшує другий на 2.
  7. Описати одновимірний масив, уводити з клавіатури значення його елементів та дописувати їх до масиву. Процес завершується уведенням нуля. Вивести суму елементів.
  8. Проініціалізувати двовимірний масив дійсних чисел списком початкових значень, замінити усі нулі одиницями, а від'ємні значення – нулями.
  9. Проініціалізувати двовимірний масив дійсних чисел списком початкових значень, замінити усі нулі середнім арифметичним усіх елементів.
  10. Створити клас для опису паралелепіпеда. Забезпечити звертання до довжини, ширини та висоти як через імена властивостей, так і через індексатор.
  11. Увести речення та вивести в окремих рядках його слова.
  12. Увести речення, зшити усі слова та вивести на екран.

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

  1. У чому полягають особливості платформи .NET?
  2. Чим відрізняються платформи .NET Framework і .NET Core?
  3. Як пов'язані типи C# зі стандартними типами CLR?
  4. У чому переваги використання checked/unchecked блоків?
  5. У чому полягають особливості використання switch у C#?
  6. Що таке простори імен C# і з якою метою їх створюють?
  7. Як здійснюється опис та підключення просторів імен?
  8. З яких основних елементів складається опис класу?
  9. Які рівні доступу до елементів класу (директиви видимості) підтримує C#?
  10. Як визначити доступ до елементів усередині складання (assembly) або простору імен?
  11. Чи завжди необхідно явно ініціалізувати поля класу?
  12. Чим відрізняються статичні та нестатичні елементи класу?
  13. У чому полягає зміст інкапсуляції?
  14. Як можна використовувати посилання this?
  15. Скільки конструкторів без параметрів може бути створено в одному класі?
  16. Як здійснюється використання конструкторів з інших конструкторів?
  17. Використання readonly. Де можна ініціалізувати елементи readonly?
  18. Деструктори у C#. Коли викликаються деструктори?
  19. Які переваги дає використання оператору using?
  20. У чому є відміни передачі параметрів за допомогою ref та out?
  21. Поняття властивостей. У чому полягають особливості використання властивостей?
  22. Чим відрізняється опис властивостей від опису методів?.
  23. Як створити властивості тільки для запису і тільки для читання?
  24. У чому переваги та недоліки автоматичних властивостей?
  25. Для чого використовують тип Index?
  26. У чому полягають особливості багатовимірних масивів у порівнянні з C++?
  27. У чому полягають особливості створення та ініціалізації невирівняних масивів?
  28. Коли і для чого застосовують параметри функції з атрибутом params?
  29. Опис індексаторів. Чим відрізняється використання індексаторів від роботи з масивами?
  30. Що таке дослівні рядки та де їх доцільно застосовувати?
  31. Чи можна змінити вміст раніше створеного рядка?
  32. Як змінити конкретний символ у рядку?
  33. У чому є недоліки й переваги класу StringBuilder у порівнянні з класом String?
  34. Як здійснюється форматування даних під час виведення?

 

up