9818
Авторский канал по разработке на C# и Unity. Ресурсы, гайды, задачи, шпаргалки. Информация ежедневно пополняется! Автор: @energy_it РКН: https://clck.ru/3SBaT3 Реклама на бирже: https://telega.in/c/csharp_ready
CSharpGuide по C# и .NET!
Каталог разбит на шесть этапов, основы языка, управление потоком, методы и коллекции, ООП, продвинутый C# и практический .NET. Внутри этапов собраны отдельные уроки по переменным, LINQ, async/await, JSON, тестированию и другим темам.
В уроках есть примеры кода, которые запускаются прямо в браузере, и последовательные переходы между темами.
Оставляю ссылочку на CSharpGuide
Gemini Pro на 24 месяца за 800₽!
Вместо 39.000₽ — всего 800₽ ($9.9) за 2 года. По акции сейчас это 33₽ в месяц, в 48 раз выгоднее!
➡Argon 4.0 + Claude Opus 5.5 — для кода
➡Nano Banana 2 Pro — для картинок
➡Генерация видео — ролики и анимации
➡AntiGravity + 5 ТБ облака
➡Opus 5.5 и Sonnet 5.5
➡Youtube Premium
Акция ограничена по времени, дальше будет официальная цена 480$!
Строим ограниченную очередь фоновых задач через Channel<T>!
Если задания поступают быстрее, чем обрабатываются, неограниченная очередь продолжает расти и расходует память. Bounded channel задаёт верхний предел, а производитель асинхронно ждёт свободное место.
Создаём очередь на 32 задания:
var queue = Channel.CreateBounded<Job>(
new BoundedChannelOptions(32)
{ FullMode = BoundedChannelFullMode.Wait });
await queue.Writer.WriteAsync(job, token);
await queue.Writer.WriteAsync(nextJob, token);
await foreach (var job in queue.Reader
.ReadAllAsync(token))
{
await HandleAsync(job, token);
}
try { await ProduceAsync(queue.Writer, token); }
finally { queue.Writer.TryComplete(); }var consumer = ConsumeAsync(queue.Reader, token);
Exception? error = null;
try { await ProduceAsync(queue.Writer, token); }
catch (Exception ex) { error = ex; }
queue.Writer.TryComplete(error);
await consumer;
Почему Task.Run не делает синхронный ввод-вывод асинхронным?
Оборачивать блокирующее чтение файла в Task.Run кажется способом освободить текущий поток, но ожидание всё равно занимает поток из пула:
await Task.Run(() => File.ReadAllText(path));
string text = await File.ReadAllTextAsync(
path, cancellationToken);
await using var stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
string text = await reader.ReadToEndAsync(token);
var result = await Task.Run(
() => Calculate(input, token), token);
foreach (var item in items)
{
token.ThrowIfCancellationRequested();
Process(item);
}
Task<string> config = File.ReadAllTextAsync(
configPath);
Task<string> data = File.ReadAllTextAsync(
dataPath);
await Task.WhenAll(config, data);
Знаете, почему регулярное выражение может надолго занять поток?
При неудачном совпадении шаблоны с вложенными повторами могут проверять множество вариантов. На пользовательском вводе такая обратная проверка способна надолго загрузить процессор:
var pattern = "^(a+)+$";
var regex = new Regex(pattern);
var regex = new Regex(pattern, RegexOptions.None,
TimeSpan.FromMilliseconds(100));
try {
return regex.IsMatch(input);
} catch (RegexMatchTimeoutException) {
throw new ValidationException(
"Regex validation timed out");
}
Запускаем периодическую проверку без наложения запросов!
Обычный таймер может вызвать следующий callback, пока предыдущая проверка ещё ждёт сеть. Сегодня соберём последовательный polling через PeriodicTimer и передадим отмену внутрь операции.
Создаём таймер с нужным интервалом:
using var timer = new PeriodicTimer(
TimeSpan.FromSeconds(15));
while (await timer.WaitForNextTickAsync(token))
{
await RefreshAsync(token);
}
using var response = await client.GetAsync(
endpoint, token);
response.EnsureSuccessStatusCode();
Знали, почему долгоживущий HttpClient может обращаться по устаревшему DNS-адресу?
HttpClient повторно использует соединения из пула, а DNS обычно проверяется при установке нового соединения. Если адрес сервиса изменился, уже открытое соединение не обязано сразу переключиться на новый IP.
Создаём общий клиент и задаём срок жизни соединений:
static readonly HttpClient Client = new(
new SocketsHttpHandler
{
PooledConnectionLifetime =
TimeSpan.FromMinutes(5)
});
using var response =
await Client.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
Проверяем заголовок PNG и читаем размеры!
Нужно быстро узнать размеры изображения до декодирования всего файла. В этой задаче читаем фиксированные 24 байта, проверяем сигнатуру PNG и первый чанк IHDR, затем получаем ширину и высоту.
В решении:
• читаем заголовок без загрузки изображения целиком
• отклоняем усечённый или неверный файл
• декодируем размеры с учётом порядка байтов
Зачем передавать TimeProvider вместо прямого вызова DateTimeOffset.UtcNow?
Если код сам читает системные часы, тест зависит от момента запуска. Передай источник времени явно:
public sealed class ExpirationService(TimeProvider clock)
{
public bool IsExpired(DateTimeOffset expiresAt) =>
clock.GetUtcNow() >= expiresAt;
}
services.AddSingleton<TimeProvider>(TimeProvider.System);
var clock = new FakeTimeProvider(
new DateTimeOffset(2030, 1, 1, 12, 0, 0, TimeSpan.Zero));
var service = new ExpirationService(clock);
var expiresAt = clock.GetUtcNow().AddMinutes(5);
clock.Advance(TimeSpan.FromMinutes(5));
Assert.True(service.IsExpired(expiresAt));
dotnet add package Microsoft.Extensions.TimeProvider.Testing
await Task.Delay(TimeSpan.FromMinutes(5), clock, cancellationToken);
Добавляем Request-Id middleware в ASP.NET Core!
Нужно связывать логи, ошибки и ответы API одним идентификатором запроса. Реализуем middleware, который берёт Request-Id из заголовка или создаёт новый.
В этой задаче:
• читаем входящий заголовок
• создаём fallback через Guid
• прокидываем значение в response
• подключаем middleware в pipeline
Разбираем 7 инструментов System.Text.Json!
С JSON можно работать как с типизированными объектами, так и без заранее описанной модели. Для потоков есть асинхронные методы, а для выборочного чтения и изменения структуры подходят разные представления документа.
В этой шпоре собраны Serialize, Deserialize, SerializeAsync, DeserializeAsync, JsonDocument, TryGetProperty и JsonNode.
➡️ C# Ready | #шпора
Карта основных возможностей C#!
На картинке язык разложен на крупные ветки: типы, операторы, инструкции, классы, коллекции и LINQ. Цвета помогают быстро увидеть, какие темы относятся к одной группе.
Например, ветка типов ведёт от числовых и ссылочных типов к пользовательским классам, а ветка LINQ объединяет фильтрацию, сортировку, группировку и преобразование данных.
Сохрани, чтобы не потерять!
➡️ C# Ready | #ресурс
Разбираем 7 приёмов для регулярных выражений в C#!
Regex удобен для поиска и проверки текста, но сложный шаблон может неожиданно долго обрабатывать почти совпавшую строку. В этой шпоре собраны настройки, которые делают регулярные выражения понятнее и помогают контролировать время выполнения.
➡️ C# Ready | #шпора
Почему для денег в C# обычно нужен decimal, а не double?
double удобен для инженерных расчётов, координат, графики и статистики. Но для денег он часто создаёт ошибки.
Причина в том, что double хранит число в двоичном формате с плавающей точкой. Не все десятичные дроби можно представить точно.
Простой пример выглядит так:
double price = 0.1 + 0.2;
Console.WriteLine(price);
if (price == 0.3) {
Console.WriteLine("ok");
}decimal price = 0.1m + 0.2m;
Console.WriteLine(price);
decimal total = Math.Round(price, 2, MidpointRounding.AwayFromZero);
Делаем простой feature flags на C#!
Feature flags позволяют включать и выключать поведение без переписывания кода. Это удобно для новых экранов и постепенного выката функций.
Для примера будем хранить флаги в словаре:
var flags = new Dictionary<string, bool>
{
["new-checkout"] = true,
["debug-panel"] = false
};
static bool IsEnabled(
Dictionary<string, bool> flags,
string name)
return flags.TryGetValue(name, out var enabled)
&& enabled;
if (IsEnabled(flags, "new-checkout"))
{
ShowNewCheckout();
}
Console.WriteLine(
IsEnabled(flags, "unknown"));
Почему ValueTask не стоит использовать ради ускорения?
ValueTask часто воспринимают как более быстрый Task. На практике он полезен не всегда и может сделать код сложнее.
Главный сценарий для ValueTask появляется, когда результат часто уже готов синхронно. Например, значение нашлось в памяти и не нужно создавать полноценный Task.
Такой метод может вернуть готовое значение без лишней аллокации:
if (cache.TryGetValue(id, out var user))
{
return new ValueTask(user);
}
return new ValueTask(LoadAsync(id));
var result = GetUserAsync(id);
await result;
await result;
Task LoadUserAsync(int id)
{
return db.Users.FindAsync(id).AsTask();
}
Шпаргалка по class, record и struct в C#!
Например, class и record class являются ссылочными типами, а struct и record struct передают значения как отдельные данные.
На карточках сравниваются равенство, присваивание и копирование через with. Отдельно показано, почему with не создаёт глубокую копию вложенного списка.
Сохрани, чтобы не потерять!
➡️ C# Ready | #шпора
Строим порядок сборки из зависимостей!
Есть список задач и связи между ними. Нужно определить, в каком порядке их можно выполнять, а при циклической зависимости вернуть понятную ошибку.
В этой задаче:
• строим граф зависимостей и считаем входящие связи
• обрабатываем готовые задачи через очередь
• проверяем, что в графе не осталось цикла
Добавляем замер времени HTTP-запросов через DelegatingHandler!
Когда несколько сервисов обращаются к внешним API, можно видеть статус и длительность каждого запроса. Вынесем замер в обработчик HttpClient, чтобы не дублировать Stopwatch в каждом месте вызова.
Создаём обработчик, который получает логгер через конструктор:
sealed class TimingHandler(
ILogger<TimingHandler> log) :
DelegatingHandler
{
}
protected override
Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken token)
var timer = Stopwatch.StartNew();
HttpStatusCode? status = null;
try
{
var response = await base.SendAsync(
request, token);
status = response.StatusCode;
return response;
}
finally
{
log.LogInformation(
"{Method} {Status} {Ms}ms",
request.Method, status,
timer.ElapsedMilliseconds);
}
builder.Services.AddTransient<TimingHandler>();
builder.Services.AddHttpClient("catalog")
.AddHttpMessageHandler<TimingHandler>();
Как код C# превращается в машинные инструкции!
Схема показывает весь путь от исходного файла до выполнения процессором. Компилятор C# сначала создаёт промежуточный IL и сборку, а не готовые инструкции для конкретного процессора.
При запуске CLR загружает сборку. Когда вызывается метод, JIT-компилятор переводит его IL в машинный код для текущей платформы.
Сохрани, чтобы не потерять!
➡️ C# Ready | #ресурс
Как индексация строк изменила систему типов HydraScript!
Автор добавил длину и обращение по индексу в интерпретируемый язык на C#. Работа над строками показала, что правила операций удобнее хранить рядом с типами, а не разносить по семантическому анализатору.
В статье разбирают:
• как интерфейс IOperator описывает допустимость операции и тип результата
• почему строки и массивы могут использовать общие правила индексации
• где проходит граница между проверкой типов и выполнением выражения
Такой подход упрощает развитие анализатора, но оставляет диагностику ошибок и контекст исходного кода в соответствующем слое.
Продолжай читать на Habr!
Шпаргалка по сериализации JSON в C#!
Source generation создаёт код сериализации во время сборки и помогает проектам с trimming и Native AOT. В контекст добавляются используемые модели, а вызовы получают сгенерированный JsonTypeInfo без поиска метаданных через reflection.
➡️ C# Ready | #шпора
Обрабатываем список запросов параллельно с ограничением!
Представь, что нужно отправить тысячи записей во внешний API. Последовательный цикл может работать слишком долго, а запуск тысячи Task сразу создаст всплеск соединений и нагрузки. Соберём обработчик, который ограничивает число одновременных операций и умеет штатно отменяться.
Сначала задаём лимит параллелизма и общий токен отмены:
var options = new ParallelOptions
{
MaxDegreeOfParallelism = 4,
CancellationToken = cancellationToken
};
await Parallel.ForEachAsync(records, options, async (record, token) =>
{
await client.PostAsJsonAsync("/events", record, token);
});
await Parallel.ForEachAsync(records, options, async (record, token) =>
{
using var response = await client.PostAsJsonAsync(
"/events", record, token);
response.EnsureSuccessStatusCode();
});
try
{
await Parallel.ForEachAsync(records, options, ProcessAsync);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
logger.LogInformation("Обработка отменена");
}
catch (HttpRequestException exception)
{
logger.LogError(exception, "Не удалось отправить запись");
}
Шпаргалка по регулярным выражениям в .NET!
Regex позволяет находить и заменять текст по шаблону, а готовые методы возвращают результат в удобном для задачи виде. В справочнике собраны короткие примеры вызова и отмечены возможности новых версий .NET.
На картинке есть IsMatch, Match, Matches, Replace, Split, Count, EnumerateMatches, Escape и Unescape. Также показаны RegexOptions и тайм-аут, который ограничивает время поиска при неудачном шаблоне.
➡️ C# Ready | #ресурс
Убираем лишние запросы при вводе текста в C#!
При поиске по мере набора символов приложение может отправить запрос на каждый ввод. Добавим небольшую задержку и отмену предыдущей операции, чтобы искать только по последнему значению.
Храним источник отмены текущего поиска и блокировку для его безопасной замены:
private readonly object _searchLock = new();
private CancellationTokenSource? _searchCts;
var current = new CancellationTokenSource();
lock (_searchLock)
{
var previous = _searchCts;
_searchCts = current;
previous?.Cancel();
}
try
{
await Task.Delay(300, current.Token);
var results = await _searchService.FindAsync(query, current.Token);
current.Token.ThrowIfCancellationRequested();
_view.Show(results);
}
catch (OperationCanceledException) when (current.IsCancellationRequested)
{
}
finally
{
lock (_searchLock)
{
if (ReferenceEquals(_searchCts, current))
{
_searchCts = null;
}
}
current.Dispose();
}
Знали, как читать обновляемые настройки из singleton-сервиса в ASP.NET Core?
Фоновый сервис обычно живёт всё время работы приложения. Если внедрить в него IOptionsSnapshot<T>, контейнер зависимостей получит scoped-сервис внутри singleton, и такая комбинация нарушает границы времени жизни.
Для долгоживущих сервисов предназначен IOptionsMonitor<T>. Он зарегистрирован как singleton, подходит для внедрения в сервисы любого времени жизни и позволяет получить актуальные настройки через CurrentValue.
Сначала связываем класс настроек с секцией конфигурации:
builder.Services.Configure<WorkerOptions>(
builder.Configuration.GetSection("Worker"));
private readonly IOptionsMonitor<WorkerOptions> _options;
WorkerOptions current = _options.CurrentValue;
await RunJobAsync(current.BatchSize, current.Endpoint);
Почему MemoryStream.GetBuffer может вернуть лишние байты?
Представим, что API должен отправить короткий двоичный ответ. Мы записали его в MemoryStream, а затем получили внутренний массив:
using var stream = new MemoryStream();
stream.Write(new byte[] { 1, 2, 3 });
byte[] data = stream.GetBuffer();
Console.WriteLine(stream.Length);
Console.WriteLine(stream.Capacity);
Console.WriteLine(data.Length);
byte[] payload = stream.ToArray();
await destination.WriteAsync(payload);
if (stream.TryGetBuffer(out ArraySegment<byte> segment)) {
await destination.WriteAsync(segment.AsMemory());
}
Планируем ближайшие задачи через PriorityQueue в C#!
Нужно хранить отложенные задания и каждый раз быстро выбирать то, чей срок наступит раньше. Обычный список придётся сортировать или просматривать целиком, а PriorityQueue хранит элемент с приоритетом и отдаёт минимальный приоритет первым.
Создадим очередь, где приоритетом будет время запуска:
var jobs = new PriorityQueue<string, DateTimeOffset>();
var now = DateTimeOffset.UtcNow;
jobs.Enqueue("backup", now.AddMinutes(20));
jobs.Enqueue("cleanup", now.AddMinutes(5));
if (jobs.TryPeek(out _, out var due) && due <= now)
jobs.TryDequeue(out var job, out _);
while (jobs.TryPeek(out _, out var due) && due <= DateTimeOffset.UtcNow) {
var job = jobs.Dequeue();
Console.WriteLine(job);
}
Разбираем System.Threading.Channels в C#!
Channels помогают строить асинхронные очереди между producer и consumer без ручного lock вокруг коллекции. Один код пишет элементы, другой читает их по мере появления.
В этой шпоре собраны CreateUnbounded, CreateBounded, Writer, Reader, WriteAsync, ReadAllAsync и Complete.
➡️ C# Ready | #шпора
Читаем большой JSON-массив по одному объекту в C#!
Пусть orders.json содержит тысячи заказов одним массивом. Если загрузить файл целиком через ReadAllText, память займут и исходный текст, и созданные объекты. Для последовательной обработки удобнее читать поток и получать элементы по мере разбора.
Сначала опишем модель входных данных. В примере у каждого заказа есть номер и сумма:
public sealed record Order(int Id, decimal Total);
await using var stream = File.OpenRead("orders.json");
var orders = JsonSerializer.DeserializeAsyncEnumerable<Order>(
stream, cancellationToken: cancellationToken);decimal total = 0;
await foreach (Order? order in orders) {
if (order is null) continue;
if (order.Total < 0) throw new InvalidDataException("Сумма меньше нуля");
total += order.Total;
}