Как стать автором
Обновить

Комментарии 87

static abstract int StaticValue { get; }

На этом мой парсер сломаслся. Мне совсем не понятна идея называть подобные методы abstract.Ну вот прям всё против этого. Во первых, это интерфейс, а не абстрактный класс. Во вторых, он СТАТИЧЕСКИЙ! Есть же много других способов, но был выбран именно этот, который вызывает дисонанс у всех подряд.

Есть же много других способов

Например?

Как по мне, хоть и немного криво, но право на жизнь имеет. Ведь что нам говорит abstract? Что это только сигнатура, без реализации. И что если мы наследуемся, то должны реализацию предоставить.

Не читал оригинальный issue, но если это предполагается только для интерфейсов, то там (если не брать default implementation) только сигнатуры и есть. То есть нужно было разрешить в интерфейсах писать static методы и, вроде бы, этого хватило ​

Да, этого бы хватило, если бы парой лет лет раньше не выкатили default interface implementation и разрешили использовать static-методы внутри интерфейсов как вспомогательные.


А так логика действительно хромает: инстанс-методы объявляются без всяких модификаторов, а для статических, будь добр, добавь слово abstract.

Через какое-то время сделают abstract опциональным для инстанс-членов. А потом и обязательным. И мы окончательно провалим вопрос на собеседовании: "чем абстрактный класс отличается от интерфейса"? ;)

Через какое-то время сделают abstract опциональным для инстанс-членов.
Я тут открою секрет — так уже и есть. Можете проверить.
А потом и обязательным.
В этом нет необходимости.
И мы окончательно провалим вопрос на собеседовании: «чем абстрактный класс отличается от интерфейса»? ;)
А использование ключевых слов никак не влияет на ответ на этот вопрос

А использование ключевых слов никак не влияет на ответ на этот вопрос

Понятно, что использование ключевых слов тут ни при чём. Просто получится, что отличий всё меньше и меньше. Что там остаётся? Невозможность множественного наследования классов, в отличие от интерфейсов? Вроде всё. Или я что-то пропускаю?

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

Это всё в итоге даёт ответ, где то или иное изменение должно происходить.

И какое это нам даёт практическое следствие? Потому что "контракт", "что может делать", "чем является" - это слова. Хотелось бы понимать, что за ними скрывается.

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

Может на примере? Чем руководствоваться, чтобы правильно выбрать интерфейс или абстрактный класс? Каких данных не хватает, чтобы принять решение?

interface IUser {
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string FullName { get; } => 
    $"{this.FirstName} {this.LastName}";
}

abstract class UserBase {
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string FullName { get; } =>
    $"{this.FirstName} {this.LastName}"; 
} 

class AzureUser : UserBase // or IUser {
}
Не хватает понимания абстракции всей системы. Что есть «пользователь», в частности. Но, за редким исключением, здесь следует выбрать абстрактный класс.

Например. У меня может быть класс Contact — скажем, некий человек-контактное лицо компании. У него тоже есть имя и фамилия. По всем признакам, он удовлетворяет IUser, и я могу без труда использовать его. На следующем шаге я понимаю, что у пользователя (IUser) вообще-то должен быть логин и пароль, секретное слово, признак заблокированности, всё в таком духе. Я добавляю их в IUser, и теперь мне придётся как-то думать о том, как мне жить с тем, что у меня контактное лицо, которое не является пользователем системы, теперь должно иметь пароль.

Вообще же, тут не нужен ни абстрактный класс, ни интерфейс. Это просто тип с данными. А то, что пользователь — Azure можно выразить чезер свойство UserType.
Да, этого бы хватило, если бы парой лет лет раньше не выкатили default interface implementation и разрешили использовать static-методы внутри интерфейсов как вспомогательные.
Только default interface implementation тут не причём, а static методы можно использовать и не внутри интерфейсов.
А так логика действительно хромает: инстанс-методы объявляются без всяких модификаторов, а для статических, будь добр, добавь слово abstract.
Это же разные вещи. Инстанс методы все по умолчанию переопредеяемы. Static методы всегда не-переопределяемы (на данный момент).

Данное предложение — о том, чтобы ввести специальный отдельный тип static методов. Если не вводить отдельное ключевое слово, то как наследуемые типы поймут, что это нужно реализовать? Сделать всё обязательным для переопределения? Это будет идти в разрез и с тем, как статические члены работают в языке вообще, и как они работают сейчас в интерфейсах в частности.
Это же разные вещи. Инстанс методы все по умолчанию переопредеяемы. Static методы всегда не-переопределяемы (на данный момент).

Собственно, об этой непоследовательности я и писал: инстанс методы по умолчанию переопределяемы, статические — нет. Непорядок.

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

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

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

В том-то и прикол, что мы не наследуемся. Как можно унаследовать статические методы?
Интерфейс в данном случае — это контракт.

А в чём проблема реализовать контракт, который диктует обязательность ещё и статических членов класса? Хочешь поддержать контракт? Будь добр, реализуй такой-то статический метод в классе.

Например, не добавлять туда abstract вовсе. Поскольку, интерфейс сам по себе требует чтобы класс все реализовал. А то получается смесь конракта и реализации.
Ещё есть вариант с Shapes. В этом случае, вводится новый способ расширать поведение классов и интерфейсов. Да, это более сложно реализовать, но они уже показывали демо с ним и расказывали что очень хотят его ввести. Наcколько я понял, Shap-ы покрывают эту фичу полностью.

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

Если меня кто то слышит - добавьте автоматическую дедукцию типов как в с++ :)

Нет ничего проще: тыц.

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

НЛО прилетело и опубликовало эту надпись здесь

Это называется получить раннюю обратную связь от настоящих пользователей, а не абстрактных фокус-групп

НЛО прилетело и опубликовало эту надпись здесь

Ну вообще-то .NET 6 ещё не зарелизили, так что они вполне имеют право так делать.

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

Мой любимый строгий C# превращается в хулиганский PHP. И в тоже время PHP обрастает строгостью.

Фича полезная, но, как уже написали, какой-то перегруз с синтаксисом.

