Здравствуйте, Слава, Вы писали:
_>>А, у тебя линух, понятно. Ну замени там первый second.join(); на какой-нибудь банальный this_thread::sleep_for(1s); и всё заработает. С>Или не заработает, если нагрузка в момент запуска будет высокой.
Заработает, т.к. данная строчка исключительно для демонстрации корректности работы теста. А так всё спокойно работает и вообще без неё.
_>> Это не имеет отношения к обсуждаемому нами вопросу, а связано с ожиданием завершения потока из двух других в pthread. В общем это тема отдельной дискуссии на тему синхронизации, можем тоже обсудить, если захочешь, но к вопросу передачи "локального" объекта между потоками это никакого отношения не имеет. ) С>Имеет-имеет. Охренительная просто кроссплатформенность, банальный join не работает. Причём, я уверен что если вызвать какой-нибудь аналог WaitForSingleObject(threadHandle) из libc, то всё работать будет.
Но я согласен, что это недоработка в libstdc++, потому что если глянуть на стандарт C++, то там нет никаких запретов на несколько вызовов join. По идее они должны были как-то обойти кривизну системных потоков, что бы полностью соответствовать стандарту, но... )
Кстати, самое забавное, при компиляции виндовым gcc (в котором сама библиотека по сути такая же, а вот pthread другой) никаких проблем естественно нет. )))
С>ЗЫ: Напиши лучше про язык D, что думаешь.
С ним всё неоднозначно. Ещё лет 5 назад было всё очевидно — идеальный язык для замены C++. В нём было всё имеющиеся в C++ (причём без врождённых уродств) и плюс ещё горы вкусностей. Правда языком было проблематично пользоваться из-за отсутствия нужных библиотек, инструментов и т.п. Но это очевидно должно было решиться со временем, так что я был в ожидание этого момента. ))) Но с тех пор ситуация несколько изменилась. Да, у D существенно добавилось библиотек и инструментов (хотя с необъятной инфраструктуры C++ естественно по прежнему не сравниться), но при этом старичок C++ неожиданно не только втянул в себя значительно количество возможностей из D, но и обзавёлся некоторыми очень интересными своими. Поэтому переход на D с C++ уже перестал выглядеть как вопрос времени и наоборот стал вызывать большие вопросы. Но язык в любом случае очень приятный и я бы перетащил в C++ ещё значительную часть его возможностей (в основном из области метапрограммирования).
Здравствуйте, alex_public, Вы писали:
_>Всё очень просто. Если указанная тобой "терминология" является общепринятой (ну мало ли, может это действительно так, а я просто не знал — всякое бывает), то это будет означать, что ты тогда сказал всё корректно, а я просто глупо придрался из-за своего незнания. Если же всё наоборот и данная "терминология" является твоим персональным изобретением...
Чтобы подчеркнуть все те (и только те) ошибки, которые будут выявлены позднее компилятором, IDE должна по сути выполнять его работу, семантически эквивалентно разбирать код. В C++ это трудно достижимо, потому что IDE работает на лету, то есть должна делать всё быстро, в процессе редактирования. Но всё равно IDE этот код подвергает компиляции (пусть подмножества языка), разве что без оптимизаций и генерации машинно-зависимого кода, оставаясь на высоком уровне.
_>Зачем? Для визуального эффекта достаточно и одной секунды. А больше ни для чего эта строчка не служит (если её убрать, то всё равно всё будет работать корректно).
Как это корректно, если в соседней ветке, говорят, падает на Линуксе? Скажи, это для тебя обычное дело — решать проблемы через `this_thread::sleep_for(1s);`?
Глаза у меня добрые, но рубашка — смирительная!
Re[23]: Visual C# vs C++. Надо сравнить перспективы.
Здравствуйте, alex_public, Вы писали:
_>Здравствуйте, itslave, Вы писали:
lpd>>>Скомпилировал clang++ в linux и запустил этот код и получил undefined behaviour: lpd>>>На мой взгляд — обычный C++ это баланс производительностью, удобством и простотой, с возможностью ручного ассемблера. I>>Ага, еще С++ кроссплатформенный!
_>Нуу не стоит путать базовые принципы и недоработку реализации библиотечной функции под одной из платформ. )
Ну знаешь ли, и .NET тоже тогда кроссплатформенный. В базовых принципах. Ога, с недоработками реализации библиотечных функций одной из платформ.
Здравствуйте, Serginio1, Вы писали:
V>>Угу, у которого создание экземпляров происходит через рефлексию. S> Угу один раз на создание компаратора, который может вызываться миллионы раз
Как и любой библиотечный объект может вызываться миллионы раз. Весело. ))
Мы обсуждали возможности "параметрического полиморфизма".
А так-то, если сравнивать различные способы выпрыгивания из штанов, но на плюсах их как бэ не меньше.
S>Другое дело, что там нет инлайнинга
Для value-типов это первое дело. Для простого сравнения двух int происходит динамический вызов метода интерфейса. Эпик фейл это, а не "параметрический полиморфизм". ))
Здравствуйте, Qbit86, Вы писали:
Q>Как это корректно, если в соседней ветке, говорят, падает на Линуксе? Скажи, это для тебя обычное дело — решать проблемы через `this_thread::sleep_for(1s);`?
Нет, судя по ссылке на stackoverflow это таки линукс — говно. Сколько лет они уже не могут сделать нормальные треды.
Здравствуйте, AlexRK, Вы писали:
ARK>Внутренний говнометод с рефлексией нужен только для создания дефолтного компарера.
Не важно. Важно, что этот "аргумент" попытались пропихнуть как образец "параметрического полиморфизма" и получили заслуженный отлуп. ))
ARK>Никто не мешает объявить своего наследника от этого абстрактного класса и создавать его обычным образом. Пардон, перепутал — речь же о внутреннем классе. Таки да, его создать можно только рефлексией.
Ну, можно пользоваться не дефолтным компарером, а написать свой. Не суть. В любом случае, в этом примере речь идёт о полиморфном IComparer<T>, у которого дергаются некие методы, а не о возможности дергать методы T, заюзанной через генерики дотнета. Это как раз был образец того, что породить экземпляры таких классов можно только через рефлексию. И тогда все рассуждения о "безопасности, обеспечиваемой констрейнами + компилятором" идут строем в лес, потому что никакой безопасности в случае рефлексии компилятор не обеспечивает. ))
Здравствуйте, vdimas, Вы писали:
V>Здравствуйте, Serginio1, Вы писали:
V>>>Угу, у которого создание экземпляров происходит через рефлексию. S>> Угу один раз на создание компаратора, который может вызываться миллионы раз
V>Как и любой библиотечный объект может вызываться миллионы раз. Весело. ))
Еще раз. Тебе нужно создать объект единожды. Который кстати можно и закэшировать. Он по сути синглетон.
V>Мы обсуждали возможности "параметрического полиморфизма". V>А так-то, если сравнивать различные способы выпрыгивания из штанов, но на плюсах их как бэ не меньше.
Это просто пример использования рефлексии для уменьшения кода. Никто не мешает сделать реальные конструкторы. Только это ни к чему.
S>>Другое дело, что там нет инлайнинга
V>Для value-типов это первое дело. Для простого сравнения двух int происходит динамический вызов метода интерфейса. Эпик фейл это, а не "параметрический полиморфизм". ))
Еще раз я тебе показал ссылки где компараторы инлайнятся.
roslyn-linq-rewrite
This tool compiles C# code by first rewriting the syntax trees of LINQ expressions using plain procedural code, minimizing allocations and dynamic dispatch.
Example input code
public int Method1()
{
var arr = new[] { 1, 2, 3, 4 };
var q = 2;
return arr.Where(x => x > q).Select(x => x + 3).Sum();
}
Allocations: input array, array enumerator, closure for q, Where delegate, Select delegate, Where enumerator, Select enumerator.
Decompiled output code
public int Method1()
{
int[] arr = new[] { 1, 2, 3, 4 };
int q = 2;
return this.Method1_ProceduralLinq1(arr, q);
}
private int Method1_ProceduralLinq1(int[] _linqitems, int q)
{
if (_linqitems == null) throw new ArgumentNullException();
int num = 0;
for (int i = 0; i < _linqitems.Length; i++)
{
int num2 = _linqitems[i];
if (num2 > q)
num += num2 + 3;
}
return num;
}
Как видишь, все течет все меняется. Так же это касается и .Net Native
и солнце б утром не вставало, когда бы не было меня
internal class GenericEqualityComparer<T>: EqualityComparer<T> where T: IEquatable<T>
V>Угу, у которого создание экземпляров происходит через рефлексию. V>Офигенный пример обсуждаемого "параметрического полиморфизма". :facepalm:
Как он там создаётся вообще пофигу. Пример был про то, как вызывается метод констрейнта у экземпляра типа, по которому параметризуется дженерик. Тебя же именно этот безобидный факт поставил в тупик: «А у тебя в примерах идёт вызов метода именно у типа Т. :facepalm:»
V>мне уже трудно сдерживать откровенное глумление... V>Как на злостный троллинг или как на рассуждения случайного в профессии человека?
Тебя почему-то :facepalm:'ят обычные вызовы методов у типов, по которым параметризуются обобщённый код, хотя это вполне заурядная практика как в C++, так и в C#. Да и в любых языках, в которых поддерживается полиморфизм — потому что он придумывался не только для обобщённых коллекций, с элементами которых ничего не делают и про них ничего не известно. Твои комментарии наглядно демонстрируют, кто в трэде «случайный в профессии человек».
V>вообще таких алгоритмов мало, которые можно выразить в дотнете для value и ref-типов в генериках и они будут корректно работать в обоих случаях. Я одно время на этом собаку съел
Здравствуйте, Qbit86, Вы писали:
Q>Чтобы подчеркнуть все те (и только те) ошибки, которые будут выявлены позднее компилятором, IDE должна по сути выполнять его работу, семантически эквивалентно разбирать код. В C++ это трудно достижимо, потому что IDE работает на лету, то есть должна делать всё быстро, в процессе редактирования.
Тем не менее, в некоторых средах успешно используется libclang, например в QT Creator есть такая опция, есть расширения для дополенения/подсветки ошибок/навигации на базе Clang для Emacs, Vim, и даже VS Code.
Q>Но всё равно IDE этот код подвергает компиляции (пусть подмножества языка), разве что без оптимизаций и генерации машинно-зависимого кода, оставаясь на высоком уровне.
Это какая-то странная терминология, с тем же успехом можно все интерпретаторы (и инструменты анализа, ага) динамических языков обозвать компиляторами
Здравствуйте, lpd, Вы писали:
_>>Что касается кэша, то конечно же любая косвенность снижает его эффективность. Но какие-то самые банальные её уровни предсказатель всё же способен побороть. Но при многоуровневой косвенности эффективная работа кэша умирает сразу. ))) lpd>В кэше не только данные, к которым обращение проходит последовательно, но и данные, к которым недавно было обращание. Причем второй вариант актуален гораздо чаще, чем первый. Кэшировать последовательное обращение нужно скорее для инструкций, чем для данных.
Вот если честно, мне уже надоело тут писать общеизвестные вещи по поводу оптимизации. Если не веришь мне на слово, то поищи информацию сам или же можем вместе поиграться в тесты на конкретных примерах. А писать что-то ещё на эту тему уже лень. )
lpd>Либо, в вычислительных алгоритмах. Но там, скорее всего, будут простые массивы.
А чем по твоему какой-нибудь vector<int> отличается от простого массива? )
lpd>Пример: я реализовывал алгоритм на простом бинарном графе, и boost::graph работал на 30% медленнее, чем моя структура. Это неплохо, но все равно неприемлемо.
Универсальные решения почти всегда менее эффективны, в сравнение со сделанными под конкретную задачу. Но зато на их написание не надо тратить своё время... )
Здравствуйте, AlexRK, Вы писали:
ARK>По сути он прав, все фичи IDE реализуются внутренним "компилятором", разве что без бекенда. Это буквоедство с вашей стороны, особенно после объяснения, что имелось в виду.
Не в этом дело. Если бы речь шла не о VS, а о чём-то другом (скажем каком-нибудь статическом анализаторе кода), то я бы возможно ещё и с большой натяжкой (всё же компилятор по своему определению должен преобразовывать исходники в какие-то другие коды, а не просто анализировать) мог бы временно поддержать подобный жаргон. Но в случае VS ситуация совсем другая, потому что в её поставку входит реальный компилятор C++, который так называется уже официально, а не в чьих-то фантазиях. И соответственно фраза вида "компилятор C++ в VS плохо реализует навигацию по коду" ассоциируется с вполне однозначной частью VS, а вся эта фраза в итоге выглядит как полный бред.
Но вообще ты прав, что весь этот вопрос ерундовый. Собственно он и в моём изначальном сообщение был мелким побочным пунктом, а не основной мыслью. Просто оппонент неожиданно упёрся и принялся доказывать что он всё корректно написал — пришлось отвечать. )
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Тем не менее, в некоторых средах успешно используется libclang, например в QT Creator есть такая опция, есть расширения для дополенения/подсветки ошибок/навигации на базе Clang для Emacs, Vim, и даже VS Code.
Как же так, ведь пользователь Qt Creator выше утверждал
: «Что-то ты тут бредишь... Компиляторы не имеет никакого отношения ни к каким "Go To Declaration" или "Go To Definition". Они занимаются исключительно преобразованием исходных кодов в исполняемые и всё. А то, что ты описываешь — это работа IDE, у которых для этих целей есть свои анализаторы кода.»
Q>>Но всё равно IDE этот код подвергает компиляции (пусть подмножества языка), разве что без оптимизаций и генерации машинно-зависимого кода, оставаясь на высоком уровне. EP>Это какая-то странная терминология, с тем же успехом можно все интерпретаторы (и инструменты анализа, ага) динамических языков обозвать компиляторами :no:
Может и странная, и точнее было бы говорить трансляторы. Но я не помню, чтоб такое встречал в обиходе, обычно компиляторы да компиляторы. И те, кто пишет интерпретаторы, тоже компиляторщики, а не интерпретаторщики или трансляторщики :)
Здравствуйте, AlexRK, Вы писали:
_>>Всё очень просто. Если указанная тобой "терминология" является общепринятой (ну мало ли, может это действительно так, а я просто не знал — всякое бывает), то это будет означать, что ты тогда сказал всё корректно, а я просто глупо придрался из-за своего незнания. Если же всё наоборот и данная "терминология" является твоим персональным изобретением (причём скорее всего вчерашней давности), то это будет означать, что ты изначально написал бред, а потом в ответ на моё справедливое замечание ещё и долго пытался оправдать свой бред нелепыми отмазками. ARK>По сути он прав, все фичи IDE реализуются внутренним "компилятором", разве что без бекенда.
А если например язык интерпретируемый, и вообще не имеет никакого компилятора?
ARK>Это буквоедство с вашей стороны, особенно после объяснения, что имелось в виду.
Это буквоедство прекратилось бы сразу как только оппонент перестал бы разрывать сову утверждениями о том что это нормальная терминология, или наоборот как-то подтвердил бы что действительно такая терминология общепринята.
Здравствуйте, alex_public, Вы писали:
_>Здравствуйте, lpd, Вы писали:
_>>>Что касается кэша, то конечно же любая косвенность снижает его эффективность. Но какие-то самые банальные её уровни предсказатель всё же способен побороть. Но при многоуровневой косвенности эффективная работа кэша умирает сразу. ))) lpd>>В кэше не только данные, к которым обращение проходит последовательно, но и данные, к которым недавно было обращание. Причем второй вариант актуален гораздо чаще, чем первый. Кэшировать последовательное обращение нужно скорее для инструкций, чем для данных.
_>Вот если честно, мне уже надоело тут писать общеизвестные вещи по поводу оптимизации. Если не веришь мне на слово, то поищи информацию сам или же можем вместе поиграться в тесты на конкретных примерах. А писать что-то ещё на эту тему уже лень. )
Мне стало интересно, написал небольшой пример:
Идет обращение к гигабайту интов. В одном случае последовательно, в другом случае в случайном порядке.
#include <iostream>
#include <chrono>
#include <stdlib.h>
using namespace std;
#define SIZE 1000*1000*1000
int *map1, *map2;
void test(int *map)
{
int *array = new int[SIZE];
int sum = 0;
for (int i=0; i<SIZE; i++)
sum += array[map[i]];
}
int main()
{
map1 = new int[SIZE];
for (int i=0;i<SIZE;i++)
map1[i] = i;
srand(time(0));
map2 = new int[SIZE];
for (int i=0;i<SIZE;i++)
map2[i] = random()%SIZE;
auto start1 = std::chrono::high_resolution_clock::now();
test(map1);
auto finish1 = std::chrono::high_resolution_clock::now();
cout << std::chrono::duration_cast<std::chrono::nanoseconds>(finish1-start1).count() << "ns\n";
auto start2 = std::chrono::high_resolution_clock::now();
test(map2);
auto finish2 = std::chrono::high_resolution_clock::now();
cout << std::chrono::duration_cast<std::chrono::nanoseconds>(finish2-start2).count() << "ns\n";
}
Вывод:
2203765521ns
2621904089ns
Да, разница на 20%. Однако здесь только одна операция суммирования чисел, так что это можно считать верхней границей выигрыша, которую дает кэш. В реальных программах обработки больше, да и обращение к данным далеко не всегда последовательное, поэтому кэш не играет такой роли. Тем более, ошибочно говорить, что кэш миссы — решающий фактор, определящий отставание Java/C# в скорости от C++.
У сложных вещей обычно есть и хорошие, и плохие аспекты.
Берегите Родину, мать вашу. (ДДТ)
Здравствуйте, Qbit86, Вы писали:
_>>Всё очень просто. Если указанная тобой "терминология" является общепринятой (ну мало ли, может это действительно так, а я просто не знал — всякое бывает), то это будет означать, что ты тогда сказал всё корректно, а я просто глупо придрался из-за своего незнания. Если же всё наоборот и данная "терминология" является твоим персональным изобретением... Q>https://en.wikipedia.org/wiki/Compiler#Front_end
Что такое компилятор я в курсе. Ты покажи какие-нибудь ссылки, где анализатор C++ кода в VS называют компилятором. )))
Q>Чтобы подчеркнуть все те (и только те) ошибки, которые будут выявлены позднее компилятором, IDE должна по сути выполнять его работу, семантически эквивалентно разбирать код. В C++ это трудно достижимо, потому что IDE работает на лету, то есть должна делать всё быстро, в процессе редактирования. Но всё равно IDE этот код подвергает компиляции (пусть подмножества языка), разве что без оптимизаций и генерации машинно-зависимого кода, оставаясь на высоком уровне.
Это называется анализ, а не компиляция. Прочитай внимательно определение компилятора по своей же ссылке выше. ))) А то по твоему получается, что и какой-нибудь bash или cmd — это компиляторы. )))
_>>Зачем? Для визуального эффекта достаточно и одной секунды. А больше ни для чего эта строчка не служит (если её убрать, то всё равно всё будет работать корректно). Q>Как это корректно, если в соседней ветке, говорят, падает на Линуксе? Скажи, это для тебя обычное дело — решать проблемы через `this_thread::sleep_for(1s);`?
Ну если ты не сумел разобраться с такими простыми исходниками, то так и быть объясню для тебя по пунктам:
1. Данный пример демонстрирует передачу локального объекта из одного потока в другой. И для его нормальной работоспособности ему не нужны вообще никакие задержки — всё будет работать без всяких join или sleep_for (более того, только так в реальном коде и будет).
2. Для того, чтобы мой собеседник был на 100% уверен в корректности этой демонстрации, я добавил ожидание завершения одного потока в другом.
3. У моего собеседника неожиданно (с учётом изначальной темы беседы) оказался линух, а в нём несколько одновременных join к одному системному потоку — это проблема (второй join (и это как раз то самое ожидание из пункта2) кидает исключение).
4. Я предложил заменить ожидание завершение потока на временной таймаут, чтобы мой собеседник смог увидеть и на своём линухе наглядную демонстрацию корректной передачи локального объекта. Что я надеюсь он и сделал.
Здравствуйте, lpd, Вы писали:
lpd>Да, разница на 20%. Однако здесь только одна операция суммирования чисел, так что это можно считать верхней границей выигрыша, которую дает кэш.
Вот нормальный тест:
— обход vector<int*> более чем в десять раз медленнее чем vector<int>
— обход list<int> более чем в сто раз медленнее чем vector<int>
lpd>В реальных программах обработки больше, да и обращение к данным далеко не всегда последовательное, поэтому кэш не играет такой роли.
В реальных управляемых программах индерекций намного больше чем в vector<int*>
lpd>Тем более, ошибочно говорить, что кэш миссы — решающий фактор, определящий отставание Java/C# в скорости от C++.
Здравствуйте, Serginio1, Вы писали:
S>Как видишь, все течет все меняется. Так же это касается и .Net Native
Я-то вижу и даже рядом писал об этом:
Например, где в дотнете принято при трансформации данных описывать акцессоры "по-месту" (навроде x => x.Y) для каждого уникального x, там в С++ ленивый программист может сделать некий шаблонный getY(x)
Т.е., в С++ у нас параметрический полиморфизм, а в C# — в каждом новом месте вручную пишем код для ad-hoc полиморфизма.
EP>Вот нормальный тест: EP>- обход vector<int*> более чем на порядк медленнее чем vector<int> EP>- обход list<int> более чем на два порядка медленнее чем vector<int>
Я, кстати, запускал свой тест последовательного обхода интов с индирекцией и без. Приведу тебе полный пример(test0 — без индирекций, test(map1) — последовательный с индирекцией, test(map2) — случайный с индирекцией.
#include <iostream>
#include <chrono>
#include <stdlib.h>
using namespace std;
#define SIZE 1000*1000*1000
int *array;
int *map1, *map2;
void test(int *map)
{
int sum = 0;
for (int i=0; i<SIZE; i++)
sum += array[map[i]];
}
void test0()
{
int sum = 0;
for (int i=0; i<SIZE; i++)
sum += array[i];
}
int main()
{
srand(time(0));
array = new int[SIZE];
map1 = new int[SIZE];
for (int i=0;i<SIZE;i++) {
map1[i] = i;
array[i] = random();
}
map2 = new int[SIZE];
for (int i=0;i<SIZE;i++)
map2[i] = random()%SIZE;
auto start0 = std::chrono::high_resolution_clock::now();
test0();
auto finish0 = std::chrono::high_resolution_clock::now();
cout << std::chrono::duration_cast<std::chrono::nanoseconds>(finish0-start0).count() << "ns\n";
auto start1 = std::chrono::high_resolution_clock::now();
test(map1);
auto finish1 = std::chrono::high_resolution_clock::now();
cout << std::chrono::duration_cast<std::chrono::nanoseconds>(finish1-start1).count() << "ns\n";
auto start2 = std::chrono::high_resolution_clock::now();
test(map2);
auto finish2 = std::chrono::high_resolution_clock::now();
cout << std::chrono::duration_cast<std::chrono::nanoseconds>(finish2-start2).count() << "ns\n";
}
Вывод:
1952730354ns
2168493713ns
2623154650ns
Видим, что индирекция при суммировании дает всего 10% разницы.
Почему у тебя другие результаты, я не знаю. Я еще пока не успел разобраться в твоем тесте, но думаю, что дело в вызовах boost.
У сложных вещей обычно есть и хорошие, и плохие аспекты.
Берегите Родину, мать вашу. (ДДТ)
Здравствуйте, Qbit86, Вы писали:
V>>Угу, у которого создание экземпляров происходит через рефлексию. V>>Офигенный пример обсуждаемого "параметрического полиморфизма". Q>Как он там создаётся вообще пофигу. Пример был про то, как вызывается метод констрейнта у экземпляра типа
Никак он не вызывается.
x.Equals(y) — это метод базового Object
Кароч, пример был неудачный, хотя удачные примеры, таки, есть.
Но их малочисленность лишь подтверждает моё исходное утверждение:
Собсно, вообще таких алгоритмов мало, которые можно выразить в дотнете для value и ref-типов в генериках и они будут корректно работать в обоих случаях.
Q>Тебя почему-то 'ят обычные вызовы методов у типов, по которым параметризуются обобщённый код
Ну так я уже показал "цену" этому параметрическому полиморфизму и твоим "констрейнам" здесь:
Здравствуйте, vdimas, Вы писали:
V>Здравствуйте, Serginio1, Вы писали:
S>>Как видишь, все течет все меняется. Так же это касается и .Net Native
V>Я-то вижу и даже рядом писал об этом: V>
V>Например, где в дотнете принято при трансформации данных описывать акцессоры "по-месту" (навроде x => x.Y) для каждого уникального x, там в С++ ленивый программист может сделать некий шаблонный getY(x)
V>Т.е., в С++ у нас параметрический полиморфизм, а в C# — в каждом новом месте вручную пишем код для ad-hoc полиморфизма.
V>А ты видишь, о чем речь была?
Это же все можно делать и в C# это же Func<X,Y>
и солнце б утром не вставало, когда бы не было меня