Skip to content

C# Шпаргалка

Современный объектно-ориентированный язык для .NET, веба и игр.

01

Начало работы

Hello World

Программы на C# начинаются в Main(). using System; импортирует пространство имён System (Console, Math и т.д.). C# 9+ поддерживает top-level statements: файл с одной строкой Console.WriteLine("Hello"); — допустимая программа. Компилятор генерирует класс и Main автоматически.

csharp
using System;

class Program {
    static void Main() {
        Console.WriteLine("Hello, World!");
    }
}

Переменные и типы

C# статически типизирован. var позволяет компилятору вывести тип (всё ещё типобезопасен во время компиляции). decimal (суффикс m) — для финансовых вычислений с точной точностью. const — константа времени компиляции; readonly — константа времени выполнения, устанавливаемая в конструкторе.

csharp
int age = 30;
double pi = 3.14159;
decimal price = 19.99m;
char grade = 'A';
bool isDev = true;
string name = "Alice";
var x = 42;  // implicitly typed (int)
const double TAX = 0.08;

Интерполяция строк

Строковая интерполяция ($"...") встраивает выражения в фигурные скобки. Спецификаторы формата после : управляют выводом (F2 для 2 знаков после запятой, yyyy-MM-dd для дат). Verbatim-строки (@"") трактуют обратные слэши буквально — полезно для путей к файлам и regex. Комбинируйте: $@"...".

csharp
string name = "Alice";
int age = 30;
Console.WriteLine($"Name: {name}, Age: {age}");

// Formatting
double pi = 3.14159;
Console.WriteLine($"Pi: {pi:F2}");  // Pi: 3.14
Console.WriteLine($"Date: {DateTime.Now:yyyy-MM-dd}");

// Verbatim strings (@"") preserve backslashes
string path = @"C:\Users\name";

Ввод и вывод

Console.ReadLine() читает всю строку как строку. int.Parse преобразует, но бросает исключение при некорректном вводе; int.TryParse безопаснее — возвращает bool и использует out-параметр. Всегда используйте TryParse для пользовательского ввода, чтобы избежать исключений от плохих данных.

csharp
Console.Write("Enter name: ");
string name = Console.ReadLine();
Console.Write("Enter age: ");
int age = int.Parse(Console.ReadLine());

Console.WriteLine($"Hi {name}, you are {age}");

// Safe parsing with TryParse
if (int.TryParse(Console.ReadLine(), out int num)) {
    Console.WriteLine($"Valid: {num}");
} else {
    Console.WriteLine("Invalid number");
}

Nullable-типы

int? — nullable value type (Nullable<int>). Оператор ?. (null-conditional) безопасно обращается к членам — возвращает null вместо бросания исключения. Оператор ?? (null-coalescing) предоставляет значение по умолчанию для null. Nullable reference types в C# 8+ предупреждают о потенциальных null во время компиляции.

csharp
int? maybeAge = null;  // nullable int
if (maybeAge.HasValue) {
    Console.WriteLine(maybeAge.Value);
}

string? maybeName = null;  // nullable reference (C# 8+)
int length = maybeName?.Length ?? 0;  // null-conditional + null-coalescing

// ValueOrDefault
int age = maybeAge ?? 0;  // 0 if null
02

Строки

Строковые методы

Строки C# неизменяемы — методы возвращают новые строки, а не модифицируют оригинал. Распространённые методы: Length, ToUpper/ToLower, Substring, IndexOf, Replace, Contains, Split. Для интенсивной работы со строками используйте StringBuilder, чтобы избежать создания множества промежуточных строк.

csharp
string s = "Hello, World!";
Console.WriteLine(s.Length);        // 13
Console.WriteLine(s.ToUpper());     // HELLO, WORLD!
Console.WriteLine(s.Substring(0, 5)); // Hello
Console.WriteLine(s.IndexOf("World")); // 7
Console.WriteLine(s.Replace("World", "C#"));
Console.WriteLine(s.Contains("Hello")); // True
Console.WriteLine(s.Split(", "));   // ["Hello", "World!"]

StringBuilder

StringBuilder изменяем и эффективен для построения строк в циклах. Конкатенация строк (+) каждый раз создаёт новую строку, поэтому повторная конкатенация — O(n^2). StringBuilder амортизируется до O(n). Используйте StringBuilder, когда у вас больше нескольких конкатенаций.

csharp
using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
    sb.Append("Line ").Append(i).Append("\n");
}
string result = sb.ToString();

// vs string concatenation (creates many temporary strings)
// string result = "";
// for (int i = 0; i < 100; i++) result += "Line " + i;

Форматирование и разбор

string.Format использует плейсхолдеры {0}, {1}. Спецификаторы формата: F (фиксированный), N (число с разделителями), P (процент), X (hex). Разбор чувствителен к культуре — десятичные разделители различаются по локали. Используйте CultureInfo для явного контроля или TryParse для безопасности.

csharp
// String formatting
string s1 = string.Format("{0} is {1} years old", "Alice", 30);
string s2 = $"{3.14159:F2}";  // "3.14"
string s3 = $"{1234567:N0}";  // "1,234,567" (thousands separator)

// Parsing
int n = int.Parse("42");
double d = double.Parse("3.14");
bool b = bool.Parse("true");

// Culture-aware parsing
double euro = double.Parse("3,14",
    System.Globalization.CultureInfo.GetCultureInfo("de-DE"));

Сравнение строк

Используйте StringComparison.OrdinalIgnoreCase для сравнения без учёта регистра — это яснее и быстрее, чем ToLower() затем ==. Избегайте == для сравнений, чувствительных к культуре; используйте string.Compare с CultureInfo. Для ключей словаря используйте StringComparer.OrdinalIgnoreCase.

csharp
string a = "Hello", b = "hello";

// Case-sensitive
bool equal = a == b;  // false
bool equal2 = a.Equals(b);  // false

// Case-insensitive
bool equalCI = a.Equals(b, StringComparison.OrdinalIgnoreCase);  // true
int cmp = string.Compare(a, b, StringComparison.OrdinalIgnoreCase);

// Best practice for comparison
bool same = string.Equals(a, b,
    StringComparison.OrdinalIgnoreCase);

Регулярные выражения

Regex (из System.Text.RegularExpressions) обеспечивает сопоставление с образцом. Используйте @"" verbatim-строки, чтобы обратные слэши не требовали двойного экранирования. Для повторного использования скомпилируйте экземпляр Regex один раз и переиспользуйте (RegexOptions.Compiled) для лучшей производительности.

csharp
using System.Text.RegularExpressions;

string text = "Phone: 123-456-7890";
var match = Regex.Match(text, @"\d{3}-\d{3}-\d{4}");
if (match.Success) Console.WriteLine(match.Value);  // 123-456-7890

// Replace
string clean = Regex.Replace(text, @"\d", "X");
// "Phone: XXX-XXX-XXXX"

// Extract all matches
var emails = Regex.Matches("[email protected], [email protected]", @"\S+@\S+");
03

Числа и математика

Числовые типы

В C# типы фиксированного размера: int (32-бит), long (64-бит), double (64-бит float), decimal (128-бит, для финансов). Подчёркивания (9_000_000) улучшают читаемость. Используйте decimal для денег — он избегает ошибок округления плавающей точки. Суффиксы: L (long), f (float), m (decimal).

csharp
int i = 42;
long big = 9_000_000_000L;  // underscores for readability
double d = 3.14159265;
float f = 3.14f;
decimal price = 19.99m;     // exact decimal for money
byte b = 255;

Console.WriteLine(sizeof(int));   // 4
Console.WriteLine(int.MaxValue);  // 2147483647

Класс Math

Math предоставляет статические методы для распространённых операций. Math.Round по умолчанию использует банковское округление (к чётному) — используйте MidpointRounding.AwayFromZero для школьного округления. Math.BigMul обрабатывает умножение long для избежания переполнения.

csharp
double x = 2.5;
Math.Pow(x, 3);     // 15.625
Math.Sqrt(x);       // 1.581
Math.Abs(-5);       // 5
Math.Floor(3.7);    // 3
Math.Ceiling(3.2);  // 4
Math.Round(3.5);    // 4 (banker's rounding)
Math.Max(3, 7);     // 7
Math.Min(3, 7);     // 3
Math.PI;            // 3.14159...

Случайные числа

Random генерирует псевдослучайные числа. Создайте один экземпляр и переиспользуйте (создание многих в цикле может давать дубликаты из-за seed по времени). Для криптографической случайности используйте System.Security.Cryptography.RandomNumberGenerator. C# 8+ предоставляет Random.Shared для потокобезопасности.

csharp
var random = new Random();
int r = random.Next(1, 101);     // 1-100
double d = random.NextDouble();   // 0.0-1.0
byte[] bytes = new byte[4];
random.NextBytes(bytes);

// C# 8+: thread-safe
// var rng = Random.Shared;
// int n = rng.Next(1, 101);

// Array of random items
int[] nums = Enumerable.Range(0, 5)
    .Select(_ => random.Next(1, 100)).ToArray();

Преобразование типов

Неявные преобразования происходят автоматически, когда нет потери данных (int в double). Явные приведения (type) нужны, когда может быть потеряна точность. Convert.ToInt32 обрабатывает многие типы и округляет (в отличие от приведения, которое обрезает). Всегда предпочитайте TryParse для преобразования строки в число, чтобы избежать исключений.

csharp
// Implicit (safe, no data loss)
int i = 42;
double d = i;  // int -> double

// Explicit cast (may lose data)
double pi = 3.14;
int truncated = (int)pi;  // 3

// Convert class
string s = "42";
int n = Convert.ToInt32(s);
string str = Convert.ToString(42);

// Parse / TryParse
int parsed = int.Parse("100");
bool ok = int.TryParse("abc", out int result);  // false

Переполнение целого и checked

По умолчанию переполнение целого тихо оборачивается (unchecked). Блок checked бросает OverflowException при переполнении — используйте его для критичного к безопасности кода. Для действительно больших чисел BigInteger (System.Numerics) обрабатывает целые произвольной точности без переполнения.

csharp
int a = int.MaxValue;  // 2147483647
// a++;  // overflow: wraps to -2147483648 (unchecked by default)

checked {
    a++;  // throws OverflowException!
}

// BigInteger for arbitrary precision
using System.Numerics;
var big = BigInteger.Pow(2, 100);
Console.WriteLine(big);  // 1267650...376 (31 digits)
04

Поток управления

If / Else

if/else if/else — стандартно. Тернарный оператор (condition ? a : b) — краткое if/else-выражение. C# требует булевы условия — в отличие от C, целые не неявно преобразуются в bool. Используйте фигурные скобки даже для одного оператора для предотвращения багов при сопровождении.

csharp
int score = 85;
if (score >= 90) {
    Console.WriteLine("A");
} else if (score >= 80) {
    Console.WriteLine("B");
} else {
    Console.WriteLine("C");
}

// Ternary operator
string grade = score >= 60 ? "Pass" : "Fail";

Switch и pattern matching

Switch-выражения C# 8+ (=>) кратки и возвращают значения. Паттерн _ — случай по умолчанию. Pattern matching (is, switch) поддерживает type-паттерны, property-паттерны и relational-паттерны (> 18). Это мощнее традиционного switch и сокращает шаблонность.

csharp
// Classic switch
int day = 3;
string name = day switch {
    1 => "Mon", 2 => "Tue", 3 => "Wed",
    6 or 7 => "Weekend",
    _ => "Invalid"  // default
};

// Pattern matching with types
object obj = "hello";
if (obj is string s && s.Length > 3) {
    Console.WriteLine(s);  // "hello"
}

// Property patterns
if (person is { Age: > 18, Name: "Alice" }) {
    // matches if Age > 18 and Name == "Alice"
}

Циклы

for — для счётных итераций; foreach итерирует коллекции (массивы, списки, IEnumerable); while циклит, пока условие ложно. foreach только для чтения — нельзя модифицировать коллекцию во время итерации. Используйте for, если нужен индекс или модификация элементов.

csharp
// for loop
for (int i = 0; i < 5; i++) {
    Console.WriteLine(i);
}

// foreach
string[] fruits = { "apple", "banana", "cherry" };
foreach (string fruit in fruits) {
    Console.WriteLine(fruit);
}

// while
int n = 5;
while (n > 0) Console.WriteLine(n--);

Break, continue и goto

break выходит из ближайшего цикла или switch; continue переходит к следующей итерации. В C# нет помеченного break для вложенных циклов — используйте флаг, извлеките в метод с return или используйте LINQ. goto допустим в switch для проваливания, но иначе не поощряется.

csharp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    Console.WriteLine(i);  // 0 1 2 4 5 6
}

// goto in switch (rare but valid)
switch (code) {
    case 1:
        DoSomething();
        goto case 2;  // fall through
    case 2:
        DoMore();
        break;
}

Итераторы и yield

yield return превращает метод в итератор, лениво производящий значения — по одному за раз по запросу. Это эффективно по памяти для больших или бесконечных последовательностей. Компилятор генерирует конечный автомат. yield break завершает итерацию досрочно. LINQ широко использует итераторы.

csharp
// yield return creates an iterator (lazy evaluation)
IEnumerable<int> GetNumbers() {
    for (int i = 0; i < 5; i++) {
        yield return i;
    }
}