Если добавят возможность для всех операторов, в т.ч. implicit, explicit приведения, тогда получится:

static abstract implicit operator Int32(T t);
static abstract explicit operator Int32(T t);

Хотя, в текущем виде "букв" не меньше
public static implicit operator Int32(T t){...}


А, вместо:
static abstract T Zero { get; }

тогда дали бы возможность определять default значение для типа:
static abstract T default();

Но и не так часто implicit нужно перегружать, так что много букаф не так страшно. Я до сих пор этот синтаксис гуглю, потому что нет надобности запоминать.

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

Просто по ,течерию обстоятельств это тоже ноль

static Int32 I.operator +(Int32 x, Int32 y) => x + y;

А как это оператор + реализуется через оператор + ? Не будет ли рекурсии?

Нет, не будет. Это же explicit interface implementation. Обратите внимание на I..

Фича хорошая и нужная, но синтаксис как всегда :boah: Ну и то, что чужие типы как всегда не расширить — а это 90% кейсов.


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

Так в чём пробема-то? Расширять чужие типы можно при помощи наследования или методов раширения. Подскажите какой-нибудь кейс, где шейпы прям необходимы?

Ну вот я хочу написать какой-нибудь MySum, аналогичный линковскому, но не хочу делать те 16 перегрузок которые сейчас есть. Что мне делать? "Но вот майкрософт щас добавит INumber.." — а что делать с типами, к которым майкрософт забыл приделать все возможные интерфейсы? Что делать если мне нужен мой ICustomNumber и чтобы System.Int32 его реализовывал? Вопросы, вопросы...


В качестве примера пусть будет хотя бы:


interface MyDeserializable<T> where T : MyDeserializable<T>
{
    static abstract T FromString(string s);
}

Как мне теперь добиться чтобы инт его реализовывал?

Я, конечно, всё-таки о реальном кейсе спрашивал, а не о том, что делать, если вы решите изобрести своё сложение) Я про те самые 90% кейсов.
а что делать с типами, к которым майкрософт забыл приделать все возможные интерфейсы?
Если прям забыл — заводить issue, очевидно.
Что делать если мне нужен мой ICustomNumber и чтобы System.Int32 его реализовывал?
Так зачем это вообще может понадобится? Я всё ещё не понимаю. Создайте свой тип просто.
Как мне теперь добиться чтобы инт его реализовывал?
Никак. Потому что это не задача инта — знать как самого себя десериализовывать. Его задача — ну, быть числом. Для десериализации инта из строки нужен отдельный тип, который будет знать как это делать, и это будет его задачей. Там же будут решаться вопросы о том, что делать, например, если string == null, либо совсем не число и т.п.
Я, конечно, всё-таки о реальном кейсе спрашивал, а не о том, что делать, если вы решите изобрести своё сложение) Я про те самые 90% кейсов.

Я один привел. А вообще их больше, конечно.


Так зачем это вообще может понадобится? Я всё ещё не понимаю. Создайте свой тип просто.

Затем, чтобы не писать врапперы на каждый чих. Библиотеке А(ну например, монгодрайверу) понадобилось расширить инт и они сделали свой враппер. Другой библиотеке (скажем, серилогу) понадобилось расширить инт и они сделали свой инт. Теперь когда мне нужно получить оба расширения, чтобы обе библиотеки работали — что мне делать? Писать враппер на враппер?


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


Если прям забыл — заводить issue, очевидно.

Ну да, майкрософт будет рассматривать каждую ишшую по добавлению какого-нибудь интерфейса. Особенно интересно будет, если интерфейс из популярной библиотеки типа Json.Net — добавят? Сомневаюсь. Зато все миллионы пользователей будут копипастить себе функционал.


Никак. Потому что это не задача инта — знать как самого себя десериализовывать.

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


Так что сидим и ждем, да. А джедайские штучки "тебе это не нужно" на меня не работают — ещё как нужно. Регулярно. Буквально на прошлой неделе в статье где я писал мне пригодилось бы расширить дейттайм методами GextNextYear/GetNextMonth, но мне пришлось писать портянку из имплиситов.

Я один привел.
Т.е. вы писали свой сериализатор, работающий только с модифицированной версией int? И в каждом проекте, который этот сериализатор использует, нужно снова и снова его реализовывать?
Библиотеке А(ну например, монгодрайверу) понадобилось расширить инт и они сделали свой враппер. Другой библиотеке (скажем, серилогу) понадобилось расширить инт и они сделали свой инт. Теперь когда мне нужно получить оба расширения, чтобы обе библиотеки работали — что мне делать?
Так зачем вам эти оба расширения? Каждый раз я не могу понять, какой смысл во всём этом «мне нужно». Библиотеки работают и так, они же реализовали всё уже.
То что тип должен перечислять все вещи которые «реализует» — это порочная практика старых языков.
Читающий, догадайся сам, зачем тут это? Или как вообще должна работать схема «контракт-реализация»?
Сценариев использования всегда больше, чем можно придумать, проектируя тип.
Поэтому есть расширения.
Особенно интересно будет, если интерфейс из популярной библиотеки типа Json.Net — добавят?
Нет, конечно. Потому что это не нужно. Нет необходимости пихать всё в базовые типы. Я думал, имеются в виду адекватные интерфейсы, а не всё подряд.
а про практическую сторону «как мне писать код без копипасты». Получается, что без написания врапперов на каждый чих — никак.
Так чем «врапперы» не угодили-то? И тот же самый «чих» будет с расширениями типов.
Буквально на прошлой неделе в статье где я писал мне пригодилось бы расширить дейттайм методами GextNextYear/GetNextMonth, но мне пришлось писать портянку из имплиситов.
Речь, я так понимаю, про это? Как бы вы написали это с шейпами и без портянки?
Читающий, догадайся сам, зачем тут это? Или как вообще должна работать схема «контракт-реализация»?

А, собственно, почему факт реализации контракта нужно прописывать в типе явно?

Чтобы на этапе компиляции понимать, подходит ли данный объект под требования контракта.

