Уявіть, що ви розробляєте систему для управління геометричними фігурами. Вам потрібно зберігати координати точок, кольори, стани об'єктів та багато інших даних. Який тип використати для кожного випадку? Клас? Структуру? Запис? Вибір правильного типу даних впливає не лише на читабельність коду, але й на продуктивність, споживання пам'яті та загальну архітектуру застосунку.
C# пропонує багатий набір розширених типів даних, кожен з яких оптимізований для конкретних сценаріїв використання. У цьому розділі ми детально розглянемо структури (structs), перерахування (enums), записи (records), кортежі (tuples), анонімні типи (anonymous types), nullable типи та концепцію discriminated unions.
Struct (структура) — це value type (тип-значення), який зберігається на стеку (stack) і передається за значенням. На відміну від класів, які є reference types (типами-посиланнями) і зберігаються в купі (heap), структури оптимізовані для невеликих, легких об'єктів.
Ключова перевага структур у продуктивності полягає у відсутності накладних витрат, пов'язаних із збирачем сміття (Garbage Collector, GC). Оскільки структури зазвичай розміщуються на стеку, пам'ять для них звільняється автоматично при виході зі своєї області видимості. Об'єкти класів, що розміщуються в купі, потребують, щоб GC періодично сканував пам'ять і звільняв її, що може спричиняти короткочасні "паузи" у виконанні програми. Для великої кількості короткоживучих об'єктів використання структур може значно зменшити тиск на GC.
| Критерій | Struct | Class |
|---|---|---|
| Тип | Value Type | Reference Type |
| Розміщення в пам'яті | Stack (зазвичай) | Heap |
| Передача параметрів | За значенням (копія) | За посиланням |
| Наслідування | Неможливе | Можливе |
null за замовчуванням | Не може бути null (крім Nullable<T>) | Може бути null |
| Конструктор без параметрів | Завжди є (автоматичний) | Потребує оголошення |
| Продуктивність | Краща для малих даних | Краща для великих об'єктів |
public struct Point
{
public double X { get; set; }
public double Y { get; set; }
public Point(double x, double y)
{
X = x;
Y = y;
}
public double DistanceFromOrigin()
{
return Math.Sqrt(X * X + Y * Y);
}
public override string ToString()
{
return $"({X}, {Y})";
}
}
// Використання
Point p1 = new Point(3, 4);
Point p2 = p1; // Копія значення, а не посилання!
p2.X = 10;
Console.WriteLine(p1); // (3, 4) - p1 не змінився
Console.WriteLine(p2); // (10, 4)
Використовуйте struct, якщо:
Приклади з .NET BCL: DateTime, TimeSpan, Guid, Int32, Double
// Гарний приклад використання struct - невеликий immutable тип
public readonly struct Color
{
public byte R { get; }
public byte G { get; }
public byte B { get; }
public Color(byte r, byte g, byte b)
{
R = r;
G = g;
B = b;
}
}
// Поганий приклад - занадто великий для struct
public struct HugeData // ❌ Краще використати class
{
public double[] Data { get; set; } // Масив з 1000 елементів
public string Description { get; set; }
// ... багато полів
}
ref struct — це спеціальний вид структури, який обов'язково має зберігатися на стеку і не може потрапити в купу. Це використовується для високопродуктивних сценаріїв, наприклад, Span<T>.
public ref struct SpanExample
{
private Span<int> _numbers;
public SpanExample(Span<int> numbers)
{
_numbers = numbers;
}
// ref struct має суворі обмеження:
// ❌ Не може бути полем класу
// ❌ Не може реалізовувати інтерфейси
// ❌ Не може бути boxing (перетворення в object)
// ❌ Не може бути типом параметра async методу
}
Span<T>, ReadOnlySpan<T>, Utf8JsonReader.
Enum (enumeration, перерахування) — це value type, який визначає набір іменованих констант. Використовується для представлення фіксованого набору можливих значень.
Головна перевага enum полягає у вирішенні проблеми "магічних чисел" (magic numbers). Замість використання в коді незрозумілих числових значень (наприклад, if (status == 2)), ви використовуєте іменовані константи (if (status == OrderStatus.Shipped)), що значно покращує читабельність та надійність коду. Якщо в майбутньому значення зміняться, вам потрібно буде оновити їх лише в одному місці — в оголошенні enum.
public enum OrderStatus
{
Pending, // 0 (за замовчуванням)
Processing, // 1
Shipped, // 2
Delivered, // 3
Cancelled // 4
}
// Використання
OrderStatus status = OrderStatus.Pending;
if (status == OrderStatus.Delivered)
{
Console.WriteLine("Замовлення доставлено!");
}
// Конвертація в int
int statusCode = (int)status; // 0
// Конвертація з int
OrderStatus newStatus = (OrderStatus)2; // Shipped
За замовчуванням enum базується на типі int, але можна вказати інший цілочисловий тип (byte, sbyte, short, ushort, int, uint, long, ulong).
// Використання ushort для економії пам'яті
public enum ErrorCode : ushort
{
None = 0,
NotFound = 404,
Unauthorized = 401,
InternalServerError = 500
}
// Великі значення - використання long
public enum LargeValues : long
{
SmallValue = 1,
HugeValue = 9_223_372_036_854_775_807 // long.MaxValue
}
[Flags] attribute дозволяє комбінувати значення enum через бітові операції. Значення повинні бути степенями двійки (1, 2, 4, 8, 16...).
[Flags]
public enum FilePermissions
{
None = 0, // 0000
Read = 1, // 0001
Write = 2, // 0010
Execute = 4, // 0100
Delete = 8, // 1000
ReadWrite = Read | Write, // 0011 (3)
FullControl = Read | Write | Execute | Delete // 1111 (15)
}
// Використання
FilePermissions userPermissions = FilePermissions.Read | FilePermissions.Write;
// Перевірка наявності прапорця
if (userPermissions.HasFlag(FilePermissions.Read))
{
Console.WriteLine("Користувач може читати файл");
}
// Додавання прапорця
userPermissions |= FilePermissions.Execute;
// Видалення прапорця
userPermissions &= ~FilePermissions.Write;
// Перевірка через бітову операцію (швидше)
bool canWrite = (userPermissions & FilePermissions.Write) == FilePermissions.Write;
[Flags] attribute та степені двійки. Це забезпечує коректну роботу методу ToString() та інших операцій.// Parse з string
OrderStatus status = Enum.Parse<OrderStatus>("Shipped");
// TryParse (безпечніший варіант)
if (Enum.TryParse<OrderStatus>("Pending", out var result))
{
Console.WriteLine($"Parsed: {result}");
}
// Отримання всіх значень
foreach (OrderStatus s in Enum.GetValues<OrderStatus>())
{
Console.WriteLine($"{s} = {(int)s}");
}
// Отримання всіх імен
string[] names = Enum.GetNames<OrderStatus>();
FilePermissions perms = FilePermissions.Read | FilePermissions.Write;
// З [Flags] attribute
Console.WriteLine(perms.ToString());
// Output: "Read, Write"
// Без [Flags] attribute
Console.WriteLine(perms.ToString());
// Output: "3" (просто число)