foreach (int n in GetNumbers()) {
    Console.WriteLine(n);  // 0 1 2 3 4
}

// Infinite sequence (consumed lazily)
IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
05

Методы и делегаты

Определение метода

Методы могут быть expression-bodied (=>) для однострочников. Параметры по умолчанию делают аргументы опциональными. Именованные аргументы (name:) улучшают читаемость вызовов со многими параметрами и позволяют пропускать опциональные. C# не поддерживает перегрузку методов только с разными типами возврата.

csharp
static int Add(int a, int b) {
    return a + b;
}

// Expression-bodied (C# 6+)
static int Multiply(int a, int b) => a * b;

// Default parameters
static void Greet(string name = "Guest", int times = 1) {
    for (int i = 0; i < times; i++)
        Console.WriteLine($"Hi {name}!");
}

Greet();              // Hi Guest!
Greet("Alice", 3);    // Hi Alice! x3
Greet(times: 2);      // named argument

out, ref и params

out-параметры должны быть присвоены методом (вызывающему не нужно инициализировать). ref-параметры должны быть инициализированы вызывающим и могут модифицироваться. params допускает переменное число аргументов. C# 7+ разрешает объявление out var inline: TryParse(s, out var result).

csharp
// out: must be assigned in method (caller doesn't init)
bool TryParse(string s, out int result) {
    result = 0;
    return int.TryParse(s, out result);
}

// ref: caller must initialize
void Increment(ref int n) { n++; }
int x = 5;
Increment(ref x);  // x is now 6

// params: variable arguments
int Sum(params int[] nums) => nums.Sum();
Sum(1, 2, 3);  // 6
Sum();         // 0

Делегаты и события

Делегаты — типобезопасные указатели на функции. Func<T,TResult> принимает входы и возвращает значение; Action<T> возвращает void; Predicate<T> возвращает bool. События — делегаты с подпиской +=/-= и безопасным вызовом ?.Invoke (null-проверка). Используйте события для паттерна observer (pub/sub).

csharp
// Delegate type
delegate int MathOp(int a, int b);

MathOp add = (a, b) => a + b;
MathOp mul = (a, b) => a * b;
Console.WriteLine(add(3, 4));  // 7

// Built-in delegate types
Func<int, int, int> divide = (a, b) => a / b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;

// Events
event Action<string> OnMessage;
OnMessage += msg => Console.WriteLine($"Got: {msg}");
OnMessage?.Invoke("Hello");

Лямбда-выражения

Лямбды — анонимные методы, использующие =>. Они захватывают переменные из охватывающей области (замыкания). Захваченные переменные вычисляются при выполнении лямбды, а не при создании — остерегайтесь этого в циклах. Используйте лямбды широко с LINQ, событиями и callback'ами.

csharp
// Lambda with explicit types
Func<int, int> square = (int x) => x * x;

// Type inference
Func<int, int, int> add = (a, b) => a + b;

// Statement lambda
Action<string> log = msg => {
    Console.WriteLine($"[{DateTime.Now}] {msg}");
};

// Capturing variables (closures)
int factor = 3;
Func<int, int> multiply = x => x * factor;
Console.WriteLine(multiply(5));  // 15

Методы расширения

Методы расширения добавляют методы к существующим типам без их модификации. Ключевое слово this перед первым параметром помечает метод расширения. Они должны быть в статическом классе. LINQ полностью реализован как методы расширения на IEnumerable. Это мощный способ добавления utility-методов.

