.NET Разработчик
6.71K subscribers
475 photos
4 videos
14 files
2.38K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 2711. #ЗаметкиНаПолях
Разбираем Сопоставления по Шаблону в C#. Часть 5/5

Части 1, 2, 3, 4

Шаблоны списка и среза
Проверяют последовательность поэлементно. [1, 2, 3] соответствует массиву длиной 3 с точно такими же значениями.

Два вспомогательных элемента выполняют основную работу:
- Шаблон среза .. соответствует нулю или более элементам.
- Шаблон отбрасывания _ соответствует одному элементу.

Например:
- [_, >0, ..] аналогично array.Length >= 2 && array[1] > 0.
- [.., <=0, _] аналогично array.Length >= 2 && array[^2] <= 0.

Шаблон списка может содержать не более одного среза. Использование двух срезов для поиска элемента в любом месте массива не допускается.

Типичный пример использования — алгоритмы с малым количеством элементов, где требуются именованные локальные переменные для первых нескольких элементов:
double Sum(ReadOnlySpan<double> vector) {
switch (vector) {
case []:
return 0;

case [double x]:
return x;

case [double x, double y]:
return x + y;

default:
double sum = 0;
for (int i = 0; i < vector.Length; ++i) {
double v = vector[i];
sum += v;
}
return sum;
};
}

Входными данными для шаблона списка может быть любой элемент, который одновременно является счётным (имеет свойство Length или Count) и индексируемым (поддерживает индексатор и форму ^ - счёт от конца - из операторов индекса и диапазона). Это включает массивы, List<T>, Span<T>, ReadOnlySpan<T>, строки и любой пользовательский тип, который предоставляет необходимые элементы.

Шаблоны списков рекурсивные. Элементы шаблона сами могут быть шаблонами:
bool EndsWithListOfOne(List<List<int>> list) =>
list is [.., [_]];

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

Ловушка выделения памяти
Шаблон var может захватывать результат среза, как в [.. var nMinusOneFirstElems, 37]. Не делайте этого в горячих путях. Компилятор под капотом аллоцирует массив для материализации среза. То же касается строк. Если вы обрабатываете миллионы элементов, выделение памяти на каждое совпадение приведёт к сбою выполнения. Либо откажитесь от захвата среза, либо переключитесь на Span<T>, чтобы компилятор мог избежать выделения памяти.

Известные ограничения
1. Внутри шаблона могут появляться только константы времени компиляции. Значения из конфигурации, базы данных и вычисления времени выполнения не могут. Это главная причина, по которой сопоставление с образцом не используется чаще в бизнес-коде.
2. Предложение when ограничено оператором switch. Вы не можете прикрепить when к оператору if.
3. Шаблон среза может появляться не более одного раза в шаблоне списка.
4. Шаблоны списков с захваченными срезами могут вызывать скрытое выделение памяти.
5. Проверка исчерпываемости не всегда работает. Выражение switch над замкнутой иерархией классов не является доказуемо исчерпывающим для компилятора так, как, например, дискриминированное объединение в F#.

Когда не использовать
Пример ниже с вычислением площади в иерархии классов фигур правильный синтаксически, но демонстрирет антипаттерн:
public static double Area(this Shape shape) =>
shape switch {
Circle c => c.Radius * c.Radius * Math.PI,
Rectangle r => r.Width * r.Height
};

Добавление каждой новой фигуры заставляет искать каждый такой оператор switch в коде. Это классическое нарушение принципа открытости/закрытости. Хуже того, таблица виртуальных методов работает быстрее, чем многократные проверки совместимости типов во время выполнения. Абстрактное свойство Area в базовом классе Shape выигрывает как в плане удобства сопровождения, так и в производительности.

Сопоставление с образцом оправдано, когда:
- Типы, которые вы используете для оператора switch, не связаны наследованием (см. пример с Count в шаблоне типа).
- Вы декодируете структурированные данные, например, парсер или конечный автомат.
- Вы пишете конечный фрагмент условной логики над примитивами, где полиморфизм был бы излишним.

