csharp_ready | Unsorted

Telegram-канал csharp_ready - C# Ready | Unity

9818

Авторский канал по разработке на C# и Unity. Ресурсы, гайды, задачи, шпаргалки. Информация ежедневно пополняется! Автор: @energy_it РКН: https://clck.ru/3SBaT3 Реклама на бирже: https://telega.in/c/csharp_ready

Subscribe to a channel

C# Ready | Unity

Разбираем System.Threading.Channels в C#!

Channels помогают строить асинхронные очереди между producer и consumer без ручного lock вокруг коллекции. Один код пишет элементы, другой читает их по мере появления.

В этой шпоре собраны CreateUnbounded, CreateBounded, Writer, Reader, WriteAsync, ReadAllAsync и Complete.

➡️ C# Ready | #шпора

Читать полностью…

C# Ready | Unity

Читаем большой JSON-массив по одному объекту в C#!

Пусть orders.json содержит тысячи заказов одним массивом. Если загрузить файл целиком через ReadAllText, память займут и исходный текст, и созданные объекты. Для последовательной обработки удобнее читать поток и получать элементы по мере разбора.

Сначала опишем модель входных данных. В примере у каждого заказа есть номер и сумма:

public sealed record Order(int Id, decimal Total);


Откроем файл как поток. JsonSerializer.DeserializeAsyncEnumerable ожидает JSON-массив и возвращает асинхронную последовательность его элементов:
await using var stream = File.OpenRead("orders.json");
var orders = JsonSerializer.DeserializeAsyncEnumerable<Order>(
stream, cancellationToken: cancellationToken);


Здесь cancellationToken позволяет прервать длительную обработку, например при остановке запроса или фоновой задачи. Не надо самостоятельно резать JSON на строки, переносы строк не определяют границы объектов.

Обработаем элементы по одному и накопим итог. Нулевые элементы пропустим, а отрицательную сумму считаем ошибкой данных:
decimal total = 0;
await foreach (Order? order in orders) {
if (order is null) continue;
if (order.Total < 0) throw new InvalidDataException("Сумма меньше нуля");
total += order.Total;
}


После завершения цикла в total будет сумма всех валидных заказов. В нужном месте вместо накопления можно отправлять запись в базу. Если важна устойчивость к повторной обработке, используй Id как ключ идемпотентности.

➡️ C# Ready | #практика

Читать полностью…

C# Ready | Unity

Шпаргалка по источникам памяти для Span<T> в C#!

На картинке рядом показаны три способа создать Span<T>. Он может ссылаться на обычный массив, область stackalloc или неуправляемый участок памяти. Отдельные примеры помогают увидеть, что Span<T> описывает диапазон, а не владеет памятью.

Сохрани, чтобы не потерять!

➡️ C# Ready | #ресурс

Читать полностью…

C# Ready | Unity

Знали, когда в C# удобно использовать Random.Shared?

Для простого случайного числа нередко создают отдельный генератор прямо в месте вызова:

int dice = new Random().Next(1, 7);


Такой код работает, но при частых вызовах приходится снова и снова создавать объект. Если генератор нужен во многих местах приложения, проще взять общий экземпляр:
int dice = Random.Shared.Next(1, 7);


Random.Shared доступен начиная с .NET 6 и безопасен для одновременного использования из разных потоков. Для обычных игровых событий, тестовых данных и случайного выбора этого достаточно.

Обратите внимание на границы Next. Нижняя включается, верхняя нет:
int index = Random.Shared.Next(0, items.Count);


Так можно выбрать индекс, не выйдя за пределы списка. Для шестигранного кубика верхняя граница должна быть 7, а не 6:
int dice = Random.Shared.Next(1, 7);


А вот пароли, токены и коды подтверждения этим способом генерировать нельзя. Для значений, которые должны быть непредсказуемы для атакующего, нужен криптографический генератор:
int code = RandomNumberGenerator.GetInt32(100000, 1000000);


