Библиотека шарписта | C#, F#, .NET, ASP.NET
21.9K subscribers
2.83K photos
41 videos
85 files
5.24K links
Все самое полезное для C#-разработчика в одном канале.

По рекламе: @proglib_adv

Учиться у нас: https://proglib.io/w/b60af5a4

Для обратной связи: @proglibrary_feeedback_bot

РКН: https://gosuslugi.ru/snet/67a5c81cdc130259d5b7fead
Download Telegram
📎 Расширение для Visual Studio, которое добавляет все недостающие using

Открываете C# файл, а там десяток красных подчёркиваний. Нажимаете Ctrl+., выбираете нужный namespace, переходите к следующей ошибке, снова Ctrl+.. И так по кругу. Visual Studio умеет добавлять using только по одному. В Rider есть Import Missing References, но это другая IDE.

OhUsings решает эту проблему внутри Visual Studio. Расширение сканирует файл, находит все неразрешённые типы через Roslyn и добавляет нужные using директивы разом. Без ручного перебора.

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

Под капотом OhUsings использует семантическую модель Roslyn, а не regex. Расширение читает диагностики компилятора (CS0246, CS0103, CS0234 и ещё около десятка), извлекает имена типов и находит подходящие namespace через SymbolFinder. Если тип однозначный, using добавляется автоматически. Если тип встречается в нескольких namespace (например, Timer живёт и в System.Timers, и в System.Threading), расширение покажет диалог выбора.

Добавленные директивы сортируются по алфавиту, System.* ставятся первыми. Всё форматируется через Roslyn.

Три области применения

Можно запустить на текущем файле, на активном проекте или на всём solution целиком.

Установка

Самый простой способ: в Visual Studio 2022 откройте Extensions → Manage Extensions, найдите OhUsings и нажмите Download. После перезапуска расширение готово к работе.

Или соберите из исходников:
git clone https://github.com/MabroukMahdhi/OhUsings.git

Откройте OhUsings.sln в Visual Studio 2022, соберите проект и установите полученный OhUsings.vsix.

Как использовать

Три варианта на выбор. Через меню: Tools → OhUsings: Import All Missing Usings. Через контекстное меню: правый клик в редакторе → OhUsings: Import All Missing Usings. Или через light bulb: ставите курсор на неразрешённый тип, нажимаете Ctrl+. и выбираете действие из предложенных OhUsings.

Результат отображается в статусной строке и Output Window.

Настройки

Расширение поддерживает несколько опций. SortUsings (по умолчанию true) отвечает за сортировку директив. PlaceSystemFirst (по умолчанию true) ставит System.* выше остальных. MaxDiagnosticsPerDocument (по умолчанию 200) ограничивает число обрабатываемых диагностик на файл для защиты производительности.

Ограничения

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

Итого

OhUsings экономит время на рутине. Вместо того чтобы обходить каждую ошибку по отдельности, вы запускаете одну команду и получаете все нужные using сразу. Расширение бесплатное, с открытым исходным кодом под MIT лицензией, без телеметрии и сетевых запросов.

➡️ Репозиторий

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
11🥱3
🛠 Монады в C#

Слово «монада» звучит так, будто пришло из учебника по высшей математике. Если вы писали на C#, вы уже пользовались монадами. Task<T>, Nullable<T>, IEnumerable<T> и весь LINQ построены на этом паттерне.

Монада в двух словах

Монада это обёртка над значением, которая умеет делать две вещи.

Первая. Оборачивать значение в контейнер. В C# это конструктор или фабричный метод. Например, Task.FromResult(42) оборачивает число 42 в Task<int>.

Вторая. Прокидывать значение через цепочку операций, где каждая операция тоже возвращает обёртку. В функциональном программировании эту операцию называют Bind. В C# она реализована как SelectMany.

Формально монада должна соблюдать три закона: левая единица, правая единица, ассоциативность; но для повседневной работы достаточно понимать саму идею цепочки.

Nullable как монада

