C# Ready | Unity
10.4K subscribers
1.47K photos
84 videos
730 links
Авторский канал по разработке на C# и Unity.
Ресурсы, гайды, задачи, шпаргалки.
Информация ежедневно пополняется!

Cотрудничество: @energy_c

РКН: https://clck.ru/3SBaT3
Download Telegram
Разбираем 7 инструментов HttpClient!

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

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

➡️ C# Ready | #шпора
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9🤝4❤3👍2
Почему копия 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 | #совет
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍4🔥3
Знали, когда в 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 | #совет
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9❤4🔥4
Почему цепочка LINQ иногда читает всю коллекцию до первого результата?

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

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

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

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


➡️ C# Ready | #статья
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6🔥6👍3
Шпаргалка по источникам памяти для Span<T> в C#!

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

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

➡️ C# Ready | #ресурс
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3❤1👍1
Почему 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 | #совет
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5👍3❤2
Читаем большой 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 | #практика
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🔥5❤2
❤8👍2🔥2
Делаем простой 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 | #практика
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤4🔥4
Разбираем System.Threading.Channels в C#!

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

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

➡️ C# Ready | #шпора
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7❤3👍3
Знали, что GetAsync может загрузить всё тело ответа в память ещё до начала обработки?

Для короткого JSON это обычно незаметно. С архивом или большим экспортом ситуация другая, поскольку тело ответа может занять значительную часть памяти процесса.

Обычный вызов ждёт, пока HttpClient буферизует всё содержимое:
using var response = await client.GetAsync(url, cancellationToken);


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

Чтобы получить управление сразу после заголовков, укажи ResponseHeadersRead:
using var response = await client.GetAsync(
url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);


Теперь тело ещё не прочитано целиком. Сначала проверь HTTP-статус, чтобы не начать сохранять страницу с ошибкой вместо файла:
response.EnsureSuccessStatusCode();


После проверки открой поток содержимого ответа:
await using var source = await response.Content.ReadAsStreamAsync(cancellationToken);


Чтение происходит по мере обработки данных. Поток нужно использовать, пока открыт response, потому что он принадлежит этому ответу.

Например, содержимое можно копировать прямо в файловый поток:
await source.CopyToAsync(destination, cancellationToken);


CopyToAsync переносит данные частями, не создавая ещё один массив размером со весь файл. CancellationToken позволяет отменить и сетевой запрос, и копирование.

Если сервер сообщил размер через Content-Length, его можно проверить до сохранения:
if (response.Content.Headers.ContentLength is long size && size > maxBytes)
throw new InvalidOperationException("File is too large");


Заголовок может отсутствовать или быть недостоверным. Для строгого ограничения размера проверяй и фактически прочитанные байты.

➡️ C# Ready | #совет
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7👍5❤4
Собираем защиту webhook от повторной обработки!

Платёжные системы и внешние сервисы могут прислать одно и то же событие несколько раз. Наша задача обработать событие один раз и пропустить повтор.

В этой задаче:
• берём idempotency key из события
• атомарно отмечаем ключ как обработанный
• пропускаем дубликаты
• отделяем проверку от бизнес-логики


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

➡️ C# Ready | #задача
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🔥5❤2