Random.Shared хорош для обычной случайности, а RandomNumberGenerator нужен там, где от случайности зависит безопасность.

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

Разбираем 7 инструментов HttpClient!

HttpClient пригодится не только для простого GET. С ним можно проверять HTTP-статус, отправлять JSON, читать ответ как текст или поток и собирать запросы с нужным методом и заголовками.

В этой шпоре собраны GetAsync, EnsureSuccessStatusCode, ReadAsStringAsync, GetFromJsonAsync, PostAsJsonAsync, SendAsync и GetStreamAsync.

➡️ C# Ready | #шпора

Читать полностью…

C# Ready | Unity

Почему фабрика в ConcurrentDictionary.GetOrAdd может сработать несколько раз?

ConcurrentDictionary удобно использовать, когда несколько потоков обращаются к одному словарю. Кажется, что GetOrAdd одновременно проверяет ключ и ровно один раз создаёт значение:

var profile = cache.GetOrAdd(id, key => LoadProfile(key));


В словаре действительно останется одно значение для ключа. Но это не означает, что LoadProfile обязательно вызовется только один раз.

Фабрика выполняется вне внутренней блокировки словаря. Если два потока одновременно не нашли ключ, оба могут начать загрузку:
var first = cache.GetOrAdd(id, LoadProfile);
var second = cache.GetOrAdd(id, LoadProfile);


Словарь сохранит один результат и вернёт его обоим вызовам, а результат другой загрузки окажется лишним. Поэтому в фабрику не стоит помещать побочные эффекты, например списание денег или отправку письма.

Если повторное вычисление недорого и безопасно, обычный GetOrAdd подходит. Для дорогой инициализации можно хранить в словаре Lazy<T>:
var pending = cache.GetOrAdd(id, key =>
new Lazy<Profile>(() => LoadProfile(key)));
var profile = pending.Value;


Здесь у разных потоков тоже могут создаться несколько объектов Lazy, но в словаре останется один. У него загрузка при обращении к Value выполнится один раз при стандартном режиме Lazy.

Не забывайте и про ошибки. По умолчанию Lazy запоминает исключение фабрики, а значит последующие обращения к тому же Value снова получат эту ошибку:
cache.TryRemove(id, out _);


Удалять запись для повтора нужно с учётом конкуренции с другими запросами.

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

Разбираем 7 инструментов для работы со временем!

В .NET важно различать длительность, измерение прошедшего времени и ожидание. TimeSpan хранит интервал, Stopwatch измеряет выполнение, а PeriodicTimer и Task.Delay помогают управлять асинхронной работой.

➡️ C# Ready | #шпора

Читать полностью…

C# Ready | Unity

Шпаргалка по поколениям сборщика мусора в .NET!

На картинке показаны поколения управляемой кучи, продвижение выживших объектов и три основные фазы GC. Mark находит достижимые объекты, sweep освобождает мусор, а compact уменьшает фрагментацию памяти.

Например, Gen 0 хранит новые короткоживущие объекты, Gen 1 служит промежуточной зоной, а Gen 2 содержит долгоживущие данные. Объекты крупнее 85 КБ обычно попадают в Large Object Heap.

Сохрани, чтобы не потерять!

➡️ C# Ready | #ресурс

Читать полностью…

C# Ready | Unity

Как в C# не забыть про ConfigureAwait(false)?

Когда пишешь библиотечный async-код, часто встречается такой вариант:

var json = await httpClient.GetStringAsync(url);
return JsonSerializer.Deserialize<User>(json);


Для приложения это обычно нормально. Но для библиотек и низкоуровневого кода часто лучше явно сказать, после await не нужно возвращаться в исходный context.

Для этого используют ConfigureAwait(false):
var json = await httpClient
.GetStringAsync(url)
.ConfigureAwait(false);


После await продолжение может выполниться на любом подходящем потоке:
return JsonSerializer.Deserialize<User>(json);


Это особенно полезно в shared-библиотеках, SDK, инфраструктурном коде и клиентах к внешним API.

Например:
public async Task<User> LoadUserAsync(string id)
{
var response = await client.GetAsync($"/users/{id}")
.ConfigureAwait(false);

return await ParseAsync(response)
.ConfigureAwait(false);
}


Но в UI-коде всё иначе. Если после await нужно обновить интерфейс, context может быть нужен:
label.Text = "Loaded";


В таком случае ConfigureAwait(false) может увести продолжение с UI-потока.

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

🐱 C# Job Questions — большая база для подготовки к собеседованиям!

Здесь собраны вопросы с развернутыми ответами и примерами кода по C# и .NET, ООП, алгоритмам и структурам данных, архитектуре, базам данных и многопоточности. Дополнительная тренировка, чтобы системно повторить теорию, проверить пробелы и подготовиться к технической части собесов.

Оставляю ссылочку: GitHub 📱


➡️ C# Ready | #репозиторий

Читать полностью…

C# Ready | Unity

Разбираем File и Path 7 инструментов для работы с файлами!

В C# для повседневной работы с файлами чаще всего хватает стандартных классов File, Directory и Path. Они закрывают проверку существования, чтение, запись, создание папок, обход директорий и безопасную сборку путей.

В этой шпоре собраны File.Exists, ReadAllText, WriteAllText, Directory.CreateDirectory, Directory.EnumerateFiles, Path.Combine и Path.GetExtension.

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

❤️ dotnet-learning — большая база ресурсов для изучения .NET!

Здесь собраны полезные материалы для обучения, практики и более глубокого изучения подходов разработки на .NET. Репозиторий помогает не просто изучать отдельные технологии, а выстраивать целостное понимание платформы. Такой формат удобен и новичкам, и разработчикам с опытом, которые хотят структурировать знания или закрыть пробелы.

Оставляю ссылочку: GitHub 📱


➡️ C# Ready | #репозиторий

Читать полностью…

C# Ready | Unity

Почему нельзя удалять элементы из List прямо внутри foreach?

foreach в C# удобен для чтения коллекции, но в нём лучше не менять коллекцию во время обхода.

На первый взгляд такой код выглядит нормально:

foreach (var user in users)
{
if (!user.Active) users.Remove(user);
}


Но во время выполнения List меняется, а перечислитель foreach ожидает, что структура коллекции останется прежней.

В результате можно получить InvalidOperationException:
Collection was modified;
enumeration operation may not execute.


Для простого удаления по условию чаще всего лучше использовать RemoveAll:
users.RemoveAll(user => !user.Active);


Мы не вручную управляем индексами, а говорим списку удалить все элементы, которые подходят под условие.

Если нужна более сложная логика, можно идти с конца по индексам:
for (int i = users.Count - 1; i >= 0; i--)
{
if (!users[i].Active) users.RemoveAt(i);
}


Обход с конца важен, потому что удаление элемента сдвигает следующие элементы влево. При движении назад это не ломает ещё не проверенную часть списка.

Ещё один вариант подходит, когда исходный список лучше не трогать:
users = users
.Where(user => user.Active)
.ToList();


Так создаётся новый список только с нужными элементами. Это удобно для pipeline-логики и случаев, где мутация исходных данных ненужна.

foreach используют для чтения, а для удаления выбирают RemoveAll, обратный for или создание нового списка.

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

Разбираем CancellationToken 7 приёмов для отмены операций!

CancellationToken помогает останавливать долгие операции без грубого убийства потока и без ручных глобальных флагов.

В этой шпоре: CancellationToken, CancellationTokenSource, CancelAfter, ThrowIfCancellationRequested, IsCancellationRequested, CreateLinkedTokenSource и Register.

➡️ C# Ready | #шпора

Читать полностью…

C# Ready | Unity

Полезная статья про тестирование ASP.NET Core API!

Автор показывает, как уменьшить повторение в интеграционных тестах и собрать удобное окружение с БД, фейковым временем и настройкой под конкретный тест.

В статье автор показывает:
• как устроить тестовое окружение для ASP.NET Core API
• почему setup-код быстро начинает повторяться
• как фикстуры помогают переиспользовать инфраструктуру

Продолжай читать на Habr


➡️ C# Ready | #статья

Читать полностью…

C# Ready | Unity

Делаем простой 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)


Внутри используем TryGetValue, чтобы не падать на неизвестном флаге:
return flags.TryGetValue(name, out var enabled)
&& enabled;


Теперь флаг можно использовать в коде:
if (IsEnabled(flags, "new-checkout"))
{
ShowNewCheckout();
}


Если флага нет, функция вернёт false:
Console.WriteLine(
IsEnabled(flags, "unknown"));


В реальном проекте словарь можно заполнять из JSON, базы или remote config. Главное, чтобы код спрашивал флаг через один понятный метод.

➡️ C# Ready | #практика

Читать полностью…

C# Ready | Unity

Почему await Task.WhenAll может показать не все ошибки сразу?

Допустим несколько независимых операций выполняются параллельно. Мы объединяем их в одну задачу:

var all = Task.WhenAll(first, second);
await all;


Если обе операции завершатся с ошибками, WhenAll дождётся обеих. Итоговая задача будет содержать исключения из всех упавших операций.

Для диагностики сохраните общую задачу перед await:
var all = Task.WhenAll(first, second);


Теперь можно дождаться её и обработать неудачу:
try
{
await all;
}
catch
{
Console.Error.WriteLine(all.Exception);
throw;
}


Свойство all.Exception содержит AggregateException. Его InnerExceptions позволяют увидеть ошибки нескольких операций, а не только ту, которую выбросил await. Например, внутри обработки именно ошибочной задачи можно пройтись по ним:
foreach (var error in all.Exception!.InnerExceptions)
Console.WriteLine(error.Message);


Знак ! здесь уместен только внутри ветки, где общая задача действительно завершилась с ошибкой. Если она была отменена без ошибок, Exception будет null, и обработку отмены нужно делать отдельно.


Почему await Task.WhenAll может показать не все ошибки сразу?

Допустим, несколько независимых операций выполняются параллельно. Мы объединяем их в одну задачу:
var all = Task.WhenAll(first, second);
await all;


Если обе операции завершатся с ошибками, WhenAll дождётся обеих. Итоговая задача будет содержать исключения из всех упавших операций. Но обычный await выбрасывает одно исключение, поэтому в catch легко увидеть только часть картины.

Для диагностики сохраните общую задачу перед await:
var all = Task.WhenAll(first, second);


Теперь можно дождаться её и обработать неудачу:
try
{
await all;
}
catch
{
Console.Error.WriteLine(all.Exception);
throw;
}


Свойство all.Exception содержит AggregateException. Его InnerExceptions позволяют увидеть ошибки нескольких операций, а не только ту, которую выбросил await. Например, внутри обработки именно ошибочной задачи можно пройтись по ним:
foreach (var error in all.Exception!.InnerExceptions)
Console.WriteLine(error.Message);


Знак ! здесь уместен только внутри ветки, где общая задача действительно завершилась с ошибкой. Если она была отменена без ошибок, Exception будет null, и обработку отмены нужно делать отдельно.

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

Почему цепочка LINQ иногда читает всю коллекцию до первого результата?

Where и Select могут отдавать элементы по одному. А вот OrderBy, GroupBy и Join устроены сложнее, им нужно подготовить данные, прежде чем продолжить обработку. Автор разбирает эту границу между ленивым вычислением и буферизацией.

В статье показывают:
• зачем OrderBy хранит элементы и сортирует индексы
• как GroupBy собирает группы и сохраняет порядок их появления
• почему Join сначала строит структуру для внутренней коллекции

Можно понять куда уходит память в длинных цепочках и почему перестановка операций иногда меняет стоимость запроса.

Продолжай читать на Habr!


➡️ C# Ready | #статья

Читать полностью…

C# Ready | Unity