Nullable<T> это контейнер, который либо содержит значение, либо пуст. До C# 8 приходилось писать вложенные проверки на null:
int? a = GetValue();
int? b = null;
if (a.HasValue)
{
b = Transform(a.Value);
}


Монадный подход позволяет выстроить цепочку без ручных проверок. Напишем свой Bind для Nullable<T>:
public static T? Bind<T>(this T? source, Func<T, T?> func)
where T : struct
{
return source.HasValue ? func(source.Value) : null;
}


Теперь вместо вложенных if мы получаем цепочку:
int? result = GetValue()
.Bind(x => Multiply(x, 2))
.Bind(x => Add(x, 10));


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

Task как монада

Task<T> работает по тому же принципу. Метод ContinueWith это грубый аналог Bind, но гораздо удобнее пользоваться async/await.

Компилятор сам выстраивает цепочку продолжений:
var user = await GetUserAsync(id);
var orders = await GetOrdersAsync(user);
var total = await CalculateTotalAsync(orders);


Каждый await разворачивает Task<T>, передаёт значение в следующую операцию и снова заворачивает результат в Task. Это и есть монадная цепочка, просто записанная через синтаксический сахар.

LINQ и SelectMany

LINQ query syntax это буквально монадный синтаксис.

Ключевое слово from вызывает SelectMany под капотом:
var pairs =
from x in new[] { 1, 2, 3 }
from y in new[] { "a", "b" }
select (x, y);


Компилятор превращает это в вызов SelectMany:
var pairs = new[] { 1, 2, 3 }
.SelectMany(
x => new[] { "a", "b" },
(x, y) => (x, y));


SelectMany берёт каждый элемент, применяет функцию, которая возвращает новую коллекцию, и склеивает всё в один плоский результат. Это и есть Bind для IEnumerable<T>.

Свой тип Result как монада

В реальных проектах монады полезны для обработки ошибок без исключений.

Напишем простой Result<T>:
public class Result<T>
{
public T Value { get; }
public string Error { get; }
public bool IsSuccess { get; }

private Result(T value)
{
Value = value;
IsSuccess = true;
}

private Result(string error)
{
Error = error;
IsSuccess = false;
}

public static Result<T> Ok(T value) => new(value);
public static Result<T> Fail(string error) => new(error);

public Result<TOut> Bind<TOut>(Func<T, Result<TOut>> func)
{
return IsSuccess ? func(Value) : Result<TOut>.Fail(Error);
}
}


Теперь цепочка бизнес-логики выглядит так:
var result = Validate(input)
.Bind(SaveToDatabase)
.Bind(SendNotification);


Если валидация упала, SaveToDatabase и SendNotification не выполнятся. Ошибка пробросится по цепочке без try/catch и без вложенных if.

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

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥155👍3
😎 Знакомьтесь с экспертом Proglib.academy: AI-архитектор Андрей Носов

Андрей — один из ключевых спикеров нашего курса AgentOps. Он выстраивает архитектуру, которая выживает в суровом проде и активно делится своим опытом.

За что его ценит IT-комьюнити:

🟣 Топ-спикер AI Conf 2026
Его доклад про мифы семантического поиска и провалы Naive RAG стал одним из самых рейтинговых на конференции.


🟣 Эксперт по GraphRAG и Knowledge Graphs
Андрей внедряет инженерный подход в сложные системы, заменяя «слепую веру» в эмбеддинги строгой логикой графов.


🟣 Автор «14 кругов ада для RAG»
Разработал уникальный набор из 14 unit-тестов, на которых ломается стандартный векторный поиск (от слепоты к отрицаниям до конфликта версий).


🟣 Спикер Saint HighLoad
Регулярно выступает на крупнейших хайлоад-площадках, разбирая архитектуру отказоустойчивых ИИ-сервисов.


Андрей упаковал свои наработки в Google Colab, где можно пощупать 14 сценариев ошибок RAG и их решения:

🔗 Забрать Colab-ноутбук

На курсе Андрей отвечает за самые «мясные» блоки: RAG, оркестрацию агентов и их промышленную эксплуатацию.

