Уявіть, що ви створюєте клас для представлення банківського рахунку. Найпростіший підхід — зробити баланс публічним полем:
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();
}
}
}
// === Приклад використання ===
var person = new Person();
// 1. Запис значення (викликається set accessor)
// Логіка всередині set обріже пробіли перед збереженням
person.FirstName = " Андрій ";
// 2. Читання значення (викликається get accessor)
Console.WriteLine($"Ім'я: '{person.FirstName}'"); // Виведе: Ім'я: 'Андрій'
// 3. Перевірка валідації
try
{
person.FirstName = ""; // Порожній рядок викличе помилку
}
catch (ArgumentException ex)
{
Console.WriteLine($"Помилка: {ex.Message}");
}
Переваги властивостей над полями:
Властивості, на відміну від полів, не є простими змінними. На рівні проміжної мови (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>
Створіть клас Employee із зарплатою, яка не може бути від'ємною.
_salary для зберігання даних.Salary з повною реалізацією get та set.set блоці перевірте: якщо значення менше 0, викиньте ArgumentException.get блоці повертайте значення приватного поля.public class Employee
{
private decimal _salary; // Backing field
public decimal Salary
{
get
{
return _salary;
}
set
{
if (value < 0)
throw new ArgumentException("Зарплата не може бути від'ємною");
_salary = value;
}
}
}
// Використання
var employee = new Employee();
employee.Salary = 5000; // OK
// employee.Salary = -100; // Помилка
Створіть клас Product, який виводить повідомлення в консоль при зміні ціни.
Price повинна мати backing field.public class Product
{
private decimal _price;
public string Name { get; set; } = string.Empty;
public decimal Price
{
get => _price;
set
{
if (_price == value) return; // Оптимізація
Console.WriteLine($"Ціна змінилася з {_price} на {value}");
_price = value;
}
}
}
Створіть клас Rectangle.
Width та Height повинні перевіряти, що значення > 0.Area повинна бути обчислюваною (тільки get) і повертати Width * Height.IsSquare (bool) повинна повертати true, якщо сторони рівні.public class Rectangle
{
private double _width;
private double _height;
public double Width
{
get => _width;
set => _width = value > 0 ? value : throw new ArgumentException();
}
public double Height
{
get => _height;
set => _height = value > 0 ? value : throw new ArgumentException();
}
// Обчислювана властивість - не зберігає значення, а рахує його
public double Area => _width * _height;
public bool IsSquare => Math.Abs(_width - _height) < 0.001;
}
Створіть клас UserProfile для системи реєстрації.
Username має бути доступна для запису тільки при ініціалізації (init).init: логін не має містити пробілів.public class UserProfile
{
private string _username;
public string Username
{
get => _username;
init
{
if (value.Contains(" "))
throw new ArgumentException("Логін не може містити пробіли");
_username = value;
}
}
}
// var user = new UserProfile { Username = "Super Admin" }; // Помилка
Створіть клас BankAccount з захищеним балансом.
AccountNumber має бути readonly (тільки get). Встановлюється в конструкторі.Balance має мати private set.Deposit(decimal amount) та Withdraw(decimal amount) повинні змінювати баланс.Balance напряму.public class BankAccount
{
public string AccountNumber { get; }
public decimal Balance { get; private set; }
public BankAccount(string accountNumber)
{
AccountNumber = accountNumber;
Balance = 0;
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException();
Balance += amount;
}
}
Створіть клас TemperatureMeasurement.
Celsius з init аксесором.Fahrenheit повинна повертати конвертоване значення.public class TemperatureMeasurement
{
private double _celsius;
private const double AbsoluteZero = -273.15;
public double Celsius
{
get => _celsius;
init
{
// Автокорекція замість помилки
_celsius = value < AbsoluteZero ? AbsoluteZero : value;
}
}
public double Fahrenheit => (_celsius * 9 / 5) + 32;
}
var temp = new TemperatureMeasurement { Celsius = -500 };
Console.WriteLine(temp.Celsius); // -273.15
Створіть клас SafeArray, який обгортає звичайний масив int[].
-1 (або 0), замість того щоб "крашити" програму винятком.-1 повертає останній елемент.public class SafeArray
{
private int[] _items = new int[10]; // Фіксований розмір для прикладу
public int this[int index]
{
get
{
// Підтримка від'ємних індексів (Python-style)
if (index < 0) index = _items.Length + index;
// Безпечна перевірка меж
if (index < 0 || index >= _items.Length)
return -1; // Значення за замовчуванням для помилки
return _items[index];
}
set
{
if (index < 0) index = _items.Length + index;
if (index >= 0 && index < _items.Length)
_items[index] = value;
}
}
}
Створіть клас EnvironmentConfig.
public class EnvironmentConfig
{
private Dictionary<string, string> _settings = new();
public string this[string key]
{
get => _settings.TryGetValue(key, out var val) ? val : "Not Set";
set => _settings[key] = value;
}
}
var config = new EnvironmentConfig();
Console.WriteLine(config["UnknownKey"]); // "Not Set"
Створіть клас DiscountManager використовуючи нове ключове слово field (якщо ваша версія C# дозволяє) або емулюйте його поведінку для розуміння.
MaxDiscount має бути auto-property, але з логікою в set.set перевірте, що знижка не перевищує 50%. Якщо намагаються встановити 60%, примусово ставте 50%.public class DiscountManager
{
public decimal MaxDiscount
{
get;
// Використання field keyword (C# 13+)
// Якщо значення > 50, обрізаємо до 50
set => field = value > 50 ? 50 : value;
}
}
У цьому розділі ви вивчили: