🟪 C#

Полный справочник: типы и nullable, строки, управляющие конструкции, ООП, record, коллекции и LINQ, async/await, исключения, файлы, DI, тесты и экосистема .NET.

Шпаргалки · Программирование · #c# #csharp #dotnet #backend

Запуск и окружение

dotnet new console -n App       dotnet run         dotnet build -c Release
dotnet add package Serilog      dotnet test        dotnet publish -c Release -r linux-x64 --self-contained
dotnet new list                 dotnet watch run

IDE: Visual Studio, Rider, VS Code (C# Dev Kit). Версии: .NET 8 LTS, .NET 9, .NET 10 LTS.

Минимальная программа (top-level statements)

Console.WriteLine("Привет");

Классический вид: namespace App; class Program { static void Main(string[] args) { } }. Файловые namespace X; и неявные using включены в современных шаблонах.

Типы данных

Группа Типы
Целые sbyte, byte, short, int, uint, long, ulong, nint
Дробные float, double, decimal (деньги, 28–29 знаков)
Прочие значения bool, char, DateTime, DateOnly, TimeOnly, Guid, enum, struct
Ссылочные string, object, массивы, классы, record, делегаты
int n = 5; long big = 10_000_000_000; double d = 1.5; decimal price = 19.99m; bool ok = true; char c = 'a';
var list = new List<int>();             // тип выводится
int? maybe = null;                       // nullable value type
string? name = null;                     // nullable reference (включён <Nullable>enable</Nullable>)
int[] arr = { 1, 2, 3 }; int[,] grid = new int[3, 4];
(int Id, string Name) tuple = (1, "a");  var (id, nm) = tuple;
const int Max = 10;  static readonly DateTime Start = DateTime.UtcNow;
int.Parse("42"); int.TryParse(text, out var value); Convert.ToInt32("42"); (double)5 / 2;

Типы-значения (struct) копируются, ссылочные (class) передаются по ссылке.

Строки

string s = "Привет";
$"{name} ({age:D3}) {price:C} {d:F2} {when:yyyy-MM-dd}"        // интерполяция
s.Length; s[0]; s.Substring(1, 3); s.Contains("р"); s.StartsWith("П"); s.IndexOf('и'); s.ToUpper();
s.Trim(); s.Replace("a", "b"); s.Split(','); string.Join(", ", list); string.IsNullOrWhiteSpace(s);
string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
var raw = """
    Сырая строка с "кавычками"
    """;                                                      // 11+
var sb = new StringBuilder(); sb.Append("a").AppendLine("b"); sb.ToString();

Строки неизменяемы. Сравнение указывайте с StringComparison.

Операторы и управление

// ?? (по умолчанию для null), ??= , ?. (безопасный доступ), ! (подавить предупреждение), is, as, =>, .., ^
var city = user?.Address?.City ?? "не указан";
list ??= new();
if (x is > 0 and < 10) { } else if (obj is string str && str.Length > 0) { }
var label = x switch { < 0 => "минус", 0 => "ноль", _ => "плюс" };           // switch-выражение
string kind = shape switch { Circle c => $"r={c.R}", Rect { W: > 0 } r => "rect", _ => "?" };

for (int i = 0; i < 3; i++) { }
foreach (var item in list) { }
while (cond) { }  do { } while (cond);
break; continue; goto (избегайте); lock (obj) { } using var f = File.OpenRead(path);
var slice = arr[1..3]; var last = arr[^1];      // диапазоны и индекс с конца

Классы, record, интерфейсы

public class User
{
    public int Id { get; init; }                         // init: задаётся один раз
    public string Name { get; set; } = "";
    public string Label => $"@{Name}";                   // вычисляемое
    private readonly List<string> _tags = new();
    public User(int id, string name) { Id = id; Name = name; }
    public virtual void Print() => Console.WriteLine(Name);
}
public class Admin : User { public Admin(int id) : base(id, "admin") { } public override void Print() { base.Print(); } }

public record Point(int X, int Y);                       // значения по структуре, with-копирование
var p2 = p1 with { X = 5 };
public readonly struct Money(decimal amount) { public decimal Amount { get; } = amount; }

public interface IRepo<T> { Task<T?> FindAsync(int id); }
public abstract class Shape { public abstract double Area(); }
public enum Role { User = 1, Admin = 2 }
public class Svc(IRepo<User> repo) { }                    // первичные конструкторы (12+)

Модификаторы: public, internal, protected, private, sealed, static, abstract, virtual, override, readonly, partial, required.

Обобщения и делегаты

T Max<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) > 0 ? a : b;
Func<int, int> sq = x => x * x;       Action<string> log = Console.WriteLine;     Predicate<int> even = x => x % 2 == 0;
event EventHandler<EventArgs>? Changed;     Changed?.Invoke(this, EventArgs.Empty);

Коллекции и LINQ