Узнать больше о программе и обучении у Андрея:
👉 Курс о том, как внедрять AI-логику в бэкенд и сохранять стабильность сервиса

Так, продолжаем знакомить вас с командой?
👍 — Да, ждем новых лиц
🔥 — Пойду тестить Colab Носова
Please open Telegram to view this post
VIEW IN TELEGRAM
😁1
🎧 Музыка, шум офиса или тишина

Одни не могут написать строчку кода без наушников. Другие считают, что любой звук убивает концентрацию. Третьи спокойно работают под разговоры коллег в опенспейсе и не понимают, в чём проблема.

Музыка помогает войти в поток

Самый популярный аргумент. Фоновая музыка создаёт ритуал. Надел наушники, включил плейлист, мозг понял, что пора работать. Многие выбирают lo-fi, эмбиент или саундтреки из игр. Главное условие: без слов, или на языке, который не понимаешь.

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

Тишина как необходимость

Противоположная позиция. Любой звук, даже фоновый, отъедает часть внимания. Когда задача сложная: архитектурное решение, отладка неочевидного бага, работа с незнакомой кодовой базой; мозгу нужны все ресурсы.

Музыка в сложные моменты не помогает, а мешает. Просто мы этого не замечаем, потому что привыкли.

Офисный шум. Кому-то нормально

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

💬 Расскажите, как у вас? Работаете под музыку, в тишине или вам всё равно?

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#entry_point
Please open Telegram to view this post
VIEW IN TELEGRAM
👨‍💻 Паттерн, который заменили DI-контейнером

Абстрактная фабрика даёт интерфейс для создания семейств связанных объектов без привязки к конкретным классам. В книге GoF это один из ключевых порождающих паттернов. В современном .NET он почти не нужен в классическом виде.

Допустим, приложение работает с разными базами данных. Для каждой нужны свои реализации подключения и команды:
public interface IDbFactory
{
IDbConnection CreateConnection();
IDbCommand CreateCommand();
}

public class PostgresFactory : IDbFactory
{
public IDbConnection CreateConnection() => new NpgsqlConnection();
public IDbCommand CreateCommand() => new NpgsqlCommand();
}

public class SqlServerFactory : IDbFactory
{
public IDbConnection CreateConnection() => new SqlConnection();
public IDbCommand CreateCommand() => new SqlCommand();
}


Клиентский код получает IDbFactory и не знает, с какой базой работает. Переключение между PostgreSQL и SQL Server сводится к замене фабрики:
public class OrderRepository
{
private readonly IDbFactory _factory;

public OrderRepository(IDbFactory factory)
{
_factory = factory;
}

public void Save(Order order)
{
using var connection = _factory.CreateConnection();
using var command = _factory.CreateCommand();
// ...
}
}


Выглядит аккуратно. Но посмотрите, сколько кода нужно написать ещё до первой строки бизнес-логики. Интерфейс фабрики, две реализации, и это только для двух продуктов. Добавьте третий, например, IDbParameter и количество классов растёт линейно.

Почему DI-контейнер делает то же самое

DI-контейнер по своей сути и есть абстрактная фабрика. Вы регистрируете конкретные реализации, а контейнер разрешает их по интерфейсу.

Тот же пример без паттерна:
// Program.cs
if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<IDbConnection, NpgsqlConnection>();
}
else
{
builder.Services.AddScoped<IDbConnection, SqlConnection>();
}


Никаких фабричных иерархий. Класс OrderRepository получает IDbConnection через конструктор и не знает, что за ним стоит. Переключение между окружениями (Development, Staging, Production) происходит в одном месте.

Когда нужен выбор в рантайме

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

Достаточно фабричного делегата:
builder.Services.AddScoped<Func<string, IPaymentProvider>>(sp => key => key switch
{
"stripe" => sp.GetRequiredService<StripeProvider>(),
"paypal" => sp.GetRequiredService<PayPalProvider>(),
_ => throw new ArgumentException($"Unknown provider: {key}")
});