Почему копия record через with может изменить исходные данные?

Выражение with удобно для создания нового record с одним изменённым полем. Кажется, что после копирования два объекта полностью независимы.

Представим профиль со списком тегов:

record Profile(string Name, List<string> Tags);
var original = new Profile("Ana", new() { "dev" });


Создадим копию с другим именем:
var copy = original with { Name = "Bob" };


Сам record действительно новый. Но with делает поверхностную копию, поэтому поле Tags в обоих объектах указывает на один и тот же список.

Теперь изменим теги только у копии:
copy.Tags.Add("admin");
Console.WriteLine(string.Join(", ", original.Tags));


Результат у исходного профиля тоже станет dev, admin. С новым именем всё в порядке, проблема только во вложенном изменяемом объекте.

Если нужен отдельный список, явно создайте его при копировании:
var independent = original with
{
Tags = new List<string>(original.Tags)
};


Теперь добавление в independent.Tags не изменит original.Tags. Но это копия только самого списка. Если элементы списка тоже изменяемые объекты, их придётся копировать отдельно.

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

Проверяем целостность скачанного файла в C#!

После загрузки архива полезно убедиться, что полученные байты совпадают с опубликованной контрольной суммой.

Пусть ожидаемый SHA-256 приходит из источника, отдельно от самого файла. Сначала проверим его формат и превратим шестнадцатеричную строку в байты:

if (expectedHex.Length != 64)
throw new ArgumentException("Неверный SHA-256");
byte[] expected = Convert.FromHexString(expectedHex);


Теперь expected содержит ровно 32 байта. Если строка нужной длины, но содержит не шестнадцатеричные символы, Convert.FromHexString выбросит FormatException. Так произвольный текст не пройдёт за корректный хеш.

Откроем файл потоком, чтобы не загружать весь архив в память:
await using var stream = File.OpenRead(path);
byte[] actual = await SHA256.HashDataAsync(stream);


HashDataAsync прочитает файл до конца и вернёт 32 байта. Для больших файлов потребление памяти остаётся небольшим, поскольку поток обрабатывается постепенно.

Сравним результат с ожидаемым значением:
bool valid = CryptographicOperations.FixedTimeEquals(actual, expected);
if (!valid) throw new InvalidDataException("SHA-256 не совпал");


➡️ C# Ready | #практика

Читать полностью…

C# Ready | Unity

Схема прохождения запроса через middleware в ASP.NET Core!

На картинке показан путь HTTP-запроса через обработчик исключений, HTTPS, статические файлы, маршрутизацию, CORS, аутентификацию и авторизацию.

Например, проверка прав должна выполняться после определения пользователя. Если поставить middleware не в том порядке, приложение может неожиданно возвращать 401, пропускать нужную обработку или выполнять лишнюю работу.

Сохрани, чтобы не потерять!

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

Читаем все страницы API через IAsyncEnumerable в C#!

Многие API отдают данные порциями. Если собрать все страницы в List сразу, код быстро разрастается, а юзер не увидит ни одной записи до конца загрузки.

IAsyncEnumerable позволяет отдавать элементы по мере чтения следующей страницы. Вызывающий код использует await foreach и не знает, сколько запросов понадобилось внутри.

record Page<T>(IReadOnlyList<T> Items, string? NextCursor);


Метод клиента пусть возвращает страницу и курсор для следующего запроса. Null означает, что сервер больше ничего не отдаёт.
Task<Page<Order>> GetPageAsync(
string? cursor, CancellationToken token);


В итераторе сохраняем текущий курсор. После каждого ответа возвращаем заказы по одному и только потом запрашиваем следующую страницу.
string? cursor = null;
do {
var page = await GetPageAsync(cursor, token);
foreach (var order in page.Items) yield return order;
cursor = page.NextCursor;
} while (cursor is not null);


Юзер может остановиться в любой момент. CancellationToken важно передавать в сетевой вызов, чтобы отмена не ждала лишнюю страницу.
await foreach (var order in ReadOrders(token))
{
await SaveAsync(order, token);
}


