C# (C Sharp) programming
18.3K subscribers
951 photos
48 videos
8 files
762 links
По всем вопросам- @notxxx1

Реестр РКН: https://clck.ru/3Fk3kb

#VRHSZ
Download Telegram
🖥 Задача


using System;
using System.Collections.Generic;
using System.Threading.Tasks;

var actions = new List<Func<Task>>();

for (int i = 0; i < 3; i++)
{
actions.Add(async () =>
{
await Task.Yield();
Console.Write(i + " ");
});
}

foreach (var action in actions)
{
await action();
}


Что выведет код?

Варианты:


0 1 2
3 3 3
0 0 0
1 2 3

Правильный ответ:

3 3 3

Разбор коротко:

i в for не копируется в каждую лямбду. Все три лямбды захватывают одну и ту же переменную i. Когда цикл закончился, i == 3. Поэтому каждая отложенная async-функция печатает уже финальное значение.

Чтобы получить 0 1 2, нужно создать локальную копию внутри цикла:
```
for (int i = 0; i < 3; i++)
{
int copy = i;

actions.Add(async () =>
{
await Task.Yield();
Console.Write(copy + " ");
});
}
```
Please open Telegram to view this post
VIEW IN TELEGRAM
⚡️ Переписывать legacy-систему редко значит «переписать код». Обычно самая дорогая часть начинается там, где старый и новый мир должны какое-то время жить одновременно.

Типичная проблема - синхронизация данных между старой БД и новой моделью. На бумаге кажется, что можно взять CDC, подключить Debezium, прокинуть события и жить спокойно. На практике это работает только пока у вас почти прямое соответствие: таблица → событие → таблица.

В реальном legacy всё еще хуже.

Одна запись в старой системе может собираться из нескольких агрегатов в новой. Поля могут иметь другой смысл. Часть данных нормализована, часть размазана по справочникам, часть хранится как «магические» статусы. А ещё при переносе нужно не просто скопировать байты, а применить бизнес-правила: пересчитать состояние, отфильтровать мусор, восстановить инварианты, иногда даже специально повторить старый баг, потому что на нём завязан внешний процесс.

Нормальное решение может выглядеть так:

* события из новой системы публикуются через outbox, а не напрямую из хендлера
* синхронизатор читает сообщения из RabbitMQ или другого брокера
* трансформации делаются явно, через application service или отдельный mapping layer
* операции проектируются идемпотентными, потому что повторная доставка будет всегда
* для каждой внешней записи хранится mapping старого и нового идентификатора
* ошибки не теряются, а уходят в retry/DLQ с понятной диагностикой
* консистентность проверяется отдельными reconciliation jobs, а не верой в «оно доедет»

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

Если всё сделано хорошо, этот компонент потом удалят. Он нужен только на период миграции. Но если сделать его плохо, миграция не закончится никогда.
Please open Telegram to view this post
VIEW IN TELEGRAM
OptimizerDuck - open-source утилита, после которой CCleaner уже не нужен

OptimizerDuck собирает в одном приложении 30+ твиков системы: от отключения телеметрии, Copilot, Cortana и рекламного ID до тонкой настройки автозагрузки, служб, питания и задержек ввода.

Укаждой настройки есть рейтинг риска. То есть вы заранее видите, что безопасно применить, а где лучше подумать, вместо классического сценария «нажал всё подряд и потом откатываешь систему».

Что умеет:

* отключать телеметрию Windows, Cortana, Copilot и рекламный ID
* управлять автозагрузкой приложений
* настраивать службы хоста под объём RAM
* включать кастомный план питания для высокой производительности
* снижать задержку клавиатуры для игр
* применять GPU-твики, которые обычно правят вручную через реестр

Все изменения обратимы. Не понравилось, можно откатить назад. можно откатить назад.

https://github.com/itsfatduck/optimizerDuck
Приходи на C# Speed Dating — 2 часа на полезные знакомства

23 июня пройдет вечер коротких онлайн-знакомств для C#-разработчиков.

Как все пройдет
Участники будут рандомно делиться по парам и общаться в Zoom. Будет 6 раундов по 10 минут.

Зачем приходить
— обсудишь темы, которые вызывают споры: AI, карьера, архитектурные паттерны и метрики.
— заберешь идеи и практики, которые работают у других, и поделишься своим опытом.
— найдешь полезные контакты и познакомишься с C#-коммьюнити.

Вечер организуют ребята из Mindbox, но они будут «без оружия»: никакого хантинга и рассказов про вакансии, пока ты сам не спросишь.

📅 23 июня
19:00–21:00 по мск
📍 Zoom (пришлем ссылку после регистрации)