В .NET 8 появились keyed services, и стало ещё проще:
builder.Services.AddKeyedScoped<IPaymentProvider, StripeProvider>("stripe");
builder.Services.AddKeyedScoped<IPaymentProvider, PayPalProvider>("paypal");


Получение в конструкторе:
public class CheckoutService
{
private readonly IPaymentProvider _provider;

public CheckoutService(
[FromKeyedServices("stripe")] IPaymentProvider provider)
{
_provider = provider;
}
}


Или через IServiceProvider, если ключ известен только в рантайме:
var provider = serviceProvider
.GetRequiredKeyedService<IPaymentProvider>(userChoice);


Где абстрактная фабрика всё ещё уместна

Паттерн имеет смысл, когда нужно гарантировать совместимость объектов внутри семейства. Например, UI-фреймворк с темами, где кнопка, поле ввода и диалог должны принадлежать одному стилю. Фабрика не даст смешать Material-кнопку с Fluent-диалогом на уровне типов.

Ещё один случай. Код работает вне DI-контейнера. Библиотека, консольная утилита, unit-тесты с ручной подстановкой зависимостей. Там фабрика по-прежнему решает задачу.

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

➡️ Наша новостная подписка никогда не постареет

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10
🔍 Thread.Start в C# — что происходит после вызова

Thread.Start существует с первой версии .NET, и большинство разработчиков знают, что он «запускает поток». Но что именно происходит в момент вызова, как передать данные в поток и почему Thread почти не используют в новом коде — стоит разобрать подробно.

Как это устроено

Thread — это обёртка над потоком операционной системы. Каждый созданный объект System.Threading.Thread соответствует одному OS-потоку со своим стеком (по умолчанию 1 МБ на x64).

Создать поток можно двумя способами — через ThreadStart или ParameterizedThreadStart:
// Без параметра
var t1 = new Thread(new ThreadStart(DoWork));

// С параметром типа object
var t2 = new Thread(new ParameterizedThreadStart(DoWorkWithParam));


После new Thread(...) поток ещё не запущен. Запуск происходит при вызове Start:
t1.Start();          // ThreadStart — без аргумента
t2.Start("hello"); // ParameterizedThreadStart — передаём object


Внутри метода поток получает управление не сразу: ОС ставит его в очередь планировщика. Поэтому код после Start() в вызывающем потоке продолжает выполняться параллельно.

Передача данных в поток

ParameterizedThreadStart принимает object — это неудобно, потому что нужно приводить тип внутри метода:
void DoWorkWithParam(object? state)
{
var message = (string)state!;
Console.WriteLine(message);
}


Более чистый способ — захват переменной через лямбду:
string data = "hello from closure";
var t = new Thread(() => Console.WriteLine(data));
t.Start();


Тут важно понимать: лямбда захватывает ссылку на переменную, а не значение. Если data изменится до того, как поток до неё доберётся, поток увидит новое значение.

Фоновые и foreground-потоки

По умолчанию Thread создаётся как foreground (IsBackground = false). Это значит, что процесс не завершится, пока поток не закончит работу. Если нужно обратное поведение:
var t = new Thread(DoWork);
t.IsBackground = true;
t.Start();


Фоновый поток будет убит принудительно при выходе из основного потока.

Подводные камни

Thread.Abort() удалён начиная с .NET 5 — он бросал ThreadAbortException в произвольном месте и приводил к непредсказуемому состоянию программы. Для отмены нужно использовать CancellationToken.

Совместный доступ к данным между потоками требует синхронизации. Без неё возникают гонки, которые воспроизводятся нестабильно и тяжело отлаживаются:
int counter = 0;
var t1 = new Thread(() => { for (int i = 0; i < 10000; i++) counter++; });
var t2 = new Thread(() => { for (int i = 0; i < 10000; i++) counter++; });
t1.Start(); t2.Start();
t1.Join(); t2.Join();
// counter будет меньше 20000 — это гонка
Console.WriteLine(counter);


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

