Уявіть, що ви створюєте клас для представлення банківського рахунку. Найпростіший підхід — зробити баланс публічним полем:
public class BankAccount
{
public decimal Balance; // Небезпечно!
}
var account = new BankAccount();
account.Balance = -1000; // Ніхто не заважає встановити негативний баланс!
Проблема очевидна: будь-який код може встановити некоректне значення. Нам потрібен механізм контролю доступу до даних. Саме для цього в C# існують Properties (Властивості) — вони надають інкапсуляцію та дозволяють додавати логіку при читанні або записі даних.
Історично підходи до управління даними в класах еволюціонували:
Field (Поле) — це змінна, оголошена безпосередньо в класі або структурі. Поля зберігають дані об'єкта.
public class Person
{
// Приватне поле (backing field)
private string _firstName;
// Публічне поле (не рекомендується)
public int Age;
// Поле тільки для читання (readonly)
private readonly DateTime _birthDate;
// [Константа](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/const)
private const int MaxAge = 150;
}
Характеристики полів:
| Характеристика | Опис |
|---|---|
| Модифікатори доступу | private, protected, internal, public |
| Readonly | Може бути ініціалізовано тільки при оголошенні або в конструкторі |
| Const | Значення визначається на етапі компіляції, неявно static |
| Використання | Зазвичай приватні, як backing fields для властивостей |
Property (Властивість) — це член класу, який надає гнучкий механізм читання, запису або обчислення значення приватного поля через accessors (аксесори): get та set.
public class Person
{
private string _firstName; // Backing field
// Властивість з повною реалізацією
public string FirstName
{
get
{
return _firstName;
}
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Ім'я не може бути порожнім");
_firstName = value.Trim();
}
}
}
Переваги властивостей над полями:
Властивості, на відміну від полів, не є простими змінними. На рівні проміжної мови (Intermediate Language, IL), компілятор перетворює властивість на пару методів: get_PropertyName() та set_PropertyName(). Це означає, що кожне звернення до властивості насправді є викликом методу, що й дозволяє додавати логіку валідації, обчислень чи логування. Поля, навпаки, транслюються в прямі операції з пам'яттю, що робить їх швидшими, але менш гнучкими.
Для простих властивостей можна використовувати скорочений синтаксис:
public class Person
{
private string _firstName;
private string _lastName;
// Read-only властивість з expression body
public string FullName => $"{_firstName} {_lastName}";
// Властивість з expression-bodied get/set
public string FirstName
{
get => _firstName;
set => _firstName = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
}
| Критерій | Fields (Поля) | Properties (Властивості) |
|---|---|---|
| Синтаксис доступу | obj.field | obj.Property |
| Валідація | ❌ Немає | ✅ Так, в set accessor |
| Інкапсуляція | ❌ Пряме зберігання | ✅ Контрольований доступ |
| Обчислення | ❌ Завжди значення | ✅ Може обчислюватись |
| Інтерфейси | ❌ Не підтримуються | ✅ Підтримуються |
| Продуктивність | Трохи швидше | Може бути незначна різниця |
| Використання | Внутрішнє зберігання | Публічний API |
Auto-implemented properties (Автоматично реалізовані властивості) — це синтаксичний цукор, коли компілятор автоматично створює приватне backing field.
public class Product
{
// Auto-implemented property - компілятор створює приховане backing field
public string Name { get; set; }
// Еквівалентно:
// private string <Name>k__BackingField;
// public string Name
// {
// get => <Name>k__BackingField;
// set => <Name>k__BackingField = value;
// }
}
public string Name { get; set; }
public string Name { get; }
// Можна встановити тільки в конструкторі
public string Name { get; private set; }
// Зовнішній код може тільки читати
public string Name { get; set; } = "Unknown";
// Значення за замовчуванням
public class Book
{
// Auto-implemented з ініціалізацією
public string Title { get; set; } = string.Empty;
// Private setter - зовнішній код не може змінювати
public string ISBN { get; private set; }
// Read-only - можна встановити тільки в конструкторі
public DateTime PublishedDate { get; }
public Book(string isbn, DateTime publishedDate)
{
ISBN = isbn;
PublishedDate = publishedDate; // OK в конструкторі
}
public void UpdateISBN(string newIsbn)
{
ISBN = newIsbn; // OK всередині класу
}
}
// Використання
var book = new Book("978-0-123456-47-2", DateTime.Now);
Console.WriteLine(book.ISBN); // OK - читання
// book.ISBN = "новий"; // ПОМИЛКА - private setter
Хоча автоматично реалізовані властивості є надзвичайно зручними, важливо розуміти їхні обмеження. Вони ідеально підходять для сценаріїв, де властивість є простим сховищем даних без додаткової логіки. Як тільки виникає потреба у валідації, логуванні, обчисленні значення "на льоту" або виконанні будь-яких побічних ефектів при читанні чи записі, необхідно переходити до повної реалізації властивості з явним backing field.
До C# 9, створення незмінних об'єктів (immutable objects) було незручним:
public class Person
{
public string Name { get; } // Тільки конструктор
public int Age { get; }
// Треба передавати ВСІ параметри
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
// Незручно для об'єктів з багатьма властивостями
var person = new Person("Іван", 25);
public class Person
{
public string Name { get; init; } // C# 9+
public int Age { get; init; }
}
// Object initializer syntax!
var person = new Person
{
Name = "Іван",
Age = 25
};
// person.Name = "Петро"; // ПОМИЛКА після ініціалізації
Init accessor дозволяє встановити значення властивості тільки під час ініціалізації об'єкта (в конструкторі або object initializer).
public class ImmutablePerson
{
public string Name { get; init; }
public int Age { get; init; }
public string Email { get; init; }
}
// Використання
var person = new ImmutablePerson
{
Name = "Іван",
Age = 25,
Email = "ivan@example.com"
};
// Після створення - тільки читання
Console.WriteLine(person.Name); // OK
// person.Age = 26; // ПОМИЛКА КОМПІЛЯЦІЇ!
Можна додати валідацію в init accessor:
public class ValidatedPerson
{
private string _name;
private int _age;
public string Name
{
get => _name;
init
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Ім'я не може бути порожнім");
_name = value.Trim();
}
}
public int Age
{
get => _age;
init
{
if (value < 0 || value > 150)
throw new ArgumentOutOfRangeException(nameof(value),
"Вік має бути від 0 до 150");
_age = value;
}
}
}
| Характеристика | { get; } (readonly) | { get; init; } |
|---|---|---|
| Встановлення | Тільки в конструкторі | Конструктор + object initializer |
| Синтаксис | new(param1, param2) | new { Prop1 = val1, Prop2 = val2 } |
| Зручність | Багато параметрів незручно | Object initializer зручніше |
| Валідація | Можлива | Можлива |
| Версія C# | Будь-яка | C# 9+ |
init accessors для створення незмінних об'єктів з concise syntax. Детальніше про record types можна прочитати в офіційній документації C#.Незмінність (immutability) є ключовою концепцією функціонального програмування, яка набуває все більшої популярності в об'єктно-орієнтованих мовах, як-от C#. Об'єкти, стан яких неможливо змінити після створення, є більш передбачуваними та безпечними для використання у багатопотокових середовищах, оскільки вони не потребують синхронізації доступу. init аксесори є потужним інструментом для побудови таких незмінних типів, поєднуючи безпеку readonly полів зі зручністю синтаксису ініціалізаторів об'єктів.
field знаходиться в режимі preview в C# 13 і планується до офіційного релізу в C# 14. Для використання потрібно увімкнути preview features.Іноді потрібна auto-implemented property, але з невеликою логікою (наприклад, trim значення):
public class Person
{
private string _middleName; // Треба оголошувати backing field
public string MiddleName
{
get => _middleName; // Дублювання
set => _middleName = value?.Trim(); // Проста логіка
}
}
public class Person
{
// Компілятор створює backing field автоматично
public string MiddleName
{
get; // Або: get => field;
set => field = value?.Trim();
}
}
field keyword — це контекстне ключове слово, яке надає доступ до compiler-synthesized backing field у властивостях. Це дозволяє писати semi-auto properties — частково автоматичні властивості.
public class TimeSlot
{
// Компілятор синтезує backing field автоматично
public int Hours
{
get; // Автоматичний get accessor
set // Власна логіка в set
{
if (value >= 0 && value <= 23)
{
field = value; // Доступ до синтезованого поля
}
else
{
throw new ArgumentOutOfRangeException(nameof(value),
"Години мають бути від 0 до 23");
}
}
}
}
public class Example
{
// ✅ set accessor з валідацією
public int Value
{
get;
set => field = value >= 0 ? value : 0;
}
// ✅ init accessor з валідацією
public string Name
{
get;
init => field = value?.Trim() ?? throw new ArgumentNullException();
}
// ✅ Обидва accessors
public decimal Price
{
get => field;
set => field = Math.Round(value, 2);
}
}
Щоб використовувати field keyword:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>preview</LangVersion>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>
</Project>
public string Email
{
get;
set => field = value?.ToLowerInvariant();
}
| Підхід | Синтаксис | Backing Field | Валідація | Складність |
|---|---|---|---|---|
| Повна реалізація | Manual get/set | Оголошується вручну | ✅ Так | Висока |
| Auto property | { get; set; } | Автоматичне | ❌ Ні | Низька |
| field keyword | get; set => field = ... | Автоматичне | ✅ Так | Середня |
field keyword для властивостей, де потрібна проста валідація або трансформація, але не хочеться вручну оголошувати backing field.Для глибшого розуміння цієї теми рекомендуємо ознайомитись з попереднім матеріалом: Класи та Об'єкти.Ключове слово field є елегантною відповіддю на давню проблему "багатослівних" властивостей. Раніше розробникам доводилося обирати між абсолютно лаконічною автоматичною властивістю (без логіки) та повною реалізацією з явним backing field (навіть для найпростішої логіки, як-от INotifyPropertyChanged). field заповнює цю прогалину, дозволяючи додавати логіку до аксесорів, не жертвуючи при цьому лаконічністю, оскільки компілятор все ще генерує поле для зберігання даних за лаштунками. Це робить код чистішим і зменшує кількість шаблонного коду.
Indexer (Індексатор) — це властивість, яка дозволяє індексувати об'єкти як масиви або колекції, використовуючи синтаксис [].
Подібно до властивостей, індексатори є синтаксичним цукром. На рівні IL-коду компілятор перетворює індексатор на методи, які зазвичай називаються get_Item та set_Item, що приймають індекс як параметр. Це дозволяє класам імітувати поведінку масивів або словників, надаючи при цьому повний контроль над внутрішньою логікою доступу до даних.
public class WeekTemperatures
{
private float[] _temperatures = new float[7];
// Indexer - використовує ключове слово 'this'
public float this[int day]
{
get
{
if (day < 0 || day > 6)
throw new ArgumentOutOfRangeException(nameof(day));
return _temperatures[day];
}
set
{
if (day < 0 || day > 6)
throw new ArgumentOutOfRangeException(nameof(day));
_temperatures[day] = value;
}
}
}
// Використання
var week = new WeekTemperatures();
week[0] = 22.5f; // Понеділок
week[1] = 24.0f; // Вівторок
Console.WriteLine($"Температура в понеділок: {week[0]}°C");
public class FibonacciSequence
{
// Тільки для читання - expression-bodied
public int this[int index] => index switch
{
0 => 0,
1 => 1,
_ => this[index - 1] + this[index - 2]
};
}
var fib = new FibonacciSequence();
Console.WriteLine(fib[10]); // 55
public class PhoneBook
{
private Dictionary<string, string> _contacts = new();
// Індексатор з string ключем
public string this[string name]
{
get => _contacts.TryGetValue(name, out var phone) ? phone : "Не знайдено";
set => _contacts[name] = value;
}
}
var book = new PhoneBook();
book["Іван"] = "+380123456789";
Console.WriteLine(book["Іван"]); // +380123456789
public class Matrix
{
private int[,] _data = new int[3, 3];
// Багатовимірний індексатор
public int this[int row, int col]
{
get
{
ValidateIndices(row, col);
return _data[row, col];
}
set
{
ValidateIndices(row, col);
_data[row, col] = value;
}
}
private void ValidateIndices(int row, int col)
{
if (row < 0 || row > 2 || col < 0 || col > 2)
throw new ArgumentOutOfRangeException();
}
}
var matrix = new Matrix();
matrix[0, 0] = 1;
matrix[1, 1] = 5;
Console.WriteLine(matrix[0, 0]); // 1
public interface IDataStore<T>
{
T this[int index] { get; set; } // Оголошення в інтерфейсі
}
::tip
Для глибшого розуміння цієї теми рекомендуємо ознайомитись з попереднім матеріалом: [Класи та Об'єкти](./2.classes-objects.md).
::
public class DataStore<T> : IDataStore<T>
{
private T[] _data = new T[100];
public T this[int index]
{
get => _data[index];
set => _data[index] = value;
}
}
public class SmartList<T>
{
private List<T> _items = new();
// Індексатор з валідацією
public T this[int index]
{
get
{
if (index < 0 || index >= _items.Count)
throw new IndexOutOfRangeException(
$"Індекс {index} поза межами [0, {_items.Count - 1}]");
return _items[index];
}
set
{
if (index < 0 || index >= _items.Count)
throw new IndexOutOfRangeException(
$"Індекс {index} поза межами [0, {_items.Count - 1}]");
_items[index] = value;
}
}
// Індексатор з іменованим доступом (string)
public T this[string selector]
{
get => selector.ToLower() switch
{
"first" => _items.FirstOrDefault(),
"last" => _items.LastOrDefault(),
_ => throw new ArgumentException($"Невідомий селектор: {selector}")
};
}
public void Add(T item) => _items.Add(item);
public int Count => _items.Count;
}
var list = new SmartList<string>();
list.Add("Apple");
list.Add("Banana");
list.Add("Cherry");
// Числовий індексатор
Console.WriteLine(list[0]); // Apple
list[1] = "Blueberry";
// Іменований індексатор
Console.WriteLine(list["first"]); // Apple
Console.WriteLine(list["last"]); // Cherry
public class Product
{
private decimal _price;
private int _quantity;
public string Name { get; init; } = string.Empty;
public decimal Price
{
get => _price;
set
{
if (value < 0)
throw new ArgumentException("Ціна не може бути від'ємною");
_price = Math.Round(value, 2); // Округлення до 2 знаків
}
}
public int Quantity
{
get => _quantity;
set
{
if (value < 0)
throw new ArgumentException("Кількість не може бути від'ємною");
_quantity = value;
}
}
// Обчислювана властивість
public decimal TotalValue => Price * Quantity;
// Expression-bodied read-only
public bool IsInStock => Quantity > 0;
}
public class AppConfiguration
{
// Semi-auto property з нормалізацією
public string ServerUrl
{
get;
set => field = value?.TrimEnd('/').ToLowerInvariant()
?? throw new ArgumentNullException(nameof(value));
}
// Валідація діапазону
public int MaxConnections
{
get;
set => field = value is >= 1 and <= 1000
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
// Init з валідацією
public int Port
{
get;
init => field = value is >= 1 and <= 65535
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
}

public class User
{
public string Email; // Публічне поле - немає валідації
}
var user = new User();
user.Email = "invalid"; // Некоректний email!
public class User
{
private string _email;
public string Email
{
get => _email;
set
{
if (!IsValidEmail(value))
throw new ArgumentException("Некоректний email");
_email = value.ToLowerInvariant();
}
}
private bool IsValidEmail(string email)
=> email?.Contains('@') == true;
}
// ❌ Небезпечно
public class BankAccount
{
public decimal Balance { get; set; } // Можна встановити від'ємне!
}
// ✅ Безпечно
public class BankAccount
{
private decimal _balance;
public decimal Balance
{
get => _balance;
private set // Тільки клас може змінювати напряму
{
if (value < 0)
throw new InvalidOperationException("Баланс не може бути від'ємним");
_balance = value;
}
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Сума має бути додатною");
Balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Сума має бути додатною");
if (Balance - amount < 0)
throw new InvalidOperationException("Недостатньо коштів");
Balance -= amount;
}
}
.csproj:<LangVersion>preview</LangVersion>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
// ❌ Проблема: init дозволяє змінювати після створення через with
public class Config
{
public string ApiKey { get; init; } // Здається незмінним
}
var config = new Config { ApiKey = "secret123" };
var modified = config with { ApiKey = "hacked!" }; // Упс!
// ✅ Рішення: readonly для справжньої незмінності
public class Config
{
public string ApiKey { get; }
public Config(string apiKey)
{
ApiKey = apiKey;
}
}
// ❌ Помилка компіляції: CS9258
public class Example
{
public string Name
{
get;
set => field = value?.Trim(); // 'field' unavailable
}
}
.csproj:<LangVersion>preview</LangVersion>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
Створіть клас Student з наступними auto-implemented properties:
FirstName (string, get/set)LastName (string, get/set)StudentId (int, get, private set)EnrollmentDate (DateTime, get only)Додайте властивість FullName, яка повертає FirstName + " " + LastName.
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int StudentId { get; private set; }
public DateTime EnrollmentDate { get; }
// Обчислювана властивість
public string FullName => $"{FirstName} {LastName}";
public Student(int studentId, DateTime enrollmentDate)
{
StudentId = studentId;
EnrollmentDate = enrollmentDate;
}
public void UpdateStudentId(int newId)
{
StudentId = newId; // Можна змінювати всередині класу
}
}
// Використання
var student = new Student(12345, DateTime.Now)
{
FirstName = "Іван",
LastName = "Петренко"
};
Console.WriteLine(student.FullName); // Іван Петренко
Console.WriteLine(student.EnrollmentDate.ToShortDateString());
Створіть клас EmailMessage з наступними вимогами:
To, Subject, Body мають використовувати init accessorTo має містити @Subject не може бути порожнім і має максимум 100 символівBody не може бути nullCreatedAt, яка зберігає час створенняpublic class EmailMessage
{
private string _to;
private string _subject;
private string _body;
public string To
{
get => _to;
init
{
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
throw new ArgumentException("Email має містити @");
_to = value.Trim().ToLowerInvariant();
}
}
public string Subject
{
get => _subject;
init
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Тема не може бути порожньою");
if (value.Length > 100)
throw new ArgumentException("Тема не може перевищувати 100 символів");
_subject = value.Trim();
}
}
public string Body
{
get => _body;
init
{
_body = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
}
public DateTime CreatedAt { get; } = DateTime.UtcNow;
}
// Використання
var email = new EmailMessage
{
To = "user@example.com",
Subject = "Вітаємо!",
Body = "Дякуємо за реєстрацію."
};
Console.WriteLine($"Email створено: {email.CreatedAt}");
// email.Subject = "Нова тема"; // ПОМИЛКА - тільки init!
Створіть клас ShoppingCart з:
TotalItems (кількість унікальних товарів)TotalPrice (загальна вартість)field keyword для властивості DiscountPercent з валідацією (0-100)public class ShoppingCart
{
private Dictionary<string, CartItem> _items = new();
// Індексатор для доступу за назвою товару
public CartItem this[string productName]
{
get => _items.TryGetValue(productName, out var item)
? item
: null;
set
{
if (value == null)
_items.Remove(productName);
else
_items[productName] = value;
}
}
// Властивість з field keyword та валідацією
public decimal DiscountPercent
{
get;
set => field = value is >= 0 and <= 100
? value
: throw new ArgumentOutOfRangeException(nameof(value),
"Знижка має бути від 0 до 100%");
}
// Обчислювані властивості
public int TotalItems => _items.Count;
public decimal TotalPrice
{
get
{
var subtotal = _items.Values.Sum(item => item.Price * item.Quantity);
return subtotal * (1 - DiscountPercent / 100);
}
}
public void AddItem(string name, decimal price, int quantity)
{
this[name] = new CartItem(name, price, quantity);
}
public void RemoveItem(string name)
{
this[name] = null;
}
}
public record CartItem(string Name, decimal Price, int Quantity);
// Використання
var cart = new ShoppingCart();
cart.AddItem("Laptop", 25000, 1);
cart.AddItem("Mouse", 500, 2);
cart.DiscountPercent = 10;
Console.WriteLine($"Товарів: {cart.TotalItems}");
Console.WriteLine($"Сума: {cart.TotalPrice:C}");
Console.WriteLine($"Mouse кількість: {cart["Mouse"].Quantity}");
cart["Mouse"] = new CartItem("Mouse", 500, 3); // Оновлення
Створіть клас Spreadsheet, який реалізує:
[row, col] для доступу до комірокGetUsedCells() який повертає список координат заповнених комірокpublic class Spreadsheet
{
private Dictionary<(int row, int col), string> _cells = new();
public string this[int row, int col]
{
get
{
ValidateIndices(row, col);
return _cells.TryGetValue((row, col), out var value) ? value : string.Empty;
}
set
{
ValidateIndices(row, col);
if (string.IsNullOrEmpty(value))
_cells.Remove((row, col)); // Очистити комірку
else
_cells[(row, col)] = value;
}
}
public int UsedCellsCount => _cells.Count;
public List<(int row, int col)> GetUsedCells()
{
return _cells.Keys.OrderBy(k => k.row).ThenBy(k => k.col).ToList();
}
private void ValidateIndices(int row, int col)
{
if (row < 0 || col < 0)
throw new ArgumentOutOfRangeException("Індекси мають бути невід'ємними");
}
public void Print()
{
var cells = GetUsedCells();
foreach (var (row, col) in cells)
{
Console.WriteLine($"[{row}, {col}] = {this[row, col]}");
}
}
}
// Використання
var sheet = new Spreadsheet();
sheet[0, 0] = "Name";
sheet[0, 1] = "Age";
sheet[1, 0] = "Іван";
sheet[1, 1] = "25";
sheet[2, 0] = "Марія";
sheet[2, 1] = "30";
Console.WriteLine($"Використано {sheet.UsedCellsCount} комірок");
sheet.Print();
// Результат:
// Використано 6 комірок
// [0, 0] = Name
// [0, 1] = Age
// [1, 0] = Іван
// [1, 1] = 25
// [2, 0] = Марія
// [2, 1] = 30

У цьому розділі ви вивчили:
Наступні Кроки: У наступному розділі ви дізнаєтеся про методи, делегати та події, які дозволяють визначати поведінку класів та реалізовувати шаблон спостерігача (Observer pattern).