Почему основные сильные статические ООП-языки препятствуют наследованию примитивов?

53

Почему это нормально и в основном ожидается:

abstract type Shape
{
   abstract number Area();
}

concrete type Triangle : Shape
{
   concrete number Area()
   {
      //...
   }
}

... пока это не нормально и никто не жалуется

concrete type Name : string
{
}

concrete type Index : int
{
}

concrete type Quantity : int
{
}

Моя мотивация - максимальное использование системы типов для проверки корректности во время компиляции.

PS: да, я прочитал это, и упаковка - это хакерский обходной путь.

логово
источник
1
Комментарии не для расширенного обсуждения; этот разговор был перемещен в чат .
maple_shaft
У меня была похожая мотивация в этом вопросе , вам может быть интересно.
default.kramer
Я собирался добавить ответ , подтверждающий идею «вы не хотите наследования», и что упаковка является очень мощной, в том числе давая вам какие из неявного или явного литья (или неудача) вы хотите, особенно с JIT оптимизациями предлагая вам получить почти такую же производительность , так или иначе, но вы связаны с этим ответом :-) Я хотел бы только добавить, что было бы неплохо , если бы языки добавлены возможности уменьшить шаблонный код , необходимый для пересылки свойств / методов, особенно , если есть только одно значение.
Марк Херд

Ответы:

82

Я полагаю, вы думаете о таких языках, как Java и C #?

В этих языках примитивы (вроде int) являются компромиссом для производительности. Они не поддерживают все функции объектов, но работают быстрее и с меньшими накладными расходами.

Чтобы объекты поддерживали наследование, каждый экземпляр должен «знать» во время выполнения, к какому классу он относится. В противном случае переопределенные методы не могут быть разрешены во время выполнения. Для объектов это означает, что данные экземпляра хранятся в памяти вместе с указателем на объект класса. Если такая информация также должна храниться вместе с примитивными значениями, требования к памяти будут увеличиваться. 16-битное целочисленное значение потребовало бы его 16-битного значения и дополнительно 32- или 64-битной памяти для указателя на его класс.