csharp
static class StringExtensions {
    public static int WordCount(this string s) {
        return s.Split(new[] { ' ' },
            StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

string text = "Hello World C#";
Console.WriteLine(text.WordCount());  // 3

// LINQ is built on extension methods
var evens = nums.Where(n => n % 2 == 0);
06

Коллекции

List<T>

List<T> — динамический массив (как vector в C++ или ArrayList в Java). Add/Remove — амортизированно O(1) / O(n). Count даёт число элементов (не capacity). Find/FindAll используют предикаты. Для частых вставок/удалений в середине LinkedList<T> может быть лучше.

csharp
using System.Collections.Generic;
var nums = new List<int> { 1, 2, 3 };
nums.Add(4);
nums.AddRange(new[] { 5, 6 });
nums.Remove(3);
nums.RemoveAt(0);
Console.WriteLine(nums.Count);  // 4
Console.WriteLine(nums.Contains(4));  // true

// Find
int first = nums.Find(n => n > 3);
List<int> all = nums.FindAll(n => n > 3);

Dictionary<TKey, TValue>

Dictionary — хеш-таблица с O(1) средним поиском. TryGetValue безопаснее индексатора (который бросает KeyNotFoundException). Для ключей-строк без учёта регистра передайте StringComparer.OrdinalIgnoreCase в конструктор. Dictionary не сохраняет порядок вставки (используйте OrderedDictionary, если нужно).

csharp
var ages = new Dictionary<string, int> {
    ["Alice"] = 30,
    ["Bob"] = 25
};
ages["Charlie"] = 35;

// Safe access
if (ages.TryGetValue("Alice", out int age)) {
    Console.WriteLine($"Alice: {age}");
}

// Iterate
foreach (var kv in ages) {
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

// Case-insensitive keys
var dict = new Dictionary<string, int>(
    StringComparer.OrdinalIgnoreCase);

HashSet<T> и SortedSet<T>

HashSet<T> хранит уникальные элементы с O(1) поиском — используйте для дедупликации и проверки членства. Поддерживает set-операции (Union, Intersect, Except). SortedSet<T> (красно-чёрное дерево) хранит элементы отсортированными с O(log n) операциями. Используйте HashSet, когда порядок не важен, SortedSet — когда важен.

csharp
var set = new HashSet<int> { 1, 2, 3 };
set.Add(2);  // duplicate, ignored
set.Add(4);
Console.WriteLine(set.Count);  // 4
Console.WriteLine(set.Contains(3));  // true

// Set operations
var other = new HashSet<int> { 3, 4, 5 };
set.IntersectWith(other);  // set: {3, 4}
set.UnionWith(other);      // set: {3, 4, 5}

// SortedSet keeps elements sorted
var sorted = new SortedSet<int> { 3, 1, 2 };  // {1, 2, 3}

Queue<T> и Stack<T>

Queue<T> — FIFO (Enqueue/Dequeue) — используйте для планирования задач, BFS. Stack<T> — LIFO (Push/Pop) — используйте для undo/redo, DFS, вычисления выражений. Оба O(1) для основных операций. Для параллельных сценариев используйте ConcurrentQueue и ConcurrentStack из System.Collections.Concurrent.

csharp
// Queue: FIFO (first in, first out)
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
string next = queue.Dequeue();  // "first"
string peek = queue.Peek();     // "second"

// Stack: LIFO (last in, first out)
var stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
int top = stack.Pop();   // 2
int peek = stack.Peek(); // 1

Массивы

Массивы фиксированного размера. Array.Sort сортирует на месте. Многомерные массивы ([,]) — прямоугольные (равномерные столбцы). Jagged-массивы ([][]) — массивы массивов (строки могут иметь разную длину). Используйте List<T> для динамического размера; массивы — для фиксированных, критичных к производительности данных.

csharp
int[] nums = { 1, 2, 3, 4, 5 };
Console.WriteLine(nums.Length);  // 5
Array.Sort(nums);
Array.Reverse(nums);
int found = Array.IndexOf(nums, 3);
int[] copy = new int[5];
Array.Copy(nums, copy, 5);

// Multidimensional
int[,] grid = { { 1, 2 }, { 3, 4 } };
Console.WriteLine(grid[1, 0]);  // 3

// Jagged array (array of arrays)
int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 } };
07

Классы и ООП

Класс и свойства

Auto-properties ({ get; set; }) автоматически генерируют backing-поля. Expression-bodied свойства (=>) вычисляются при доступе. Init-only свойства C# 9+ ({ get; init; }) устанавливаются только во время конструирования, обеспечивая неизменяемость. Инициализаторы объектов: new Person { Name = "X" }.

csharp
class Person {
    // Auto-properties (C# 3+)
    public string Name { get; set; }
    public int Age { get; set; }

    // Read-only property
    public bool IsAdult => Age >= 18;

    // Constructor
    public Person(string name, int age) {
        Name = name;
        Age = age;
    }
}

var p = new Person("Alice", 30);
Console.WriteLine(p.IsAdult);  // true

Наследование и virtual

virtual помечает метод для переопределения; override в подклассах предоставляет новую реализацию. C# требует явного override (в отличие от Java). sealed предотвращает наследование класса или переопределение метода. Все классы неявно наследуют от object (System.Object).

csharp
class Animal {
    public virtual void Speak() {
        Console.WriteLine("...");
    }
}

class Dog : Animal {
    public override void Speak() {
        Console.WriteLine("Woof");
    }
}

Animal a = new Dog();
a.Speak();  // Woof (polymorphism)

// sealed prevents further inheritance
sealed class Puppy : Dog { }

Интерфейсы

Интерфейсы определяют контракты без реализации. Класс может реализовывать несколько интерфейсов (в отличие от одиночного наследования классов). C# 8+ допускает default interface methods. Используйте интерфейсы для полиморфизма и внедрения зависимостей. Соглашение об именовании: префикс I (IShape, ILogger).

csharp
interface IShape {
    double Area();  // interface member
    double Perimeter { get; }
}

class Circle : IShape {
    public double Radius { get; set; }
    public double Area() => Math.PI * Radius * Radius;
    public double Perimeter => 2 * Math.PI * Radius;
}

// Default interface methods (C# 8+)
interface ILogger {
    void Log(string msg);
    void Error(string msg) => Log($"ERROR: {msg}");  // default
}

Абстрактные классы

Абстрактные классы нельзя инстанцировать, и они могут иметь как абстрактные (требующие override), так и конкретные (virtual) члены. Используйте абстрактные классы, когда есть разделяемая реализация; интерфейсы — для чистых контрактов. Класс может наследовать только один абстрактный класс, но реализовывать много интерфейсов.

csharp
abstract class Shape {
    public abstract double Area();  // must be overridden
    public virtual void Describe() {
        Console.WriteLine($"Area: {Area()}");
    }
}

class Rectangle : Shape {
    public double Width { get; set; }
    public double Height { get; set; }
    public override double Area() => Width * Height;
}

// Cannot instantiate abstract class
// var s = new Shape();  // ERROR
var r = new Rectangle { Width = 3, Height = 4 };

Records и structs

Records (C# 9+) предоставляют равенство по значению, неизменяемость и краткий синтаксис — идеальны для DTO и моделей данных. Выражение with создаёт копию с модифицированными свойствами. Structs — типы значений (копируются при присваивании, размещаются в стеке) — используйте для небольших, лёгких данных. Классы — ссылочные типы (размещаются в куче).

csharp
// Record (C# 9+): value-based equality, immutability
record Point(int X, int Y);

var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
Console.WriteLine(p1 == p2);  // true (value equality)

// with expression (non-destructive mutation)
var p3 = p1 with { X = 10 };

// Struct: value type (stack-allocated)
struct Vector {
    public double X, Y;
    public Vector(double x, double y) { X = x; Y = y; }
}
08

LINQ

Where и Select

LINQ (Language Integrated Query) предоставляет SQL-подобные запросы к коллекциям. Where фильтрует, Select преобразует (map). LINQ ленив — запросы выполняются только при перечислении (например, через ToList()). Это обеспечивает эффективную цепочку без промежуточных коллекций. Method syntax (выше) наиболее распространён; query syntax также доступен.

csharp
using System.Linq;
int[] nums = { 1, 2, 3, 4, 5, 6 };

// Filter
var evens = nums.Where(n => n % 2 == 0);  // 2, 4, 6

// Map/Transform
var squares = nums.Select(n => n * n);  // 1, 4, 9, 16, 25, 36

// Chain operations
var result = nums
    .Where(n => n > 2)
    .Select(n => n * 10)
    .ToList();  // 30, 40, 50, 60

Сортировка и группировка

OrderBy/OrderByDescending сортируют; ThenBy добавляет вторичный критерий сортировки. GroupBy группирует элементы по ключу, возвращая IGrouping<key, element> группы. Каждая группа имеет Key и сама является IEnumerable своих членов. Это заменяет ручные циклы и словари для группировки.

csharp
var people = new[] {
    new { Name = "Alice", Age = 30 },
    new { Name = "Bob", Age = 25 },
    new { Name = "Charlie", Age = 30 },
};

// Order
var sorted = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
var desc = people.OrderByDescending(p => p.Age);

// Group
var grouped = people.GroupBy(p => p.Age);
foreach (var g in grouped) {
    Console.WriteLine($"Age {g.Key}: {g.Count()} people");
}
// Age 25: 1 people
// Age 30: 2 people

Агрегация

Методы агрегации LINQ (Sum, Average, Min, Max, Count) вычисляют одиночные значения из коллекций. Aggregate — наиболее общий, это fold/reduce, применяющий функцию кумулятивно. Они бросают исключение на пустых последовательностях; используйте варианты *OrDefault или проверяйте пустоту.

csharp
int[] nums = { 1, 2, 3, 4, 5 };

Console.WriteLine(nums.Sum());    // 15
Console.WriteLine(nums.Average()); // 3
Console.WriteLine(nums.Min());    // 1
Console.WriteLine(nums.Max());    // 5
Console.WriteLine(nums.Count());  // 5

// Count with predicate
int evens = nums.Count(n => n % 2 == 0);  // 2

// Aggregate (fold/reduce)
int product = nums.Aggregate((a, b) => a * b);  // 120
string joined = nums.Aggregate("", (s, n) => s + n);  // "12345"

First, Single и ElementAt

First возвращает первый элемент (бросает, если пуст); FirstOrDefault возвращает default (0 для int, null для ссылочных типов), если не найден. Single требует ровно один элемент (иначе исключение) — используйте для валидации. Any/All возвращают bool без перечисления всей коллекции (short-circuit).

csharp
int[] nums = { 1, 2, 3, 4, 5 };

int first = nums.First();           // 1
int firstEven = nums.First(n => n % 2 == 0);  // 2
int firstOrDef = nums.FirstOrDefault(n => n > 10);  // 0 (default)

int single = new[] { 42 }.Single();  // 42
// nums.Single();  // throws! more than one element

int third = nums.ElementAt(2);  // 3 (0-indexed)

bool any = nums.Any(n => n > 3);  // true
bool all = nums.All(n => n > 0);  // true

Join и Zip

Join выполняет внутреннее соединение (как SQL), сопоставляя элементы по ключу. Zip парит элементы из двух последовательностей по позиции. Оба производят новые последовательности. LINQ также поддерживает GroupJoin (left join с группировкой). Это мощно для комбинирования связанных данных из нескольких источников.

csharp
var users = new[] {
    new { Id = 1, Name = "Alice" },
    new { Id = 2, Name = "Bob" },
};
var orders = new[] {
    new { UserId = 1, Item = "Book" },
    new { Id = 2, Item = "Pen" },
};

// Join (inner join)
var joined = users.Join(orders,
    u => u.Id, o => o.UserId,
    (u, o) => new { u.Name, o.Item });
// { Alice, Book }

// Zip (pair elements by position)
var names = new[] { "Alice", "Bob" };
var ages = new[] { 30, 25 };
var pairs = names.Zip(ages, (n, a) => $"{n}: {a}");
// "Alice: 30", "Bob: 25"
09

Обработка ошибок

Try / Catch / Finally

Сначала ловите конкретные исключения, затем общее Exception последним (от наиболее специфичного к наименее). finally выполняется всегда — используйте для очистки (закрытие файлов, освобождение ресурсов). Избегайте catch (Exception) для потока управления; ловите только то, что можете обработать. Используйте throw; (не throw e;) для сохранения стека вызовов.

csharp
try {
    int x = 10;
    int y = 0;
    int z = x / y;  // throws DivideByZeroException
} catch (DivideByZeroException e) {
    Console.WriteLine($"Error: {e.Message}");
} catch (Exception e) {
    Console.WriteLine($"Unexpected: {e.Message}");
} finally {
    // always runs, even with return/throw
    Console.WriteLine("Cleanup");
}

Пользовательские исключения

Наследуйте пользовательские исключения от Exception (или более специфичной базы). Добавляйте контекстные поля, помогающие отладке. Всегда вызывайте базовый конструктор с сообщением. По соглашению имена классов исключений заканчиваются на Exception. Используйте пользовательские исключения для домен-специфичных ошибочных условий, которые вызывающий может обработать.

csharp
class ValidationError : Exception {
    public string Field { get; }
    public ValidationError(string field, string msg)
        : base(msg) {
        Field = field;
    }
}

throw new ValidationError("email", "Invalid format");

try {
    // ...
} catch (ValidationError e) {
    Console.WriteLine($"{e.Field}: {e.Message}");
}

using и IDisposable

using гарантирует вызов Dispose(), даже если возникает исключение — это эквивалент RAII в C# для управления ресурсами. using-объявление (C# 8+) чище — Dispose вызывается в конце области. Реализуйте IDisposable для классов, удерживающих неуправляемые ресурсы (файловые дескрипторы, подключения к БД).

csharp
// using statement (C# 1+)
using (var file = new StreamReader("data.txt")) {
    string content = file.ReadToEnd();
}  // file.Dispose() called automatically

// using declaration (C# 8+)
using var file2 = new StreamReader("data.txt");
string content2 = file2.ReadToEnd();
// Dispose called at end of scope

// Implementing IDisposable
class Resource : IDisposable {
    public void Dispose() {
        // release unmanaged resources
        GC.SuppressFinalize(this);
    }
}

Обработка null

Nullable reference types C# 8+ (string?) включают null-безопасность во время компиляции. ?. возвращает null вместо бросания NullReferenceException. ?? предоставляет запасное значение. throw-выражение (?? throw) кратко для валидации. Включите <Nullable>enable</Nullable> в .csproj для проверки null на уровне проекта.

csharp
string? name = null;

// Null-conditional operator
int? length = name?.Length;  // null (no exception)

// Null-coalescing operator
string display = name ?? "Unknown";  // "Unknown"

// Throw if null
string required = name ?? throw new ArgumentNullException(nameof(name));

// Pattern matching null check
if (name is null) { /* ... */ }
if (name is { Length: > 5 }) { /* not null and length > 5 */ }

Фильтры исключений

Фильтры исключений (when) добавляют условия к catch-блокам — исключение ловится, только если фильтр истинен. Это мощнее, чем ловить и перебрасывать, так как сохраняет стек вызовов. Паттерн логирования (when возвращает false) позволяет наблюдать исключения без их обработки.

csharp
try {
    // ...
} catch (HttpRequestException e) when (e.StatusCode == 404) {
    Console.WriteLine("Not found");
} catch (HttpRequestException e) when (e.StatusCode >= 500) {
    Console.WriteLine("Server error");
} catch (HttpRequestException e) {
    Console.WriteLine($"HTTP error: {e.Message}");
}

// Log without catching
catch (Exception e) when (LogError(e)) {
    // never enters if LogError returns false
}
private bool LogError(Exception e) { /* log */ return false; }
10

Файловый ввод-вывод и async

Класс File (быстрый I/O)

File предоставляет статические методы для быстрых одноразовых файловых операций — простых, но загружающих весь файл в память. Для больших файлов используйте StreamReader/StreamWriter для построчной обработки. File.Exists проверяет существование, но есть TOCTOU-гонка — всегда обрабатывайте исключения от файловых операций.

csharp
using System.IO;

// Read all text at once
string content = File.ReadAllText("input.txt");
string[] lines = File.ReadAllLines("input.txt");

// Write
File.WriteAllText("output.txt", "Hello\n");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });

// Append
File.AppendAllText("log.txt", $"[{DateTime.Now}] Event\n");

// File info
bool exists = File.Exists("data.txt");
var info = new FileInfo("data.txt");
Console.WriteLine(info.Length);  // bytes

Потоки (StreamReader/Writer)

StreamReader/StreamWriter обрабатывают файлы построчно без загрузки всего в память — используйте для больших файлов. Оператор using гарантирует закрытие файла даже при исключении. StreamWriter буферизует данные; вызовите Flush() для немедленной записи или позвольте Dispose обработать это.

csharp
using System.IO;

// Reading line by line
using var reader = new StreamReader("input.txt");
string? line;
while ((line = reader.ReadLine()) != null) {
    Console.WriteLine(line);
}

// Writing
using var writer = new StreamWriter("output.txt");
writer.WriteLine("First line");
writer.WriteLine("Second line");
writer.Flush();  // optional, using disposes and flushes

async / await

async/await включает неблокирующие асинхронные операции. Метод возвращает Task<T> (или Task для void). await приостанавливает метод без блокировки потока — поток свободен для другой работы. Это необходимо для I/O-связанных операций (файлы, сеть, БД) в UI и веб-приложениях.

csharp
async Task<string> ReadFileAsync(string path) {
    using var reader = new StreamReader(path);
    return await reader.ReadToEndAsync();
}

// Calling async method
string content = await ReadFileAsync("data.txt");
Console.WriteLine(content.Length);

// Async doesn't block the calling thread
// The thread is freed while waiting for I/O

Task и Parallel

Task.Run планирует работу в пуле потоков. Task.WhenAll ожидает несколько задач параллельно (не используйте async/await для CPU-связанной работы — используйте Task.Run). Parallel.For — для параллельных CPU-циклов. PLINQ (AsParallel) параллелит LINQ-запросы. Используйте async для I/O, Parallel/PLINQ для CPU.

csharp
// Run work on a thread pool thread
var task = Task.Run(() => HeavyComputation());
int result = await task;

// Run multiple tasks concurrently
var t1 = Task.Run(() => DownloadAsync("url1"));
var t2 = Task.Run(() => DownloadAsync("url2"));
await Task.WhenAll(t1, t2);  // wait for both

// Parallel.For for CPU-bound parallelism
Parallel.For(0, 100, i => {
    Process(i);  // runs on multiple threads
});

// PLINQ
var results = nums.AsParallel()
    .Where(n => IsPrime(n))
    .ToArray();

JSON-сериализация

System.Text.Json — современный высокопроизводительный JSON-сериализатор (заменяет Newtonsoft.Json в большинстве случаев). Serialize/Deserialize обрабатывают объект<->JSON. Используйте JsonSerializerOptions для форматирования (indented, camelCase). Для async используйте stream-методы, чтобы не загружать большой JSON в память.

csharp
using System.Text.Json;

var person = new { Name = "Alice", Age = 30 };

// Serialize
string json = JsonSerializer.Serialize(person);
// {"Name":"Alice","Age":30}

// Deserialize
Person p = JsonSerializer.Deserialize<Person>(json);

// With options
var options = new JsonSerializerOptions {
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string pretty = JsonSerializer.Serialize(person, options);

// Async (for streams)
using var stream = File.OpenRead("data.json");
var data = await JsonSerializer.DeserializeAsync<Person>(stream);
11

Углублённое изучение LINQ

Query vs Method syntax

В LINQ два эквивалентных синтаксиса. Query syntax SQL-подобен и более читаем для сложных join/grouping. Method syntax (fluent) более распространён, поддерживает все операторы и естественно цепляется. Они компилируются в один IL. Используйте query syntax для сложных многоклеточных запросов; method syntax для простых цепочек. Оба ленивы (отложенное выполнение).

csharp
List<int> nums = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Query syntax (SQL-like)
var evens = from n in nums
            where n % 2 == 0
            orderby n descending
            select n * 2;

// Method syntax (fluent) — equivalent
var evens2 = nums.Where(n => n % 2 == 0)
                  .OrderByDescending(n => n)
                  .Select(n => n * 2);

// Query syntax supports: from, where, orderby, select, group, join, let
// Method syntax supports everything query does, plus more

Отложенное vs немедленное выполнение

LINQ использует отложенное выполнение — Where/Select/OrderBy только строят запрос; он выполняется при перечислении. Это значит, результаты отражают состояние источника во время перечисления, а не создания запроса. ToList/ToArray/Count/etc. форсируют немедленное выполнение, делая снимок. Будьте осторожны: перечисление отложенного запроса дважды запускает его дважды. Кэшируйте через ToList, если нужны стабильные результаты.

csharp
var nums = new List<int> { 1, 2, 3 };

// Deferred: query not run until enumerated
var query = nums.Where(n => n > 1);
nums.Add(4);  // query sees this!
Console.WriteLine(query.Count());  // 3 (2, 3, 4)

// Immediate: forces execution now
var list = nums.Where(n => n > 1).ToList();  // runs now
nums.Add(5);
Console.WriteLine(list.Count);  // still 3 (snapshot)

// Operators that force immediate execution:
// ToList, ToArray, ToDictionary, ToLookup,
// Count, Sum, Min, Max, Average, First, Last, Any, All

Группировка и соединение

GroupBy группирует элементы по ключу, возвращая последовательности IGrouping<key, element>. Join выполняет внутренние соединения (сопоставление ключей). Group join (join...into) создаёт иерархические результаты и включает left outer join через DefaultIfEmpty(). Эти операции мощны для анализа данных. Для больших наборов рассмотрите ToLookup для повторных запросов (это предвычисленная группировка).

csharp
var students = new[] {
    new { Name = "Alice", Grade = "A", Dept = "CS" },
    new { Name = "Bob", Grade = "B", Dept = "Math" },
    new { Name = "Carol", Grade = "A", Dept = "CS" },
};

// Group by department
var byDept = students.GroupBy(s => s.Dept);
foreach (var g in byDept) {
    Console.WriteLine(`{g.Key}: {g.Count()}`);
    // CS: 2, Math: 1
}

// Inner join
var courses = new[] { new { Dept = "CS", Course = "Algo" } };
var joined = from s in students
             join c in courses on s.Dept equals c.Dept
             select new { s.Name, c.Course };

// Group join (left outer join with DefaultIfEmpty)
var groupJoin = from s in students
                join c in courses on s.Dept equals c.Dept into sc
                from c in sc.DefaultIfEmpty()
                select new { s.Name, Course = c?.Course };

Агрегация и кванторы

LINQ предоставляет стандартные агрегации (Sum, Min, Max, Average, Count) и универсальный Aggregate для пользовательских редукций (как reduce в JavaScript). Кванторы (Any, All, Contains) возвращают булевы и short-circuit — Any останавливается на первом совпадении, All на первом несовпадении. Используйте Any() (не Count() > 0) для проверки существования — это эффективнее и читаемее.

csharp
var nums = new[] { 1, 2, 3, 4, 5 };

// Aggregations
int sum = nums.Sum();              // 15
double avg = nums.Average();       // 3.0
int min = nums.Min();              // 1
int max = nums.Max();              // 5
int count = nums.Count();          // 5

// Custom aggregation (like reduce/fold)
int product = nums.Aggregate((a, b) => a * b);  // 120
int withSeed = nums.Aggregate(10, (a, b) => a + b);  // 25

// Quantifiers (return bool)
bool anyEven = nums.Any(n => n % 2 == 0);  // true
bool allPos = nums.All(n => n > 0);        // true
bool contains3 = nums.Contains(3);         // true

// LongCount for > int.MaxValue elements
long big = largeList.LongCount();

IEnumerable и IQueryable

IEnumerable<T> — для коллекций в памяти (LINQ to Objects) — использует делегаты, выполняется локально. IQueryable<T> — для удалённых источников (Entity Framework, LINQ to SQL) — использует деревья выражений, транслируется в язык запросов источника (SQL). IQueryable эффективно композирует серверные запросы. Переключайтесь на IEnumerable через AsEnumerable(), когда нужна клиентская логика, которую нельзя транслировать.

csharp
// IEnumerable<T>: in-memory, LINQ to Objects
List<int> nums = new() { 1, 2, 3 };
var q1 = nums.Where(n => n > 1);  // executes in memory

// IQueryable<T>: translates to query language (SQL)
// Expression trees, not delegates
using var db = new MyDbContext();
var q2 = db.Users.Where(u => u.Age > 18);  // generates SQL
// SELECT * FROM Users WHERE Age > 18

// IQueryable composes queries that execute server-side
var query = db.Users
    .Where(u => u.Active)
    .OrderBy(u => u.Name)
    .Take(10);
// All translated to one SQL query

// Force client-side with AsEnumerable()
var clientSide = db.Users.AsEnumerable()
    .Where(u => ExpensiveCheck(u));
12

Углублённое изучение async/await

Task и Task<T>

Task представляет асинхронную операцию без результата; Task<T> возвращает T. ValueTask<T> (C# 7) избегает выделения в куче, когда результат часто доступен синхронно (сценарии кэширования) — но может быть awaited только один раз. Используйте Task.FromResult для синхронных результатов в async-API. Предпочитайте Task вместо void для async-методов (void только для обработчиков событий).

csharp
// Task: represents an async operation (no result)
async Task DoWorkAsync() {
    await Task.Delay(1000);
    Console.WriteLine("Done");
}

// Task<T>: async operation returning T
async Task<int> GetCountAsync() {
    await Task.Delay(500);
    return 42;
}

// ValueTask<T>: lightweight, avoids allocation
ValueTask<int> GetCachedAsync() {
    if (_cache != null) return new ValueTask<int>(_cache);
    return new ValueTask<int>(LoadFromDbAsync());
}

// Converting to Task
Task t = Task.Run(() => Console.WriteLine("hi"));
Task<int> t2 = Task.FromResult(42);  // already complete
Task completed = Task.CompletedTask;

CancellationToken

CancellationToken включает кооперативную отмену. Передавайте в async-методы и проверяйте ThrowIfCancellationRequested() в циклах. Вызывающий отменяет через CancellationTokenSource.Cancel(); метод решает, когда/как ответить. Всегда принимайте параметр CancellationToken в async-API (особенно в коде библиотек). Используйте CancellationTokenSource с TimeSpan для таймаутов. Отмена бросает OperationCanceledException.

csharp
async Task DownloadAsync(string url, CancellationToken ct) {
    using var client = new HttpClient();
    // Pass token to async operations
    var data = await client.GetByteArrayAsync(url, ct);
    // Check manually in loops
    foreach (var chunk in data) {
        ct.ThrowIfCancellationRequested();
        Process(chunk);
    }
}

// Usage with timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try {
    await DownloadAsync("https://example.com", cts.Token);
} catch (OperationCanceledException) {
    Console.WriteLine("Timed out or cancelled");
}

// Cooperative cancellation: caller cancels, method checks
cts.Cancel();  // request cancellation

Task.WhenAll vs WhenAny

Task.WhenAll ожидает несколько задач параллельно и продолжается, когда все завершены (параллелизм). Task.WhenAny продолжается, когда первая задача завершена (гонки/избыточность). WhenAll по умолчанию бросает только первое исключение; инспектируйте IsFaulted/Exception каждой задачи, чтобы увидеть все сбои. Для fire-and-forget используйте Task.Run и обрабатывайте исключения внутри, чтобы избежать незамеченных исключений.

csharp
// WhenAll: wait for ALL to complete (like Promise.all)
var tasks = new[] {
    FetchAsync("url1"),
    FetchAsync("url2"),
    FetchAsync("url3"),
};
string[] results = await Task.WhenAll(tasks);
// Continues when all 3 done; throws AggregateException if any fails

// WhenAny: wait for FIRST to complete (race)
var racing = new[] { PrimaryAsync(), FallbackAsync() };
Task<string> firstDone = await Task.WhenAny(racing);
string result = await firstDone;

// WhenAll with exception handling
try {
    await Task.WhenAll(tasks);
} catch (Exception ex) {
    // Only first exception is thrown; access all via Task.Exception
    foreach (var inner in tasks.Where(t => t.IsFaulted))
        Console.WriteLine(inner.Exception);
}

Асинхронные потоки (IAsyncEnumerable)

IAsyncEnumerable<T> (C# 8) — асинхронный поток — выдаёт элементы по мере доступности с await между yield'ами. Потребляется через await foreach. Идеально для потоковой передачи больших наборов данных или данных реального времени без буферизации всего в памяти. Атрибут [EnumeratorCancellation] обеспечивает корректное прохождение токена отмены при потреблении потока с WithCancellation.

csharp
// C# 8: async iterator (like async generator)
async IAsyncEnumerable<int> GenerateAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int i = 0; i < 10; i++) {
        await Task.Delay(100, ct);
        yield return i;  // async yield
    }
}

// Consume with await foreach
await foreach (int n in GenerateAsync().WithCancellation(ct)) {
    Console.WriteLine(n);
}

// Use case: streaming data from a database or API
async IAsyncEnumerable<Record> StreamRecordsAsync() {
    await using var reader = await cmd.ExecuteReaderAsync();
    while (await reader.ReadAsync())
        yield return MapRecord(reader);
}

Ловушки async (взаимные блокировки)

Ловушка async №1: вызов .Result или .Wait() на Task может вызвать взаимную блокировку при наличии SynchronizationContext (UI-приложения, legacy ASP.NET). await захватывает контекст; .Result блокирует поток, нужный продолжению. Исправление: сделайте методы async до самого верха или используйте ConfigureAwait(false) в коде библиотек. async void опасен — исключения нельзя поймать, и его нельзя await. Используйте async Task.

csharp
// BAD: .Result blocks (can deadlock in UI/ASP.NET classic)
public string BadMethod() {
    string data = GetDataAsync().Result;  // DEADLOCK risk!
    return data;
}

// GOOD: async all the way
public async Task<string> GoodMethodAsync() {
    string data = await GetDataAsync();
    return data;
}

// Why deadlock: in UI/ASP.NET context, await captures
// SynchronizationContext. .Result blocks the thread,
// but the continuation needs that same thread → deadlock.

// Fix if you MUST block (last resort):
string data = Task.Run(GetDataAsync).Result;  // offloads
// Or ConfigureAwait(false) in library code:
await GetDataAsync().ConfigureAwait(false);

// async void is EVIL (except event handlers)
async void Bad() { await Task.Delay(1); throw new Exception(); }
// Exception can't be caught by caller!
13

События и делегаты

Основы делегатов

Делегаты — типобезопасные указатели на функции. Пользовательские делегаты (delegate int MathOp(int,int)) в значительной степени заменены встроенными Action (возврат void) и Func<T> (возвращает T). Делегаты — multicast, они могут содержать несколько методов (комбинируются через +). При вызове выполняются все методы. Возвращаемое значение multicast-делегата — результат последнего метода. Используйте Action/Func для большинства современного кода.

csharp
// Declare a delegate type
public delegate int MathOp(int a, int b);

// Methods matching the signature
static int Add(int a, int b) => a + b;
static int Mul(int a, int b) => a * b;

MathOp op = Add;
Console.WriteLine(op(2, 3));  // 5
op = Mul;
Console.WriteLine(op(2, 3));  // 6

// Multicast: combine delegates
MathOp multi = (MathOp)Add + Mul;
// Note: returns last result (Add's), but both run
// (return value of multicast is the last one's)

// Built-in: Action (void), Func<T> (returns T)
Action<string> log = s => Console.WriteLine(s);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0;  // returns bool

События (Publisher/Subscriber)

События — ограниченная форма делегатов — только объявляющий класс может их вызывать, но внешний код может подписываться/отписываться (+=/-=). Используйте EventHandler<TArgs> для стандартного паттерна. Всегда используйте null-conditional оператор (Click?.Invoke), так как событие без подписчиков — null. Всегда отписывайтесь для предотвращения утечек памяти (событие содержит сильную ссылку на обработчик).

csharp
public class Button {
    // Event: restricted multicast delegate
    public event EventHandler<EventArgs> Click;

