Властивості та Поля
Властивості та Поля
Вступ: Проблема Контролю Доступу
Уявіть, що ви створюєте клас для представлення банківського рахунку. Найпростіший підхід — зробити баланс публічним полем:
public class BankAccount
{
public decimal Balance; // Небезпечно!
}
var account = new BankAccount();
account.Balance = -1000; // Ніхто не заважає встановити негативний баланс!
Проблема очевидна: будь-який код може встановити некоректне значення. Нам потрібен механізм контролю доступу до даних. Саме для цього в C# існують Properties (Властивості) — вони надають інкапсуляцію та дозволяють додавати логіку при читанні або записі даних.
Еволюція: Від Полів до Властивостей
Історично підходи до управління даними в класах еволюціонували:
Фундаментальні Концепції
Fields (Поля)
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 для властивостей |
Properties (Властивості)
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}");
}
Переваги властивостей над полями:
✅ Інкапсуляція: Приховування внутрішньої реалізації
✅ Обчислювані значення: Властивість може повертати обчислене значення
✅ Зворотна сумісність: Зміна поля на властивість не ламає API ✅ Підтримка інтерфейсів: Інтерфейси можуть оголошувати властивості, але не поля
Властивості, на відміну від полів, не є простими змінними. На рівні проміжної мови (Intermediate Language, IL), компілятор перетворює властивість на пару методів: get_PropertyName() та set_PropertyName(). Це означає, що кожне звернення до властивості насправді є викликом методу, що й дозволяє додавати логіку валідації, обчислень чи логування. Поля, навпаки, транслюються в прямі операції з пам'яттю, що робить їх швидшими, але менш гнучкими.
Expression-Bodied Properties
Для простих властивостей можна використовувати скорочений синтаксис:
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 vs Properties
| Критерій | Fields (Поля) | Properties (Властивості) |
|---|---|---|
| Синтаксис доступу | obj.field | obj.Property |
| Валідація | ❌ Немає | ✅ Так, в set accessor |
| Інкапсуляція | ❌ Пряме зберігання | ✅ Контрольований доступ |
| Обчислення | ❌ Завжди значення | ✅ Може обчислюватись |
| Інтерфейси | ❌ Не підтримуються | ✅ Підтримуються |
| Продуктивність | Трохи швидше | Може бути незначна різниця |
| Використання | Внутрішнє зберігання | Публічний API |
Auto-Implemented Properties
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;
// }
}
Варіації Auto-Implemented Properties
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.
Init Accessors (C# 9+)
Проблема: Immutable Objects
До 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
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 з Валідацією
Можна додати валідацію в 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;
}
}
}
Порівняння: readonly vs init
| Характеристика | { 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 Keyword (C# 13/14 Preview)
field знаходиться в режимі preview в C# 13 і планується до офіційного релізу в C# 14. Для використання потрібно увімкнути preview features.Проблема: Semi-Auto Properties
Іноді потрібна 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?
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");
}
}
}
}
Де можна використовувати field
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);
}
}
Увімкнення Preview Features
Щоб використовувати field keyword:
Крок 1: Оновіть .csproj файл
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>preview</LangVersion>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>
</Project>
Крок 2: Використовуйте field у властивостях
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 заповнює цю прогалину, дозволяючи додавати логіку до аксесорів, не жертвуючи при цьому лаконічністю, оскільки компілятор все ще генерує поле для зберігання даних за лаштунками. Це робить код чистішим і зменшує кількість шаблонного коду.
Indexers (Індексатори)
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");
Read-Only Indexer
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
Візуалізація: Property Lifecycle
Практичні Приклади
Приклад 1: Клас Product з Валідацією
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;
}
Приклад 2: Конфігурація з field keyword
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));
}
}

Troubleshooting: Типові Помилки
Помилка 1: Публічні Поля Замість Властивостей
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;
}
Помилка 2: Забута Валідація
// ❌ Небезпечно
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>
Помилка 3: Неправильне Використання init vs set
// ❌ Проблема: 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;
}
}
Помилка 4: Preview Features Без Увімкнення
// ❌ Помилка компіляції: CS9258
public class Example
{
public string Name
{
get;
set => field = value?.Trim(); // 'field' unavailable
}
}
.csproj:<LangVersion>preview</LangVersion>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
Практичні Завдання
Рівень 1: Інкапсуляція та Валідація
Завдання 1.1: Захист Даних
Створіть клас 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; // Помилка
Завдання 1.2: Побічні Ефекти (Side Effects)
Створіть клас Product, який виводить повідомлення в консоль при зміні ціни.
- Властивість
Priceповинна мати backing field. - При кожній успішній зміні ціни виводьте в консоль: "Ціна змінилася з X на Y".
- Якщо нова ціна така сама, як стара — нічого не робіть.
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;
}
}
}
Завдання 1.3: Обчислювані Властивості
Створіть клас 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;
}
Рівень 2: Контроль Створення (Immutable State)
Завдання 2.1: Валідація при Ініціалізації
Створіть клас 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" }; // Помилка
Завдання 2.2: Приватні Сетери
Створіть клас 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;
}
}
Завдання 2.3: Smart Init
Створіть клас TemperatureMeasurement.
- Властивість
Celsiusзinitаксесором. - Якщо користувач задає температуру нижче -273.15 (абсолютний нуль), автоматично виправляйте її на -273.15.
- Властивість
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
Рівень 3: Індексатори та Просунуті Техніки
Завдання 3.1: Безпечний Доступ до Масиву
Створіть клас SafeArray, який обгортає звичайний масив int[].
- Реалізуйте індексатор.
- Якщо індекс виходить за межі масиву (менше 0 або більше довжини), повертайте
-1(або0), замість того щоб "крашити" програму винятком. - Додайте підтримку "зворотної індексації" (як в Python): індекс
-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;
}
}
}
Завдання 3.2: Індексатор за Рядком (Словник)
Створіть клас EnvironmentConfig.
- Зберігайте налаштування (Key-Value) у приватному словнику.
- Реалізуйте індексатор, який приймає рядок-ключ (наприклад, "DbConnection").
- Якщо ключа не існує, індексатор повинен повертати рядок "Not Set" (замість помилки чи null).
- Сеттер повинен додавати нове значення або оновлювати існуюче.
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"
Завдання 3.3: Field Keyword (Preview)
Створіть клас DiscountManager використовуючи нове ключове слово field (якщо ваша версія C# дозволяє) або емулюйте його поведінку для розуміння.
- Властивість
MaxDiscountмає бути auto-property, але з логікою вset. - У
setперевірте, що знижка не перевищує 50%. Якщо намагаються встановити 60%, примусово ставте 50%. - Це демонструє "напівавтоматичні" властивості, де ми не оголошуємо backing field вручну, але додаємо логіку.
public class DiscountManager
{
public decimal MaxDiscount
{
get;
// Використання field keyword (C# 13+)
// Якщо значення > 50, обрізаємо до 50
set => field = value > 50 ? 50 : value;
}
}
Резюме
У цьому розділі ви вивчили:
✅ Auto-implemented Properties: Синтаксичний цукор для простих властивостей
✅ Init Accessors: Створення immutable об'єктів з зручним синтаксисом
✅ Field Keyword: Semi-auto properties з доступом до backing field (C# 13+)
✅ Indexers: Надання array-like доступу до даних об'єкта
✅ Best Practices: Завжди використовуйте властивості для публічних даних з валідацієюНаступні Кроки: У наступному розділі ви дізнаєтеся про методи, делегати та події, які дозволяють визначати поведінку класів та реалізовувати шаблон спостерігача (Observer pattern).