Додаткові Можливості C#
Додаткові Можливості C#
Вступ та Контекст
C# постійно еволюціонує, пропонуючи розробникам потужні механізми для написання виразного, підтримуваного та ефективного коду. У цьому розділі ми розглянемо п'ять ключових можливостей мови:
- Operator Overloading — надання нового значення стандартним операторам для власних типів
- Extension Methods — розширення функціональності типів без зміни їх вихідного коду
- Partial Classes/Methods/Properties — розбиття визначення типу на кілька файлів
- Interceptors — експериментальна можливість заміни викликів методів на етапі компіляції
- Source Generators — генерація коду під час компіляції за допомогою Roslyn API
Еволюція Можливостей
| Можливість | Версія C# | Статус | Призначення |
|---|---|---|---|
| Operator Overloading | C# 1.0 | Stable | Природний синтаксис для власних типів |
| Extension Methods | C# 3.0 | Stable | Розширення типів без наслідування |
| Partial Classes | C# 2.0 | Stable | Організація коду, generated code |
| Partial Methods | C# 3.0 | Stable | Хуки в generated code |
| Partial Properties | C# 13 | Stable | Розділення декларації та реалізації властивостей |
| Extension Members | C# 14 | Preview | Властивості та статичні розширення |
| Interceptors | C# 12 | Experimental | AOP, source generator hooks |
| Source Generators | C# 9+ | Stable | Compile-time code generation |
Operator Overloading (Перевантаження Операторів)
Навіщо це потрібно?
Уявіть, що ви створюєте математичну бібліотеку для роботи з векторами. Без перевантаження операторів ваш код виглядатиме так:
Vector a = new Vector(1, 2);
Vector b = new Vector(3, 4);
Vector sum = a.Add(b);
Vector scaled = sum.Multiply(2);
З перевантаженням операторів:
Vector a = new Vector(1, 2);
Vector b = new Vector(3, 4);
Vector sum = a + b;
Vector scaled = sum * 2;
Набагато природніше! Код читається як математична формула.
Money + Money — зрозуміло. Customer + Order — що це означає?Теоретична основа: Синтаксичний цукор та виразність мови
Перевантаження операторів — це класичний приклад синтаксичного цукру (syntactic sugar). Цей термін означає синтаксичну конструкцію, яка не додає нового функціоналу до мови, але робить її "солодшою" — тобто, легшою для читання, написання та розуміння.
- Декларативність проти Імперативності: Виклик
a.Add(b)є імперативним — він каже комп'ютеру "виконай операцію додавання". Виразa + bє декларативним — він описує математичну ідею суми, а компілятор вже сам вирішує, як її реалізувати. Це робить код ближчим до предметної області (математика, фінанси), а не до деталей реалізації. - Когнітивне навантаження: Людський мозок краще сприймає звичні символи. Використання
+для додавання векторів або грошей знижує когнітивне навантаження, оскільки не потрібно запам'ятовувати назви методів (Add,Sum,Plus?).
Однак, як і з цукром, зловживання може бути шкідливим. Неправильне використання перевантаження операторів може призвести до коду, який є неінтуїтивним і складним для розуміння, порушуючи "принцип найменшого здивування".
Фундаментальні Концепції
Operator Overloading дозволяє визначити поведінку стандартних операторів C# (+, -, *, ==, тощо) для власних типів.
Ключові правила:
- Оператори оголошуються як
public staticметоди - Використовується ключове слово
operator - Деякі оператори мають бути перевантажені парами
Синтаксис
public static ReturnType operator SymbolOperator(ParameterType parameter)
{
// Реалізація
}
Категорії Операторів
Приклад: Vector Class
public class Vector
{
public double X { get; }
public double Y { get; }
public Vector(double x, double y)
{
X = x;
Y = y;
}
// Унарний оператор: -vector
public static Vector operator -(Vector v)
{
return new Vector(-v.X, -v.Y);
}
// Бінарний оператор: vector + vector
public static Vector operator +(Vector a, Vector b)
{
return new Vector(a.X + b.X, a.Y + b.Y);
}
// Бінарний оператор: vector - vector
public static Vector operator -(Vector a, Vector b)
{
return new Vector(a.X - b.X, a.Y - b.Y);
}
// Множення на скаляр: vector * number
public static Vector operator *(Vector v, double scalar)
{
return new Vector(v.X * scalar, v.Y * scalar);
}
// Комутативне множення: number * vector
public static Vector operator *(double scalar, Vector v)
{
return v * scalar;
}
// Скалярний добуток: vector * vector
public static double operator *(Vector a, Vector b)
{
return a.X * b.X + a.Y * b.Y;
}
public override string ToString() => $"({X}, {Y})";
}
Vector v1 = new Vector(3, 4);
Vector v2 = new Vector(1, 2);
// Унарний мінус
Vector negated = -v1;
Console.WriteLine(negated); // (-3, -4)
// Додавання
Vector sum = v1 + v2;
Console.WriteLine(sum); // (4, 6)
// Віднімання
Vector diff = v1 - v2;
Console.WriteLine(diff); // (2, 2)
// Множення на скаляр
Vector scaled = v1 * 2;
Console.WriteLine(scaled); // (6, 8)
// Комутативне множення
Vector scaled2 = 3 * v1;
Console.WriteLine(scaled2); // (9, 12)
// Скалярний добуток
double dotProduct = v1 * v2;
Console.WriteLine(dotProduct); // 11
Розбір коду:
- Рядки 13-16: Унарний оператор
-інвертує знаки компонентів вектора - Рядки 19-22: Бінарний
+додає відповідні компоненти - Рядки 30-33: Множення вектора на число
- Рядки 36-39: Комутативна версія (число спереду)
- Рядки 42-45: Перевантаження для скалярного добутку (інший тип повернення!)
* має дві різні версії з різними типами повернення:Vector * double→ повертаєVectorVector * Vector→ повертаєdouble
Приклад: Money Class
public class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency ?? throw new ArgumentNullException(nameof(currency));
}
// Додавання грошей
public static Money operator +(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException($"Cannot add {a.Currency} and {b.Currency}");
return new Money(a.Amount + b.Amount, a.Currency);
}
// Віднімання грошей
public static Money operator -(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException($"Cannot subtract {a.Currency} and {b.Currency}");
return new Money(a.Amount - b.Amount, a.Currency);
}
// Множення на коефіцієнт
public static Money operator *(Money money, decimal multiplier)
{
return new Money(money.Amount * multiplier, money.Currency);
}
// Ділення на коефіцієнт
public static Money operator /(Money money, decimal divisor)
{
if (divisor == 0)
throw new DivideByZeroException();
return new Money(money.Amount / divisor, money.Currency);
}
public override string ToString() => $"{Amount:C} {Currency}";
}
// Використання
Money price = new Money(100, "UAH");
Money discount = new Money(20, "UAH");
Money finalPrice = price - discount; // 80 UAH
Money doubled = price * 2; // 200 UAH
Money half = price / 2; // 50 UAH
// Money usd = new Money(50, "USD");
// Money invalid = price + usd; // ❌ Exception: Cannot add UAH and USD
Оператори Порівняння
==та!=<та><=та>=
Equals() та GetHashCode().public class Vector : IEquatable<Vector>
{
public double X { get; }
public double Y { get; }
public Vector(double x, double y)
{
X = x;
Y = y;
}
// Оператор рівності
public static bool operator ==(Vector? a, Vector? b)
{
if (ReferenceEquals(a, b)) return true;
if (a is null || b is null) return false;
return a.X == b.X && a.Y == b.Y;
}
// Оператор нерівності (пара для ==)
public static bool operator !=(Vector? a, Vector? b)
{
return !(a == b);
}
// Перевизначення Equals для узгодженості
public override bool Equals(object? obj)
{
return Equals(obj as Vector);
}
public bool Equals(Vector? other)
{
return this == other;
}
// Перевизначення GetHashCode
public override int GetHashCode()
{
return HashCode.Combine(X, Y);
}
}
// Використання
Vector v1 = new Vector(1, 2);
Vector v2 = new Vector(1, 2);
Vector v3 = new Vector(3, 4);
Console.WriteLine(v1 == v2); // True
Console.WriteLine(v1 != v3); // True
Чому потрібні Equals і GetHashCode?
==працює з операторамиEquals()використовується в колекціях (LINQ, Dictionary)GetHashCode()критичний дляDictionary<T>таHashSet<T>
Оператори Перетворення
Дозволяють неявно або явно перетворювати один тип на інший.
public class Celsius
{
public double Temperature { get; }
public Celsius(double temperature)
{
Temperature = temperature;
}
// Неявне перетворення: Celsius → double
public static implicit operator double(Celsius c)
{
return c.Temperature;
}
// Явне перетворення: double → Celsius
public static explicit operator Celsius(double temp)
{
return new Celsius(temp);
}
public override string ToString() => $"{Temperature}°C";
}
public class Fahrenheit
{
public double Temperature { get; }
public Fahrenheit(double temperature)
{
Temperature = temperature;
}
// Явне перетворення: Celsius → Fahrenheit
public static explicit operator Fahrenheit(Celsius c)
{
return new Fahrenheit(c.Temperature * 9 / 5 + 32);
}
public override string ToString() => $"{Temperature}°F";
}
// Використання
Celsius celsius = new Celsius(25);
// Неявне перетворення
double temp = celsius; // ✅ OK, implicit
Console.WriteLine(temp); // 25
// Явне перетворення
Celsius c2 = (Celsius)30.0; // ✅ OK, explicit cast required
Console.WriteLine(c2); // 30°C
// Перетворення між класами
Fahrenheit fahrenheit = (Fahrenheit)celsius;
Console.WriteLine(fahrenheit); // 77°F
implicit vs explicit?implicit: Коли перетворення завжди безпечне і не втрачає інформаціїexplicit: Коли можлива втрата даних або потрібна явна згода розробника
Індексатори (Indexers)
Хоча індексатори технічно не є операторами, вони перевантажують синтаксис [] і дуже корисні для колекцій.
public class Matrix
{
private double[,] _data;
public int Rows { get; }
public int Cols { get; }
public Matrix(int rows, int cols)
{
Rows = rows;
Cols = cols;
_data = new double[rows, cols];
}
// Індексатор для доступу до елементів
public double this[int row, int col]
{
get
{
if (row < 0 || row >= Rows || col < 0 || col >= Cols)
throw new IndexOutOfRangeException();
return _data[row, col];
}
set
{
if (row < 0 || row >= Rows || col < 0 || col >= Cols)
throw new IndexOutOfRangeException();
_data[row, col] = value;
}
}
// Додатковий індексатор для лінійного доступу
public double this[int index]
{
get
{
int row = index / Cols;
int col = index % Cols;
return this[row, col];
}
set
{
int row = index / Cols;
int col = index % Cols;
this[row, col] = value;
}
}
}
// Використання
Matrix matrix = new Matrix(3, 3);
matrix[0, 0] = 1.0; // Використання індексатора
matrix[1, 1] = 2.0;
Console.WriteLine(matrix[0, 0]); // 1.0
// Лінійний доступ
matrix[4] = 5.0; // Рядок 1, колонка 1
Console.WriteLine(matrix[1, 1]); // 5.0
Переваги індексаторів:
- Природний синтаксис доступу до елементів
- Можуть мати різні сигнатури (різна кількість параметрів)
- Підтримка get та set accessors
Оператори Інкременту/Декременту
public class Counter
{
private int _value;
public Counter(int initialValue = 0)
{
_value = initialValue;
}
public int Value => _value;
// Prefix increment: ++counter
public static Counter operator ++(Counter c)
{
c._value++;
return c;
}
// Prefix decrement: --counter
public static Counter operator --(Counter c)
{
c._value--;
return c;
}
public override string ToString() => _value.ToString();
}
// Використання
Counter counter = new Counter(5);
// Prefix increment
++counter;
Console.WriteLine(counter); // 6
// Postfix також працює (компілятор обробляє автоматично)
counter++;
Console.WriteLine(counter); // 7
--counter;
Console.WriteLine(counter); // 6
++x) та postfix (x++) форми. Компілятор автоматично обробляє обидві форми на основі одного перевантаженого оператора.Оператори True/False
Дозволяють використовувати власні типи в умовних виразах (if, while).
public class Result
{
public bool IsSuccess { get; }
public string Message { get; }
public Result(bool isSuccess, string message)
{
IsSuccess = isSuccess;
Message = message;
}
// Оператор true
public static bool operator true(Result r)
{
return r.IsSuccess;
}
// Оператор false (має бути парою з true)
public static bool operator false(Result r)
{
return !r.IsSuccess;
}
// Логічний AND для short-circuit evaluation
public static Result operator &(Result a, Result b)
{
if (!a.IsSuccess) return a;
if (!b.IsSuccess) return b;
return new Result(true, $"{a.Message} and {b.Message}");
}
// Логічний OR для short-circuit evaluation
public static Result operator |(Result a, Result b)
{
if (a.IsSuccess) return a;
if (b.IsSuccess) return b;
return new Result(false, $"{a.Message} or {b.Message}");
}
}
// Використання
Result CheckAge(int age)
{
return age >= 18
? new Result(true, "Age OK")
: new Result(false, "Too young");
}
Result CheckLicense(bool hasLicense)
{
return hasLicense
? new Result(true, "License OK")
: new Result(false, "No license");
}
Result ageCheck = CheckAge(20);
Result licenseCheck = CheckLicense(true);
// Використання в умові!
if (ageCheck && licenseCheck)
{
Console.WriteLine("Can drive!");
}
// З операторами & та | працює short-circuit
Result combined = ageCheck && licenseCheck;
Console.WriteLine(combined.Message); // "Age OK and License OK"
true/false у поєднанні з & і | дозволяють реалізувати short-circuit evaluation (&& і ||) для власних типів!Bitwise та Логічні Оператори
[Flags]
public enum Permission
{
None = 0,
Read = 1,
Write = 2,
Execute = 4,
Delete = 8
}
public class PermissionSet
{
private Permission _permissions;
public PermissionSet(Permission permissions)
{
_permissions = permissions;
}
// Bitwise OR для додавання прав
public static PermissionSet operator |(PermissionSet a, PermissionSet b)
{
return new PermissionSet(a._permissions | b._permissions);
}
// Bitwise AND для перевірки спільних прав
public static PermissionSet operator &(PermissionSet a, PermissionSet b)
{
return new PermissionSet(a._permissions & b._permissions);
}
// Bitwise XOR для symmetric difference
public static PermissionSet operator ^(PermissionSet a, PermissionSet b)
{
return new PermissionSet(a._permissions ^ b._permissions);
}
// Bitwise NOT для інверсії прав
public static PermissionSet operator ~(PermissionSet p)
{
return new PermissionSet(~p._permissions);
}
public bool HasPermission(Permission permission)
{
return (_permissions & permission) == permission;
}
public override string ToString() => _permissions.ToString();
}
// Використання
var readWrite = new PermissionSet(Permission.Read | Permission.Write);
var writeExecute = new PermissionSet(Permission.Write | Permission.Execute);
// Об'єднання прав
var all = readWrite | writeExecute;
Console.WriteLine(all); // Read, Write, Execute
// Спільні права
var common = readWrite & writeExecute;
Console.WriteLine(common); // Write
// Перевірка прав
Console.WriteLine(all.HasPermission(Permission.Read)); // True
Console.WriteLine(readWrite.HasPermission(Permission.Execute)); // False
Shift Operators (Зсув)
public class BitArray
{
private uint _bits;
public BitArray(uint bits)
{
_bits = bits;
}
// Left shift: битовий зсув вліво
public static BitArray operator <<(BitArray array, int shift)
{
return new BitArray(array._bits << shift);
}
// Right shift: битовий зсув вправо
public static BitArray operator >>(BitArray array, int shift)
{
return new BitArray(array._bits >> shift);
}
// Unsigned right shift (C# 11+)
public static BitArray operator >>>(BitArray array, int shift)
{
return new BitArray(array._bits >>> shift);
}
public override string ToString()
{
return Convert.ToString(_bits, 2).PadLeft(32, '0');
}
}
// Використання
BitArray bits = new BitArray(0b00001111); // 15
BitArray shiftedLeft = bits << 2;
Console.WriteLine(shiftedLeft); // 00111100 (60)
BitArray shiftedRight = bits >> 2;
Console.WriteLine(shiftedRight); // 00000011 (3)
>>> (unsigned right shift) був доданий у C# 11 для явного беззнакового зсуву.Таблиця Операторів
| Категорія | Оператори | Можна перевантажити? | Парність |
|---|---|---|---|
| Арифметичні | + - * / % | ✅ Так | Немає |
| Унарні | + - ! ~ ++ -- | ✅ Так | Немає |
| Порівняння | == != | ✅ Так | Обов'язкова |
| Порів Няння | < > <= >= | ✅ Так | Попарно |
| Логічні | && || | ❌ Ні (але можна & |) | - |
| Присвоєння | = += -= *= /= | ❌ Ні (виводяться автоматично) | - |
| Індексування | [] | ✅ Так (через indexers) | - |
| Виклик | () | ❌ Ні | - |
| Перетворення | implicit explicit | ✅ Так | Немає |
Best Practices
- Оператор має очевидне значення (
Complex + Complex) - Підвищує читабельність математичного коду
- Відповідає математичним або доменним конвенціям
- Значення оператора неочевидне чи неоднозначне
- Операція дорога (краще явний метод)
- Логіка складна і може здивувати користувача
Extension Methods (Методи-Розширення)
Проблема: Розширення Типів, які Ви не Контролюєте
Уявіть, що ви працюєте з класом string, але вам потрібен метод для перевірки, чи є рядок валідним email:
// ❌ Не можемо модифікувати клас string
public class string
{
public bool IsValidEmail() // Неможливо!
{
// ...
}
}
// ❌ Створення wrapper класу — громіздко
public class StringHelper
{
public static bool IsValidEmail(string email)
{
// ...
}
}
StringHelper.IsValidEmail("[email protected]"); // Не дуже зручно
Extension Methods вирішують цю проблему!
Класичний Синтаксис (C# 3.0+)
Extension methods дозволяють додавати методи до існуючих типів без:
- Модифікації вихідного коду
- Створення нових похідних типів
- Використання wrapper-класів
Синтаксис:
public static class StringExtensions
{
// Ключове слово 'this' перед першим параметром
public static bool IsValidEmail(this string email)
{
if (string.IsNullOrWhiteSpace(email))
return false;
return email.Contains('@') && email.Contains('.');
}
}
// Використання як метод екземпляра!
string email = "[email protected]";
bool isValid = email.IsValidEmail(); // ✅ Природний синтаксис
Console.WriteLine(isValid); // True
Ключові моменти:
- Extension method має бути
static - Має бути у
staticкласі - Перший параметр має модифікатор
this - Викликається як метод екземпляра
Під Капотом
Компілятор перетворює виклик extension method на виклик статичного методу:
### Архітектурний аспект: Принцип Відкритості/Закритості
Методи-розширення є чудовою реалізацією **Принципу Відкритості/Закритості (Open/Closed Principle)** з SOLID. Цей принцип говорить:
> Програмні сутності (класи, модулі, функції) мають бути **відкритими для розширення**, але **закритими для модифікації**.
- **Закриті для модифікації**: Ви не можете (і не повинні) змінювати вихідний код `string` або `IEnumerable<T>`. Ці типи є стабільними та надійними.
- **Відкриті для розширення**: За допомогою extension methods ви можете додавати нову функціональність до цих типів, не втручаючись у їхню внутрішню реалізацію.
Таким чином, методи-розширення дозволяють адаптувати та розширювати існуючі, навіть фундаментальні, типи під потреби вашого домену, не порушуючи їх цілісність. Це особливо важливо при роботі з бібліотеками та фреймворками сторонніх розробників. LINQ є найяскравішим прикладом цієї ідеї: вся його функціональність побудована на методах-розширеннях для інтерфейсу `IEnumerable<T>`.
// Ви пишете:
email.IsValidEmail();
// Компілятор перетворює на:
StringExtensions.IsValidEmail(email);
Приклади Extension Methods
String Extensions
public static class StringExtensions
{
public static string Reverse(this string str)
{
char[] chars = str.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
public static bool IsPalindrome(this string str)
{
string reversed = str.Reverse();
return str.Equals(reversed, StringComparison.OrdinalIgnoreCase);
}
public static string Truncate(this string str, int maxLength)
{
if (string.IsNullOrEmpty(str) || str.Length <= maxLength)
return str;
return str.Substring(0, maxLength) + "...";
}
public static int ToInt(this string str, int defaultValue = 0)
{
return int.TryParse(str, out int result) ? result : defaultValue;
}
}
// Використання
string text = "Hello";
Console.WriteLine(text.Reverse()); // olleH
Console.WriteLine("radar".IsPalindrome()); // True
string longText = "This is a very long text";
Console.WriteLine(longText.Truncate(10)); // This is a ...
string number = "42";
Console.WriteLine(number.ToInt()); // 42
IEnumerable Extensions
public static class EnumerableExtensions
{
public static IEnumerable<T> WhereNot<T>(
this IEnumerable<T> source,
Func<T, bool> predicate)
{
return source.Where(x => !predicate(x));
}
public static bool IsNullOrEmpty<T>(this IEnumerable<T>? source)
{
return source == null || !source.Any();
}
public static string JoinWith<T>(
this IEnumerable<T> source,
string separator)
{
return string.Join(separator, source);
}
public static void ForEach<T>(
this IEnumerable<T> source,
Action<T> action)
{
foreach (T item in source)
{
action(item);
}
}
}
// Використання
var numbers = new[] { 1, 2, 3, 4, 5 };
// Відфільтрувати все, що НЕ парне
var oddNumbers = numbers.WhereNot(x => x % 2 == 0);
Console.WriteLine(oddNumbers.JoinWith(", ")); // 1, 3, 5
// ForEach з лямбдою
numbers.ForEach(n => Console.WriteLine(n * 2));
// 2, 4, 6, 8, 10
Chaining (Ланцюжки Викликів)
Extension methods чудово працюють у ланцюжках:
string result = " Hello World "
.Trim()
.ToLower()
.Reverse()
.Truncate(5);
Console.WriteLine(result); // dlrow...
Обмеження та Підводні Камені
- Отримати доступ до
privateабоprotectedчленів класу - Перевизначити існуючі методи класу
- Виступати як віртуальні методи (polymorphism)
public class MyString
{
public string Value { get; set; }
// Метод екземпляра
public void Print()
{
Console.WriteLine($"Instance: {Value}");
}
}
public static class MyStringExtensions
{
// Extension method з тією ж назвою
public static void Print(this MyString str)
{
Console.WriteLine($"Extension: {str.Value}");
}
}
var obj = new MyString { Value = "test" };
obj.Print(); // Instance: test ← Метод екземпляра має пріоритет!
using MyExtensions; // Без цього extension method не буде видний!
C# 14: Extensions / Roles (Preview)
C# 14 впроваджує нову, потужнішу синтаксичну модель для розширень під назвою Extension Members або Roles.
Нові Можливості
З C# 14 ви зможете створювати:
- Extension Properties (властивості-розширення)
- Extension Static Members (статичні розширення)
- Extension Operators (оператори-розширення)
Новий Синтаксис: extension блок
public static class PointExtensions
{
public static double DistanceFromOrigin(this Point point)
{
return Math.Sqrt(point.X * point.X + point.Y * point.Y);
}
}
public extension PointExtension for Point
{
// Extension property!
public double DistanceFromOrigin
{
get => Math.Sqrt(this.X * this.X + this.Y * this.Y);
}
// Extension static property!
public static Point Origin => new Point(0, 0);
// Extension method (як і раніше)
public Point MoveTo(double x, double y)
{
return new Point(x, y);
}
}
Extension Properties (Властивості)
public struct Point
{
public double X { get; set; }
public double Y { get; set; }
}
public extension PointExtensions for Point
{
// Read-only extension property
public double Magnitude
{
get => Math.Sqrt(X * X + Y * Y);
}
// Property з get та set
public bool IsAtOrigin
{
get => X == 0 && Y == 0;
set
{
if (value)
{
X = 0;
Y = 0;
}
}
}
}
// Використання
Point p = new Point { X = 3, Y = 4 };
Console.WriteLine(p.Magnitude); // 5
p.IsAtOrigin = true;
Console.WriteLine(p); // {0, 0}
Extension Static Members
public extension PointExtensions for Point
{
// Static extension property
public static Point Zero => new Point(0, 0);
public static Point UnitX => new Point(1, 0);
public static Point UnitY => new Point(0, 1);
// Static extension method
public static Point Parse(string input)
{
var parts = input.Split(',');
return new Point
{
X = double.Parse(parts[0]),
Y = double.Parse(parts[1])
};
}
}
// Використання як статичні члени Point!
Point origin = Point.Zero;
Point unit = Point.UnitX;
Point parsed = Point.Parse("3.5, 4.2");
Порівняльна Таблиця
| Можливість | Classic Extensions (C# 3.0) | Extension Members (C# 14) |
|---|---|---|
| Extension Methods | ✅ | ✅ |
| Extension Properties | ❌ | ✅ |
| Extension Static Members | ❌ | ✅ |
| Extension Operators | ❌ | ✅ (заплановано) |
| Синтаксис | static class + this | extension блок |
Доступ до this | Через параметр | ✅ Природно |
Best Practices
- Додаєте utility методи до типів, які не контролюєте
- Створюєте fluent API (ланцюжки викликів)
- Реалізуєте Domain-Specific Language (DSL)
- Розширюєте інтерфейси (LINQ)
- Можете додати метод безпосередньо в клас
- Потрібен доступ до
privateчленів - Extension method дублює існуючий функціонал
Partial Classes, Methods, Properties
Partial Classes (Часткові Класи)
Partial Classes дозволяють розбити визначення одного класу на кілька файлів.
Навіщо це потрібно?
Use Cases:
- Generated Code: Відокремлення згенерованого коду від написаного розробником
- Team Collaboration: Декілька розробників працюють над одним великим класом
- Code Organization: Логічне групування функціоналу всередині одного типу
Синтаксис
public partial class Customer
{
// Properties
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public partial class Customer
{
// Validation methods
public bool IsValidEmail()
{
return Email.Contains('@');
}
public bool IsValidName()
{
return !string.IsNullOrWhiteSpace(Name);
}
}
public partial class Customer
{
// Database methods
public void Save()
{
// Logic to save to database
}
public static Customer Load(int id)
{
// Logic to load from database
return new Customer();
}
}
Компіляція
Теоретичний аспект: Розділення відповідальностей (Separation of Concerns)
Partial класи та методи є потужним інструментом для реалізації принципу розділення відповідальностей. Вони дозволяють логічно (і фізично) відокремити різні аспекти одного класу.
Найважливіший сценарій — це співпраця між людиною та генератором коду.
- Машина (Генератор коду): відповідає за монотонний, шаблонний код (boilerplate), який може часто змінюватися (наприклад, властивості, що відповідають стовпцям у базі даних). Цей код знаходиться в одному файлі (
*.designer.cs,*.g.cs). - Людина (Розробник): відповідає за унікальну бізнес-логіку, валідацію, кастомні методи. Цей код знаходиться в іншому файлі.
Такий підхід дозволяє генератору коду безпечно перезаписувати свій файл при кожній зміні, не ризикуючи затерти логіку, написану розробником. Partial методи діють як чітко визначений "контракт" або "API" між згенерованим кодом та кодом розробника, дозволяючи першому надавати "гачки" (hooks), які другий може реалізувати.
На етапі компіляції всі частини об'єднуються в один клас:
Правила для Partial Classes
- Всі частини мають мати ключове слово
partial - Всі частини мають бути в одному namespace та assembly
- Всі частини мають мати однаковий access modifier
- Якщо хоча б одна частина
abstractабоsealed, весь клас є таким
Partial Methods (Часткові Методи)
Partial Methods дозволяють розділити оголошення методу (declaration) та його реалізацію (implementation).
Use Case: Generated Code Hooks
Генератори коду (наприклад, Entity Framework, LINQ to SQL) створюють partial methods як "хуки" для додавання custom логіки.
Синтаксис
public partial class Product
{
private string _name;
public string Name
{
get => _name;
set
{
OnNameChanging(value); // Hook викликається
_name = value;
OnNameChanged(); // Hook викликається
}
}
// Partial method declarations (оголошення)
partial void OnNameChanging(string newName);
partial void OnNameChanged();
}
public partial class Product
{
// Partial method implementations (реалізація)
partial void OnNameChanging(string newName)
{
if (string.IsNullOrWhiteSpace(newName))
throw new ArgumentException("Name cannot be empty");
}
partial void OnNameChanged()
{
Console.WriteLine($"Product name changed to: {_name}");
}
}
Що відбувається?
- Якщо реалізація є, метод викликається
- Якщо реалізації немає, компілятор повністю видаляє виклик (zero overhead!)
Partial Methods з Return Types (C# 9+)
Починаючи з C# 9, partial methods можуть мати:
- Тип повернення (не тільки
void) - Access modifiers (
public,private, тощо) outпараметри
public partial class Calculator
{
// Оголошення з return type - реалізація обов'язкова!
public partial int Calculate(int a, int b);
}
public partial class Calculator
{
// Реалізація
public partial int Calculate(int a, int b)
{
return a + b;
}
}
Partial Properties ::badge
C# 13 ::
C# 13 додає можливість розділити деклар��цію та реалізацію властивостей.
Синтаксис
public partial class Person
{
// Declaring declaration
public partial string Name { get; set; }
}
public partial class Person
{
// Implementation declaration
private string _name = string.Empty;
public partial string Name
{
get => _name;
set => _name = value ?? throw new ArgumentNullException(nameof(value));
}
}
Переваги:
- Згенерований код може оголосити властивість
- Розробник реалізує custom логіку (validation, change tracking)
Best Practices
- Для відокремлення generated коду від custom
- Для великих класів (логічне розбиття)
- У генераторах коду
- Для hooks у generated коді
- Для optional розширювальної логіки
- Для контролю над властивостями з generated classes (C# 13+)
- Розбиття малих класів (overengineering)
- Використання замість proper composition
Interceptors (Перехоплювачі) ::badge
Experimental ::
Що таке Inter ceptors?
Interceptors — це механізм, що дозволяє на етапі компіляції замінити виклик одного методу викликом іншого.
Ключові особливості:
- Працює на рівні компілятора
- Використовується переважно source generators
- Дозволяє реалізувати AOP (Aspect-Oriented Programming) паттерни
Налаштування
Для використання interceptors потрібно:
- Активувати в
.csproj:
<PropertyGroup>
<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);MyApp.Generated</InterceptorsPreviewNamespaces>
### Теоретичний аспект: Аспектно-орієнтоване програмування (АОП)
Interceptors є кроком C# у бік **Аспектно-орієнтованого програмування (АОП)**. АОП — це парадигма програмування, яка дозволяє виділити та реалізувати **наскрізну функціональність (cross-cutting concerns)**.
**Що таке наскрізна функціональність?**
Це логіка, яка "розрізає" багато частин вашої програми, але не є частиною основної бізнес-логіки. Приклади:
- **Логування**: запис інформації про виклик методів.
- **Кешування**: збереження результатів дорогих операцій.
- **Безпека**: перевірка прав доступу перед виконанням методу.
- **Транзакції**: керування транзакціями бази даних.
**Як Interceptors реалізують АОП?**
Традиційно, для реалізації АОП використовували складні техніки, такі як динамічні проксі (runtime) або пост-компіляційна обробка IL-коду. Interceptors пропонують новий підхід:
- **Compile-time weaving**: "Вплетення" аспектів (логіки перехоплення) відбувається під час компіляції.
- **Без рефлексії**: На відміну від багатьох АОП-фреймворків, interceptors не використовують рефлексію, що робить їх надзвичайно швидкими.
- **Керування генераторами коду**: Вони розроблені для використання Source Generators, що дозволяє створювати потужні, автоматизовані та типізовані аспекти.
Таким чином, interceptors, хоч і експериментальні, відкривають двері для створення чистіших, більш декларативних та продуктивних архітектур, де бізнес-логіка залишається відокремленою від технічних аспектів.
</PropertyGroup>
- Створити interceptor метод:
using System.Runtime.CompilerServices;
namespace MyApp.Generated;
public static class CalculatorInterceptors
### Теоретичний аспект: Метапрограмування та Compile-Time магія
Source Generators є потужною формою **метапрограмування** — написання коду, який пише або маніпулює іншим кодом.
Традиційно, багато завдань, що вимагали метапрограмування в .NET, вирішувалися за допомогою **рефлексії (Reflection)** під час виконання (runtime). Рефлексія дозволяє аналізувати та викликати код динамічно, але має суттєві недоліки:
- **Низька продуктивність**: аналіз метаданих та динамічні виклики є повільними.
- **Відсутність безпеки типів**: помилки виявляються лише під час виконання.
- **Складність для AOT-компіляції**: код, що використовує рефлексію, важко або неможливо компілювати в нативний код (Ahead-of-Time).
**Source Generators переносять метапрограмування з runtime у compile-time**:
- **Максимальна продуктивність**: згенерований код є звичайним C# кодом, який компілюється напряму в IL. Немає жодних накладних витрат під час виконання.
- **Повна безпека типів**: компілятор перевіряє згенерований код так само, як і написаний вручну. Будь-які помилки виявляються на етапі компіляції.
- **Дружність до AOT**: оскільки вся "магія" відбувається до етапу виконання, згенерований код ідеально підходить для AOT-сценаріїв (мобільні додатки, Blazor WebAssembly).
Таким чином, Source Generators є еволюційним кроком, що дозволяє вирішувати ті ж самі проблеми, що й рефлексія, але у більш безпечний, продуктивний та сучасний спосіб.
{
[InterceptsLocation("Program.cs", line: 10, column: 5)]
public static int InterceptAdd(this Calculator calc, int a, int b)
{
Console.WriteLine($"[INTERCEPTED] Add({a}, {b})");
return calc.Add(a, b); // Викликаємо оригінальний метод
}
}
// Program.cs (line 10, column 5)
Calculator calculator = new Calculator();
int result = calculator.Add(5, 10); // ← Цей виклик буде перехоплено!
// Output: [INTERCEPTED] Add(5, 10)
[InterceptsLocation] вимагає точних координат (файл, рядок, колонка) виклику методу. Це робить interceptors практично непридатними для ручного написання — вони призначені для source generators.Use Cases
- Logging/Tracing: Автоматичне логування викликів методів
- Performance Monitoring: Вимірювання часу виконання
- Mock Injection: Заміна викликів у тестах
- Compile-time AOP: Впровадження cross-cutting concerns
Обмеження
- Потребує exact source location (file, line, column)
- Працює тільки в .NET 8+
- Експериментальний статус
- Namespace має бути оголошений у
InterceptorsPreviewNamespaces
Source Generators (Генератори Вихідного Коду)
Навіщо це потрібно?
Уявіть, що вам потрібно створити 100 класів з однаковою структурою. Або реалізувати INotifyPropertyChanged для 50 властивостей. Source Generators дозволяють автоматизувати це!
Source Generators — це компоненти, які генерують C# код під час компіляції на основі аналізу вашого проєкту.
Переваги
✅ Zero Runtime Cost — код генерується на етапі компіляції
✅ Type-Safe — згенерований код компілюється разом з проєктом
✅ IntelliSense Support — IDE бачить згенерований код
✅ Performance — заміна Reflection на compile-time generation
Roslyn API: IIncrementalGenerator
Roslyn — це компілятор C#/.NET з відкритим кодом, який надає API для аналізу та генерації коду.
IIncrementalGenerator — це сучасний interface для source generators (замість застарілого ISourceGenerator).
Переваги Incremental Generators
- Кращий performance: Регенерує тільки змінені частини
- Pipeline-based: Функціональний підхід до трансформації коду
Приклад: Hello World Generator
Крок 1: Створіть проєкт генератора
dotnet new classlib -n HelloGenerator
cd HelloGenerator
dotnet add package Microsoft.CodeAnalysis.CSharp
dotnet add package Microsoft.CodeAnalysis.Analyzers
Крок 2: Реалізуйте IIncrementalGenerator
using Microsoft.CodeAnalysis;
[Generator]
public class HelloSourceGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Реєструємо post-initialization output
context.RegisterPostInitializationOutput(ctx =>
{
// Генеруємо простий клас
string source = @"
namespace Generated
{
public static class HelloWorld
{
public static void SayHello()
{
System.Console.WriteLine(""Hello from Source Generator!"");
}
}
}";
ctx.AddSource("HelloWorld.g.cs", source);
});
}
}
Крок 3: Налаштуйте .csproj генератора
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
</Project>
Крок 4: Використайте генератор у проєкті
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- Посилання на генератор як Analyzer -->
<ProjectReference Include="..\HelloGenerator\HelloGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
Крок 5: Використовуйте згенерований код
using Generated;
HelloWorld.SayHello();
// Output: Hello from Source Generator!
Складніший Приклад: AutoNotify Generator
Генератор, який автоматично реалізує INotifyPropertyChanged:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Text;
[Generator]
public class AutoNotifyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Знайти всі класи з атрибутом [AutoNotify]
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => IsSyntaxTargetForGeneration(s),
transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx))
.Where(static m => m is not null);
// Згенерувати код для кожного класу
context.RegisterSourceOutput(classDeclarations,
static (spc, source) => Execute(source, spc));
}
static bool IsSyntaxTargetForGeneration(SyntaxNode node)
{
return node is ClassDeclarationSyntax c &&
c.AttributeLists.Count > 0;
}
static ClassDeclarationSyntax? GetSemanticTargetForGeneration(
GeneratorSyntaxContext context)
{
var classDeclaration = (ClassDeclarationSyntax)context.Node;
foreach (var attributeList in classDeclaration.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
if (context.SemanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol attributeSymbol)
continue;
string attributeName = attributeSymbol.ContainingType.ToDisplayString();
if (attributeName == "AutoNotifyAttribute")
{
return classDeclaration;
}
}
}
return null;
}
static void Execute(ClassDeclarationSyntax? classDeclaration, SourceProductionContext context)
{
if (classDeclaration is null)
return;
string namespaceName = "Generated";
string className = classDeclaration.Identifier.Text;
var source = $@"
using System.ComponentModel;
namespace {namespaceName}
{{
public partial class {className} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}}
}}
}}";
context.AddSource($"{className}.g.cs", source);
}
}
Використання:
[AutoNotify]
public partial class Person
{
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged(nameof(Name)); // Метод згенеровано!
}
}
}
Best Practices
- Використовуйте
IIncrementalGeneratorзамістьISourceGenerator - Генеруйте
partialкласи для розширення користувацького коду - Додавайте
.g.csсуфікс до згенерованих файлів - Обробляйте помилки через
context.ReportDiagnostic - Тестуйте генератори за допомогою
Microsoft.CodeAnalysis.CSharp.Testing
- Генерації величезних файлів (розбивайте на частини)
- Складних обчислень у генераторах (performance!)
- Зміни існуючого коду (тільки додавання нового)
Підсумок
У цьому розділі ми розглянули п'ять потужних можливостей C#:
| Можливість | Призначення | Коли використовувати |
|---|---|---|
| Operator Overloading | Природний синтаксис для власних типів | Математичні типи, DSL |
| Extension Methods | Розширення типів без модифікації | Utility-методи, fluent APIs |
| Partial Types | Розділення класу на файли | Generated code, організація |
| Interceptors | Заміна викликів методів | AOP з source generators |
| Source Generators | Генерація коду під час компіляції | Boilerplate elimination, perf |
- Практикуйте operator overloading на власних типах
- Створіть корисні extension methods для ваших проєктів
- Експериментуйте з source generators для автоматизації рутинних задач
- Вивчайте Roslyn API для глибшого розуміння компілятора C#
Практичні Завдання
Початковий Рівень
Задача 1: Клас Fraction з операторами
Опис: Створіть клас Fraction (дріб) з перевантаженням операторів +, -, *, / та порівняння.
// Очікуване використання:
Fraction a = new Fraction(1, 2); // 1/2
Fraction b = new Fraction(1, 3); // 1/3
Fraction sum = a + b; // 5/6
Console.WriteLine(sum); // 5/6
public class Fraction : IEquatable<Fraction>
{
public int Numerator { get; }
public int Denominator { get; }
public Fraction(int numerator, int denominator)
{
if (denominator == 0)
throw new ArgumentException("Denominator cannot be zero");
// Спрощення дробу
int gcd = GCD(Math.Abs(numerator), Math.Abs(denominator));
Numerator = numerator / gcd;
Denominator = denominator / gcd;
// Знак завжди в чисельнику
if (Denominator < 0)
{
Numerator = -Numerator;
Denominator = -Denominator;
}
}
private static int GCD(int a, int b)
{
while (b != 0)
{
int temp = b;
b = a % b;
a = temp;
}
return a;
}
public static Fraction operator +(Fraction a, Fraction b)
{
return new Fraction(
a.Numerator * b.Denominator + b.Numerator * a.Denominator,
a.Denominator * b.Denominator);
}
public static Fraction operator -(Fraction a, Fraction b)
{
return new Fraction(
a.Numerator * b.Denominator - b.Numerator * a.Denominator,
a.Denominator * b.Denominator);
}
public static Fraction operator *(Fraction a, Fraction b)
{
return new Fraction(
a.Numerator * b.Numerator,
a.Denominator * b.Denominator);
}
public static Fraction operator /(Fraction a, Fraction b)
{
return new Fraction(
a.Numerator * b.Denominator,
a.Denominator * b.Numerator);
}
public static bool operator ==(Fraction? a, Fraction? b)
{
if (a is null) return b is null;
return a.Equals(b);
}
public static bool operator !=(Fraction? a, Fraction? b) => !(a == b);
public override bool Equals(object? obj) => Equals(obj as Fraction);
public bool Equals(Fraction? other)
{
if (other is null) return false;
return Numerator == other.Numerator && Denominator == other.Denominator;
}
public override int GetHashCode() => HashCode.Combine(Numerator, Denominator);
public override string ToString() => $"{Numerator}/{Denominator}";
}
Задача 2: Extension Methods для String
Опис: Створіть extension methods для роботи з українським текстом:
CountWords()— підрахунок кількості слівCapitalize()— перша літера кожного слова великаRemoveExtraSpaces()— видалення зайвих пробілів
public static class UkrainianStringExtensions
{
public static int CountWords(this string text)
{
if (string.IsNullOrWhiteSpace(text))
return 0;
return text.Split(new[] { ' ', '\t', '\n', '\r' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
public static string Capitalize(this string text)
{
if (string.IsNullOrWhiteSpace(text))
return text;
var words = text.Split(' ');
for (int i = 0; i < words.Length; i++)
{
if (words[i].Length > 0)
{
words[i] = char.ToUpper(words[i][0]) + words[i].Substring(1).ToLower();
}
}
return string.Join(' ', words);
}
public static string RemoveExtraSpaces(this string text)
{
if (string.IsNullOrWhiteSpace(text))
return string.Empty;
return string.Join(' ',
text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
}
}
// Використання
string text = " привіт світ ";
Console.WriteLine(text.CountWords()); // 2
Console.WriteLine(text.Capitalize()); // Привіт Світ
Console.WriteLine(text.RemoveExtraSpaces()); // "привіт світ"
Середній Рівень
Задача 3: Complex Number Library
Опис: Створіть повнофункціональну бібліотеку для роботи з комплексними числами з:
- Операторами (
+,-,*,/) - Extension methods (
Magnitude(),Phase(),Conjugate()) - Implicit/Explicit conversions
Complex з перевантаженими операторами, extension methods для обчислення величини та фази, оператори конверсії з/до double)Задача 4: C# 14 Extension Library
Опис: Створіть бібліотеку extension members (C# 14) для DateTime:
- Extension property
IsWeekend - Extension static property
EpochStart - Extension method
AddBusinessDays(int days)
Примітка: Потребує C# 14 Preview
public extension DateTimeExtensions for DateTime
{
// Extension property
public bool IsWeekend
{
get => this.DayOfWeek == DayOfWeek.Saturday ||
this.DayOfWeek == DayOfWeek.Sunday;
}
// Extension static property
public static DateTime EpochStart => new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// Extension method
public DateTime AddBusinessDays(int days)
{
var result = this;
var added = 0;
var increment = days > 0 ? 1 : -1;
while (added != days)
{
result = result.AddDays(increment);
if (!result.IsWeekend)
{
added += increment;
}
}
return result;
}
}
Високий Рівень
Задача 5: INotifyPropertyChanged Generator
Опис: Створіть source generator, який автоматично генерує реалізацію INotifyPropertyChanged для класів з атрибутом [AutoNotify].
Вимоги:
- Генерація
OnPropertyChangedметоду - Підтримка властивостей з
[Notify]атрибутом - Згенерований код має бути у
partialкласі
Базуйтеся на прикладі AutoNotifyGenerator з розділу Source Generators вище. Розширте функціонал для:
- Аналізу атрибутів
[Notify]на властивостях - Генерації backing fields
- Автоматичного виклику
OnPropertyChangedу setters - Підтримки колекцій та nested properties
Задача 6: Logging Interceptor + Generator
Опис: Створіть source generator, який:
- Знаходить методи з атрибутом
[LogMethod] - Генерує interceptor для логування
- Автоматично додає
[InterceptsLocation]атрибути
Складність: Requires deep Roslyn API knowledge
Ключові кроки:
- Syntax Analysis: Використайте
ISyntaxReceiverдля знаходження методів з[LogMethod] - Interceptor Generation: Згенеруйте метод з
[InterceptsLocation]для кожного виклику - Location Tracking: Використайте
SyntaxTree.GetLocation()для точних координат - Logging Logic: Додайте код логування до interceptor методу
- MSBuild Integration: Налаштуйте
InterceptorsPreviewNamespaces
Приклад структури interceptor:
[InterceptsLocation("file.cs", line: 10, column: 5)]
public static void LoggedMethod(params)
{
Console.WriteLine($"[LOG] Method called at {DateTime.Now}");
// Original method call
}
Pattern Matching
Вивчіть Pattern Matching у C# - потужний механізм для виразної та безпечної роботи з даними через перевірку структур та вилучення значень.
Software Design Principles (Частина 1)
Глибоке вивчення фундаментальних принципів проектування програмного забезпечення в C#: вступ, Single Responsibility Principle (SRP) та Open/Closed Principle (OCP)