Если вы обнаружите, что добавляете третий тип в оператор switch в объекте предметной области, остановитесь и спросите себя, не является ли правильным решением виртуальный метод.

Источник:
https://blog.ndepend.com/c-pattern-matching-explained/
День 2712. #Оффтоп #AI
Почему Важно Качество Кода, Созданного ИИ Агентом?

Автор оригинала: Steve “Ardalis” Smith

Один из разработчиков, с которым я недавно общался, задал вопрос, который я слышу всё чаще: «Раз уж мы можем создавать код так быстро, имеет ли качество кода такое уж большое значение? Предположим, код соответствует требованиям, подтверждён реальным пользовательским тестированием, тогда какая разница, как он выглядит? Мы просто находимся на другом уровне абстракции. Когда появились языки программирования второго и третьего поколений, вас волновало, какой ассемблерный код они выдают — лишь бы он делал то, что нужно? Поддерживаемость — кем? Какая разница, если ИИ-боты могут переписывать целые блоки кода за минуты? Зачем архитектура? Она для того, чтобы облегчить работу людям. Наблюдаемость — кем? ИИ-боты могут обрабатывать логи почти мгновенно. Тестируемость? ИИ может писать модульные и интеграционные тесты.»

Если всё это соблюдено, имеет ли значение сам код?

Думаю, ответ по-прежнему «да», но по причинам, которые не были очевидны два года назад.

Аналогия с ассемблером
Нас не волнует ассемблерный код, генерируемый компилятором, потому что компиляторы, JIT и компоновщики детерминированы. Если доказано, что фрагмент кода работает правильно, мы уверены, что он будет продолжать работать правильно при каждом запуске. Мы десятилетиями доверяли этим инструментам. Но даже в них есть ошибки. Особенно это касается оптимизирующих компиляторов. Поэтому аналогия не совсем «компиляторы идеальны, LLMs — нет».

Фактическое различие заключается в детерминизме. Ошибка компилятора воспроизводима. Вы можете создать минимальный тестовый пример, отправить отчёт, и в следующем релизе это будет исправлено. Доверие к компиляторам зарабатывалось десятилетиями именно потому, что детерминизм позволял систематически сокращать количество ошибок.

LLMы работают не так. Запрос, который сегодня выдаёт правильный результат, завтра может выдать слегка неверный результат — не потому, что что-то изменилось в модели, а потому, что процесс выборки по своей природе стохастический. Вы не можете создать такое же доверие, как к компиляторам, потому что вы никогда не можете быть уверены, что проблема, которую вы наблюдали ранее, действительно исчезла.

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

Больше не будет бесплатных обедов
Есть вторая, все более практичная причина: ИИ стоит денег, а плохой код требует больше использовать ИИ. Компиляторы не взимают плату за минуту использования. Вас не беспокоит, что создание класса из 50 000 строк кода приведет к большим затратам. А ИИ-агенты платные. Каждый токен, который агент считывает для понимания вашей кодовой базы, стоит денег. И чем запутаннее кодовая база, тем больше токенов ей нужно потратить.

Два года работы над проектом с поддержкой ИИ без структуры — код, добавляемый «на лету» для каждого частного случая, дублирование повсюду, отсутствие связности — и ИИ будет невероятно сложно вносить какие-либо изменения. Ему понадобятся огромные размеры контекста, чтобы просто понять, что происходит. За это вы заплатите токенами и временем. Архитектура «Большого комка грязи» - такая же проблема для LLM, как и для людей.

Люди, отвергающие принципы качества кода как «для людей» (которые не нужны для агентов), вернутся к ним. Потому что затраты на поддержку станут непомерно высокими.

Несколько примеров:
1. Используйте промежуточное ПО для сквозных задач, вместо того чтобы копировать логирование, аутентификацию, блоки try-catch и т.д. в каждую конечную точку или сервис. Это позволяет сохранить фактическую бизнес-логику в этих конечных точках, обработчиках и сервисах более чистой и понятной для людей. Но это не единственная причина. Каждое дублирование этой логики — это дополнительный контекст, который агент должен прочитать, понять и поддерживать в согласованном состоянии. Вы платите за каждый токен.