👉 Зарегистрироваться
C# вопрос с собеседований: скомпилируется ли этот код?

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


order._items.AddRange(items);


Поле _items объявлено как private.

Значит ли это, что доступ к нему разрешён только через this._items?

Нет.

Код скомпилируется.

В C# модификатор private ограничивает доступ типом, а не конкретным экземпляром объекта.

То есть любой код внутри класса Order может обращаться к private-полям любого другого экземпляра Order.

Пример:


public class Order
{
private readonly List<OrderItem> _items = new();

public void CopyItemsFrom(Order other)
{
_items.AddRange(other._items);
}
}


Здесь other._items тоже валиден, потому что мы всё ещё находимся внутри типа Order.

Это часто путают на собеседованиях:
private означает не «доступно только этому объекту», а «доступно только коду внутри этого класса».

В примере это используется в static factory method:


var order = new Order { ... };
order._items.AddRange(items);
return order;


Метод Create находится внутри Order, поэтому он имеет полный доступ к private-состоянию создаваемого экземпляра.

Более интересный вопрос тут даже не в компиляции, а в дизайне.

Такой подход часто встречается в DDD:

* private constructor
* static factory method
* закрытая коллекция _items
* наружу отдаётся IReadOnlyList<OrderItem>
* изменение состояния контролируется внутри агрегата

Но есть нюанс.

_items.AsReadOnly() каждый раз создаёт новый wrapper. Обычно лучше кэшировать read-only view или возвращать IReadOnlyCollection<T>, если индексатор не нужен.

Ещё важнее: фабрика должна не просто копировать items, а проверять инварианты:


if (items is null || items.Count == 0)
throw new DomainException("Order must contain at least one item.");


Иначе получается не DDD entity, а просто объект с красивой фабрикой.

Да, код компилируется.
Потому что private в C# работает на уровне типа, а не экземпляра.

А хороший senior-вопрос здесь такой:
скомпилируется ли код - это база.
А вот защищает ли этот Order свои инварианты - уже архитектура.
Fenwick Tree на C#: всё держится на одном битовом трюке

Fenwick Tree, или Binary Indexed Tree, считает prefix sums за O(log n).

Главная операция:


i & -i


Она находит младший установленный бит числа.

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

Пример:


i = 12 // 1100
i & -i = 4 // 0100


Реализация на C#:


public sealed class FenwickTree
{
private readonly int[] _tree;

public FenwickTree(int size)
{
_tree = new int[size + 1];
}

public void Update(int index, int delta)
{
while (index < _tree.Length)
{
_tree[index] += delta;
index += index & -index;
}
}

public int Query(int index)
{
var sum = 0;

while (index > 0)
{
sum += _tree[index];
index -= index & -index;
}

return sum;
}
}


Как это работает:

* Update идёт вверх по структуре и обновляет все узлы, которые отвечают за индекс
* Query идёт вниз и собирает блоки, из которых состоит prefix sum
* index & -index каждый раз выбирает размер текущего блока

Главный нюанс: Fenwick Tree обычно использует 1-based indexing.

То есть первый элемент имеет индекс 1, а не 0.

Пример использования:


var tree = new FenwickTree(5);

tree.Update(1, 10);
tree.Update(2, 20);
tree.Update(3, 30);

Console.WriteLine(tree.Query(3)); // 60


Красота Fenwick Tree в том, что дерево не хранится явно.

• Нет узлов.
• Нет ссылок.
• Нет рекурсии.

Только массив и один битовый трюк.

Дерево спрятано прямо внутри двоичного представления индексов.
Media is too big
VIEW IN TELEGRAM
⚡️ Геймдеверы, обновляемся: Unreal Engine 5.8 уже вышел

Epic Games выпустила Unreal Engine 5.8.

Ссылка:
https://www.unrealengine.com/news/unreal-engine-5-8-is-now-available

Главное обновление для всех, кто следит за AI в геймдеве: в движок добавили поддержку MCP.

Теперь Claude, Gemini и другие AI-агенты могут напрямую подключаться к Unreal Engine, видеть структуру проекта и выполнять задачи внутри редактора. Не просто советовать в чате, а реально работать с сценой.

На демо агент создаёт целый городской квартал прямо в Unreal Editor. Это уже не «ИИ поможет написать промпт», а шаг к агентам, которые собирают уровни, прототипируют локации, правят ассеты и ускоряют production pipeline.

Похоже, поток AI-контента в играх только начинается.

Скачать:
https://www.unrealengine.com/download
Please open Telegram to view this post
VIEW IN TELEGRAM
Аллокации, которых нет в коде: охота на скрытый боксинг в .NET 10

