Перевод ироничного поста из блога Боба Мартина в котором он рассуждает о том, насколько неудачным является использование слова interface
в современных языках программирования, и какую путаницу и проблемы оно несёт разработчикам.
— Что ты думаешь об интерфейсах?
— Имеешь в виду интерфейсы в Java или C#?
— Да. Классная фича этих языков?
— Просто великолепная!
— Правда? А что такое интерфейс? Это то же самое что и класс?
— Ну… Не совсем!
— В каком плане?
— Ни один из его методов не должен иметь реализации.
— Значит это интерфейс?
public abstract class MyInterface {
public abstract void f();
}
— Нет, это абстрактный класс.
— Так, а в чём разница?
— Абстрактный класс может иметь реализованные методы.
— Да, но этот класс их не имеет. Тогда почему его нельзя назвать интерфейсом?
— Абстрактный класс может иметь нестатические поля, а интерфейс не может.
— У моего класса их тоже нет, почему он не интерфейс?
— Потому что!
— Такой себе ответ… В чем реальное отличие от интерфейса? Что такого можно делать с интерфейсом, чего нельзя делать с этим классом?
— Класс, который наследуется от другого, не может унаследоваться от твоего.
— Почему?
— Потому что в Java нельзя наследоваться от нескольких классов.
— А почему?
— Компилятор тебе не позволит.
— Очень странно. Тогда почему я могу реализовать(implements
), а не отнаследоваться(extend
) от него?
— Потому что компилятор позволяет множественную реализацию интерфейсов, но наследовать ты можешь только один класс.
— Интересно, зачем такие ограничения?
— Потому что наследование от множества классов опасно.
— Вот так новости! И чем же?
— Смертельным Бриллиантом Смерти(Deadly Diamond of Death)!
— Звучит пугающе! Но что это значит?
— Это когда класс наследует два других класса, оба и которых наследуют третий.
— Ты имеешь ввиду что-то типо этого?
class B {}
class D1 extends B {}
class D2 extends B {}
class M extends D1, D2 {}
— Верно, это очень плохо!
— Почему?
— Потому что класс B может содержать переменные!
— Вот так?
class B {private int i;}
— Да! И как много переменных i
будет в экземпляре класса M
?
— Понятно. Т.е раз D1
и D2
содержат переменную i
, a M
наследуется от D1
и D2
то ты ожидаешь, что экземпляр класса M
должен иметь две разных переменные i
?
— Да! Но т.к M
наследуется от B
у которого только одна переменная i
, то ты ожидаешь что в M
у тебя тоже будет всего одна i.
— Вот так неоднозначность.
— Да!
— Получается что Java(и C#) не могут во множественное наследование классов, потому что кто-то может создать "Смертельный Бриллиант Смерти"?
— Не просто может создать. Каждый априори создавал бы их т.к все объекты неявно наследуют Object
.
— Ясно. А авторы компилятора не могли пометить Object
как частный случай?
— Ну… не пометили.
— Интересно почему. А как решается эта проблема в других компиляторах?
— Компилятор C++ позволяет делать это
— Я думаю Eiffel тоже.
— Черт, даже в Ruby смогли решить эту проблему!
— Ладно, получается что "Смертельный Бриллиант Смерти" это проблема, которую решили еще в прошлом веке, и она не фатальна и даже не ведёт к смерти.
— Вынужден согласиться.
— Давай вернемся к первоначальному вопросу. Почему это не интерфейс?
public abstract class MyInterface {
public abstract void f();
}
— Потому что он использует кейворд class
и язык не позволит унаследоваться от множества классов.
— Верно. Получается, что кейворд interface
был изобретен для предотвращения множественного наследования классов?
— Да, наверное.
— Так почему бы разработчикам Java (да и C#) не воспользоваться любым из решений проблемы множественного наследования?
— Откуда я знаю?
— Кажется я знаю.
— ??
— Лень!
— Лень?
— Да, им было лень разбираться с проблемой. Вот они и создали новую фичу, которая позволила им не решать её. Этой фичей стал interface
.
— Т.е ты хочешь сказать, что разработчики Java ввели понятие интерфейса чтобы избежать лишней работы?
— У меня нет другого объяснения!
— Звучит грубовато. Да и в любом случае, круто что у нас есть интерфейсы. Они тебе чем нибудь мешают?
— Ответь себе на вопрос: Почему класс должен знать что он реализует именно интерфейс? Разве это не должно быть скрыто от него?
— Имеешь в виду, что производный тип должен знать что именно он делает — наследует или реализует(extends or implements
)?
— Абсолютно! И если ты поменяешь класс на интерфейс, то в код скольки наследников придется вносить изменения?
— Во всех. В Java во всяком случае. В C# разобрались хотя бы с этой проблемой.
— Да уж. Ключевые слова implements
и extends
излишни и опасны. Было бы лучше если бы Java использовала решения C# или C++.
— Ладно, ладно. Но когда тебе реально нужно было множественное наследование?
— Я бы хотел делать так:
public class Subject {
private List<Observer> observers = new ArrayList<>();
private void register(Observer o) {
observers.add(o);
}
private void notify() {
for (Observer o : observers)
o.update();
}
}
public class MyWidget {...}
public class MyObservableWidget extends MyWidget, Subject {
...
}
— Это же паттерн "Наблюдатель"!
— Да. Это правильный "Наблюдатель".
— Но он не скомпилируется, т.к ты пытаешься отнаследоваться от двух классов сразу.
— Да, в этом и трагедия.
— Трагедия? Какого… Ты же можешь просто унаследовать MyWidget
от Subject
!
— Но я не хочу, чтобы MyWidget
знал что за ним наблюдают. Мне бы хотелось разделять ответственности(separation of concerns). Быть наблюдаемым и быть виджетом, это две совершенно разные ответственности.
— Тогда просто реализуй функции регистраци и уведомления в MyObservableWidget
.
— Что? Дублировать код для каждого наблюдаемого класса? Нет спасибо!
— Тогда пусть твой MyObservableWidget
содержит ссылку на Subject
и делегирует ему все что нужно.
— И дублировать код делегирования? Это какая-то фигня.
— Но тебе все равно придется выбрать что-то из предложенного
— Знаю. Но я ненавижу это.
— У тебя нет выхода. Либо нарушай разделение ответственностей, либо дублируй код.
— Да. Это язык сам толкает меня в это дерьмо.
— Да, это очень грустно.
— И?
— Могу лишь сказать, что кейворд interface
— вреден и губителен!
Буду признателен за ошибки и замечания в ЛС.
Комментарии (354)
Veikedo
19.03.2018 02:56+1Лениться делегировать — это экономия на спичках. Больше огребётесь от огромного количества наследников.
Наследование это не способ писать меньше кода. Это способ выразить отношение is-a (ну и ещё способ сделать discriminated union, в языках где его нет). Более того, я считаю что наследование как раз более вредно — чем чаще вы наследуете, тем более костной становится ваша система.
Примеры:
У вас есть базовый класс, который часто наследуется в вашей системе. Затем, только некоторому числу наследников понадобилось новое поведение — вы меняете базовый класс, но вместе с этим вы также меняете и контракт тех классов, которым это поведение не нужно. Как итог, вам нужно протестировать те компоненты, которые даже не менялись — нарушение OCP. А ещё частенько ведёт и к нарушению LSP.
В укор первому примеру, вы можете сказать — "да я щас наделаю много мелких классов (например, VisibleAndSolid, VisibleAndMovable, VisibleAndSolidAndMovable) с точечным поведением и буду множественно наследовать от них". Ок, но чего вы этим сэкономите? Количество LOC будет примерно сравнимым при композиции. Только в этот раз вы усложнили систему, наделав в ней кучу ненужных сущностей.
Имея некоторый базовый класс, вы делаете вид, что знаете как он будет использоваться. Нарушение инкапсуляции здесь ещё грубее — каждый разработчик должен знать детали реализации в базовом классе (иначе опять можно нарушить LSP).
- Ещё более опасна длинная цепочка наследования. Например, есть у вас некоторая иерархия с некоторым поведением в самом верхнем родителе. Затем, одному или нескольким наследникам нужно отличное поведение. Как итог порождается ещё одна иерархия классов, что в конечном итоге ведёт к сложности системе.
Да и вообще сто раз это исписанно.
ps. В java (и скоро в c#) ведь есть partial interface implementation — пользуйтесь.
DrPass
19.03.2018 03:03Затем, только некоторому числу наследников понадобилось новое поведение — вы меняете базовый класс, но вместе с этим вы также меняете и контракт тех классов
Нет, в таком случае обычно вводится ещё один промежуточный класс в иерархию
что в конечном итоге ведёт к сложности системе
Если существовала бы какая-то «серебряная пуля», которая помогала бы реализовать сложное поведение с помощью простой и очевидной модели, мы бы все ей пользовались :)Bonart
19.03.2018 08:40Серебряная пуля конкретно для наследования есть и она очень простая: «не наследуйтесь от реализаций».
AndreyRubankov
19.03.2018 10:06Нет, в таком случае обычно вводится ещё один промежуточный класс в иерархию
Что в свою очередь делает всю систему еще более запутанной. Видел я систему компонентов из 7+ уровней наследования, казалось бы все красиво и круто, но разбираться в этом – то еще удовольствие :(
Если существовала бы какая-то «серебряная пуля», которая помогала бы реализовать сложное поведение с помощью простой и очевидной модели, мы бы все ей пользовались :)
Из своего опыта могу сказать, что для меня «серебряная пуля» – это модульный подход (использовать interface и лишь один уровень иерархии).
Да, придется дублировать код в разных модулях, но в результате мы получаем Очевидную реализацию и поведение, легкость в тестировании и следование принципам LSP и SRP, что в свою очередь дает нам взаимозаменяемость модулей.Bonart
19.03.2018 10:18Да, придется дублировать код в разных модулях,
Почему придется что-то дублировать? Композицию никто не отменял.
AndreyRubankov
19.03.2018 11:44+1В статье есть об этом фраза:
— И дублировать код делегирования? Это какая-то фигня.
Как минимум код делегирования нужно будет дублировать. Лично я не считаю это проблемой, но в контексте этой статьи – это «проблема».
Athari
19.03.2018 10:46Более того, я считаю что наследование как раз более вредно — чем чаще вы наследуете, тем более костной становится ваша система.
"Не пользуйтесь ООП в ООП"? Ну круто, теперь заживём.
А ещё частенько ведёт и к нарушению LSP.
А если писать код как попало, то это "частенько" ведёт к нарушению всего SOLID. Это не причина.
И вообще, при слепом следовании SOLID и всему зоопарку сокращений с прикольными именами код получается тем ещё говном, так что ненавижу, когда прикрываются коллекциями советов для домохозяк.
Количество LOC будет примерно сравнимым при композиции.
Ага. Все рекламируют композицию и делегирование, а что в большинстве языков нет реализации интерфейса через член, советующих не волнует — ну, крутись как хочешь, плоди сотни строк непродуктивного кода.
Ещё более опасна длинная цепочка наследования.
Жить вообще опасно. Просто если хочешь унаследоваться вместо композиции — подумай 10 раз, унаследоваться глубоко — 100, унаследоваться множественно — 1000.
Глубокое наследование часто можно наблюдать в иерархиях гуёвых контролов. Потому что это работает.
Множественное наследование можно наблюдать в простых реализациях паттернов. Потому что это работает.
Если же пихать везде без разбора — получается чёрт знает что. Но это работает с любой фичей языка. Что угодно можно использовать во вред.
Интерфейсы ни от чего не спасают. Это ужасная сущность с точки зрения развития системы, потому что их изменять вообще невозможно. Любое изменение — всё, система сломана. Чем это лучше классов, где что-то в предках изменилось, и вдруг поломался потомок? Ну, хотя бы есть ненулевой шанс, что оно будет работать. Изменение интерфейса ломает систему всегда.
Впрочем, при желании можно считать это плюсом — но только в лабораторных условиях. В тех же лабораторных условиях, в которых проверяемые исключения работают, а не заставляют писать сомнительный код или мешают работе с лямбдами.
Рассуждение выше про интерфейсы немного устаревает с введением костыля под названием "default interface implementation" — интерфейсы теперь становятся недо-классами. Вот только не понимаю, как проповедник всего чистого в коде и идеализированных принципов построения архитектуры может оправдывать эту фичу. Default interface implementaon, между прочим, тоже вполне себе может ломать наследников, причём именно в непредсказуемом стиле, как и любые классы при наследовании.
Athari
19.03.2018 11:40Народ, если у вас есть мнение, то, пожалуйста, выразите его словами.
Veikedo
19.03.2018 12:20+1"Не пользуйтесь ООП в ООП"? Ну круто, теперь заживём.
ООП это не только наследование. Я не призываю отказываться от наследования совсем, но мой подход чем реже, тем лучше.
А если писать код как попало, то это "частенько" ведёт к нарушению всего SOLID. Это не причина.
Серебряной пули и правда нет. Но есть best practices и они появились не с пустого места. А насчёт аббревиатур — это удобный способ донести мысль другому человеку по-быстрому.
Ага. Все рекламируют композицию и делегирование, а что в большинстве языков нет реализации интерфейса через член, советующих не волнует — ну, крутись как хочешь, плоди сотни строк непродуктивного кода.
Везде свои компромиссы. Хотите хорошую систему, с которой приятно работать и удобно вносить изменения — делегируйте; проект небольшой — колбасьте код как угодно.
Да и про какие сотни строк вы говорите? Если у вас есть класс, который реализует так много интерфейсов, то проблема возникла раньше. Опять же из-за не следования хорошим практикам.
Жить вообще опасно. Просто если хочешь унаследоваться вместо композиции — подумай 10 раз, унаследоваться глубоко — 100, унаследоваться множественно — 1000.
Об этом и речь — зачем усложнять и думать 10-100-1000 раз, если можно сделать просто?
Интерфейсы ни от чего не спасают. Это ужасная сущность с точки зрения развития системы, потому что их изменять вообще невозможно. Любое изменение — всё, система сломана.
Интерфейсы это ваш контракт. Это ваш api, если хотите. И если контракт меняется, значит на то была причина — изменилось требуемое поведение.
Чем это лучше классов, где что-то в предках изменилось, и вдруг поломался потомок? Ну, хотя бы есть ненулевой шанс, что оно будет работать. Изменение интерфейса ломает систему всегда.
В том и проблема, что оно может будет работать. А может не будет. А может будет работать не так, как надо. А может появиться новое поведение, которое не ожидалось. В любом случае, чтобы быть уверенным придётся проверить всех наследников. С композицией надо проверить только там, где изменилось.
Рассуждение выше про интерфейсы немного устаревает с введением костыля под названием "default interface implementation" — интерфейсы теперь становятся недо-классами.
Согласен, default interface implementation неоднозначная фича. Пока что, я вижу ей применение для добавление утилитарного поведения, вроде того же observer'a.
Кстати, насчёт глубокой цепочки наследования в GUI — тот же реакт построен на High Order Components и там этот подход весьма органичен.
Athari
19.03.2018 12:43Если у вас есть класс, который реализует так много интерфейсов, то проблема возникла раньше.
Идеальную чистоту интерфейсов можно содержать только в полностью контролируемой кодовой базе, то есть примерно никогда. Во всех остальных случаях интерфейсы плодятся как кролики, потому что они не версионируемые, они в принципе не поддерживают совместимость назад.
В результате в COM мы имеем наследование от IContextMenu1, IContextMenu2, IContextMenu3, IContextMenu4, а в C# имеем ICollection, IReadOnlyCollection, IReadOnlyList (причины разные, результаты разные, но последствия всегда неприятные). И вот никуда от этого не деться. Ну не задизайнить интерфейсы так, чтобы один раз и на всю жизнь.
Интерфейсы это ваш контракт. Это ваш api, если хотите. И если контракт меняется, значит на то была причина — изменилось требуемое поведение.
Ну вот допустим в C# был бы не класс FileStream, а интерфейс IFileStream. Теперь мы хотим добавить поддержку асинхронного чтения. Ваши действия? Добавить новый интерфейс? Расширить существующий? Любое решение с интерфейсами (без default interface implementation) будет неудобным для потребителя.
С композицией надо проверить только там, где изменилось.
Идеализирование. Реализация всего может поменяться в любой момент несовместимым способом. Ломается всё, независимо от архитектуры, поэтому всё равно вы всё будете тестировать, если хотите спокойно спать по ночам.
VolCh
19.03.2018 13:03Интерфейсы это ваш контракт. Это ваш api, если хотите.
Не люблю интерфейсы как раз потому, что они очень грубо описывают контракт. То есть вроде есть контракт, но в то же время он очень-очень общий. С одной стороны, вроде лучше чем ничего, а, с другой, иной раз тратишь много времени на оформление такого контракта, а толку мало: соблюдая его на уровне синтаксиса языка, имплементации полностью рушат все даже задокументированные пред- и постусловия с инвариантами.
ApeCoder
19.03.2018 13:14Некоторые советуют дополнять реализацию абcтрактными тестами
PsyHaSTe
19.03.2018 22:30Просто перебрать всех наследников недостаточно хорошо?
vedenin1980
19.03.2018 22:36+2А как если у вас opensource библиотека у которой тысячи пользователей и каждый может сделать своего наследника от ваших классов?
ApeCoder
20.03.2018 10:05Там же написано — каждый наследник может иметь свой конструктор + дополнительно свой набор каких-то еще тестов. Абстрактный тест вызвает абстрактный factory method для создания конкретной реализации.
К тому же какие-то ассерты тоже могут быть абстрактными
Dicebot
19.03.2018 18:39+2Народ, если у вас есть мнение, то, пожалуйста, выразите его словами.
Человек, для которого ООП сводится к наследованию ("Не пользуйтесь ООП в ООП") — настолько далёк от понимания предмета, что нет смысла даже тратить время на объяснения — просто ставишь минус и идёшь дальше.
Flammar
19.03.2018 19:07Это, наверное, человек, для которого ООП сводится к избеганию лишнего дублирования кода. Такой взгляд тоже кажется имеющим право на существование.
Athari
19.03.2018 20:05Человек, для которого ООП сводится к наследованию ("Не пользуйтесь ООП в ООП")
Это передёргивание слов. Я нигде не говорил, что наследование — единственное. Но всё-таки наследование и полиморфизм — столпы классического ООП. Заметать их под ковёр странно.
Varim
19.03.2018 11:57в большинстве языков нет реализации интерфейса через член
Можно другими словами или пример кода, что бы стало понятней о чем речь?Athari
19.03.2018 12:05+1Фиче-реквест для C# не нашёл. Идея в том, чтобы писать что-то подобное (синтаксис условный):
class MyCollection : ICollection, ICollection<T>, IReadOnlyCollection<T> { private IList<T> _collection; [ ICollection is implemented by _collection ] [ ICollection<T> is implemented by _collection ] [ IReadOnlyCollection<T> is implemented by _collection ] }
вместо ручной реализации каждого метода:
// ... int Count { get { return _collection.Count; } } bool IsReadOnly { get { return _collection.IsReadOnly; } } // ...
Собственно, сказка для композиции.
Varim
19.03.2018 12:27Интересно, а в каких языках такое есть?
VolCh
19.03.2018 13:05Очень похожее есть в PHP — ниже есть пример. Только нет явной привязки "имплементации" к интерфейсу.
xander27
19.03.2018 13:43ookami_kb
19.03.2018 14:19В котлине – почти то, что нужно. Но там, я так понял, это возможно только для делегата, определяемого в конструкторе.
artemshitov
19.03.2018 20:09Иначе небезопасно — поведение становится неконсистентным. Оно зависит от того, проставлен ли у меня член-делегат в корректное значение, или нет. Тогда даже имея корректный not null объект MyCollection, я все равно не могу быть никогда уверен, что я могу вызывать на нем с гарантированно корректными аргументами методы интерфейсов, которые он должен реализовать.
Если бы авторы языка такое разрешили — вы бы от каждой новой библиотеки (или нового обновления старой) вздрагивали при попытке использовать. А используют ли там делегацию? А точно ли проставлен дочерний объект?
А так у вас есть гарантии — на not null объекте всегда можно корректно вызывать методы интерфейсов, которые он реализует, и дальнейшее поведение зависит только от реализации.
Хотите поменять вложенный объект, который определяет поведение? Вы всегда можете сделать вместо:
class Derived(b: Base) : Base by b
вот так:
class Derived(var b: Base) : Base by b
и потом где-то в коде:
derived.b = otherB
и компилятор также проверит, что otherB — not null, и поведение останется консистентным.
Если вы хотите это обойти и действительно понимаете, что делаете. Вы всегда можете сделать очень грязный и плохой хак:
derived = Derived(null!!) // не делайте так
или даже так:
class Derived(var b: Base = null!!): Base by b // не делайте так тем более!
artemshitov
19.03.2018 22:37(хотя, кстати, я неправ — Kotlin даже не позволит сделать такие хаки, тогда для таких случаев только делать подобный код на Java без синтаксического сахара)
ookami_kb
20.03.2018 13:14+1Хотите поменять вложенный объект, который определяет поведение? Вы всегда можете сделать вместо:
class Derived(b: Base): Base by b
вот так:
class Derived(var b: Base): Base by b
и потом где-то в коде:
derived.b = otherBКак бы не тут-то было. Написать-то вы так можете, и даже компилятор не ругнется, но работать оно не будет:
The by-clause in the supertype list for Derived indicates that b will be stored internally in objects of Derived and the compiler will generate all the methods of Base that forward to b.
Т.е. оно внутри где-то сохранится, и делегировать будет все время одному и тому же объекту. Вот, например, обратите внимание, что в обоих случаях выводится "Hello from A"
Veikedo
19.03.2018 12:31Не то, чтобы это прям решение для всех, но в решарпере есть возможность делегировать реализацию в один клик.
Athari
19.03.2018 12:45+1Да, но несуществующий код лучше сгенерированного.
PsyHaSTe
19.03.2018 22:32-2Если бы это давало существенную экономию, я бы согласился. Врапперы все же не такой часто нужный функционал, у меня на солюшен хорошо если десяток таких, причем они пишутся один раз (или генерируются) и больше не трогаются.
А вот вещи вроде асинхронных конструкторов, которые нужны то и дело, действительно упростили бы все.
То есть это хорошо, конечно, но есть более ценные фичи.Athari
19.03.2018 22:45Это у вас "врапперов" мало. Однако есть сторонники идеологии, что должны быть только интерфейсы и их реализации, наследования быть не должно, только композиция. Не могу сказать, что это плохая идея, но без подобного сахара ей следовать проблематично, и сгенерированный код мало помогает.
PsyHaSTe
20.03.2018 11:42В таком случае да, было бы полезно.
Но в реальности я такого кода встречал мало. Все же часто используется отношение is, композиция тоже используется, но тогда вызывающий код просто игнорирует закон Деметры и стучится к нужным свойствам напрямую. По крайней мере кодовая база десятков проектов, что я видел, устроена именно так. Писать однострочные прокси обычно никто не хочет.Athari
20.03.2018 12:02Но в реальности я такого кода встречал мало.
Потому что все горазды рассуждать про преимущества композиции над наследованием, про разработку через тестирование, про закон Деметры и прочее, а когда надо делать работу, а не чесать языком, берут и пишут как все простые смертные.
Athari
19.03.2018 12:14Нашёл: C# Feature Request: Expression bodied member syntax for interface implemented by field.
Вот когда эту фичу реализуют, я буду готов рассматривать вариант плоских иерархий и композиции вместо наследования.
Flammar
19.03.2018 18:30К сожалению, агрегации «из коробки», в отличие от наследования, в языках программирования, по крайней мере распространённых, нет.
Varim
19.03.2018 18:35Объясните о чем вы, Field в объекте это не агрегация / композиция?
Flammar
19.03.2018 19:10-1Нужно специальное ключевое слово для поля, которое обозначало бы, что оно и есть делегат.
zagayevskiy
20.03.2018 15:55+1Kotlin, делегирование реализации интерфейсов, делегирование пропертей. Слово — "by".
Steamus
19.03.2018 04:13+1Мдя. Надо ставить тег «Юмор». Бивис и Баттхет обсуждают ООП. И не понимая что слово интерфейс означает в обычной жизни.
Myxach
19.03.2018 04:13+1"— Правда? А что такое интерфейс? Это то же самое что и класс?
— Ну… Не совсем!
— В каком плане?
— Не один из его методов не должен иметь реализации."
В Плане Java и C# — не верно, а в общем плане
— интерфейсpublic abstract class MyInterface { public abstract void f(); }
saltukkos
19.03.2018 06:31— Правда? А что такое интерфейс? Это то же самое что и класс?
— Ну… Не совсем!
— В каком плане?
— Не один из его методов не должен иметь реализации.
Очень уж мне не нравится это определение интерфейса. Лично для себя сформулировал, что интерфейс — это описание контракта, и это не класс со всеми методами без реализаций. Это разные сущности, в том-же C# можно явно реализовать интерфейс, попробуйте сделать то же самое с абстрактным классом.Free_ze
19.03.2018 12:32Чем абстрактный класс — не описание контракта?
zodchiy
19.03.2018 12:50В чем преимущество (кроме наследования) абстрактного класса без реализации перед интерфейсом?
Free_ze
19.03.2018 12:59В возможности добавить реализацию и состояние не ломая иерархию, очевидно.
Интерфейс — это синтетическая сущность, которая переваливает проблемы ромба с «головы» компилятора на программиста.mayorovp
19.03.2018 13:43Но если добавить реализацию и состояние — он перестанет быть описанием контракта…
DistortNeo
19.03.2018 12:50Да, абстрактный класс тоже можно использовать как контракт, но возможности интерфейсов C# шире — вы можете навесить интерфейс в классе-потомке.
class CBase { public void Foo() { ... } } interface IFoo { void Foo(); } class CDerived: CBase, IFoo {}
Попробуйте, не меняя класса CBase, сделать то же самое, но с помощью классовFree_ze
19.03.2018 13:06В C# нет множественного наследования ведь) Если бы было, то ничто не мешало бы запилить подобное и для абстрактного метода
Foo
.DistortNeo
19.03.2018 13:19Да не вопрос. Представьте, что оно есть. Напишите подобное на C++.
Ограничение: метод базового класса не является виртуальным и вообще мы не можем в него лезть.Free_ze
19.03.2018 13:27Напишите подобное на C++
C++ — плохой пример, там куча проблем с дизайном. Я могу на C#-подобном псевдокоде написать, хотите?
Представьте, что оно есть.
Не вопрос)class CBase { public void Foo() { ... } } abstract class BaseFoo { abstract void Foo(); } class CDerived: CBase, BaseFoo {}
DistortNeo
19.03.2018 14:27Такое возможно только в одном случае: если метод BaseFoo.Foo() перекрывает CBase.Foo() и имеет автоматическую неизменяемую реализацию:
class CDerived: CBase, BaseFoo { // Compiler generated public sealed override void Foo() { CBase.Foo(); } }
Free_ze
19.03.2018 15:40Похоже на то. Это уже технические детали реализации компилятора/CLR, неизменяемость зависит от виртуальности
CBase.Foo
.DistortNeo
19.03.2018 15:56В этом случае получается слишком много неявного: поведение метода зависит от того, имелся ли метод в базовом классе, был ли он виртуальным.
Гораздо проще раздить интерфейсы и абстрактные классы. Интерфейсы требуют объявление методов в текущем классе или любом из родителей, тогда как абстрактные классы объявляют метод здесь и сейчас.Free_ze
19.03.2018 16:06Не больше неявности, чем с интерфейсами на самом деле) Эта фича меня не восторгает и о ней я узнал впервые от вас в прошлой аналогичной дискуссии.
Гораздо проще раздить интерфейсы и абстрактные классы.
Выделить чистые контракты в API — вполне съедобная идея, но только не ценой множественного наследования, ИМХО.
VolCh
19.03.2018 13:06Тем, что в него можно добавить реализацию, состояние и вообще почти всё, что угодно :)
saltukkos
19.03.2018 14:12Не знаю, насколько точную я аналогию смогу привести, но интерфейс — это принципиальная схема какого-то устройства, а абстрактный класс — это, если хотите, печатная плата, в которую надо впаять нужные детали.
Если говорить в терминах С++, то эта «плата» — таблица виртуальных функций. Если объект унаследован от N абстрактных классов, то, в зависимости от того, под каким из N типов вы на него смотрите, this сможет принимать до N+1 значения. В случае с интерфейсами — это один объект, который реализует N интерфейсов и this там одинаковый. Учитывая то, что мы захотим виртуальный деструктор — вот вам и пачка смешений.
Например, если посмотреть на __declspec(interface), то он подразумевает под собой novtable, из-за чего мы не можем удалить объект по ссылке на интерфейс.Free_ze
19.03.2018 14:56интерфейс — это принципиальная схема какого-то устройства, а абстрактный класс — это, если хотите, печатная плата, в которую надо впаять нужные детали
Это справедливо лишь в случае, если абстрактный класс содержит состояние. Тогда интерфейс, как синтаксическая конструкция, избыточен.
Если говорить в терминах С++
Да, в C++ плохая реализация множественного наследования. Другие языки вводят специальные «правила разруливания», позволяющие использовать одного родителя, а не таскать за собой топу зачастую идентичных состояний.saltukkos
19.03.2018 15:15Да, я с вами согласен, что в случае, когда у вас есть класс, в котором все методы чисто виртуальные — эта синтаксическая конструкция эквивалентна интерфейсу.
Но есть одно «но». Никто вам не гарантирует, что завтра какой-нибудь абстрактный програмист не решит добавить в этот базовый абстрактный класс маленький флажочек/кэш, и тогда код скомпилируется, но если где-то упаси боже (увидел, что вы C#/C++ разработчик), при маршалинге из нативных плюсов в Managed-мир вы решите кастануть указатель к IntPtr, а потом где-то обратно передать в нативный мир, кастанув к указателю на базовый класс, то this поедет и код будет мазать по памяти (понятно, что это легко решается типизацией IntPtr до какого-то своего типа в managed-мире, но всё же).
Пример, конечно, надуманный, но я хочу донести то, что ЯП предоставляет гарантии и уменьшает количество способов стрельнуть в ногу. И понятие интерфейса в том же самом С++ дало бы гарантию того, что в этой сущности не может быть состояния.Free_ze
19.03.2018 15:32Никто вам не гарантирует, что завтра какой-нибудь абстрактный программист не решит
Я правильно понимаю, что человек лезет изменять контракты и не знает, как они работают?
ЯП предоставляет гарантии и уменьшает количество способов стрельнуть в ногу
Не бесплатно. Сужая возможности язык вынуждает писать больше кода. Больше кода — больше багов, как известно.saltukkos
19.03.2018 15:53Я правильно понимаю, что человек лезет изменять контракты и не знает, как они работают?
Нет, речь не про это, эти изменения можно осуществить без изменения контракта.
Сужая возможности язык вынуждает писать больше кода
Мы же вроде бы с вами говорим о случае, когда вы хотите именно интерфейс, но вместо этого используете абстрактный класс со всеми чисто виртуальными методами? Как гарантия того, что интерфейс останется интерфейсом вынудит писать вас больше кода?Free_ze
19.03.2018 16:00Нет, речь не про это, эти изменения можно осуществить без изменения контракта.
Согласен. Но для этого отдельное понятие «интерфейс» не необходимо.
Мы говорим о плохом сценарии — попытке заменить интерфейсами множественное наследование. Там где контракт и должен быть контрактом — лишь одна претензия за избыточность конструкции. Просто как гарантия — это не самый лучший аргумент, кмк, ведь есть тесты.
Боль в том, что мы сейчас занимаемся противопоставлением, в то время как обе возможности могли бы соседствовать.saltukkos
19.03.2018 16:13Именно так, тоже хотел об этом написать, можно добавить ключевое слово interface хоть в С++, оставив множественное наследование (если бы не проблема с виртуальными деструкторами).
Проблема с виртуальным деструктором тоже решаема, но это тема для отдельной дискуссии, уходящей в итоге в наличие «object» в языке.0xd34df00d
19.03.2018 20:17Если всё, что вам нужно от интерфейсов — отсутствие состояния и реализаций функций, то это можно будет сделать с метаклассами.
areht
19.03.2018 20:14> Больше кода — больше багов, как известно.
Источник знания покажете?Szer
19.03.2018 20:23Ну, ГОСТ 27.002—89 и https://ru.wikipedia.org/wiki/Вероятность_безотказной_работы
Вероятность безотказной работы группы объектов равна произведению вероятностей безотказной работы каждого объекта в этой группе.
Чем больше объектов в группе, тем ниже надежность всей группыДля кода тоже работает.
0xd34df00d
19.03.2018 20:25Компьютеры и всякую гидравлику на самолётах и космических аппаратах зря резервируют?
Szer
19.03.2018 20:33-1Так это разное. Блоки кода — последовательно соединенные элементы, каждый со своей вероятностью отказа.
P — вероятность отказа системы. p — вероятность отказа узла системы. n — кол-во узлов
Последовательное соединение узлов: P=p^n
Параллельное: P=1-(1-p)^n
Для p=0,99 n = 5,
P последовательного ~= 0.95
P параллельного = 0,9999999999
Последовательное соединение теряет надёжность. Параллельное — приобретает.
http://lib.alnam.ru/book_rdm.php?id=204
Когда вы свой продукт деплоите в 4 инстанса — вы параллельно их соединяете. Повышаете надёжность.
Когда вы пишете код — вы последовательно соединяете блоки — с каждой новой строчкой надёжность падает, даже если вероятность отказа каждой конкретной строчки (бредово, но допустим) равна 0.999999999999999mayorovp
19.03.2018 20:37Почему вы считаете, что у любых строк кода одинаковая вероятность отказа?
Szer
19.03.2018 20:38Почему вы считаете, что у любых строк кода одинаковая вероятность отказа?
Потому что это всего лишь пример. Я ещё считаю что строчек кода всего 5, это не смутило?
По ссылке в книге полная формула приведена для общего случая.
mayorovp
19.03.2018 20:42Меня смутило другое. Правило "Чем больше объектов в группе, тем ниже надежность всей группы" работает только при постоянной вероятности отказа объекта.
VolCh
19.03.2018 20:49Главное, что при ненулевой вероятности отказа.
mayorovp
19.03.2018 21:08Нет, совсем не главное. Сравните что меньше: 1 — 0,93 или 1 — 0,995?
Szer
19.03.2018 21:12Нет, совсем не главное. Сравните что меньше: 1 — 0,93 или 1 — 0,995?
Условия разные, в одном случае 5 в 10 раз более надёжных элементов, а в другом 3 очень ненадёжных.
Очевидно же, что говорить о снижении надёжности системы с увеличением кол-ва элементов в ней можно только при прочих равных условиях, а именно что элементы те же самые.
Аналогия: Как сравнить надёжность 3х строк на C# и 100 строках на asm?
Я думаю надо сравнивать 3 строки на C# и 100 строк на C#
mayorovp
19.03.2018 21:14А почему вы думаете что элементы те же самые? Напомню, изначально как раз и шла речь о разных объемах кода на разных языках:
Сужая возможности язык вынуждает писать больше кода. Больше кода — больше багов, как известно.
Szer
19.03.2018 21:17А почему вы думаете что элементы те же самые? Напомню, изначально как раз и шла речь о разных объемах кода на разных языках
Согласен, эту цитату я упустил.
Что не отменяет того что принцип работает. Применение его к очень маленьким элементам (вроде одной строки кода) под вопросом.
Szer
19.03.2018 20:55Правило "Чем больше объектов в группе, тем ниже надежность всей группы" работает только при постоянной вероятности отказа объекта.
Я думаю в этом случае только подсчёт вероятности отказа сложнее — через цепи Маркова (редирект на пдф). Придётся мучаться либо с интегралами по времени, либо измерять вероятность отказа в определённом состоянии (с учётом того как объект попал в это состояние).
Интуитивно должно работать так же: сложнее система (больше последовательных элементов) — надёжность меньше. Дублирование системы (больше параллельных элементов) — повышает надёжность.
mayorovp
19.03.2018 21:11Не должно. Ошибки проектирования могут вовсе не давать никакой вероятности отказа (если только не брать вероятность в пространстве всех возможных программ) — но несколько элементов вместе дадут 100% отказ.
Классическая ситуация — нарушение LSP при наследовании квадрата от прямоугольника.Athari
19.03.2018 22:57+1Насчёт LSP, квадрата и прямоугольника есть иные мнения: одно, второе. Первое — верно с любой точки зрения. Второе — спорное, но я придерживаюсь его. Интерфейс IRectangle никак не запрещает свойствам меняться по желанию левой пятки, в том числе при изменении другого свойства. Какую конкретно логику и контракты вы вкладываете в этот интерфейс — большинством языков программирования не описывается.
Скажем, я опишу IWindow с IsMinimzed, присвою значение, потом вдруг окажется, что там не то значение, которое я присвоил, а в BoundingRect вообще непонятно что. Это нарушение контракта? Нет, это юзер нажал кнопочку.
0xd34df00d
19.03.2018 20:52Я это всё к тому, что процитированная вами часть не может являться доказательством ввиду очевидного приведённого выше контрпримера. Надо наворачивать поверх неё определение группы объектов, что-то говорить о зависимости и независимости, и это всё очень нетривиально для строк кода.
Szer
19.03.2018 21:00-1И это всё очень нетривиально для строк кода.
Инженерия делает проще.
Берём тысячу плат и тестируем час. Отказало 2 из 1000? Эмпирическая вероятность отказа одной платы (как целого объекта) в течении часа — 0.2% (а надёжность — 99.8%)
Повторяем с прочими элементами, считаем суммарную надёжность.
С программными продуктами можно сделать похоже.
Выкатываем web application, нагружаем час, считаем кол-во out of service, service deny, timeout, 502 и пр. на общее кол-во запросов.
Абстрактно всё то же самое, программирование та же инженерия.
areht
19.03.2018 21:28Область и условия применения: Настоящий стандарт устанавливает основные понятия, термины и определения понятий в области надежности.
Настоящий стандарт распространяется на технические объекты
Ну если вы считаете, что это про код…
Даже не знаю, то ли у вас тогда спросить «как вы тройное резервирование в исходном коде делать предлагаете?», то ли предложить попробовать глючность программы по количеству опкодов в .exe считать.Szer
19.03.2018 21:32Даже не знаю, то ли у вас тогда спросить «как вы тройное резервирование в исходном коде делать предлагаете?», то ли предложить попробовать глючность программы по количеству опкодов в .exe считать.
Ну если в маразм впадать, то да, можно и опкоды считать.
А вообще люди придумали кластеры, рейд массивы, зоопарки всякие, облака с динамическим масштабированием — это всё примеры параллелизации вашего кода с целью повышения надёжности.
Ну, и банальный
retry 5 (fun () -> saveOnDisk ())
в коде, где
saveOnDisk
может завалиться с ненулевой вероятностью.mayorovp
19.03.2018 21:33Вот-вот, банальный retry 5 — и уже выбивается из общего «закона».
Szer
19.03.2018 21:41Вот-вот, банальный retry 5 — и уже выбивается из общего «закона».
Это просто незнание "законов". А в терминах ТАУ — этот блок всего лишь система с отрицательной обратной связью.
Added: я не призываю считать надёжность каждой строчки.
areht
19.03.2018 22:39> Ну если в маразм впадать, то да, можно и опкоды считать.
Ваша теория или работает, или не работает. Если вы в ГОСТе ограничений не покажете — значит должно быть можно по опкодам считать.
> retry 5 (fun () -> saveOnDisk ())
О да, такое по надежности явно превосходит одиночный вызов как раз в (1-(1-p)^n)/p раз. Особенно, когда дескриптор закрыт строкой выше.
И не надо подменять надежность и параллелизацию кода на установку дополнительных железок.Szer
19.03.2018 23:16Ваша теория или работает, или не работает. Если вы в ГОСТе ограничений не покажете — значит должно быть можно по опкодам считать.
Конечно можно, кто ж мешает. Если заняться нечем в ближайшие пару лет.
О да, такое по надежности явно превосходит одиночный вызов как раз в (1-(1-p)^n)/p раз. Особенно, когда дескриптор закрыт строкой выше.
Если вы не знаете зачем применяют ретраи в IO, ничем не могу помочь. Не от закрытого дескриптора строкой выше, нет.
И не надо подменять надежность и параллелизацию кода на установку дополнительных железок.
Почему же? Дополнительные фейловер инстансты моей системы — это как раз оно самое, что не так? Одна упадёт, другая подхватит.
areht
19.03.2018 23:42> Если заняться нечем в ближайшие пару лет.
Так вы вручную считать хотите?
> Не от закрытого дескриптора строкой выше, нет.
То есть закрытый дескриптор под вашу формулу пересчёта кода в баги не подходит? Ок, вопросов больше не имею.
> Дополнительные фейловер инстансты моей системы — это как раз оно самое, что не так?
Всё хорошо, кроме того, что вы приравняли «код» и «систему».Szer
19.03.2018 23:52Так вы вручную считать хотите?
Я вообще не хочу считать.
То есть закрытый дескриптор под вашу формулу пересчёта кода в баги не подходит?
Передёргивание. Я такую "формулу" не приводил. Но приводил ту, которая может посчитать надёжность системы из надёжностей её элементов.
И она не моя, вы мне льстите.
Всё хорошо, кроме того, что вы приравняли «код» и «систему».
Ну пожалуйста, объясните чего ж такого магического в коде, что его нельзя рассматривать как систему объектов, друг с другом взаимодействующих.
areht
20.03.2018 02:10> Но приводил ту, которая может посчитать надёжность системы из надёжностей её элементов.
Ну, главное разобрались, что к моему вопросу о происхождении «Больше кода — больше багов» ваш ответ отношения никакого не имеет.
> Ну пожалуйста, объясните чего ж такого магического в коде, что его нельзя рассматривать как систему объектов, друг с другом взаимодействующих.
У меня ощущение, что у вас есть собственное понимание «кода», «системы», «объектов», «баг» и «взаимодействующих». Или вы троллите.
Давайте так: научный подход — это не «объясните чего ж такого магического», а подтверждение гипотеза экспериментом. А расчёты по результатам вашего рассмотрения в отношении кода не сойдутся с реальностью. Вот покажете эксперимент — будет повод для разговора.
0xd34df00d
19.03.2018 20:16Другие языки вводят специальные «правила разруливания», позволяющие использовать одного родителя, а не таскать за собой топу зачастую идентичных состояний.
Так и в плюсах есть virtual-наследование. Другое дело, что про него должны знать все потенциальные участники потенциального ромба, это недостаток, да.
DistortNeo
19.03.2018 15:51На самом деле, это implementation specific.
Есть такая штука — Borland C++ Builder, так вот он абстрактные классы, не имеющие полей, реализует именно как интерфейсы, без заведения дополнительного поля vtable. И при этом никаких __declspec указывать не надо — он сам определяет по семантике, является ли сущность полноценным классом или легковесным интерфейсом.
А вообще, такое поведение связано с бинарной совместимостью с Delphi, как раз таки имеющего интерфейсы, и желанием Borland остаться в рамках стандарта C++.saltukkos
19.03.2018 15:59так вот он абстрактные классы, не имеющие полей, реализует именно как интерфейсы
Отлично! Это то, что и нужно, чтобы называться интерфейсом. А теперь давайте в идельном мире закрепим это явно на уровне языка, чтобы это происходило всегда по спецификации, а когда не происходило — не компилировалось. И вот мы получаем ключевое слово interface.DistortNeo
19.03.2018 16:33Ну вот так и сделали: создали ключевое слово «interface», а потом решили, что множественное наследование не нужно и выпилили его.
Athari
19.03.2018 17:49Тут надо учточнить: выпиливание множественного наследования и поддержка интерфейсов возникают при смешении с дельфовой системой типов. Если не смешивать, то правила остаются плюсовыми. Если смешивать, становятся непонятно-гибридными.
Flammar
19.03.2018 19:59Тем, что он может быть чем-то фатально бОльшим, что исключит его использование в качестве описания контракта: тем, что в него можно добавить состояние, тем более изменяемое. Интерфейс хорош тем, что он это отсекает по определению.
iit
19.03.2018 07:53Я php-шник и могу делать еще большее зло выводя общий код в трейты, описывать под них интерфейсы и вообще отказываться от базовых классов, тупо подмешивая функционал в нужные объекты.
php код не для слабонервныхinterface ObservableInterface { public function addObserve(ObserverInterface $observer); public function notify(); } trait Observable { protected $observers = []; public function addObserve(ObserverInterface $observer) $this->observers[] = $observer; } public function notify(){ array_map(function($observer){ $observer->update(); }, $this->observers) } } class MySomeWidget implements ObservableInterface, WidgetInterface { use Observable, WidgetUx, WidgetSupport, WidgetConfigs; } class MyParser implements ObservableInterface { use Observable; } class MyCollection extends Illuminatie\Support\Collection implements ObservableInterface{ use Observable; }
GraneDirval
19.03.2018 09:58+1Мэтт Зандстра с вами бы не согласился. В его книге по объектам и паттернам как раз такой пример идёт как норма. Собственно, это действительно нормально, естественно при условии использования по назначению :)
VolCh
19.03.2018 10:22Лучше бы от наследования отказались в Collection, а не в Widget. :)
iit
19.03.2018 11:27Если бы я описывал свою коллекцию со всей этой фигней то да.
Но так как я использую один из бесполезных и ненужных и тормознутых фреймворков то мне проще тупо использовать его коллекции а не писать собственные велосипеды.
Хотя в данном случае, если мне хочется чтобы любое изменение в коллекции генерировало событие update мне конечно придется переопределить некоторые методы.
SergeyVin
19.03.2018 08:42А теперь добавим в эту солянку default-методы в интерфейсах в Java 8. Вот уж костыль из костылей...
Dair_Targ
19.03.2018 13:41interfaces + defaults как-раз таки делают из всей этой «ООП»-каши что-то вразумительное:
— появляется возможность множественного наследования с проверками отсутствия неочевидного кода во время компиляции (unrelated defaults)
— кодогенераторы вроде immutables.github.io позволяют очень сильно очистить кодовую базу от всякого шлака вроде equals и toString, в результате чего остаётся практически чистая бизнес-логика
— накнец-то можно замокать любой(!) оъект, что существенно облегчает написание автотестов
По-моему, лучше чем interfaces + defaults было бы только иметь возможность добавлять реализацию классом произвольного интерфейса без изменения самого класса.
lany
19.03.2018 09:34Самое интересное, что в
MyObservableWidget
вы должны будете переопределить все методыMyWidget
, изменяющие состояние, чтобы вызвать в нихnotify()
. При этом вы попадаете на жёсткую зависимость от реализацииMyWidget
. При добавлении нового метода вMyWidget
, который изменяет состояние, вам придётся изменятьMyObservableWidget
, иначе ничего вас не спасёт от багов. А если метод вродеaddAll
вместо ручной реализации в новой версии начнёт в цикле вызыватьadd
, вы пришлёте миллион эвентов вместо одного (либо обратное произойдёт, тогда вы в новой версии вообще не пришлёте эвент). В этой ситуации невозможность множественного наследования и необходимость вручную делегировать пару методов к какому-нибудь подобиюjavax.swing.event.EventListenerList
— это наименьшая из ваших проблем. Я считаю, если вы не используете аспекты или что-то аналогичное (а я совсем не призываю их использовать), то вы не сможете сделать изменяемую структуру данных отдельно от нотификаций, а потом прикрутить нотификации в дочернем классе. Вообще наследование конкретных классов плохо пахнет. Если вам при этом приходится переопределить реализацию N методов (например, "все методы, которые изменяют состояние"), вы точно ищете себе проблемы. На это напоролся, например, EclipseLink, который расширилArrayList
, переопределив все методы, а в Java 8 — сюрприз — появились новые методы.areht
19.03.2018 10:04> При добавлении нового метода в MyWidget, который изменяет состояние, вам придётся изменять MyObservableWidget, иначе ничего вас не спасёт от багов.
При добавлении нового метода в MyWidget, который изменяет состояние, вам придётся не забыть добавить куда-то нотификацию, да.
Если у вас нет привычки смотреть на происходящее в наследниках при правке базового класса — вас ничего не спасёт от багов (к сожалению).lany
19.03.2018 10:52Не все клиенты вашего класса могут быть вам доступны. А если доступны все и всегда (у вас не библиотека, а приложение, которое не подразумевает сторонних плагинов), то необходимость выделения виджета без нотификаций вызывает ещё больше вопросов. Ключевой принцип ООП — инкапсуляция. В том числе она означает, что пока существующий класс сохраняет свой контракт, приложение не должно ломаться. Добавление нового метода или изменение реализации addAll через add не изменяет существующий контракт, но ломает приложение.
areht
19.03.2018 13:38> Добавление нового метода или изменение реализации addAll через add не изменяет существующий контракт, но ломает приложение.
Я против такого определения «контракта». Если у вас приложение сломалось — кто-то нарушил контракт. Или это не контракт, а undefined behaviour.lany
19.03.2018 13:41-1Нарушил контракт наследник: переопределил метод, делая помимо добавления элемента дополнительную логику, не предусмотренную контрактом (оповещение слушателей).
mayorovp
19.03.2018 14:02Нет, тут скорее базовый класс нарушил свой контракт для наследников.
lany
19.03.2018 14:12Если в контракте написано, что "метод
add
добавляет элемент, а методaddAll
добавляет все элементы", то базовый класс волен поменять реализацию и использовать, либо не использоватьadd
внутриaddAll
, этим он контракт не нарушает.mayorovp
19.03.2018 14:18Этим он не нарушает публичный контракт. А вот контракт для расширения он именно что нарушает…
areht
19.03.2018 15:42Ну нет, наследник — новый класс, он имеет право на свою логику, тем более, что это не нарушает даже LSP.
Вот полагаться, что addAll в родителе дергает/не дергает add, если это отдельно не прописано — не стоит.
iit
19.03.2018 11:32Не знаю как в java но в php можно тупо сделать wrapper который будет прозрачно проксировать вызов любого метода да и еще и notify вызывать при этом.
Scf
19.03.2018 09:48— Да, но этот класс их не имеет. Тогда почему его нельзя назвать интерфейсом?
— Абстрактный класс может иметь нестатические поля, а интерфейс не может.
Дальше не читал. Основное отличие интерфейса — поддержка "множественного наследования".
Scf
19.03.2018 22:19Ого. Это за "дальше не читал" или за основное отличие интерфейса? Если второе, то хотелось бы критики :-)
ookami_kb
20.03.2018 16:55+2За остальных говорить не буду, но лично от меня – за пафосно высказанную глупость. Некоторые языки запрещают множественное наследование классов, но разрешают множественное наследование интерфейсов. К самой сущности интерфейса это относится чуть менее, чем никак, и уж претендовать на основное отличие явно не может.
Scf
21.03.2018 08:52-1Можно пример языков, где есть и множественное наследование, и интерфейсы? Я знаю только два языка с множественным наследованием — C++ и Scala. И там, и там интерфейсов нет.
ookami_kb
21.03.2018 12:20+3Эм… ну Вы не путайте причину и следствие. Это не интерфейс нужен для поддержки множественного наследования, это некоторые языки, которые запрещают множественное наследование классов, разрешают множественное наследование интерфейсов, потому что оно не имеет побочных эффектов. Соответственно, в таких языках как Java вводится ключевое слово interface, чтобы синтаксически различать эти сущности. В таких же языках как python в этом ключевом слове нет необходимости, поскольку разница чисто семантическая.
В общем же случае интерфейсы – это вообще не про наследования. Это отдельная семантическая сущность, контракт, если хотите, которому должен следовать класс. Поэтому, на мой взгляд, в Java как раз правильно "наследование" интерфейсов идет через
implements
– класс реализует интерфейс, а не наследует его. А наследование классов – это вообще проis-a
отношение. Так что использовать интерфейсы, чтобы обойти ограничение во множественном наследовании, это не очень правильно.
С моей же точки зрения, реализация по умолчанию, определенная непосредственно в интерфейсе – это некий компромисс в угоду краткости кода. Строго говоря, это еще одна, отдельная, сущность "Интерфейс с частичной реализацией", но вводить для этого отдельное ключевое слово (насколько я помню, сначала в котлине так и было, какое-то время существовал
trait
) может быть уже перебором. Но семантически это именно "интерфейс" + "его реализация по умолчанию" в одном флаконе.
dimka_sokol
19.03.2018 09:58— Не один из его методов не должен иметь реализации.
Если я не ошибаюсь, в C# 8.0 интерфейсы смогут определять реализацию по умолчанию, так что это заявление скоро будет неактуальным.Myxach
19.03.2018 10:25И поэтому, и JAVA 8, и C# 8, ломают интерфейсы.
Интерфейсы в понятие опп, никогда не имели и не будут иметь реализациюAthari
19.03.2018 10:57Банальная победа прагматизма над идеализмом. Идеалы работают, если есть возможность в любой момент переписать весь код. В остальных случаях приходится заботиться об обратной совместимости и чистоте кода потребителя.
lair
19.03.2018 12:40+3Интерфейсы в понятие опп, никогда не имели и не будут иметь реализацию
Осталось найти это "понятие ООП" и выяснить, почему оно — правильное, а любое другое, в котором интерфейсов нет вообще или они могут иметь реализацию, — неправильное.
Myxach
19.03.2018 15:22Интерфейсы это спецификация только, я про это. Интерфейсов может не быть, ну если интерфейс изначально что-то реализовывает, то это уже не интерфейс, как-бы вы его не называли. В C++ же, изначального ключевого слово интерфейс не было, ну понятие было и обозначало оно именно это. А Дефолтные методы это костыль — был, есть и будет
lair
19.03.2018 15:34Интерфейсы это спецификация только, я про это.
А с чего вы решили, что это определение — правильное?
PsyHaSTe
20.03.2018 00:03+1Чем связка
interface IFoo { } public static class IFooExtensions { public static void Foo(this IFoo @this) {} }
лучше, чем просто написать
interface IFoo { public default void Foo() {} }
?
Не надо про архитектурную чистоту и независимость интерфейса в первом случае, на самом деле они точно так же связаны.Athari
20.03.2018 03:26Второй способ лучше, потому что default interface implementation полноценно участвует в полиморфизме. Первый способ — синтаксический сахар над неполиморфным вызовом статического метода.
PsyHaSTe
20.03.2018 11:45Хм, почитал RFC, действительно кривовато выглядит. Беру свои слова назад, я считал, что это просто сахар для такой записи.
areht
20.03.2018 12:39> на самом деле они точно так же связаны.
Ну а вы, например, вынесите IFooExtensions в отдельную сборку — развяжутся?
AntonLozovskiy
19.03.2018 09:58Проблема не в том, что не смогли реализовать в C# наследование от множества классов… Язык пытались сделать таким, чтобы в нем было меньше внутренних проблем, которые потом сложно выявить. как и по умолчанию убрали поддержку работы с указателями в том виде, как она есть в C++. С одной стороны это хорошо, с другой плохо, но сидеть ныть о том, что это хреновый вариант — глупо… не нравится пиши на другом языке — благо выбор сейчас богатый
gnkoshelev
19.03.2018 10:12+2Класс подразумевает наличие состояния (state).
Интерфейс — нет.
Да, в Java 8 добавилиdefault
-методы и статические методы, а в Java 9 — приватныеdefault
-методы и приватные статические методы, но с состоянием это не имеет ничего общего.
Если не ошибаюсь, в C# вошло в моду называть интерфейсы с префиксомI
, т.к. этот костыль помогает глядя в код понять, что после:
находится класс или интерфейс. А использование implements / extends делает код лучше для восприятия (субъективно).PsyHaSTe
20.03.2018 00:07Если не ошибаюсь, в C# вошло в моду называть интерфейсы с префиксом I
Этому правилу уже лет 15, начиная с версий 1.Х и он описан в древнейших официальных гайдлайнах. Не очень похоже на «моду».
т.к. этот костыль помогает глядя в код понять, что после: находится класс или интерфейс. А использование implements / extends делает код лучше для восприятия (субъективно).
Что делать с ипользованиями вне наследования? Например, когда это поля/параметры/…
Это не больший «костыль», чем писать название переменных с маленькой буквы, а имена классов — с большой.DistortNeo
20.03.2018 00:24Это не больший «костыль», чем писать название переменных с маленькой буквы, а имена классов — с большой.
А чем это правило плохо? Часто же бывает, когда и переменную, и имя класса хочется называть одинаково. Уж лучше различие в регистре первой буквы, чем всякие мерзкие префиксы-постфиксы.
То же самое и с интерфейсами, когда имя интерфейса и класса совпадает: List и IList, Dictionary и IDictionary и т.д.
Все эти правила именования — не более, чем способы разрешения коллизии имён, ставшие со временем гайдлайнами.
TheShock
20.03.2018 00:33Мне, кстати, ужасно не нравится традиция C# называть свойства с большой буквы. Читал документацию и все время думал, что они обращаются к статичному методу класса. Думал еще, что такую серьезную библиотеку так странно сделали. Оказалось, я ошибался:
Container.Bind<IFoo>().To<IBar>().FromResolve();
PsyHaSTe
20.03.2018 01:19Вопрос привычки. Вполне логичное правило, чтобы отличать свойства класса от локальных переменных. Тем более, что от этого ничего не страдает. В угловых скобках может быть только тип (соответственно IFoo и IBar это типы), а вне их — типом только если есть намекающий `new`. В остальных случаях это всегда свойство. Таким образом мы минимизируем количество коллизий, когда нам нужна доп. информация чтобы понять, что это перед нами.
TheShock
20.03.2018 02:05Какой привычки? Я уже два года на шарпах пишу, сколько еще ждать, пока привыкну?
Всмысле типом может быть намекающий new? Это вполне может быть статический метод класса
Зачем отличать от локальных переменных? Методы обычно очень короткие — всегда видно локальные переменные и так. Тем более свойства, которые не get-set пишутся с маленькой буквы, хотя разницы не должно быть никакой. Бред какой-то. Это такая мелочь, но, пожалуй, бесит меня больше всего в C#PsyHaSTe
20.03.2018 02:18Какой привычки? Я уже два года на шарпах пишу, сколько еще ждать, пока привыкну?
Всмысле типом может быть намекающий new? Это вполне может быть статический метод класса
В угловых скобках? Не может.
Самое простое — покажите код, где может быть как тип, так и не тип, и из-за стиля именования непонятно, что где.
Зачем отличать от локальных переменных? Методы обычно очень короткие — всегда видно локальные переменные и так. Тем более свойства, которые не get-set пишутся с маленькой буквы, хотя разницы не должно быть никакой. Бред какой-то. Это такая мелочь, но, пожалуй, бесит меня больше всего в C#
Свойства всегда пишутся с большой буквы, не знаю, откуда эта информация про «не гет сет». Решарпер на дефолтных настройках как раз следует стандартному гайдлайну. Ну, разве что я предпочитаю еще у приватнах переменных префикс "_" писать. Но тут уже вопрос привычки.
Методы обычно очень короткие — всегда видно локальные переменные и так.
Когда короткие, а когда нет. Я не замерял, но в сложных продуктах средняя длина метода 50-100 строк. Типичный такой пример.
Ну и да, тут нет варианта писать или не писать — как-то писать надо. Текущие правила, как я уже сказал, уменьшают количество коллизий.TheShock
20.03.2018 03:01Свойства всегда пишутся с большой буквы, не знаю, откуда эта информация про «не гет сет».
В вашем же примере куча свойств с маленькой буквы:
private int[] buckets; private Entry[] entries; private int count; private int version; private int freeList; private int freeCount; private IEqualityComparer<TKey> comparer; private KeyCollection keys; private ValueCollection values;
Самое простое — покажите код, где может быть как тип, так и не тип, и из-за стиля именования непонятно, что где.
Я ведь выше давал уже пример, вот:
Это может быть как и статический методBind
классаContainer
, так и динамический метод свойства. И невозможно определить, что именно это.
Вот Hello World пример:
using Zenject; using UnityEngine; using System.Collections; public class TestInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<string>().FromInstance("Hello World!"); Container.Bind<Greeter>().AsSingle().NonLazy(); } } public class Greeter { public Greeter(string message) { Debug.Log(message); } }
Я искренне считал первое время, что этот пример означает следующее:
Zenject.Container.Bind<string>().FromInstance("Hello World!"); Zenject.Container.Bind<Greeter>().AsSingle().NonLazy();
И суть в том, что нельзя узнать, как именно оно есть на самом деле. А вот если бы все свойства были с маленькой буквы — было бы очевидно, потому что. Ну вот вам пример. Это корректный код на C#. Я в нем обращаюсь как к классу, так и к свойству. Так вот — какой вариант правильный:
1. ProjectContext — это класс, а LazyInstanceInjector — это свойство, или
2. LazyInstanceInjector — класс, а ProjectContext — свойство?
Вы можете ответить на этот вопрос без дополнительного контекста?
using Zenject; public class TestInstaller : MyClass { public IEnumerable<object> MyMethod () { return ProjectContext.HasInstance ? LazyInstanceInjector.Instances : null; } }
Название свойств с маленькой буквы полностью убрало бы этот гемор
Я не замерял, но в сложных продуктах средняя длина метода 50-100 строк
Не средняя, а максимальная. Вы ведь нашли метод побольше, а не средний. А вот количество файлов — сотни и в каждом сотни строк. Вот уж где место для коллизий.
Текущие правила, как я уже сказал, уменьшают количество коллизий.
С нуля вверх? Это невозможно. И если ваши слова бы имели логику, то как вы избегаете коллизий локальных переменных и приватных свойств?Athari
20.03.2018 03:38Это может быть как и статический метод Bind класса Container, так и динамический метод свойства.
Более того, "Container" может быть и свойством, и классом одновременно, и что вызывается — зависит и от инстансных членов класса свойства, и от статических членов класса. Это не баг, это фича.
Если хотите знать, что используете, то поставьте решарпер и включите расширенную подсветку.
Ну или на VB немного код попишите. Там вообще всё с большой буквы. Потом будете с радостью вспоминать C#, где хоть какое-то разнообразие.
И если ваши слова бы имели логику, то как вы избегаете коллизий локальных переменных и приватных свойств?
Приватные свойства и методы тоже пишутся с большой буквы. Что касается приватных полей, то им часто дают префикс "_", чтобы не заморачиваться с this.
TheShock
20.03.2018 03:48Это не баг, это фича.
В чем фича?
Решарпер у меня стоит. С IDE, конечно, значительно меньше проблем, но это не значит, что я не имею права считать такую идею плохой — она реально мешает пониманию кода, в чем я лично убедился совершенно недавно. Значительно лучше, если бы все публичные/защищенные поля и свойства были с маленькой буквы (какого они вообще разделяются, это нарушение инкапсуляции). Но я понимаю, что так, к сожалению, уже не будет. А жаль.Athari
20.03.2018 03:59Если бы были CClass, PProperty, MMethod, FField в дополнение к IInterface, было бы лучше? Не думаю. Если что-то непонятно, есть IDE. C R# вопрос про разновидность идентификатора не возникает никогда, потому что всё видно по цвету. Проблемы нет.
TheShock
20.03.2018 04:23было бы лучше?
Я же написал как было бы лучше. Зачем вы мне отвечаете, если не читаете мои сообщения? Вот что я писал:
Значительно лучше, если бы все публичные/защищенные поля и свойства были с маленькой буквы
Дальшевопрос про разновидность идентификатора не возникает никогда
Еще раз — у меня вопрос возник, когда я читал документацию с примерами. К ГитХабу решарпер не подключишь. Вы странный.Athari
20.03.2018 11:51Еще раз — у меня вопрос возник, когда я читал документацию с примерами.
Если у вас возник такой вопрос, то это проблема документации.
mayorovp
20.03.2018 06:16В вашем же примере куча свойств с маленькой буквы:
Это поля, а не свойства!
TheShock
20.03.2018 06:19Да, в термине я ошибся. Тем не менее, это не меняет сути аргумента. Поля пишут с маленькой буквы, как и локальные переменные, так что подобное именование не направлено на избежание коллизий полей/свойств с локальными переменными.
lair
20.03.2018 09:31Есть стратегия, где поля пишут с подчеркивания. Как раз для уменьшения таких коллизий, особенно в конструкторах.
iPilot
20.03.2018 18:32+1Свойства изначально были обертками над полями. И тут уже появлялись коллизии: хотелось, чтобы свойство и оборачиваемое поле именовались одинаково без всяких префиксных "_", поэтому поля продолжили именовать с маленькой буквы, а свойства вокруг них — с большой. Потом появились автосвойства, которым явно задание поля не требуется, а как именно назовет поле компилятор — не важно. Но привычки и много легаси кода сделали свое дело.
TheShock
20.03.2018 18:36Вот это мне, кстати, и не понятно. Ведь поля и свойства лучше называть одинаково. Когда ты обращаешься к
array.length
— тебя не должно интересовать — поле это или свойство, инкапсуляция ведь. А в результате разработчик библиотеки должен иметь легкую возможность подменить поле свойством.
Сейчас это решили тем, что поля вообще никогда в паблик не выходят, только свойства.areht
20.03.2018 19:32+1> тебя не должно интересовать — поле это или свойство, инкапсуляция ведь
Если в C# синтаксис работы с полями и свойствами похож — это ещё не инкапсуляция.
Ну и на практике это тоже не работает. Начиная с того, что с полями и свойствами через Reflection надо работать отдельно (а это куча тулинга).
PsyHaSTe
20.03.2018 20:33Все это прекрасно, я даже в принципе хотел бы, чтобы ПОЛЕЙ не было в языке вообще, но этого не будет.
Насчет «не должно быть разницы» — поля всегда приватные, а поля как правило публичные. Так что видеть разницу в видимости в названии переменной часто полезно. Ну и всякие мелочи вроде использования с `ref` и все такое прилагается.
Athari
21.03.2018 02:42Когда ты обращаешься к array.length — тебя не должно интересовать — поле это или свойство
А вас и не интересует. Публичные поля запрещены всеми кодостилями, поэтому при обращении к члену-значению имя всегда с большой буквы.
А в результате разработчик библиотеки должен иметь легкую возможность подменить поле свойством.
Не должен. Ломается бинарная и даже сорцовая совместимость.
PsyHaSTe
20.03.2018 20:31При чем тут легаси? У класса может быть приватное поле, которое не должно быть видно извне. Обычный сценарий.
PsyHaSTe
20.03.2018 12:10+1Выше уже ответили, что это поля, а не свойства. Разница существенная, но для разрешения я как раз и написал про "_".
Что касается вашего примера, то тут есть два способа разрешить его:
1. Подсветка в IDE подсвечивает тип аквамариновым, а поле оставляет белым. Решарпер тут не нужен, это дефолтная подсветка во всех связанных тулзах, даже менее мощных, чем студия
2. Если ВДРУГ подсветки нет, то можно использовать следующую логику: методы очевидно являются мутабельными, а изменять глобальную статическую переменную из инстансного метода это… запашок. Так что это свойство с таким именем, скорее всего protected в базовом классе.
Да, и благодаря префиксу I я сразу вижу тут в этом коде ошибку, потому что биндить интерфейс можно только к классу, но никак не к другому интерфейсу.
Не средняя, а максимальная. Вы ведь нашли метод побольше, а не средний. А вот количество файлов — сотни и в каждом сотни строк. Вот уж где место для коллизий.
Тут я нашел побольше, да. В продакшн коде, где не специлисты уровня майкрософт код пишут средний размер кода поболее будет.
С нуля вверх? Это невозможно. И если ваши слова бы имели логику, то как вы избегаете коллизий локальных переменных и приватных свойств?
Как это с нуля? Окей, пишем с маленькой, как теперь без контекста понять, локальная это переменная или свойство? «Ну метод маленький, там все видно» — не аргумент, ибо он не отменяет коллизии, а переводит стрелки в стиле «самдурак» на программиста.TheShock
20.03.2018 18:19Выше уже ответили, что это поля, а не свойства. Разница существенная, но для разрешения я как раз и написал про "_".
Вы их переменными назвали, не полями:
приватнах переменных префикс "_" писать
Да, и благодаря префиксу I я сразу вижу тут в этом коде ошибку, потому что биндить интерфейс можно только к классу, но никак не к другому интерфейсу.
Нету там ошибки
В продакшн коде, где не специлисты уровня майкрософт код пишут средний размер кода поболее будет.
Сомнительно. Я специалист далекий до МС, но методы у меня корокие.
«Ну метод маленький, там все видно» — не аргумент,
Почему не аргумент? Метод значительно меньше, чем все файлы, а значит значительно уменьшает влияние коллизий.PsyHaSTe
20.03.2018 20:27Сомнительно. Я специалист далекий до МС, но методы у меня корокие.
Значит, у вас достаточно простая область вроде написания WebAPI прокси для другого сервиса. При сложной логике, например при работе с компилятором, получается что-то в таком духе. Разбить его на кучу мелких методов можно, но читаемости это не добавит.
Нету там ошибки
Я про этот код:
Container.Bind<IFoo>().To<IBar>().FromResolve();
Судя по официальной доке, эта запись означает, что вместо интерфейса IFoo прокидывать инстанс IBar, инстанс IBar создать нельзя, значит ошибка.TheShock
20.03.2018 21:41Значит, у вас достаточно простая область вроде написания WebAPI прокси для другого сервиса
GameDev.
При сложной логике, например при работе с компилятором, получается что-то в таком духе
Вы берете какие-то крайности и ставите в качестве примеров. А раньше вы, вроде, про среднестатистический код говорили. И я посмотрел всю либу. Это единственный файл, в котором только два таких метода. И вы хотите мне это выдать за среднестатистический код? Несерьезно. Это исключение, а далеко не правило
Я про этот код:
Да, я понял. Там нету ошибки.PsyHaSTe
20.03.2018 21:59Вы берете какие-то крайности и ставите в качестве примеров. А раньше вы, вроде, про среднестатистический код говорили. И я посмотрел всю либу. Это единственный файл, в котором только два таких метода. И вы хотите мне это выдать за среднестатистический код? Несерьезно. Это исключение, а далеко не правило
Какие крайности? Это единственный класс с логикой, все остальное — просто ДТО, которые и считать-то не обязательно.
Да, я понял. Там нету ошибки.
Окей, а в этом коде — есть:
builder.RegisterType<IFoo>.As<IBar>();
Athari
21.03.2018 02:47+1При сложной логике, например при работе с компилятором, получается что-то в таком духе.
Я не вижу сложной логики, я вижу сложный API (
AddModifiers(Token(SyntaxKind.PublicKeyword))
вместоpublic
). Реальных ветвлений в коде метода мало.PsyHaSTe
21.03.2018 11:18Это самый просто АПИ, который может предоставить компилятор, ибо это тот АПИ, которым он сам пользуется при парсинге исходного текста. То, что он сложный, обусловлено сложностью предметной области.
Про ветвления никто не говорил ничего.
Flammar
20.03.2018 14:07Вполне логичное правило, чтобы отличать свойства класса от локальных переменных.
Спасибо, интересное объяснение. Сегодня, правда, для этого есть IDE с подсветкой синтаксиса и с переходом к декларации по ссылке. Как и для отличения класса от интерфейса.
PsyHaSTe
20.03.2018 01:17А чем это правило плохо? Часто же бывает, когда и переменную, и имя класса хочется называть одинаково. Уж лучше различие в регистре первой буквы, чем всякие мерзкие префиксы-постфиксы.
Не понял этого утверждения. Намешано про типы, префиксы, регистры…
То же самое и с интерфейсами, когда имя интерфейса и класса совпадает: List и IList, Dictionary и IDictionary и т.д.
IList реализует не только List, и не только в стандартной библиотеке.
ApeCoder
20.03.2018 13:34Имхо это костыль:
- Классы, префиксы энамы и прочее — это типы и находятся в одном адресном пространстве. Их не надо разделять.
- Получается что даже тот код, который использует интерфейс, а не реализует, связан с тем, что это именно интерфейс. Т.е. убрав эту I можно было бы уменьшить compile time dependency
- Если у нас класс называется так же как интерфейс, это значит, что мы что-то не выразили в имени (IList это список вообще, а List — это конкретная реализация, которая уже не список вообще, но называется как список вообще. До дженериков ее назвали ArrayList — что, имхо, более явно отличает абстрактный список от конкретной реализации)
PsyHaSTe
20.03.2018 13:45+2На вкус и цвет, как говорится. Я с одной стороны привык, с другой стороны получаю из этого полезную информацию, в частности всегда ли сюда приходит одна реализация, или может приходить как реализация, так и мок-объект. Отсюда могут быть всякие разные забавные следствия, могу рассказать как-нибудь.
Получается что даже тот код, который использует интерфейс, а не реализует, связан с тем, что это именно интерфейс. Т.е. убрав эту I можно было бы уменьшить compile time dependency
Не могли бы, потому что тесты
Если у нас класс называется так же как интерфейс, это значит, что мы что-то не выразили в имени (IList это список вообще, а List — это конкретная реализация, которая уже не список вообще, но называется как список вообще. До дженериков ее назвали ArrayList — что, имхо, более явно отличает абстрактный список от конкретной реализации)
Наоборот, в 99% случаев у интерфейса одна реализация, а интерфейс нужен для того, чтобы подсовывать моки.ApeCoder
20.03.2018 14:19Получается что даже тот код, который использует интерфейс, а не реализует, связан с тем, что это именно интерфейс. Т.е. убрав эту I можно было бы уменьшить compile time dependency
Тесты это другой код.
Наоборот, в 99% случаев у интерфейса одна реализация, а интерфейс нужен для того, чтобы подсовывать моки.
Не явлюятся ли моки другой реализацией?
Flammar
20.03.2018 14:36+3Не являются ли моки другой реализацией?
Да, являются — единственной другой реализацией, создаваемой на лету и не имеющей нормального имени класса.ApeCoder
21.03.2018 10:00Итого
- IFoo это на самом деле "какая-то фигня для тестирования" а не "Foo вообще"
- Foo это "Foo вообще" (но пользователь использует IFoo в качестве "Foo вообще")
- Тест подсовывает неименованную частичную реализацию IFoo
Лично я предпочитаю так не делать. По поводу моков была продуктивная дискуссия c lair
Athari
20.03.2018 14:29+2Получается что даже тот код, который использует интерфейс, а не реализует, связан с тем, что это именно интерфейс.
Интерфейс на класс в стандартной библиотеке никто никогда менять не будет, потому что это сломает бинарную совместимость. Особенно подобное изменение убийственно для языков без множественного наследования, потому что очень усложнит миграцию даже при сорцах. Сорцовая совместимость в частном случае — слишком слабое требование для большого количества библиотек.
И в случае
List<T>
сильный упор был сделан на производительность, а не на красоту абстракций. В этом классе нет виртуальных методов, а вызов через интерфейс полиморфен всегда, причём в ранних версиях дотнета заметно медленнее обычного полиморфного вызова, если не изменяет память.ApeCoder
20.03.2018 14:37Я скорее про саму нотацию, а не про использование ее в публичных типах стандартной библиотеки.
DistortNeo
20.03.2018 17:51а вызов через интерфейс полиморфен всегда, причём в ранних версиях дотнета заметно медленнее обычного полиморфного вызова, если не изменяет память.
Сейчас однаково.
Разница есть только при навешивании ограничений на generic-параметры:Foo<T>(T param) where T: ISomething
будет чуть (но только совсем чуть!) медленнее, чемFoo(ISomething param)
.TheShock
20.03.2018 18:20А разве это все не в Компайл-Тайм делается?
PsyHaSTe
20.03.2018 20:34+1Для структур делается в компайл тайм. На дотнексте целый доклад был про то, как превращать
if (typeof(T) == typeof(SomeType))
вif (true)
.
А для всех классов вообще используется одна-единственная реализация
Foo__Cannon
DistortNeo
20.03.2018 22:02Для структур делается в компайл тайм
Да, но это происходит на этапе выполнения программы при JIT-компиляции, а не при создании сборки.
DistortNeo
20.03.2018 22:01Нет. В случае дженерик-функции тип `T` просто становится её параметром. Аналогично работают дженерики в Java.
Кстати, такое поведение позволяет делать виртуальные дженерик методы, что невозможно в случае шаблонных методов C++.
Примечание: когда T — структура, а не класс возможна генерация отдельного кода для каждого из типов — это приводит к code bloating, но позволяет избавиться от боксинга.
Athari
19.03.2018 10:20+1Если бы проблема была только в interface a.k.a. protocol… Чтобы не прикасаться к проклятому ромбу, дизигнеры языков плодят сущности, придумывая всякие trait a.k.a mixin, кроме того сами интерфейсы мутируют в недо-абстрактные классы добавлением default interface implementation, а поверх всего этого добра сбоку прикрепляются extension methods a.k.a. helper classes a.k.a protocol extensions.
Остапа понесло. Он почувствовал прилив новых сил и
шахматныхООП идей.
При этом сама статья — тоже тот ещё бред.
Никаких "специальных случаев" быть не может. Ты или полноценно реализуешь множественное наследование, как в C++, или не делаешь этого вообще. Иначе возникает момент, когда нельзя наследоваться из-за того, что где-то в предках кто-то решил унаследоваться от чего-то другого.
Кроме высосанной из пальца причины про "невозможность" ромба и "лени" у авторов языков были объективные причины не реализовывать множественное наследование: авторы джавы упарывались по "простоте", а множественное наследование — "сложно"; авторы шарпа делали "джаву по-нормальному", и не нашли достаточно оправданий отступить от разделения типов базовых классов, ну и плюс упирали на "изменение базы должно ломать как можно меньше".
- Изменение interface на class — это ломающее изменение что в Java, что в C#. Более того, любое изменение интерфейса — ломающее изменение. Это ужасно. Костыль с default interface implementation — попытка замести эту проблему под ковёр и сделать из интерфейсов более гибкие и послушные абстрактные классы.
Flammar
20.03.2018 14:14Default interface implementation — это очень хорошо, это попытка выжать из интерфейсов по максимуму без примешивания к ним состояния, что превратило бы их в абстрактные классы.
Athari
20.03.2018 14:32Раньше интерфейсы были "классами без реализации", сейчас уже "классы без состояния". Интересно, куда определение интерфейсов дальше мигрирует.
KIVagant
19.03.2018 10:36Выше уже писали, что в php уклонились от проблемы с помощью трейтов, но не решились пойти до конца и дать трейтам реализовывать интерфейс, что уменьшило бы писанину и дублирование одних и тех же слов.
iit
19.03.2018 12:01На самом деле я тоже так думал, пока не додумался об одной маленькой проблеме — трейт не самостоятельный объект и если к нему привязывать интерфейс тогда у нас 3 варианта.
1) Интерфейс не должен влиять на исходный объект куда примешивается никак — тогда собственно зачем он нужен
2) Необходимо выдавать ошибку если класс не реализует интерфейс трейта, что усложнит код
3) Необходимо делать магию что у объекта как бы есть интерфейс но он не описан явно, что будет выдавать еще больше юмора, особенно если методы интерфейса переопределяются родительским объектом или другим интерфейсом.
maxzh83
19.03.2018 11:30+2— Правда? А что такое интерфейс? Это то же самое что и класс?
— Ну… Не совсем!
— В каком плане?
— Ни один из его методов не должен иметь реализации.
В этом месте ответ невнятный, а дальше все как на неудачном собеседовании, все цепляется одно за другое. Интерфейс — договор, описание функционала. Класс — реализация, которая может быть частичной или не быть вообще (абстракный класс). Если кто-то использует отсутствие реализации как договор, это его личные трудности (как в примере).
mayorovp
19.03.2018 11:38Ладно, получается что "Смертельный Бриллиант Смерти" это проблема, которую решили еще в прошлом веке, и она не фатальна и даже не ведёт к смерти.
Нет, нет, и еще раз нет! Эта проблема так и не была толком решена в C++. На нее просто закрыли глаза, порекомендовав программистам так не делать. (Это вообще популярный способ борьбы с языковыми проблемами в С++)
Проблема тут не только в
переменныхполях, она в методах.
Рассмотрим простейший "бриллиант":
class B { void foo() { /* ... */ } } class D1 extends B { void foo() { super.foo(); } } class D2 extends B { void foo() { super.foo(); } } class M extends D1, D2 { void foo() { ??? } }
Как теперь в классе M переопределить метод foo так, чтобы базовая реализация была вызвана не два раза, а один?
В языке C++ это можно сделать только копированием логики из D2 в M (или из D1 в M).
В языке Python это попытались решить "выпрямлением" наследования — что тоже не лишено проблем, поскольку позволяет писать код с неочевидным поведением.
Athari
19.03.2018 11:55Это не "простейший" бриллиант, это бриллиант с виртуальным методом в виртуальном базовом классе, с переопределением метода на нескольких уровнях.
Если происходит что-то подобное, и надо разруливать хитросплетения из B.foo, D1.foo, D2.foo и вызывать это всё в нужной последовательности, и в этом есть какой-то смысл, то, вероятно, авторам всего этого добра стоило выделить основную логику foo в D1 и D2 в отдельные методы doFoo.
Более того, проблема с вызовом базовых методов в нужный момент в нужной последовательности возникает и без множественного наследования, если в виртуальных методах очень хитрая логика, которая зависит от порядка выполнения отдельных частей на разных уровнях иерархии. Один из способов разруливания этой ситуации окрестили паттерном "шаблонный метод". Всё уже изобретено.
Разумеется, если у вас подобная хитрая ситуация, и автор этих трёх классов не побеспокоился, что делать наследникам, то у вас проблемы. Но это просто непродуманный дизайн, а не какая-то концептуально неразрешимая ситуация.
MaxLich
19.03.2018 12:45А если в D1 и D2 будут разные реализации метода foo(), в классе M не будет переопределён этот метод, и у объекта типа M вызвать метод foo(), то какая реализация отработает?
mayorovp
19.03.2018 12:50+2Ошибка компиляции будет (в C++). Это еще одна проблема множественного наследования.
Antervis
19.03.2018 14:14Как теперь в классе M переопределить метод foo так, чтобы базовая реализация была вызвана не два раза, а один?
в с++: M::foo() { D1::foo(); }mayorovp
19.03.2018 14:20И теряем ту логику, которая была добавлена в D2::foo.
Antervis
19.03.2018 17:33и точно та же проблема будет при аналогичной композиции
mayorovp
19.03.2018 18:01Какая именно проблема является "точно той же"?
Разумеется, когда "бриллиант" уже начал рисоваться (классы B, D1 и D2) — правильно замкнуть его никакая композиция уже не поможет. Композицию надо применять с самого начала, вместо наследования.
Antervis
19.03.2018 20:13Простой пример:struct Point { float x, y; }; class Ellipse { Point p1, p2; public: // set/get/whatever }; class Rectangle { Point p1, p2; public: // set/get/whatever }; // А теперь я хочу вписанный в прямоугольник овал class EllipseInRectangle { Ellipse _ellipse; Rectangle _rect; // Тут четыре Point, но необходимо и достаточно две точки public: // И что делать будем? };
Eagle_NN
19.03.2018 14:34B это правильное поведение, имхо.
Аналогично можно пытаться создавать какой-либо класс, который наследуется от базового, если базовый не описан в include.
Компилятор просто не знает что за символ использовать и ему надо об этом рассказать.MaxLich
20.03.2018 12:05Ну это хорошо, что в Си++ эта ситуация проверяется, и компилятор не даст пустить это в продакшен. В других языках с возможностью множественного наследования это может вызвать ошибки в работе самой программы.
А если будет другая ситуация: в методе foo() класса M вызвать аналогичный метод предка (super.foo()), то какой метод вызовется?
MaxLich
19.03.2018 17:01Да, то-то и оно. При множественном наследовании не только проблема с переменными, но и с методами.
mayorovp
19.03.2018 18:02Кажется, именно так я сразу и сказал. О чем же вы в таком случае спорите?
MaxLich
20.03.2018 11:59Вообще-то я только подтвердил то, что Вы написали, и немного расширил это. Просто выше писали про переменные, но и с методами бывают проблемы при множественном наследовании (и когда возникает смертельный ромб смерти).
bm13kk
19.03.2018 13:09+2Отличная статья, с котороя я согласен на 100%… если бы заменили интерфейсы на абстрактные классы.
Абстрактные классы — неудачная упрощенная магия, которая должна воплощать интерфейсы и трейты (не знаю, как их назвать обще для всех языков).
lazard105
19.03.2018 13:25Интерфейсы нужны чтобы никто не мог написать в нем реализацию метода.
Тот факт, что в вашем абстрактом классе нет публичных переменных и реализаций методов, не означает, что их туда никто не добавит потом.Dair_Targ
19.03.2018 13:45Чем плоха реализация метода, которая зависит от других методов? Например:
interface Repr {
default String repr() {
return getClass().getSimpleName() + "(" + value() + ")";
}
String value();
}
?lazard105
19.03.2018 14:06Ничем, и даже хороша — тем что не использует внутренние методы и не имеет доступ к полям класса.
Точно такой-же метод вы могли бы написать в каком-нибудь классе Utils.
zagayevskiy
19.03.2018 14:22А чем она хороша, зачем это надо?
Flammar
20.03.2018 15:11+1Тем, что, например, в Java можно было бы выкинуть классы типа AbstractCollection и AbstractList, в которых все «остальные» методы определены через один-два «основных» и перенести реализации в соответствующие интерфейсы.
zagayevskiy
20.03.2018 15:36Зачем?
Flammar
20.03.2018 14:59Не реализацию метода, а переменные состояния aka поля. А так — да, именно чтоб никто никогда не мог.
ganqqwerty
19.03.2018 13:48Интерфейсы в TypeScript при этом вообще другие, они просто описывают примерную форму объекта.
BogdanF
19.03.2018 13:59В C#8 могут появиться дефолтные реализации интерфейсов… Может жизнь упростит. Или усложнит. Вообще, было бы здорово, если бы MS поступил с множественным наследованием как с unsafe — типа, используй на свой страх и риск, только поставь галочку. Вот только пользы от этого, скорей всего, будет гораздо меньше, чем вреда.
AxisPod
19.03.2018 14:03+1А смысл наследовать от 2х классов? Как это потом покрывать тестами? Это не C++ и PHP где можно подсунуть стаб.
А вот агрегация на пару с DI, а далее и IoC решают все описанные проблемы.Flammar
19.03.2018 21:01Смысл есть наследовать от двух и более типов. При желании строгой или статической типизации эта необходимость рано или поздно вылезает. Во времена создания C++ типы в ООП-языке могли быть только структурами, классами или алиасами типов.
sami777
19.03.2018 14:33— Правда? А что такое интерфейс? Это то же самое что и класс?
— Ну… Не совсем!
— В каком плане?
— Не один из его методов не должен иметь реализации.
Самое печальное, что здесь если даже «не» убрать, все равно получается белиберда.vg7
19.03.2018 18:18«Не» надо не убирать, а заменить на «ни» — «Ни один из его методов не должен иметь реализации» — так это пишется по-русски.
А так, никакой белиберды я не вижу.
alexs0ff
19.03.2018 14:33касательно интерфейсов в c#. Не представляю как можно обходится без них при написании юнит тестов. Например есть два класса, один использует другой. Если я в объект передам явно конкретный тип, то при написании теста для одного, мне придется учитывать поведение другого, что добавляет всякой сложно логики. (виртуальные методы не предлагать)
class A
{
public void MethodA(int val){
return val+1;
}
}
class B{
A _a;
B(A a){
_a = a;
}
public int MethodB(int val){
val +=3;
return _a.MethodA(val);
}
}
Так вот, если в конструктор B передать вместо типа A интерфейс (через который реализован Mock), то можно легко закрыть тестами два класса, а вот если один — то в тесте нужно будет учитывать логику поведения логики двух одновременно классов.DistortNeo
19.03.2018 16:17Всё зависит от того, готовы ли вы терять в производительности при отказе от сильно связанного кода. Ведь вызовы через интерфейс — виртуальные, они медленнее прямых вызовов.
alexs0ff
19.03.2018 17:201) Потеря производительности от виртуальных методов где-то 5-10%
2) Экономить везде на спичках тоже не очень хорошо, всегда открываю профайлер и смотрю где находятся участки, которые нужно «подмазать» и на моей памяти еще ни разу не приходилось править виртуальные методы.
zagayevskiy
19.03.2018 18:38Вот я ещё в С# и джаве не задумывался на эту тему. Если начинаются такие мысли, то надо подумать, а то ли ты делаешь.
Xandrmoro
19.03.2018 15:05Проблему в статье надо решать не множественным наследованием, а миксинами.
Эх…Flammar
19.03.2018 18:44-1Интерфейсы и есть нечто типа микс-инов.
Xandrmoro
19.03.2018 21:00Миксины по определению содержат в себе реализацию.
В шарпе можно изощриться и сделать подобие через интерфейс и методы расширения к нему, но это такой себе хак — не то чтоб сильно воняющий, но определённо с душком. И без внутреннего состояния у миксинов.
OYTIS
19.03.2018 15:26TL;DR: хочу множественного наследования в Java, множественное наследование бывает полезно.
Я вот считаю, что указатели и pointer arithmetic — это важные фичи, и не пишу на Java. Но Java сознательно спроектированна так, что «может быть полезные» фичи приносятся в жертву простоте и унификации.Free_ze
19.03.2018 15:35Кто мешает игнорировать опасные фичи? Зачем сознательно сужать сферу применимости инструмента?
OYTIS
19.03.2018 16:31Java придумана для того, чтобы координировать работу десятков тысяч взаимозаменяемых программистов. Для такой области применения отсутствие фичи — это тоже фича, только не для программиста, а для менеджера. Позволяет сэкономить силы, время и деньги на административных мерах.
Free_ze
19.03.2018 19:04+1Java придумана была для написания кроссплатформенного кода, а тот интерпрайз, в который она выродилась в итоге, обладает достаточно высоким порогом вхождения.
vedenin1980
19.03.2018 17:49Кто мешает игнорировать опасные фичи? Зачем сознательно сужать сферу применимости инструмента?
А кто мешает пользоваться ядерным оружием только для полезных вещей (вроде изменения ланшафта), зачем запрещать любое. его использование? Очевидно, потому что слишком сильные последствия.
1. В Java программе может использоваться сотни разных библиотек, как вы сможете гарантировать, что автор одной из них не добавит опасную фичу?
2. Придется контролировать всех junior'ов и самоучек, чтобы они не использовали эти фичи. Зачем опытным разработчикам тратить на это время?
3. В мире полоно дилетантов и самоучек, для которых даже циклы что-то сложное (реально видел код состоящий из сотен копипащенных строчек, потому что цикл это думать надо). Если у них будет опасный инструмент — в какой-то момент появится куча кривого и глючного софта. который будет ассоцироваться с языком,
4. Если опасную фичу не стоит использовать никому — то зачем она нужна? Если бы можно было сказать — а теперь игнорируем опасные фичи и их не используем — то Java и C# оказались бы не нужны, хватило C++, где все игноруют опасые фичи,
5. Каждая ненужная фича усложняет язык и порог вхождения сильно увеличивается. И получается, что изучить Java нужно полгода, изучить на том же уровне C++ — 2 года, и хуже многие программисты просто не осилят такую сложность, а значит дефицит кадров,
Free_ze
19.03.2018 18:02как вы сможете гарантировать, что автор одной из них не добавит опасную фичу?
Я не должен заботиться об этом, ибо сфера ответственности разработчика библиотеки.
Придется контролировать всех junior'ов и самоучек, чтобы они не использовали эти фичи.
То есть без этого никого контролировать не нужно? Код-ревью, статические анализаторы и прочие линтеры. Банальные документы-соглашения о процессе разработки.
В мире полоно дилетантов и самоучек, для которых даже циклы что-то сложное
Это будет проблемой инструмента лишь в случае, если опасная фича будет необходимой. В остальном же это будет «terra incognita» для избранных.
Если опасную фичу не стоит использовать никому
Если звезды зажигают, значит кому-то это нужно. Даже если это не вы.
Каждая ненужная фича усложняет язык и порог вхождения сильно увеличивается
Туда же. Насчет порога вхождения: если вы ее не используете в повседневной работе, то порог не изменяется.vedenin1980
19.03.2018 18:22Я не должен заботиться об этом, ибо сфера ответственности разработчика библиотеки.
Да ладно. Вы сдали программу заказчику, а она во время продашена начала падает и вызывает синий экран смерти ОС каждые 15 минут. Чья это проблема ваша или разработчика опенсорс библиотеки? Заказчика не будет волновать чья это сфера ответвенности, его будет волновать вопрос почему он несет миллионые убытки. А даже просто обнаружить какая библиотека использует опасные фичи — нетревиальная задача, тем более заменить ее во время продакшена.
То есть без этого никого контролировать не нужно? Код-ревью, статические анализаторы и прочие линтеры. Банальные документы-соглашения о процессе разработки.
Нужно, но чем меньше у начинающих выстрелить в ногу — тем лучше.
Туда же. Насчет порога вхождения: если вы ее не используете в повседневной работе, то порог не изменяется.
Нет, вы не можете считаться опытным разработчиком, если не знаете все фичи языка и возможные проблемы. Иначе какой вы сеньер, если не можете прочитать обычный код на этом языке и не понимаете его проблем?
Если звезды зажигают, значит кому-то это нужно. Даже если это не вы.
А их не зажигают, на C# и Java большинстом разрабатывающих язык от них отказались. Для тех кому нужно оставили возможность писать небезопасный код на C++ или небезопасные блоки на C#.
Это будет проблемой инструмента лишь в случае, если опасная фича будет необходимой. В остальном же это будет «terra incognita» для избранных.
Тогда ответьте зачем появилась Java и C# вместо С++? Можно было бы просто сказать, а теперь мы не используем опасные фичи. Любая фича влечет в себя проблемы поддержки и обратной совместимости, необходимости ее знать всем пользователям. Зачем нужно было придумывать Java смысл которой убрать из С++ самые спорные и опасные фичи (вроде указателей), если можно было просто не использовать?Free_ze
19.03.2018 18:46-1Чья это проблема ваша или разработчика опенсорс библиотеки?
Какое отношение низкое качество библиотеки имеет к фичам в языке?) Самый очевидный ответ — архитектора, который решил, что использование конкретной библиотеки будет уместно, ибо есть куча софта, код которого вам недоступен или защищен он фиксиков лицензионным соглашением.
чем меньше у начинающих выстрелить в ногу — тем лучше.
Хорошее — хорошо, плохое — плохо. Только если эта простота вызвана меньшей гибкостью системы, то это будет больше кода, багов и… сложность поддержки)
Нет, вы не можете считаться опытным разработчиком, если не знаете все фичи языка и возможные проблемы.
«Порог вхождения» — это про джуниоров. Опытные его преодолевают.
на C# и Java большинством разрабатывающих язык от них отказались
Только те и другие сейчас костылями себе миксины изобретают, а Java-сообщество воет, что unsafe у них забрать хотят. Это так, навскидку.
Тогда ответьте зачем появилась Java и C# вместо С++?
Затем, что не было стандартизации C++ и язык долгое время загибался. Сейчас он вполне позволяет регулировать уровень красноглазия и писать годный высокоуровневый код. Битву за десктопную кроссплатформенность Java проиграла старшему брату к настоящему моменту.Flammar
19.03.2018 18:53+1Какое отношение низкое качество библиотеки имеет к фичам в языке?)
К фичам — косвенное, а к степени опасности «виртуальной машины» — прямое. Одно дело, когда библиотека иногда, раз в полчаса, не работает и пишет сообщения со стек-трейсами в лог, другое — когда раз в полчаса вмест этого происходит BSOD.Free_ze
19.03.2018 18:55-1Что вы хотите на это услышать?) Не используйте плохой третесторонний софт, хорошо тестируйте перед продакшном, грамотно логгируйте.
На более низкий порог вхождения откликнутся еще более бестолковые разработчики.На современную веб-сферу посмотрите.Один из законов жизни.vedenin1980
19.03.2018 21:05Free_ze, я понял вам хочется развести холивар на тему почему С++ уже торт и круче Java/C# и т.п. языков. Это скучно и не интересно.
Free_ze
20.03.2018 11:16-1Вы мне задали вопрос про языки, я на него ответил. Вам не нравятся мнения, отличные от вашего?
Free_ze
19.03.2018 19:01-1Одно дело, когда библиотека иногда, раз в полчаса, не работает и пишет сообщения со стек-трейсами в лог, другое — когда раз в полчаса вмест этого происходит BSOD.
Прикладной софт при всём желании не сделает вам BSOD.
Flammar
19.03.2018 18:46+1А кто мешает нести лишнюю ответственность непонятно за кого и непонятно за что (конкретно — за используемую стороннюю библиотеку и её авторов)?
Free_ze
19.03.2018 18:53-1Никто, но я бы не советовал этим заниматься. Тем более, что к предмету обсуждения это напрямую не относится.
PashaNedved
20.03.2018 08:46Поставщик библиотеки несет ответственность за свою библиотеку.
Разработчик ПО несет ответственность за свое ПО, включая использование сторонних библиотек (за используемые библиотеки, по прежнему, несут ответственность поставщики этих библиотек).
Если опустить формальности, то нет никакой лишней ответственности. Курение мануалов, апдейтов и прочих легальных смесей — это обязанность и зона ответственности разработчика ПО.
owlet255
19.03.2018 15:41Контракт, контракт без реализации и т.п. — не очень хорошее определение интерфейса. Дело в том, что интерфейс не возникает исторически как контракт — так декларируется в учебниках, но рассуждать так — идеализм. Интерфейс определяется в процессе разработки. Если вы его определили как "контракт", то с высокой долей вероятности еще не раз перепишете его детали, когда погрузитесь в разработку. Так что, если и "контракт" — то "контракт, написанный задним числом".
Вот неплохое определение, на мой взгляд: "Интерфейс — это общая граница между двумя функциональными объектами".
SimSonic
19.03.2018 18:42Но вообще вот эта идея выше в комментариях с определением метода через член очень красиво выглядит. Хочется что-то вроде такого:
@Override public T[] values() => container::values;
Плюс минус вариации…mayorovp
19.03.2018 19:05Нет, это совсем не то. «Идея выше в комментариях» была про реализацию сразу всех методов интерфейса, а не только одного.
Кстати, то что написали вы давно есть в C# и называется Expression Bodied Member.SimSonic
19.03.2018 19:22+1Посмотрел, что это такое, да, отдаленно похоже. В Java сильно не хватает чего-то такого, что помогло бы убрать бойлерплейт, возникающий при композиции и реализации интерфейсов. Сказать компилятору «хочу этот/те метод(ы) оттуда».
Flammar
19.03.2018 18:48+1— Правда? А что такое интерфейс? Это то же самое что и класс?
Определение в стиле «утиной» типизации. Более правильно было бы «класс, который не может иметь состояния», «класс, который не может содержать данные».
— Ну… Не совсем!
— В каком плане?
— Ни один из его методов не должен иметь реализации.
Flammar
19.03.2018 19:06+1Ну, интерфейсы возникли от потребности иметь безопасную, т.е. с объектами только по ссылке, без арифметики, без преобразования указателей и без постоянной необходимости явного преобразования типов, статическую типизацию. Для её реализации необходимо иметь возможность присвоить (по ссылке) объект в качестве значения нескольким переменным разных типов, не являющихся наследниками друг друга. Для этого необходимо иметь множественное наследование хотя бы типов. С другой стороны, известно, что множественное наследование структур данных порождает проблемы. Следовательно, нужно иметь типы данных, которые не были бы структурами данных. Это и есть всякие трейты, микс-ны и интерфейсы.
Повторяю, они нужны в первую очередь для того, чтоб иметь возможность эффективно использовать статическую типизацию.
Героям диалога из поста, так как они, судя по всему, не понимают зачем нужна статическая типизация, лучше использовать динамически типизируемые языки вроде Javascript.VolCh
19.03.2018 20:00-1PHP тоже динамически типизируемый, но интерфейсы в нём есть :) И появились они не результате стремления не к статической типизации, а к строгой.
Flammar
19.03.2018 20:10— Абстрактный класс может иметь нестатические поля, а интерфейс не может.
Потому что в реальном мире в следующей версии никакие правила языка не мешают их добавить в абстрактный класс. А в интерфейс — мешают.
— У моего класса их тоже нет, почему он не интерфейс?
strangeraven
20.03.2018 00:41Статья смешная, но производит впечатление, что ее писал джуниор, начитавшийся книжек по ООП.
Просто автор видимо не дебажил часами падающие на C++ программы, в которых такой же студент сваял ажурные конструкции из множественного/виртуального наследования, потом запутался в выборе нужного cast при проведении типов и получил UB по лбу.
К слову, в большинстве проектах на C++, на которых я работал, множественное наследование было тупо запрещено. Это к вопросу о том, насколько «успешно» решили эту задачу в C++Flammar
20.03.2018 14:30К слову, дебажить и диагностировать metadata-driven системы, а именно ту самую метадату в них — тоже отдельное особое удовольствие…
Athari
20.03.2018 14:36Ну, знаете, C++ — не показатель. В нём UB можно получить от любого чиха, и дебажь потом. Что в каком-то случае оно вылезло из-за множественного наследования и неправильного приведения типа — случайность.
ShadowTheAge
20.03.2018 11:46Лично мне иногда не хватает чего-то типа mixin-ов — небольшого кусочка класса, с полями, методами, свойствами и указанием реализуемых интерфейсов, который можно «примешать» к любому классу (но только один раз)
bystr1k
20.03.2018 11:46В чем реальное отличие от интерфейса?
Абстрактный класс может в себе хранить состояние, а интерфейс — нет
guai
20.03.2018 14:09-2Какой хороший у мистера Мартина strawman, здоровенный, с*ка!
C++ порешал множественное наследование? Но ведь это привело к другой проблеме, которая называется, ну, «C++» :)
Переусложненный тормозной язык с кучами UB, к которому тулинг могут запилить полтора человека в мире, да и те со временем начинают пилить каждый свой язык :)
Дизайн языков программирования — это нахождение баланса, чем то жертвуем, что-то хотим во что бы то ни стало.
Да, в яве решили, что ну его нафиг то множественное наследование, это упростило компилятор, и, вероятно, помогло избежать тормозов.0xd34df00d
20.03.2018 18:40+1Тормозной, ясно. А в чём тормоза заключаются?
guai
20.03.2018 19:27в скорости сборки, в чем же еще?
DistortNeo
20.03.2018 22:09Самая долгая операция при сборке C++ программ — это оптимизации при линковке.
И это не проблема C++, это проблема любого компилируемого языка: хотите высокую производительность — платите за неё временем компиляции.0xd34df00d
20.03.2018 22:47Ну окей, отключите
-flto
или что там у вас на вашей платформе. Случаи, когда использование, скажем, precompiled headers уменьшает время компиляции на порядок, в моей практике нередки.
Ну ничего, завезут модули (особенно в виде Another take on modules) — будет хорошо.DistortNeo
20.03.2018 23:59Ну окей, отключите -flto или что там у вас на вашей платформе
А зачем? Я пишу на С++ исключительно ради производительности, поэтому LTO мне нужно. Там же, где производительность не критична, я пишу на других языках, а не отключаю LTO.
Насколько я знаю, крупные проекты с LTO часто не собирают даже в релизе — слишком долго. Максимум — компилят так отдельные компоненты.
0xd34df00d
21.03.2018 00:32А зачем?
Чтобы не тратить время на LTO при разработке, когда вы проверяете ваш код на мелких тестовых данных (или вообще на то, что он тупо тайпчекается).
Ну и в ряде задач горячий код всё равно оказывается плюс-минус в одном TU, так что от этого LTO ни горячо, ни холодно.
TargetSan
21.03.2018 13:18К слову, а модули не померли вообще? В соответствующей гугл группе мёртвая тишина уже с полгода как. Мои попытки спросить в общей группе isocpp привели к "иди в группу модулей". На заявку вступления в гугл-группу модулей я ответа так и не получил. Или там открытое обсуждение не приветствуется?
netch80
21.03.2018 13:10+1Тормоза сборки C++ на ~99% заключаются в обработке шаблонов, а не в множественном наследовании (которое используется чуть чаще, чем никогда).
guai
21.03.2018 13:22Что только подтверждает мой тезис о поиске баланса в дизайне языков программирования. Хотим наркоманские шаблоны — жертвуем скоростью.
Есть и другие апекты в дизайне языков программирования, напрямую влияющие на скорость компиляции. Наличие/отсутствие модулей, например.
А еще аспект совместимости.
Могла бы ява заиметь множественное наследование? Ну, наверное, могла бы, если поломать всю совместимость, над которой очень пекутся. И ради чего? Вы сами правильно заметили, что она редко когда нужна.netch80
21.03.2018 13:36> Вы сами правильно заметили, что она редко когда нужна.
Категорически прошу внимательно читать и не подтасовывать. Я не сказал, что она редко _нужна_, я сказал, что она редко _используется_. А используется она редко, в частности, из-за того, что мало где адекватно реализована, и из-за критиканства неосиляторов. Я её использовал и в Python, и в C++ (без общего базового класса), и как-то программы не рухнули и работают без проблем.
Остальной ваш пафос пропускаю мимо за непрактичной банальностью.
DistortNeo
21.03.2018 14:12Плюс плохо масштабируемая система с хедерами. Даже если не использовать шаблоны и писать на чистом C, сборка все равно будет тормозить из-за огромного размера хедеров.
В итоге приходится весь код пихать в один файл на несколько тысяч строк, а не удобно разбивать по куче мелких файлов.Antervis
21.03.2018 14:23В итоге приходится весь код пихать в один файл на несколько тысяч строк, а не удобно разбивать по куче мелких файлов.
Во-первых, самый долгий этап сборки как правило в LTO, а не парсинг заголовочников. Тем более что он не параллелится. Во-вторых, многократный парсинг хедеров чаще связан с тем, что нерадивые кодеры инклюдят много лишнего в хедеры вместо forward declaration. В-третьих, даже в отсутствие модулей, призванных решить эту проблему, существуют PCH.DistortNeo
21.03.2018 14:43+1Во-первых, самый долгий этап сборки как правило в LTO, а не парсинг заголовочников
Даже с отключенным LTO, если объединить все c/cpp файлы в один, скорость компиляции вырастет в разы.
Во-вторых, многократный парсинг хедеров чаще связан с тем, что нерадивые кодеры инклюдят много лишнего в хедеры вместо forward declaration.
И что же делать, если нужен какой-нибудь монструозный Windows.h?
Плюс сам стандарт C++ не одобряет forward declaration для библиотечных типов.
В-третьих, даже в отсутствие модулей, призванных решить эту проблему, существуют PCH.
Это неудобный костыль. Но приходится пользоваться из-за отсутствия других вариантов.
Antervis
21.03.2018 17:08Даже с отключенным LTO, если объединить все c/cpp файлы в один, скорость компиляции вырастет в разы.
В перспективе такой подход лишь усугубляет проблему, т.к. при изменении малейшего участка кода пересобирается не один TU с изменением, а весь проект.
И что же делать, если нужен какой-нибудь монструозный Windows.h?
Можно инклюдить хедеры в *.c/*.cpp файлах — тогда они используются только в тех TU, где они нужны. Иногда большие хедеры являются лишь коллекцией из include'ов хедеров поменьше, лишь некоторые из которых востребованы, классический пример — QtWidgets.
Плюс сам стандарт C++ не одобряет forward declaration для библиотечных типов.
за интерфейс библиотек должны отвечать их авторы. В собственном коде советую «всегда инклюдить аскетичный минимум»
Hedgar2018
Пора уже перейти на Rust и выкинуть всю эту допотопную дичь. А со временем в Ruste всё допилят, если там чего-то еще не хватает.
Flux
— Пора уже перейти на C и выкинуть всю эту допотопную asm дичь.
© Hedgar2018 — Full-stack Golang & JS developer— Пора уже перейти на C++ и выкинуть всю эту допотопную C дичь.
— Пора уже перейти на Java/.Net и выкинуть всю эту допотопную дичь.
— Пора уже перейти на Go и выкинуть всю эту допотопную дичь.
Никогда такого не было — и вот опять! Когда на haskell переходить будем с этой допотопной дичи?
Eagle_NN
До Rust еще далеко… Да и не факт что к тому времени что то другое не изобретут… А вот в 2018 году на Go вполне пора переходить :)
TheShock
На Go вообще никогда не стоит переходить
Eagle_NN
Наверняка такому категорическому заявлению есть и железная аргументация...?
TheShock
Сразу после того, как вы аргументируете, что на Go надо переходить в 2018 =)
Eagle_NN
1. Быстрый
2. Стабильный
3. Развивается
4. Структурирован (Сокращает количество ошибок)
5. Мультиплатформенный
6. Общего назначения
Asm — переросли, С — переросли, С++ — переросли, Java/С# медленно и перерастаем… Go отличный кандидат на следующий уровень.
P.S. Да, я на всем перечисленном писал. :)
TheShock
Ровно то же самое можно сказать про кучу других языков. Как-то слабовато для аргументов. Но раз вы хотите, то я напишу его недостатки в стиле вашего комментария
1. Непродуманный и устаревший синтаксис языка, провоцирующий быдлокод
2. Непоследовательная и плохая стандартная библиотека
3. Плохой менеджер пакетов
Eagle_NN
Возможно я чего-то не знаю…
Что провоцирует на «быдлокод»?
Что в ней плохого и непоследовательного?
Менеджер пакетов (наверное имеется ввиду менеджер зависимостей) штатный — просто очень удобный по сравнению с Java/C#. Да, есть и лучше, но ни кто не ограничивает их использование.
Хотелось бы пример языка общего назначения с идеальным менеджером (штатным) и лучшей стандартной библиотекой.
Athari
По пунктам можно?
TheShock
Простите, но на ваши пространственные плюсы, которые подходят большинству языков — я выдал свои пространственные минусы.
На самом деле, я уверен, что большинство аргументов вам знакомы и вы просто хотите в очередной раз похоливарить на эту тему.
Тем не менее я вам отвечу. Отсутствие Дженериков приводит к необходимости копипастить, использовать грязные хаки, делать кодогенерацию, которая ухудшает поддержку. Тот же парсинг JSON из-за этого выглядит как сущая магия. Пример непоследовательности библиотеки — это парсинг JSON и пакет flag, которые заточены на похожую задачу, но выполняют ее настолько кардинально разными способами, насколько можно было придумать. Ну вот что мешало сделать парсинг флагов через теги, как уже сделан парсинг JSON? Только понты.
Из-за странного запрета циклической зависимости многие люди просто пишут весь код приложения в одном пакете. Да, иногда это плохая практика, а иногда — необходимость. Отвратительная работа с ошибками, которая стала уже мемом.
Прекрасный пример ущербности языка Go — это теги. Как можно было придумать такое неюзабельное говно в 21-м веке я вообще не представляю.
Менеджер пакетов недавно обсуждали, пришли к выводу, что стандартный настолько плох, что им никто не пользуется.
w0den
Если нужны
холиварывеские аргументы и интересные факты, рекомендую пригласить здесь участников этой дискуссии (точнее, тех, кто считают, что в Go концептуально нельзя писать плохой код, в частности благодаря тому, что в нём не поддерживается тернарный оператор).zagayevskiy
Что? :-| Какая вообще связь-то?..
Athari
Теперь скажите это Линусу. Или любителям микроконтроллеров. Пока что это всё ещё язык по умолчанию, который есть везде.
Вот когда всё плюсовое наследие превратится в фортрановое наследие, а в индустрии будет доминировать условный Rust — тогда поговорим. Потенциал есть, интересные языки есть, но пока и близко к цели не приблизились. Условный D революцию 10 лет обещал. От некогда доминирующих всё и вся плюсов отвалилась куча ниш, да, но и у плюсов осталось достаточно.
Не наблюдаю. Весь кровавый интерпрайз там, и особых претензий к платформам JVM и .NET нет. Более того, если посмотреть на статистику опроса Stack Overflow, то .NET Core куда-то там вырывается. Не понимаю, почему, но факт.
TheShock
Eagle_NN
Я и не говорил что языки отмерли. Я просто обозначал вектор развития.
Сам не так давно вынужден был на asm под ARM кодить. Но это не значит что это тренд…
Athari
А что такое "тренд"? В любом Мухосранске можно найти работу по любому из "нетрендовых" языков, а вот найдёте ли по "трендовым" — ещё большой вопрос. И эти языки пользуются спросом на протяжении десятилетий, пока "тренды" с условными руби приходят и уходят. Что-то из "трендов" абсорбируется в "нетренд", и "нетренд" опять побеждает.
Eagle_NN
Не все и не всегда работают чисто ради денег.
У некоторых есть основные проекты «нетренд» и дополнительные, в которых инструментарий не лимитируется.
А вообще по ответам, к сожалению, тенденция всех форумов прослеживается и тут. Только в 1 ответе было показано с примерами что человеку нравится С++. Остальные просто ругают то что не пробовали или вообще, просто, ругают, при этом не говорят где лучше.
Грустно все это, ну да ладно. У каждого свой путь самовыражения.
0xd34df00d
Не обязательно сильно много ковыряться в каком-то языке и досконально его пробовать, чтобы понять, интересен ли он вам или нет. И если выбирать, на что потратить своё время, скажем, на Go или на какой-нибудь Idris, особенно если при этом не нужно задумываться о деньгах, то выбор-то довольно простой. Для меня, по крайней мере.
0xd34df00d
1. На С++ можно писать не медленнее.
2. С++ очень следит за сохранением совместимости с имеющимся кодом, иногда даже излишне.
3. С++ развивается. Вон, в 20-й версии наверняка будут корутины, концепты и компилтайм-рефлексия, и почти наверняка адекватные модули, например. Если повезёт, успеют прикрутить к корутинам всё необходимое для выражения через них произвольных линейных монад.
4. Особенно их сокращает паттерн проверки на ошибки, наверное.
5. Как и куча других языков.
6. Как и куча других языков.
Athari
C++ — это круто, и в современном языке можно выделить разумное красивое подмножество. Проблема в том, что подмножество у всех своё, стадия эволюции у всех своя, компиляторы у всех свои, и вообще чёрт ногу сломит.
Вот сделают модули. Может быть. Через пару лет. Ещё через несколько лет они будут поддерживаться компиляторами. Казалось бы, вот оно счастье. Но как быстро я смогу не знать про инклюды?
И вот так с каждой фичей.
0xd34df00d
К счастью, сегодня не начало двухтысячных с VC6 и gcc 2.95. Modules TS уже сделаны в clang, например, можно начинать играться. Да и вообще опыт показывает, что новые фичи в большинстве своём реализуются в clang и gcc примерно в районе выхода стандарта, если не раньше. А учитывая, что я таки надеюсь, что модули не примут в виде их текущей TS, а смержат с Another take on modules, получается вполне неплохо.
И никаких пары лет.
PsyHaSTe
А если примут, то что? Стреляться?
Athari
Здесь же уже постили недавно:
Go позиционируется как простой язык, который может за вечер изучить любой школьник. Это как взять Java и довести многословность до абсурда под лозунгом "так проще". Я бы не строил карьеру вокруг этой идеи. Когда вокруг "простых языков" начинают строить сложную индустрию, получаются франкенштейны.
Eagle_NN
Нескромный вопрос, а вы это пробовали делать, или не видел, но осуждаю?
TheShock
Я пробовал делать и немало. Отвратительный язык. Хотя с `go something()` придумали неплохо, но это не перевешивает его недостатки.
Athari
В конечном счёте это ИМХО, конечно.
Чтобы понять, что мне не подходит Go, мне не надо много программировать на Go. Мне достаточно увидеть, какие решения предлагаются на замену исключениям, дженерикам и прочему, посмотреть на разнообразные исходники, чтобы понять, что я так писать не хочу. Всё.
Точно так же я не хочу писать обфусцированный код на CoffeeScript, вермишель из колбэков на JavaScript или ещё что-то подобное.
У меня есть выбор. У меня есть определённые критерии к эстетическим свойствам кода, который я пишу, и краткость, выразительность и наглядность далеко не на последнем месте. Мне не надо писать много кода на языке, чтобы понять, что он меня не устраивает.
Прежде, чем писать на JavaScript вермишель из колбэков, я лучше спокойно посижу в сторонке и подожду, когда в язык добавят async/await и классы. Что, уже добавили? Ну, теперь язык для меня подходит.
И даже если я не люблю какой-то синтаксис (LISP), я могу понять эстетику и мощь, которые скрываются за бесконечными скобочками. У лиспов есть внутренняя красота, в них вложен инженерный гений, даже если язык мне не нравится и я не собираюсь на нём писать.
Что красивого в Go — я не понимаю. Может быть, вы объясните?
Eagle_NN
Проблема в том что всех приучили что ООП это классы и только классы. Это не правда.
Дженерик — а оно правда необходимо?
1. Это тормозит во всех языках. Не тормозит только при условии кодогенерации. Но именно такая возможность заложена в Go. Просто ее надо вызывать более осмысленно.
2. Парадигма Go — простота. Использование дженериков ведет к усложнению кода. Потенциально есть возможность использовать нечто очень похожее используя интерфейсы и контейнеры, но, опять же, это усложнение.
Async/await — Классический костыль для синхронизации.
В Go используются горутины и общаются они через каналы. Этот костыль просто не нужен при правильном построении программы.
Вермишели из коллбеков вроде тоже нет…
Вообще, на Go как и на многих языках, нельзя писать «как я раньше писал на @@@» (@@@ замените на ваш любимый язык). Надо понять принципы, идеологию, и все станет сильно проще.
А что для вас язык с идеальным синтаксисом?
Varim
TheShock
То есть
Это быстро и просто?
А это — медленно и сложно?
Использование дженериков, очевидно, ведет к упрощению кода, а не усложнению. Хватит повторять библию Гошников. Есть куча примеров, где отсутствие дженериков ведет или к отсутствию статики или к копипасту.
Eagle_NN
Вот ведь, век живи, век учись. :)
То что я писал выше — основывается на моем скромном опыте Go. Про «библию» был не в курсе.
Конкретных примеров как в одну сторону так и в другую можно найти массу.
Например я с содроганием вспоминаю как писал в одном из проектов template of template с лямбдами внутри на C++. И там это было обосновано. А потом, через год примерно, отлаживал это.
Так что каждый ищет проблемы соразмерные своей пятой точке в независимости от используемого языка :)
mayorovp
template != generic
Дженерики уже до предела упрощены.
Eagle_NN
Я в курсе. Это просто пример приключений для пятой точки.
TheShock
Вы знаете, я видел по телевизу, как человек убивал младенцев. Я в курсе, что Гоу тут (наверное) ни при чем, но это просто пример приключений для пятой точки, потому, на всякий случай, Гоу лучше не использовать
0xd34df00d
Полиморфные методы в ML-подобных языках тоже предельно просты и вполне мономорфизируются при компиляции.
mayorovp
Так ведь они больше похожи на generic чем на template же. Или я ошибаюсь?
0xd34df00d
У меня недостаточно знаний о дженериках, чтобы однозначно сказать. Они к ним почти наверняка ближе, чем к темплейтам, но насколько близко — я не знаю.
Вы должны явно описать принадлежность тайпклассу в точке определения вашего типа.
У тайпклассов могут быть реализации по умолчанию, и могут быть реализации методов через другие методы, и можно указать набор минимально необходимых множеств методов (см. какой-нибудь Foldable).
Компилятор может вывести реализацию тайпкласса за вас.
Тайпклассы могут включать в себя несколько типов (раз, два). Могут включать в себя и значения (готового примера не приведу, но можно наваять при желании).
Но вообще я это всё скорее в подтверждение ваших слов.
mayorovp
Хм, посмотрел я по вашим ссылкам… Похоже, все-таки успели испортить хорошие языки шаблонами. Кошмар начинается с появлением Type families. Даже не представляю как может работать вывод типов в таких условиях.
0xd34df00d
Ну, на практике семейства типов не сильно-то его и ломают, особенно инъективные.
Что действительно ломает — rank-N polymorphism, и иногда бывает вообще магия.
А в Idris вывода типов функций, считайте, толком и нет (за парой исключений), но это в основном потому, что в присутствии зависимых типов вывод типов неразрешим в смысле Тьюринга.
TheShock
JekaMas
Неиспользование дженериков, кроме как из базовых типов, приводит к еще большему усложнению кода.
Про генераторы не надо… Доводилось это "добро" писать и поддерживать.
mayorovp
На этих самых ваших каналах простейшая задача вида «отменить асинхронную операцию» превращается в такую кашу…
Szer
потому что в Go нет примитива синхронизации со встроенным NACK…
Это в огород гошников, что у них самый лучший язык в мире, а там только базовые ченелы завезли, остальное надо руками делать
Athari
И чем goroutine от async/await принципиально отличается? Те же яйца, только в профиль: потроха разные, диапазон функций разный, синтаксис разный, но суть одна — написание асинхронного кода как синхронного. Что у авторов Go была возможность внедрить асинхронность в язык с самого начала — это хорошо, но async/await в остальных языках — тоже нормальное решение, причём ещё и более гибкое.
Из универсального мейнстрима — C# вне конкуренции по сахару. Из того, что я видел, но на чём сам не писал — Haskell. Из того, что теоретически идеально, но я использовать не буду — LISP.
DistortNeo
Принципиальное отличие: async/await — синтаксический сахар над колбэками (stackless реализация), а горутины — над волокнами (stackfull userspace multithreading).
То есть горутины не только выглядят как синхронные, они и внутри тоже синхронные. Просто планировщик из пространства ядра перенесён в юзерспейс, что значительно сокращает накладные расходы на переключение контекста.
При нормальной реализации оба способа имеют право на жизнь, и асинхронный код выглядит одинаково и работает одинаково за исключением нюансов, связанных со стэком и отладкой.
0xd34df00d
К слову о костылях и каналах. Типобезопасную и проверяемую компилятором STM завезли уже?
Минимум навязанного синтаксиса, сахар, возможность легко делать свои операторы под задачу и вообще eDSL. Ну, хаскель подойдёт, например.
alex1t
Так async/await по сути и есть более удобный синтаксис корутин/горутин/сопроцедур, что есть одно и то же. Тем более, что вообще C# поддерживал в некотором роде сопроцедуры ещё с версии 2.0, когда появилось слово yeild и итераторы. Это уже позволяло (пусть и не так удобно) писать корутины и получать всю прелесть, что сейчас есть с async/await. Так что в этом плане горутины не новость.
Eagle_NN
Async/await это по сути своей синтаксический сахар, который при компиляции просто заменяется на последовательность вызовов. На сколько помню, все вызовы будут вполне линейны с точки зрения процессора, но программе будет казаться что исполняется параллельно.
Аналогично и слово yeild. Только там генерируется класс, который, по мере надобности, возвращает результат и имеет внутреннюю стейт машину.
В отличии от этих методов горутины это реальные параллельные потоки. Да, количество реально параллельных потоков соотносится с количеством ядер процессора. Но все равно они параллельны.
PsyHaSTe
async/await это про асинхронность, которая ничего общего с параллельностью не имеет.
Тем не менее, что за «линейные относительно процессора вызовы» вы имеете ввиду я так и не понял. async/await разворачиваются в типичную цепочку a.then(b).then©, которая планировщиком где-то выполняется.
lair
Вы неправильно помните.
await
выделяет весь код после него в continuation, который кладется на планировщик и может быть выполнен где угодно.alex1t
Горутины ведь тоже синтаксический сахар, превращённый в killer-фичу языка. Async/await является удобной и приятной штукой, но он не гарантирует исполнение на другом потоке, также как и yeild, но в последнем случае нам приходится больше писать самим и мы можем подготовить свой класс Awaiter'а, который всегда будет делать новый поток как только мы напишем
yeild return Yeild.RunAsync(...)
. Но чего об этом спорить — это всего лишь сахар — кто-то любит крепкий чай, а кто-то послаще :)PsyHaSTe
await не должен ничего гарантировать, гарантировать должны нижележащие инструменты, в данном случае объект, на котором авейтимся. Если это какой-нибудь
AlwaysExecutableInAnotherThreadTask.GetAwaiter
то всё ок. Просто не надо мешать зоны ответственности.alex1t
Полностью согласен.
0xd34df00d
Хаскелю почти 30 лет, поздно уже переходить ради хайпа, давайте лучше на Idris.
А если серьёзно, лично я был бы очень рад, если бы больше софта и библиотек писалось на выразительных и безопасных языках.
strangeraven
deleted
guai
тенденция скорее такая, что руст сам обрастет той же допотопной дичью :)