    public void SimulateClick() {
        // Null-conditional invoke pattern (thread-safe)
        Click?.Invoke(this, EventArgs.Empty);
    }
}

// Custom event args
public class SearchEventArgs : EventArgs {
    public string Query { get; init; }
}

// Subscribe
var btn = new Button();
btn.Click += (sender, e) => Console.WriteLine("Clicked!");
btn.Click += HandlerMethod;  // method group

// Unsubscribe (MUST to avoid memory leaks)
btn.Click -= HandlerMethod;

void HandlerMethod(object sender, EventArgs e) { /* ... */ }

INotifyPropertyChanged (привязка данных)

INotifyPropertyChanged — стандартный паттерн для привязки данных в WPF, MAUI и WinForms. При изменении свойства вызывайте PropertyChanged, чтобы связанный UI обновился. [CallerMemberName] (C# 5) автоматически передаёт имя вызывающего свойства — без магических строк. Проверка (if != value) предотвращает ненужные уведомления. ViewModel'и используют это активно; source generators (CommunityToolkit.Mvvm) могут авто-генерировать этот шаблонный код.

csharp
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ViewModel : INotifyPropertyChanged {
    private string _name;
    public string Name {
        get => _name;
        set {
            if (_name != value) {
                _name = value;
                OnPropertyChanged();  // raises event
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    // CallerMemberName auto-fills the property name
    protected void OnPropertyChanged([CallerMemberName] string prop = null) {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
    }
}

// WPF/MAUI bindings automatically update UI when Name changes

Событие vs делегат

Ключевое различие: публичное поле-делегат может быть вызвано, переназначено или очищено кем угодно. Событие ограничивает внешний доступ только += (подписка) и -= (отписка) — только объявляющий класс может вызывать или очищать его. Эта инкапсуляция — причина, по которой события стандарт для паттерна observer. События также генерируют потокобезопасные add/remove-аксессоры и интегрируются с инструментами дизайнера.

csharp
// Public delegate: anyone can invoke or clear
public delegate void Notify();
public class Bad {
    public Notify OnDone;  // field-like, public
}
// External code can do:
//   bad.OnDone();        // invoke (BAD: not the owner)
//   bad.OnDone = null;   // clear all subscribers (BAD)

// Event: only owner can invoke/clear
public class Good {
    public event Notify OnDone;  // restricted
    public void Complete() {
        OnDone?.Invoke();  // OK: owner invokes
    }
}
// External code can ONLY += or -= (subscribe/unsubscribe)
// Cannot invoke or clear from outside

// Event = delegate + access restriction
// Under the hood: event generates add/remove accessors

Слабые события (предотвращение утечек памяти)

События содержат сильные ссылки на подписчиков, вызывая утечки памяти, если подписчик должен быть собран сборщиком мусора, но издатель живёт. Решения: явно отписывайтесь (лучше), используйте WeakEventManager (WPF) или реализуйте weak event pattern через WeakReference. Это частый источник утечек в долгоживущих приложениях (серверы, десктоп). Всегда отписывайтесь в Dispose или при завершении жизни подписчика.

csharp
// Problem: event holds strong ref to subscriber → leak
// if subscriber should be GC'd but publisher lives long

// Solution 1: WeakEventManager (WPF)
WeakEventManager<Publisher, EventArgs>.AddHandler(pub, "Event", Handler);

// Solution 2: WeakReference in custom pattern
public class WeakEvent<TArgs> where TArgs : EventArgs {
    private List<WeakReference<EventHandler<TArgs>>> _handlers = new();
    public void Subscribe(EventHandler<TArgs> h) =>
        _handlers.Add(new WeakReference<EventHandler<TArgs>>(h));
    public void Raise(object sender, TArgs args) {
        _handlers.RemoveAll(wr => !wr.TryGetTarget(out _));
        foreach (var wr in _handlers) {
            if (wr.TryGetTarget(out var h)) h(sender, args);
        }
    }
}

// Best practice: unsubscribe (-=) when done, or use
// IDisposable pattern with weak references for long-lived publishers
14

Рефлексия

Информация о типе

Рефлексия инспектирует метаданные типа во время выполнения. typeof(T) получает Type во время компиляции; obj.GetType() — во время выполнения. Type предоставляет Name, IsClass, IsValueType и методы для перечисления членов (GetMethods, GetProperties, GetFields). BindingFlags контролируют возвращаемое (NonPublic, Instance, Static, Public). Рефлексия мощна, но медленна — кэшируйте результаты, когда возможно.

csharp
using System;

Type t = typeof(string);
// Or: Type t = "hello".GetType();

Console.WriteLine(t.Name);          // String
Console.WriteLine(t.FullName);      // System.String
Console.WriteLine(t.IsClass);       // true
Console.WriteLine(t.IsValueType);   // false
Console.WriteLine(t.IsAbstract);    // false

// Get methods
foreach (var m in t.GetMethods())
    Console.WriteLine(m.Name);

// Get properties
var props = t.GetProperties();

// Get fields
var fields = t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

// Check if assignable
Type ienum = typeof(IEnumerable<int>);
Console.WriteLine(ienum.IsAssignableFrom(t));  // false

Инстанцирование и вызов

Activator.CreateInstance создаёт объекты динамически. MethodInfo.Invoke вызывает методы через рефлексию (медленно из-за упаковки аргументов и проверок безопасности). Для повторных вызовов CreateDelegate намного быстрее — создаёт строго типизированный делегат, обходящий накладные расходы рефлексии. Рефлексия нарушает типобезопасность времени компиляции и медленнее прямых вызовов; используйте для фреймворков (сериализация, DI, ORM) и сценариев кэширования.

csharp
using System.Reflection;

// Create instance
Type t = typeof(StringBuilder);
object obj = Activator.CreateInstance(t);
// Or with args: Activator.CreateInstance(t, "initial")

// Invoke method
MethodInfo method = t.GetMethod("Append", new[] { typeof(string) });
object result = method.Invoke(obj, new object[] { "hello" });

// Get/set property
PropertyInfo prop = t.GetProperty("Length");
int len = (int)prop.GetValue(obj);

// Get/set field
FieldInfo field = t.GetField("_field", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(obj, 42);

// Create delegate from MethodInfo (faster than Invoke)
var appendDel = (Func<StringBuilder, string, StringBuilder>)
    method.CreateDelegate(typeof(Func<StringBuilder, string, StringBuilder>));

Атрибуты

Атрибуты прикрепляют метаданные к элементам кода (классы, методы, свойства). Определяются через [AttributeUsage(...)] с указанием допустимых целей. Извлекаются через GetCustomAttributes<T>() (generic, современно) или Attribute.GetCustomAttribute. Встроенные атрибуты: [Obsolete], [Serializable], [DllImport], [Conditional]. Фреймворки используют атрибуты широко: [HttpGet] (ASP.NET), [JsonProperty] (JSON), [Table] (EF Core).

csharp
// Define an attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class AuthorAttribute : Attribute {
    public string Name { get; }
    public AuthorAttribute(string name) => Name = name;
}

// Apply
[Author("Alice")]
[Author("Bob")]
public class Widget {
    [Author("Charlie")]
    public void DoWork() { }
}

// Read via reflection
var attrs = typeof(Widget).GetCustomAttributes<AuthorAttribute>();
foreach (var a in attrs) Console.WriteLine(a.Name);  // Alice, Bob

// Check if attribute is applied
bool hasAuthor = Attribute.IsDefined(typeof(Widget), typeof(AuthorAttribute));

Emit и динамическая генерация кода

Reflection.Emit генерирует IL во время выполнения для максимальной производительности в динамических сценариях (компиляция выражений, сериализаторы, mock-фреймворки). Вы строите типы/методы и emits IL-опкоды напрямую. Это продвинуто — используйте деревья выражений (Expression.Compile) для более простой динамической генерации кода. Emit используется DLR (динамические языки), ORM и библиотеками сериализации для генерации быстрого типизированного кода во время выполнения.

csharp
using System.Reflection;
using System.Reflection.Emit;

// Create a dynamic assembly/module/type
var asmName = new AssemblyName("Dynamic");
var asm = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
var mod = asm.DefineDynamicModule("Main");
var type = mod.DefineType("Calculator", TypeAttributes.Public);

// Add a method
var method = type.DefineMethod("Add",
    MethodAttributes.Public | MethodAttributes.Static,
    typeof(int), new[] { typeof(int), typeof(int) });

// Emit IL
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);  // load first arg
il.Emit(OpCodes.Ldarg_1);  // load second arg
il.Emit(OpCodes.Add);      // add
il.Emit(OpCodes.Ret);      // return

Type created = type.CreateType();
int result = (int)created.GetMethod("Add").Invoke(null, new object[] { 3, 4 });  // 7

Деревья выражений

Деревья выражений представляют код как данные (дерево узлов Expression). Это основа IQueryable (LINQ-провайдеры транслируют их в SQL и т.д.). Compile() превращает выражение в выполнимый делегат. Можно строить выражения вручную для динамической генерации кода (читаемее, чем Reflection.Emit). Деревья выражений включают ORM, rule engines и динамический LINQ.

csharp
using System.Linq.Expressions;

// Expression<Func<T>>: code as data (tree), not executable
Expression<Func<int, int, int>> expr = (a, b) => a + b + 1;

// Inspect the tree
Console.WriteLine(expr.Body.NodeType);  // Add
Console.WriteLine(expr.Body.Type);      // int

// Compile to a delegate (fast, runs in memory)
Func<int, int, int> compiled = expr.Compile();
int result = compiled(2, 3);  // 6

// EF Core translates expressions to SQL
Expression<Func<User, bool>> filter = u => u.Age > 18;
// → WHERE Age > 18

// Build expressions manually (advanced)
ParameterExpression x = Expression.Parameter(typeof(int), "x");
Expression body = Expression.Multiply(x, Expression.Constant(2));
var lambda = Expression.Lambda<Func<int, int>>(body, x);
Func<int, int> doubler = lambda.Compile();
15

Методы расширения и паттерны

Методы расширения

Методы расширения позволяют 'добавлять' методы к существующим типам без их модификации. Определяются в статическом классе с 'this' на первом параметре. Это синтаксический сахар — компилятор переписывает email.IsEmail() в StringExtensions.IsEmail(email). LINQ полностью состоит из методов расширения на IEnumerable. Используйте расширения для добавления utility-методов к sealed-классам, интерфейсам или сторонним типам. Подключайте через using.

csharp
public static class StringExtensions {
    // Must be static, in static class, first param with 'this'
    public static bool IsEmail(this string s) =>
        s.Contains('@') && s.Contains('.');

    public static string Repeat(this string s, int n) =>
        string.Concat(Enumerable.Repeat(s, n));

    // Chainable
    public static string Truncate(this string s, int max) =>
        s.Length <= max ? s : s[..max];
}

// Usage: appears as instance method
string email = "[email protected]";
bool valid = email.IsEmail();
string repeated = "ab".Repeat(3);  // "ababab"
string short_ = "hello world".Truncate(5);  // "hello"

// LINQ is built on extension methods (Enumerable class)
var evens = nums.Where(n => n % 2 == 0);  // extension

Fluent-интерфейсы

Fluent-интерфейсы возвращают 'this', позволяя chaining методов, создавая читаемый код, читающийся как предложение. Распространено в builder'ах (QueryBuilder), конфигурации (ASP.NET, EF) и DSL. Методы расширения могут добавлять fluent-методы к примитивным типам (5.Seconds()). Держите методы чистыми (возвращайте новый экземпляр) для неизменяемых fluent-API, или мутируйте и возвращайте this для builder'ов.

csharp
public class QueryBuilder {
    private string _table = "";
    private string _where = "";
    private int _limit;

    public QueryBuilder From(string table) { _table = table; return this; }
    public QueryBuilder Where(string clause) { _where = clause; return this; }
    public QueryBuilder Limit(int n) { _limit = n; return this; }

    public string Build() =>
        $"SELECT * FROM {_table} WHERE {_where} LIMIT {_limit}";
}

// Fluent chaining
var sql = new QueryBuilder()
    .From("users")
    .Where("age > 18")
    .Limit(10)
    .Build();

// Extension methods enable fluent APIs on existing types
public static class IntExtensions {
    public static TimeSpan Seconds(this int n) => TimeSpan.FromSeconds(n);
    public static TimeSpan Minutes(this int n) => TimeSpan.FromMinutes(n);
}
var delay = 5.Seconds();  // TimeSpan
var timeout = 2.Minutes();

Методы-итераторы (yield)

yield return превращает метод в итератор — компилятор генерирует конечный автомат. Выполнение лениво: код выполняется только при извлечении элементов. Это включает бесконечные последовательности и эффективную потоковую передачу (обработка по одному элементу без буферизации). yield break завершает итерацию. Метод должен возвращать IEnumerable<T> или IEnumerator<T>. Операторы LINQ построены на yield. Повторное перечисление повторно запускает метод.

csharp
// yield return: lazy iterator (state machine generated)
public static IEnumerable<int> Evens(int max) {
    for (int i = 0; i <= max; i += 2)
        yield return i;
}

// Lazy: nothing runs until enumerated
var evens = Evens(100);  // no execution yet
foreach (var e in evens) { /* runs here */ }

// yield break: end iteration
public static IEnumerable<T> TakeWhile<T>(IEnumerable<T> src, Func<T, bool> pred) {
    foreach (var item in src) {
        if (!pred(item)) yield break;
        yield return item;
    }
}

// Infinite sequence (lazy, safe)
public static IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
var first10 = Naturals().Take(10);  // 0..9

Внедрение зависимостей

Внедрение зависимостей (DI) передаёт зависимости через конструкторы, а не создаёт их внутри. Преимущества: тестируемость (подмена реализаций в тестах), слабая связанность, единая ответственность. Встроенный DI-контейнер .NET (Microsoft.Extensions.DependencyInjection) обрабатывает регистрацию и время жизни (AddTransient, AddScoped, AddSingleton). Всегда зависите от интерфейсов, а не конкретных классов, для гибкости.

csharp
// Interface for the dependency
public interface IEmailSender {
    Task SendAsync(string to, string body);
}

// Implementation
public class SmtpEmailSender : IEmailSender {
    public async Task SendAsync(string to, string body) { /* SMTP */ }
}

// Consumer: depends on abstraction, not concretion
public class UserService {
    private readonly IEmailSender _email;
    public UserService(IEmailSender email) => _email = email;  // inject

    public async Task RegisterAsync(string email) {
        // ... save user ...
        await _email.SendAsync(email, "Welcome!");
    }
}

// Register in DI container (ASP.NET Core)
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<UserService>();

// Resolve
var userService = app.Services.GetRequiredService<UserService>();

Паттерны Factory и Builder

Паттерн Factory централизует создание объектов, скрывая конкретные классы от вызывающих (полезно, когда создание сложно или тип выбирается во время выполнения). Паттерн Builder строит сложные объекты пошагово с fluent-методами, избегая 'телескопических конструкторов' (много параметров). Оба улучшают читаемость и поддерживаемость. Records в C# с выражениями 'with' часто заменяют builder'ы для неизменяемых данных.

csharp
// Factory: create objects without specifying exact class
public interface IShape { void Draw(); }
public class Circle : IShape { public void Draw() => Console.WriteLine("○"); }
public class Square : IShape { public void Draw() => Console.WriteLine("□"); }

public static class ShapeFactory {
    public static IShape Create(string type) => type switch {
        "circle" => new Circle(),
        "square" => new Square(),
        _ => throw new ArgumentException()
    };
}

// Builder: step-by-step construction of complex objects
public class Pizza {
    public string Size { get; set; }
    public bool Cheese { get; set; }
    public bool Pepperoni { get; set; }
}
public class PizzaBuilder {
    private readonly Pizza _p = new();
    public PizzaBuilder WithSize(string s) { _p.Size = s; return this; }
    public PizzaBuilder AddCheese() { _p.Cheese = true; return this; }
    public PizzaBuilder AddPepperoni() { _p.Pepperoni = true; return this; }
    public Pizza Build() => _p;
}

var pizza = new PizzaBuilder()
    .WithSize("large").AddCheese().AddPepperoni().Build();
16

Pattern matching (C# 7-10+)

Type-паттерны и is

Pattern matching (C# 7+) объединяет проверку типа и привязку переменной за один шаг: 'o is string s' проверяет и присваивает. Property-паттерны ({ Length: > 3 }) сопоставляют свойства объекта. C# 9 добавляет комбинаторы 'not', 'and', 'or'. Switch-выражения (C# 8) возвращают значения и используют паттерны — гораздо чище цепочек if-else. _ — discard-паттерн (случай по умолчанию).

csharp
object o = "hello";

// Type pattern (C# 7)
if (o is string s) {
    Console.WriteLine(s.Length);  // s is string here
}

// Combined with null check
if (o is string { Length: > 3 } longStr) {
    Console.WriteLine(longStr);  // string with length > 3
}

// Negation and conjunction (C# 9)
if (o is not null) { /* ... */ }
if (o is string and not null) { /* ... */ }

// Switch on type
string Describe(object o) => o switch {
    int i => $"Integer: {i}",
    string s => $"String: {s}",
    double d => $"Double: {d}",
    null => "null",
    _ => "Unknown"
};

Property и позиционные паттерны

Property-паттерны сопоставляют свойства объекта: { Prop: pattern }. Позиционные паттерны деконструируют через метод Deconstruct (records имеют это автоматически). Паттерны компонуются: можно вкладывать и комбинировать с реляционными операторами (<, >, etc.). C# 10 допускает сокращение вложенных свойств (Name.Length вместо Name: { Length: }). Pattern matching делает сложную условную логику декларативной и читаемой.

csharp
public record Point(int X, int Y);
public record Person(string Name, int Age);

// Property pattern
string Describe(Person p) => p switch {
    { Age: < 18 } => "Minor",
    { Age: >= 65 } => "Senior",
    { Name.Length: > 10 } => "Long name",
    { } => "Adult"  // non-null Person
};

// Positional pattern (deconstruction)
string Quadrant(Point p) => p switch {
    (0, 0) => "Origin",
    ( > 0, > 0) => "Q1",
    ( < 0, > 0) => "Q2",
    ( < 0, < 0) => "Q3",
    ( > 0, < 0) => "Q4",
    _ => "On axis"
};

// Nested patterns
bool IsValid(Person p) => p is { Name: { Length: > 0 }, Age: > 0 };
// C# 10: nested property shorthand
bool IsValid2(Person p) => p is { Name.Length: > 0, Age: > 0 };

Реляционные и логические паттерны (C# 9)

C# 9 представил реляционные паттерны (<, >, <=, >=) и логические комбинаторы (and, or, not) для паттернов. Они делают switch-выражения выразительными для проверок диапазона и категориальной логики. 'or' комбинирует паттерны (совпадает, если любой); 'and' требует оба; 'not' отрицает. Скобки группируют для приоритета. Это заменяет многословные цепочки if-else декларативной, исчерпывающей (проверяемой компилятором) логикой.

csharp
// Relational patterns: <, >, <=, >=
string Classify(int n) => n switch {
    < 0 => "Negative",
    0 => "Zero",
    > 0 and < 10 => "Small positive",
    >= 10 and < 100 => "Medium",
    >= 100 => "Large"
};

// Logical patterns: and, or, not
bool IsWeekend(DayOfWeek d) => d is DayOfWeek.Saturday or DayOfWeek.Sunday;

// Combined with type patterns
decimal GetDiscount(object customer) => customer switch {
    Student => 0.2m,
    Senior or Veteran => 0.15m,
    not null => 0.0m,
    null => throw new ArgumentNullException()
};

// Parenthesized patterns for grouping
bool IsValid(int n) => n is (> 0 and < 10) or (> 100 and < 200);

List-паттерны (C# 11)

List-паттерны (C# 11) сопоставляют форму массивов и списков: пустой [], один элемент [x], точное [a, b] или со срезами [first, .., last]. Slice-паттерн (..) захватывает ноль или более средних элементов в переменную. В комбинации с var и guards (when) это обеспечивает выразительное сопоставление последовательностей. Полезно для парсинга, валидации и реализации алгоритмов.

csharp
// List patterns match array/list shapes
int[] nums = { 1, 2, 3, 4, 5 };

string Describe(int[] arr) => arr switch {
    [] => "Empty",
    [single] => $"Single: {single}",
    [first, second] => $"Pair: {first}, {second}",
    [first, .., last] => $"First: {first}, Last: {last}",  // slice
    [.., var last] => $"Last: {last}",
    _ => "Other"
};

// Slice pattern (..) captures middle elements
if (nums is [var first, .. var middle, var last]) {
    Console.WriteLine(`First {first}, middle count {middle.Length}, last {last}`);
}

// With guards
string Classify(int[] arr) => arr switch {
    [var a, var b] when a == b => "Equal pair",
    [var a, var b] => "Unequal pair",
    _ => "Other"
};

Switch-выражения

Switch-выражения (C# 8) — это выражения, возвращающие значения, в отличие от switch-операторов. Они используют pattern matching (=>) и более кратки. Tuple-switching ((a, b) switch) обрабатывает несколько значений. Компилятор проверяет исчерпываемость для enum (предупреждает, если случай пропущен). Включайте default (_), если не хотите исключение во время выполнения. Switch-выражения предпочтительнее операторов для логики, возвращающей значения.

csharp
// Traditional switch statement (verbose)
string Old(int n) {
    switch (n) {
        case 1: return "one";
        case 2: return "two";
        default: return "many";
    }
}

// Switch expression (C# 8): expression, returns value
string New(int n) => n switch {
    1 => "one",
    2 => "two",
    _ => "many"
};

// With multiple values per arm
string Describe(int day, bool holiday) => (day, holiday) switch {
    (6 or 7, _) => "Weekend",
    (_, true) => "Holiday",
    (1, false) => "Monday",
    _ => "Weekday"
};

// Exhaustiveness: compiler warns if not all cases covered
// (especially with enums)
17

Records и неизменяемость

Основы Record (C# 9)

Records (C# 9) — ссылочные типы с равенством по значению — две записи равны, если их данные равны (в отличие от классов, использующих ссылочное равенство). Компилятор генерирует Equals, GetHashCode, ToString и Deconstruct автоматически. Выражение 'with' создаёт копию с модифицированными свойствами (неразрушающая мутация). Records идеальны для DTO, value objects и неизменяемых моделей данных.

csharp
// Record: value-equality, immutable, concise
public record Person(string Name, int Age);

// Create
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);

// Value equality (auto-generated)
Console.WriteLine(p1 == p2);  // true (compares properties)
Console.WriteLine(p1.Equals(p2));  // true

// ToString (auto-generated, readable)
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

// Deconstruction
var (name, age) = p1;

// Non-destructive mutation (with-expression)
var older = p1 with { Age = 31 };
// p1 is unchanged, older is a copy with Age = 31

Record structs (C# 10)

C# 10 добавил record structs (типы значений с возможностями record) и readonly record structs (неизменяемые типы значений). Выбор: record class (ссылочный тип, для крупных/разделяемых данных), record struct (тип значения, для небольших данных, избегает выделения в куче), readonly record struct (неизменяемый тип значения, безопаснее). Record structs имеют равенство по значению и поддержку 'with' как record classes. Используйте readonly record struct для небольших неизменяемых значений вроде Point, Money.

csharp
// record class (default, reference type)
public record PersonRef(string Name, int Age);

// record struct (C# 10): value type with record features
public record struct Point(int X, int Y);

// readonly record struct: fully immutable value type
public readonly record struct Money(decimal Amount, string Currency);

// Mutable record struct (allowed but discouraged)
public record struct Counter(int Value) {
    public int Value { get; set; } = Value;
}

var p = new Point(1, 2);
var p2 = p with { X = 5 };  // copy with X=5
Console.WriteLine(p == p2);  // false

// Records can have additional members
public record Person(string Name) {
    public int Age { get; init; }  // extra init-only prop
    public string DisplayName => Name.ToUpper();
}

Init-only setters

init-only setters (C# 9) разрешают установку во время инициализации объекта, но не после — неизменяемость после конструирования без шаблонного конструктора. 'required' (C# 11) принуждает вызывающего устанавливать свойство в инициализаторе (compile-time проверка). Records используют init setters по умолчанию. Это включает неизменяемые паттерны объектов с синтаксисом object-initializer, который читаемее параметров конструктора.

csharp
public class Config {
    // init: settable only during construction (object initializer)
    public string Name { get; init; } = "";
    public int Timeout { get; init; } = 30;
}

var c = new Config { Name = "MyApp", Timeout = 60 };
// c.Name = "Other";  // ERROR: init-only after construction

// Required properties (C# 11): must be set in initializer
public class User {
    public required string Email { get; init; }
    public string? DisplayName { get; init; }
}

// Must set Email, DisplayName optional
var u = new User { Email = "[email protected]" };

// Records use init by default (positional parameters)
public record Point(int X, int Y);
// Equivalent to: public int X { get; init; }
//                public int Y { get; init; }

Наследование records

Records поддерживают наследование: производная record включает параметры базовой record в конструктор. Выражение 'with' сохраняет тип времени выполнения (создаёт Dog из Dog, не Animal). Проверка равенства проверяет тип времени выполнения — две записи равны, только если они одного типа с равными данными. Это делает records корректно работающими в полиморфных коллекциях, в отличие от наивного равенства по значению.

csharp
// Base record
public record Animal(string Name);

// Derived record: must include base parameters
public record Dog(string Name, string Breed) : Animal(Name);

// Derived can add to base
public record Teacher(string Name, string Subject) : Person(Name);

var d = new Dog("Rex", "Labrador");
Console.WriteLine(d);  // Dog { Name = Rex, Breed = Labrador }

// with preserves the runtime type
var d2 = d with { Name = "Buddy" };
Console.WriteLine(d2.GetType().Name);  // Dog (not Animal)

// Value equality across hierarchy
Animal a = new Dog("Rex", "Lab");
Animal b = new Dog("Rex", "Lab");
Console.WriteLine(a == b);  // true

// Different types are not equal even with same data
Animal c = new Animal("Rex");
Console.WriteLine(a == c);  // false (different runtime types)

Records vs классы vs structs

Классы используют ссылочное равенство (== сравнивает ссылки); records — равенство по значению (сравнивает данные); structs используют равенство по значению по умолчанию, но являются типами значений (копируются при присваивании). Используйте records для неизменяемых данных с value-семантикой (DTO, value objects, сообщения). Используйте классы для изменяемых сущностей с идентичностью (User, Order) и иерархий наследования. Используйте structs для небольших (≤16 байт) значений, которые должны копироваться (Point, Color, Money).

csharp
// Class: reference type, reference equality
public class PersonClass {
    public string Name { get; set; }
    public PersonClass(string name) => Name = name;
}
var c1 = new PersonClass("Alice");
var c2 = new PersonClass("Alice");
Console.WriteLine(c1 == c2);  // false (different references)

// Record: reference type, value equality
public record PersonRec(string Name);
var r1 = new PersonRec("Alice");
var r2 = new PersonRec("Alice");
Console.WriteLine(r1 == r2);  // true (value equality)

// Struct: value type, value equality (manual)
public struct PointStruct {
    public int X, Y;
    public PointStruct(int x, int y) { X = x; Y = y; }
}
var s1 = new PointStruct(1, 2);
var s2 = new PointStruct(1, 2);
Console.WriteLine(s1.Equals(s2));  // true (default struct equality)

// Choose:
// - record: immutable data with value equality (DTOs, value objects)
// - class: mutable, reference semantics, inheritance (entities, services)
// - struct: small, value type, avoid heap allocation (Point, Color)
18

Углублённое pattern matching

Property-паттерны

Property-паттерны сопоставляют свойства объекта inline. Switch-выражение возвращает значение напрямую. Комбинированные паттерны (and/or) создают гибкие условия. null и _ (discard) обрабатывают крайние случаи. Это заменяет многословные цепочки if-else декларативным, читаемым кодом.

csharp
static decimal CalcDiscount(Customer c) => c switch
{
    { IsPremium: true, Orders.Count: > 100 } => 0.20m,
    { IsPremium: true } => 0.10m,
    { Orders.Count: > 50 } => 0.05m,
    { Country: "US" or "CA" } => 0.03m,
    null => 0m,
    _ => 0m
};

List-паттерны

List-паттерны (C# 11) сопоставляют массивы и индексируемые последовательности. [] совпадает с пустым, [single] с одним элементом, [first, .. rest] использует slice-паттерн для захвата оставшихся элементов. Полезно для парсинга команд, валидации последовательностей и рекурсивных алгоритмов.

csharp
int Sum(int[] n) => n switch
{
    [] => 0,
    [int single] => single,
    [int first, int second] => first + second,
    [int first, .. int[] rest] => first + Sum(rest),
};

// Matching array shapes
var arr = new[] { 1, 2, 3, 4 };
// [1, ..] matches array starting with 1
// [.., 4] matches array ending with 4
// [1, 2, .., 4] matches 1,2,anything,4

Tuple-паттерны

Tuple-паттерны сопоставляют несколько значений одновременно. Каждый случай проверяет кортеж значений. В комбинации с реляционными паттернами (>, <, >=) и логическими паттернами (and, or, not) это создаёт мощное многомерное сопоставление без вложенных if-операторов.

csharp
static string Classify(int t, bool rain) => (t, rain) switch
{
    (< 0, _) => "Freezing",
    (>= 0 and < 15, true) => "Cold and wet",
    (>= 0 and < 15, false) => "Cold",
    (>= 15 and < 25, _) => "Mild",
    (>= 25, true) => "Hot and wet",
    (>= 25, false) => "Hot",
};

Type-паттерны и guards

Type-паттерны сопоставляют типы времени выполнения. Предложение when добавляет дополнительные guards. var совпадает с чем угодно (включая null). Порядок важен: более специфичные паттерны должны идти перед общими. Это идиоматичный способ type-based диспетчеризации в современном C#.

csharp
object Process(object input) => input switch
{
    int i when i > 0 => $"Positive: {i}",
    int i => $"Non-positive: {i}",
    string s when s.Length > 10 => s.Substring(0, 10),
    string s => s,
    IList list => $"List with {list.Count} items",
    var x => x?.ToString() ?? "null"
};

Вложенные паттерны

Паттерны вкладываются на произвольную глубину. var внутри паттерна захватывает совпавшее значение для использования в guards или теле выражения. Этот пример сопоставляет свойства Point внутри records Line. Вложенные паттерны мощны для валидации сложных графов объектов кратко.

csharp
record Point(int X, int Y);
record Line(Point Start, Point End);

string Describe(Line line) => line switch
{
    Line({ X: 0, Y: 0 }, { X: 0, Y: 0 }) => "Point at origin",
    Line({ X: var x1, Y: var y1 }, { X: var x2, Y: var y2 })
        when x1 == x2 && y1 == y2 => "Degenerate line",
    Line({ X: var x1 }, { X: var x2 }) when x1 == x2 => "Vertical",
    Line({ Y: var y1 }, { Y: var y2 }) when y1 == y2 => "Horizontal",
    _ => "Diagonal line"
};
19

Records и with-выражения

Равенство по значению record

Records предоставляют равенство по значению по умолчанию: два экземпляра с одинаковыми данными равны. Компилятор генерирует Equals, GetHashCode, ToString и оператор ==. Records — ссылочные типы, но спроектированы для неизменяемости. Используйте их для DTO, value objects и данных, которые должны сравниваться по содержимому.

csharp
public record Person(string Name, int Age);

var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine(p1 == p2);  // True (value equality)
Console.WriteLine(p1.GetHashCode() == p2.GetHashCode()); // True
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

With-выражения

Выражение with создаёт копию record с модифицированными свойствами. Оригинал остаётся неизменным (неразрушающая мутация). Это идиоматичный способ обновления неизменяемых данных. Под капотом компилятор использует защищённый copy-конструктор и init-only setters.

csharp
var alice = new Person("Alice", 30);
var alice31 = alice with { Age = 31 };
var aliceMarried = alice with { Name = "Alice Smith" };

// Original is unchanged (immutable)
Console.WriteLine(alice.Age);      // 30
Console.WriteLine(alice31.Age);    // 31
Console.WriteLine(aliceMarried.Name); // Alice Smith

Позиционные vs init-only

Позиционные records используют primary constructor-синтаксис и генерируют deconstruct-методы. Init-only records используют синтаксис object initializer, допуская значения по умолчанию и модификатор required. Выбирайте позиционные для простых value-типов; init-only для сложных records с опциональными или вычисляемыми свойствами.

csharp
// Positional record (primary constructor)
public record Point(int X, int Y);

// Init-only record (more flexible properties)
public record Employee
{
    public required string Name { get; init; }
    public decimal Salary { get; init; }
    public string Dept { get; init; } = "General";
}

var emp = new Employee { Name = "Bob", Salary = 50000 };
var promoted = emp with { Salary = 60000 };

Record structs и наследование

Record structs — типы значений (копируются при присваивании) и не могут наследовать от других record structs. Reference-type records поддерживают наследование. readonly record struct предотвращает мутацию. Используйте record struct для небольших неизменяемых значений; record class для крупных объектов, выигрывающих от ссылочной семантики и наследования.

csharp
// Record struct (value type)
public readonly record struct Money(decimal Amount, string Currency);

// Record inheritance (reference type only)
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);

var dog = new Dog("Rex", "Labrador");
Console.WriteLine(dog is Animal);  // True
var copied = dog with { Breed = "Poodle" };

Deconstruct и matching

Позиционные records авто-генерируют метод Deconstruct, включая деконструкцию кортежа. Это бесшовно интегрируется с pattern matching: можно сопоставлять свойства record позиционно или по имени. Деконструкция работает в var-паттернах, tuple-паттернах и switch-выражениях.

csharp
var person = new Person("Alice", 30);
var (name, age) = person;  // Deconstruct
Console.WriteLine($"{name} is {age}");

// Works with pattern matching
string Greet(Person p) => p switch
{
    ("Alice", _) => "Hi Alice!",
    (_, < 18) => "Hello young one",
    var (n, a) => $"Hello {n}, age {a}"
};
20

Source generators

Базовый source generator

Source generators выполняются во время компиляции и добавляют C#-исходники в компиляцию. IIncrementalGenerator — современный API (C# 9+). RegisterPostInitializationOutput добавляет статический код без анализа пользовательского кода. Generators только для чтения: они могут добавлять файлы, но не могут модифицировать существующий код.

csharp
[Generator]
public class HelloGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(ctx =>
        {
            ctx.AddSource("Hello.g.cs", """
                namespace Generated;
                public static class Hello
                {
                    public static string World => "Hello!";
                }
                """);
        });
    }
}

Генерация из атрибутов

ForAttributeWithMetadataName находит типы, помеченные конкретным атрибутом. Конвейер: syntax provider фильтрует узлы, transform извлекает данные, Collect батчит результаты, RegisterSourceOutput emits код. Этот инкрементальный конвейер кэширует результаты и регенерирует только при изменении входов, сохраняя сборки быстрыми.

csharp
// Find types marked with an attribute
context.SyntaxProvider.ForAttributeWithMetadataName(
    "MyApp.AutoToStringAttribute",
    predicate: (node, _) => node is ClassDeclarationSyntax,
    transform: (ctx, _) => (ClassDeclarationSyntax)ctx.TargetNode)
    .Collect()
    .RegisterSourceOutput((spc, classes) =>
    {
        foreach (var cls in classes)
            GenerateToString(spc, cls);
    });

StringBuilder-вывод

Generators производят исходники как строки. StringBuilder эффективен для многострочного вывода. Целевой класс должен быть partial, чтобы generator мог добавить члены. Сгенерированные файлы появляются в IDE под Dependencies > Analyzers. Используйте суффикс .g.cs по соглашению для отличия сгенерированных файлов.

csharp
void GenToString(SourceProductionContext ctx, ClassDeclarationSyntax cls)
{
    var sb = new StringBuilder();
    sb.AppendLine($"namespace {cls.ContainingNamespace};");
    sb.AppendLine($"public partial class {cls.Identifier}");
    sb.AppendLine("{");
    sb.AppendLine("  public override string ToString()");
    sb.AppendLine("  {");
    var props = cls.Members.OfType<PropertyDeclarationSyntax>();
    sb.AppendLine($"    return $"{cls.Identifier}: " +
      string.Join(", ", props.Select(p => $"{p.Identifier}={{{p.Identifier}}}")) + "";");
    sb.AppendLine("  }");
    sb.AppendLine("}");
    ctx.AddSource($"{cls.Identifier}_ToString.g.cs", sb.ToString());
}

JsonSerializerContext

System.Text.Json включает встроенный source generator, который прекомпилирует логику сериализации, устраняя runtime-рефлексию. Это улучшает производительность и поддерживает AOT-компиляцию (Native AOT). Объявляйте типы через JsonSerializable, затем используйте сгенерированный контекст для вызовов serialize/deserialize.

csharp
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    WriteIndented = true)]
[JsonSerializable(typeof(List<Person>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
public partial class AppJsonContext : JsonSerializerContext { }

// Usage (no runtime reflection):
var json = JsonSerializer.Serialize(people, AppJsonContext.Default.ListPerson);
var data = JsonSerializer.Deserialize(json, AppJsonContext.Default.ListPerson);

Распространённые случаи использования

Популярные применения generator'ов: генерация DTO, DI-регистрация, строго типизированная конфигурация, AutoMapper-профили, шаблоны логирования и JSON-сериализация. Библиотеки вроде MediatR, AutoMapper и Microsoft.Extensions.Logging используют generator'ы для сокращения шаблонности и улучшения AOT-совместимости.

csharp
// 1. DTO generation from interfaces
public interface IUserDto { string Name { get; } int Age { get; } }
// Generator creates: public class UserDto : IUserDto { ... }

// 2. Dependency injection registration
[RegisterService(ServiceLifetime.Scoped)]
public class MyService : IService { }
// Generator creates: services.AddScoped<MyService>();

// 3. Strongly-typed configuration
[ConfigurationSection("App:Db")]
public class DbSettings { public string Conn { get; set; } }
// Generator creates: services.Configure<DbSettings>(...)
21

Span и Memory

Основы Span

Span<T> обеспечивает типобезопасный, безопасный по памяти взгляд на непрерывную память без копирования. Он может оборачивать массивы, stackalloc-память, неуправляемую память или строки. Span — ref struct (только стек), поэтому его нельзя упаковывать, хранить в полях или захватывать лямбдами. Используйте для высокопроизводительных срезов и парсинга.

csharp
// Span<T> is a stack-only view over contiguous memory
Span<int> numbers = stackalloc int[] { 1, 2, 3, 4, 5 };

// From array
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> slice = arr.AsSpan(1, 3);  // [20, 30, 40]
slice[0] = 99;  // Modifies original array

// From string
ReadOnlySpan<char> text = "Hello, World".AsSpan();
ReadOnlySpan<char> hello = text.Slice(0, 5);  // "Hello"

Memory для async

Memory<T> — heap-безопасный аналог Span<T>. Может храниться в полях, захватываться лямбдами и использоваться через границы await. Преобразуйте в Span<T> через .Span при фактической работе. Используйте Memory для async-API и Span для синхронных горячих путей.

csharp
// Memory<T> can live on the heap and cross async boundaries
async Task ProcessAsync(Memory<byte> buffer)
{
    await Task.Delay(100);
    Span<byte> span = buffer.Span;
    for (int i = 0; i < span.Length; i++)
        span[i] = (byte)(span[i] * 2);
}

byte[] data = new byte[1024];
await ProcessAsync(data);  // Implicit conversion to Memory<byte>

Избегание выделений

Span устраняет промежуточные строковые выделения при парсинге и срезах. Для горячих путей, обрабатывающих большие входы (HTTP-парсинг, JSON, лог-файлы), это может радикально снизить нагрузку на GC. int.TryParse и другие примитивы имеют Span-перегрузки. Вызывайте ToString() только когда действительно нужна строка.

csharp
// Traditional (allocates new string)
string Extract(string input, int start, int len)
    => input.Substring(start, len);

// Zero-allocation with Span
ReadOnlySpan<char> ExtractFast(ReadOnlySpan<char> input, int start, int len)
    => input.Slice(start, len);

// Parse without allocating substrings
bool TryParseInt(ReadOnlySpan<char> span, out int result)
    => int.TryParse(span, out result);

stackalloc и unmanaged

stackalloc выделяет на стеке вызовов (авто-освобождается, без нагрузки на GC), но ограничен малыми размерами. Для больших буферов используйте NativeMemory.Alloc/Free (C# 7+) или Marshal.AllocHGlobal. Всегда оборачивайте неуправляемую память в Span и освобождайте в finally-блоке для предотвращения утечек.

csharp
// Stack allocation (small, fast, auto-freed)
Span<int> buffer = stackalloc int[128];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = i;

// Unmanaged memory (large buffers, manual lifetime)
unsafe
{
    int* ptr = (int*)NativeMemory.Alloc(1024, sizeof(int));
    try
    {
        Span<int> span = new Span<int>(ptr, 1024);
        span.Fill(42);
    }
    finally
    {
        NativeMemory.Free(ptr);
    }
}

Span-friendly API

Проектируйте API для приёма Span<T> или ReadOnlySpan<T> для буферов, избегая выделения массивов. Вызывающий решает, использовать stackalloc, массив или неуправляемую память. Span.TryWrite (C# 10+) форматирует прямо в span без выделения строки, полезно для логирования и сериализации.

csharp
// Writing to a Span buffer
int WriteValues(Span<int> dest)
{
    int count = 0;
    foreach (var v in new[] { 1, 2, 3 })
    {
        if (count >= dest.Length) break;
        dest[count++] = v;
    }
    return count;
}

// Span-based string formatting (C# 10+)
Span<char> dest = stackalloc char[64];
bool ok = dest.TryWrite($"Value: {42:N2}", out int charsWritten);
Console.WriteLine(dest.Slice(0, charsWritten).ToString());
22

Углублённое изучение интерполированных строк

Спецификаторы формата

Строковая интерполяция ($"...") встраивает выражения в фигурные скобки. Спецификаторы формата после двоеточия управляют выводом: C для валюты, F для фиксированных знаков, X для hex, D для цифр с паддингом, P для процента, yyyy-MM-dd для дат. Компилятор транслирует это в string.Format или FormattableString.

csharp
string name = "Alice";
int age = 30;
decimal price = 19.99m;
DateTime now = DateTime.Now;

Console.WriteLine($"Name: {name}, Age: {age}");
Console.WriteLine($"Price: {price:C}");  // $19.99
Console.WriteLine($"Date: {now:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"Hex: {255:X}");  // FF
Console.WriteLine($"Padded: {age:D5}");  // 00030
Console.WriteLine($"Percent: {0.85:P0}");  // 85%

Raw string interpolation

Verbatim interpolated strings ($@) сохраняют обратные слэши буквально и допускают многострочный контент. Raw string interpolation (C# 11) использует тройные кавычки и устраняет все экранирования: не нужно экранировать скобки или кавычки. Количество $ управляет, сколько скобок нужно для интерполяции.

csharp
// Verbatim interpolated ($@)
string path = $@"C:\Users\{name}\Documents";
string json = $@"{{
  ""name"": ""{name}"",
  ""age"": {age}
}}";

// Raw string interpolation (C# 11)
string query = $$"""
    SELECT * FROM Users
    WHERE Name = '{{name}}'
    AND Age > {{age - 5}}
    """;
// $$ means {{ }} for interpolation, no escaping needed

FormattableString и культура

FormattableString сохраняет структуру интерполяции, позволяя отложенное форматирование с конкретными культурами. Это критично для библиотек, не зависящих от текущей культуры. FormattableString.Invariant форсирует инвариантную культуру, предотвращая локаль-специфичные проблемы в сериализации и логировании.

csharp
// FormattableString captures the format for deferred execution
FormattableString msg = $"Hello {name}, you are {age} years old";

// Explicit culture (avoid CurrentCulture issues in libraries)
string german = msg.ToString(CultureInfo.GetCultureInfo("de-DE"));
string invariant = msg.ToString(CultureInfo.InvariantCulture);

// Invariant by default
string formatted = FormattableString.Invariant($"Total: {price}");

Обработчики интерполированных строк

Обработчики интерполированных строк (C# 10) позволяют компилятору передавать компоненты интерполяции напрямую в метод, избегая выделения строки, когда результат не нужен. Так работает высокопроизводительное логирование: если уровень логирования отключён, строка не строится. Обработчик получает литералы и форматированные значения отдельно.

csharp
// Custom handler (C# 10+) for conditional allocation
[InterpolatedStringHandler]
public ref struct LogStringHandler
{
    public LogStringHandler(int literal, int hole, Logger logger, out bool enabled)
    {
        enabled = logger.IsEnabled;
        // Skip allocation if logging is disabled
    }
    public void AppendLiteral(string s) { /* ... */ }
    public void AppendFormatted<T>(T value) { /* ... */ }
}

// Usage: no string allocation if log level is off
logger.Info($"Processing {item} with {count} items");

Span-based форматирование

Span.TryWrite форматирует интерполированные строки прямо в стек-выделенный буфер, полностью избегая выделения в куче. string.Create с интерполированной строкой и культурой даёт культурно-осведомлённое форматирование без промежуточных выделений. Это важные техники для горячих путей в высоконагруженных приложениях.

csharp
// Format directly into a buffer (C# 10+)
Span<char> buffer = stackalloc char[256];

if (buffer.TryWrite($"User: {name}, Age: {age}", out int chars))
{
    var result = buffer[..chars];
    Console.WriteLine(result);
}

// Concatenation with DefaultInterpolatedStringHandler
string built = string.Create(CultureInfo.InvariantCulture, $"Total={price}");
23

File-scoped namespaces

File-scoped синтаксис

File-scoped namespaces (C# 10) объявляют пространство имён для всего файла точкой с запятой вместо фигурных скобок. Это убирает один уровень отступа, делая код более плоским и читаемым. Допускается только одно file-scoped namespace на файл. Все типы в файле принадлежат этому пространству имён.

csharp
// Traditional (adds a level of indentation)
namespace MyApp.Services
{
    public class UserService
    {
        public void Process() { }
    }
}

// File-scoped (C# 10+, no indentation)
namespace MyApp.Services;

public class UserService
{
    public void Process() { }
}

Global usings

Global usings (C# 10), объявленные в одном файле, применяются ко всем файлам проекта. Комбинируйте с file-scoped namespaces для чистого минимального шаблона. .NET SDK-проекты авто-генерируют global usings для распространённых пространств имён (ImplicitUsings). Порядок: global usings, затем file usings, затем namespace.

csharp
// Global usings (apply to all files in the project)
global using System.Collections.Generic;
global using System.Linq;

// File-scoped namespace with usings
using System.Text.Json;
using MyApp.Models;

namespace MyApp.Api;

public class UserController
{
    // Can use List, LINQ, Json, Models without per-file usings
}

Вложенные пространства имён

Пространства имён в области файла используют точечные имена для иерархии: MyApp.Services.Authentication эквивалентно вложенным блокам пространств имён. Несколько файлов могут использовать одно и то же пространство имён. Нельзя вкладывать типы в другое пространство имён в том же файле; используйте отдельные файлы.

csharp
// File-scoped: flat name with dots
namespace MyApp.Services.Authentication;

public class TokenService { }

// Multiple files can use the same namespace
// File 1: namespace MyApp.Services;
// File 2: namespace MyApp.Services;  // Same namespace

Конфигурация проекта

ImplicitUsings в файле проекта автоматически создаёт глобальные using для распространённых пространств имён System. В сочетании с пространствами имён в области файла это устраняет большую часть шаблонного кода. Nullable включает типы ссылок, допускающие значения null. Это значения по умолчанию для новых проектов .NET 6+.

csharp
<!-- .csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

<!-- ImplicitUsings generates:
     global using System;
     global using System.Collections.Generic;
     global using System.IO;
     global using System.Linq;
     global using System.Net.Http;
     global using System.Threading;
     global using System.Threading.Tasks;
-->

Миграция и лучшие практики

Используйте .editorconfig для принудительного применения пространств имён в области файла (IDE0161). IDE предоставляет миграцию в один клик из традиционного формата в файловый. Лучшая практика: один публичный тип на файл, причём имя файла совпадает с именем типа. Это сохраняет файлы небольшими и облегчает навигацию.

csharp
// .editorconfig to enforce file-scoped namespaces
[*.{cs,vb}]
dotnet_diagnostic.IDE0161.severity = error  // Prefer file-scoped

// Migration: use the IDE fixer to convert
// Right-click > Quick Actions > Convert to file-scoped namespace

// Best practice: one type per file
// UserService.cs
namespace MyApp.Services;
public class UserService { }
24

Обязательные члены

Обязательные свойства

Модификатор required (C# 11) заставляет вызывающий код инициализировать свойства при создании объекта. Компилятор выдаёт CS9035, если обязательный член не установлен. Это заменяет проверку в конструкторе для обязательных свойств. Работает с init-only установщиками и инициализаторами объектов, обеспечивая гарантии на этапе компиляции.

csharp
public class User
{
    public required Guid Id { get; init; }
    public required string Name { get; init; }
    public required string Email { get; init; }
    public string? Phone { get; init; }  // Optional
}

// Compiler enforces initialization
var user = new User
{
    Id = Guid.NewGuid(),
    Name = "Alice",
    Email = "[email protected]"
};

// Error CS9035: Required member 'Name' must be set
// var bad = new User { Id = Guid.NewGuid() };

SetsRequiredMembers

Атрибут SetsRequiredMembers сообщает компилятору, что конструктор инициализирует все обязательные члены, позволяя вызывающему коду использовать конструктор без инициализатора объекта. Это заполняет пробел между обязательными свойствами и традиционными конструкторами. Атрибут не проверяет во время выполнения; это обещание на этапе компиляции.

csharp
public class Configuration
{
    public required string ConnectionString { get; init; }
    public required int Port { get; init; }

    [SetsRequiredMembers]
    public Configuration(string conn, int port)
    {
        ConnectionString = conn;
        Port = port;
    }
}

// Both work: constructor or initializer
var c1 = new Configuration("localhost", 5432);
var c2 = new Configuration
{
    ConnectionString = "localhost",
    Port = 5432
};

Required с записями

Обязательные члены работают с записями. Для позиционных записей обязательные свойства объявляются в теле, а не в первичном конструкторе. Выражение with автоматически сохраняет обязательные значения. Значения по умолчанию (например, OpenedAt) делают свойства необязательными без модификатора required.

csharp
public record Account
{
    public required string AccountNumber { get; init; }
    public required decimal Balance { get; init; }
    public DateTime OpenedAt { get; init; } = DateTime.UtcNow;
}

// Positional record with required
public record Customer(string Name)
{
    public required string Email { get; init; }
}

var cust = new Customer("Alice") { Email = "[email protected]" };

Init vs Required vs Set

init делает свойство устанавливаемым только во время создания (после этого неизменяемым). required принуждает к инициализации. Вместе (required + init) они создают обязательные неизменяемые свойства. required + set допускает изменение после конструктора. Выбирайте в зависимости от того, должно ли значение меняться после создания.

csharp
// Init-only: can be set in initializer, but not required
public class Settings
{
    public string Name { get; init; } = "Default";
}

// Required + init: must be set, immutable after
public class SecureSettings
{
    public required string ApiKey { get; init; }
}

// Required + set: must be set, but can be modified later
public class MutableConfig
{
    public required string Environment { get; set; }
}

Сериализация JSON

System.Text.Json учитывает модификатор required: десериализация вызывает JsonException, если обязательное свойство отсутствует в JSON. Это обеспечивает проверку во время выполнения, соответствующую контракту на этапе компиляции. Сочетайте с init для неизменяемых DTO, которые гарантированно имеют все обязательные поля заполненными.

csharp
using System.Text.Json;

public class ApiResponse
{
    public required string Status { get; init; }
    public required int Code { get; init; }
    public string? Message { get; init; }
}

// Deserialization enforces required at runtime
var json = """{"Status":"OK","Code":200}""";
var response = JsonSerializer.Deserialize<ApiResponse>(json);

// Throws JsonException if required member is missing
// var bad = JsonSerializer.Deserialize<ApiResponse>("""{"Code":200}""");
25

Выражения коллекций

Синтаксис выражений коллекций

Выражения коллекций (C# 12) предоставляют единый синтаксис [..] для инициализации любого типа коллекции. Компилятор определяет тип и использует соответствующий построитель. Это заменяет многословный синтаксис new List<int> { 1, 2, 3 }. Работает с массивами, List<T>, Span<T>, IEnumerable<T> и пользовательскими коллекциями с атрибутом CollectionBuilder.

csharp
// C# 12 collection expressions
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Span<int> span = [10, 20, 30];
IEnumerable<int> seq = [1, 2, 3];

// Empty collections
int[] empty = [];
List<int> emptyList = [];

// Works with any collection type that has a builder
HashSet<string> set = ["a", "b", "c"];

Оператор spread

Оператор spread (..) разворачивает коллекцию в охватывающее выражение коллекции. Это заменяет Concat, AddRange и ручные циклы. Можно смешивать разворачивание с отдельными элементами. Spread работает с любым IEnumerable<T>, что упрощает лаконичное объединение нескольких источников данных.

csharp
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];

// Spread elements with ..
int[] combined = [..a, ..b, 7, 8];  // [1,2,3,4,5,6,7,8]

// Mix spreads and literals
List<int> result = [0, ..a, 100, ..b];

// Spread any IEnumerable
List<int> evens = [.. Enumerable.Range(0, 10).Where(x => x % 2 == 0)];

Паттерны return и параметров

Выражения коллекций работают в операторах return, аргументах и присваиваниях. Компилятор автоматически преобразует к целевому типу. Условное разворачивание (тернарный оператор с ..) позволяет опционально включать элементы. Это делает построение коллекций из нескольких необязательных источников чистым и читаемым.

csharp
// Return collection expressions
List<int> GetNumbers() => [1, 2, 3];
int[] GetArray() => [10, 20];

// Pass as arguments
void Process(IEnumerable<int> data) { }
Process([1, 2, 3]);

// Conditional spread
bool includeExtra = true;
var items = [
    1, 2, 3,
    .. includeExtra ? [4, 5] : []
];

Пользовательские построители коллекций

Атрибут CollectionBuilder связывает тип коллекции с фабричным методом, обеспечивая поддержку выражений коллекций. Метод Create получает ReadOnlySpan<T> элементов. Это позволяет сторонним коллекциям участвовать в едином синтаксисе [..]. Паттерн построителя сохраняет инициализацию эффективной.

csharp
[CollectionBuilder(typeof(MyCollection), "Create")]
public class MyCollection<T> : IEnumerable<T>
{
    private readonly List<T> _items = new();
    public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();

    public static MyCollection<T> Create(ReadOnlySpan<T> items)
    {
        var col = new MyCollection<T>();
        foreach (var item in items) col._items.Add(item);
        return col;
    }
}

// Now usable with collection expressions
MyCollection<int> custom = [1, 2, 3, 4];

Коллекции params

C# 12 допускает params с любым типом коллекции, а не только с массивами. Выражения коллекций можно передавать в методы params. Если целевым типом является Span<T>, компилятор может использовать стековое выделение для небольших коллекций, полностью избегая выделения в куче. Это делает вариативные методы как эргономичными, так и эффективными.

csharp
// C# 12: params with collection expressions
void Log(params string[] messages)
{
    foreach (var m in messages) Console.WriteLine(m);
}

// Traditional params call
Log("Error", "Warning", "Info");

// Collection expression call (C# 12)
Log(["Error", "Warning", "Info"]);

// params with Span for zero-allocation
void ProcessFast(params Span<int> values) { }
ProcessFast([1, 2, 3]);  // May use stackalloc internally

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.