Как стать автором
Обновить
11
0
Вадим Седов @SIDOVSKY

Android и Xamarin разработчик

Отправить сообщение

Для отправки единичных эвентов из viewModel лучше использовать
MutableSharedFlow<T>(replay = 0, extraBufferCapacity = 1, BufferOverflow.DROP_OLDEST)
В данном случае будет сохраняться последний отправленный эвент, до первого считывания его подписчиком.

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

In the absence of subscribers only the most recent [replay] values are stored and the buffer overflow behavior is never triggered and has no effect.

Интерактивный пример

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

Отмечу, что для множественного наследования, когда нужны только методы, вполне достаточно использовать C# 8 default interface methods.


Лично мне в C# пару раз очень пригодилась бы реализация traits как в rust, без необходимости модифицировать класс, чтобы их можно было применять к существующим типам, например из готовых библиотек.
Чтобы к классам прикреплялся интерфейс трейта, и под ним их можно было использовать как параметры в методах или обобщениях.

Или так же, как в KMP, можете сделать библиотеку, которую можно подключить к нативному проекту.

Вы имеете ввиду подключение .NET dll в Java/Kotlin и Obj-C проекты?
Я знаю только mono/Embeddinator-4000, но он кажется не особо актуальным т. к. последний осмысленный коммит был 1.5 года назад и даже доки деприкейтнули.


У вас часом не было опыта подобных чародейств? Было бы очень интересно узнать об этом больше.

Немного дополню, что Roslyn Source Generators никогда не зависили от .NET 5, только от С# 9, а точнее от версии компилятора, который его поддерживает.
В этом можно убедиться и по официальным примерам.


Чтобы быть уверенным, что генератор отработает на всех окружениях можно


  • либо добавить в транзитивную зависимость Microsoft.Net.Compilers.Toolset 3.8.0+, что точно подойдет не всем, особенно публичным библиотекам.
  • либо воспользоваться Uno.SourceGeneration, опыт использования которого я описывал тут.

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


Всё происходит внутри одного экземпляра студии.

JIT отладчик при запуске спрашивает вас, в каком экземпляре студии вы ходите ее производить, текущем или новом:


(изображение из статьи по ссылке выше)

image


Кому-то возможно проще и удобнее использовать отдельный экземпляр студии для отладки.


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

Важна не сборка, а исходный код, который получается и то как он работает.

Факт остается фактом, с вашим подходом отладить сгенерированный код нельзя.


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

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


Так в чем же простота отладки?

А можете раскрыть, чем именно проще отладка в вашем варианте? По моему опыту JIT отладчик спокойно подключается вызовом из кода генератора, никогда с этим проблем не испытывал.


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

Правильно ли я понимаю, что вам нужно тестировать не сам генератор (статус/ошибки/синтаксис), а именно сгенерированный код?
В таком случае, зачем делать первые три шага из вашего пути:


• Создадим компиляцию;
• Создадим и запустим генератор;
• Выполним сборку библиотеки и загрузим её в текущий процесс;
• Найдём там полученный код и выполним его.

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

А что вы понимаете под «интерпретацией» Expression-ов?

Преобразование узлов дерева выражений в набор вызовов операций. У библиотек выше это функции рефлексии, у Compile(true) под капотом то же. Например выражение получения значения свойства преобразовывают из MemberExpression в PropertyInfo.GetValue.


Компиляция Expression-ов занимает десятки минут? Или какая часть из десятков минут — на компиляцию?)

Десятки минут может занимать прогон unit-тестов, если их в проекте несколько тысяч. В целом, безотносительно к проектам с компиляцией выражений.


Я в таких проектах замеров не делал, но раз уж упомянул Moq, то по нему и прикинем:
Там на данный момент 1603 теста и в них компиляция используется 1007 раз.
Судя по результатам профилирования прогона тестов (Sampling, CPU-Time) на компиляцию уходит 1.1% от общего времени выполнения.
image


image
В моем случае компиляция выражений составила суммарно 725 мс, что соответствует порядку ее значений на моем ПК.
Обычно в CI тесты прогоняются на неслабом железе, так что, пожалуй, для прикинутого мною сценария с Moq проблема задержки из-за компиляции не особо актуальна.