2. Организуйте систему в инкапсулированные модули с чёткими границами. Не потому, что людям проще ориентироваться (хотя это так), а потому, что хорошо ограниченный модуль позволяет агенту сосредоточиться на небольшом, согласованном фрагменте кодовой базы. Контекст меньше - стоимость ниже.

3. Принципы SOLID, DRY, разделение ответственности — всё это было сформулировано как способы сделать код более понятным и изменяемым для людей. Эти же свойства делают код дешевле и надёжнее для понимания и изменения ИИ-агентами.

Аргумент о том, что «архитектура предназначена только для людей», предполагает, что ИИ-агенты могут идеально обрабатывать сколь угодно большие контексты и действовать на их основе без затрат или ошибок. Это не верно сегодня, и вряд ли будет верно в будущем.

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

Так что аналогия с ассемблером привлекательна. Но результаты компиляции в ассемблер бесплатны, детерминированы и проверены. Код, написанный агентом, не обладает ни одним из этих качеств. Т.е. хорошо структурированная кодовая база — это способ предотвратить съедание ИИ-агентами всего вашего бюджета.

Источник: https://ardalis.com/why-care-about-agent-authored-code-quality/
👍20
День 2713. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

38. Blazor WebAssembly
«Опишите, как бы вы создали клиентское веб-приложение с использованием Blazor WebAssembly? Укажите, как бы вы настроили проект, управляли зависимостями и обрабатывали вызовы API».

Хороший ответ
Создание клиентского веб-приложения с использованием Blazor WebAssembly в .NET включает в себя несколько ключевых шагов для эффективного использования фреймворка Blazor для запуска кода .NET в браузере.

Начнём с создания нового проекта Blazor WebAssembly с помощью .NET CLI:
dotnet new blazorwasm -o MyBlazorApp

Эта команда настроит новый проект автономного приложения Blazor WebAssembly, который включает в себя всё необходимое для начала работы с клиентским приложением Blazor.

В проекте Blazor WebAssembly файл Program.cs используется для настройки сервисов и запуска приложения. Здесь можно настроить внедрение зависимостей, как в серверном приложении ASP.NET Core:
var builder = WebApplication.CreateBuilder(args);

// Добавим HttpClient для вызовов API
builder.Services.AddScoped(sp => new HttpClient
{ BaseAddress =
new Uri(builder.HostEnvironment.BaseAddress) });

var app = builder.Build();
app.Run();


Вызовы API из приложения Blazor WebAssembly обрабатываются с помощью HttpClient. Можно внедрить HttpClient в компоненты для выполнения вызовов REST API:
@inject HttpClient Http

@code {
private List<Product> products;

protected override async Task OnInitializedAsync()
{
products = await Http.GetFromJsonAsync<List<Product>>("api/products");
}
}

Этот компонент получает данные из серверного API и привязывает их к UI, позволяя Blazor реактивно управлять обновлениями интерфейса.

Blazor обрабатывает маршрутизацию на стороне клиента в файле App.razor, определяя, какой компонент отображать в зависимости от URL:
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Не найдено</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Извините, страница не найдена.</p>
</LayoutView>
</NotFound>
</Router>


Основные моменты:
- Оптимизация производительности: поскольку всё приложение работает в браузере пользователя, важно оптимизировать размер и производительность. Эффективными стратегиями являются ленивая загрузка модулей и минимизация размера приложения.
- Безопасность: важно обеспечить безопасную обработку пользовательских данных, особенно при выполнении вызовов API, и убедиться, что любая конфиденциальная логика или обработка данных, требующая защиты, происходит на стороне сервера.

Используя Blazor WebAssembly, разработчики могут использовать существующие навыки и библиотеки .NET для создания многофункциональных интерактивных веб-приложений без использования JavaScript-фреймворков.

Часто встречающийся плохой ответ
«Blazor WebAssembly позволяет обрабатывать всю логику на стороне клиента. А всё, что нельзя сделать на C#, можно сделать через JavaScript-interop».

