У C# для організації та зберігання даних використовують різні типи. Серед основних конструкцій мови — перерахування enum, кортежі (tuples), структури struct та класи class. Вони допомагають описувати набори значень, об’єднувати пов’язані дані й створювати власні типи.
Структури, класи, перерахування enum і кортежі в C#
На перший погляд struct і class схожі: обидва можуть містити поля, властивості, конструктори та методи. Проте вони мають важливу відмінність: структури є типами значень, а класи — посилальними типами. Кортежі зручні для повернення кількох значень з методу, а enum дає змогу працювати з обмеженим набором іменованих констант.
У статті розглянемо синтаксис enum, кортежів, структур і класів у C#, наведемо приклади коду та порівняємо, коли краще використовувати кожну конструкцію.
1. Перерахування enum у C#
Перерахування (enum) — це тип значень, який визначає набір іменованих констант. Його зручно використовувати, коли змінна може приймати одне значення з наперед відомого списку: час доби, статус замовлення, роль користувача або стан процесу.
Наприклад, створимо перерахування Time:
enum Time : byte
{
Morning,
Afternoon,
Evening,
Night
}
Тут Time — назва перерахування, а Morning, Afternoon, Evening і Night — його елементи. Після двокрапки вказано базовий тип byte.
Якщо явно не задавати числові значення, компілятор призначає їх послідовно, починаючи з нуля:
enum Time : byte
{
Morning, // 0
Afternoon, // 1
Evening, // 2
Night // 3
}
Використання перерахування:
Time currentTime = Time.Morning;
if (currentTime == Time.Morning)
{
Console.WriteLine("Доброго ранку!");
}
1.1. Явне задання значень enum
Елементам enum можна призначити власні числові значення. Це корисно, якщо значення мають відповідати кодам із бази даних, протоколу або зовнішнього API.
enum OrderStatus
{
New = 1,
Processing = 2,
Shipped = 3,
Delivered = 4,
Cancelled = 5
}
Якщо значення не вказано, наступний елемент отримує число, на одиницю більше за попередній.
1.2. Базові типи перерахувань
За замовчуванням базовим типом enum є int. Також можна використовувати інші цілочисельні типи: byte, sbyte, short, ushort, uint, long або ulong.
enum Priority : byte
{
Low = 1,
Normal = 2,
High = 3
}
Вибирайте базовий тип з урахуванням діапазону значень і вимог до сумісності з іншими системами.
1.3. Перетворення enum у число та назад
Значення перерахування можна перетворити на його базовий числовий тип:
Priority priority = Priority.High;
byte code = (byte)priority;
Console.WriteLine(code); // 3
Зворотне перетворення:
Priority priority = (Priority)2;
Console.WriteLine(priority); // Normal
Важливо: приведення числа до enum саме по собі не перевіряє, чи існує відповідний іменований елемент. Для перевірки можна скористатися Enum.IsDefined():
int value = 10;
if (Enum.IsDefined(typeof(Priority), value))
{
Priority priority = (Priority)value;
Console.WriteLine(priority);
}
else
{
Console.WriteLine("Невідоме значення");
}
1.4. Перерахування з прапорцями Flags
Якщо потрібно комбінувати кілька незалежних значень, можна використовувати атрибут [Flags] і значення, задані як степені двійки:
[Flags]
enum FilePermissions
{
None = 0,
Read = 1,
Write = 2,
Execute = 4
}
Комбінація прапорців виконується оператором |:
FilePermissions permissions =
FilePermissions.Read | FilePermissions.Write;
bool canRead = permissions.HasFlag(FilePermissions.Read);
bool canExecute = permissions.HasFlag(FilePermissions.Execute);
Console.WriteLine(canRead); // True
Console.WriteLine(canExecute); // False
Не використовуйте однакові числові значення для незалежних прапорців, якщо плануєте комбінувати їх побітовими операціями.
2. Кортежі (Tuple) у C#
Кортеж (tuple) — це структура даних, яка об’єднує кілька значень, потенційно різних типів, в один об’єкт. У C# кортежі зручні для тимчасового групування пов’язаних даних і повернення кількох значень із методу.
Сучасний синтаксис кортежів використовує тип ValueTuple.
2.1. Створення кортежів
Кортеж можна оголосити, явно вказавши типи елементів:
(int, int) t1 = (5, 10);
Можна також задати імена елементів:
(int x, int y) t2 = (x: 1, y: 2);
Або скористатися неявним визначенням типу через var:
var t3 = (x: 3, y: 4);
Доступ до елементів іменованого кортежу здійснюється за їхніми іменами:
Console.WriteLine(t3.x);
Console.WriteLine(t3.y);
Якщо імена не задані, елементи доступні через Item1, Item2 тощо:
var point = (5, 10);
Console.WriteLine(point.Item1);
Console.WriteLine(point.Item2);
2.2. Повернення кортежу з методу
Кортежі часто використовують, щоб повернути з методу кілька результатів без створення окремого класу або структури.
private static (int, int) GetValues()
{
var result = (1, 3);
return result;
}
Краще називати елементи результату, щоб код було легше читати:
private static (int min, int max) GetValues()
{
return (min: 1, max: 3);
}
var values = GetValues();
Console.WriteLine(values.min);
Console.WriteLine(values.max);
2.3. Деконструкція кортежу
Значення кортежу можна розкласти на окремі змінні:
var person = ("Tom", 34);
var (name, age) = person;
Console.WriteLine(name);
Console.WriteLine(age);
Деконструкція зручна, коли потрібно одразу отримати кілька значень і не звертатися до елементів кортежу окремо.
2.4. Коли використовувати кортежі
Кортежі добре підходять для:
- повернення кількох значень із методу;
- тимчасового об’єднання пов’язаних даних;
- результатів обчислень;
- простих пар значень, наприклад координат або мінімуму й максимуму.
Якщо дані мають складну структуру, використовуються в багатьох частинах програми або потребують зрозумілої предметної назви, краще створити окремий class, struct або record.
3. Структури struct у C#
Структура (struct) — це користувацький тип значень. Вона може містити поля, властивості, методи, конструктори та інші члени типу.
Структури зазвичай використовують для невеликих значень, які логічно поводяться як одне ціле: координат, кольорів, дат, геометричних величин.
Приклад структури User:
struct User
{
public string Name;
public int Age;
public void DisplayInfo()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
Створення та використання структури:
User tom = new User();
tom.Name = "Tom";
tom.Age = 34;
tom.DisplayInfo();
У сучасному C# поля структури отримують значення за замовчуванням під час ініціалізації екземпляра. Водночас локальну змінну потрібно ініціалізувати так, щоб компілятор міг гарантувати присвоєння всіх необхідних полів до їх читання.
3.1. Поля та властивості структури
У прикладі вище використано публічні поля. У прикладному коді частіше варто приховувати стан і надавати доступ до нього через властивості:
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y)
{
X = x;
Y = y;
}
public void Display()
{
Console.WriteLine($"X: {X}, Y: {Y}");
}
}
Використання:
var point = new Point(10, 20);
point.Display();
3.2. Особливості структур
Головна особливість struct полягає в тому, що це тип значень. Під час присвоєння однієї змінної структури іншій копіюється значення структури:
Point first = new Point(10, 20);
Point second = first;
second.X = 100;
Console.WriteLine(first.X); // 10
Console.WriteLine(second.X); // 100
Зміна копії не змінює початковий екземпляр.
Важливо пам’ятати, що структура може містити посилальні поля, наприклад string або масив. У такому разі копіюється значення посилання, а не сам об’єкт, на який воно вказує. Тому копіювання структури не завжди означає глибоке копіювання всіх вкладених даних.
3.3. Коли використовувати struct
Структура доречна, якщо:
- тип описує одне невелике значення;
- екземпляри мають семантику копіювання;
- тип не потребує успадкування від іншого класу;
- важливо уникнути зайвого створення окремих об’єктів у певному сценарії.
Не варто автоматично замінювати класи структурами заради продуктивності. Великі структури можуть бути дорогими для копіювання, а остаточний вибір слід робити з урахуванням поведінки та вимірювань.
4. Класи class у C#
Клас (class) — це користувацький посилальний тип. Він описує стан і поведінку об’єктів за допомогою полів, властивостей, конструкторів і методів.
На відміну від структур, змінна класового типу зберігає посилання на об’єкт. Кілька змінних можуть посилатися на той самий екземпляр.
Розглянемо клас User:
public class User
{
private string _name;
private int _age;
public string Name
{
get { return _name; }
set { _name = value; }
}
public int Age
{
get { return _age; }
}
public User(string name, int age)
{
_name = name;
_age = age;
}
public void DisplayInfo(params int[] numbers)
{
foreach (var item in numbers)
{
Console.WriteLine(item);
}
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
Створення об’єкта та виклик методу:
User tom = new User("Serg", 44);
tom.DisplayInfo(1, 2, 3, 4);
4.1. Поля та властивості класу
У класі User поля _name і _age оголошені як private. Це означає, що безпосередньо звернутися до них ззовні класу не можна.
Для контрольованого доступу використовують властивості:
public string Name
{
get { return _name; }
set { _name = value; }
}
Властивість може мати:
get— повертає значення;set— встановлює значення;init— дозволяє встановити значення під час ініціалізації об’єкта.
Якщо додаткова логіка не потрібна, можна використовувати автоматичні властивості:
public class User
{
public string Name { get; set; }
public int Age { get; }
public User(string name, int age)
{
Name = name;
Age = age;
}
}
4.2. Конструктор класу
Конструктор — це спеціальний метод, який викликається під час створення об’єкта. Він має назву класу та не має типу повернення.
public User(string name, int age)
{
Name = name;
Age = age;
}
Створити об’єкт із цим конструктором можна так:
var user = new User("Serg", 44);
4.3. Параметр params у C#
Ключове слово params дозволяє передавати в метод змінну кількість аргументів. У початковому прикладі метод DisplayInfo() приймає масив цілих чисел:
public void DisplayInfo(params int[] numbers)
{
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
Викликати його можна з різною кількістю аргументів:
user.DisplayInfo(1, 2, 3);
user.DisplayInfo(10, 20);
user.DisplayInfo();
Також можна передати готовий масив типу int[].
4.4. Копіювання посилань на об’єкти класу
Під час присвоєння змінної класового типу іншій змінній копіюється посилання, а не сам об’єкт:
User first = new User("Tom", 34);
User second = first;
second.Name = "Bob";
Console.WriteLine(first.Name); // Bob
Console.WriteLine(second.Name); // Bob
Обидві змінні посилаються на один об’єкт, тому зміна через одну змінну видима через іншу.
5. Основні відмінності між struct і class у C#
Структури та класи можуть мати схожий синтаксис, але відрізняються семантикою.
| Характеристика | struct | class |
|---|---|---|
| Категорія типу | Тип значень | Посилальний тип |
| Присвоєння змінної | Копіюється значення | Копіюється посилання |
| Успадкування від іншого класу | Не підтримується | Підтримується |
| Реалізація інтерфейсів | Підтримується | Підтримується |
| Значення за замовчуванням | Екземпляр із полями за замовчуванням | null для посилання |
| Використання | Невеликі значення | Об’єкти зі станом і поведінкою |
Обидва типи можуть містити методи, властивості, конструктори та реалізовувати інтерфейси. Структури також можуть бути змінюваними або незмінними, але для багатьох сценаріїв зручніше проєктувати їх як незмінні типи.
5.1. Приклад різниці під час присвоєння
public struct CounterStruct
{
public int Value;
}
public class CounterClass
{
public int Value;
}
// Структура
var struct1 = new CounterStruct { Value = 10 };
var struct2 = struct1;
struct2.Value = 20;
Console.WriteLine(struct1.Value); // 10
// Клас
var class1 = new CounterClass { Value = 10 };
var class2 = class1;
class2.Value = 20;
Console.WriteLine(class1.Value); // 20
Цей приклад демонструє ключову різницю: структура копіюється за значенням, а змінні класового типу можуть посилатися на той самий об’єкт.
6. Що вибрати: enum, tuple, struct чи class?
Вибір залежить від того, які саме дані потрібно представити:
| Конструкція | Коли використовувати | Приклад |
|---|---|---|
enum | Обмежений набір іменованих варіантів | Статус замовлення |
tuple | Тимчасове групування кількох значень | Повернення мінімуму й максимуму |
struct | Невеликий тип значень | Координати точки |
class | Об’єкт зі станом, поведінкою та ідентичністю | Користувач, замовлення |
Практичне правило:
- Якщо потрібно вибрати один варіант із переліку — використовуйте
enum. - Якщо потрібно повернути кілька пов’язаних значень із методу — підійде кортеж.
- Якщо описуєте невелике значення, яке має копіюватися, — розгляньте
struct. - Якщо створюєте повноцінну сутність із власним станом і поведінкою — використовуйте
class.
7. Типові помилки під час роботи з enum, tuple, struct і class
1. Використання неіснуючого значення enum. Перетворення числа на enum не гарантує, що відповідний елемент оголошений. За потреби перевіряйте значення через Enum.IsDefined().
2. Надмірне використання кортежів. Кортеж із багатьма елементами та незрозумілими назвами ускладнює читання коду. Для складних моделей краще створити окремий тип.
3. Очікування, що структура копіюється глибоко. Копіюються поля структури. Якщо поле містить посилання на масив або об’єкт, копії можуть посилатися на ті самі дані.
4. Плутанина між копією класу та копією посилання. Присвоєння змінної класового типу іншій змінній не створює новий об’єкт.
5. Надмірна кількість публічних полів. Для класів і структур краще надавати контрольований доступ до стану через властивості.
8. Часті запитання про структури та класи в C#
Що таке enum у C#?
enum — це тип значень, який містить набір іменованих констант. Його використовують для представлення обмежених наборів варіантів, наприклад статусів, режимів або рівнів доступу.
Що таке кортеж у C#?
Кортеж — це набір кількох значень, об’єднаних в одну структуру. Він дає змогу повертати з методу кілька результатів і тимчасово групувати пов’язані дані.
Чим відрізняється struct від class у C#?
struct є типом значень, тому під час присвоєння копіюється його значення. class є посилальним типом: під час присвоєння копіюється посилання на об’єкт.
Коли краще використовувати структуру, а коли клас?
Структуру зазвичай використовують для невеликих значень із семантикою копіювання. Клас краще підходить для складних об’єктів, які мають стан, поведінку, ідентичність або потребують успадкування.
Чи може struct містити методи та конструктори?
Так. Структура може містити поля, властивості, методи, конструктори та реалізовувати інтерфейси. Вона не може успадковуватися від іншої структури або класу, але всі структури походять від System.ValueType.
Чи можна повернути кілька значень із методу C#?
Так. Один зі зручних способів — повернути іменований кортеж:
static (string name, int age) GetUser()
{
return ("Tom", 34);
}
Висновок
У C# конструкції enum, tuple, struct і class призначені для різних завдань. Перерахування допомагають описувати фіксовані набори значень, кортежі — об’єднувати кілька результатів, структури — створювати власні типи значень, а класи — моделювати повноцінні об’єкти.
Розуміння різниці між типами значень і посилальними типами допомагає уникати помилок під час присвоєння, передавання параметрів і зміни даних. Обирайте конструкцію відповідно до призначення даних, а не лише за схожістю синтаксису.
