Начало работы
Hello World
Программы на C# начинаются в Main(). using System; импортирует пространство имён System (Console, Math и т.д.). C# 9+ поддерживает top-level statements: файл с одной строкой Console.WriteLine("Hello"); — допустимая программа. Компилятор генерирует класс и Main автоматически.
using System;
class Program {
static void Main() {
Console.WriteLine("Hello, World!");
}
}Переменные и типы
C# статически типизирован. var позволяет компилятору вывести тип (всё ещё типобезопасен во время компиляции). decimal (суффикс m) — для финансовых вычислений с точной точностью. const — константа времени компиляции; readonly — константа времени выполнения, устанавливаемая в к онструкторе.
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. Комбинируйте: $@"...".
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 для пользовательского ввода, чтобы избежать исключений от плохих данных.
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 во время компиляции.
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Строки
Строковые методы
Строки C# неизменяемы — методы возвращают новые строки, а не модифицируют оригинал. Распространённые методы: Length, ToUpper/ToLower, Substring, IndexOf, Replace, Contains, Split. Для интенсивной работы со строками используйте StringBuilder, чтобы избежать создания множества промежуточных строк.
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, когда у вас больше нескольких конкатенаций.
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 для безопасности.
// 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.
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) для лучшей производительности.
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+");Числа и математика
Числовые типы
В C# типы фиксированного размера: int (32-бит), long (64-бит), double (64-бит float), decimal (128-бит, для финансов). Подчёркивания (9_000_000) улучшают читаемость. Используйте decimal для денег — он избегает ошибок округления плавающей точки. Суффиксы: L (long), f (float), m (decimal).
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 для избежания переполнения.
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 для потокобезопасности.
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 для преобразования строки в число, чтобы избежать исключений.
// 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) обрабатывает целые произвольной точности без переполнения.
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)Поток управления
If / Else
if/else if/else — стандартно. Тернарный оператор (condition ? a : b) — краткое if/else-выражение. C# требует булевы условия — в отличие от C, целые не неявно преобразуются в bool. Используйте фигурные скобки даже для одного оператора для предотвращения багов при сопровождении.
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 и сокращает шаблонность.
// 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, если нужен индекс или модификация элементов.
// 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 для проваливания, но иначе не поощряется.
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 широко использует итераторы.
// 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++;
}Методы и делегаты
Определение метода
Методы могут быть expression-bodied (=>) для однострочников. Параметры по умолчанию делают аргументы опциональными. Именованные аргументы (name:) улучшают читаемость вызовов со многими параметрами и позволяют пропускать опциональные. C# не поддерживает перегрузку методов только с разными типами возврата.
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 argumentout, ref и params
out-параметры должны быть присвоены методом (вызывающему не нужно инициализировать). ref-параметры должны быть инициализированы вызывающим и могут модифицироваться. params допускает переменное число аргументов. C# 7+ разрешает объявление out var inline: TryParse(s, out var result).
// 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).
// 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'ами.
// 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-методов.
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);Коллекции
List<T>
List<T> — динамический массив (как vector в C++ или ArrayList в Java). Add/Remove — амортизированно O(1) / O(n). Count даёт число элементов (не capacity). Find/FindAll используют предикаты. Для частых вставок/удалений в середине LinkedList<T> может быть лучше.
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, если нужно).
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 — когда важен.
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.
// 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> для динамического размера; массивы — для фиксированных, критичных к производительности данных.
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 } };Классы и ООП
Класс и свойства
Auto-properties ({ get; set; }) автоматически генерируют backing-поля. Expression-bodied свойства (=>) вычисляются при доступе. Init-only свойства C# 9+ ({ get; init; }) устанавливаются только во время конструирования, обеспечивая неизменяемость. Инициализаторы объектов: new Person { Name = "X" }.
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).
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).
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) члены. Используйте абстрактные классы, когда есть разделяемая реализация; интерфейсы — для чистых контрактов. Класс может наследовать только один абстрактный класс, но реализовывать много интерфейсов.
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 — типы значений (копируются при присваивании, размещаются в стеке) — используйте для небольших, лёгких данных. Классы — ссылочные типы (размещаются в куче).
// 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; }
}LINQ
Where и Select
LINQ (Language Integrated Query) предоставляет SQL-подобные запросы к коллекциям. Where фильтрует, Select преобразует (map). LINQ ленив — запросы выполняются только при перечислении (например, через ToList()). Это обеспечивает эффективную цепочку без промежуточных коллекций. Method syntax (выше) наиболее распространён; query syntax также доступен.
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 своих членов. Это заменяет ручные циклы и словари для группировки.
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 или проверяйте пустоту.
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).
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); // trueJoin и Zip
Join выполняет внутреннее соединение (как SQL), сопоставляя элементы по ключу. Zip парит элементы из двух последовательностей по позиции. Оба производят новые последовательности. LINQ также поддерживает GroupJoin (left join с группировкой). Это мощно для комбинирования связанных данных из нескольких источников.
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"Обработка ошибок
Try / Catch / Finally
Сначала ловите конкретные исключения, затем общее Exception последним (от наиболее специфичного к наименее). finally выполняется всегда — используйте для очистки (закрытие файлов, освобождение ресурсов). Избегайте catch (Exception) для потока управления; ловите только то, что можете обработать. Используйте throw; (не throw e;) для сохранения стека вызовов.
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. Используйте пользовательские исключения для домен-специфичных ошибочных условий, которые вызывающий может обработать.
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 для классов, удерживающих неуправляемые ресурсы (файловые дескрипторы, подключения к БД).
// 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 на уровне проекта.
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) позволяет наблюдать исключения без их обработки.
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; }Файловый ввод-вывод и async
Класс File (быстрый I/O)
File предоставляет статические методы для быстрых одноразовых файловых операций — простых, но загружающих весь файл в память. Для больших файлов используйте StreamReader/StreamWriter для построчной обработки. File.Exists проверяет существование, но есть TOCTOU-гонка — всегда обрабатывайте исключения от файловых операций.
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 обработать это.
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 flushesasync / await
async/await включает неблокирующие асинхронные операции. Метод возвращает Task<T> (или Task для void). await приостанавливает метод без блокировки потока — поток свободен для другой работы. Это необходимо для I/O-связанных операций (файлы, сеть, БД) в UI и веб-приложениях.
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/OTask и Parallel
Task.Run планирует работу в пуле потоков. Task.WhenAll ожидает несколько задач параллельно (не используйте async/await для CPU-связанной работы — используйте Task.Run). Parallel.For — для параллельных CPU-циклов. PLINQ (AsParallel) параллелит LINQ-запросы. Используйте async для I/O, Parallel/PLINQ для CPU.
// 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 в память.
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);Углублённое изучение LINQ
Query vs Method syntax
В LINQ два эквивалентных синтаксиса. Query syntax SQL-подобен и более читаем для сложных join/grouping. Method syntax (fluent) более распространён, поддерживает все операторы и естественно цепляется. Они компилируются в один IL. Используйте query syntax для сложных многоклеточных запросов; method syntax для простых цепочек. Оба ленивы (отложенное выполнение).
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, если нужны стабильные результаты.
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 для повторных запросов (это предвычисленная группировка).
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) для проверки существования — это эффективнее и читаемее.
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(), когда нужна клиентская логика, которую нельзя транслировать.
// 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));Углублённое изучение async/await
Task и Task<T>
Task представляет асинхронную операцию без результата; Task<T> возвращает T. ValueTask<T> (C# 7) избегает выделения в куче, когда результат часто доступен синхронно (сценарии кэширования) — но может быть awaited только один раз. Используйте Task.FromResult для синхронных результатов в async-API. Предпочитайте Task вместо void для async-методов (void только для обработчиков событий).
// 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.
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 cancellationTask.WhenAll vs WhenAny
Task.WhenAll ожидает несколько задач параллельно и продолжается, когда все завершены (параллелизм). Task.WhenAny продолжается, когда первая задача завершена (гонки/избыточность). WhenAll по умолчанию бросает только первое исключение; инспектируйте IsFaulted/Exception каждой задачи, чтобы увидеть все сбои. Для fire-and-forget используйте Task.Run и обрабатывайте исключения внутри, чтобы избежать незамеченных исключений.
// 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.
// 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.
// 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!События и делегаты
Основы делегатов
Делегаты — типобезопасные указатели на функции. Пользовательские делегаты (delegate int MathOp(int,int)) в значительной степени заменены встроенными Action (возврат void) и Func<T> (возвращает T). Делегаты — multicast, они могут содержать несколько методов (комбинируются через +). При вызове выполняются все методы. Возвращаемое значение multicast-делегата — результат последнего метода. Используйте Action/Func для большинства современного кода.
// 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. Всегда отписывайтесь для предотвращения утечек памяти (событие содержит сильную ссылку на обработчик).
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) могут авто-генерировать этот шаблонный код.
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-аксессоры и интегрируются с инструментами дизайнера.
// 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 или при завершении жизни подписчика.
// 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Рефлексия
Информация о типе
Рефлексия инспектирует метаданные типа во время выполнения. typeof(T) получает Type во время компиляции; obj.GetType() — во время выполнения. Type предоставляет Name, IsClass, IsValueType и методы для перечисления членов (GetMethods, GetProperties, GetFields). BindingFlags контролируют возвращаемое (NonPublic, Instance, Static, Public). Рефлексия мощна, но медленна — кэшируйте результаты, когда возможно.
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) и сценариев кэширования.
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).
// 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 и библиотеками сериализации для генерации быстрого типизированного кода во время выполнения.
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.
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();Методы расширения и паттерны
Методы расширения
Методы расширения позволяют 'добавлять' методы к существующим типам без их модификации. Определяются в статическом классе с 'this' на первом параметре. Это синтаксический сахар — компилятор переписывает email.IsEmail() в StringExtensions.IsEmail(email). LINQ полностью состоит из методов расширения на IEnumerable. Используйте расширения для добавления utility-методов к sealed-классам, интерфейсам или сторонним типам. Подключайте через using.
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); // extensionFluent-интерфейсы
Fluent-интерфейсы возвращают 'this', позволяя chaining методов, создавая читаемый код, читающийся как предложение. Распространено в builder'ах (QueryBuilder), конфигурации (ASP.NET, EF) и DSL. Методы расширения могут добавлять fluent-методы к примитивным типам (5.Seconds()). Держите методы чистыми (возвращайте новый экземпляр) для неизменяемых fluent-API, или мутируйте и возвращайте this для builder'ов.
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. Повторное перечисление повторно запускает метод.
// 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). Всегда зависите от интерфейсов, а не конкретных классов, для гибкости.
// 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'ы для неизменяемых данных.
// 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();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-паттерн (случай по умолчанию).
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 делает сложную условную логику декларативной и читаемой.
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 декларативной, исчерпывающей (проверяемой компилятором) логикой.
// 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) это обеспечивает выразительное сопоставление последовательностей. Полезно для парсинга, валидации и реализации алгоритмов.
// 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-выражения предпочтительнее операторов для логики, возвращающей значения.
// 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)Records и неизменяемость
Основы Record (C# 9)
Records (C# 9) — ссылочные типы с равенством по значению — две записи равны, если их данные равны (в отличие от классов, использующих ссылочное равенство). Компилятор генерирует Equals, GetHashCode, ToString и Deconstruct автоматически. Выражение 'with' создаёт копию с модифицированными свойствами (неразрушающая мутация). Records идеальны для DTO, value objects и неизменяемых моделей данных.
// 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 = 31Record 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.
// 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, который читаемее параметров конструктора.
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 корректно работающими в полиморфных коллекциях, в отличие от наивного равенства по значению.