Такой подход полезен для импорта каталога, обработки событий и синхронизации данных. Память не растёт вместе с общим количеством страниц, а обработка начинается сразу после первого ответа.

➡️ C# Ready | #практика

Читать полностью…

C# Ready | Unity

Blazor против Vue.js глазами C# разработчика!

Автор сравнивает два frontend-подхода на опыте реальных проектов. Статья показывает, что будет привычным, а что неожиданным для разработчика, который приходит в интерфейсы из C# и .NET.

В статье разбирают:
• как ощущается компонентная разработка в Blazor и Vue
• где C# стек даёт сильную сторону на frontend
• какие различия важнее синтаксиса и шаблонов

Продолжай читать на Habr


➡️ C# Ready | #статья

Читать полностью…

C# Ready | Unity

Почему ToLower() перед сравнением строк может быть лишним?

Когда нужно сравнить две строки без учёта регистра, часто сначала приводят обе к нижнему регистру. Код работает, но создаёт новые строки и может зависеть от текущей культуры.

if (input.ToLower() == "admin")
{
GrantAccess();
}


Для имён ролей, HTTP-заголовков, ключей конфигурации и технических идентификаторов обычно нужен предсказуемый ordinal-сравнитель.
if (string.Equals(input, "admin",
StringComparison.OrdinalIgnoreCase))
{
GrantAccess();
}


Этот вариант не создаёт две временные строки и сразу показывает намерение. Он сравнивает символы без правил конкретного языка пользователя.

Особенно важно не использовать ToLower или ToUpper при проверке ключей и значений.
bool json = contentType.StartsWith(
"application/json", StringComparison.OrdinalIgnoreCase);


Для отображаемого пользователю текста иногда действительно нужен CurrentCultureIgnoreCase. Но для кода и протоколов чаще всего выбирают OrdinalIgnoreCase.

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

Почему Count() в LINQ может сделать лишнюю работу?

В C# часто нужно проверить, есть ли в последовательности хотя бы один элемент. Иногда для этого пишут так:

if (items.Count() > 0)
{
Console.WriteLine("not empty");
}


Для List<T> это может выглядеть безобидно, но LINQ работает не только со списками. Если items является IEnumerable<T>, Count() может пройти всю последовательность до конца.

На ленивых источниках это особенно неприятно:
var items = GetRows()
.Where(row => row.IsActive);


Проверка через Count() заставит перебрать все подходящие элементы, хотя для ответа достаточно первого.

Когда нужен только факт наличия элемента, лучше использовать Any():
if (items.Any())
{
Console.WriteLine("not empty");
}


Any() останавливается сразу после первого найденного элемента. Это проще по смыслу и часто дешевле по выполнению.

Если коллекция уже лежит в памяти как List<T>, массив или HashSet<T>, можно использовать свойство:
if (users.Count > 0)
{
Send(users);
}


А если действительно нужно точное количество, тогда Count() уместен:
var activeCount = users.Count(u => u.IsActive);


Для вопроса есть ли что-то используй Any(), для точного числа используй Count(), а для готовых коллекций смотри на свойство Count или Length.

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

Шпаргалка по DateTime и форматам дат в C#!

Например, yyyy отвечает за год, MM за месяц, dd за день, HH за часы в 24-часовом формате, а zzz показывает смещение часового пояса.

На картинке собраны частые преобразования дат и времени. Есть Parse, ParseExact, ToString с форматами, Unix timestamp, DateTimeOffset и примеры конвертации между строками, DateTime и DateTimeOffset.

Сохрани, чтобы не потерять!

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

Почему record в C# не делает объект полностью неизменяемым?

record часто воспринимают как “immutable-класс”:

public record User(string Name, List<string> Roles);


Но неизменяемым становится не всё содержимое объекта.

Свойство Roles нельзя просто заменить, если оно init-only:
var user = new User("Ann", new List<string>());