Record (запис) — це reference type (або value type для record struct), оптимізований для незмінних (immutable) даних з вбудованою value equality (рівністю за значенням). Records автоматично генерують методи Equals(), GetHashCode(), та ToString().
Введення записів у C# 9 є значним кроком у напрямку підтримки функціонального стилю програмування. Основна ідея полягає в тому, щоб працювати з даними як з незмінними сутностями. Замість того, щоб змінювати стан існуючого об'єкта (що може призвести до непередбачуваних побічних ефектів), ви створюєте новий об'єкт з оновленими даними. Цей підхід, відомий як "non-destructive mutation", робить код більш передбачуваним, легшим для тестування та безпечнішим для використання у багатопотокових середовищах.
// Positional syntax (компактний синтаксис)
public record Person(string FirstName, string LastName, int Age);
// Еквівалентно:
public record Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public int Age { get; init; }
public Person(string firstName, string lastName, int age)
{
FirstName = firstName;
LastName = lastName;
Age = age;
}
}
// Використання
Person person1 = new("John", "Doe", 30);
Person person2 = new("John", "Doe", 30);
Console.WriteLine(person1 == person2); // True - value equality!
with expression дозволяє створити копію record з зміненими певними властивостями, не змінюючи оригінал.
public record Person(string FirstName, string LastName, int Age);
Person person = new("John", "Doe", 30);
// Створення копії зі зміненим віком
Person olderPerson = person with { Age = 31 };
Console.WriteLine(person); // Person { FirstName = John, LastName = Doe, Age = 30 }
Console.WriteLine(olderPerson); // Person { FirstName = John, LastName = Doe, Age = 31 }
// person залишився незмінним!
// Можна змінювати кілька властивостей
Person different = person with
{
FirstName = "Jane",
Age = 25
};
// Record class (reference type) - за замовчуванням
public record Person(string Name, int Age);
Person p1 = new("Alice", 25);
Person p2 = p1; // Обидві змінні вказують на той самий об'єкт
// Але через immutability це не проблема
// Record struct (value type)
public record struct Point(double X, double Y);
Point p1 = new(3, 4);
Point p2 = p1; // Копія значення
p2 = p2 with { X = 10 };
Console.WriteLine(p1); // Point { X = 3, Y = 4 }
Console.WriteLine(p2); // Point { X = 10, Y = 4 }
| Критерій | Record Class | Record Struct | Class | Struct |
|---|---|---|---|---|
| Тип | Reference | Value | Reference | Value |
| Equality | Value Equality | Value Equality | Reference Equality | Value Equality |
| Mutability | Immutable (recommended) | Immutable (recommended) | Mutable | Mutable/Immutable |
with expression | ✅ | ✅ | ❌ | ❌ |
| Inheritance | ✅ | ❌ | ✅ | ❌ |
| Performance | Heap allocation | Stack allocation | Heap allocation | Stack allocation |