Так это можно проверить, просто перебрав реализуемые объектом методы. При этом методы можно искать не только среди членов класса, но и среди расширений.

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

И всё это — ради чего? Ради того, чтобы гордо не написать, что я реализую контракт в то время, когда это буквально то, что я делаю?

Факт реализации контракта нужно прописывать явно. Но не в самом типе. В этом плане я согласен, что имплисит-интерфейсы а-ля го это не очень.


Но и фиксировать что у нас богом данный тип и ничего больше он не умеет — ерундистика.

Т.е. вы писали свой сериализатор, работающий только с модифицированной версией int? И в каждом проекте, который этот сериализатор использует, нужно снова и снова его реализовывать?

Я не понял, при чем тут инт. Сериализатор должен работать со всеми типами. А смысл интерфейса в том, чтобы можно было писать абстрагированный от конкретного типа код:


public static T DeserializeFromConsole<T>() where T : MyDeserializable<T> =>
  T.FromString(Console.ReadLine();

И чтобы мне не надо было писать DeserializeIntFromConsole/DeserializeDoubleFromConsole… Вам не приходило в голову, что ошибки Json.Net по тому что "Не могу десериализовать" могли бы быть ошибками компиляции? В языках, где интерфейсы не прибиты гвоздями к типам, их реализующих, так и есть. Например, в Rust.


Так зачем вам эти оба расширения? Каждый раз я не могу понять, какой смысл во всём этом «мне нужно». Библиотеки работают и так, они же реализовали всё уже.

Зачем мне нужно, чтобы я мог соединить 2 библиотеки вместе и не писать клей в своем приложении, а чтобы оно просто заработало? Действительно, зачем.


Читающий, догадайся сам, зачем тут это? Или как вообще должна работать схема «контракт-реализация»?

Документация, IDE. Нам обычно не нужно знать все контракты, которые реализует тип (впрочем, это обычно написано в модуле, где он объявлен). Нам важнее, реализует ли он контракт Х или нет.


Поэтому есть расширения.

Которые нельзя использовать полиморфно, ага. Можно сделать только "полиморфизм" уровня LINQ "зафигач 20 перегрузок". А я вот не хочу 20 перегрузок, я хочу чтобы из коробки работало со всеми типами.


Нет, конечно. Потому что это не нужно.

Классика


Так чем «врапперы» не угодили-то? И тот же самый «чих» будет с расширениями типов.

Не будет


Речь, я так понимаю, про это? Как бы вы написали это с шейпами и без портянки?

Вышло бы что-то вроде:


struct True;
struct False;

trait MyScheduleExt<T> {
  fn nextYear();
  fn nextMonth();
  ...
}

impl MyScheduleExt<True> for DateTime {
  ..
}

impl MyScheduleExt<False> for DateTime {
  ..
}

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




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

И чтобы мне не надо было писать DeserializeIntFromConsole/DeserializeDoubleFromConsole…
По сути, вам это будет всё равно нужно писать, просто это будут не «врапперы», а «расширения». Но окей, тут я понял что вы имеете в виду.
Вам не приходило в голову, что ошибки Json.Net по тому что «Не могу десериализовать» могли бы быть ошибками компиляции? В языках, где интерфейсы не прибиты гвоздями к типам, их реализующих, так и есть.
Мне не приходило в голову то, что ошибки времени исполнения могут быть ошибками времени компиляции, да) Ну то есть, вот у вас есть приходящая строка. Как вы хотите гарантировать, что она будет в корректном формате в момент компиляции?
Про прибитые интерфейсы в C# вы что-то странное пишете. Видимо, имелось в виду, что нельзя для чужих типов определять свои интерфейсы. Ну так наследуемся и определяем.
Зачем мне нужно, чтобы я мог соединить 2 библиотеки вместе и не писать клей в своем приложении, а чтобы оно просто заработало? Действительно, зачем.
Давайте так: либо конкретно, либо вовсе не отвечать. Демагогию разруливать нет никакого желания. Соединяйте библиотеки, на здоровье.
Документация, IDE.
То есть, вместо простого списка — целая документация? Спасибо, я как-нибудь по старинке.
Нам обычно не нужно знать все контракты, которые реализует тип (впрочем, это обычно написано в модуле, где он объявлен). Нам важнее, реализует ли он контракт Х или нет.
Разумеется, и это мы можем обнаружить, глядя на список всех контрактов) Если контракты большие — можно разбить файл на несколько, на каждый оставив один контракт.
А я вот не хочу 20 перегрузок, я хочу чтобы из коробки работало со всеми типами.
Это как? Волшебным образом среда сама поймёт, как каждый из типов должен работать?
Вышло бы что-то вроде:
И ничего качественно не изменилось. Тот же набор методов, та же копипаста.
Тут мы не объявляем 10 типов просто чтобы выразить простую идею
Я вам вынужден сказать, что изначально не надо было объявлять 10 типов. Все методы можно уместить в одном. Более того, изначально вы хотели уйти от копипасты, но теперь у вас не 10 методов, а 20.
Хотя щас подумалось, что в конкретном случае можно было бы обойтись просто пачкой методов расширений. Но в общем случае это задачу не решает
Угу, и тоже в одном типе. Собственно, мне и интересно, какая задача должна решаться и каким образом. Но, видимо, пока не узнаю.
Мне не приходило в голову то, что ошибки времени исполнения могут быть ошибками времени компиляции, да) Ну то есть, вот у вас есть приходящая строка. Как вы хотите гарантировать, что она будет в корректном формате в момент компиляции?

Вопрос не в формате, а в том, что все типы рекурсивно умеют десериализовываться. Вот написал я JsonConvert.DeseralizeObject<WeatherController>("Hello"); а он падает, потому что не умеет этот тип десериализовывать и конвертер для него не задан. А хотелось бы ошибку времени компиляции получить. Гарантировать что формат корректный — никак не выйдет, да, а вот гарантировать, что тип хоть какой-то формат определяет (т.е. в целом знает про жсон и умеет с ним работать) — уже вполне.


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

