Почему это нормально и в основном ожидается:
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: да, я прочитал это, и упаковка - это хакерский обходной путь.
Ответы:
Я полагаю, вы думаете о таких языках, как Java и C #?
В этих языках примитивы (вроде
int
) являются компромиссом для производительности. Они не поддерживают все функции объектов, но работают быстрее и с меньшими накладными расходами.Чтобы объекты поддерживали наследование, каждый экземпляр должен «знать» во время выполнения, к какому классу он относится. В противном случае переопределенные методы не могут быть разрешены во время выполнения. Для объектов это означает, что данные экземпляра хранятся в памяти вместе с указателем на объект класса. Если такая информация также должна храниться вместе с примитивными значениями, требования к памяти будут увеличиваться. 16-битное целочисленное значение потребовало бы его 16-битного значения и дополнительно 32- или 64-битной памяти для указателя на его класс.
Помимо накладных расходов на память, вы также ожидаете, что сможете переопределять обычные операции над примитивами, такими как арифметические операторы. Без подтипов операторы like
+
могут быть скомпилированы в простую инструкцию машинного кода. Если бы он мог быть переопределен, вам нужно было бы разрешить методы во время выполнения, гораздо более дорогостоящая операция. (Возможно, вы знаете, что C # поддерживает перегрузку операторов, но это не одно и то же. Перегрузка операторов разрешается во время компиляции, поэтому штраф за время выполнения по умолчанию отсутствует.)Строки не являются примитивами, но они все еще «особенные» в том, как они представлены в памяти. Например, они «интернированы», что означает, что два одинаковых строковых литерала могут быть оптимизированы для одной и той же ссылки. Это было бы невозможно (или, по крайней мере, намного менее эффективно), если строковые экземпляры также должны отслеживать класс.
То, что вы описываете, безусловно, было бы полезно, но поддержка этого потребовала бы снижения производительности при каждом использовании примитивов и строк, даже если они не используют наследование.
Язык Smalltalk позволяет (я считаю) разрешать подклассы целых чисел. Но когда Java был спроектирован, Smalltalk считался слишком медленным, и издержки, связанные с тем, чтобы все было объектом, считались одной из главных причин. Java пожертвовала некоторой элегантностью и концептуальной чистотой, чтобы получить лучшую производительность.
источник
string
запечатан, потому что он разработан, чтобы вести себя неизменным. Если бы кто-то мог наследовать от строки, можно было бы создать изменяемые строки, что сделало бы его действительно подверженным ошибкам. Тонны кода, включая саму платформу .NET, опираются на строки, не имеющие побочных эффектов. Смотрите также здесь, говорит вам то же самое: quora.com/Why-String-class-in-C-is-a-sealed-classString
, отмеченная иfinal
в Java.string
в C♯ это просто песок? Нет, конечно, нет,string
в C♯ это то, что в спецификации C♯ сказано. Как это реализовано, не имеет значения. Нативная реализация C♯ будет реализовывать строки в виде байтовых массивов, реализация ECMAScript будет отображать их в ECMAScriptString
и т. Д.То, что предлагает какой-то язык, это не подклассы, а подтипы . Например, Ada позволяет вам создавать производные типы или подтипы . Раздел Ada Programming / Type System стоит прочитать, чтобы понять все детали. Вы можете ограничить диапазон значений, который вы хотите большую часть времени:
Вы можете использовать оба типа как целые, если вы конвертируете их явно. Также обратите внимание, что вы не можете использовать одно вместо другого, даже если диапазоны структурно эквивалентны (типы проверяются по именам).
Указанные типы несовместимы, даже если они представляют один и тот же диапазон значений.
(Но вы можете использовать Unchecked_Conversion; не говорите людям, что я вам это сказал)
источник
type Index is -MAXINT..MAXINT;
что-то, что как-то ничего не делает для меня, так как все целые числа будут действительными? Так какую же ошибку я получу, передав Угол в Индекс, если все, что проверяется, это диапазоны?Я думаю, что это вполне может быть вопросом X / Y. Существенные моменты, из вопроса ...
... и из вашего комментария уточняю:
Извините, если я что-то упустил, но ... Если это ваши цели, то почему на Земле вы говорите о наследовании? Неявная замещаемость ... как ... все это. Знаете, принцип подстановки Лискова?
В действительности вам кажется, что вам нужна концепция «сильной определения типа», при которой что-то «есть», например, с
int
точки зрения диапазона и представления, но не может быть заменено контекстами, которые ожидают «int
и», и наоборот. Я бы посоветовал поискать информацию по этому термину и какому бы языку он ни был. Опять же, это в значительной степени буквально противоположность наследования.И для тех, кому может не понравиться ответ X / Y, я думаю, что заголовок все еще может быть ответственным со ссылкой на LSP. Примитивные типы примитивны, потому что они делают что-то очень простое, и это все, что они делают . Разрешение их наследования и, таким образом, бесконечное их возможное влияние приведет к большому удивлению в лучшем случае и фатальному нарушению LSP в худшем случае. Если я могу с оптимизмом предположить, что Фалес Перейра не возражает, я приведу этот феноменальный комментарий:
Если кто-то видит примитивный тип на нормальном языке, он справедливо полагает, что он всегда просто сделает свою маленькую вещь, очень хорошо, без сюрпризов. Примитивные типы не имеют доступных объявлений классов, которые указывают, могут ли они быть унаследованы или нет, и имеют ли их методы переопределенные. Если бы они были, это было бы очень удивительно (и полностью нарушило бы обратную совместимость, но я знаю, что это обратный ответ на вопрос «почему X не был разработан с Y»).
... хотя, как указывал в ответ Mooing Duck , языки, допускающие перегрузку операторов, позволяют пользователю путать себя в равной или равной степени, если они действительно этого хотят, поэтому сомнительно, имеет ли место этот последний аргумент. И я перестану обобщать комментарии других людей, хе.
источник
Чтобы разрешить наследование с виртуальной диспетчеризацией 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
, мы теряем те инструменты, которые дает нам математика, потому что мы больше не можем гарантировать выполнение определенных свойств. Таким образом, я бы сказал, что способность не иметь возможности подклассировать примитивные типы на самом деле хорошая вещь. Меньше вещей, которые кто-то может сломать, плюс компилятор часто может доказать, что ему разрешено делать определенные оптимизации.источник
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
методы, на которые должна быть указана таблица методов для класса в каждом экземпляреdynamic_cast
иtypeinfo
требую. Виртуальная диспетчеризация практически реализуется с использованием указателя на vtable для конкретного класса объекта, что позволяет вызывать нужные функции, но не требует детализации типа и отношения, присущих RTTI. Все, что нужно знать компилятору, это то, является ли класс объекта полиморфным и, если это так, что такое vptr экземпляра. Можно легко собрать виртуально разосланные классы-fno-rtti
.dynamic_cast
на классах без виртуальной отправки. Причина реализации заключается в том, что RTTI обычно реализуется как скрытый элемент vtable.В основных сильных статических языках ООП подтипирование рассматривается главным образом как способ расширения типа и переопределения текущих методов типа.
Для этого «объекты» содержат указатель на их тип. Это накладные расходы: код в методе, который использует
Shape
экземпляр, сначала должен получить доступ к информации о типе этого экземпляра, прежде чем он узнает правильныйArea()
метод для вызова.Примитив, как правило, допускает только те операции с ним, которые могут переводиться в инструкции на одном машинном языке и не несут с собой никакой информации о типах. Делать целое число медленнее, чтобы кто-то мог разделить его на подклассы, было недостаточно привлекательно, чтобы не допустить превращения любых языков в мейнстрим.
Итак, ответ на:
Является:
Тем не менее, мы начинаем получать языки, которые допускают статическую проверку на основе свойств переменных, отличных от 'type', например F # имеет «размерность» и «единицу измерения», так что вы не можете, например, добавить длину в область ,
Существуют также языки, которые допускают «определяемые пользователем типы», которые не изменяют (или не обменивают) то, что делает тип, а просто помогают со статической проверкой типов; см. ответ coredump.
источник
Я не уверен, что пропускаю что-то здесь, но ответ довольно прост:
Обратите внимание, что на самом деле есть только два сильных статических языка ООП, которые даже имеют примитивы, AFAIK: Java и C ++. (На самом деле, я даже не уверен в последнем, я мало что знаю о C ++, и то, что я нашел, когда искал, сбивало с толку.)
В C ++ примитивы в основном являются наследством (предназначенным для каламбура) от C. Таким образом, они не участвуют в объектной системе (и, следовательно, в наследовании), потому что C не имеет ни объектной системы, ни наследования.
В Java примитивы являются результатом ошибочной попытки улучшить производительность. Примитивы также являются единственными типами значений в системе, фактически невозможно писать типы значений в Java и невозможно, чтобы объекты были типами значений. Таким образом, кроме того факта, что примитивы не участвуют в объектной системе и, следовательно, идея «наследования» даже не имеет смысла, даже если бы вы могли наследовать от них, вы не смогли бы поддерживать « значение Бытийность». Это отличается от , например , C♯ , который делает имеют значения типов (
struct
ы), которые тем не менее являются объектами.Другое дело, что неспособность наследовать на самом деле не уникальна и для примитивов. В C♯
struct
s неявно наследуютSystem.Object
и могут реализовыватьinterface
s, но они не могут ни наследоваться, ни наследоватьсяclass
es илиstruct
s. Кроме того,sealed
class
es не могут быть унаследованы от. В Javafinal
class
es не может быть унаследовано от.тл; др :
final
илиsealed
в Java или C♯,struct
в C♯,case class
в Scala)источник
virtual
, что означает, что они не подчиняются LSP. Напримерstd::string
, не примитив, но он ведет себя как еще одна ценность. Такая семантика значений довольно распространена, вся часть STL в C ++ предполагает это.int
вы используете. Каждое выделение занимает порядка 100 нс плюс накладные расходы на сборку мусора. Сравните это с одним циклом ЦП, потребляемым добавлением двух примитивовint
. Ваши java-коды будут ползти, если разработчики языка решат иначе.int
, поэтому они работают точно так же. (Scala-native компилирует их в примитивные машинные регистры, Scala.js компилирует их в примитивные ECMAScriptNumber
.) Ruby не имеет примитивов, но YARV и Rubinius компилируют целые числа в примитивные машинные, JRuby компилирует их в примитивы JVMlong
. Практически каждая реализация Lisp, Smalltalk или Ruby использует примитивы в виртуальной машине . Вот где оптимизации производительности ...Джошуа Блох в «Эффективной Java» рекомендует явно проектировать наследование или запрещать его. Примитивные классы не предназначены для наследования, потому что они разработаны так, чтобы быть неизменяемыми, и разрешение наследования может изменить это в подклассах, нарушая, таким образом, принцип Лискова, и это станет источником многих ошибок.
В любом случае, почему это хакерский обходной путь? Вы должны действительно предпочесть композицию наследству. Если причина в производительности, то у вас есть точка зрения, и ответ на ваш вопрос заключается в том, что невозможно включить все функции в Java, потому что требуется время для анализа всех различных аспектов добавления функции. Например, у Java не было Generics до 1.5.
Если у вас много терпения, то вам повезло, потому что есть план добавить классы значений в Java, которые позволят вам создавать ваши классы значений, которые помогут вам повысить производительность и в то же время дадут вам больше гибкости.
источник
На абстрактном уровне вы можете включить все, что захотите, в язык, который вы разрабатываете.
На уровне реализации неизбежно, что некоторые из этих вещей будут проще для реализации, некоторые будут сложными, некоторые могут быть сделаны быстро, некоторые обязательно будут медленнее и так далее. Чтобы объяснить это, дизайнерам часто приходится принимать сложные решения и идти на компромиссы.
На уровне реализации одним из самых быстрых способов доступа к переменной является определение ее адреса и загрузка содержимого этого адреса. В большинстве процессоров есть специальные инструкции для загрузки данных с адресов, и эти инструкции обычно должны знать, сколько байтов им нужно загрузить (один, два, четыре, восемь и т. Д.) И куда поместить загружаемые данные (один регистр, регистр). пара, расширенный регистр, другая память и т. д.). Зная размер переменной, компилятор может точно знать, какую инструкцию использовать для использования этой переменной. Не зная размера переменной, компилятору придется прибегнуть к чему-то более сложному и, возможно, более медленному.
На абстрактном уровне смыслом подтипирования является возможность использовать экземпляры одного типа, где ожидается одинаковый или более общий тип. Другими словами, может быть написан код, который ожидает объект определенного типа или что-то более производное, не зная заранее, что именно это будет. И, разумеется, поскольку большее количество производных типов может добавлять больше элементов данных, производный тип не обязательно имеет те же требования к памяти, что и его базовые типы.
На уровне реализации нет простого способа для переменной заранее определенного размера, чтобы хранить экземпляр неизвестного размера и быть доступным способом, который вы обычно называете эффективным. Но есть способ немного изменить положение вещей и использовать переменную не для хранения объекта, а для идентификации объекта и сохранения этого объекта в другом месте. Таким способом является ссылка (например, адрес памяти) - дополнительный уровень косвенности, который гарантирует, что переменная должна содержать только некоторую информацию фиксированного размера, при условии, что мы можем найти объект через эту информацию. Чтобы достичь этого, нам просто нужно загрузить адрес (фиксированного размера), а затем мы можем работать как обычно, используя те смещения объекта, которые, как мы знаем, действительны, даже если этот объект имеет больше данных с смещениями, которые мы не знаем. Мы можем сделать это, потому что мы не
На абстрактном уровне этот метод позволяет сохранять (ссылку на a)
string
вobject
переменной, не теряя информацию, которая делает ее astring
. Это нормально для всех типов, и вы также можете сказать, что это элегантно во многих отношениях.Тем не менее, на уровне реализации дополнительный уровень косвенности включает в себя больше инструкций и на большинстве архитектур делает каждый доступ к объекту несколько медленнее. Вы можете позволить компилятору выжать из программы больше производительности, если вы включите в свой язык некоторые часто используемые типы, которые не имеют такого дополнительного уровня косвенности (ссылка). Но, удалив этот уровень косвенности, компилятор не может позволить вам выполнять подтипы безопасным способом памяти. Это связано с тем, что если вы добавите больше элементов данных к своему типу и назначите более общий тип, любые дополнительные элементы данных, которые не помещаются в пространство, выделенное для целевой переменной, будут вырезаны.
источник
В общем
Если класс является абстрактным (метафора: блок с дырой (ями)), то можно (даже необходимо иметь что-то полезное!) «Заполнить дыру (я)», поэтому мы подклассируем абстрактные классы.
Если класс конкретный (метафора: ящик заполнен), не стоит изменять существующий, потому что, если он полон, он полон. У нас нет места, чтобы добавить что-то большее внутри коробки, поэтому мы не должны создавать подклассы конкретных классов.
С примитивами
Примитивы - это конкретные классы по дизайну. Они представляют собой нечто хорошо известное, полностью определенное (я никогда не видел примитивный тип с чем-то абстрактным, иначе это уже не примитив) и широко используемое в системе. Позволяя создавать подклассы примитивного типа и предоставлять свою собственную реализацию другим, которые полагаются на разработанное поведение примитивов, могут вызвать много побочных эффектов и огромных убытков!
источник
Обычно наследование - это не та семантика, которую вы хотите, потому что вы не можете заменить свой специальный тип там, где ожидается примитив. Если заимствовать из вашего примера,
Quantity + Index
семантически не имеет смысла, поэтому отношения наследования - это неправильные отношения.Однако в нескольких языках есть концепция типа значения , который выражает тип отношений, которые вы описываете. Скала является одним из примеров. Тип значения использует примитив в качестве базового представления, но имеет другой идентификатор класса и операции снаружи. Это имеет эффект расширения примитивного типа, но это скорее композиция вместо отношения наследования.
источник