Самая дорогая аллокация в вашем сервисе та, которой нет в исходниках. Вы написали struct ради zero-allocation, прошли code review, а в проде Gen0-коллекции все равно идут косяком. Потому что между вашим кодом и машинным кодом стоит компилятор, и он молча упаковывает ваш value-тип в кучу там, где вы этого не просили — а на код-ревью этого не видно.

TL;DR. Боксинг (boxing) в .NET - это не только object o = 42. Он прячется в вызовах интерфейсных методов на struct, в дефолтном ValueType.Equals, в params object[]-аргументах, в foreach по интерфейсу и в замыканиях. При этом часть “классических” примеров боксинга из старых гайдов на современном рантайме уже не аллоцирует — JIT научился их вырезать, и слепо копировать советы десятилетней давности вредно. Ниже — карта мест, где боксинг живёт и сейчас, отдельный разбор того, что рантайм уже оптимизировал, реальный мини-кейс, воспроизводимый бенчмарк на BenchmarkDotNet с MemoryDiagnoser, способ ловить упаковку через DOTNET_JitDisasm и dotnet-gcdump, и паттерны лечения без потери читаемости.

О версиях и числах. Всё прверялось на .NET 10 (текущий LTS) и C# 13/14-уровне компилятора, Release, без отладчика, BenchmarkDotNet с MemoryDiagnoser. На .NET 8/9 поведение в основном такое же, но отдельные оптимизации JIT отличаются между мажорными версиями — поэтому главный принцип статьи: не верьте на слово (в том числе мне), гоняйте MemoryDiagnoser на своей версии рантайма. Числа в таблицах ниже - иллюстративные, порядок величины, а не точные замеры с вашего железа.

Пролог: “у нас же всё на struct, откуда Gen0?”
Сервис на горячем пути считает метрики: миллионы маленьких readonly struct-значений в секунду, никакого new, никаких классов в hot path. По задумке — ноль аллокаций. На дашборде — стабильный поток Gen0-коллекций раз в несколько секунд под нагрузкой.

Профайлер показывает аллокации, но стек ведёт в метод, где в коде нет ни одного new. Там цикл по интерфейсу, пара вызовов .Equals(), передача значения в params-метод лога. Глазами — чисто. В машинном коде — box-инструкции на каждой итерации.

Это и есть скрытый боксинг: компилятор C# и JIT упаковывают ваш struct в объект на куче, потому что в конкретной точке кода value-тип нужно представить как ссылочный. Симптом — Gen0-коллекции “из ниоткуда”, и его не видно ни в code review, ни в дампе, пока не посмотришь на IL или дизасм.

Если тема близка - я регулярно разбираю такие штуки по C# и .NET (внутренности рантайма, перформанс, неочевидные грабли с замерами и дизасмом) в своём Telegram-канале: t.me/csharp_ci. Заходите, если интересно копаться глубже.

Что такое боксинг и почему он стоит дорого
Боксинг — это упаковка value-типа (struct, enum, примитив) в объект на управляемой куче. Рантайму нужно выделить заголовок объекта, скопировать туда значение и вернуть ссылку. Анбоксинг - обратная операция с проверкой типа.

Цена не в самой инструкции, а в последствиях: каждая упаковка - это аллокация в Gen0. Много мелких аллокаций на горячем пути означают частые Gen0-коллекции, паузы (пусть и короткие), вытеснение полезных данных из кэша и общий рост CPU на ровном месте. На сервисе с SLA по p99 это бьёт по хвосту латентности так же, как и любая другая лишняя аллокация.

В IL боксинг виден явно - инструкция box. Именно её мы и будем искать.

Читать дальше: https://habr.com/ru/articles/1049236/
Тест прошёл. А PostgreSQL вообще в курсе?

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

На локалке всё зелёное. В CI всё зелёное. Моки довольны. In-memory база тоже не против. А потом в проде внезапно выясняется, что реальный PostgreSQL иначе обрабатывает запрос, constraint не даёт сохранить данные, транзакция ведёт себя не так, как ожидалось, а Redis показывает проблему, которую тесты вообще не могли поймать.

Именно поэтому Testcontainers в .NET так хорошо заходят для интеграционных тестов. Вместо имитации базы вы поднимаете настоящий PostgreSQL, Redis или другой сервис в Docker-контейнере, прогоняете приложение против реальной зависимости и удаляете контейнер после тестов.

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

В хорошей схеме контейнеры запускаются через fixture, приложение получает connection string динамически, версии образов фиксируются, а настройка прячется за небольшими helper-классами. Сам тест при этом остаётся читаемым: он проверяет бизнес-сценарий, а не превращается в простыню из настройки базы и очистки состояния.

