Здравствуйте, vfedosov, Вы писали:
V>Я уже в разработке 20 лет и мне не нужно "верить" — я знаю, что типизация нужна. Но для нетривиальных решений. Для описанного скрипта она нахрен не нужна. Она не везде нужна даже в больших проектах. Если у тебя 2 приватные функи объявлены рядом в коде и первая вызывает вторую и ты уверен, что вторая больше никогда не потребуется, то можно обойтись без типизации — ты ничего потеряешь. Проверено многократно. Типизация внутри функции мне также не дает ничего — в основном чисто мусор. Типы ясны из контекста — за редким исключением. Безусловно полезна типизация интерфейсов — особенно на уровне классов, которые доступны из других модулей. Тут вообще без нее никак — иначе проект скатывается в гавно влет. Короче, у любого инструмента есть границы полезности. Для одних проектов хороший инструмент — Шарп, для других Пайтон. Те кто утверждает, что Шарп везде лучше — фанатики. Так же как и те, кто утверждает подобное про Пайтон. Я бы сказал, что пока нет единого языка для всех целей — да он и не факт, что возможен.
Более интересен тот факт, что Питон является пожалуй единственным популярным языком, которому пока не нашли замену.
Если посмотреть на нашу индустрию лет 10 назад, то тогда было ровно 4 (ну я там два похожих за один считаю) важных языка, которые имело смысл изучать. Причём для профессиональной работы было достаточно знания одного из них:
1. C/C++ — для всего инфраструктурного или требовательного к ресурсам ПО.
2. Java/C# — для так называемого энтерпрайза (ключевое требование: чтобы и обезьяна могла написать безопасный код)
3. JavaScript — для веб-фронтенда (думаю не надо уточнять, по каким причинам это Г... заняло всю эту нишу)
4. Python — для автоматизации (всякие там админские и т.п. скрипты), веб-бэкенда, научных/инженерных/аналитических рассчётов.
Да, помимо этого настоящий профессионал должен знать ещё и всяческие не мейнстримовые языки (Лисп, Пролог, Хаскель и т.п.), для развития своего понимания разных подходов к проектированию. И так же в различных областях имеются десятки необходимых для изучения DSL'е (типа HTML, SQL и т.п.), которые тоже необходимо знать. Но это всё является лишь дополнением к хорошему знанию основного инструмента.
Всё выше сказанное — это естественно исключительно моё субъективное видение (хотя оно и хорошо коррелирует с такими источниками, как TIOBE и т.п.). Однако я думаю, что большинство даже на этом специфическом форуме согласится с этой картиной.
Так вот, если взглянуть на эту же область сейчас (в 2021-ом году), то мы увидим, что опять есть ровно 4 языка, занимающие точно такие же ниши (что впрочем не удивительно, т.к. ниши диктуются фундаментальными запросами человечества, а не модой в IT) как и 10 лет назад. Только вот сами языки эти стали совсем другими (речь естественно о выборе языка для нового проекта, т.к. гигабайты легаси написанного на языках из списка выше будут с нами ещё много десятилетий и количество вакансий по ним ещё долго будут опережать все современные языки). Трансформация языков в указанных нишах выглядит так:
Каждый из этих языков улучшает ключевую характеристику своего предшественника. Rust — может всё тоже, что и C++, но снижает завышенный уровень квалификации, требующийся для написания безопасного кода. Go — ещё намного проще чем Java и при это одновременно имеет меньше накладных расходов и проблем с развёртыванием. TypeScript пытается сделать из мира-помойки JS что-то похожее на нормальное программирование, хотя не уверен, что такое возможно в принципе.
Частично по этому (хотя были и другие причины) языки уже не столь разделены по нишам. Rust стал хорошо себя чувствовать в веб-фронтенде (когда-то эксклюзивной ниши JS), Go часто используют для написания системных утилит (т.к. в отличие от Java/C# для этого можно просто кинуть один бинарник, как когда-то делали с питон/перл скриптами), JS/TS стал хорошо себя чувствовать в бэкенде. Но это всё так сказать не основные направления использования этих базовых современных языков.
И только древнющему Питону так и не нашлась современная замена. Одно время были надежды на язык Julia. Но время показало, что это получился просто бесплатный и опенсорсный вариант Матлаба, который способен заменить Питон только в одной очень узкой области (и то этого скорее всего не произойдёт, т.к. будет сказываться "давление" остальной индустрии). Более того, за эти 10 лет, Питон только нарастил популярность (в основном за счёт бурного развития "его области" аналитических вычислений, которую сейчас принято называть Data Science). И на горизонте не видно вообще никаких потенциальных его заменителей.
Вот это действительно очень примечательный факт. Хотя и не скажу что позитивный — я бы предпочёл иметь на примете какого-то модного потенциального заменителя Питона, потому как улучшать в Питоне вообще то можно очень много чего.
Re[51]: MS забило на дотнет. Питону - да, сишарпу - нет?
Где только возможно стоит использовать readonly-модификатор метода, ReadOnlySpan.
Одно плохо, кривая сигнатура MemoryMarshal.CreateReadOnlySpan:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan<T> CreateReadOnlySpan<T>(ref T reference, int length) => new ReadOnlySpan<T>(ref reference, length);
Должно было быть так:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan<T> CreateReadOnlySpan<T>(in T reference, int length) => new ReadOnlySpan<T>(Unsafe.AsRef(reference), length);
Потому что в readonly-методах приходится делать так:
internal readonly ReadOnlySpan<Box<N, T>> Children => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(_children._data0), _childrenCount);
Что лечится своим каким-нить хелпером
public static SpanHelper {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan<T> CreateReadOnlySpan<T>(in T reference, int length) => MemoryMarshal.CreateReadOnlySpan(Unsafe.AsRef(reference), length);
}
readonly ref модификатор "in" можно опускать, что делает код чище:
int data = 42;
var span = SpanHelper.CreateReadOnlySpan(data, 1);
Здравствуйте, Ночной Смотрящий, Вы писали:
I>>Более того, с наследованием интерфейсов ровно те же проблемы. НС>Проблем с высокой зависимостью наследника от предка там нет.
Никто не запрещает использовать эту технику прямо сейчас.
Особенно когда в интерфейсы ввели дефолтную реализацию, а до этого снабдили язык методами-расширениями.
Re[52]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, vdimas, Вы писали:
V>Здравствуйте, Sinclair, Вы писали:
S>>Попробовал на шарплабе — неа, не хочет инлайнить:
V>У меня заинлайнил BranchNode.this[int index]:
Не вижу. По ссылке он выглядит так:
Здравствуйте, vdimas, Вы писали:
V>Это как примерно описывается в АПИ различных ОС всякие union.
Да, ясно. Хороший трюк.
И вообще, надо этот Pointer<T> взять на вооружение. В Linq2d мне приходится генерировать MSIL руками во многом потому, что не получается породить Expression<Func<int*>>.
При использовании Pointer<T> есть шанс вернуться к Expression, и использовать альтернативные компиляторы их в MSIL для ускорения.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[53]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, vdimas, Вы писали:
V>Здравствуйте, vdimas, Вы писали:
V>Интереса ради разобрал ассемблер по ссылке: V>
V>// выделили память на стеке (что-то дохрена)
V> L0000: sub rsp, 0x28
V>// в rcx пришло this
V>// rax=_indicies.Pointer (i.e. Bunch<int>._data0)
V> L0004: lea rax, [rcx+8]
V>// r8d = _indicies.Length
V> L0008: mov r8d, [rcx]
V>// for(i = 0;
V> L000b: xor r9d, r9d
V> L000e: jmp short L0013
V>// ===================== начало цикла ==============
V>// i++
V> L0010: inc r9d
V>// ? i < indices.Length
V> L0013: cmp r9d, r8d
V>// выход из цикла по i==indicies.Length
V> L0016: jge short L0021
V>// лишнее преобразование... всё-таки, в x64 Span.Length должно быть int64, т.е. nint.
V> L0018: movsxd r10, r9d
V>// ? indices[i] <= index
V>// здесь соптимизирована проверка выхода за границы при вызове indices[i]
V> L001b: cmp [rax+r10*4], edx
V>// зацикливание
V> L001f: jle short L0010
V>// =================== конец цикла ===================
V>// return (Children[i], i > 0 ? index - indices[i - 1] : index); L0021: lea r10, [rcx+0x48]
V> // r10=_children.Pointer
V> L0021: lea r10, [rcx+0x48]
V>// ecx = indices.Length (хотя, это значение сидит в r8d)
V> L0025: mov ecx, [rcx]
V>// проверили выход за массив Children[i]
V> L0027: cmp r9d, ecx
V> L002a: jae short L005f
V>// rcx=Childer[i]
V> L002c: movsxd rcx, r9d
V> L002f: mov rcx, [r10+rcx*8]
V>// ? i > 0
V> L0033: test r9d, r9d
V> L0036: jg short L003a
V> L0038: jmp short L004e
V>// r10d = i-1
V> L003a: lea r10d, [r9-1]
V>// проверка выхода за диапазон indices[i - 1] (как глупо)
V> L003e: cmp r10d, r8d
V> L0041: jae short L005f
V>// r8 = i-1 (опять?)
V> L0043: lea r8d, [r9-1]
V> L0047: movsxd r8, r8d
V>// index - indices[i - 1]
V> L004a: sub edx, [rax+r8*4]
V>// return t.Node[i]
V> L004e: cmp [rcx], ecx
V> L0050: add rcx, 8
V> L0054: call BranchNode`2[[LeafNode`1[[System.Int32, System.Private.CoreLib]], _],[System.Int32, System.Private.CoreLib]].get_Item(Int32)
V> L0059: nop
V> L005a: add rsp, 0x28
V> L005e: ret
V>// выброс IndexOutOfRangeException
V> L005f: call 0x00007ffb4bfba5e0
V> L0064: int3
V>
V>Целевой цикл вылизан по самое нимогу. V>Остальное с огрехами.
L0054 убивает всю производительность. В прошлой инкарнации этот метод был циклом c FindChild до тех пор, пока не дойдём до листа, и дети и индексы были массивами (+1 к косвенности), но при этом this[] работал быстрее, чем у штатного System.Collections.Immutable.ImmutableList.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[52]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, vdimas, Вы писали:
V>Где только возможно стоит использовать readonly-модификатор метода, ReadOnlySpan.
А зачем? Это внутренний код, я и так могу убедиться, что я при поиске индексы не меняю.
Кажется, я понял, в чём дело. JIT отказывается инлайнить рекурсию.
Если посмотреть на BranchNode<LeafNode<int>, int>.getItem(), то там всё в порядке — всё инлайнится, как и ожидалось.
А вот когда JIT видит внутри BranchNode<LeafNode<int>, int>.getItem()
IL_001b: constrained. !N
IL_0021: callvirt instance !0 class IListNodeBase`1<!T>::get_Item(int32)
и понимает, что N::get_Item — это тоже BranchNode<..., ...>::get_Item, то несмотря на то, что это совсем другой метод, джит решает "ой, не, это рекурсия — её инлайнить бесполезно".
Надо бы найти это место в исходниках JIT и посмотреть, можно ли доработать детектор рекурсии.
Варианты, как это обойти, не фикся джит:
— вернуться к варианту с циклом. Он, в целом, был хорош всем, кроме однотипности узлов. В итоге у меня внутри листьев тратилось место на индексы, увеличивая потребление памяти, и ухудшая характеристики всех модифицирующих операций.
— развернуть рекурсию, добавив 2-3-4 типа BranchNode1, BranchNode2, которые будут отличаться только методом SplitNGrow. Тогда медленный код будет вызываться только при очень больших глубинах дерева (например, миллион элементов требуют всего-то 4х уровней).
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[66]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, vdimas, Вы писали:
V>Никто не запрещает использовать эту технику прямо сейчас.
Да я и не спорю. Я наоборот, рядом говорю что, по факту, у нас ее особо никто и не использует. Ну, покуда используемые библиотеки это позволяют. А они, к счастью, в 2021 году почти всегда позволяют.
... << RSDN@Home 1.3.17 alpha 5 rev. 62>>
Re[63]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, alex_public, Вы писали:
_>Например в Rust'е нет наследования в классическом его понимание. И при этом полезный код отлично пишется.
_>Наследование применяют для двух целей: полиморфизма и переиспользования кода. Для обеих этих целей в Rust'е есть отдельные инструменты.
Это и есть причины проблем с наследованием Наследование это механизм моделирования отношения is, то есть, subtype. А вот если городить наследование ради переиспользования и полиморфизма, то начинается "адъ и израиль".
Скажем, та же композиция дает гораздо лучший результат при переиспользовании. Более того, переиспользовать можно вообще безо всякого наследования и даже композиции.
Re[65]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, Ночной Смотрящий, Вы писали:
I>>Интерфейсы это слабенько.
НС>Для полиморфизма — более чем достаточно.
Недостаточно, т.к. интерфейс не предоставляет никаких гарантий. Алгебраические типы данных поддерживаются языками которые в мейнстриме не летают. Абстрактные типы данных ровно так же в мейнстриме не летают. То есть, все гарантии обеспечиваешь руками.
И когда некто понаимплементит в одном классе кучку интерфейсов, уследить за свойствами этого решения становится, мягко говоря, проблематично.
I>>Обычно проблемы возникают не с самой связью is, а с реализацией композиции через наследование
НС>Именно.
Ты меняешь показания:
"использование is для логики — та еще кака".
I>>Более того, с наследованием интерфейсов ровно те же проблемы. НС>Проблем с высокой зависимостью наследника от предка там нет.
Непонятно. Приведи пример для отношения is, subtype то есть.
I>> или так — понареализовывает в классе целую кучу интерфейсов — мутебальных и иммутабельных. Вроде бы наследование нет, но проблемы ровно те же
НС>Какие?
Ломаются гарантии. Ты получил ссылку с типом иммутабельного интерфейса и твой код полагается на это свойство иммутабельности. Например, тебе удобно сравнивать объекты по ссылке ради экономии времени.
Но штука в том, что класс реализовывает дочерний интерфейс, который мутабельный, а значит тебе нельзя полагаться на свойство иммутабельности.
То есть, на самом деле нельзя порождать интерфейсы, которые нарушают свойства предков. А раз так, то у нас и для интерфейсов остаётся тот самый is или subtype.
I>>, как будто оно есть Отсюда ясно, что дело не в наследовании.
НС>Не знаю что тебе ясно, но логика "раз мы не можем отловить всех воров, то вообще их ловить не будем" так себе.
А у тебя выбора нет, т.к. в языках отсутствует проверка внятных гарантий. А это значит, что стоит учитывать такое положение дел при разработке иерархии классов.
То есть, не полагаться на мифические интерфейсы, а явно прописывать гарантии в базовом классе.
>Есть вполне кокретная проблема очень сильной связи между предком и потомком, и ее интерфейсы устраняют. А проблемы иерархического полиморфизма в ОО в целом, разумеется, можно устранить только отказом от такого полиморфизма.
Нужно заменить extends на subtype и все станет хорошо.
Re[67]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, Ночной Смотрящий, Вы писали:
НС>Здравствуйте, vdimas, Вы писали:
V>>Никто не запрещает использовать эту технику прямо сейчас.
НС>Да я и не спорю. Я наоборот, рядом говорю что, по факту, у нас ее особо никто и не использует. Ну, покуда используемые библиотеки это позволяют. А они, к счастью, в 2021 году почти всегда позволяют.
Я сам предпочитаю интерфейсы. Но есть куча функционала который есть в базовом классе, наследники наследуют полностью или частично корректируют поведения вызывая внутри base.
И очень редко полностью переопределяют.
Просто лучше делать самый базовый класс полностью абстрактным. А то лезешь в реализацию видишь код, но это не тот код.
И суть полностью абстракного класса это и есть интерфейс
и солнце б утром не вставало, когда бы не было меня
Re[68]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, Ikemefula, Вы писали:
НС>>Для полиморфизма — более чем достаточно. I>Недостаточно, т.к. интерфейс не предоставляет никаких гарантий.
Вообще никаких? К чему эта софистика? Очевидно что какие то гарантии он дает, но иммутабельность в этот список не входит.
I>Алгебраические типы данных поддерживаются языками которые в мейнстриме не летают.
Это пока. Хотя вон F# почти мейнстрим.
I>И когда некто понаимплементит в одном классе кучку интерфейсов, уследить за свойствами этого решения становится, мягко говоря, проблематично.
У любой технологии есть свои недостатки. Вывод то какой? Не использовать ООП?
I>>>Более того, с наследованием интерфейсов ровно те же проблемы. НС>>Проблем с высокой зависимостью наследника от предка там нет. I>Непонятно. Приведи пример для отношения is, subtype то есть.
Зачем мне приводить какой то пример? Тебюе без примера непонятно, что связь базовый класс — наследник намного сильнее, чем интерфейс — реализация?
I>>> или так — понареализовывает в классе целую кучу интерфейсов — мутебальных и иммутабельных. Вроде бы наследование нет, но проблемы ровно те же НС>>Какие? I>Ломаются гарантии. Ты получил ссылку с типом иммутабельного интерфейса и твой код полагается на это свойство иммутабельности.
С чего бы? Гарантию иммутабельности ООП в принципе никогда не давал и не дает, хоть с интерфейсами, хоть без. Можно, конечно, что нибудь придумать, но я тогда тебя спрошу — а что насчет какой нибудь другой гарантии? Например, гарантии времени выполнения или гарантии по объему потребляемой памяти, да даже проще — гарантий на диапазон возвращаемых значений или даже на отсутствие там null. Очевидно, что абсолютных гарантий тебе никто не даст, но отсутствие гарантии иммутабельности это далеко не единственный и даже не главный недостаток наследования классов.
НС>>Не знаю что тебе ясно, но логика "раз мы не можем отловить всех воров, то вообще их ловить не будем" так себе. I>А у тебя выбора нет,
У меня выбор есть
I> т.к. в языках отсутствует проверка внятных гарантий.
А внятными гарантии кто назначает?
I>Нужно заменить extends на subtype и все станет хорошо.
Нет, все не станет.
... << RSDN@Home 1.3.17 alpha 5 rev. 62>>
Re[69]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, Ночной Смотрящий, Вы писали:
НС>Здравствуйте, Serginio1, Вы писали:
S>>Но есть куча функционала который есть в базовом классе,
НС>Например?
Да примеров куча в прикладном коде.
Ну или хотя бы те же Control или wpf https://professorweb.ru/my/WPF/base_WPF/level1/1_7.php S>> Просто лучше делать самый базовый класс полностью абстрактным.
НС>Почему тогда не сделать его интерфейсом?
S>>И суть полностью абстрактного класса это и есть интерфейс
НС>Но мы сделаем его классом, чтобы никто не догадался?
Нет, что бы сделать наследника реализующий основные методы, а на базе его сделать иерархию, что бы не писать лишний код.
Конечно можно посмотреть на override или virtual но это не сразу в глаза бросается, а абстрактный без кода сразу виден.
Можно посмотреть реализацию и увидеть все возможные реализации.
Еще раз есть куча функционала который есть в базовом классе, наследники наследуют полностью или частично корректируют поведения вызывая внутри base.
И чем в данном случае лучше интерфейсы?
и солнце б утром не вставало, когда бы не было меня
Re[54]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, Sinclair, Вы писали:
S>L0054 убивает всю производительность.
А там разве не рекурсия?
(руки не дошли взять полный код, пока справшиваю словами)
Если рекурсия, то вполне нормально по-максимому заинлайнить тело рекурсивного метода.
Или тебе хотелось автоматического преобразования рекурсии в цикл?
В крайнем случае это можно сделать ручками, но при малой фактической вложенности (единицы десятков, те самые 20-30 максимум) смысла обычно нет.
И да, я когда-то более 15 лет назад сравнивал бинарные деревья с N-арными, ну вот 2-4 элемента в узле давали максимум производительности, т.е. увеличение кол-ва элементов в узле ничего не давало. Гораздо больше дал кастомный аллокатор узлов, но вряд ли это поможет в случае дотнета.
Еще на малой арности эффективно работает балансировка дерева.
(Кстате, не увидел у тебя балансировки, но может не весь код смотрел)
Еще, если заменить в твоей реализации _dataX на inplace-массив, то через одну константу арности можно легко менять кол-во элементов в узле, т.е. задешево сравнить сетку реализаций с отличающейся арностью.
Еще из этой области — когда-то сравнивал различные принципы построения хеш-таблиц:
— хеш-таблица в хеш-таблице (т.е. любой конфликт оборачивается в хеш-таблицу следующего подуровня и т.д. рекурсивно, т.е. тоже получается дерево, просто большой арности узлов, и размер подтаблицы не может быть равен размеру родительской таблицы, т.е. это должно быть другое простое число);
— дефолтные реализации (занятие соседних пустых ячеек);
— в хеш-таблице по данному хеш-коду содержится первый элемент и опционально указатель на сортированный список остальных конфликтных элементов.
Последний вариант показал себя самым эффективным для большинства случаев.
Второй вариант для сравнимой эффективности требует большого запаса по размеру ячеек хеш-таблицы относительно данных, т.е. с приличным превышением.
Первый вариант оказался самый тормозной, хотя всё тестирование ради него затевалось, т.к. оно хорошо подходит под иммутабельные сценарии.
Самое время проверить еще раз этот вариант на актуальной технике и актуальных компиляторах-джитах, сравнить с честными деревьями, типа как у тебя.
===================
Но в хеш-таблицах есть еще любопытный способ их построения — через подбор коэфициентов хеш-функции.
На пальцах — существует несколько классов хеш-функций, в которых через параметры можно управлять "законом" разброса.
Наполнение такой таблицы дорогое, т.к. в процессе наполнения происходит подбор коэфициентов хеш-функции для минимизации конфликтов.
Но зато при последующем чтении данных такие таблицы показывают хорошие результаты практически при любых сценариях, например даже в таких, где классические хеш-таблицы с фиксированными хеш-функциями из-за стечения обстоятельств (вернее, данных) вырождаются в своей эффективности до эффективности только механизма, разруливающего конфликты.
Т.е. когда классические (дефолтные) реализации хеш-таблиц де-факто профанируются.
Здравствуйте, Serginio1, Вы писали:
S>Ну или хотя бы те же Control или wpf
Т.е. нечто древнее как дерьмо мамонта. О чем и речь.
НС>>Но мы сделаем его классом, чтобы никто не догадался? S> Нет, что бы сделать наследника реализующий основные методы, а на базе его сделать иерархию, что бы не писать лишний код.
Наследование — далеко не единственный способ сделать это. Есть, к примеру, паттерн Стратегия.
S>Еще раз есть куча функционала который есть в базовом классе
Еще раз — это особенности конкретного дизайна, а не нечто фундаментальное
S>, наследники наследуют полностью или частично корректируют поведения вызывая внутри base. S>И чем в данном случае лучше интерфейсы?
Я пока не увидел никакого данного случая, одни общие слова.
... << RSDN@Home 1.3.17 alpha 5 rev. 62>>
Re[71]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, Ночной Смотрящий, Вы писали:
НС>Здравствуйте, Serginio1, Вы писали:
S>>Ну или хотя бы те же Control или wpf
НС>Т.е. нечто древнее как дерьмо мамонта. О чем и речь.
S>>, наследники наследуют полностью или частично корректируют поведения вызывая внутри base. S>>И чем в данном случае лучше интерфейсы?
НС>Я пока не увидел никакого данного случая, одни общие слова.
Угу дерьмо манмонта ты так и не увидел?
Тот же Xamarin.Forms тоже мамонты?https://docs.microsoft.com/ru-ru/xamarin/xamarin-forms/internals/class-hierarchy-images/class-diagram-large.png#lightbox
Ты давай отвечай чем в данном случае интерфейсы лучше и паттерн стратегия.
В Delphi есть возможность объявить за реализацию класса свойство класса реализующего этот интерфейс (Implements). https://www.delphiplus.org/programirovanie-v-srede-delphi-for-net/direktiva-implements.html
Но по сути это аналог множественного наследования в C++
Вот такая возможность с возможностью переопределения методов с возможностью вызова base была бы интересна/
Кстати с помощью Source Generator это легко сделать!
и солнце б утром не вставало, когда бы не было меня