Только от инта не отнаследуешься. Куча полезных классов sealed. И главное — мне не нужно наследование, мне нужно навесить немного функционала сверху. Навешивать реализацию как раз не хочу. Ну, можно сказать, нужно виртуальные экстешн методы. Чтобы можно было не только расширить типы парой экстеншнов, но и использовать их в каком-то едином коде. Если я написал MyExtMethod() для пяти разных типов, я не могу потом использовать его в MyWrapperMethod — потому что эти перегрузки друг про друга ничего не знают. Это неудобно.

То есть, вместо простого списка — целая документация? Спасибо, я как-нибудь по старинке.

У сишников тот же аргумент в пользу хэдер файлов. Ну как же, вот в одном месте все собрано, без реализаций! На практике же как вы наверное знаете — не особо удобно. Так что список всех интерфейсов — не очень нужен. А если нужен то выглядит примерно так и гуглится ну на 5 секунд дольше мб.


Разумеется, и это мы можем обнаружить, глядя на список всех контрактов) Если контракты большие — можно разбить файл на несколько, на каждый оставив один контракт.

Нет, можно просто написать foo as MyDesiredInterface и посмотреть, работает или нет.


Это как? Волшебным образом среда сама поймёт, как каждый из типов должен работать?

Не волшебным, а вполне конкретным.


И ничего качественно не изменилось. Тот же набор методов, та же копипаста.

Вопрос в open-closed принципе. Если у вас для того чтобы доработать ваш код нужно чтобы в стд был некий INumber, иначе с числами нельзя работать на основе единого интерфейса — то это плохой подход. Лучше было бы, если бы можно было объявить свой INumber, объяснить как стандартные типы ему соответствуют и получить все нужные преимущества. Да, если интерфейс достаточно очевидный и нужный всем (вроде нашего INumber) его можно включить в стандартную поставку, никто не против. Но не должно быть такого, что список интерфейсов конечен и нерасширяем. Сколько лет ждали пока этот INumber впилят?


Угу, и тоже в одном типе. Собственно, мне и интересно, какая задача должна решаться и каким образом. Но, видимо, пока не узнаю.

Да возьмите тот же LINQ: у вас 20 перегрузок метода Sum. А должнен быть ровно один генерик метод который требует только SGroup<T>. Чтобы можно было работать не только со стандартными типами, но и с BigInreger и каким-нибудь MongoDB.Core.Number.

а он падает, потому что не умеет этот тип десериализовывать и конвертер для него не задан
Ну так тип же ничего не должен уметь. Он просто представляет данные, а «умеет» — сам механизм. И в итоге, всё работает без конвертеров. И рекурсивно в том числе.
Ну как же, вот в одном месте все собрано, без реализаций! На практике же как вы наверное знаете — не особо удобно.
Хэдер файлы — это совсем неудобно. Но они тут не причём, все интерфейсы в C# собираются вместе с реализацией. А то, что вы показываете по ссылке — нечитаемо, и больше похоже как раз на хэдер файлы.
Нет, можно просто написать foo as MyDesiredInterface и посмотреть, работает или нет.
Оно всегда работает. Просто результат работы разный.
Вопрос в open-closed принципе.
Ну так сначала вопрос был в копипасте же. В остальном спорить не буду — такой подход может быть, хоть он, мне кажется, противоречит концепции C# и может быть сложен в понимании и реализации. Например, что, если мне не нужны все стандартные типы? Сидеть и писать реализации для всяких там short и ushort, когда мне они вообще не нужны — это прямая противоположность изначальной идее, когда мы хотим уменьшить объём кода. Тут уж лучше написать 16 перегрузок, честное слово. (точнее, их будет меньше, т.к. в конечном коде нужно ограниченное количество типов)
Да возьмите тот же LINQ: у вас 20 перегрузок метода Sum. А должнен быть ровно один генерик метод который требует только SGroup.
Нельзя не согласится. Но вот только чем это отличается от условного INumber? Зачем отдельный механизм? Как не добавили сразу интерфейс — так не добавили бы шейп, и наоборот.
Ну так тип же ничего не должен уметь. Он просто представляет данные, а «умеет» — сам механизм. И в итоге, всё работает без конвертеров. И рекурсивно в том числе.

Так а где он умеет? Поменял я Account на System.IO.FileStream и он прекрасно мне все скомпилировал, только в рантайме выдает ерунду. Тогда как я бы хотел явной ошибки "тип который вы хотите не может быть десериализован". То же самое с IQueryable-запросами, написал такой foo.Select(x => (x.Foo, x.Bar)).ToArrayAsync() а он тебе все отлично скомпилировал, а в рантайме выдал System.InvalidOperationException: The LINQ expression could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly.


А как должно быть? А вот хотя бы так. Не умеет тип десериализовываться — выдай ошибку компиляции. Если бы меня устраивали рантайм эксепшны я бы на питоне писал.


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

Они причем: то что все реализуемые типом интерфейсе перечисленны через запятую зачастую лишняя информация. А вот копипаста из-за необходиммости расширять через всякие адаптеры — вполне реальная.


Например, что, если мне не нужны все стандартные типы? Сидеть и писать реализации для всяких там short и ushort, когда мне они вообще не нужны — это прямая противоположность изначальной идее, когда мы хотим уменьшить объём кода.

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


Нельзя не согласится. Но вот только чем это отличается от условного INumber? Зачем отдельный механизм? Как не добавили сразу интерфейс — так не добавили бы шейп, и наоборот.

Затем, что на все случаи жизни INumber не напасешься, и за каждым нужным интерфейсом просить умолять майкрософт добавить интерфейсик — задача с пятью звездочками. Одно дело самому на коленке на за 10 минут написать реализацию и другое сидеть строчить RFC "пожалуйста, добавьте интерфейс Х" и потом месяцами отвечать зачем это вообще нужно, с шансов в 99.9999% что скажут "не приоритет", если вообще будут отвечать.

А что мешает написать, например, так:
public static T DeserializeFromConsole<T,U>() where U : IMyDeserializable<T> => U.FromString(Console.ReadLine());


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