Есть важный нюанс. Общие fixtures ускоряют тесты, но требуют дисциплины со shared state. Когда изоляция важнее скорости, лучше использовать отдельные fixtures и не ловить фантомные падения из-за данных, оставшихся от соседнего теста.

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

Поэтому в следующий раз, когда интеграционный тест прошёл против in-memory базы, стоит задать неприятный вопрос: а настоящая база с ним согласится?
🖥 На Stepik обновили курс «C# с нуля до профи»

Представьте: через четыре месяца вы открываете чужой .NET-проект и читаете его как книгу.

IServiceCollection не вызывает ступора. async Task<IActionResult> пишется на автомате. Вы точно знаете, почему EF Core сгенерировал именно такой SQL - и как переписать запрос, чтобы он летал.

Это не фантазия. Это результат после 16 модулей, в которых каждая концепция объясняется через код и закрепляется практикой.

ООП, SOLID, LINQ, async/await, DI, EF Core, ASP.NET Core, Docker, Kubernetes - всё, что казалось магией, станет рабочим инструментом.

А бонусом - портфолио проектов: от CLI-утилит и REST API до собственного SaaS с multi-tenancy, JWT и деплоем в Kubernetes под TLS.

Скидка - 58% доступна 48 часов: https://stepik.org/a/282984/
Please open Telegram to view this post
VIEW IN TELEGRAM
Совет по .NET Aspire: не воспринимайте его только как удобную локальную панель.

Самая полезная часть начинается, когда у приложения появляется инфраструктура: API, Postgres, Redis, фоновые сервисы, переменные окружения и connection strings.

Вместо того чтобы вручную собирать docker-compose.yml, опишите сервисы в AppHost. Aspire Docker publisher сможет сгенерировать Compose-артефакты из этой модели.

Но важно понимать границу: Aspire не деплоит приложение за вас.

Он не заменяет CI/CD, не управляет секретами и не переносит контейнеры на сервер. Вам всё равно нужно собрать image, задать реальные env-переменные, скопировать файлы и запустить Docker Compose.

Зато это хороший баланс: меньше ручной YAML-рутины, но без магии, которая скрывает реальную схему деплоя.
🚀 DDD на C#: от теории к микросервису за 6 недель

Если ты пишешь на C# и в какой-то момент начал чувствовать, что вроде всё работает, но как-то костыльно — это тревожный сигнал.
Новая фича затрагивает десятки файлов. Тесты становятся сложнее самого кода. Любое изменение заставляет переживать, что сломается что-то ещё.

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

На курсе по Domain-Driven Design и Clean Architecture на C# ты научишься:

— Отделять бизнес-логику от инфраструктуры
— Организовывать код так, чтобы новые требования не приводили к переписыванию половины сервиса
— Писать тесты, которые проверяют поведение системы, а не набор моков
— Подключать HTTP, gRPC и Kafka без изменений в доменной логике
— Строить сервисы, которые проще поддерживать и развивать

За 6 недель ты соберёшь полноценный микросервис на C# с DDD, Kafka, gRPC и Clean Architecture на реальном кейсе диспетчеризации заказов.

👨‍🏫 Автор курса — Кирилл Ветчинкин, архитектор Авито, ex Staff Engineer Купер, ex Head of Backend BCS Broker.

🎁 Первый модуль доступен бесплатно.

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

Посмотри демо-модуль и оцени, насколько этот подход подходит для твоих проектов:
https://microarch.ru/courses/ddd/languages/csharp?utm_source=posev&utm_medium=erid:2VtzqwrtmqB&utm_campaign=3

Реклама. ИП Ветчинкин К.Е. ИНН: 773376451099 Erid: 2VtzqvzVXjP
Задачка C#

Зачем указывать RunContinuationsAsynchronously у TaskCompletionSource?

A - Чтобы продолжения выполнялись синхронно при SetResult

B - Чтобы не исполнять продолжения синхронно в потоке SetResult, а планировать их асинхронно, избегая дедлоков и глубоких стеков

C- Чтобы запретить отмену задач

D- Чтобы обойти планировщик и ускорить завершение
Классическая задача на собеседовании: вывести бинарное дерево по уровням.

На входе дерево:

1
2 3
4 5 6

Нужно не просто пройти узлы, а напечатать каждый уровень с новой строки.

Первое, что вспоминается, это BFS. Для обхода в ширину идеально подходит Queue<T>: кладём корень, достаём узел, добавляем его детей, повторяем.

Так легко получить порядок:

1 2 3 4 5 6