Но сам список внутри всё ещё изменяемый:
user.Roles.Add("admin");


То есть record защищает ссылку на список, но не защищает сам список от изменений.

Это особенно важно, если объект передаётся между слоями приложения:
public record Profile(string Name, List<string> Permissions);


Код снаружи может сохранить ссылку и позже поменять содержимое:
var roles = new List<string> { "user" };
var profile = new Profile("Ann", roles);

roles.Add("admin");


Если нужен более безопасный вариант, лучше принимать IReadOnlyList:
public record Profile(
string Name,
IReadOnlyList<string> Permissions
);


А внутри создавать копию:
var profile = new Profile(
"Ann",
roles.ToArray()
);


Для публичных моделей это часто снижает риск случайных мутаций.

record даёт удобное сравнение и init-свойства, но не делает вложенные коллекции immutable. Изменяемые поля всё равно нужно контролировать отдельно.

➡️ C# Ready | #совет

Читать полностью…

C# Ready | Unity

Полезная статья про бенчмарки в одном C# файле!

Автор показывает, как запускать BenchmarkDotNet без полноценного проекта. Идея особенно удобна для маленьких проектов, когда нужно быстро проверить информацию по производительности.

В статье автор показывает:
• как работают однофайловые приложения в .NET 10
• почему BenchmarkDotNet не стартует из коробки
• как использовать in-process режим и какие у него ограничения

Продолжай читать на Habr


➡️ C# Ready | #статья

Читать полностью…

C# Ready | Unity

Собираем простой таймер в C#!

Иногда нужно запустить действие через задержку: показать уведомление, выполнить повторную проверку или отложить сохранение. Но важно уметь отменить задачу, если она уже не нужна.

Создадим CancellationTokenSource:

using var cts = new CancellationTokenSource();


Теперь напишем отложенное действие:
static async Task RunLater(
TimeSpan delay,
Func<Task> action,
CancellationToken token)
{
await Task.Delay(delay, token);
await action();
}


Запуск выглядит так:
var task = RunLater(
TimeSpan.FromSeconds(3),
() => SaveAsync(),
cts.Token
);


Если пользователь изменил данные ещё раз, старую задачу можно отменить:
cts.Cancel();


Чтобы отмена не выглядела как ошибка, обработаем её отдельно:
try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("Задача отменена");
}


Такой подход удобно использовать для debounce-сценариев: например, не сохранять форму после каждого символа, а ждать короткую паузу.

Пример с новым запуском:
cts.Cancel();

using var nextCts = new CancellationTokenSource();
await RunLater(
TimeSpan.FromSeconds(1),
() => SaveAsync(),
nextCts.Token
);


В реальном приложении вместо SaveAsync может быть отправка запроса, пересчёт фильтра или обновление превью.

➡️ C# Ready | #практика

Читать полностью…

C# Ready | Unity

Ограничиваем параллельные запросы через SemaphoreSlim!

Иногда нужно обработать много элементов асинхронно, но не запускать всё сразу. Например, нельзя одновременно ударить сотнями запросов по API или базе.

Наивный вариант запускает все задачи сразу:

var tasks = urls.Select(DownloadAsync);
await Task.WhenAll(tasks);


Если urls много, приложение может упереться в лимиты сети, rate limit или пул соединений.

Для ограничения параллельности удобно использовать SemaphoreSlim:
var gate = new SemaphoreSlim(4);


Перед началом работы задача ждёт свободное место:
await gate.WaitAsync();


После завершения место обязательно освобождается через finally:
try
{
await DownloadAsync(url);
}
finally
{
gate.Release();
}


Теперь собираем задачи так, чтобы одновременно работало не больше четырёх:
var tasks = urls.Select(ProcessAsync);
await Task.WhenAll(tasks);


Такой подход подходит для скачивания файлов, вызовов внешних API, фоновой обработки сообщений и любых задач, где важна скорость, но нельзя терять контроль над нагрузкой.

➡️ C# Ready | #практика

Читать полностью…
Subscribe to a channel