То что эти U начинают плодиться как ненормальные. У вас спокойно может быть 10-20 генерик аргументов из-за этого) Причем которые нужно тянуть с самого верха приложения. У меня например в паре мест из-за этого стоит грязный хак if (IsAllowed(typeof(T)) { ... }, вместо честного if (TMyImplicit.IsAllowed) { .. }. Потому что оказалось что надо прокидывать в сервис, который соответствено требует прокинуть в другой сервис, тот в третий, тот в контроллер, а контроллеров которые вызывают его три десятка… В итоге пожертвовали безопасностью ради того чтобы не править полсотни файлов. И даже не то, что править, а нарушать SRP и OCP, когда у вас файлы самого верхнего уровня (контроллеры) знают про низкоуровневые особенности какие типы при сериализации в монгу нужно особым образом обрабатыать.


А хочется, чтобы не надо было жертвовать.


P.S. у меня рендер хабра сожрал почти весь ваш код, пришлось смотреть в консоли F12 :D

То что эти U начинают плодиться как ненормальные. У вас спокойно может быть 10-20 генерик аргументов из-за этого) Причем которые нужно тянуть с самого верха приложения.

Ну тогда м.б. как-то так:
var serializer = IMyDeserializable<T>.Create();
или
var serializer = DiContainer.Create<IMyDeserializable<T>>();

а потом вызываем наш метод:

public static T DeserializeFromConsole<T>

(IMyDeserializable<T> serializer) =>

serializer.FromString(Console.ReadLine());

так:

var data = DeserializeFromConsole<T>(serializer);

>> P.S. у меня рендер хабра сожрал почти весь ваш код, пришлось смотреть в консоли F12 :D

В таком случае прошу прощения, у меня код показывается нормально (на FF).

В общем мне стало понятно, что решение с шейпами (трейтами, тайпклассами) тут будет элегантнее, но языки типа Rust и Haskell изначально на них построены, а C# - нет, у него дизайн гораздо ближе к классическому ООП. И тут объясним скепсис разработчиков компилятора - внедрять в язык фичу, ортогональную текущим возможностям, создавая при этом доп. когнитивную нагрузку на мозг программистов, особенно Junior, ну такое себе. Вон выше люди даже по поводу этой фичи из статьи голосуют: "Хватит делать С++", хотя она и проще. Да и в общем наверное правильно: нужно оно редко, обойтись без него можно, а вот изучать все новые фишки языка всё равно придётся, т.к. где-то да встретится, хотя бы на интервью.

А для энтузиастов и так уже есть Haskell, Rust, Idris и что там ещё сейчас популярно... C# всё-таки задумывался как мейнстримный язык и язык-клей. И ИМХО предназначен он не для самых сильных и увлечённых, а скорее для таких, которые будут копать отсюда и до обеда :).

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


Куда проще написать что-то в духе:


extension class int: IMyDeserializer<int>
{
    public bool TryDeserialize(Span<char> buffer, out int value)
    {
        // impl
    }
}

Главный вопрос: а как это вообще можно реализовать?

В общем мне стало понятно, что решение с шейпами (трейтами, тайпклассами) тут будет элегантнее, но языки типа Rust и Haskell изначально на них построены, а C# — нет, у него дизайн гораздо ближе к классическому ООП. И тут объясним скепсис разработчиков компилятора — внедрять в язык фичу, ортогональную текущим возможностям, создавая при этом доп. когнитивную нагрузку на мозг программистов, особенно Junior, ну такое себе.

Это да, встроить её в язык может быть сложно или даже не возможно. Посмотри, выйдет ли у людей что-то. Вес наследия дает о себе знать, безусловно.


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

Мне кажется люди любого языка после любых изменений так говорят) Кому-то вон до сих пор var мешает дескать неявно слишком.


А для энтузиастов и так уже есть Haskell, Rust, Idris и что там ещё сейчас популярно… C# всё-таки задумывался как мейнстримный язык и язык-клей. И ИМХО предназначен он не для самых сильных и увлечённых, а скорее для таких, которые будут копать отсюда и до обеда :).

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


Что до DiContainer.Create<IMyDeserializable<T>>() то во-первых этот код использует сервис локатор, во-вторых имеет рантайм кост, а в-третьих не дает ошибку компиляции если тип для такого Create не зарегистрирован — будет эксепшн и все.

С сериализаторами тоже не всё так просто. Сделать кастомную реализацию для простых типов — ок. Но как быть с объектами, которые сериализуются единообразным образом просто как набор полей или свойств через рефлексию?

Да никак с ними не быть) Можно просто сгенерировать реализацию через SourceGenerators если она тривиальная. Но реализация шейпа ICanBeDeserialized должна быть явной.

Возможно стоило бы разделить C# на 2 ветки c раздельной сертификацией:
С# Standard - это где-то уровень C# 6.0 + м.б. какие-то синтаксические плюшки из более старших версий. А вот скажем честно: все эти Span<T>, ref readonly struct, pattern matching не особо и нужны. И вносить потом изменения в этот диалект крайне редко и только самые нужные.

C# Advanced - а вот на нём уже можно экспериментировать сколько угодно, догоняя Haskell по сложности. Тут вам и dependent types и HKT и GADT, и что душа пожелает.

Зачем? Это же новый язык получится. Например, можно в качестве примера взять Kotlin vs Java. Но у Kotlin мощное лобби от JetBrains. А вот альтернативу C# предложить некому. Не, ну есть F#, но это совсем не то.

Ну не новый язык, а 2 ветки старого. Одна развивается с нормальной скоростью, а в другую только изредка делаются бэкпорты. Это всё же намного легче, чем поддерживать 2 разных языка.
Пример из обычных языков: English и Simple English. Последний нужен, например, тем, кто не планирует читать Шекспира или Байрона в оригинале, но иногда смотрит в документацию, чтобы узнать, как вызвать нужную функцию.

С# Standard — это где-то уровень C# 6.0 + м.б. какие-то синтаксические плюшки из более старших версий. А вот скажем честно: все эти Span<T>, ref readonly struct, pattern matching не особо и нужны. И вносить потом изменения в этот диалект крайне редко и только самые нужные.