Но настоящая часть задачи начинается дальше: как понять, где закончился уровень?

Есть два нормальных варианта:

• хранить вместе с узлом его уровень
• на каждой итерации брать queue.Count и обрабатывать ровно столько узлов текущего уровня

Второй способ часто чище: размер очереди в начале цикла и есть количество элементов на текущем уровне.

Такие задачи редко проверяют «знание деревьев ради деревьев».

Они проверяют другое: умеешь ли ты разложить проблему, выбрать структуру данных и аккуратно контролировать состояние.

Для C# это отличный мини-тест на мышление, работу с Queue<T> и понимание алгоритмов без магии фреймворков.
Globbing. - это удобный способ искать файлы по маскам, без ручного перебора папок и костылей со строками.

Например:

**/*.cs - все C# файлы во всех вложенных папках
wwwroot/**/*.js - все JS-файлы внутри wwwroot
!bin/** и !obj/** - исключить мусорные директории сборки

Где это полезно:

• генерация списков файлов
• поиск конфигов
• обработка шаблонов
• сборка ассетов
• утилиты для проектов
• backend/frontend tooling

Это не Regex. Globbing проще, читаемее и отлично подходит для файловой структуры.

Backend пример:
https://github.com/karenpayneoregon/vs2026-how-to/blob/f1c136c864b05ec9eec91e120997314d978b0966/CommonLibrary/GlobbingOperations.cs?plain=1#L6C15-L6C15

Frontend пример:
https://github.com/karenpayneoregon/vs2026-how-to/blob/f1c136c864b05ec9eec91e120997314d978b0966/ExperimentsApp/Classes/GlobbingCode.cs?plain=1#L20C37-L20C37
Методы, их перегрузка и расширения. Бесплатный урок специализации «C#-разработчик»

Методы — одна из базовых вещей в C#, без которой невозможно нормально писать, читать и поддерживать код. Но у начинающих разработчиков часто всё смешивается: где обычный метод, где перегрузка, как работает сигнатура, зачем нужны параметры по умолчанию и в каких случаях использовать params.

На открытом уроке 2 июля в 20:00 разберём, что такое метод в C#, как писать собственные методы и как использовать перегрузку без хаоса в коде. Поговорим о сигнатуре метода, параметрах по умолчанию, ключевом слове params и методах-расширениях. На примерах покажем, как эти механики помогают делать код понятнее, гибче и удобнее для повторного использования.

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

👉 Записаться: https://otus.pw/Dbvh/?erid=2W5zFG69SGh

Реклама. ООО "ОТУС ОНЛАЙН-ОБРАЗОВАНИЕ". ИНН 9705100963.
# C# record: что выведет код?

На собеседованиях record часто объясняют как «сравнение по значению». Но есть нюанс, который легко пропустить.


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

var u1 = new User("Alice", new List<string> { "admin" });

var u2 = u1 with { };

u2.Roles.Add("owner");

Console.WriteLine(u1 == u2);
Console.WriteLine(string.Join(", ", u1.Roles));
Console.WriteLine(ReferenceEquals(u1.Roles, u2.Roles));


Что будет в консоли?


True
admin, owner
True


Почему так?

with для record делает не глубокую копию, а поверхностную. Сам объект User скопировался, но List<string> внутри остался тем же самым объектом в памяти.

Поэтому изменение u2.Roles меняет и u1.Roles.

И сравнение тоже остаётся True, потому что оба record указывают на один и тот же список.

Вот почему record не делает модель автоматически immutable. Он только упрощает синтаксис и даёт value-based equality. Если внутри лежат изменяемые reference-типы, их всё равно можно случайно протащить в состояние.

Более безопасный вариант:


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


А для строгой неизменяемости лучше смотреть в сторону immutable collections.
⚡️ Idempotency: важная вещь в REST API

В распределённых системах запрос может не дойти, ответ может потеряться, клиент может словить timeout и отправить тот же запрос ещё раз.

Если API не готов к такому сценарию, начинаются дубли: два платежа, два заказа, две записи в базе.

Idempotency решает эту проблему.

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

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


Idempotency-Key: 8f7a2c9e-12a4-4f8b-91c2


Сервер проверяет этот ключ.

Если ключ новый, он выполняет операцию и сохраняет результат.

Если ключ уже был, сервер не запускает операцию повторно, а возвращает сохранённый ответ.

Так API нормально переживает повторы, сетевые сбои и retries без случайных дублей.

Особенно важно для платежей, создания заказов, бронирований и любых операций, где повторный запрос может стоить денег.
Please open Telegram to view this post
VIEW IN TELEGRAM