Помимо накладных расходов на память, вы также ожидаете, что сможете переопределять обычные операции над примитивами, такими как арифметические операторы. Без подтипов операторы like +могут быть скомпилированы в простую инструкцию машинного кода. Если бы он мог быть переопределен, вам нужно было бы разрешить методы во время выполнения, гораздо более дорогостоящая операция. (Возможно, вы знаете, что C # поддерживает перегрузку операторов, но это не одно и то же. Перегрузка операторов разрешается во время компиляции, поэтому штраф за время выполнения по умолчанию отсутствует.)

Строки не являются примитивами, но они все еще «особенные» в том, как они представлены в памяти. Например, они «интернированы», что означает, что два одинаковых строковых литерала могут быть оптимизированы для одной и той же ссылки. Это было бы невозможно (или, по крайней мере, намного менее эффективно), если строковые экземпляры также должны отслеживать класс.

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

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

JacquesB
источник
12
@Den: stringзапечатан, потому что он разработан, чтобы вести себя неизменным. Если бы кто-то мог наследовать от строки, можно было бы создать изменяемые строки, что сделало бы его действительно подверженным ошибкам. Тонны кода, включая саму платформу .NET, опираются на строки, не имеющие побочных эффектов. Смотрите также здесь, говорит вам то же самое: quora.com/Why-String-class-in-C-is-a-sealed-class
Док Браун
5
@DocBrown Это также причина String, отмеченная и finalв Java.
Девятого
47
«Когда разрабатывался Java, Smalltalk считался слишком медленным […]. Java пожертвовала некоторой элегантностью и концептуальной чистотой, чтобы повысить производительность». - По иронии судьбы, конечно же, Java не достигла такой производительности, пока Sun не купила компанию Smalltalk, чтобы получить доступ к технологии Smalltalk VM, потому что собственная JVM от Sun работала очень медленно, и выпустила JVM HotSpot, слегка модифицированную виртуальную машину Smalltalk.
Йорг Миттаг,
3
@underscore_d: В ответе, на который вы ссылались, очень явно говорится, что C♯ не имеет примитивных типов. Конечно, некоторые платформы, для которых существует реализация C♯, могут иметь или не иметь примитивные типы, но это не означает, что C♯ имеет примитивные типы. Например, существует реализация Ruby для CLI, и CLI имеет примитивные типы, но это не означает, что Ruby имеет примитивные типы. Реализация может или не может выбрать реализацию типов значений путем сопоставления их с примитивными типами платформы, но это частная внутренняя деталь реализации, а не часть спецификации.
Йорг Миттаг,
10
Все дело в абстракции. Мы должны держать голову в чистоте, иначе мы получим глупость. Например: C♯ реализован на .NET. .NET реализован в Windows NT. Windows NT реализована на x86. x86 реализован на диоксиде кремния. SiO₂ это просто песок. Так, а stringв C♯ это просто песок? Нет, конечно, нет, stringв C♯ это то, что в спецификации C♯ сказано. Как это реализовано, не имеет значения. Нативная реализация C♯ будет реализовывать строки в виде байтовых массивов, реализация ECMAScript будет отображать их в ECMAScript Stringи т. Д.
Jörg W Mittag
20

То, что предлагает какой-то язык, это не подклассы, а подтипы . Например, Ada позволяет вам создавать производные типы или подтипы . Раздел Ada Programming / Type System стоит прочитать, чтобы понять все детали. Вы можете ограничить диапазон значений, который вы хотите большую часть времени:

 type Angle is range -10 .. 10;
 type Hours is range 0 .. 23; 

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

 type Reference is Integer;
 type Count is Integer;

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

(Но вы можете использовать Unchecked_Conversion; не говорите людям, что я вам это сказал)

CoreDump
источник
2
На самом деле, я думаю, что это больше о семантике. Использование количества, для которого ожидается индекс, может привести к ошибке времени компиляции
Marjan Venema
@MarjanVenema Это так, и это делается с целью отловить логические ошибки.
coredump
Я хотел сказать, что не во всех случаях, когда вам нужна семантика, вам нужны диапазоны. Тогда у вас есть type Index is -MAXINT..MAXINT;что-то, что как-то ничего не делает для меня, так как все целые числа будут действительными? Так какую же ошибку я получу, передав Угол в Индекс, если все, что проверяется, это диапазоны?
Марьян Венема
1
@MarjanVenema Во втором примере оба типа являются подтипами Integer. Однако, если вы объявляете функцию, которая принимает Count, вы не можете передать Reference, потому что проверка типов основана на эквивалентности имен , что противоречит «все, что проверяется, это диапазоны». Это не ограничено целыми числами, вы можете использовать перечисляемые типы или записи. ( archive.adaic.com/standards/83rat/html/ratl-04-03.html )
coredump
1
@Marjan Один хороший пример того, почему типы тегов могут быть достаточно мощными, можно найти в серии статей Эрика Липперта о реализации Zorg в OCaml . Это позволяет компилятору отлавливать множество ошибок - с другой стороны, если вы позволяете неявно преобразовывать типы, это делает эту функцию бесполезной ... семантически не имеет смысла просто назначать тип PersonAge типу PersonId потому что они оба имеют один и тот же базовый тип.
Воо
16

Я думаю, что это вполне может быть вопросом X / Y. Существенные моменты, из вопроса ...

Моя мотивация - максимальное использование системы типов для проверки корректности во время компиляции.

... и из вашего комментария уточняю:

Я не хочу иметь возможность заменить одно на другое безоговорочно.

Извините, если я что-то упустил, но ... Если это ваши цели, то почему на Земле вы говорите о наследовании? Неявная замещаемость ... как ... все это. Знаете, принцип подстановки Лискова?

В действительности вам кажется, что вам нужна концепция «сильной определения типа», при которой что-то «есть», например, с intточки зрения диапазона и представления, но не может быть заменено контекстами, которые ожидают « intи», и наоборот. Я бы посоветовал поискать информацию по этому термину и какому бы языку он ни был. Опять же, это в значительной степени буквально противоположность наследования.

И для тех, кому может не понравиться ответ X / Y, я думаю, что заголовок все еще может быть ответственным со ссылкой на LSP. Примитивные типы примитивны, потому что они делают что-то очень простое, и это все, что они делают . Разрешение их наследования и, таким образом, бесконечное их возможное влияние приведет к большому удивлению в лучшем случае и фатальному нарушению LSP в худшем случае. Если я могу с оптимизмом предположить, что Фалес Перейра не возражает, я приведу этот феноменальный комментарий:

Существует еще одна проблема: если бы кто-то смог унаследовать от Int, у вас был бы невинный код типа «int x = y + 2» (где Y - производный класс), который теперь записывает журнал в базу данных, открывает URL и как-то воскресить Элвиса. Предполагается, что примитивные типы безопасны и имеют более или менее гарантированное, четко определенное поведение.

Если кто-то видит примитивный тип на нормальном языке, он справедливо полагает, что он всегда просто сделает свою маленькую вещь, очень хорошо, без сюрпризов. Примитивные типы не имеют доступных объявлений классов, которые указывают, могут ли они быть унаследованы или нет, и имеют ли их методы переопределенные. Если бы они были, это было бы очень удивительно (и полностью нарушило бы обратную совместимость, но я знаю, что это обратный ответ на вопрос «почему X не был разработан с Y»).

... хотя, как указывал в ответ Mooing Duck , языки, допускающие перегрузку операторов, позволяют пользователю путать себя в равной или равной степени, если они действительно этого хотят, поэтому сомнительно, имеет ли место этот последний аргумент. И я перестану обобщать комментарии других людей, хе.

underscore_d
источник
4

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

Существует два основных языка ООП (управляемых, запускаемых на ВМ) с примитивами: C # и Java. Многие другие языки не имеют примитивов, во-первых, или используют аналогичные рассуждения для их разрешения / использования.

Примитивы - это компромисс для производительности. Для каждого объекта вам нужно место для заголовка объекта (в Java, как правило, 2 * 8 байт на 64-битных виртуальных машинах), плюс его поля и возможное заполнение (в Hotspot каждый объект занимает количество байтов, кратное 8). Таким образом, для intобъекта as потребуется как минимум 24 байта памяти, а не только 4 байта (в Java).

Таким образом, примитивные типы были добавлены для улучшения производительности. Они делают много вещей проще. Что a + bзначит, если оба являются подтипами int? Чтобы выбрать правильное дополнение, нужно добавить какой-то вид диспетчеризации. Это означает виртуальную отправку. Возможность использовать очень простой код операции для добавления намного, намного быстрее и позволяет оптимизировать время компиляции.

Stringэто другой случай. И в Java, и в C # Stringэто объект. Но в C # он запечатан, а в Java - окончательный. Это потому, что стандартные библиотеки Java и C # требуют, Stringчтобы переменные были неизменяемыми, и их наследование нарушило бы эту неизменность.

В случае Java, виртуальная машина может (и делает) интернировать строки и «объединяет» их, обеспечивая лучшую производительность. Это работает только тогда, когда строки действительно неизменны.

Кроме того, редко нужно создавать подклассы примитивных типов. Пока примитивы не могут быть разделены на подклассы, есть много интересных вещей, которые математика говорит нам о них. Например, мы можем быть уверены, что сложение коммутативно и ассоциативно. Это то, что математическое определение целых чисел говорит нам. Кроме того, во многих случаях мы можем легко преобразовать инварианты в циклы посредством индукции. Если мы разрешаем создавать подклассы int, мы теряем те инструменты, которые дает нам математика, потому что мы больше не можем гарантировать выполнение определенных свойств. Таким образом, я бы сказал, что способность не иметь возможности подклассировать примитивные типы на самом деле хорошая вещь. Меньше вещей, которые кто-то может сломать, плюс компилятор часто может доказать, что ему разрешено делать определенные оптимизации.

Polygnome
источник
1
Этот ответ лишен ... узок. to allow inheritance, one needs runtime type information.Ложь. For every object, some data regarding the type of the object has to be stored.Ложь. There are two mainstream OOP languages that feature primitives: C# and Java.Что, C ++ сейчас не мейнстрим? Я буду использовать его в качестве опровержения, поскольку информация о типе среды выполнения - это термин C ++. Это абсолютно не требуется, если вы не используете dynamic_castили typeid. И даже если RTTI включен, наследование занимает место только в том случае, если у класса есть virtualметоды, на которые должна быть указана таблица методов для класса в каждом экземпляре
underscore_d
1
Наследование в C ++ работает совсем не так, как в языках, работающих на виртуальной машине. Виртуальная диспетчеризация требует RTTI, чего изначально не было в C ++. Наследование без виртуальной диспетчеризации очень ограничено, и я даже не уверен, стоит ли сравнивать его с наследованием с виртуальной диспетчеризацией. Кроме того, понятие «объект» сильно отличается в C ++, чем в C # или Java. Вы правы, есть некоторые вещи, о которых я мог бы сказать лучше, но если разобраться во всех довольно сложных вопросах, это быстро приводит к необходимости написать книгу по языковому дизайну.
Полигном
3
Кроме того, это не тот случай, когда «виртуальная диспетчеризация требует RTTI» в C ++. Опять только dynamic_castи typeinfoтребую. Виртуальная диспетчеризация практически реализуется с использованием указателя на vtable для конкретного класса объекта, что позволяет вызывать нужные функции, но не требует детализации типа и отношения, присущих RTTI. Все, что нужно знать компилятору, это то, является ли класс объекта полиморфным и, если это так, что такое vptr экземпляра. Можно легко собрать виртуально разосланные классы -fno-rtti.
underscore_d
2
На самом деле, наоборот, RTTI требует виртуальной отправки. Буквально -C ++ не позволяет dynamic_castна классах без виртуальной отправки. Причина реализации заключается в том, что RTTI обычно реализуется как скрытый элемент vtable.
MSalters
1
@MilesRout C ++ имеет все, что нужно языку для ООП, по крайней мере, несколько более новые стандарты. Кто-то может возразить, что в старых стандартах C ++ отсутствуют некоторые вещи, которые необходимы для языка ООП, но даже это довольно сложно. C ++ не является языком ООП высокого уровня , так как он позволяет более прямо и низкоуровнево контролировать некоторые вещи, но, тем не менее, он позволяет ООП. (Высокий уровень / Низкий уровень здесь с точки зрения абстракции , другие языки, такие как управляемые, абстрагируют больше системы, чем C ++, поэтому их абстракция выше).
Полигном
4

В основных сильных статических языках ООП подтипирование рассматривается главным образом как способ расширения типа и переопределения текущих методов типа.

Для этого «объекты» содержат указатель на их тип. Это накладные расходы: код в методе, который использует Shapeэкземпляр, сначала должен получить доступ к информации о типе этого экземпляра, прежде чем он узнает правильный Area()метод для вызова.

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

Итак, ответ на:

Почему основные сильные статические ООП-языки препятствуют наследованию примитивов?

Является:

  • Там было мало спроса
  • И это сделало бы язык слишком медленным
  • Подтипирование в основном рассматривалось как способ расширения типа, а не как способ улучшить (определяемый пользователем) статическую проверку типов.

Тем не менее, мы начинаем получать языки, которые допускают статическую проверку на основе свойств переменных, отличных от 'type', например F # имеет «размерность» и «единицу измерения», так что вы не можете, например, добавить длину в область ,

Существуют также языки, которые допускают «определяемые пользователем типы», которые не изменяют (или не обменивают) то, что делает тип, а просто помогают со статической проверкой типов; см. ответ coredump.

Ян
источник
Единицы измерения F # - приятная особенность, хотя, к сожалению, неправильно. Кроме того, это только время компиляции, поэтому не является супер-полезным, например, при использовании скомпилированного пакета NuGet. Правильное направление, хотя.
День
Возможно, интересно отметить, что «измерение» - это не «свойство, отличное от« типа », это просто более богатый вид типа, чем вы привыкли.
porglezomp
3

Я не уверен, что пропускаю что-то здесь, но ответ довольно прост:

  1. Определение примитивов таково: примитивные значения не являются объектами, примитивные типы не являются типами объектов, примитивы не являются частью объектной системы.
  2. Наследование - это особенность объектной системы.
  3. Поэтому примитивы не могут участвовать в наследовании.

Обратите внимание, что на самом деле есть только два сильных статических языка ООП, которые даже имеют примитивы, AFAIK: Java и C ++. (На самом деле, я даже не уверен в последнем, я мало что знаю о C ++, и то, что я нашел, когда искал, сбивало с толку.)

В C ++ примитивы в основном являются наследством (предназначенным для каламбура) от C. Таким образом, они не участвуют в объектной системе (и, следовательно, в наследовании), потому что C не имеет ни объектной системы, ни наследования.

В Java примитивы являются результатом ошибочной попытки улучшить производительность. Примитивы также являются единственными типами значений в системе, фактически невозможно писать типы значений в Java и невозможно, чтобы объекты были типами значений. Таким образом, кроме того факта, что примитивы не участвуют в объектной системе и, следовательно, идея «наследования» даже не имеет смысла, даже если бы вы могли наследовать от них, вы не смогли бы поддерживать « значение Бытийность». Это отличается от , например , C♯ , который делает имеют значения типов ( structы), которые тем не менее являются объектами.

Другое дело, что неспособность наследовать на самом деле не уникальна и для примитивов. В C♯ structs неявно наследуют System.Objectи могут реализовывать interfaces, но они не могут ни наследоваться, ни наследоваться classes или structs. Кроме того, sealed classes не могут быть унаследованы от. В Java final classes не может быть унаследовано от.

тл; др :

Почему основные сильные статические ООП-языки препятствуют наследованию примитивов?

  1. примитивы не являются частью объектной системы (по определению, если бы они были, они не были бы примитивными), идея наследования связана с объектной системой, следовательно, примитивное наследование является противоречием в терминах
  2. примитивы не являются уникальными, многие другие типы также не могут быть унаследованы ( finalили sealedв Java или C♯, structв C♯, case classв Scala)
Йорг Миттаг
источник
3
Эмм ... Я знаю , что это произносится "C Sharp", но, EHM
Mr Lister
Я думаю, что вы довольно сильно ошибаетесь на стороне C ++. Это не чистый ОО язык вообще. Методы класса по умолчанию не являются virtual, что означает, что они не подчиняются LSP. Например std::string, не примитив, но он ведет себя как еще одна ценность. Такая семантика значений довольно распространена, вся часть STL в C ++ предполагает это.
MSalters
2
«В Java примитивы являются результатом ошибочной попытки повысить производительность». Я думаю, что вы понятия не имеете о величине снижения производительности реализации примитивов как типов объектов, расширяемых пользователем. Это решение в Java является и преднамеренным, и обоснованным. Просто представьте, что вам нужно выделить память для каждого, что intвы используете. Каждое выделение занимает порядка 100 нс плюс накладные расходы на сборку мусора. Сравните это с одним циклом ЦП, потребляемым добавлением двух примитивов int. Ваши java-коды будут ползти, если разработчики языка решат иначе.
Cmaster
1
@cmaster: у Scala нет примитивов, и его числовая производительность точно такая же, как у Java. Потому что, ну, он компилирует целые числа в примитивы JVM int, поэтому они работают точно так же. (Scala-native компилирует их в примитивные машинные регистры, Scala.js компилирует их в примитивные ECMAScript Number.) Ruby не имеет примитивов, но YARV и Rubinius компилируют целые числа в примитивные машинные, JRuby компилирует их в примитивы JVM long. Практически каждая реализация Lisp, Smalltalk или Ruby использует примитивы в виртуальной машине . Вот где оптимизации производительности ...
Йорг W Mittag
1
… Принадлежат: в компиляторе, а не в языке.
Йорг Миттаг,
2

Джошуа Блох в «Эффективной Java» рекомендует явно проектировать наследование или запрещать его. Примитивные классы не предназначены для наследования, потому что они разработаны так, чтобы быть неизменяемыми, и разрешение наследования может изменить это в подклассах, нарушая, таким образом, принцип Лискова, и это станет источником многих ошибок.

В любом случае, почему это хакерский обходной путь? Вы должны действительно предпочесть композицию наследству. Если причина в производительности, то у вас есть точка зрения, и ответ на ваш вопрос заключается в том, что невозможно включить все функции в Java, потому что требуется время для анализа всех различных аспектов добавления функции. Например, у Java не было Generics до 1.5.

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

CodesInTheDark
источник
2

На абстрактном уровне вы можете включить все, что захотите, в язык, который вы разрабатываете.

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

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

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

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

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

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

Теодорос Чатзигианнакис
источник
1

В общем

Если класс является абстрактным (метафора: блок с дырой (ями)), то можно (даже необходимо иметь что-то полезное!) «Заполнить дыру (я)», поэтому мы подклассируем абстрактные классы.

Если класс конкретный (метафора: ящик заполнен), не стоит изменять существующий, потому что, если он полон, он полон. У нас нет места, чтобы добавить что-то большее внутри коробки, поэтому мы не должны создавать подклассы конкретных классов.

С примитивами

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

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

Обычно наследование - это не та семантика, которую вы хотите, потому что вы не можете заменить свой специальный тип там, где ожидается примитив. Если заимствовать из вашего примера, Quantity + Indexсемантически не имеет смысла, поэтому отношения наследования - это неправильные отношения.

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

Карл Билефельдт
источник