Известный факт, что люди пользуютяс только 5-10% ворда. Проблема в том, что у всех эти 5-10% свои.


Так и с языками. На всех эдишнов не напасешься.

Во многих языках присутствуют стандарты:
C99

C++2x
Fortran88

да миллион их

И это работает. Проблема с C# только в том, что его стандарт ECMA-2 очень устарел и не актуален. Надо бы обновить. И добавить возможность при необходимости компилятором включать отдельные расширения, как это сделано, например, в Haskell.

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

Ну тогда почти всё уже готово. Дело за немногим - довести стандарт до уровня C# 6.0, добавив попутно полезные фичи, нужные всем, типа null checking. Затем популяризовать среди производителей ПО и ввести отдельную сертификацию по нему (экзамены).

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

Haskell смущает своей обязательной ленивостью, кот. ИМХО не особо-то на практике и нужна, зато может приводить к утечкам памяти в неожиданных ситуациях.
А вот Rust, да, имеет шансы, но пока видел его только в разработке микросервисов для банкинга и HFT. Но всё же он, со своими лайфтаймами, сложнее C#, а основная масса программистов идёт по пути наименьшего сопротивления. И поэтому там, где доп. производительность, предоставляемая Rust'ом, не критична, легче использовать язык со сборкой мусора.

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

Ленивость обоюдоострое оружие. Из плюсов можно отметить отсутствие необходимости явно делать перегрузку на каждое нужное поведение. Например, клиент может на своей стороне выбрать short-circuit вычислени производить или нет и т.п.


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


Но всё же он, со своими лайфтаймами, сложнее C#, а основная масса программистов идёт по пути наименьшего сопротивления. И поэтому там, где доп. производительность, предоставляемая Rust'ом, не критична, легче использовать язык со сборкой мусора.

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

Ну, на .NET есть ещё F#. Там до сих пор не было правда из коробки traits, но в следующей версии собираются запилить вот эту фичу https://github.com/dotnet/fsharp/pull/6805 (сам Don Syme - автор языка, её пилит).
Что, как я понимаю, будет фактически реализацией traits, правда только для inline-функций и с упоротым синтаксисом.

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


Я лично для себя фарш рассматривают как го от мира ФП. Очень изолированное коммьюнити, которую на всю критику и/или предложения по улучшению ответить могут только "нинужна". А если не видеть недостатки, то и улучшений не будет.


Вот и этот пропозал:


This slightly strengthens the "type-class"-like capabilities of SRTP resolution. This means that people may increasingly use SRTP code as a way to write generic, reusable code rather than passing parameters explicitly. While this is reasonable for generic arithmetic code, it has many downsides when applied to other things.

Т.е. "Мы вам фичу дадим, но у нее куча минусов так шо вы там смотрите осторожно, мб не юзайте вообще"

Ну визуально он не очень похож на C#, скорее уж на своего непосредственного родителя - OCaml. Те же Rust или Nemerle на C# гораздо более похожи.

Мне кажется, там проблемы с финансированием группы разработки. Раньше, лет 15 назад, во времена F# 2.0, он на мегапарсеки превосходил C#, и был самым передовым на .NET. Но за все эти 15 лет C# натырил практически всё полезное из F# (LINQ, async, pattern matching...), и в результате последний растратил почти всё своё преимущество. Кое-что осталось, но уже не суть. И теперь началась обратная гонка F# -> C#. И Дон Сайм пытается тянуть разработку вперёд лично, но его сил, кажется, недостаточно.

Например, приведённый пропозал практически единственный хоть сколько-то интересный в следующей версии (хотя и для C# верно то же самое с Generic Math).

>> Т.е. "Мы вам фичу дадим, но у нее куча минусов так шо вы там смотрите осторожно, мб не юзайте вообще"

Ну, тут надо понимать, что имеется в виду под минусами. Насколько я понимаю, это то что оно работает только для inline-функций, никак не поддерживается в .NET или в C#, и несколько загадочный синтаксис вызова функций через SRTP. А не то что оно будет как-то криво работать.
Так что для кого-то, это м.б. и минусы, а кому-то и норм. А полноценная поддержка шейпов отложена до появления подобной в C#.

Кстати, вспомнил ещё один важный кейс: условная реализация интерфейса. Вот пишу я :


class MyList<T> { ... }

И вопрос: реализовывать ли мне интерфейс IEquitable<T>? С одной стороны было бы полезно, а с другой в текущем шарпе я могу сделать только


class MyList<T> : IEquitable<MyList<T>> 
  where T : IEquitable<T> { ... }

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


Можно ли так сделать? В мире, где интерфейсы не перечислены вот так через запятую: можно:


struct MyList<T> { ... }

impl Eq for MyList<T> where T : Eq {
  ...
}

В текущей парадигме C#, как я понимаю, это не проблема. Реализуется IEnumerable<T>, а как сравнивать коллекции, все уже давно знают:
list1.SequenceEquals(list2);

Ну, или если это не сравнение, а что-то более другое, тогда:

public static class EnumerableExtensions {
public static bool AreMyWishesFullfilled<T>(this IEnumerable<T>) {...}
}

А если всё же хочется через интерфейс, тогда нужен отдельный
class MyListEqualityComparer<T> : IEqualityComparer<MyList<T>>

where T : IEquatable<T> {...}

В текущей парадигме C#, как я понимаю, это не проблема. Реализуется IEnumerable<T>, а как сравнивать коллекции, все уже давно знают:
list1.SequenceEquals(list2);

Так это какой-то хелпер из линку который реализует сравнение, а мне нужен свой метод MyList1.Equals(MyList2). Который например всегда возвращает true если MyList2 — пустой. И конечно же это должен быть метод интерфейса, да.


А если всё же хочется через интерфейс, тогда нужен отдельный

Окей, для сравнения у нас есть IEqualityComparer и даже есть паттерн работы с ним. А что для условной реализации IDisposable? Из того что я видел люди обычно делают:


class Foo<T> : IDisposable { 
  ...
  if (this._inner is IDisposable) this._inner.Dispose();
}

Т.е. просто реализуют интерфейс всегда, а потом проверяют в диспозе а надо ли что-то делать или нет. Для диспоза это работает (хотя выглидит отвратно), потому что метод войдовый, а вот с интерфейсами которые что-то возвращают — уже проблема. Ну например, как сделать тип, который IList если внутренний тип тоже IList, а если нет то нет? В отличие от IEquitable/IComparable у них нет паттерна передачи "реальной" реализации отдельно.


В мире, где интерфейсы перечислены через запятую, тоже можно, просто делаем лист-наследник

Упадет в случае кучи кейсов, проверяющих на типы в рантайме — всякие десериализаторы и прочее.


А как в вашем «мире без списка интерфейсов» я узнаю, поддерживает ли тот или иной класс сейчас то или иное поведение?

Список интерфейсов есть, просто реализации могут быть не в 1 месте. А узнать точно так же, как "какие эстешн методы объявлены для типа" — вот ровно 1 в 1. Разница с экстеншнами только в том, что эти методы можно вызывать виртуально, а в остальном все то же самое. Или у вас экстешны запрещены?)