Tuple (кортеж) дозволяє групувати кілька значень в один об'єкт без створення окремого типу. Сучасний ValueTuple (C# 7+) є value type і має зручний синтаксис.
До появи ValueTuple в C# 7.0, для повернення кількох значень з методу часто використовували out параметри. Цей підхід був громіздким і менш читабельним. Кортежі пропонують значно елегантніше рішення, дозволяючи методу повернути єдиний об'єкт-кортеж, що містить усі необхідні значення. Це покращує сигнатуру методу та робить його використання більш інтуїтивним.
// Оголошення tuple
(string Name, int Age) person = ("Alice", 25);
// Доступ до елементів
Console.WriteLine(person.Name); // Alice
Console.WriteLine(person.Age); // 25
// Без імен (доступ через Item1, Item2...)
(string, int) unnamed = ("Bob", 30);
Console.WriteLine(unnamed.Item1); // Bob
// Tuple literal
var point = (X: 3.0, Y: 4.0);
Tuples ідеально підходять для повернення декількох значень з методу без створення спеціального класу.
public (int Min, int Max, double Average) AnalyzeNumbers(int[] numbers)
{
if (numbers.Length == 0)
{
return (0, 0, 0);
}
int min = numbers.Min();
int max = numbers.Max();
double avg = numbers.Average();
return (min, max, avg);
}
// Використання
int[] data = { 5, 2, 8, 1, 9 };
var result = AnalyzeNumbers(data);
Console.WriteLine($"Min: {result.Min}"); // 1
Console.WriteLine($"Max: {result.Max}"); // 9
Console.WriteLine($"Avg: {result.Average}"); // 5
Deconstruction дозволяє розпакувати tuple в окремі змінні.
// Повний приклад deconstructon
(string Name, int Age) GetPerson() => ("Alice", 25);
// Deconstruct в нові змінні
var (name, age) = GetPerson();
Console.WriteLine($"{name} is {age} years old");
// Deconstruct в існуючі змінні
string personName;
int personAge;
(personName, personAge) = GetPerson();
// Використання discard (_) для ігнорування значень
var (_, onlyAge) = GetPerson(); // Ігноруємо ім'я
Console.WriteLine($"Age: {onlyAge}");
// Deconstruction в foreach
var people = new[]
{
("Alice", 25),
("Bob", 30)
};
foreach (var (n, a) in people)
{
Console.WriteLine($"{n}: {a}");
}
// ✅ ValueTuple (C# 7+) - Рекомендується
(int X, int Y) point = (3, 4);
Console.WriteLine(point.X);
// Value type (struct)
// Зручний синтаксис
// Named elements
// ❌ Старий Tuple - Не використовуйте!
Tuple<int, int> point = Tuple.Create(3, 4);
Console.WriteLine(point.Item1);
// Reference type (class)
// Незручний синтаксис
// Тільки Item1, Item2...
Anonymous type (анонімний тип) — це reference type без явного оголошення класу. Компілятор автоматично генерує клас з read-only властивостями.
Коли ви створюєте анонімний тип, компілятор C# "за лаштунками" генерує звичайний клас з унікальним, недоступним для вас іменем (наприклад, <>f__AnonymousType0\2). Цей клас має публічні readonly` властивості, імена яких виводяться з ініціалізаторів. Оскільки ім'я типу генерується компілятором і невідоме на етапі написання коду, ви не можете використовувати його в сигнатурах методів (наприклад, як тип, що повертається), що обмежує їх використання межами одного методу.
// Створення анонімного типу
var person = new
{
Name = "Alice",
Age = 25,
City = "Kyiv"
};
Console.WriteLine(person.Name); // Alice
Console.WriteLine(person.Age); // 25
// Тип відомий тільки компілятору
// person.Name = "Bob"; // ❌ Помилка - read-only
Анонімні типи найчастіше використовуються в LINQ запитах для проекції (projection) даних.
public record Product(string Name, decimal Price, string Category);
List<Product> products = new()
{
new("Laptop", 1200m, "Electronics"),
new("Mouse", 25m, "Electronics"),
new("Desk", 300m, "Furniture")
};
// Проекція в анонімний тип
var result = products
.Where(p => p.Category == "Electronics")
.Select(p => new
{
ProductName = p.Name,
PriceInUah = p.Price * 40, // Конвертація в гривні
Discount = p.Price > 1000 ? 10 : 5
});
foreach (var item in result)
{
Console.WriteLine($"{item.ProductName}: {item.PriceInUah} UAH (Discount: {item.Discount}%)");
}
var person = new { Name = "Alice", Age = 25 };
// Створення копії зі зміненою властивістю
var olderPerson = person with { Age = 26 };
Console.WriteLine(person); // { Name = Alice, Age = 25 }
Console.WriteLine(olderPerson); // { Name = Alice, Age = 26 }
// ✅ Гарне використання - локально в методі
public void ProcessData()
{
var temp = new { X = 10, Y = 20 };
Console.WriteLine(temp.X + temp.Y);
}
// ❌ Погане використання - повернення з методу
public object GetData() // Тип object - втрата type safety!
{
return new { X = 10, Y = 20 };
}
// Краще використати record або tuple
public (int X, int Y) GetData()
{
return (10, 20);
}
Nullable value types дозволяють value types приймати значення null. Це корисно, коли значення може бути відсутнім (наприклад, дані з бази даних).
Проблема null посилань, яку її винахідник Тоні Хоар назвав "помилкою на мільярд доларів", десятиліттями була джерелом незліченних помилок NullReferenceException. Спочатку в C# 2.0 з'явився Nullable<T> для типів-значень. Однак справжня революція відбулася в C# 8.0 з введенням Nullable Reference Types (NRT). Ця функція не змінює поведінку CLR, а є потужним інструментом статичного аналізу, який змушує розробника явно вказувати, чи може посилальний тип бути null. Це дозволяє компілятору виявляти потенційні помилки NullReferenceException ще на етапі компіляції, роблячи код набагато безпечнішим.
// Два способи оголошення - еквівалентні
Nullable<int> age1 = null;
int? age2 = null; // Скорочений синтаксис (рекомендується)
// Присвоєння значення
age2 = 25;
// Перевірка наявності значення
if (age2.HasValue)
{
Console.WriteLine($"Age: {age2.Value}");
}
// Безпечний доступ
Console.WriteLine(age2.GetValueOrDefault()); // 25
Console.WriteLine(age2.GetValueOrDefault(18)); // 25 (або 18, якщо null)
// Null-coalescing operator
int definiteAge = age2 ?? 18; // Якщо age2 == null, то 18
Починаючи з C# 8, reference types за замовчуванням non-nullable (якщо ввімкнено NRT). Для nullable потрібен ?.
// Увімкнення NRT у файлі
#nullable enable
// Non-nullable reference type
string name = "Alice";
// name = null; // ⚠️ Попередження компілятора
// Nullable reference type
string? nullableName = null; // OK
// Compiler warning якщо не перевірити на null
void PrintLength(string? input)
{
// Console.WriteLine(input.Length); // ⚠️ Попередження
// ✅ Правильно - перевірка на null
if (input != null)
{
Console.WriteLine(input.Length);
}
// Або з null-conditional operator
Console.WriteLine(input?.Length);
}
| Оператор | Назва | Опис | Приклад |
|---|---|---|---|
?? | Null-coalescing | Повертає ліве значення, якщо не null, інакше праве | int age = nullableAge ?? 18; |
??= | Null-coalescing assignment | Присвоює праве значення, якщо ліве null | name ??= "Default"; |
?. | Null-conditional | Викликає член, тільки якщо об'єкт не null | int? length = text?.Length; |
?[] | Null-conditional index | Індексує масив, тільки якщо не null | int? first = array?[0]; |
! | Null-forgiving | Підказує компілятору, що значення точно не null | string name = nullableText!; |
string? name = null;
string? city = "Kyiv";
// ?? - Null-coalescing
string displayName = name ?? "Guest"; // "Guest"
// ??= - Null-coalescing assignment
name ??= "Default"; // name тепер "Default"
// ?. - Null-conditional member access
int? nameLength = name?.Length; // 7 (довжина "Default")
int? nullLength = ((string?)null)?.Length; // null
// ?[] - Null-conditional indexing
int[] numbers = { 1, 2, 3 };
int? first = numbers?[0]; // 1
int? nullArrayFirst = ((int[]?)null)?[0]; // null
// ! - Null-forgiving operator (використовуйте обережно!)
string definitelyNotNull = name!; // Компілятор не попереджає
string riskyAccess = name!.ToUpper(); // "DEFAULT"
!): Використовуйте тільки коли ви точно впевнені, що значення не null. Якщо помилитеся, отримаєте NullReferenceException у runtime.У .csproj файлі:
<Nullable>enable</Nullable>
if (nullableValue != null)
{
// Безпечне використання
}
// Замість:
int length = text != null ? text.Length : 0;
// Краще:
int length = text?.Length ?? 0;
// ❌ Погано
string name = GetName()!;
// ✅ Добре
string? name = GetName();
if (name != null)
{
// використання
}
Discriminated Unions (також tagged unions або sum types) — це тип даних, який може бути одним з кількох можливих варіантів. Це поширена концепція в функціональному програмуванні (F#, Rust, TypeScript), але нативно не підтримується в C# (станом на C# 13).
type PaymentMethod =
| Cash of amount: decimal
| CreditCard of cardNumber: string * cvv: string
| BankTransfer of accountNumber: string
let processPayment payment =
match payment with
| Cash amount -> printfn "Cash payment: %M" amount
| CreditCard (number, cvv) -> printfn "Card: %s" number
| BankTransfer account -> printfn "Transfer from: %s" account
Найкращий спосіб емулювати discriminated unions у C# — sealed class hierarchy з pattern matching.
// База - sealed щоб запобігти розширенню ззовні
public abstract record PaymentMethod;
// Варіанти
public record Cash(decimal Amount) : PaymentMethod;
public record CreditCard(string CardNumber, string Cvv) : PaymentMethod;
public record BankTransfer(string AccountNumber) : PaymentMethod;
// Використання з pattern matching
public static string ProcessPayment(PaymentMethod payment)
{
return payment switch
{
Cash { Amount: var amount } =>
$"Cash payment: {amount:C}",
CreditCard { CardNumber: var number } =>
$"Card payment: {number[^4..]} (last 4 digits)",
BankTransfer { AccountNumber: var account } =>
$"Bank transfer from: {account}",
_ => throw new ArgumentException("Unknown payment method")
};
}
// Використання
PaymentMethod payment1 = new Cash(100m);
PaymentMethod payment2 = new CreditCard("1234-5678-9012-3456", "123");
Console.WriteLine(ProcessPayment(payment1));
// Cash payment: $100.00
Console.WriteLine(ProcessPayment(payment2));
// Card payment: 3456 (last 4 digits)
OneOf — популярна бібліотека NuGet, яка надає discriminated unions для C#.
using OneOf;
// Визначення union type
public class Result : OneOfBase<Success, Error>
{
public Result(OneOf<Success, Error> input) : base(input) { }
}
public record Success(string Message);
public record Error(string ErrorMessage, int Code);
// Метод, що повертає union
public Result DivideNumbers(int a, int b)
{
if (b == 0)
{
return new Error("Division by zero", 400);
}
return new Success($"Result: {a / b}");
}
// Pattern matching з OneOf
Result result = DivideNumbers(10, 2);
string output = result.Match(
success => $"✓ {success.Message}",
error => $"✗ Error {error.Code}: {error.ErrorMessage}"
);
Console.WriteLine(output); // ✓ Result: 5
| Тип | Value/Reference | Mutability | Equality | Inheritance | Коли використовувати |
|---|---|---|---|---|---|
| Struct | Value | Mutable | Value | ❌ | Малі, логічно одне значення, передається часто |
| Enum | Value | Immutable | Value | ❌ | Фіксований набір варіантів |
| Record class | Reference | Immutable* | Value | ✅ | DTO, immutable дані, value equality |
| Record struct | Value | Immutable* | Value | ❌ | Малі immutable дані з value equality |
| Tuple | Value | Mutable | Value | ❌ | Повернення кількох значень, локальні групи |
| Anonymous | Reference | Immutable | Value | ❌ | LINQ проекції, тимчасові об'єкти |
| Class | Reference | Mutable | Reference | ✅ | Складні об'єкти, entity, багато логіки |
* Recommended immutable, але можна зробити mutable
object або інтерфейс відбувається boxing — копіювання даних на heap.public struct Point
{
public int X, Y;
}
Point p = new Point { X = 10, Y = 20 };
object obj = p; // ⚠️ Boxing - копія на heap
OrderStatus status = (OrderStatus)999; // Компілюється!
Enum.IsDefined().if (Enum.IsDefined(typeof(OrderStatus), status))
{
// Валідне значення
}
public record Person(string Name, List<string> Hobbies);
var p1 = new Person("Alice", new List<string> { "Reading" });
var p2 = p1 with { };
p2.Hobbies.Add("Coding"); // Змінює і p1.Hobbies!
using System.Collections.Immutable;
public record Person(string Name, ImmutableList<string> Hobbies);
NullReferenceException при доступі до nullable reference.string? name = GetName();
Console.WriteLine(name.Length); // 💥 NullReferenceException
Console.WriteLine(name?.Length ?? 0);
Створіть програму для управління геометричними фігурами:
struct Point з властивостями X та Y (readonly)DistanceTo(Point other) для обчислення відстаніenum Shape { Circle, Rectangle, Triangle }enum Direction { North, South, East, West } та метод GetOpposite() для отримання протилежного напрямкуpublic readonly struct Point
{
public double X { get; }
public double Y { get; }
public Point(double x, double y)
{
X = x;
Y = y;
}
public double DistanceTo(Point other)
{
double dx = X - other.X;
double dy = Y - other.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
}
public enum Shape
{
Circle,
Rectangle,
Triangle
}
public enum Direction
{
North,
South,
East,
West
}
public static Direction GetOpposite(this Direction direction)
{
return direction switch
{
Direction.North => Direction.South,
Direction.South => Direction.North,
Direction.East => Direction.West,
Direction.West => Direction.East,
_ => throw new ArgumentException()
};
}
Створіть систему для управління користувачами та правами доступу:
record User(string Name, string Email, DateTime RegisteredAt)GetAge() до record через extension[Flags] enum Permissions з правами: Read, Write, Execute, DeleteAddPermission, RemovePermission, HasPermissionwith expression для створення адміністратора з копії звичайного користувачаpublic record User(string Name, string Email, DateTime RegisteredAt, Permissions Permissions);
[Flags]
public enum Permissions
{
None = 0,
Read = 1,
Write = 2,
Execute = 4,
Delete = 8,
Full = Read | Write | Execute | Delete
}
public static class PermissionsExtensions
{
public static Permissions Add(this Permissions current, Permissions permission)
{
return current | permission;
}
public static Permissions Remove(this Permissions current, Permissions permission)
{
return current & ~permission;
}
public static bool Has(this Permissions current, Permissions permission)
{
return (current & permission) == permission;
}
}
// Використання
User user = new("Alice", "alice@example.com", DateTime.Now, Permissions.Read);
User admin = user with
{
Permissions = Permissions.Full
};
Console.WriteLine(admin.Permissions.Has(Permissions.Write)); // True
Створіть систему обробки результатів операцій без exceptions:
Result<T> з варіантами Success<T> та FailureSuccess містить значення типу TFailure містить повідомлення про помилку та код помилкиMatch<TOut>, Bind<TOut>, Map<TOut> для функціональної роботиpublic abstract record Result<T>
{
public abstract TOut Match<TOut>(
Func<T, TOut> onSuccess,
Func<string, int, TOut> onFailure);
}
public record Success<T>(T Value) : Result<T>
{
public override TOut Match<TOut>(
Func<T, TOut> onSuccess,
Func<string, int, TOut> onFailure)
{
return onSuccess(Value);
}
}
public record Failure<T>(string Message, int ErrorCode) : Result<T>
{
public override TOut Match<TOut>(
Func<T, TOut> onSuccess,
Func<string, int, TOut> onFailure)
{
return onFailure(Message, ErrorCode);
}
}
// Extension methods для функціонального програмування
public static class ResultExtensions
{
public static Result<TOut> Bind<T, TOut>(
this Result<T> result,
Func<T, Result<TOut>> func)
{
return result.Match(
onSuccess: func,
onFailure: (msg, code) => new Failure<TOut>(msg, code)
);
}
public static Result<TOut> Map<T, TOut>(
this Result<T> result,
Func<T, TOut> mapper)
{
return result.Match(
onSuccess: value => new Success<TOut>(mapper(value)),
onFailure: (msg, code) => new Failure<TOut>(msg, code)
);
}
}
// Приклад використання
Result<int> ParseNumber(string input)
{
if (int.TryParse(input, out int value))
{
return new Success<int>(value);
}
return new Failure<int>("Invalid number format", 400);
}
Result<int> ValidatePositive(int number)
{
if (number > 0)
{
return new Success<int>(number);
}
return new Failure<int>("Number must be positive", 400);
}
Result<int> Double(int number)
{
return new Success<int>(number * 2);
}
// Ланцюжок операцій
var result = ParseNumber("42")
.Bind(ValidatePositive)
.Map(Double);
string output = result.Match(
onSuccess: value => $"Result: {value}",
onFailure: (msg, code) => $"Error {code}: {msg}"
);
Console.WriteLine(output); // Result: 84
У цьому розділі ми детально розглянули розширені типи даних в C#:
ref struct для high-performance сценаріїв.[Flags] attribute для бітових операцій.with expressions для immutable даних.null для value (int?) та reference types (string?). Null-coalescing operators для безпечної роботи.Вибір правильного типу залежить від розміру даних, mutability, equality semantics та потреби в inheritance. Розуміння цих відмінностей дозволяє писати більш ефективний, читабельний та підтримуваний код.