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

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

#VRHSZ
Download Telegram
Приходи на 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