Уявіть, що ви створюєте банківську систему. Вам потрібно зберігати інформацію про клієнтів, рахунки, транзакції. Використовуючи тільки примітивні типи та функції, ви швидко зіткнетеся з хаосом: безліч змінних, складність у підтримці зв'язків між даними, дублювання коду.
Класи та об'єкти (Classes & Objects) — це фундаментальна концепція об'єктно-орієнтованого програмування (ООП), яка дозволяє організувати код в логічні, самодостатні одиниці, що моделюють реальні сутності.
У цьому розділі ви опануєте:
thisПеред вивченням цієї теми рекомендується ознайомитися з:
Клас (Class) — це шаблон або blueprint, який визначає структуру (дані) та поведінку (методи) для створення об'єктів. Клас описує, які властивості та дії може мати об'єкт, але сам по собі не займає пам'яті для зберігання даних екземплярів.
Об'єкт (Object) або екземпляр (Instance) — це конкретна реалізація класу, що існує в пам'яті програми та має власні значення полів.
Клас у C# є reference type (посилальним типом). Це означає, що змінна, яка зберігає об'єкт, насправді містить не сам об'єкт, а посилання (адресу в пам'яті) на нього. Самі дані об'єкта розміщуються в керованій купі (Managed Heap). Це фундаментально відрізняє класи від структур (struct), які є value types і зберігають свої дані безпосередньо у змінній (зазвичай у стеці).
Подумайте про клас як про архітектурний план будинку:
Базова структура класу в C#:
public class BankAccount
{
// Поля (Fields) - приватні дані класу
private string _accountNumber;
private string _owner;
private decimal _balance;
// Властивості (Properties) - публічний доступ до даних
public string AccountNumber { get; set; }
public string Owner { get; set; }
public decimal Balance { get; private set; }
// Методи (Methods) - поведінка класу
public void Deposit(decimal amount)
{
if (amount > 0)
{
Balance += amount;
}
}
public bool Withdraw(decimal amount)
{
if (amount > 0 && Balance >= amount)
{
Balance -= amount;
return true;
}
return false;
}
}
Для створення об'єкта використовується оператор new:
// Створення об'єкта з викликом конструктора
BankAccount account1 = new BankAccount();
// Присвоєння значень властивостям
account1.AccountNumber = "UA001";
account1.Owner = "Іван Петренко";
// Компілятор автоматично визначає тип
var account2 = new BankAccount();
account2.AccountNumber = "UA002";
account2.Owner = "Марія Коваленко";
// Тип визначається з лівої частини
BankAccount account3 = new();
account3.AccountNumber = "UA003";
Оператор new виділяє пам'ять в купі (Heap) для зберігання всіх полів об'єкта.
Усі поля ініціалізуються значеннями за замовчуванням (числа = 0, референсні типи = null).
Виконується код конструктора для додаткової ініціалізації.
Оператор new повертає посилання (reference) на створений об'єкт, яке зберігається у змінній.
Коли ви створюєте об'єкт (new BankAccount()), відбувається два ключових процеси, пов'язаних з пам'яттю:
account1 (яка є посиланням) живе у стеці.Цей механізм автоматичного керування пам'яттю є однією з ключових переваг .NET, що захищає розробників від поширених помилок, таких як витоки пам'яті.
Конструктор (Constructor) — це спеціальний метод класу, який автоматично викликається під час створення об'єкта. Його призначення — ініціалізувати об'єкт у валідному стані.
void)Неявний конструктор за замовчуванням:
Якщо ви не оголошуєте жодного конструктора у класі, компілятор C# автоматично генерує для вас публічний конструктор без параметрів. Цей неявний конструктор просто ініціалізує всі поля класу їхніми значеннями за замовчуванням (0 для числових типів, null для посилальних типів, false для bool тощо).
Важливо: як тільки ви визначаєте хоча б один конструктор (навіть з параметрами), компілятор більше не створює конструктор за замовчуванням автоматично. Якщо він вам потрібен, ви повинні оголосити його явно.
public class BankAccount
{
public string AccountNumber { get; set; }
public string Owner { get; set; }
public decimal Balance { get; private set; }
// Конструктор без параметрів (Default Constructor)
public BankAccount()
{
AccountNumber = "N/A";
Owner = "Unknown";
Balance = 0m;
}
// Конструктор з параметрами
public BankAccount(string accountNumber, string owner)
{
AccountNumber = accountNumber;
Owner = owner;
Balance = 0m;
}
// Перевантажений конструктор з початковим балансом
public BankAccount(string accountNumber, string owner, decimal initialBalance)
{
AccountNumber = accountNumber;
Owner = owner;
Balance = initialBalance;
}
}
Використання:
var account1 = new BankAccount();
var account2 = new BankAccount("UA001", "Іван Петренко");
var account3 = new BankAccount("UA002", "Марія Коваленко", 5000m);
Конструктори можуть викликати один одного за допомогою ключового слова this:
public class BankAccount
{
public string AccountNumber { get; set; }
public string Owner { get; set; }
public decimal Balance { get; private set; }
// Головний конструктор з усією логікою
public BankAccount(string accountNumber, string owner, decimal initialBalance)
{
// Валідація
if (string.IsNullOrWhiteSpace(accountNumber))
throw new ArgumentException("Account number cannot be empty", nameof(accountNumber));
if (string.IsNullOrWhiteSpace(owner))
throw new ArgumentException("Owner name cannot be empty", nameof(owner));
if (initialBalance < 0)
throw new ArgumentException("Initial balance cannot be negative", nameof(initialBalance));
AccountNumber = accountNumber;
Owner = owner;
Balance = initialBalance;
}
// Делегує виклик головному конструктору
public BankAccount(string accountNumber, string owner)
: this(accountNumber, owner, 0m)
{
}
// Делегує виклик попередньому конструктору
public BankAccount()
: this("N/A", "Unknown", 0m)
{
}
}
this(...). Це зменшує дублювання коду та полегшує підтримку.Primary Constructors — це синтаксичний цукор, введений у C# 12, який дозволяє визначати параметри конструктора безпосередньо в оголошенні класу.
public class BankAccount
{
public string AccountNumber { get; }
public string Owner { get; }
public BankAccount(string accountNumber, string owner)
{
AccountNumber = accountNumber;
Owner = owner;
}
}
// Параметри у дужках після назви класу
public class BankAccount(string accountNumber, string owner)
{
// Параметри доступні у всіх членах класу
public string AccountNumber { get; } = accountNumber;
public string Owner { get; } = owner;
public void DisplayInfo()
{
// Параметри primary constructor доступні як змінні
Console.WriteLine($"Account: {accountNumber}, Owner: {owner}");
}
}
Використання з Dependency Injection:
// Primary constructor ідеально підходить для DI
public class BankController(IBankService bankService, ILogger<BankController> logger) : Controller
{
public IActionResult GetAccount(string accountNumber)
{
logger.LogInformation("Fetching account {AccountNumber}", accountNumber);
var account = bankService.GetAccount(accountNumber);
return View(account);
}
}
Приклад проблеми з подвійним зберіганням:
// ❌ НЕПРАВИЛЬНО - подвійне зберігання
public class BankAccount(string accountNumber, string owner)
{
public string AccountNumber { get; set; } = accountNumber;
public string Owner { get; set; } = owner;
// accountNumber та owner також зберігаються як приховані поля
public void Display() => Console.WriteLine($"{accountNumber} - {owner}");
}
// ✅ ПРАВИЛЬНО - одиничне зберігання
public class BankAccount(string accountNumber, string owner)
{
public string AccountNumber { get; } = accountNumber;
public string Owner { get; } = owner;
// Використовуємо властивості замість параметрів
public void Display() => Console.WriteLine($"{AccountNumber} - {Owner}");
}
Static Constructor викликається автоматично один раз перед першим використанням класу (створення екземпляра або доступ до статичних членів).
public class Configuration
{
public static string DatabaseConnection { get; private set; }
public static int MaxRetries { get; private set; }
// Статичний конструктор (без модифікаторів доступу, без параметрів)
static Configuration()
{
Console.WriteLine("Static constructor called!");
DatabaseConnection = Environment.GetEnvironmentVariable("DB_CONNECTION")
?? "DefaultConnection";
MaxRetries = 3;
}
public static void Display()
{
Console.WriteLine($"DB: {DatabaseConnection}, Retries: {MaxRetries}");
}
}
// Використання
Configuration.Display(); // Спочатку викличеться static constructor один раз
Configuration.Display(); // Static constructor вже не викличеться
Приватний конструктор запобігає створенню екземплярів класу ззовні. Використовується для:
public class DatabaseConnection
{
private static DatabaseConnection _instance;
private static readonly object _lock = new();
// Приватний конструктор - неможливо викликати ззовні
private DatabaseConnection()
{
Console.WriteLine("Database connection initialized");
}
// Публічний статичний метод для отримання єдиного екземпляра
public static DatabaseConnection Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
_instance ??= new DatabaseConnection();
}
}
return _instance;
}
}
}
// Використання
var db1 = DatabaseConnection.Instance;
var db2 = DatabaseConnection.Instance;
// db1 == db2 (один і той самий об'єкт)
// ❌ Це не скомпілюється:
// var db3 = new DatabaseConnection(); // Error: Constructor is inaccessible
Object Initializers дозволяють присвоювати значення публічним властивостям або полям під час створення об'єкта у більш зручному та читабельному синтаксисі.
var account = new BankAccount();
account.AccountNumber = "UA001";
account.Owner = "Іван Петренко";
var account = new BankAccount
{
AccountNumber = "UA001",
Owner = "Іван Петренко"
};
var account = new BankAccount("UA001", "Іван Петренко")
{
// Додаткова ініціалізація після конструктора
Balance = 1500m // Якщо властивість має public set
};
Ініціалізатори також працюють з колекціями:
var accounts = new List<BankAccount>
{
new BankAccount { AccountNumber = "UA001", Owner = "Іван" },
new BankAccount { AccountNumber = "UA002", Owner = "Марія" },
new BankAccount { AccountNumber = "UA003", Owner = "Петро" }
};
Deconstructor — це метод, який дозволяє "розпакувати" об'єкт на окремі складові частини. Це обернена операція до конструктора.
public class BankAccount
{
public string AccountNumber { get; set; }
public string Owner { get; set; }
public decimal Balance { get; set; }
public BankAccount(string accountNumber, string owner, decimal balance)
{
AccountNumber = accountNumber;
Owner = owner;
Balance = balance;
}
// Deconstruct метод з out параметрами
public void Deconstruct(out string accountNumber, out string owner, out decimal balance)
{
accountNumber = AccountNumber;
owner = Owner;
balance = Balance;
}
}
var account = new BankAccount("UA001", "Іван Петренко", 1500m);
// Деконструкція в окремі змінні
var (number, owner, balance) = account;
Console.WriteLine($"Номер: {number}"); // UA001
Console.WriteLine($"Власник: {owner}"); // Іван Петренко
Console.WriteLine($"Баланс: {balance}"); // 1500
// Можна ігнорувати непотрібні значення
var (accountNum, _, _) = account;
Клас може мати кілька перевантажених деконструкторів:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string City { get; set; }
// Деконструктор для імені та прізвища
public void Deconstruct(out string firstName, out string lastName)
{
firstName = FirstName;
lastName = LastName;
}
// Деконструктор для повної інформації
public void Deconstruct(out string firstName, out string lastName, out int age, out string city)
{
firstName = FirstName;
lastName = LastName;
age = Age;
city = City;
}
}
// Використання
var person = new Person
{
FirstName = "Іван",
LastName = "Петренко",
Age = 30,
City = "Київ"
};
var (fname, lname) = person; // Перший деконструктор
var (fname2, lname2, age, city) = person; // Другий деконструктор
thisКлючове слово this представляє посилання на поточний екземпляр класу. Використовується для:
public class BankAccount
{
private string owner;
public BankAccount(string owner)
{
// this.owner - поле класу
// owner - параметр конструктора
this.owner = owner;
}
}
public class BankAccount
{
public BankAccount(string accountNumber, string owner, decimal balance)
{
// Основна логіка
}
public BankAccount(string accountNumber, string owner)
: this(accountNumber, owner, 0m) // Виклик іншого конструктора
{
}
}
public class BankAccount
{
public void RegisterWithBank(Bank bank)
{
// Передаємо поточний об'єкт у метод іншого класу
bank.AddAccount(this);
}
}
public static class BankAccountExtensions
{
// this - перший параметр робить метод розширенням
public static string GetFormattedBalance(this BankAccount account)
{
return $"{account.Balance:C}";
}
}
// Використання
var account = new BankAccount("UA001", "Іван", 1500m);
string formatted = account.GetFormattedBalance(); // Виглядає як метод екземпляра
this є неявним і його можна опустити, якщо немає неоднозначності між іменами полів і параметрів.Теорія: this та статичний контекст
Ключове слово this є посиланням на конкретний екземпляр класу. Статичні члени (поля та методи) належать самому класу, а не його екземплярам. Вони існують незалежно від того, скільки об'єктів створено, і навіть якщо не створено жодного.
Оскільки статичний контекст не пов'язаний з жодним конкретним об'єктом, у ньому просто не існує "поточного екземпляра", на який могло б вказувати this. Саме тому спроба використати this усередині статичного методу призведе до помилки компіляції.
::
Static members належать самому класу, а не окремим екземплярам. Вони спільні для всіх об'єктів класу.
Теорія: Зберігання статичних членів На відміну від полів екземпляра, які зберігаються в купі (Heap) разом з кожним об'єктом, статичні члени зберігаються в спеціальній області пам'яті, яка називається High-Frequency Heap (або як частина метаданих типу). Ця пам'ять виділяється один раз за весь час життя програми — коли завантажувач CLR вперше завантажує метадані типу (класу).
Це означає, що статичні поля ініціалізуються лише один раз (при першому зверненні до класу або при виклику статичного конструктора) і існують протягом усього життєвого циклу домену додатку (AppDomain), незалежно від кількості створених об'єктів. ::
public class BankAccount
{
// Static field - спільний лічильник для всіх рахунків
private static int _accountCount = 0;
// Instance fields - унікальні для кожного об'єкта
public string AccountNumber { get; set; }
public string Owner { get; set; }
public BankAccount(string owner)
{
_accountCount++; // Збільшуємо загальний лічильник
AccountNumber = $"UA{_accountCount:D6}";
Owner = owner;
}
// Static property
public static int TotalAccounts => _accountCount;
}
// Використання
var acc1 = new BankAccount("Іван"); // AccountNumber = UA000001
var acc2 = new BankAccount("Марія"); // AccountNumber = UA000002
var acc3 = new BankAccount("Петро"); // AccountNumber = UA000003
Console.WriteLine(BankAccount.TotalAccounts); // 3
Статичні методи не мають доступу до this та instance members:
public class MathHelper
{
// Static method - утилітна функція
public static double CalculateInterest(decimal principal, double rate, int years)
{
return (double)principal * rate * years;
}
// Static method з параметром екземпляра
public static bool IsValidAccount(BankAccount account)
{
return account != null && !string.IsNullOrEmpty(account.AccountNumber);
}
}
// Використання через назву класу
double interest = MathHelper.CalculateInterest(1000m, 0.05, 3);
public class Calculator
{
private int baseValue;
public int Add(int x)
{
return baseValue + x; // Доступ до instance field
}
}
var calc = new Calculator();
int result = calc.Add(5);
public class Calculator
{
public static int Add(int x, int y)
{
// Немає доступу до instance members
// Немає доступу до this
return x + y;
}
}
int result = Calculator.Add(5, 3);
Static class — це клас, який:
sealed та abstractpublic static class TemperatureConverter
{
public static double CelsiusToFahrenheit(double celsius)
{
return (celsius * 9.0 / 5.0) + 32;
}
public static double FahrenheitToCelsius(double fahrenheit)
{
return (fahrenheit - 32) * 5.0 / 9.0;
}
}
// Використання
double temp = TemperatureConverter.CelsiusToFahrenheit(25);
// ❌ Це не скомпілюється:
// var converter = new TemperatureConverter(); // Error: Cannot create instance
| Критерій | Static Members | Instance Members |
|---|---|---|
| Дані | Спільні для всього класу | Унікальні для кожного об'єкта |
| Доступ | Через назву класу | Через екземпляр |
| Використання | Утилітні функції, константи, лічильники | Дані та поведінка конкретного об'єкта |
| Приклади | Math.Sqrt(), Console.WriteLine() | account.Deposit(), person.GetAge() |
Access Modifiers контролюють видимість і доступність членів класу. Вони є основою інкапсуляції (Encapsulation) — одного з трьох стовпів ООП.
Теорія: Навіщо потрібна Інкапсуляція?
Інкапсуляція — це механізм, який об'єднує дані (поля) та методи, що ними маніпулюють, в єдиний компонент (клас) і приховує внутрішню реалізацію від зовнішнього світу. Це дає дві ключові переваги:
private, а для доступу до них надати public властивості або методи. Це дозволяє вам додавати логіку валідації, запобігаючи запису некоректних даних і гарантуючи, що об'єкт завжди перебуває у валідному стані.Модифікатори доступу є головним інструментом для реалізації інкапсуляції в C#.
| Модифікатор | Опис | Доступ з класу | Доступ з похідного класу | Доступ з assembly | Доступ з іншого assembly |
|---|---|---|---|---|---|
public | Повна видимість | ✅ | ✅ | ✅ | ✅ |
private | Тільки в межах класу | ✅ | ❌ | ❌ | ❌ |
protected | Клас + похідні класи | ✅ | ✅ | ❌ (тільки через спадкування) | ✅ (тільки через спадкування) |
internal | Тільки в межах assembly | ✅ | ✅ | ✅ | ❌ |
protected internal | protected АБО internal | ✅ | ✅ | ✅ | ✅ (тільки через спадкування) |
private protected | protected І internal | ✅ | ✅ (тільки в цьому assembly) | ❌ | ❌ |
file (C# 11) | Тільки в межах файлу | ✅ | ❌ | ❌ | ❌ |
public - Публічний ДоступНайбільш відкритий рівень доступу. Член доступний звідусіль.
public class BankAccount
{
// Публічна властивість - доступна всім
public string AccountNumber { get; set; }
// Публічний метод
public void Deposit(decimal amount)
{
// ...
}
}
// Використання з будь-якого місця
var account = new BankAccount();
account.AccountNumber = "UA001"; // OK
account.Deposit(100); // OK
private - Приватний ДоступЧлен доступний тільки в межах того самого класу.
public class BankAccount
{
// Приватне поле - доступне тільки всередині BankAccount
private decimal _balance;
public void Deposit(decimal amount)
{
_balance += amount; // OK - доступ з того самого класу
}
private bool ValidateAmount(decimal amount)
{
return amount > 0;
}
}
// Використання
var account = new BankAccount();
// account._balance = 100; // ❌ Error: Cannot access private member
// account.ValidateAmount(50); // ❌ Error: Cannot access private method
private) і надавайте доступ через публічні властивості або методи. Це дозволяє контролювати валідацію та зміну даних.protected - Захищений ДоступЧлен доступний всередині класу та в усіх похідних класах (навіть в інших assemblies).
public class BankAccount
{
// Захищене поле - доступне в похідних класах
protected decimal _balance;
protected virtual void LogTransaction(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
public class SavingsAccount : BankAccount
{
public void AddInterest(double rate)
{
// Доступ до protected члена батьківського класу
_balance += _balance * (decimal)rate;
LogTransaction($"Interest added: {rate * 100}%");
}
}
// Використання
var savings = new SavingsAccount();
// savings._balance = 1000; // ❌ Error: Cannot access protected member
// savings.LogTransaction(""); // ❌ Error: Cannot access protected method
internal - Внутрішній Доступ (Assembly-Level)Член доступний тільки в межах того самого assembly (проекту, DLL).
// Файл у проекті "BankingCore.dll"
public class BankAccount
{
// Доступний тільки в межах BankingCore.dll
internal string InternalId { get; set; }
internal void InternalAudit()
{
Console.WriteLine("Internal audit performed");
}
}
// В іншому класі того самого проекту
public class BankManager
{
public void Process(BankAccount account)
{
account.InternalId = "ID123"; // OK - той самий assembly
account.InternalAudit(); // OK
}
}
// Файл у проекті "BankingApp.exe" (інший assembly)
var account = new BankAccount();
// account.InternalId = "ID"; // ❌ Error: Cannot access internal member
// account.InternalAudit(); // ❌ Error: Cannot access internal method
protected internal - КомбінаціяЧлен доступний в межах assembly АБО в похідних класах (навіть в інших assemblies).
public class BankAccount
{
// Доступний: в цьому assembly АБО в похідних класах інших assemblies
protected internal decimal GetInternalBalance()
{
return _balance;
}
}
// В іншому assembly, але похідний клас
public class PremiumAccount : BankAccount
{
public void Display()
{
// OK - доступ через спадкування
var balance = GetInternalBalance();
}
}
private protected - Найбільш ОбмеженийЧлен доступний тільки в похідних класах І тільки в межах того самого assembly.
public class BankAccount
{
// Доступний: тільки в похідних класах цього assembly
private protected void SensitiveOperation()
{
// Критична операція
}
}
public class SavingsAccount : BankAccount
{
public void Process()
{
SensitiveOperation(); // OK - похідний клас в тому самому assembly
}
}
public class ForeignAccount : BankAccount
{
public void Process()
{
// SensitiveOperation(); // ❌ Error: Інший assembly
}
}
file - File-Scoped Types (C# 11)Обмежує видимість типу до одного файлу.
// Клас доступний тільки в цьому файлі
file class TransactionValidator
{
public bool Validate(decimal amount)
{
return amount > 0;
}
}
public class BankAccount
{
public void Deposit(decimal amount)
{
var validator = new TransactionValidator();
if (validator.Validate(amount))
{
// ...
}
}
}
public class AccountManager
{
public void Process()
{
// ❌ Error: TransactionValidator не доступний в цьому файлі
// var validator = new TransactionValidator();
}
}
file: Корисно для helper-класів, які потрібні тільки в одному файлі, наприклад, для Source Generators або для ізоляції допоміжних типів.public class BankAccount
{
private decimal balance;
}
var account = new BankAccount();
account.balance = 1000; // ❌ Error CS0122: 'BankAccount.balance' is inaccessible
Рішення: Використовуйте публічні властивості або методи:
public class BankAccount
{
private decimal _balance;
public decimal Balance
{
get => _balance;
private set => _balance = value; // Public get, private set
}
public void Deposit(decimal amount)
{
_balance += amount;
}
}
BankAccount account; // Тільки оголошення, не ініціалізація
account.Deposit(100); // ❌ NullReferenceException
Рішення: Завжди ініціалізуйте об'єкти перед використанням:
BankAccount account = new BankAccount(); // Або new()
account.Deposit(100); // ✅ OK
public class BankAccount
{
public static int TotalAccounts;
public string Owner;
}
var account = new BankAccount();
account.TotalAccounts = 5; // ❌ Error: Cannot access static member via instance
Рішення: Доступ до static через назву класу:
BankAccount.TotalAccounts = 5; // ✅ Static через клас
account.Owner = "Іван"; // ✅ Instance через об'єкт
// ❌ Параметри зберігаються двічі
public class BankAccount(string owner)
{
public string Owner { get; set; } = owner; // Зберігається тут
public void Display()
{
Console.WriteLine(owner); // І тут (прихована змінна)
}
}
Рішення: Використовуйте властивості замість параметрів primary constructor:
// ✅ Одиничне зберігання
public class BankAccount(string owner)
{
public string Owner { get; } = owner;
public void Display()
{
Console.WriteLine(Owner); // Використовуємо властивість
}
}
Завдання: Створіть клас Person з наступними характеристиками:
FirstName, LastName, BirthYearGetAge(), що розраховує вік на основі поточного рокуGetFullName(), що повертає повне ім'яpublic class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int BirthYear { get; set; }
public Person(string firstName, string lastName, int birthYear)
{
FirstName = firstName;
LastName = lastName;
BirthYear = birthYear;
}
public int GetAge()
{
return DateTime.Now.Year - BirthYear;
}
public string GetFullName()
{
return $"{FirstName} {LastName}";
}
}
// Використання
var person = new Person("Іван", "Петренко", 1990);
Console.WriteLine($"Повне ім'я: {person.GetFullName()}");
Console.WriteLine($"Вік: {person.GetAge()}");
Завдання: Створіть ієрархію банківських рахунків:
BankAccount з primary constructor:- Параметри: `accountNumber`, `owner`
- Властивості: `AccountNumber`, `Owner`, `Balance` (тільки get)
- Методи: `Deposit(amount)`, `Withdraw(amount)`, `GetAccountInfo()`
- Static поле `TotalAccounts` для підрахунку кількості рахунків
2. Похідний клас SavingsAccount:
- Додаткова властивість: `InterestRate`
- Метод `ApplyInterest()` для нарахування відсотків
3. Використайте різні модифікатори доступу:
- Balance має private set
- Метод валідації має бути private
public class BankAccount(string accountNumber, string owner)
{
// Static counter
private static int _totalAccounts = 0;
// Properties
public string AccountNumber { get; } = accountNumber;
public string Owner { get; } = owner;
public decimal Balance { get; private set; }
// Static property
public static int TotalAccounts => _totalAccounts;
// Constructor logic
public BankAccount(string accountNumber, string owner, decimal initialBalance)
: this(accountNumber, owner)
{
if (initialBalance < 0)
throw new ArgumentException("Initial balance cannot be negative");
Balance = initialBalance;
_totalAccounts++;
}
// Methods
public void Deposit(decimal amount)
{
if (!ValidateAmount(amount))
throw new ArgumentException("Invalid deposit amount");
Balance += amount;
Console.WriteLine($"Deposited {amount:C}. New balance: {Balance:C}");
}
public virtual bool Withdraw(decimal amount)
{
if (!ValidateAmount(amount) || Balance < amount)
{
Console.WriteLine("Insufficient funds or invalid amount");
return false;
}
Balance -= amount;
Console.WriteLine($"Withdrawn {amount:C}. New balance: {Balance:C}");
return true;
}
public string GetAccountInfo()
{
return $"Account: {AccountNumber}, Owner: {Owner}, Balance: {Balance:C}";
}
// Private validation
private bool ValidateAmount(decimal amount)
{
return amount > 0;
}
// Deconstructor
public void Deconstruct(out string accountNumber, out string owner, out decimal balance)
{
accountNumber = AccountNumber;
owner = Owner;
balance = Balance;
}
}
public class SavingsAccount : BankAccount
{
public double InterestRate { get; set; }
public SavingsAccount(string accountNumber, string owner, decimal initialBalance, double interestRate)
: base(accountNumber, owner, initialBalance)
{
InterestRate = interestRate;
}
public void ApplyInterest()
{
decimal interest = Balance * (decimal)InterestRate;
Deposit(interest);
Console.WriteLine($"Interest applied: {interest:C} at rate {InterestRate:P}");
}
}
// Використання
var checking = new BankAccount("UA001", "Іван Петренко", 5000m);
var savings = new SavingsAccount("UA002", "Марія Коваленко", 10000m, 0.05);
checking.Deposit(1500m);
checking.Withdraw(500m);
savings.ApplyInterest();
Console.WriteLine($"\nTotal accounts created: {BankAccount.TotalAccounts}");
// Використання деконструктора
var (number, owner, balance) = checking;
Console.WriteLine($"\nDeconstructed: {number}, {owner}, {balance:C}");
Завдання: Створіть thread-safe Singleton для системи логування з наступними особливостями:
Logger:- Приватний конструктор
- Static властивість `Instance` для отримання єдиного екземпляра
- Thread-safe ініціалізація
- Метод `Log(message)` для запису логів
- Static метод `GetLogCount()` для отримання кількості логів
2. Використайте:
- private constructor
- private static поле для екземпляра
- lock для thread-safety
- internal метод для тестування
public sealed class Logger
{
// Singleton instance
private static Logger? _instance;
private static readonly object _lock = new();
// Instance members
private readonly List<string> _logs = new();
private int _logCount;
// Private constructor
private Logger()
{
Console.WriteLine("Logger instance created");
}
// Public static property for accessing singleton
public static Logger Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
// Double-check locking pattern
_instance ??= new Logger();
}
}
return _instance;
}
}
// Public method
public void Log(string message)
{
lock (_lock)
{
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
string logEntry = $"[{timestamp}] {message}";
_logs.Add(logEntry);
_logCount++;
Console.WriteLine(logEntry);
}
}
// Static method
public static int GetLogCount()
{
return Instance._logCount;
}
// Internal method for testing
internal List<string> GetAllLogs()
{
lock (_lock)
{
return new List<string>(_logs); // Return copy
}
}
// Clear logs (useful for testing)
internal void ClearLogs()
{
lock (_lock)
{
_logs.Clear();
_logCount = 0;
}
}
}
// Допоміжний static клас для демонстрації
public static class LoggerHelper
{
public static void LogMultipleMessages(params string[] messages)
{
var logger = Logger.Instance;
foreach (var message in messages)
{
logger.Log(message);
}
}
}
// Використання
var logger1 = Logger.Instance;
var logger2 = Logger.Instance;
Console.WriteLine($"Same instance? {ReferenceEquals(logger1, logger2)}"); // True
logger1.Log("Application started");
logger2.Log("Processing data");
LoggerHelper.LogMultipleMessages(
"Step 1: Initialization",
"Step 2: Processing",
"Step 3: Completion"
);
Console.WriteLine($"\nTotal logs: {Logger.GetLogCount()}");
// ❌ Це не скомпілюється:
// var logger3 = new Logger(); // Error: Constructor is inaccessible
У цьому розділі ви навчились:
Класи та Об'єкти
new та розуміти процес виділення пам'яті.Конструктори
Ініціалізатори та Деконструктори
Ключове Слово this
this для доступу до поточного екземпляра, розрізнення між полями та параметрами, та виклику інших конструкторів.Статичні Члени
Модифікатори Доступу
public, private, protected, internal, protected internal, private protected, file) та навчились правильно їх застосовувати для інкапсуляції.private fields і public properties для контролю доступу