🏫 Lviv Polytechnic National University labs and other staff 👨🏻🎓
Фреймворк .NET представляє потужну платформу для створення додатків. Можна виділити наступні її основні риси:
Потужна бібліотека класів .NET представляє єдину для всіх підтримуваних мов бібліотеку класів. І яке б додаток ми не збиралися писати на C # - текстовий редактор, чат або складний веб-сайт - так чи інакше ми задіємо бібліотеку класів
Різноманітність технологій. Середовище виконання CLR і базова бібліотека класів є основою для цілого стека технологій, які розробники можуть задіяти при побудові тих чи інших додатків. Наприклад, для роботи з базами даних в цьому стеку технологій призначена технологія ADO.NET. Для побудови графічних додатків з багатим насиченим інтерфейсом - технологія WPF. Для створення веб-сайтів - ASP.NET і т.д. Також ще слід відзначити таку особливість мови C # і фреймворка .NET, як автоматичне збирання сміття. А це означає, що нам в більшості випадків не доведеться, на відміну від С ++, піклуватися про звільнення пам’яті. Вищезазначене середовище CLR самостійно викличе збирач сміття і очистить пам’ять.
Нерідко додаток, створений на C #, називають керованим кодом (managed code). Що це означає? А це означає, що для цієї програми створений на основі платформи .NET і тому керується середовищем CLR, який завантажує додаток і при необхідності очищає пам’ять. Але є також додатки, наприклад, створені на мові С++, які компілюються не в спільну мову CIL, як C # або VB.NET, а в звичайний машинний код. В цьому випадку .NET не керує додатком.
Код на C # компілюється в додаток з розширеннями exe або dll на мові CIL. Далі при запуску на виконання подібної програми відбувається JIT-компіляція (Just-In-Time) в машинний код, який потім виконується. При цьому, оскільки наш додаток може бути великим і містити багато інструкцій, в поточний момент часу компілюватиметься лише та частина програми, до якої безпосередньо йде звернення. Якщо ми звернемося до іншої частини коду, то вона буде скомпільована з CIL в машинний код. При тому вже скомпільована частина програми зберігається до завершення роботи програми.
Для створення додатків на C # будемо використовувати безкоштовне середовище розробки - Visual Studio Community 2015 року, Також можна використовувати Visual Studio 2013. При інсталяції Visual Studio на ваш комп’ютер будуть встановлені всі необхідні інструменти для розробки програм, в тому числі фреймворк .NET 4.6. Після завершення встановлення створимо першу програму. Вона буде простенькою. Спочатку відкриємо Visual Studio і вгорі в рядку меню виберемо пункт File (Файл) -> New (Створити) -> Project (Проект). Перед нами відкриється діалогове вікно створення нового проекту:
Рис. 1. Створення консольного застосування на мові C#
Після цього Visual Studio створить і відкриє нам проект:
Рис. 2. Структура проекту
По центру відображено вихідний код на мові C# (1). Праворуч знаходиться вікно Solution Explorer, в якому можна побачити структуру нашого проекту (2). В даному випадку у нас згенерувана за замовчуванням структура: вузол Properties (3) або Властивостей (він зберігає файли властивостей додатки і поки нам не потрібен); вузол References - це вузол містить збірки dll, які додані в проект за замовчуванням. Ці збірки якраз містять класи бібліотеки .NET, які буде використовувати C #. Однак не завжди все збірки потрібні. Непотрібні потім можна видалити, в той же час якщо знадобиться додати якусь потрібну бібліотеку, то саме в цей вузол вона буде додаватися.
Спочатку розберемо, що весь цей код представляє:
/* Підключення простору імен */
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
/* оголошення нового простору імен */
namespace FirstApp {
/* оголошення нового класу */
class Program {
/* оголошення нового методу */
static void Main (string [] args) {
// Body
} /* Кінець оголошення нового методу */
} /* Кінець оголошення нового класу */
} /* Кінець оголошення нового простору імен */
На початку файлу йдуть директиви using після яких йдуть назви просторів імен. Простори імен є організацію класів. Наприклад, на першому рядку using System; підключається простір імен System, яке містить фундаментальні і базові класи платформи
.NET. Підключені бібліотеки dll можна побачити у вікні Solution Explorer, відкривши вузол References:
Так, ви можете побачити там бібліотеку System.dll
, яка містить класи з простору імен System. Однак точної відповідності між просторами імен і назв файлів dll немає. Другий рядок знову ж підключається вкладене простір імен System.Collections.Generic: тобто у нас в просторі імен System визначено простір імен Collections, а вже в ньому простір імен Generic. І так як C # має Сі-подібний синтаксис, кожен рядок завершується крапкою з комою, а кожен блок коду поміщається в фігурні дужки.
Далі починається вже власне наш простір імен, який буде створювати окрему збірку або виконувану програму: спочатку йде ключове слово namespace, Після якого назва простору імен. За замовчуванням Visual Studio дає йому назву проекту. Далі всередині фігурних дужок йде блок простору імен. Простір імен може включати інші простори або класи. В даному випадку у нас за замовчуванням згенерований один клас - Program. Класи оголошуються схожим способом - спочатку йде ключове слово class, А потім назву класу, і далі блок самого класу в фігурних дужках.
Клас може містити різні змінні, методи, властивості, інші інструкції. В даному випадку у нас оголошений один метод Main. Зараз він порожній і нічого не робить. У програмі на C # метод Main є вхідною точкою програми, з нього починається все керування. Він обов’язково повинен бути присутнім в програмі. Слово static вказує, що метод Main - статичний, а слово void- що він не повертає ніякого значення. Далі в дужках у нас йдуть параметри методу - string [] args - це масив args, який зберігає значення типу string, тобто рядки. В даному випадку ні нам поки не потрібні, але в програмі це ті параметри, які передаються при запуску програми з консолі.
Тепер змінимо весь цей код на наступний:
using System;
namespace FirstApp {
class Program {
static void Main (string [] args) {
Calculator calc = new Calculator ();
calc.Add (2, 3);
}
}
//оголошення нового класу
class Calculator {
public void Add (int x, int y) {
int z = x + y;
Console.WriteLine ( " Сума {0} і {1} дорівнює {2}", x, y, z);
Console.ReadLine ();
}
}
}
Я додав в наш простір імен новий клас - Calculator, який має один метод Add. Цей метод приймає в якості параметрів два числа - x і y і складає їх. А суму виводимо на консоль за допомогою методу Console.WriteLine. Метод Console.ReadLine
використовується для введення від користувача, щоб командний рядок не закрився відразу ж після виведення результату. У цих двох рядках коду я звертаюся до класу Console, який знаходиться в просторі імен System. Це простір підключено на початку за допомогою директиви using. Однак нам необов’язково підключати простір імен. Ми можемо навіть видалити перший рядок, але в цьому випадку ми тоді повинні будемо вказати повний шлях до використовуваного класу. Наприклад, в нашому випадку ми могли б написати: Console.ReadLine()
.
Після оголошення нового класу ми можемо використовувати його в методі Main
:
// створення об'єкта нового класу
Calculator calc = new Calculator ();
// виклик методу Add нового класу
calc.Add (2, 3);
Пізніше ми розберемо створення об’єктів, чому саме такий синтаксис використовується,
А зараз же достатньо розуміти, що всі дії, які ми хочемо зробити, ми робимо в методі Main, так як це вхідна точка в програму. Якби ми не звернулися до методу Add, то він би ніколи б не спрацював.Тепер ми можемо запустити на виконання за допомогою клавіші F5 або з панелі інструментів, натиснувши на зелену стрілку. І якщо ви все зробили правильно, то вам відобразиться консоль де буде красуватися число 5 - тобто сума чисел 2 і 3.
Отже, ми створили перший додаток. Ви його можете знайти на жорсткому диску в папці проекту в каталозі bin/Debug
(буде називатися по імені проекту і мати розширення exe) і потім вже запускати без Visual Studio
, а також переносити його на інші комп’ютери, де є .NET
.
Як і в багатьох мовах програмування, в C # є своя система типів даних, яка використовується для створення змінних. Вона представлена наступними типами:
bool
: Зберігає значення true або false. Представлений системним типом System.Boolean
byte
: Зберігає ціле число від 0 до 255 і займає 1 байт. Представлений системним типом System.Byteshort
: Зберігає ціле число від -32768 до 32767 і займає 2 байта. Представлений системним типом System.Int16int
: Зберігає ціле число від -2147483648 до 2147483647 і займає 4 байта. Представлений системним типом System.Int32long
: Зберігає ціле число від -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 і займає 8 байт. Представлений системним типом System.Int64float
: Зберігає число з плаваючою крапкою від -3.4 * 1038 до 3.4 * 1038 і займає 4 байта. Представлений системним типом System.Singledouble
: Зберігає число з плаваючою крапкою від ± 5.0 * 10324 до ± 1.7 * 10308 і займає 8 байта. Представлений системним типом System.Doublechar
: Зберігає одиночний символ в кодуванні Unicode і займає 2 байта. Представлений системним типом System.Charstring
: Зберігає набір символів Unicode. Представлений системним типом System.String
object
: Може зберігати значення будь-якого типу даних і займає 4 байта на 32-розрядної платформі і 8 байт на 64-розрядної платформі. Представлений системним типом System.Object, який є базовим для всіх інших типів і класів .NET.Загальний спосіб оголошення змінних наступний:
тип_даних назва_змінної;
Наприклад, int x;
У цьому виразі ми оголошуємо змінну x
типу int
. Тобто x буде зберігати деякий число не більше 4 байт.
Як ім’я змінної може виступати будь-який довільну назву, яке задовольняє наступним вимогам:
Оголосивши змінну, ми можемо тут же присвоїти їй значення або форматувати її.
Варіанти оголошення змінних:
bool isEnabled = true;
int x;
double y = 3.0;
string hello = "Hello World";
char c = 's';
int a = 4;
int z = a + 5;
При присвоєнні значень треба мати на увазі наступну тонкість. При присвоєнні змінним типу float
і decimal
чисел з плаваючою точкою, Visual Studio
розглядає всі ці числа як значення типу double
. І щоб конкретизувати, що дане значення має розглядатися як float
, нам треба використовувати суфікси (f
і m
відповідно для float
і decimal
):
float b = 30.6f;
decimal d = 334.8m;
Вище при перерахуванні всіх базових типів даних для кожного згадувався системний тип. Тому що назва вбудованого типу по суті являє собою скорочене позначення системного типу. Наприклад, такі змінні будуть еквівалентні по типу:
int a = 4;
System.Int32 b = 4;
Раніше ми явно вказували тип змінних, наприклад, int x. І компілятор при запуску вже знав, що x зберігає цілочисельне значення.
Однак ми можемо використовувати і модель неявній типізації:
var stroka = "Hell to World";
var c = 20;
Console.WriteLine(c.GetType().ToString());
Console.WriteLine(stroka.GetType().ToString());
Для неявної типізації замість назви типу даних використовується ключове слово var. Потім вже при компіляції компілятор сам виводить тип даних виходячи з присвоєного значення. В наведеному вище прикладі використовувався вираз Console.WriteLine (c.GetType (). ToString ()), яке дозволяє нам дізнатися виведений тип змінної с. Так як за замовчуванням всі цілочисельні значення розглядаються як значення типу int, то тому в результаті змінна c матиме тип int або SystemInt32.
Ці змінні подібні звичайним, однак вони мають деякі обмеження.
По-перше, ми не можемо спочатку оголосити неявно тіпізіруемую змінну, а потім ініціалізувати:
// цей код працює
int a;
a = 20;
// цей код не працює var c;
c = 20;
По-друге, ми не можемо вказати в якості значення неявно типізованої змінної null
:
//цей код не працює
var c = null;
Так як значення null
, то компілятор не зможе вивести тип даних.
Кожна змінна доступна в рамках певного контексту або області видимість. Поза цим контекстом змінна вже не існує.
Існують різні контексти:
Наприклад, нехай клас Program визначений таким чином:
class Program { // початок контексту класу
static int a = 9; // змінна рівня | класу |
static void Main (string [] args) { // початок контексту методу Main
int b = a - 1; // змінна рівня методу
{ // початок контексту блоку коду
int c = b - 1; // змінна рівня блоку коду
}// кінець контексту блоку коду, змінна з знищується
// так можна, змінна c визначена в блоці коду
// Console.WriteLine(c);
// так можна, змінна d визначена в іншому методі
// Console.WriteLine(d);
} // кінець контексту методу Main, змінна b знищується
void Display () { // початок контексту методу Display
//змінна a визначена в контексті класу, тому доступна
int d = a + 1;
} // кінець Конекст методу Display, змінна d знищується
} // кінець контексту класу, змінна a знищується
Тут виразно чотири змінних: a
, b
, c
, d
. Кожна з них існує в своєму контексті. Змінна a існує в контексті всього класу Program і доступна в будь-якому місці і блоці коду в методах Main і Display.
Змінна b існує тільки в рамках методу Main. Також як і змінна d існує в рамках методу Display. У методі Main ми не можемо звернутися до змінної d, так як вона в іншому контексті.
Змінна c існує тільки в блоці коду, межами якої є що відкриваються і закриваються фігурні дужки. Поза його межами змінна c не існує і до неї не можна звернутися.
Нерідко межу різних контекстів можна асоціювати з відкриваючими і закриваючими фігурними дужками, як в даному випадку, які задають межі блоку коду, методу, класу.
При роботі зі змінними треба враховувати, що локальні змінні, визначені в методі або в блоці коду, приховують змінні рівня класу, якщо їх імена співпадають:
class Program {
static int a = 9; // змінна рівня класу
static void Main (string [] args) {
int a = 5; // приховує змінну a, яка оголошена на рівні класу
Console.WriteLine (a); // 5
}
}
При оголошенні змінних також треба враховувати, що в одному контексті не можна визначити кілька змінних з одним і тим же ім’ям.
Перетворення базових типів даних
При розгляді типів даних вказувалося, які значення може мати той чи інший тип і скільки байт пам’яті він може займати. І ми можемо написати, наприклад, так:
byte a = 4;
int b = a + 70;
Але важливо розуміти, що це запис не є еквівалентним (хоча результат буде той же):
byte a = 4;
byte b = (byte) (a + 70);
перетворює дані типу byte до типу int. Даний тип перетворень називається розширюючим (widening), так як значення типу byte розширює свій розмір до розміру типу int або System.Int32. При таких перетвореннях, як правило проблем не виникає.
Крім розширючого перетворення є ще і звужуюче (narrowing). У другому випадку у нас виходить саме звужуюче перетворення. Або, припустимо, нам треба привласнити змінної типу byte суму двох змінних типу int:
int a = 4;
int b = 6;
byte c = a + b;
Незважаючи на те, що сума (10) вкладається в діапазон типу byte, Visual Studio все одно відобразить помилку. І щоб уникнути цієї ситуації. нам треба застосувати явне перетворення до типу byte.
byte c = (byte)a + b;
Явні і неявні перетворення
Якщо у випадку з розширюючим перетвореннями компілятор за нас виконував всі перетворення даних, тобто перетворення були неявними (implicit conversion), то при явних перетвореннях (explicit conversion) ми самі повинні застосувати операцію перетворення (операція()). Суть операції перетворення типів полягає в тому, що перед значенням вказується в дужках тип, до якого треба привести дане значення:
int a = 4;
int b = 6;
byte c = (byte)(a + b);
Цикли також є керуючими конструкціями, дозволяючи в залежності від певних умов виконувати деяку дію безліч разів. У C # є такі види циклів:
Якщо змінні зберігають деякі значення, то методи містять собою набір операторів, які виконують певні дії.
Загальне визначення методів виглядає наступним чином:
[Модифікатори] тіп_возвращаемого_значенія названіе_метода ([параметри]) {
//тіло методу
}
Модифікатори і параметри необов’язкові.
Розглянемо на прикладі методу Main:
static void Main (string [] args) {
Console.WriteLine ( "привіт світ!");
}
Ключове слово static є модифікатором. Далі йде тип значення. В даному випадку ключове слово void вказує на те, що метод нічого не повертає. Такий метод ще називається процедурою. Далі йде назва методу - Main і в дужках параметри - string []args.І в фігурні дужки укладено тіло методу-всі дії,які він виконує.
Створимо ще пару процедур:
static void Method1() {
Console.WriteLine ("Method1");
}
void Method2() {
Console.WriteLine("Method2");
}
Визначивши методи, ми можемо використовувати їх в програмі. Щоб викликати метод в програмі, треба вказати ім’я методу, а після нього в дужках значення для його параметрів :
static void Main (string [] args) {
string message = Hello(); // виклик першого методу
Console.WriteLine(message);
Sum(); // виклик другого методу
Console.ReadLine();
}
static string Hello() {
return "Hell to World!";
}
static void Sum() {
int x = 2;
int y = 3;
Console.WriteLine ( "{0} + {1} = {2}", x, y, x + y);
}
Тут визначені два методу. Перший метод Hello повертає значення типу string. Тому ми можемо присвоїти це значення якої-небудь змінної типу string: string message = Hello();
Другий метод - процедура Sum
- просто додає два числа і виводить результат на консоль.
У попередній темі ми використовували методи без параметрів. Тепер подивимося, як будуть використовуватися параметри конфігурації. Існує два способи передачі параметрів в метод в мові C #: за значенням і за посиланням.
Найбільш простий спосіб передачі параметрів представляє передача за значенням:
static int Sum (int x, int y) {
return x + y;
}
static void Main (string [] args) {
int x = 10;
int z = Sum (x, 15);
Console.WriteLine (z);
Console.ReadLine ();
}
При передачі параметрів по посиланню перед параметрами використовується модифікатор ref:
static void Main (string [] args) {
int x = 10;
int y = 15;
Addition (ref x, y); // виклик методу
Console.WriteLine (x);
Console.ReadLine ();
}
//визначення методу
static void Addition (ref int x, int y) {
x + = y;
}
У чому відмінність двох способів передачі параметрів? При передачі по значенню метод отримує не саму змінну, а її копію. А при передачі параметра за посиланням метод отримує адресу змінної в пам’яті. І, таким чином, якщо в методі змінюється значення параметра, переданого за посиланням, то також змінюється і значення змінної, яка передається на його місце.
C# дозволяє використовувати необов’язкові параметри. Для таких параметрів нам необхідно оголосити значення за замовчуванням. Також слід враховувати, що після необов’язкових параметрів всі наступні параметри також мають бути необов’язковими:
static int OptionalParam (int x, int y, int z = 5, int s = 4) {
return x + y + z + s;
}
Так як останні два параметри оголошені як необов’язкові, то ми можемо один з них або обидва опустити:
static void Main (string [] args) {
OptionalParam (2, 3);
OptionalParam (2,3,10);
Console.ReadLine ();
}
У попередніх прикладах при виклику методів значення для параметрів передавалися в порядку оголошення цих параметрів в методі. Але ми можемо порушити подібний порядок, використовуючи іменовані параметри:
static int OptionalParam (int x, int y, int z = 5, int s = 4) {
return x + y + z + s;
}
static void Main (string [] args) {
OptionalParam (x: 2, y: 3);
//Необов'язковий параметр z використовує значення за замовчуванням
OptionalParam (y: 2, x: 3, s: 10);
Console.ReadLine ();
}
Описом об’єкта є клас, а об’єкт являється екземпляром цього класу.
Клас визначається за допомогою ключового слова сlass:
class Book {
}
Вся функціональність класу представлена його членами - полями (полями називаються змінні класу), властивостями, методами, подіями. Структура класу Book:
class Book {
public string name;
public string author;
public int year;
public void Info() {
Console.WriteLine ( "Книга '{0}' (автор {1}) була видана в {2} році", name, author, year);
}
}
Крім звичайних методів в класах використовуються також і спеціальні методи, які називаються конструкторами. Конструктори викликаються при створенні нового об’єкта даного класу. Відмінною рисою конструктора є те, що його назва повинна збігатися з назвою класу:
class Book {
public string name;
public string author;
public int year;
public Book () {}
public Book (string name, string author, int year) {
this.name = name;
this.author = author;
this.year = year;
}
public void Info () {
Console.WriteLine ( "Книга '{0}' (автор {1}) була видана в {2} році", name, author, year);
}
}
Одне з призначень конструктора - початкова ініціалізація членів класу. В даному випадку ми використовували два конструктора. Один порожній. Другий конструктор наповнює поля класу початковими значеннями, які передаються через його параметри.
Оскільки імена параметрів і імена полів (name, author, year) в даному випадку збігаються, то ми використовуємо ключове слово this. Це ключове слово представляє посилання на поточний екземпляр класу. Тому в вираженні this.name = name; перша частина this.name означає, що name - це поле поточного класу, а не назву параметра name. Якби у нас параметри і поля називалися по-різному, то використовувати слово this було б необов’язково.
Тепер використовуємо клас в програмі. Створимо новий проект. Потім натиснемо правою кнопкою миші на назву проекту у вікні Solution Explorer (Оглядач рішень) і в меню виберемо пункт Class.
У діалоговому вікні дамо нового класу ім’я Book і натиснемо кнопку Add (Додати). У проект буде додано новий файл Book.cs, що містить клас Book.
Змінимо в цьому файлі код класу Book на наступний:
class Book {
public string name;
public string author;
public int year;
public Book () {
name = "невідомо";
author = "невідомо";
year = 0;
}
public Book (string name, string author, int year) {
this.name = name;
this.author = author;
this.year = year;
}
public void GetInformation () {
Console.WriteLine ( "Книга '{0}' (автор {1}) була видана в {2} році", name, author, year);
}
}
Тепер перейдемо до коду файлу Program.cs і змінимо метод Main класу Program наступним чином:
class Program {
static void Main (string [] args) {
Book b1 = new Book ( " Заповіт", "Т. Г. Шевченко", 1845); b1.GetInformation ();
Book b2 = new Book ();
b2.GetInformation ();
Console.ReadLine ();
}
}
Якщо ми запустимо код на виконання, то консоль виведе нам інформацію про книги b1 і b2. Зверніть увагу, що щоб створити новий об’єкт з використанням конструктора, нам треба використовувати ключове слово new. Оператор new створює об’єкт класу і виділяє для нього область в пам’яті.
Ініціалізація об’єктів
Для нашого класу Book ми могли б встановити послідовно значення для всіх трьох полів класу:
Book b1 = new Book ();
b1.name = "Заповіт";
b1.author = "Т.Г.Шевченко";
b1.year = тисячу вісімсот сорок п"ятий;
b1.GetInformation ();
Але можна також використовувати ініціалізатор об’єктів:
Book b2 = new Book ();
b2 = new Book {name = "Батьки і діти", author = "І. С. Тургенєв", year = 1862};
b2.GetInformation ();
За допомогою ініціалізатора об’єктів можна присвоювати значення всім доступним полях і властивостями об’єкта в момент створення без явного виклику конструктора.
Часткові класи (partial class) представляють можливість розділити функціонал одного класу на кілька файлів. Наприклад, зараз у нас код класу Book весь знаходиться в одному файлі Book.cs. Але ми можемо розділити весь код на кілька різних файлів. У цьому випадку нам треба буде поставити перед визначенням класу ключове слово partial. Припустимо в одному файлі буде:
partial class Book {
public string name;
public string author;
public int year;
}
А в іншому файлі буде:
partial class Book {
public Book (string name, string author, int year) {
this.name = name;
this.author = author;
this.year = year;
}
public void GetInformation () {
Console.WriteLine ( "Книга '{0}' (автор {1}) була видана в {2} році", name, author, year);
}
}
Всі члени класу - поля, методи, властивості - всі вони мають модифікатори доступу. Модифікатори доступу дозволяють задати допустиму область видимості для членів класу. Тобто контекст, в якому можна використовувати цю змінну або метод. У попередній темі ми вже з ними стикалися, коли оголошували поля класу Book публічними (тобто з модифікатором public).
У C# застосовуються такі модифікатори доступу:
Оголошення полів класу без модифікатора доступу рівнозначно їх оголошенню з модифікатором private. Класи, оголошені без модифікатора, за замовчуванням мають доступ internal.
Подивимося на прикладі і створимо наступний клас State:
public class State {
int a; // все одно, що private int a;
private int b; // поле доступно тільки з поточного класу protected
int c; // доступно з поточного класу і похідних класів
internal int d; // доступно в будь-якому місці програми
protected internal int e; // доступно в будь-якому місці програми і з класів-спадкоємців
public int f; // доступно в будь-якому місці програми, а також для інших програм і збірок
private void Display_f () {
Console.WriteLine ( "Змінна f = {0}", f);
}
public void Display_a () {
Console.WriteLine ( "Змінна a = {0}", a);
}
internal void Display_b () {
Console.WriteLine ( "Змінна b = {0}", b);
}
protected void Display_e () {
Console.WriteLine ( "Змінна e = {0}", e);
}
}
Так як клас State оголошений з модифікатором public, Він буде доступний з будь-якого місця програми, а також з інших програм і збірок. Клас State має п’ять полів для кожного рівня доступу. Плюс одна змінна без модифікатора, яка є закритою за замовчуванням.
Також є чотири методи, які будуть виводити значення полів класу на екран. Зверніть увагу, що так як всі модифікатори дозволяють використовувати члени класу всередині даного класу, то і всі змінні класу, в тому числі закриті, у нас доступні всім його методам, так як всі знаходяться в контексті класу State.
Тепер подивимося, як ми зможемо використовувати змінні нашого класу в програмі (тобто в методі Main класу Program):
class Program {
static void Main (string [] args) {
State state1 = new State ();
//привласнити значення змінної a у нас не вийде,
//так як вона закрита і клас Program її не бачить
//І цього рядка середу підкреслить як неправильну
state1.a = 4; // Помилка, отримати доступ не можна
//то ж саме відноситься і до змінної b
state1.b = 3; // Помилка, отримати доступ не можна
//привласнити значення змінної з то ж не вийти,
//так як клас Program не є класом-спадкоємцем класу State
state1.c = 1; // Помилка, отримати доступ не можна
//змінна d з модифікатором internal доступна з будь-якого місця програми
//тому спокійно присвоюємо їй значення
state1.d = 5;
//змінна e так само доступна з будь-якого місця програми state1.e = 8;
//змінна f загальнодоступна
state1.f = 8;
//Спробуємо вивести значення змінних
//Так як цей метод оголошений як private, ми можемо використовувати його тільки всередині класу State
state1.Display_f (); // Помилка, отримати доступ не можна
//Так як цей метод оголошений як protected, а клас Program не є спадкоємцем класу State
state1.Display_e(); // Помилка, отримати доступ не можна
//Загальнодоступний метод
state1.Display_a();
//Метод доступний з будь-якого місця програми state1.Display_b ();
Console.ReadLine ();
}
}
Таким чином, ми змогли встановити тільки змінні d, e і f, так як їх модифікатори дозволяють використовувати в даному контексті. І нам виявилися доступні тільки два методи: state1.Display_a () і state1.Display_b (). Однак, так як значення змінних a і b були встановлені, то ці методи виведуть нулі, так як значення змінних типу int за замовчуванням не започатковано нулями.
Незважаючи на те, що модифікатори public і internal схожі за своєю дією, але вони мають велике відмінність. Класи і члени класу з модифікатором public також будуть доступні і іншим програмам, якщо даних клас помістити в динамічну бібліотеку dll і потім її використовувати в цих програмах.
Завдяки такій системі модифікаторів доступу можна приховувати деякі моменти реалізації класу від інших частин програми. Таке приховування називається інкапсуляцією.
Полями класу називаються звичайні змінні рівня класу. Ми вже раніше розглядали змінні - їх оголошення і ініціалізацію. Однак деякі моменти ми ще не торкалися, наприклад, константи і поля для читання.
Особливістю констант є те, що їх значення можна встановити тільки один раз. Наприклад, якщо у нас в програмі є деякі змінні, які не повинні змінювати значення (наприклад, число PI, число e і т.д.), ми можемо оголосити їх константами. Для цього використовується ключове слово const:
const double PI = 3.14;
const double E = 2.71;
При використанні констант треба пам’ятати, що оголосити ми їх можемо тільки один раз і що до моменту компіляції вони повинні бути визначені.
class MathLib {
public const double PI = 3.141;
public const double E = 2.81;
public const double K;
public MathLib () {
K = 2.5; // помилка - константа повинна бути визначена до компіляції
}
}
class Program {
static void Main (string [] args) {
MathLib.E = 3.8; // константу можна встановити кілька разів
}
}
Також зверніть увагу на синтаксис звернення до константи. Так як це статичне поле, нам необов’язково створювати об’єкт класу за допомогою конструктора. Ми можемо звернутися до неї, використовуючи ім’я класу.
Поля для читання схожі на константи: їх також не можна встановити двічі, проте їх можна встановлювати під час виконання програми, наприклад, в конструкторі, що з константами неприпустимо.
Поле для читання оголошується з ключовим словом readonly:
class MathLib {
public readonly double K;
public MathLib (double _k) {
K = _k; // поле для читання може бути визначено після компіляції
}
}
class Program {
static void Main (string [] args) {
MathLib mathLib = new MathLib (3.8);
Console.WriteLine (mathLib.K); // 3.8
// mathLib.K = 7.6;
// поле для читання можна встановити ізольованим від свого класу
Console.ReadLine ();
}
}
Крім звичайних методів в мові C # передбачені спеціальні методи доступу, які називають властивості. Вони забезпечує простий доступ до полів класу, дізнатися їх значення або виконати їх установку.
Стандартне опис властивості має наступний синтаксис:
[Модіфікатор_доступа] возвращаемий_тіп проізвольное_названіе {
// код властивості
}
Наприклад:
class Person {
private string name;
public string Name {
get { return name; }
set { name = value; }
}
}
Тут у нас є закрите поле name і є загальнодоступна властивість Name. Хоча вони мають практично однакову назву за винятком регістра, але це не більше ніж стиль, назви у них можуть бути довільні і не обов’язково повинні збігатися.
Через цю властивість ми можемо керувати доступом до змінної name. Стандартне визначення властивості містить блоки get і set. У блоці get ми повертаємо значення поля, а в блоці set встановлюємо. Параметр value представляє передане значення.
Ми можемо використовувати дану властивість наступним чином:
Person p = new Person ();
//Встановлюємо властивість - спрацьовує блок Set
//значення “Tom” і є передане в властивість value p.Name = “Tom”;
//Отримуємо значення властивості і присвоюємо його змінної - спрацьовує блок
Get
string personName = p.Name;
Можливо, може виникнути питання, навіщо потрібні властивості, якщо ми можемо в даній ситуації обходитися звичайними полями класу? Але властивості дозволяють вкласти додаткову логіку, яка може бути необхідна, наприклад, при присвоєнні змінної класу будь-якого значення. Наприклад, нам треба встановити перевірку за віком:
class Person {
private int age;
public int Age {
get { return age; }
set {
if (Value > 18) {
Console.WriteLine ( "Вік повинен бути більше 18");
} else {
age = value;
}
}
}
}
Блоки set
і get
не обов’язково одночасно повинні бути присутніми в властивості. Наприклад, ми можемо закрити властивість від установки, щоб тільки можна було отримувати значення. Для цього опускаємо блок set. І, навпаки, можна видалити блок get, тоді можна буде тільки встановити значення, але не можна отримати:
class Person {
private string name;
//властивість тільки для читання
public string Name {
get { return name; }
}
private int age;
//властивість тільки для запису
public int Age {
set { age = value; }
}
}
Ми можемо застосовувати модифікатори доступу не тільки до всіх властивостей, але і до окремих блокіів - або get, або set. При цьому якщо ми застосовуємо модифікатор до одного з блоків, то до іншого ми вже не можемо застосувати модифікатор:
class Person {
private string name;
public string Name {
get { return name; }
private set { name = value; }
}
public Person (string name, int age) {
Name = name;
Age = age;
}
}
Тепер закритий блок set ми зможемо використовувати тільки в даному класі - в його методах, властивості, конструкторі, але ніяк не в іншому класі:
Person p = new Person( "Tom", 24);
//Помилка - set оголошений з модифікатором private
//p.Name = "John";
Console.WriteLine (p.Name);
Автоматичні властивості
Властивості керують доступом до полів класу. Однак що, якщо у нас з десяток і більше полів ? Тому з версії .NET 4.0 в фреймворк були додані автоматичні властивості. Вони мають скорочене оголошення:
class Person {
public string Name {get; set; }
public int Age {get; set; }
public Person (string name, int age) {
Name = name;
Age = age;
}
}
Насправді тут також створюються поля для властивостей, тільки їх створює не програміст в коді, а компілятор автоматично генерує при компіляції.
З одного боку, автоматичні властивості досить зручні. З іншого боку, стандартні властивості мають ряд переваг: наприклад, вони можуть інкапсулювати додаткову логіку перевірки значення; не можна створити автоматичну властивість тільки для запису або читання, як у випадку зі стандартними властивостями.
В C# 6.0 (Visual Studio 2015) була додана така функціональність, як ініціалізація автовластивостей:
class Person {
public string Name {get; set; } = "Tom";
public int Age {get; set; } = 23;
}
class Program {
static void Main (string [] args) {
Person person = new Person ();
Console.WriteLine (person.Name); // Tom
Console.WriteLine (person.Age); // 23
Console.Read ();
}
}
І якщо ми не вкажемо для об’єкта Person значення властивостей Name і Age, то будуть діяти значення за замовчуванням.
Ще одна зміна торкнулася визначення автовластивостей. Наприклад, якщо в C # 5.0 ми захотіли зробити автовластивості доступним для установки тільки з класу, то треба було вказати private set:
class Person {
public string Name {get; private set; }
public Person (string n) {
Name = n;
}
}
Крім як з класу Person це властивість неможливо встановити. В C # 6.0 нам необов’язково писати private set:
class Person {
public string Name {get;}
public Person (string n) {
Name = n;
}
}
Іноді виникає необхідність створити один і той же метод, але з різним набором параметрів. І в залежності від наявних параметрів застосовувати певну версію методу. Припустимо, у нас є наступний клас State:
class State {
Public string Name {get; set; } //назву
Public int Population {get; set; } //населення
Public double Area {get; set; } //площа
}
І ми хочемо визначити метод для нападу на іншу державу - метод Attack. Перша реалізація методу буде приймати як параметр об’єкт State - тобто держава, на яке ми нападаємо:
public void Attack (State enemy) {}
Але припустимо, що ми хочемо визначити версію даного методу, де ми будемо вказувати не тільки держава, але кількість військ, за допомогою яких ми нападаємо на ворога. Тоді ми можемо просто додати другу версію даного методу:
public void Attack (State enemy) {
// тут код методу
}
public void Attack (State enemy, int army) {
//тут код методу
}
Використовуючи механізм наслідування, ми можемо доповнювати і перевизначати загальний функціонал базових класах в класах-нащадках. Однак безпосередньо ми можемо наслідувати тільки від одного класу, на відміну, наприклад, від мови С ++, де є множинне спадкування.
У мові C # подібну проблему дозволяють вирішити інтерфейси. Вони грають важливу роль в системі ООП. Інтерфейси дозволяють визначити деякий функціонал, який не має конкретної реалізації. Потім цей функціонал реалізують класи, які застосовують дані інтерфейси.
Для визначення інтерфейсу використовується ключове слово interface. Як правило, назви інтерфейсів в C # починаються з великої літери I, наприклад, IComparable, IEnumerable (так звана угорська нотація), однак це не обов’язкова вимога, а більше стиль програмування. Інтерфейси також, як і класи, можуть містити властивості, методи і події, тільки без конкретної реалізації.
Визначимо наступний інтерфейс IAccount
, який буде містити методи і властивості для роботи з рахунком клієнта. Для додавання інтефрейса в проект можна натиснути правою кнопкою миші на проект і в контекстному меню вибрати Add -> New Item
і в діалоговому вікні додавання нового компонента вибрати Interface
:
Змінимо порожній код інтерфейсу IAccount на наступний:
interface IAccount {
// Поточна сума на рахунку
int CurrentSum {get; }
// Покласти гроші на рахунок
void Put (int sum);
//Взяти з рахунку
void Withdraw (int sum);
//Відсоток нарахувань
int Percentage {get; }
}
У інтерфейсі методи і властивості не мають реалізації, в цьому вони зближуються з абстрактними методами абстрактних класів. Сутність даного інтерфейсу проста: він визначає дві властивості для поточної суми грошей на рахунку і ставки відсотка за вкладами і два методи для додавання грошей на рахунок і вилучення грошей.
Ще один момент в оголошенні інтерфейсу: всі його члени - методи і властивості не мають модифікаторів доступу, але фактично за замовчуванням доступ public, так як мета інтерфейсу - визначення функціоналу для реалізації його класом. Тому весь функціонал повинен бути відкритий для реалізації.
Застосування інтерфейсу аналогічно спадкоємства класу:
class Client: IAccount {
// реалізація методів і властивостей інтерфейсу
}
Тепер же реалізуємо інтерфейс в класі Client, так як клієнт у нас має рахунком:
class Client: IAccount {
int _sum; // Змінна для зберігання суми
int _percentage; // Змінна для зберігання відсотка
public string Name { get; set; }
public Client (string name, int sum, int percentage) {
Name = name;
_sum = sum;
_percentage = percentage;
}
public int CurrentSum {
get {return _sum; }
}
public void Put (int sum) {
_sum + = sum;
}
public void Withdraw (int sum) {
if (Sum <= _sum) {
_sum - = sum;
}
}
public int Percentage {
get {return _percentage; }
}
public void Display () {
Console.WriteLine ( "Клієнт" + Name + "має рахунок на суму" + _sum);
}
}
Як і у випадку з абстрактними методами абстрактного класом клас Client реалізує всі методи інтерфейсу. При цьому оскільки всі методи і властивості інтерфейсу є публічними, при реалізації цих методів і властивостей в класі до них можна застосовувати тільки модифікатор public. Тому якщо клас повинен мати метод з якимось іншим модифікатором, наприклад, protected, То інтерфейс не підходить для визначення подібного методу.
Застосування класу в програмі:
Client client = new Client ( "Tom", 200, 10); client.Put (30);
Console.WriteLine (client.CurrentSum); // 230 client.Withdraw (100);
Console.WriteLine (client.CurrentSum); // 130
Інтерфейси, як і класи, можуть наслідуватися:
interface IDepositAccount: IAccount {
void GetIncome (); // нарахування відсотків
}
При застосуванні цього інтерфейсу клас Client повинен буде реалізувати як методи і властивості інтерфейсу IDepositAccount, так і методи і властивості базового інтерфейсу IAccount.
Може скластися ситуація, коли клас застосовує кілька інтерфейсів, але вони мають один і той же метод з одним і тим же повертається результатом і одінм і тим же набором параметрів. наприклад:
class Person: ISchool, IUniversity {
public void Study () {
Console.WriteLine ( "Навчання в школі або в університеті");
}
}
interface ISchool {
void Study ();
}
interface IUniversity {
void Study ();
}
Клас Person
визначає один метод Study()
, Створюючи одну спільну реалізацію для обох застосованих інтерфейсів. І незалежно від того, чи будемо ми розглядати об’єкт Person як об’єкт типу ISchool або IUniversity, результат методу буде один і той же.
Однак нерідко буває необхідно розмежувати реалізовані інтерфейси. В цьому випадку треба явним чином застосувати інтерфейс:
class Person: ISchool, IUniversity {
void ISchool.Study () {
Console.WriteLine ( "Навчання в школі");
}
void IUniversity.Study() {
Console.WriteLine ( "Навчання в університеті");
}
}
При явній реалізації вказується назва методу разом з назвою інтерфейсу, при цьому ми не можемо використовувати модифікатор public, тобто методи є закритими. В цьому випадку при використанні методу Study в програмі нам треба об’єкт Person привести до типу відповідного інтерфейсу:
static void Main (string[] args) {
Person p = new Person();
( (ISchool)p ).Study();
( (IUniversity)p ).Study();
Console.Read();
}