23284
Все самое полезное для C#-разработчика в одном канале. По рекламе: @proglib_adv Учиться у нас: https://proglib.io/w/b60af5a4 Для обратной связи: @proglibrary_feeedback_bot РКН: https://gosuslugi.ru/snet/67a5c81cdc130259d5b7fead
💡 Шпаргалка по форматам DateTime в C#
В C# форматирование даты и времени строится вокруг строк шаблонов: ими можно красиво вывести DateTime, DateTimeOffset и при необходимости точно распарсить строку обратно в объект.
Для повседневной работы чаще всего хватает нескольких шаблонов и пары правил, но именно они спасают от путаницы в датах и времени.
Вот короткая шпаргалка по самым полезным символам:
dd - День с нулем
ddd - Короткий день недели
dddd - Полный день недели
MM - Месяц с нулем
MMM - Короткий месяц
MMMM - Полный месяц
yy - Год две цифры
yyyy - Год четыре цифры
HH - Часы 24
hh - Часы 12
mm - Минуты
ss - Секунды
fff - Миллисекунды
tt - AM/PM
zzz - Часовой пояс
var dt = new DateTime(2026, 4, 7, 17, 34, 12);
dt.ToString("yyyy-MM-dd") // 2026-04-07
dt.ToString("dd.MM.yyyy") // 07.04.2026
dt.ToString("HH:mm:ss") // 17:34:12
dt.ToString("dddd, dd MMMM") // вторник, 07 апреля
var text = "2026-04-07 17:34:12";
var parsed = DateTime.ParseExact(text, "yyyy-MM-dd HH:mm:ss", null);
👀 Управление видимостью памяти
Баги в многопоточном коде редко связаны с логикой. Чаще проблема в видимости данных. Процессор, компилятор и среда выполнения переупорядочивают операции ради производительности. Без явных барьеров один поток может никогда не увидеть изменения, которые сделал другой. Именно это решает Volatile.
Как это работаетVolatile.Read и Volatile.Write расставляют барьеры памяти в нужных местах:
• запись до Volatile.Write не может быть перенесена после неё
• чтение после Volatile.Read не может быть перенесено до него
На практике это значит: когда один поток устанавливает флаг, другие потоки рано или поздно увидят актуальное значение:
private int _flag;
public void Set()
{
Volatile.Write(ref _flag, 1);
}
public bool IsSet()
{
return Volatile.Read(ref _flag) == 1;
}
Volatile не захватывает монитор и не блокирует поток. Это просто барьер памяти. Накладные расходы минимальны по сравнению с полноценной блокировкой.Volatile гарантирует только видимость, не атомарность.if (Volatile.Read(ref _initialized) == 0)
{
Initialize();
Volatile.Write(ref _initialized, 1);
}
Volatile гарантирует, что другие потоки увидят _initialized == 1 после того, как инициализация завершится._initialized == 0 и установкой _initialized = 1 другой поток уже успеет войти и тоже вызовет Initialize(). Здесь нужен Interlocked или полноценный lock.Volatile подходит, когда один поток пишет, остальные читают. Как только несколько потоков начинают писать, то нужна атомарность, и тут Volatile уже не справится.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#garbage_collector
👨💻 Маскировка данных в .NET
Утечки в логах это один из самых распространённых и незаметных compliance-рисков. Пароль в трейсе, email в structured log, номер карты в HTTP-дампе. Всё это копится в Kibana и ждёт своего часа.
Moongazing.Veil — библиотека для .NET 8/9/10, которая закрывает эту дыру декларативно, на всех уровнях сразу.
Три пакета:
# ядро
dotnet add package Moongazing.Veil
# HTTP middleware
dotnet add package Moongazing.Veil.AspNetCore
# structured logs
dotnet add package Moongazing.Veil.Serilog
Veil.Mask("john.doe@gmail.com");
// j******e@g****.com
Veil.Mask("5425123456789012");
// 5425 **** **** 9012
Veil.Mask("Bearer eyJhbGci...");
// Bearer eyJh***...public class CustomerDto
{
public string Name { get; set; }
[Veiled]
public string Email { get; set; }
[Veiled(Show = 4, Position = VeilPosition.Last)]
public string CardNumber { get; set; }
}
var masked = Veil.MaskObject(original); // original не трогается
app.UseVeilRedaction(); // middleware в pipeline
// конфиг:
http.RedactHeaders("Authorization", "X-Api-Key");
http.RedactBodyFields("$.password", "$.creditCard");
http.RedactQueryParams("token", "api_key");
Log.Logger = new LoggerConfiguration()
.Destructure.WithVeil()
.Enrich.WithVeilRedaction()
.WriteTo.Console()
.CreateLogger();
Log.Information("User {@User}", user) начинают маскировать данные автоматически. Ни один вызов переписывать не нужно.
👨💻 Первый дайджест апреля
Пошутили и хватит.
— Разработчик написал программу для проигрывателя винила
— OpenFeature для .NET
— dotnet-adr для архитектурных заметок
— C# 15 Union Types
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#async_news
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#garbage_collector
📎 Union-типы в действии
Мы уже рассмотрели что такое union-тип, теперь пора подумать где это использовать.
Бывает, что API принимает как одиночное значение, так и коллекцию. Union с телом позволяет добавить вспомогательный метод прямо в объявление:
public union OneOrMore<T>(T, IEnumerable<T>)
{
public IEnumerable<T> AsEnumerable() => Value switch
{
T single => [single],
IEnumerable<T> multiple => multiple,
null => []
};
}
OneOrMore<string> tags = "dotnet";
OneOrMore<string> moreTags = new[] { "csharp", "unions", "preview" };
foreach (var tag in tags.AsEnumerable())
Console.Write($"[{tag}] ");
// [dotnet]
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;
public interface IUnion
{
object? Value { get; }
}
}
👀 C# 15 Union Types: наконец-то закрытые типы в языке
В C# 15 появился ключевой синтаксис union. Он решает давнюю проблему: когда метод должен вернуть одно из нескольких возможных значений, раньше выбор был невелик. object не накладывает никаких ограничений, маркерные интерфейсы нельзя «запечатать», а базовые классы требуют общего предка. Union types убирают все эти ограничения.
Что это и как работаетUnion-тип объявляет закрытое множество допустимых типов. Компилятор знает полный список, поэтому проверяет исчерпываемость switch-выражений прямо при сборке.
Простейший пример:
public record class Cat(string Name);
public record class Dog(string Name);
public record class Bird(string Name);
public union Pet(Cat, Dog, Bird);
Pet pet = new Dog("Rex");
Console.WriteLine(pet.Value); // Dog { Name = Rex }switch по такой переменной не требует ветки default или _. Если вы позже добавите четвёртый тип в объявление union, компилятор выдаст предупреждение в каждом месте, где не хватает обработчика.string name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
👀 Разрешение перегрузок в C#
Фрагмент кода выглядит как задача с подвохом. Два метода, оба принимают null. Какой вызовется:
void Print(string text) => Console.WriteLine("String");
void Print(object obj) => Console.WriteLine("Object");
Print(null);null совместим и со string, и с object, потому что оба являются ссылочными типами и принимают null. Выбор делается по принципу наибольшей специфичности: из нескольких подходящих перегрузок выбирается та, чей параметр является более производным типом. string наследует от object, значит string более специфичный тип. null к нужному типу: Print((object)null); // выведет "Object"
Print((string)null); // выведет "String"
💡 Фиксируйте архитектурные решения прямо в репозитории
Architectural Decision Records это короткие Markdown-файлы, которые фиксируют контекст, само решение и последствия. Не многостраничная спецификация, а что-то ближе к протоколу встречи. Читается за минуты, но объясняет «почему» лучше любого комментария в коде.
Проблема большинства существующих инструментов для ADR в том, что шаблоны зашиты в сам инструмент. Поменяла команда подход к документированию, нужно ставить другой инструмент.
dotnet-adr это .NET Global Tool, который отделяет сам инструмент от шаблонов. Шаблоны живут как NuGet-пакеты: их можно менять, публиковать свои и раздавать внутри организации через приватный feed.
Установка:
dotnet tool install -g adr
adr templates package set adr.templates
adr templates package install
adr new "Use PostgreSQL instead of MongoDB"
{
"path": "./Docs/Adr"
}adr new "Switch to Cosmos DB" -i 3
🤩 Подборка вакансий для шарпистов
C# Backend Developer — от 180 000 ₽ гибрид в Санкт-Петербурге
Unity разработчик — до 4 500 €, гибрид в Алматы
Fullstack-разработчик (C# / React Native) — удалёнка или гибрид в Пензе
➡️ Еще больше топовых вакансий — в нашем канале C# Jobs
🐸 Библиотека шарписта
🚩 OpenFeature для .NET
Смена провайдера feature flags обычно означает переписывание интеграции. OpenFeature это открытый стандарт под крылом CNCF, который даёт единый vendor-agnostic API: меняете провайдера, меняете одну строчку, код не трогаете.
Установка
dotnet add package OpenFeature
await Api.Instance.SetProviderAsync(new InMemoryProvider());
var client = Api.Instance.GetClient();
bool v2Enabled = await client.GetBooleanValueAsync("v2_enabled", false);
if (v2Enabled)
{
// новая логика
}
// Глобально
EvaluationContext ctx = EvaluationContext.Builder()
.Set("region", "us-east-1")
.Build();
Api.Instance.SetContext(ctx);
// Или прямо в вызове
bool flagValue = await client.GetBooleanValueAsync(
"some-flag", false, reqCtx);
// Глобально для всех вызовов
Api.Instance.AddHooks(new ExampleGlobalHook());
// Только для конкретного клиента
client.AddHooks(new ExampleClientHook());
LoggingHook пишет детальные логи через Microsoft.Extensions.Logging.Api.Instance.AddHandler(
ProviderEventTypes.ProviderReady,
(eventDetails) => Console.WriteLine(eventDetails.Type)
);
ProviderReady, ProviderError, ProviderConfigurationChanged.dotnet add package OpenFeature.Hosting
builder.Services.AddOpenFeature(featureBuilder => {
featureBuilder
.AddInMemoryProvider()
.AddHook<LoggingHook>();
});var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy());
await Api.Instance.SetProviderAsync(multiProvider);
public class MyProvider : FeatureProvider
{
public override Metadata GetMetadata() =>
new Metadata("My Provider");
public override Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(
string flagKey, bool defaultValue,
EvaluationContext? context = null, ...)
{
// ваша логика
}
// + ResolveString, ResolveInteger, ResolveDouble, ResolveStructure
}
Api.Instance.SetTransactionContextPropagator(
new AsyncLocalTransactionContextPropagator());
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#garbage_collector
💡 Красивые алгоритмы медленны при малом n
Красивые алгоритмы с хорошей асимптотикой имеют большие константы. O(log n) звучит лучше O(n), но если n=20 — линейный поиск по массиву быстрее бинарного поиска по дереву просто потому, что данные помещаются в кэш процессора и нет накладных расходов на обход структуры.
Допустим, нужно найти обработчик по типу события. Первый импульс это словарь или дерево:
// "Правильное" решение — O(1) lookup
private readonly Dictionary<string, IHandler> _handlers = new()
{
["OrderCreated"] = new OrderCreatedHandler(),
["OrderCancelled"] = new OrderCancelledHandler(),
["OrderShipped"] = new OrderShippedHandler(),
};
// "Наивное" решение — O(n) linear scan
private readonly (string EventType, IHandler Handler)[] _handlers =
[
("OrderCreated", new OrderCreatedHandler()),
("OrderCancelled", new OrderCancelledHandler()),
("OrderShipped", new OrderShippedHandler()),
];
public IHandler? Find(string eventType)
{
foreach (var (type, handler) in _handlers)
if (type == eventType) return handler;
return null;
}
[MemoryDiagnoser]
public class LookupBenchmark
{
private readonly Dictionary<string, int> _dict;
private readonly (string, int)[] _array;
public LookupBenchmark()
{
var data = Enumerable.Range(0, 10)
.Select(i => ($"key{i}", i))
.ToArray();
_dict = data.ToDictionary(x => x.Item1, x => x.Item2);
_array = data;
}
[Benchmark(Baseline = true)]
public int DictLookup() => _dict["key7"];
[Benchmark]
public int ArrayScan()
{
foreach (var (k, v) in _array)
if (k == "key7") return v;
return -1;
}
}
📰 Дайджест недели
Последний дайджест марта.
— Generative AI for Beginners .NET v2
— Почти год с Copilot Coding Agent в dotnet/runtime
— Пять типичных ошибок при проектировании интеграции с помощью Kafka
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#async_news
⭐️ Подборка вакансий для шарпистов
Middle Frontend Developer (C#, WPF) — гибрид в Нови-Саде, Сербия
C#/.NET Junior Developer — офис в Ростове-на-Дону
Backend .NET developer ( Middle/Middle+) — удалёнка
➡️ Еще больше топовых вакансий — в нашем канале C# Jobs
🐸 Библиотека шарписта
🛠 Регистрация сервисов в .NET
Три способа зарегистрировать сервис в .NET отличаются одним: как долго живёт экземпляр.
Transient — новый экземпляр при каждом обращении к контейнеру. Подходит для лёгких, stateless-сервисов. Для тяжёлых объектов с дорогой инициализацией будет дорого.
Scoped — один экземпляр на HTTP-запрос. Правильный выбор по умолчанию. DbContext работает именно так: отслеживает сущности в рамках одного запроса и утилизируется по его завершении.
Singleton — один экземпляр на всё время жизни приложения. Подходит только для stateless-сервисов или тех, где всё изменяемое состояние явно защищено для параллельного доступа.
Сервис с длинным жизненным циклом не должен зависеть от сервиса с коротким. Вот безопасная иерархия зависимостей:
Singleton → может зависеть от → Singleton
Scoped → может зависеть от → Singleton, Scoped
Transient → может зависеть от → Singleton, Scoped, Transient
📎 Когда нужно соединить два мираTaskCompletionSource<T> решает одну конкретную проблему: вы хотите вернуть Task, но не можете использовать async/await, потому что результат приходит через коллбэк, событие или другой внешний сигнал.
Пример с коллбэк-API
Допустим, есть метод BeginOperation, который работает по старинке через onSuccess и onError. Оборачиваем его в нормальный Task:
public Task<string> GetDataAsync()
{
var tcs = new TaskCompletionSource<string>(
TaskCreationOptions.RunContinuationsAsynchronously);
BeginOperation(
onSuccess: result => tcs.SetResult(result),
onError: ex => tcs.SetException(ex));
return tcs.Task;
}
await GetDataAsync() и не знает, что внутри коллбэки.RunContinuationsAsynchronouslySetResult. В сложных системах это может привести к неожиданной реентерабельности или дедлоку.TaskCompletionSource проявляется, когда нужно синхронизировать несколько частей системы:public Task WaitForSignalAsync()
{
return _signalTcs.Task;
}
public void Signal()
{
_signalTcs.TrySetResult();
}
await WaitForSignalAsync(), другой вызывает Signal() когда готов. Это базовый паттерн для кастомных async-локов, очередей и event-систем.Task, реализовать собственный примитив синхронизации, или управлять завершением задачи вручную из внешнего кода. Если ситуация стандартная и async/await справляется, TaskCompletionSource лучше не трогать.
📎 Гибридный кэш для .NET с защитой от типичных проблем
Кэширование в .NET часто выглядит так: либо IMemoryCache для одного узла, либо Redis для распределённых сценариев. А если нужно и то, и другое одновременно, с нормальной устойчивостью к сбоям, то приходится писать обёртки самим.
FusionCache это гибридный кэш для .NET с открытым исходным кодом. Он работает как двухуровневый кэш: L1 в памяти и L2 в распределённом хранилище. Переключение между режимами прозрачно, поэтому код менять не нужно.
Минимальный пример без DI:
var cache = new FusionCache(new FusionCacheOptions());
var product = cache.GetOrSet<Product>(
$"product:{id}",
_ => GetProductFromDb(id),
TimeSpan.FromSeconds(30)
);
services.AddFusionCache()
.WithDefaultEntryOptions(new FusionCacheEntryOptions()
.SetDuration(TimeSpan.FromMinutes(2))
.SetPriority(CacheItemPriority.High)
.SetFailSafe(true, TimeSpan.FromHours(2))
.SetFactoryTimeouts(
TimeSpan.FromMilliseconds(100), // soft timeout
TimeSpan.FromSeconds(2) // hard timeout
)
);
🗂 Гайд: где ещё искать работу в IT
Cобрали 30 джоб-сайтов на любой вкус: для джунов с первым pet-проектом, для мидлов в поиске удалёнки, для тех, кто хочет работать в геймдеве или уехать в Европу.
Каждый сайт описан коротко и по делу: что за аудитория, какие вакансии, для какого грейда подходит. Отдельно платформы для стажировок и первой работы, и для тех, кто ищет валютную удалёнку.
➡️ Искать работу
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
👨💻 Данные важнее алгоритмов
Большинство разработчиков переоценивают алгоритмы. Принято считать, что сложная логика и умные решения делают код хорошим. Но на практике всё решается на этапе выбора структуры данных.
Почему структура данных первична
Когда данные организованы правильно, алгоритм становится очевидным. Его не нужно изобретать, ведь он вытекает из формы данных сам. Роб Пайк сформулировал это ещё в 1989 году, и с тех пор ничего не изменилось.
Если ясно, кто владеет данными, как они перемещаются по системе и как структура соответствует паттернам доступа, сложные части кода упрощаются без дополнительных усилий.
Что происходит, когда структура выбрана плохо
Состояние расползается по десяткам мест. Объекты мутируют там, где не должны. Каждое изменение ломает что-то в трёх других местах. Команда тратит время не на новые фичи, а на поиск причин, почему старые перестали работать.
Умный алгоритм, написанный поверх плохо организованных данных, не решает проблему. Он только откладывает её.
Что меняется после переосмысления структуры
Когда команда задаёт правильный вопрос: не "как это починить?", а "как данные должны течь через систему?", запутанный код превращается в понятный пайплайн. Становится короче. Баги исчезают. Новые фичи добавляются без страха сломать существующее.
Никакой магии. Просто правильно выбранная структура снимает нагрузку с алгоритма.
Практический ориентир
Перед тем как писать функцию, полезно ответить на четыре вопроса. Кто владеет этими данными. Где находится источник правды. Как данные попадают из точки А в точку Б. Соответствует ли структура тому, как к ней будут обращаться.
Если ответы нечёткие, сложность будет нарастать независимо от качества кода.
💬 Что думаете? Прав ли Роб Пайк или всё уже поменялось?
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#entry_point
✏️ Перегрузка и переопределение
На технических интервью по C# для джунов и иногда для мидлов вопрос про перегрузки и переопределения задаётся одним из первых.
Оба механизма связаны с методами и их именами. Оба выглядят похоже на первый взгляд. Но работают они в совершенно разных ситуациях и решают разные задачи.
Разница между ними принципиальная. Один работает на уровне компиляции, другой на уровне выполнения программы. Один не требует наследования, другой без него невозможен.
➡️ Как ответить на собесе
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#dotnet_challenge
👨💻 Баги, которые уничтожат ваших пользователей
Большинство проблем с многопоточностью выглядят одинаково везде. Но часть из них появляется только в конкретной среде. WinForms, WPF и ASP.NET имеют свои контексты синхронизации, и если их игнорировать, получаем краш или дедлок там, где вроде бы всё выглядело нормально.
Что идёт не так
В WinForms и WPF UI-компоненты не являются потокобезопасными. Обновлять их можно только из UI-потока. Если фоновый поток попытается напрямую записать что-то в label.Text или textBox.Value, получаем InvalidOperationException. В WPF для этого используется Dispatcher.BeginInvoke, в WinForms — Control.Invoke или Control.BeginInvoke.
Казалось бы, очевидное правило. Но баг всё равно появляется: чаще всего тогда, когда разработчик делает Task.Run, внутри него обращается к UI, а анализатор это не видит.
С Dispatcher.Invoke другая история. Это синхронный вызов, он блокирует текущий поток до завершения. Если вызвать Invoke из самого UI-потока или из кода, который UI-поток уже ждёт, получаем дедлок. Правило простое: почти всегда нужен BeginInvoke (асинхронный), а не Invoke.
Как находить такие баги до прода
Статические анализаторы: Roslyn, AsyncFixer, Microsoft.VisualStudio.Threading.Analyzers и ThreadSafetyAnalyzer умеют находить прямые обращения к UI из неправильного потока, синхронные блокировки async-методов и потенциальные дедлоки в диспетчере.
Подключить их можно через NuGet:
dotnet add package Microsoft.VisualStudio.Threading.Analyzers
dotnet add package AsyncFixer
✌🏻 У нас две новости — хорошая и плохая!
Хорошая: Ваших знаний, скорее всего, хватит, чтобы собрать рабочую демку AI-агента в Colab. 🫡
Плохая: Вы вряд ли выведете его в прод, не обанкротившись на токенах и не слив базу. 🤯
Для защиты от таких сценариев мы полностью пересобрали курс «Разработка AI-агентов». Теперь внутри плотная работа с экономикой ресурсов, дебаг через time-travel в LangGraph, извлечение данных из кривых сканов для RAG и комплаенс по 152-ФЗ.
Если всё ещё сомневаетесь, послушайте голосовое от спикера курса Влада Прошинского, где он объясняет, как правильно тестировать агентов перед релизом.
📎 Task.Run внутри ASP.NET пайплайна
Один из самых распространённых антипаттернов в .NET, который выглядит как хорошая практика, но на деле замедляет систему.
Обычный код:
await Task.Run(() => _logger.LogInformation("Processing..."));
await Task.Run(() => MapToDto(entity));
await Task.Run(() => ValidateHeaders(request));Task.Run внутри ASP.NET запроса:// Логирование — всегда синхронно
_logger.LogInformation("Processing...");
// Маппинг — синхронно
var dto = MapToDto(entity);
// Валидация заголовков — синхронно
ValidateHeaders(request);
// async оставляем только для реального I/O
var data = await _repository.GetAsync(id);
var response = await _httpClient.GetAsync(url);
⚡️ Никаких больше var
Microsoft официально объявила: в C# 15 ключевое слово var признаётся устаревшим.
Команда языка ссылается на исследования читаемости кода: оказывается, явное указание типов снижает когнитивную нагрузку на 34% и ускоряет код ревью. Roslyn уже умеет автоматически выводить тип, но теперь хочет, чтобы это делал и программист.
Миграция через dotnet-upgrade-assistant проставит типы автоматически. Но 40 000 строк кода всё равно ждут вас в ближайшем будущем.
➡️ Источник
Попались? С первым апреля!😁
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
🚫 Span<T> и async несовместимы Span<T> это ref struct. А ref struct не может существовать в куче. Это не ограничение реализации, это гарантия безопасности по дизайну.
Async-методы компилятор превращает в state machine — объект, который живёт в куче и может приостанавливаться между await-точками. Локальные переменные такого метода становятся полями этого объекта. Поле типа ref struct в объекте на куче — запрещено. Поэтому компилятор просто не даст использовать Span<T> в async-методе.
// Не скомпилируется
async Task ProcessAsync(byte[] data)
{
Span<byte> span = data; // CS4012: Span нельзя использовать в async
await Task.Delay(100);
Process(span);
}
// Упрощённо — что генерирует компилятор
private struct ProcessAsyncStateMachine : IAsyncStateMachine
{
public byte[] data;
public Span<byte> span; // ← невозможно: ref struct не может быть полем
public int _state;
// ...
}
await не гарантирован, потому что поток может смениться, метод может возобновиться на другом потоке. Span на стеке к тому моменту уже не существует.Memory<T> — это то, для чего он и создан. Может жить в куче, передаётся через await, конвертируется в Span в синхронных участках:async Task ProcessAsync(Memory<byte> memory)
{
await Task.Delay(100); // можно
// Span получаем только там, где нет await
Span<byte> span = memory.Span;
Process(span);
}
Memory<T> для хранения и передачи через async-границы, Span<T> для фактической работы с данными в синхронном контексте.async Task<int> ReadAndProcessAsync(Stream stream)
{
// Memory живёт в куче — await доволен
var buffer = new byte[4096];
Memory<byte> memory = buffer;
int bytesRead = await stream.ReadAsync(memory);
// Переходим в sync-контекст — достаём Span
Span<byte> span = memory.Span[..bytesRead];
return CountNewlines(span);
}
static int CountNewlines(Span<byte> data)
{
int count = 0;
foreach (var b in data)
if (b == '\n') count++;
return count;
}
Span<T> — инструмент для горячего пути в синхронном коде. Как только появляется await переходите на Memory<T> и конвертируйте в Span только там, где он нужен непосредственно для вычислений.
🎶 Разработчик написал программу для управления самодельным проигрывателем винила
Разработчик с Reddit строит автоматический проигрыватель пластинок с нуля: механику, электронику и прошивку для STM32. Чтобы тестировать и отлаживать железо в процессе разработки, он написал десктопное управляющее приложение на C#.
Приложение позволяет управлять проигрывателем с компьютера, снимать статистику и диагностировать проблемы на лету — по сути, это инструментарий для разработчика железа, написанный на том же языке, что и обычный бизнес-софт.
Для него это первый опыт написания control software для физического железа и судя по его словам, ощущение от того, что код управляет реальным устройством в реальном мире, совершенно другое.
➡️ Источник
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#entry_point
👨💻 Разделяемое состояние в многопоточке
Кажется, что примитивы атомарны. Это не так в смысле видимости между потоками: процессор и компилятор переупорядочивают инструкции, каждое ядро держит своё значение в кэше:
private bool _cacheLoaded;
// Поток A
_cacheLoaded = true;
// Поток B — может прочитать false, даже если A уже записал true
if (!_cacheLoaded) LoadCache(); // загружается дважды, данные затираются
lock. Подходит для составных операций: «прочитать → изменить → записать» должны выполняться как одно целое:private readonly object _sync = new object();
private int _count;
public void Increment()
{
lock (_sync) { _count++; }
}
volatile. Запрещает кэширование значения в регистре. Не заменяет lock. Только для простого чтения/записи одного поля без зависимостей от других.private volatile bool _cacheLoaded;
Interlocked. Атомарная операция на уровне процессора. Быстрее lock, но только для простых числовых операций:private int _count;
public void Increment()
{
Interlocked.Increment(ref _count);
}
⚙️ Substring или Slice
Substring и Slice выглядят похоже, но работают принципиально по-разному.
Substring — это new string(...). Каждый вызов:
— выделяет новый объект в хипе
— копирует символы в него
— создаёт нагрузку на GC
Slice не создаёт объектов. Это просто новый указатель + длина поверх той же памяти. int.Parse(ReadOnlySpan<char>) читает символы напрямую оттуда.
Частая ошибка
// Так делать не надо — убивает весь смысл
int id = int.Parse(span.Slice(5, 2).ToString());
ToString() на Span создаёт новую строку. Вернулись к исходной проблеме.Substring перед Parse это кандидат на замену.