Задумался о том, когда же такие сценарии бывают, что нужно компилировать множество Expression-ов при запуске приложения или окна (?).

Я по части фронтенда и мобильной разработки и среди библиотек, с которыми сталкивался, вижу следующие сценарии создания множества делегатов из Expression<>:


  • Дата-байндинг в MVVM
    Открывается экран, слой представления которого связывается с большим количеством свойств ViewModel. Например: экран с деталями товара или любой экран с большим списком.
    MvvmCrossинтерпретирует
    MvvmLightинтерпретирует
    ReactivePropertyкомпилирует
    Mugen MVVM Toolkitкомпилирует
    Praeclarum.Bindкомпилирует
    ReactiveUIинтерпретирует
    В последнем ExpressionTrees используются еще и для представления свойств классов в виде потока при помощи WhenAny, чтобы формировать правила поведения при изменении состояния. Одна из базовых вещей, часто задается.
  • Валидация данных
    Открывается экран с множеством полей, над которыми производится сложная валидация. Например: экран профиля пользователя, обратной связи или какой-нибудь заявки.
    FluentValidationкомпилирует.
    ReactiveUI.Validation — интерпретирует
  • DryIoс — создание деревьев из лямбд используется для спецификаций конструкторов и фабричных методов, интерпретируются
    Запускается приложение или экран, в дереве зависимостей которого большое количество компонентов разрешаются фабричными методами.
  • Moq — создание деревьев из лямбд используется для создания и настройки поведения моков. Деревья и компилируются, и интерпретируются.
    Unit-тесты и соответственно моки с деревьями в них запускаются пачками, одновременно. В больших проектах unit-тесты могут исчисляться тысячами и занимать десятки минут, а значит задерживать CI процессы.

В статье не указано никаких ограничений на Expression-ы, но тесты только с простыми вызовами свойств, методов и конструкторов.

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


Ограничения синтаксиса в лямбдах для создания деревьев описаны в отличной обзорной статье от Alexey Golub.
Ограничения библиотеки для создания делегатов указаны в её описании, раздел Possibilities and Limitations.
Не думаю, что они являются частью предмета исследования статьи.


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

У интересующих нас фрагментов кода: методов, конструкторов и свойств на стыке run-time и compile-time естественный идентификатор — это сигнатура.

Среди всех узлов синтаксических деревьев сборки нам нужно найти только интересующие нас лямбда-выражения типа System.Linq.Expressions.Expression и отобрать из их узлов-потомков выражения, описывающие доступ к членам классов, создание объектов и вызов методов

GitHub Gist для каждого такого фрагмента кода (несколько файлов, в пару кликов) создает самый обычный git репозиторий, который так же можно подключить как submodule, в отдельную папку.
К сожалению, в своей практике такого тоже не встречал. Возможно никто так код не распространяет из-за того, что умение обслуживать подмодули — это дополнительный порог для вхождения.

Заворачивать в обобщения пробовал, с ними действительно есть ускорение.
Но подстраивая геттеры/сеттеры без типобезопасности (как в статье) под generic-делегаты


в таком виде

наблюдалось ровно обратное замедление:


|             Вызов геттера: |     Mean |     Error |    StdDev |
|--------------------------- |---------:|----------:|----------:|
|         Accessor из статьи | 5.879 ns | 0.1525 ns | 0.3009 ns |
| Accessor<TTarget, TMember> | 3.245 ns | 0.0156 ns | 0.0138 ns |
|                  IAccessor | 8.521 ns | 0.1514 ns | 0.1416 ns |

Как совместить generic и non-generic варианты, да чтобы не пришлось дважды генерировать похожий код — пока думаю.
Для текущей реализации решил остановиться на non-generic варианте т. к. он все-таки универсальнее.
Например, при анализе дерева выражения, имея только объект Expression<TDelegate>, клиент, конечно, знает тип свойства, но знает он его в run-time, по объекту System.Type и подставить type-argument в дженерик у него нет возможности, кроме как перебирать типы в условных выражениях.


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

В качестве альтернативы: httpstat.us

Информация

В рейтинге
Не участвует
Откуда
Красноярск, Красноярский край, Россия
Дата рождения
Зарегистрирован
Активность