Почему это неправильно
- Непонимание возможностей и лучших практик Blazor: ответ указывает на недостаток понимания архитектуры Blazor и вариантов его использования. Хотя Blazor позволяет широко использовать C# в браузере, это не означает, что все операции должны выполняться на стороне клиента, особенно те, которые чувствительны к безопасности.

- Чрезмерное полагание на JavaScript-interop: это противоречит одной из главных целей использования Blazor, а именно — позволить разработчикам использовать C# для выполнения задач, обычно предназначенных для JavaScript. Хотя взаимодействие с JavaScript полезно для доступа к API браузера, выходящим за рамки возможностей Blazor, его следует использовать экономно.

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

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

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👎4
День 2714. #ЗаметкиНаПолях #HTML
В URL Можно Использовать Перенос Строки

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

Например, эта ссылка работает в браузерах:
<a href="https://example.com/docs/
url-parsing">Documentation</a> <!-- табы, перенос строки, но не пробелы -->

Браузеры сообщают о внутренней ошибке валидации, когда встречают символы табуляции или перевода строки ASCII в URL, затем удаляют эти символы и продолжают анализ. Таким образом, совместимые парсеры перенаправляют на тот же адрес, что и эквивалентный URL без этих символов. Стандарт WHATWG URL описывает это в базовом алгоритме парсера URL:
«Если входные данные содержат какие-либо символы табуляции или перевода строки ASCII, возникает ошибка валидации invalid-URL-unit. Удалите все символы табуляции или перевода строки ASCII из входных данных.»

Поэтому эти ссылки обычно интерпретируются одинаково:
<a href="https://exa
mple.com/path">A</a>

<a href="https://example.com/path">B</a>


Многострочный URL данных с встроенным SVG
URL данных также могут быть написаны в несколько строк. Это полезно при встраивании читаемого документа SVG:
<img alt="Simple sunset" src='data:image/svg+xml,
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 80">
<rect width="120" height="80" fill="deepskyblue" />
<circle cx="60" cy="45" r="14" fill="gold" />
<rect y="58" width="120" height="22" fill="sienna" />
</svg>' />

Этот пример намеренно отформатирован для удобства чтения. Для производственного кода лучше по-прежнему использовать URL в одну строку или процентное кодирование.

Маскируем mailto: от парсеров, использующих регулярные выражения
Такое поведение можно использовать для маскировки URL, таких как mailto:, от парсеров, использующих простые регулярные выражения:
<a href="mail
to:security@example.com">Замаскированный контакт</a>