var list = new List<int> { 3, 1, 2 };   list.Add(4); list.Remove(1); list.Contains(2); list.Sort();
var dict = new Dictionary<string, int> { ["a"] = 1 };   dict.TryGetValue("a", out var v); dict.GetValueOrDefault("z");
var set = new HashSet<int>(); var q = new Queue<int>(); var st = new Stack<int>();
IReadOnlyList<int> ro = list;

var result = users
    .Where(u => u.Age >= 18)
    .OrderBy(u => u.Name).ThenByDescending(u => u.Age)
    .Select(u => new { u.Id, u.Name })
    .ToList();
users.Any(u => u.IsAdmin); users.All(u => u.Active); users.FirstOrDefault(u => u.Id == 1); users.Count(u => u.Age > 30);
users.GroupBy(u => u.Role).Select(g => new { Role = g.Key, N = g.Count() });
users.Sum(u => u.Salary); users.Max(u => u.Age); users.Distinct(); users.Take(10).Skip(20); users.Chunk(100);
orders.Join(users, o => o.UserId, u => u.Id, (o, u) => new { u.Name, o.Sum });

LINQ откладывает выполнение: запрос выполняется при ToList, foreach, Count.

Асинхронность

static async Task<string> LoadAsync(HttpClient http, CancellationToken ct = default)
{
    var res = await http.GetAsync("https://example.com", ct);
    res.EnsureSuccessStatusCode();
    return await res.Content.ReadAsStringAsync(ct);
}
var results = await Task.WhenAll(urls.Select(u => LoadAsync(http)));      // параллельно
await Task.Delay(1000);   Task.Run(() => HeavyWork());                       // CPU в пул потоков
await foreach (var x in StreamAsync()) { }                                    // IAsyncEnumerable
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));               // отмена

Правила: async до конца (не блокируйте .Result, .Wait()), ConfigureAwait(false) в библиотеках, возвращайте Task, а не async void (кроме обработчиков событий). Параллельные циклы: Parallel.ForEachAsync.

Исключения

try { var n = int.Parse(text); }
catch (FormatException ex) when (text.Length > 0) { logger.LogWarning(ex, "..."); }
catch (Exception ex) { throw; }              // throw; сохраняет стек (не throw ex)
finally { }
throw new InvalidOperationException("состояние неверно");
ArgumentNullException.ThrowIfNull(x);
public class AppException(string msg, Exception? inner = null) : Exception(msg, inner);
using (var conn = new SqlConnection(cs)) { }   // Dispose гарантирован

Файлы, JSON, HTTP

File.ReadAllText(path); File.WriteAllText(path, text); File.Exists(path); Directory.CreateDirectory(dir);
await File.ReadAllLinesAsync(path); Path.Combine("a", "b");
var json = JsonSerializer.Serialize(obj, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var obj = JsonSerializer.Deserialize<User>(json);
await http.GetFromJsonAsync<User>("/api/users/1");   await http.PostAsJsonAsync("/api/users", user);

HttpClient создавайте через IHttpClientFactory, не по экземпляру на запрос.

ASP.NET Core (минимальный API)

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IRepo<User>, Repo>();
var app = builder.Build();
app.MapGet("/users/{id:int}", async (int id, IRepo<User> r) => await r.FindAsync(id) is { } u ? Results.Ok(u) : Results.NotFound());
app.MapPost("/users", (User u) => Results.Created($"/users/{u.Id}", u));
app.Run();

Время жизни DI: Singleton, Scoped (на запрос), Transient.

Тесты

[Fact] public void Adds() => Assert.Equal(3, Calc.Add(1, 2));
[Theory][InlineData(1, 2, 3)][InlineData(0, 0, 0)] public void Adds2(int a, int b, int e) => Assert.Equal(e, Calc.Add(a, b));
var repo = new Mock<IRepo<User>>(); repo.Setup(r => r.FindAsync(1)).ReturnsAsync(user);

xUnit, NUnit, MSTest, Moq, NSubstitute, FluentAssertions, Testcontainers, WebApplicationFactory.

Подводные камни

  • decimal для денег, double приблизителен.
  • Сравнение строк без указания культуры.
  • Захват переменных в замыканиях и отложенный LINQ.
  • Блокировки async кода (.Result): взаимные блокировки.
  • Забыли Dispose (using).
  • Коллекция изменена во время foreach.

Экосистема по задачам

Задача Что взять
Веб и API ASP.NET Core, Minimal API, gRPC, SignalR
БД Entity Framework Core, Dapper, Npgsql
UI Blazor, MAUI, WPF, WinForms, Avalonia
Логи Serilog, NLog, ILogger
Сообщения MassTransit, RabbitMQ.Client, Confluent.Kafka
Паттерны MediatR, FluentValidation, AutoMapper (Mapperly)
Задачи Hangfire, Quartz.NET
Кэш IMemoryCache, StackExchange.Redis
Тесты xUnit, Moq, Testcontainers, Bogus