C# постійно еволюціонує, пропонуючи розробникам потужні механізми для написання виразного, підтримуваного та ефективного коду. У цьому розділі ми розглянемо п'ять ключових можливостей мови:
| Можливість | Версія 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 |
Уявіть, що ви створюєте математичну бібліотеку для роботи з векторами. Без перевантаження операторів ваш код виглядатиме так:
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 методиoperatorpublic static ReturnType operator SymbolOperator(ParameterType parameter)
{
// Реалізація
}
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
Розбір коду:
- інвертує знаки компонентів вектора+ додає відповідні компоненти* має дві різні версії з різними типами повернення:Vector * double → повертає VectorVector * Vector → повертає doublepublic 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: Коли можлива втрата даних або потрібна явна згода розробникаХоча індексатори технічно не є операторами, вони перевантажують синтаксис [] і дуже корисні для колекцій.
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
Переваги індексаторів:
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++) форми. Компілятор автоматично обробляє обидві форми на основі одного перевантаженого оператора.Дозволяють використовувати власні типи в умовних виразах (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 (&& і ||) для власних типів![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
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 | ✅ Так | Немає |
Complex + Complex)Уявіть, що ви працюєте з класом 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 вирішують цю проблему!
Extension methods дозволяють додавати методи до існуючих типів без:
Синтаксис:
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
Ключові моменти:
staticstatic класіthisКомпілятор перетворює виклик extension method на виклик статичного методу:
### Архітектурний аспект: Принцип Відкритості/Закритості
Методи-розширення є чудовою реалізацією **Принципу Відкритості/Закритості (Open/Closed Principle)** з SOLID. Цей принцип говорить:
> Програмні сутності (класи, модулі, функції) мають бути **відкритими для розширення**, але **закритими для модифікації**.
- **Закриті для модифікації**: Ви не можете (і не повинні) змінювати вихідний код `string` або `IEnumerable<T>`. Ці типи є стабільними та надійними.
- **Відкриті для розширення**: За допомогою extension methods ви можете додавати нову функціональність до цих типів, не втручаючись у їхню внутрішню реалізацію.
Таким чином, методи-розширення дозволяють адаптувати та розширювати існуючі, навіть фундаментальні, типи під потреби вашого домену, не порушуючи їх цілісність. Це особливо важливо при роботі з бібліотеками та фреймворками сторонніх розробників. LINQ є найяскравішим прикладом цієї ідеї: вся його функціональність побудована на методах-розширеннях для інтерфейсу `IEnumerable<T>`.
// Ви пишете:
email.IsValidEmail();
// Компілятор перетворює на:
StringExtensions.IsValidEmail(email);
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
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
Extension methods чудово працюють у ланцюжках:
string result = " Hello World "
.Trim()
.ToLower()
.Reverse()
.Truncate(5);
Console.WriteLine(result); // dlrow...
private або protected членів класу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 впроваджує нову, потужнішу синтаксичну модель для розширень під назвою Extension Members або Roles.
З C# 14 ви зможете створювати:
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);
}
}
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}
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 | Через параметр | ✅ Природно |
private членівPartial Classes дозволяють розбити визначення одного класу на кілька файлів.
Use Cases:
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();
}
}
Partial класи та методи є потужним інструментом для реалізації принципу розділення відповідальностей. Вони дозволяють логічно (і фізично) відокремити різні аспекти одного класу.
Найважливіший сценарій — це співпраця між людиною та генератором коду.
*.designer.cs, *.g.cs).Такий підхід дозволяє генератору коду безпечно перезаписувати свій файл при кожній зміні, не ризикуючи затерти логіку, написану розробником. Partial методи діють як чітко визначений "контракт" або "API" між згенерованим кодом та кодом розробника, дозволяючи першому надавати "гачки" (hooks), які другий може реалізувати.
На етапі компіляції всі частини об'єднуються в один клас:
partialabstract або sealed, весь клас є такимPartial Methods дозволяють розділити оголошення методу (declaration) та його реалізацію (implementation).
Генератори коду (наприклад, 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}");
}
}
Що відбувається?
Починаючи з C# 9, partial methods можуть мати:
void)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;
}
}
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));
}
}
Переваги:
Experimental ::
Interceptors — це механізм, що дозволяє на етапі компіляції замінити виклик одного методу викликом іншого.
Ключові особливості:
Для використання 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>
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.InterceptorsPreviewNamespacesУявіть, що вам потрібно створити 100 класів з однаковою структурою. Або реалізувати INotifyPropertyChanged для 50 властивостей. Source Generators дозволяють автоматизувати це!
Source Generators — це компоненти, які генерують C# код під час компіляції на основі аналізу вашого проєкту.
✅ Zero Runtime Cost — код генерується на етапі компіляції
✅ Type-Safe — згенерований код компілюється разом з проєктом
✅ IntelliSense Support — IDE бачить згенерований код
✅ Performance — заміна Reflection на compile-time generation
Roslyn — це компілятор C#/.NET з відкритим кодом, який надає API для аналізу та генерації коду.
IIncrementalGenerator — це сучасний interface для source generators (замість застарілого ISourceGenerator).
dotnet new classlib -n HelloGenerator
cd HelloGenerator
dotnet add package Microsoft.CodeAnalysis.CSharp
dotnet add package Microsoft.CodeAnalysis.Analyzers
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);
});
}
}
.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>
<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>
using Generated;
HelloWorld.SayHello();
// Output: Hello from Source 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)); // Метод згенеровано!
}
}
}
IIncrementalGenerator замість ISourceGeneratorpartial класи для розширення користувацького коду.g.cs суфікс до згенерованих файлівcontext.ReportDiagnosticMicrosoft.CodeAnalysis.CSharp.TestingУ цьому розділі ми розглянули п'ять потужних можливостей C#:
| Можливість | Призначення | Коли використовувати |
|---|---|---|
| Operator Overloading | Природний синтаксис для власних типів | Математичні типи, DSL |
| Extension Methods | Розширення типів без модифікації | Utility-методи, fluent APIs |
| Partial Types | Розділення класу на файли | Generated code, організація |
| Interceptors | Заміна викликів методів | AOP з source generators |
| Source Generators | Генерація коду під час компіляції | Boilerplate elimination, perf |
Опис: Створіть клас 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}";
}
Опис: Створіть 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()); // "привіт світ"
Опис: Створіть повнофункціональну бібліотеку для роботи з комплексними числами з:
+, -, *, /)Magnitude(), Phase(), Conjugate())Complex з перевантаженими операторами, extension methods для обчислення величини та фази, оператори конверсії з/до double)Опис: Створіть бібліотеку extension members (C# 14) для DateTime:
IsWeekendEpochStartAddBusinessDays(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;
}
}
Опис: Створіть source generator, який автоматично генерує реалізацію INotifyPropertyChanged для класів з атрибутом [AutoNotify].
Вимоги:
OnPropertyChanged методу[Notify] атрибутомpartial класіБазуйтеся на прикладі AutoNotifyGenerator з розділу Source Generators вище. Розширте функціонал для:
[Notify] на властивостяхOnPropertyChanged у settersОпис: Створіть source generator, який:
[LogMethod][InterceptsLocation] атрибутиСкладність: Requires deep Roslyn API knowledge
Ключові кроки:
ISyntaxReceiver для знаходження методів з [LogMethod][InterceptsLocation] для кожного викликуSyntaxTree.GetLocation() для точних координат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)