Мне кажется, что вы уже начали нервничать, раз отвечаете 2-м разным людям в одном комменте :).

Что касается того, как проксировать IList статически, то, думаю, на C# можно воспользоваться генераторами, а на F# - провайдерами типов. А если можно динамически, то через Reflection.Emit или Mono.Cecil, возможно, есть и другие способы, напр. DLR или DispatchProxy, но тут я не специалист. Но, как я понимаю, вопрос не про это.
И вы, конечно, можете начать меня сейчас гонять по ограничениям системы типов C#, задавая задачки, например, из мануала Idris'а. И, понятное дело, что я сольюсь, т.к. их возможности несравнимы. Вопрос в другом, нужно ли это всё обычному Васе при написании его сайта или утилиты или игры или чего-там ещё? А если действительно такая потребность возникла, то почему тогда он не перейдёт на более продвинутый инструмент, а продолжает ругать такой плохой C#? ИМО, язык, написанный одновременно для всех - это язык ни для кого, поэтому всегда будут разные языки под разные задачки и интеллектуальные способности программиста, решающего эти задачки.

Вопрос в другом, нужно ли это всё обычному Васе при написании его сайта или утилиты или игры или чего-там ещё?

Вполне себе используется. Просто многие инструменты обходят это через рефлекшн, иногда чуть более успешно, иногда менее. Например, сравнение как я показал выше очень полезно в тестах. Assert.Equals(arr1, arr2) выдает ерунду, поэтому майкрософт завезли CollectionAssert, который делает все то же самое но чуточку иначе. Подобные примеры копипасты и дублирования АПИ как раз и вызваны ограничениями со стороны языка. Ну а динамическая природа переводит ошибки в рантайм, что печально.


А если действительно такая потребность возникла, то почему тогда он не перейдёт на более продвинутый инструмент, а продолжает ругать такой плохой C#?

Если ты такой умный, то чего ты такой бедный (с).
Во-первых через 3 недели я заканчиваю пользоваться этим инструментом. Во-вторых кроме инструмента есть ещё куча сторонних факторов, как минимум наличие библиотек и рынок труда, определяющий басфактор. Какой бы я там разработчик ни был, нужно подбирать инструмент под команду. Нужно обоснование, почему Х а не Y и так далее. Если на условном хачкеле мы бы написали в 1.5 раза быстрее, чем на итоне, но время поиска разраба на питон в 100 раз меньше, и зп у него на 20% ниже, при наборе команды из 10-20 человек выбор будет вероятно в сторону питона. Вне зависимости от того, что там за супер типы ловящие все на этапе компиляции тогда как в питоне можно строк вместо числа случайно передать и он не скажет об этом.


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


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

>> Нужно обоснование, почему Х а не Y и так далее
А в чём проблема дать такое обоснование, если начальство адекватно?
И не обязательно же прям всё писать на Haskell, вызывая ощущение, что вы занимаетесь Job Insurance. Можно же написать только тот кусок, который реально получает выгоду от его системы типов. А потом привязать его при помощи FFI. Так и обосновать проще.
Например, у меня большая часть написана на C#, но есть и библиотеки на F#, и никто голову мне за это не открутил (пока).

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

Когда программист с чем то незнаком, то он и не может придумать примеры, зачем бы ему это понадобилось в его текущей работе. Например, я позавчера сидел и думал, зачем бы мне понадобился Generic Math, описанный в этой статье как мегакрутая фишка. И так и не смог придумать ни одного алгоритма, кот. бы его требовал. Все сортировки обходятся без него. А складывать, умножать и делить нужно конкретные типы. Поэтому это всё воспринимается как очередной ненужный висяк на голову, синтаксический трюк, кот. жизнь не облегчает, никогда использовать не будешь, но выучить всё равно будет надо, хотя бы ради собесов.

И не обязательно же прям всё писать на Haskell, вызывая ощущение, что вы занимаетесь Job Insurance. Можно же написать только тот кусок, который реально получает выгоду от его системы типов. А потом привязать его при помощи FFI. Так и обосновать проще.
Например, у меня большая часть написана на C#, но есть и библиотеки на F#, и никто голову мне за это не открутил (пока).

Если это сервис, в который никто никогда не будет лазить и с ним просто через апи в черный ящик будут пихать данные а он давать ответ — то не вопрос, писать можно на чем угодно. У меня стати один такой сервисн а ноде в прод затащен, просто потому что под ноду была нужная либа, а под другие языки — нет. Но это крайне редкий сценарий. Так что да, важно, чтобы команда знала язык. На уровне разработчика это ещё норм, на уровне сениора/лида когда ты можешь на звонке 8 часов за день провести нужно чтобы был человек, которому можно делегировать написание кода, и у него не будет вылупливания глаз "ээ что это за язык такой?". Обосновать чем X > Y я могу в плане языка, но когда начинаем сравнивать экосистемы и рынок труда все не так хорошо становится. Для компаний типа фейсбука кандидаты не проблема, да и библиотеки они чаще свои пишут, чем пользуются — поэтому хачкелистов там полно, пишут свои спам фильтры и в ус не дуют. Но не все — фейсбуки.


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