Браузер обрабатывает это как mailto:security@example.com, но наивный шаблон, например mailto:[^\"'\s>]+, может не обнаружить его из-за добавленного символа новой строки.

Это важно для парсинга, статического анализа, антиспам-ботов и сканеров безопасности. Любой инструмент, которому необходимо надёжно обнаруживать ссылки, должен анализировать HTML и URL-адреса с помощью стандартного парсера, а не полагаться только на регулярные выражения.

Ограничения и предостережения
- Не все пробельные символы везде правильно обрабатываются.
- Поведение зависит от контекста анализа и схемы URL.
- Некоторые инструменты и валидаторы могут предупреждать или отклонять такую разметку.
- Обфускация не является мерой безопасности. Она нацелена только на слабые парсеры.

В общем случае, пишите обычные однострочные URL-адреса, если у вас нет веской причины форматировать их иначе.

Источник: https://www.meziantou.net/you-can-use-newline-characters-in-urls.htm
👍1
Хайлоад: производительность и планирование мощностей


Приглашаем на практический курс для Middle/Senior-разработчиков, техлидов, архитекторов, EM и CTO, которые хотят не просто “знать про хайлоад”, а руками разобраться, как работают производительность, нагрузочное тестирование и масштабирование.

Будем выжимать 20–100K RPS из своих сервисов на своей инфраструктуре, строить latency/RPS-диаграммы, искать ограничения в стеке и использовать эти данные для capacity planning. В программе: Linux-инфраструктура, nginx, Prometheus/Grafana, нагрузочное тестирование через wrkx, тюнинг производительности, планирование мощностей.

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

📌 Старт потока 13 июля.

Кто мы: R&D-центр Devhands, основатель и автор курса Алексей Рыбак, ex-СТО Badoo и Yum! Brands, член программного комитета Highload.

Изучайте программу и записывайтесь.

Реклама. ИП Рыбак А.А. ИНН 771407709607 Erid: 2VtzquZx6p5
#реклама
👍1
День 2715. #ЧтоНовенького #NET11
Закрытые Иерархии Классов. Начало

Закрытая иерархия классов может быть определена только в рамках одной сборки. Попытка создания производного класса от закрытого класса из другой сборки приведёт к ошибке компиляции.

Пусть у нас есть иерархия классов в одной сборке:
// Закрытый базовый класс 
public closed class Animal { }

// Все наследуют от закрытого класса
public class Dog : Animal { }
public class Cat : Animal { }
public class Horse : Animal { }

Здесь обычное наследование C#, ключевое слово closed никак не влияет.
Разница ощущается, если попробовать наследовать от Animal из другой сборки. Этот выдаст ошибку компиляции:
error CS9382: '…' cannot use a closed type 'Animal' from another assembly as a base type. (класс не может использовать закрытый тип 'Animal' из другой сборки в качестве базового типа).

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

Ведь это можно было сделать с помощью приватных конструкторов?
Имея приватный защищенный (private protected) или внутренний (internal) конструктор по умолчанию в базовом классе, практически невозможно создать производный тип. Вы получали ошибку CS0122 или CS1729 о невозможности использования конструктора.

Использование closed явно семантически чище. Но кроме того, в подходе с приватным конструктором компилятор на самом деле «не знал», что нельзя создавать производные типы, потому что теоретически это не исключено, даже если невозможно на практике.

С ключевым словом closed, когда компилятор собирает сборку, он точно знает все производные типы есть у закрытого класса. Теперь компилятор может применять проверку исчерпываемости к выражениям switch.

Допустим, у нас есть «обычная» иерархия классов, похожая на показанную выше, но на этот раз с использованием abstract вместо closed. Мы можем определить следующий метод:
static string Speak(Animal animal) => animal switch
{
Dog => "Гав",
Cat => "Мяу",
Horse => "Игого",
};

Но мы получим предупреждение CS8509, что выражение switch не охватывает все возможные значения, и вынуждены будем добавить вариант «по умолчанию» (catch-all), типа _ => …. Даже если вы обрабатываете все типы, компилятор об этом не знает. С его точки зрения, вы можете создать производный от Animal тип в другой сборке и передать его методу.

Использование закрытого типа означает, что компилятор знает, что это все реализации, и больше их быть не может. Т.е. он может корректно применять проверку исчерпываемости к выражениям switch, использующим закрытые типы. Более того, при добавлении нового класса, мы получим предупреждение во всех местах, где выражения switch не охватывают все случаи.

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

Заметьте, что это более «функциональный» подход, в котором данные (классы Animal) отделены от методов (Speak()). В ООП метод Speak, скорее всего, был бы определен непосредственно в иерархии типов Animal.

Окончание следует…

Источник:
https://andrewlock.net/exploring-the-dotnet-11-preview-4-closed-class-hierarchies/
👍11
День 2716. #ЧтоНовенького #NET11
Закрытые Иерархии Классов. Окончание

Начало

Закрытые классы и объединения
Одним из преимуществ поддержки объединений в .NET 11 является то, что вы получаете аналогичную проверку исчерпываемости в выражениях switch:
// 3 разных типа
public record Windows(string Version);
public record Linux(string Distro, string Version);
public record MacOS(string Name, int Version);

// Объединение представляет один из них
public union SupportedOS(Windows, Linux, MacOS);

// Объединение в switch
string GetDescription(SupportedOS os)
=> os switch
{
Windows windows => $"Windows {windows.Version}",
Linux linux => $"{linux.Distro} {linux.Version}",
MacOS macOS => $"MacOS {macOS.Name} ({macOS.Version})",
};

Как и в случае с закрытой иерархией классов, объединение поддерживает проверку исчерпываемости, поэтому вам не нужно обрабатывать варианты «catch-all». Однако в объединении нет иерархии; оно определяет конкретные возможные значения, и компилятор гарантирует обработку всех этих случаев в выражении switch.

Ограничения и особенности
Когда вы применяете ключевое слово closed к классу или записи, это неявно делает класс абстрактным, поэтому вы не можете использовать модификаторы sealed или static для этого типа. Кроме того, вы также не можете явно использовать abstract для типа, даже если он является абстрактным по логике.

При наследовании от закрытого типа производные классы не являются автоматически закрытыми, но их можно объявить закрытыми:
public closed record Animal { }
public closed record Dog : Animal{ }
public record Labrador : Dog { }


Можно использовать обобщённые закрытые типы, но если производный класс также является обобщённым, то все его параметры типа должны использоваться в спецификации базового класса:
public closed class Animal<T> { }
class Dog<U> : Animal<U> { } // ОК
class Cat<W> : Animal<int> { } // ошибка CS9383
// Тип 'W' должен быть указан в базовом типе 'Animal<int>', т.к. базовый тип закрыт.


Ещё одно тонкое ограничение возникает, если вы применяете ключевое слово sealed к производным типам. Тогда говорят, что у типа запечатанная иерархия. В этих случаях компилятор может делать ещё более строгие предположения о вашем коде и, следовательно, может добавлять ошибки для сценариев, когда в вашем коде, вероятно, что-то не так. Например, объявим производные типы как sealed и добавим интерфейс IPet:
public closed class Animal { }

public sealed class Dog : Animal { }
public sealed class Cat : Animal { }

public interface IPet { }

Теперь, если у нас есть код, который пытается явно привести тип из экземпляра Animal к типу IPet, компилятор может «видеть» все производные типы и что ни один из них не реализует IPet, и, следовательно, может на этапе компиляции предположить, что это преобразование завершится неудачей:
Animal animal = GetSomeAnimal();
var pet = (IPet)animal; // CS0030: Cannot convert type 'Animal' to 'IPet' (Нельзя привести тип 'Animal' к 'IPet')

Это превращает гарантированную ошибку времени выполнения в ошибку компиляции. Отлично!

Источник: https://andrewlock.net/exploring-the-dotnet-11-preview-4-closed-class-hierarchies/
День 2717. #Оффтоп #AI
Прекратите Промптить. Спроектируйте Цикл. Начало

Примерно два года единицей работы с ИИ-агентом был промпт. Вы писали хороший промпт, давали ему достаточно контекста, читали ответ и писали следующий. Агент был инструментом, и вы направляли его всё время, шаг за шагом.

Это заканчивается. На смену приходит «проектирование циклов». Вы перестаёте быть тем, кто пишет промпты для агента. Вы проектируете цикл, который пишет ему промпты за вас. Люди, создающие эти инструменты, уже совершили этот скачок.

Борис Черни, возглавляющий Claude Code в Anthropic больше не пишет промпты для Клода. У него работают циклы, которые запрашивают Клод и решают, что делать дальше, сканируя трекер задач, командный чат и временную шкалу в поисках того, что нужно создать.

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

По сути, это цикл в цикле. Внутренний выполняет работу в соответствии со спецификацией. Внешний определяет, какой должна быть работа: он отслеживает багтрекер, поток ошибок, список изменений, затем пишет следующую спецификацию и передает её дальше. Большинство людей по-прежнему запускают этот внешний цикл вручную, в уме, и называют это бэклогом.

Удивительно, что это уже почти не проблема инструментов. Год назад цикл означал кучу кода на Bash, который вы писали и поддерживали бесконечно. Теперь же компоненты поставляются внутри продуктов, и аналогичные структуры встречаются в Claude Code и Codex. Цикл работает по таймеру, запускает вспомогательные средства и питает себя сам.

Пять составляющих и шестая, которая их объединяет
И Claude Code, и Codex теперь имеют все пять. Названия немного различаются, но возможности одинаковы.

1. Автоматизация. Превращает цикл в цикл, а не в одноразовый запуск. Промпт, команда, запланированная задача, хук, срабатывающий в определённый момент жизненного цикла агента, или задание в CI, которое продолжает работать после закрытия ноутбука. Обнаружение и анализ выполняются сами, а важные результаты поступают к вам.

2. Рабочие деревья (Worktrees). Как только вы запускаете более одного агента, они начинают конфликтовать за файлы. Рабочее дерево Git — это отдельный рабочий каталог в своей собственной ветке, поэтому изменения одного агента не могут коснуться изменений другого.

3. Навыки. Агент начинает каждую сессию с нуля и заполняет любые пробелы в вашем намерении уверенным предположением. Навык — это соглашения, этапы сборки, фраза «мы так не делаем из-за одного инцидента», записанные один раз, и агент читает их при каждом запуске. Без навыков цикл каждый раз заново анализирует весь ваш проект с нуля.

4. Коннекторы. Созданные на основе MCP, они позволяют агенту читать трекер задач, запрашивать БД, обращаться к API или отправлять сообщения в чат. В этом разница между агентом, который говорит «вот исправление», и циклом, который открывает пул-реквест, связывает тикет и отправляет уведомление в канал, как только CI завершится успешно.

5. Суб-агенты. Модель, написавшая код, слишком предвзята в оценке собственной работы. Второй агент с другими инструкциями, а иногда и с другой моделью, выявляет то, во что первый себя загнал.

И наконец, память. Файл Markdown, файл состояния, всё, что хранит информацию о том, что сделано и что будет дальше. Модель забывает всё между запусками, поэтому память должна храниться на диске, а не в окне контекста. Агент забывает. Репозиторий — нет.

Окончание следует…

Источник:
https://www.pulumi.com/blog/stop-prompting-design-the-loop/
👎11👍3
День 2718. #Оффтоп #AI
Прекратите Промптить. Спроектируйте Цикл. Окончание

Начало

Что обеспечивает целостность цикла?
Цикл, работающий без присмотра, также втихаря совершает ошибки. Единственное, что обеспечивает его правильность, — это верификация, а верификации нужен «оракул», нечто вне модели, что возвращает однозначное «да» или «нет». Пройденные тесты, чистая сборка, зелёный конвейер CI, и т.п. Без «оракула» цикл уверенно накапливает ошибки быстрее, чем вы успеваете их читать.

Самая чистая версия этого уже включена в инструменты. Функция /goal в Claude Code продолжает работать на протяжении нескольких итераций, пока не выполнится условие, которое вы фактически написали, например, «все тесты в auth/ проходят и сборка не имеет предупреждений», и после каждой итерации отдельная, более быстрая модель считывает протокол и решает, достигли ли вы цели. Агент, написавший код, не тот, кто его оценивает. Это разделение «создатель и проверяющий», применяется к самому условию остановки. Функция /goal в Claude Code достигает той же финишной линии другим способом: агент проверяет свою собственную работу на соответствие доказательствам, прежде чем объявить цель выполненной.

Чего цикл по-прежнему не сделает для вас
Цикл меняет форму работы, а не делает её за вас. Отдельный рецензент — это то, что делает фразу «готово» осмысленной, но «готово» — это утверждение, а не доказательство. Ваша задача по-прежнему — выпускать код, работоспособность которого вы подтвердили, в чём сложнее быть уверенным, если изменения сделаны, пока вы были на обеде.

Расчёты приходят в двух валютах: токенах и внимании. Один запуск без участия пользователя может израсходовать миллионы токенов, и это стоит того только тогда, когда токены «оплачивают» что-то, что стоит больше, чем стоят они. Вторая валюта: память — то, что позволяет циклу усложняться со временем, но и плохие навыки накапливаются вместе с этим. Цикл, направленный на неопределённую цель, не устанет и остановится, а будет становиться быстрее.

Ваше понимание растает, если вы это допустите. Чем быстрее цикл выдаёт код, который вы не писали, тем больше разрыв между тем, что есть в репозитории, и тем, что вы на самом деле понимаете. Этот разрыв растёт быстрее, если вы не читаете то, что создаётся. Удобная позиция, когда вы перестаёте иметь собственное мнение и принимаете того, что выдаёт цикл, — рискованная. Два инженера могут создать одинаковый цикл, но на выходе один будет быстрее продвигаться в работе, которую глубоко понимает, а другой полностью избегать этой работы. Цикл не знает, кто из них вы.

С чего начать
1. Начните с того места, где понятие «готово» однозначно. Отладка конвейера CI, проблемы с зависимостями, поиск нестабильных тестов, падающее задание, которое вы постоянно перезапускаете вручную. Циклам нужен «оракул», поэтому начните с того места, где «оракул» уже существует.

2. Напишите файл памяти перед циклом. Один файл Markdown. Что сделано, что дальше, что было опробовано и не удалось. Это основа, и всё остальное от неё зависит.

3. Отделите проверку от создания. Используйте /goal с проверяемым условием или второй агент с отдельными инструкциями. Никогда не позволяйте агенту, который выполнил работу, решать, что работа завершена.

4. Остановите агента, а затем прочтите всё. Максимальное количество итераций, бюджет токенов, шаг завершения. Запустите его один раз от начала до конца, а затем прочтите каждую строку кода. Первый запуск — это измерение, а не результат.

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

Источник:
https://www.pulumi.com/blog/stop-prompting-design-the-loop/
👎3👍2
День 2719. #TipsAndTricks
Паттерн «Строитель» с Неявным Оператором
Обычно паттерн «Строитель» позволяет вам выполнить сложную логику создания объекта, а потом вызвать метод Build(), который вернёт нужный вам объект. Например:
internal record PersonBuilder
{
internal PersonBuilder()
{
FirstName = "Jane";
LastName = "Doe";
Age = 42;
Height = 169;
}

internal string FirstName { get; init; }
internal string FamilyName { get; init; }
internal int Age { get; init; }
internal int Height { get; init; }

internal Person Build()
=> new Person(FirstName, LastName, Age, Height);
}

Мы можем создать объект Person так:
var person = new PersonBuilder
{
FirstName = "John"
}.Build();

Было бы неплохо, если бы можно было опустить вызов .Build() в конце. А это возможно. Можно создать неявный оператор для нашего строителя:
public static implicit operator 
Person(PersonBuilder b) => b.Build();


Теперь можно использовать строитель так:
Person person = new PersonBuilder
{
FirstName = "John"
};

Неявный оператор вызывается, т.к. мы изменили var на Person при объявлении переменной.

Источник:
https://josef.codes/bonus-builder-pattern-with-the-implicit-operator-using-c-sharp/
👎11👍1
День 2720. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

39. Преимущества микросервисов
«Расскажите о преимуществах использования микросервисной архитектуры в приложениях .NET?»

Хороший ответ
Микросервисная архитектура предлагает ряд преимуществ для разработки сложных приложений, особенно в средах, требующих гибкости, масштабируемости и высокой доступности.

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

- Гибкость: микросервисы позволяют небольшим автономным командам разрабатывать, развёртывать и масштабировать свои сервисы независимо друг от друга. Это приводит к более быстрым циклам разработки и упрощает экспериментирование и инновации.

- Устойчивость: независимость сервисов повышает общую устойчивость системы. При сбое в одном сервисе остальные могут продолжать работать.

- Гибкость развёртывания: микросервисы могут развёртываться независимо. Это особенно полезно в средах непрерывного развёртывания, где необходимо часто выпускать обновления.

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

Часто встречающийся неверный ответ
«Можно разбить приложение на более мелкие проекты для каждой сущности или группы сущностей. Это делает приложение микросервисной архитектурой.»

Почему это неверно
- Непонимание концепции микросервисов: ответ демонстрирует фундаментальное непонимание того, что такое микросервисы. Микросервисы — это не просто разбиение приложения на части, а определение сервисов на основе бизнес-возможностей и границ предметной области.

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

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

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

Источник:
https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👎5👍1