Пример кода

Корректный вариант с отменой через CancellationToken:
var cts = new CancellationTokenSource();

var t = new Thread(() =>
{
while (!cts.Token.IsCancellationRequested)
{
Console.WriteLine("Working...");
Thread.Sleep(500);
}
Console.WriteLine("Stopped.");
});

t.IsBackground = true;
t.Start();

Thread.Sleep(2000);
cts.Cancel();
t.Join();


Thread.Start запускает OS-поток, но не гарантирует немедленное выполнение. Данные лучше передавать через замыкания, а не через ParameterizedThreadStart. Для большинства задач сегодня используют Task, async/await или ThreadPool — они не блокируют поток в ожидании и экономят ресурсы.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍97❤‍🔥2👏1
🤩 Паттерн, который компилятор реализует за вас

Паттерн Iterator позволяет обходить коллекцию, не раскрывая её внутреннюю структуру. Классическая реализация на C# требовала ручного написания IEnumerator<T> с методами MoveNext(), Reset() и свойством Current.

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

Как это выглядело раньше:
public class NumberCollection : IEnumerable<int>
{
private readonly int[] _items = { 1, 2, 3, 4, 5 };

public IEnumerator<int> GetEnumerator()
{
return new NumberEnumerator(_items);
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public class NumberEnumerator : IEnumerator<int>
{
private readonly int[] _items;
private int _position = -1;

public NumberEnumerator(int[] items)
{
_items = items;
}

public int Current => _items[_position];
object IEnumerator.Current => Current;

public bool MoveNext()
{
_position++;
return _position < _items.Length;
}

public void Reset() => _position = -1;
public void Dispose() { }
}


Отдельный класс, ручное управление индексом, реализация IDisposable. И всё это ради последовательного перебора массива.

Что изменил `yield return`

Теперь компилятор умеет генерировать всю эту машину состояний самостоятельно. Вы пишете yield return, а компилятор создаёт класс с MoveNext(), Current и корректным управлением состоянием за вас.

Тот же результат в несколько строк:
public static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
yield return 4;
yield return 5;
}


Обход ленивый. Каждый элемент вычисляется только при запросе. Это важно, когда вы работаете с большими или бесконечными последовательностями:
public static IEnumerable<int> EvenNumbers()
{
int n = 0;
while (true)
{
yield return n;
n += 2;
}
}

// Берём первые 10 чётных чисел
var result = EvenNumbers().Take(10).ToList();


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

Связка с LINQ

yield return хорошо сочетается с LINQ. Можно строить цепочки обработки, где каждый шаг выполняется лениво:
public static IEnumerable<string> ProcessUsers(IEnumerable<User> users)
{
foreach (var user in users)
{
if (user.IsActive)
yield return user.Name.ToUpper();
}
}

// Или через LINQ
var names = users
.Where(u => u.IsActive)
.Select(u => u.Name.ToUpper());


Оба варианта ленивые. Ни один не создаёт промежуточных коллекций, пока вы не вызовете ToList(), foreach или другой терминальный оператор.

Когда ручной итератор всё ещё нужен

Есть случаи, где yield return не подойдёт. Например, если нужен асинхронный обход с отменой, сложная логика очистки ресурсов при досрочном прерывании, или обход нетривиальных структур вроде графов с контролем состояния обхода. В таких ситуациях ручная реализация IEnumerator<T> или IAsyncEnumerator<T> оправдана.

Но для большинства задач yield return закрывает вопрос полностью. Компилятор C# решил эту задачу ещё в 2005 году. Если вы пишете ручной итератор для простого обхода коллекции, стоит проверить, не делаете ли вы работу, которую компилятор готов взять на себя.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
8👍8
🥸 Подборка вакансий для шарпистов

C# Разработчик — удалёнка.

.NET-разработчик (Senior) — удалёнка.

Разработчик C# (.NET) — тоже удалёнка.

➡️ Еще больше топовых вакансий — в нашем канале C# Jobs

🐸 Библиотека шарписта
Please open Telegram to view this post
VIEW IN TELEGRAM
👨‍💻 Паттерн стратегия в C#. Когда он нужен, а когда нет

Стратегия инкапсулирует семейство алгоритмов и позволяет подменять их на лету. Звучит полезно. На практике это часто превращается в десятки файлов ради замены одной функции. В современном C# для этого есть средства попроще.

Классическая реализация

Стандартный подход выглядит так. Интерфейс ISortStrategy, пять конкретных реализаций, класс-контекст, который принимает стратегию и вызывает её метод.

public interface ISortStrategy
{
void Sort(List<int> list);
}

public class BubbleSortStrategy : ISortStrategy
{
public void Sort(List<int> list)
{
// реализация пузырьковой сортировки
}
}

public class QuickSortStrategy : ISortStrategy
{
public void Sort(List<int> list)
{
// реализация быстрой сортировки
}
}

public class SortContext
{
private ISortStrategy _strategy;

public SortContext(ISortStrategy strategy)
{
_strategy = strategy;
}

public void SetStrategy(ISortStrategy strategy)
{
_strategy = strategy;
}

public void ExecuteSort(List<int> list)
{
_strategy.Sort(list);
}
}


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

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

Что предлагает C# вместо этого

Начиная с .NET 3.5 в языке есть Func<T>, Action<T> и лямбда-выражения. Strategy по сути означает «передай поведение как параметр». А в C# функции и так являются объектами первого класса.

Тот же пример без интерфейса и дополнительных классов:

public class SortContext
{
public void ExecuteSort(List<int> list, Action<List<int>> sortAlgorithm)
{
sortAlgorithm(list);
}
}

// использование
var context = new SortContext();

context.ExecuteSort(numbers, list => list.Sort());
context.ExecuteSort(numbers, list =>
{
// своя логика сортировки
});


Один метод принимает делегат Action<List<int>>. Никаких интерфейсов, никаких отдельных файлов. Поведение передаётся напрямую.

Ещё пример. Допустим, есть расчёт скидки:

public class PriceCalculator
{
public decimal Calculate(decimal price, Func<decimal, decimal> discountStrategy)
{
return discountStrategy(price);
}
}

var calculator = new PriceCalculator();

decimal result = calculator.Calculate(100m, price => price * 0.9m);
decimal vipResult = calculator.Calculate(100m, price => price * 0.8m);


Func<decimal, decimal> принимает цену, возвращает цену со скидкой. Стратегия задаётся в одну строку прямо в месте вызова.

Когда интерфейсы всё-таки нужны

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

public interface IPaymentStrategy
{
bool Validate(PaymentDetails details);
PaymentResult Process(PaymentDetails details);
void Rollback(string transactionId);
}


Здесь три связанных метода. Засунуть их в три отдельных Func можно, но читаемость пострадает. Интерфейс даёт единую точку контракта и нормально тестируется через моки.

Ещё один аргумент за интерфейс: когда реализации регистрируются в DI-контейнере и выбираются в рантайме. Например, разные провайдеры оплаты в зависимости от региона пользователя. Тут интерфейс упрощает конфигурацию и тестирование.

Если стратегия укладывается в одну функцию, используйте Func<T> или Action<T>. Это проще, короче и не требует дополнительных абстракций. Если стратегия включает несколько связанных операций или вам нужна подмена реализаций через DI, интерфейс по-прежнему оправдан. Паттерн не устарел, но применять его стоит осознанно, а не по привычке.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
👍144
🔥 База по экономике токенов и кэшированию от AI Platform Lead из Bitrix24

Знакомьтесь, Сергей Нотевский. AI Platform Lead в Bitrix24.

Он один из ключевых экспертов нашего курса AgentOps. На своих лекциях он детально разбирает экономику AI-агентов, кэширование токенов, LLM-инфраструктуру и вывод генеративных систем в стабильный прод.

Мы попросили Сергея поделиться материалами для тех, кто хочет оптимизировать косты на LLM в проде. Сохраняйте методичку по prefix cache метрике, которая напрямую влияет на ваши деньги.

Как говорят создатели Manus:
“KV-cache hit rate is the single most important metric for a production-stage AI agent.”


🛠 Что внутри методички (комбо из 3 статей + код):
Экономика кэширования — особенности провайдеров и как правильно считать затраты.

Частые анти-паттерны — почему ваш кэш постоянно сбрасывается и вы платите больше.

Кэш в AI-агентах — специфика работы с памятью в автономных системах.


🍒 Вишенка на торте: готовый SKILL для агента, который делает ревью вашего проекта, находит анти-паттерны и предотвращает низкое попадание в кэш.

Забрать комбо-материалы на GitHub

P.S. Если хотите послушать Сергея вживую — ловите его на конференциях Kode Waves (май), Conversations AI и Highload Spb (июнь).

🎁 Акция в честь старта продаж!

Прямо сейчас при покупке Инженерного трека вы получаете полный доступ к материалам курса «Разработка ИИ-агентов» в подарок.

👉 Забрать 2 курса по цене 1 и начать обучение
1
😶 Vibe hiring: статистика, которая объясняет странные отказы

Найм по интуиции и «культурному фиту» вместо реальных навыков — не редкость, а норма для большой части IT-рынка. И это подтверждается исследованиями, а не только ощущениями кандидатов.
 
➡️ Статья разбирает vibe hiring с цифрами

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта
Please open Telegram to view this post
VIEW IN TELEGRAM
📎 Паттерн Visitor. Почему в C# он больше не нужен

Visitor позволяет добавлять операции к иерархии объектов без изменения их классов. Классический пример — обход AST, дерева документа или иерархии фигур. Вы создаёте отдельный объект-посетитель, который «обходит» структуру и выполняет нужное действие для каждого типа.

Долгое время это был единственный нормальный способ реализовать double dispatch в C#. Но начиная с C# 8 появился switch по типам, а в C# 12 эта возможность стала ещё мощнее. И теперь Visitor выглядит как лишняя обвязка.

Как выглядел Visitor

Допустим, есть иерархия фигур и нужно посчитать площадь.

public interface IShape
{
void Accept(IShapeVisitor visitor);
}

public class Circle : IShape
{
public double Radius { get; init; }
public void Accept(IShapeVisitor visitor) => visitor.Visit(this);
}

public class Rectangle : IShape
{
public double Width { get; init; }
public double Height { get; init; }
public void Accept(IShapeVisitor visitor) => visitor.Visit(this);
}

public interface IShapeVisitor
{
void Visit(Circle circle);
void Visit(Rectangle rectangle);
}

public class AreaCalculator : IShapeVisitor
{
public double Result { get; private set; }

public void Visit(Circle circle) =>
Result = Math.PI * circle.Radius * circle.Radius;

public void Visit(Rectangle rectangle) =>
Result = rectangle.Width * rectangle.Height;
}


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

Как это делается через pattern matching

Тот же расчёт площади в одном switch выражении.

public static double CalculateArea(IShape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
_ => throw new ArgumentException($"Unknown shape: {shape.GetType().Name}")
};


Нет интерфейса IShapeVisitor. Нет метода Accept. Нет отдельного класса калькулятора. Логика читается сверху вниз, в одном месте.

Что происходит, когда добавляется новый тип

Это ключевое отличие. Допустим, вы добавили Triangle.

В Visitor нужно добавить метод Visit(Triangle t) в интерфейс IShapeVisitor, потом реализовать его в каждом посетителе. Если забыли — получите ошибку в рантайме. Или хуже — молчаливое некорректное поведение, если базовый класс имеет реализацию по умолчанию.

В switch выражении компилятор выдаст предупреждение, что Triangle не обработан. Вы узнаете об этом до запуска, на этапе сборки.

// CS8509: The switch expression does not handle all possible values
public static double CalculateArea(IShape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height
// Triangle не обработан — компилятор предупредит
};


Когда Visitor всё ещё уместен

Если иерархия типов находится в чужой библиотеке и вы не можете на неё влиять, а switch по типам становится слишком длинным и разбросанным по кодовой базе — Visitor помогает собрать логику в одном месте. Также он полезен, когда операция требует сложного состояния между вызовами, которое неудобно протаскивать через отдельные методы.

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

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
8🤔3
🆚 Minimal APIs vs контроллер

ASP.NET Core предлагает два способа строить HTTP API. Minimal APIs и контроллеры. Microsoft в документации рекомендует Minimal APIs для новых проектов, потому что это меньше кода, меньше конфигурации и выше производительность. Но это не значит, что контроллеры устарели. Это значит, что нужен осознанный выбор.

Когда Minimal APIs подходят лучше

Minimal APIs хорошо работают, когда API небольшой и сфокусированный. Микросервисы, внутренние API, легковесные эндпоинты, vertical slice архитектура. Каждый эндпоинт явно определяет маршрут, нет лишней церемонии с наследованием и атрибутами.

Вот как выглядит чистый эндпоинт на Minimal API:
public static class CreateOrderEndpoint
{
public static IEndpointRouteBuilder MapCreateOrder(this IEndpointRouteBuilder app)
{
app.MapPost("/orders", async (
CreateOrderRequest request,
ICreateOrderUseCase useCase,
CancellationToken cancellationToken) =>
{
var result = await useCase.ExecuteAsync(request, cancellationToken);
return Results.Created($"/orders/{result.OrderId}", result);
});

return app;
}
}


Читаемо, прямолинейно, бизнес-логика вынесена в ICreateOrderUseCase.

Когда контроллеры всё ещё уместны

Контроллеры полезны там, где уже есть устоявшаяся MVC-инфраструктура, стандартизированные фильтры, сложное версионирование или большая команда, привыкшая к этому паттерну. Если в проекте выстроены конвенции вокруг контроллеров и это работает, переписывать на Minimal APIs ради моды нет смысла.

Что изменилось в .NET 10

В .NET 10 Microsoft добавила поддержку валидации для Minimal APIs, включая кастомизацию ответов через IProblemDetailsService. Раньше отсутствие встроенной валидации было одним из главных аргументов против Minimal APIs. Теперь этот аргумент слабее.

Главная ловушка обоих подходов

Плохо организованный Minimal API проект превращается в огромный Program.cs. Плохо организованный проект на контроллерах превращается в папку с пустыми pass-through экшенами. Ни один из подходов не спасёт от слабой архитектуры.

Что выбрать

Для новых микросервисов и сфокусированных API автор статьи рекомендует Minimal APIs с vertical slices. Но не сваливать всё в Program.cs, а организовывать эндпоинты по фичам, выносить бизнес-логику из обработчиков маршрутов, использовать явные контракты запросов и ответов.

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

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
6🔥2
Что на самом деле запрещает ref struct в C#
 
ref struct появился не просто как ограниченная версия обычного struct. За этим стоит конкретная гарантия компилятора о том, где может находиться такой тип в памяти.
 
Подумайте: что произойдёт, если попробовать передать ref struct в лямбду, сохранить в поле класса или сделать boxing?
 
➡️ Правильный ответ

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#dotnet_challenge
Please open Telegram to view this post
VIEW IN TELEGRAM
😁1
📌 Зачем дата-сайентисту матанализ?

Основная компетенция специалиста по Data Science – способность анализировать и интерпретировать данные, а математика является фундаментом для начала работы.

В карточках мы разбираем основные разделы математики, с которых стоит начать изучение специалисту по анализу данных.

Хотите подготовиться к офферу или подтянуть знания? Оставляйте заявку на наш курс по математике для Data Science 💙

P.S. Только до 31 мая на курс (и вообще на все программы Академии) действует СКИДКА 40%

А как у вас дела с высшей математикой?
❤️ — Помню всё
🔥 — Знаю основы
🌚 — Ничего не знаю

🏃‍♀️ Proglib Academy
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🌚1👾1