Коты хорошие, зря вы их так. А если серьезно, то это парадокс блаба, да. И я не знаю, Как вам показать пример без реального приложения, так, чтобы это не выглядело как "да я могу то же самое сделать с помощью Х". Да, путем некоторого количества глины, палок, чьей-то матери, адаптеров и рефлекшна можно сделать все что угодно. В конце концов все языки тьюринг полные, и на любом можно написать что угодно. Но "Это не та мощь, которая нужна программисту" (с).

>> и у него не будет вылупливания глаз "ээ что это за язык такой?"

На собесе можно же примерно так спросить: "Тут у нас Haskell кое-где, не слишком часто, но встречается. Обычно я сам его пилю, но мало ли, а вдруг. Я вижу, что у тебя в резюме он не указан. Ты как, непротив посмотреть его на досуге? Ну или не на досуге. Очень интересная штука на самом деле". И если человек не идёт в отказ, то значит проблема решена. А если идёт, то тут уже повод задуматься, а чего это он так незнакомых технологий боится? А если ему, например, PostgreSQL на MongoDB заменить, то также испугается? А стоит ли его вообще брать?

А за статью спасибо, почитаю :)

А узнать точно так же, как «какие эстешн методы объявлены для типа» — вот ровно 1 в 1.
То есть, никак. Экстеншн методы — это всё же дополнительные упрощалки жизни, а не собственное поведение. И да, если нет дополнительной документации или знания проекта, об эстеншенах мы ничего не узнаем, а если брать такое поведение как у вас, мы будем удивлены поведением, которое описано где-то там.

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

Ну, мне кажется что необходимость "вынь да полож все интерфейсы, который тип реализует" не сильно нужна. Все, что тип реально реализует так же через запятую показанно в документации и можно попросить IDE выводить (если вам не понравился рендеринг того сайта что я скинул — ничто не мешает сделать лучше. Просто никому это не нужно — и так все понятно). Как это работает: Есть у нас библиотека A в которой есть тип X и реализации всяких стандартных интерфейсов (вроде тустринга, сравнения, и всего такого). Рядом есть библиотека A.Serialized которая например расширяет тип А возможность (де)сериализации из/в жсона там и т.п. Потом есть A.Loggable которая добавляет возможность использовать тип для логгирования каких-нибудь кишочков.


И все это можно в любых комбинациях подключать на call site, т.е. выбирать что нужно по обстоятельствам. Это весьма удобно на практике получается. Для нативных языков вроде раста это ещё и в уменьшение кода выливается, но в целом семантически это удобно. И если у вас есть этот тип X и у вас есть интефрейс Y и вам хочется использовать X в контексте Y вы можете сами написать реализацию, и использовать везде, безо всяких адаптеров. Это весьма удобно на практике.


Пропагандировать, впрочем, не буду. Я поработал и с шарповым подходом 10 лет, и с тайпклассовым 4 года. Второй кажется строго лучше, чем первый. Минус что у нас не фиксирован набор интерфейсов, которые мы реализуем — мне кажется не минусом, а плюсом. А список интерфейсов реализующихся в "стандартной поставке" мы можем и так узнать. Никакой магии или неявности тут не происходит — просто возможность писать код минуя адаптеры, из-за чего комбинаторика дает нам сильное уменьшение кодовой базы.


Что до эффективности, то при компиляции Rust/Haskell кода незаметно, чтобы поиск интерфейса занимал какое-либо длительное время. В конце концов, список неймспейсов куда смотреть у вас всегда есть в начале файла. И он не такой большой обычно.

А если серьезно, то это парадокс блаба, да. И я не знаю, Как вам показать пример без реального приложения, так, чтобы это не выглядело как "да я могу то же самое сделать с помощью Х"

Ну вот в F# есть этот Generic Math. Причём с самого начала он есть, хоть и реализован м.б. и не так красиво как в Haskell или Rust. И никогда ни потребности ни желания его использовать не возникало.

Ладно, потом погуглю ещё эту тему :).

В мире, где интерфейсы перечислены через запятую, тоже можно, просто делаем лист-наследник

class MyList<T> { ... }

class MyComparableList<T> : MyList<T>, IEquitable<MyList<T>> 
  where T : IEquitable<T> { ... }

А как в вашем «мире без списка интерфейсов» я узнаю, поддерживает ли тот или иной класс сейчас то или иное поведение?

Случайно ответил выше

Можно немного поподробнее про текущие костыли

struct IOperationProvider<T>
{
    T Sum(T lhs, T rhs)
}

void SomeProcessing<T, TOperation>(...)
    where TOperation : IOperationProvider<T>
{
    T var1 = ...;
    T var2 = ...;
    T sum = default(TOperation).Sum(var1, var2);  // This is zero cost!
}

я правильно понимаю, что вместо struct IOperationProvider<T>, на самом деле, должен быть объявлен интерфейс, и совсем избавиться от виртуальных вызовов не получится?

Спасибо за замечание. Поправил код в статье (ошибся при наборе).
Там смысл в том, что используется трюк, что JIT-компилятор не делает боксинг структур в дженериках и генерит отдельный код для каждого типа-структуры. Структуры наследловать нельзя, поэтому виртуальных вызовов тоже нет.

Ну, в общем, всё плохо с реализацией этой фичи.
https://github.com/dotnet/csharplang/blob/main/proposals/static-abstracts-in-interfaces.md
В .NET 7 уже до неё руки похоже, не доходят.
А в 6 она даже не билдится в VS 2022 17.2 Preview 2.
Хотя, по идее, на то они и превью фичи, чтобы работать в превью студии.
(System.Runtime.Experimental у них целиком отвалился. Вроде исправили. Когда зарелизят, не известно)
Интересно и, может быть, было бы даже удобно, если эффективность не проседает.
Но пока не понятно будет ли это все. И в каком окончательном виде, если будет.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории