Pull to refresh

Comments 97

Агрегация – это когда экземпляр двигателя создается где-то в другом месте кода, и передается в конструктор автомобиля в качестве параметра.

А откуда вы взяли это магическое определение?

В объектно-ориентированных языках программирования существует три способа организации взаимодействия между классами. [...] Наследование описывается словом «является».

А почему никак не учтены интерфейсы? Потому что между интерфейсом и реализующим его классом тоже отношение "is a" (то, что вы называете "является"), но называть это наследованием не стоило бы.

наследованием не стоило бы

Интерфейсы совершенно к ООП не относятся (ну то есть ООП возможно и без интерфейсов), хотя присутствуют в ООП языках.

В этом смысле и классы к ООП не относятся (ООП возможно и без классов). Почему классы рассматриваем, а интерфейсы — нет?

классы к ООП

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

Ну так и наследование для ООП не обязательно.

Ну так и наследование для ООП не обязательно

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

(Аргумент истинного шотландца?)


Вы про Go не слышали?


Или, вот, скажем, JS тоже вполне себе ОО-язык, а классов там не было (я, если честно, не помню, завезли ли).


Зачем конкретно, по-вашему, нужно наследование ОО-языку?

Вы про Go не слышали?

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

JS тоже вполне себе ОО-язы

Ну так давно уже есть наследование, погуглите JavaScript extends. Ну вот первая ссылка developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Classes/extends
Тут другой вопрос, является ли Go — ООПшным языком.

Я же говорю: аргумент истинного шотландца.


Ну так давно уже есть наследование, погуглите JavaScript extends

Наследование там было намного раньше. Классов не было.


Но: так зачем же ОО-языку наследование?

Классов не было.

Вот вам пример класса, которого «не было» на чистом JS (в функциональном стиле):

function Animal(name) {
  this.speed = 0;
  this.name = name;

  this.run = function(speed) {
    this.speed += speed;
    alert( this.name + ' бежит, скорость ' + this.speed );
  };

  this.stop = function() {
    this.speed = 0;
    alert( this.name + ' стоит' );
  };
};

var animal = new Animal('Зверь');

Так это и не класс, это фабричная функция.

да нет, это определение и создание «прототипоориентированного объекта» prototype-based object. Которое в последствии получило синтаксический сахар в виде ключевого слова class.

… я и говорю: это не класс, это создание объекта.

что вы понимаете под словом «класс»? я лично — определенную конструкцию для описания какого-либо типа. Из которого можно потом создать объект.
я и говорю: это не класс

Ну т.е. класс из нового JS вы за класс и не считаете.
UFO just landed and posted this here
Это понятно, что JS это прототипы. А класс — это просто способ описания.
Ну так что вы понимаете под словом «класс».

.
Это просто синтаксический сахар над прототипным наследованием

И почитайте, что я писал две ветки назад
Которое в последствии получило синтаксический сахар в виде ключевого слова class.

UFO just landed and posted this here
Вот, дошли. Я понимаю класс как
«Класс — это описание пользовательского типа данных»
Все.
Далее пошли подуровни:
1) Классы в ОО типа C++/Java
2) Классы у прототипно ориентированных языков (например описанные для JS ES2015 Classes)
3) Мультиметоды у других языков.
4) и т.д. и т.п.
Видно, из-за этого и вся путаница, что я не правильно писал «класс» вместо «описание объекта».
что вы понимаете под словом «класс»? я лично — определенную конструкцию для описания какого-либо типа.

Вот только там, что вы показали, никакого типа нет. Есть объект.

Да, я неправильно писал «класс», а нужно было «описание объекта».
Признаюсь — неправильная терминология у меня.

Ура. Значит, в JS (ну, старом) классов нет. JS — ОО-язык?

Я до этого момента, думал, что спор у нас из-за того что в JS нет инструментов описания/создания объектов. Неправильно интерпретировал слово «класс».
Да, классов пришедших из SmallTalk нет.

Значит, классы не обязательны для ООП. Что возвращает нас к вопросу: почему классы в статье рассматриваются, а интерфейсы — хотя во многих случаях они лучше подходят для решения задач, описанных в статье — нет?

классы не обязательны для ООП

Еще раз. Я интерпретировал, что «описание/создание объектов» не обязательны для ООП. Поэтому у нас и возникла дискуссия.

почему классы в статье рассматриваются, а интерфейсы — хотя во многих случаях они лучше подходят для решения задач

Вам не кажется, что лучше этот вопрос задать автору статьи?
Вам не кажется, что лучше этот вопрос задать автору статьи?

Ну так с моего комментария с этим вопросом автору статьи (на который вы решили ответить) эта дискуссия и началась.

Значит, в JS (ну, старом) классов нет. JS — ОО-язык

Отсутствие наследования и прочего разрешено в ООП, но тогда его полноценным не назовешь.

Со мной даже википедия согласна, что бывают недо «оопешные языки»

en.wikipedia.org/wiki/List_of_object-oriented_programming_languages
For example, C++ is a multi-paradigm language including object-oriented paradigm;[2] however, it is less object-oriented than some other languages such as Python[3] and Ruby.[4] Therefore, someone considers C++ as an OOP language, while others do not or prefer to name it as «semi-object-oriented programming language».

Так что деление «полу/недо оопешный язык» имеет место быть.
Отсутствие наследования и прочего разрешено в ООП, но тогда его полноценным не назовешь.

И вот теперь мы приходим к вопросу определения "полноценного ООП".

так зачем же ОО-языку наследование?

Еще раз, если вам не нужно — не пользуйтесь. Я вас уверяю, даже если использовать процедурный стиль — написать можно все что угодно, чисто теоретически даже с goto, можно все засунуть в один метод.

Что мне нужно, я как-нибудь разберусь. Но это же вы утверждаете, что ОО-языку необходимо наследование, вот я и спрашиваю: зачем?

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

Говорят, что для "сборки систем" лучше подходит композиция. Врут, наверное?


Хорошо, поставим вопрос иначе: если в языке нет наследования — он не ОО? Если да, то почему?

«Говорят, что для „сборки систем“ лучше подходит композиция. Врут, наверное?»
Ну каждый решает сам, что ему удобнее

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

Тут другой вопрос, является ли Go — ООПшным языком.


Я же говорю: аргумент истинного шотландца.


Ну так сами разработчики «сомневаются»

golang.org/doc/faq#Is_Go_an_object-oriented_language

Is Go an object-oriented language?

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy.

Но еще раз — я на эту тему холиварить не хочу.

… я, кстати, могу с тем же успехом сказать, что никто в здравом уме не будет реализовывать современный ОО-язык без интерфейсов. Неправда, конечно, но дальше же можно поспорить за "современный" и "здравый ум".

Вот тут не соглашусь — тот же JS, интерфейсы есть в typescript — но ИМХО — совершенно не нужная часть (В отличие от C# например)

… а в JS уже завезли статическую типизацию, чтобы ему интерфейсы были нужны?

Какая статическая типизация в JS? Нет там ее и не планируется, я писал о другом языке — typescript.

Тогда не приводите JS в пример. Возьмем, значит, TS. Там множественное наследование есть? Вроде как не было.

Причем тут множественное наследование? Мы говорим об интерфейсах.

При том, что есть прямая связь между наличием или отсутствием интерфейсов как отдельной сущности и наличием или отсутствием множественного наследования. Понимаете, какая?

Скажем так, в языках где есть множественного наследование, интерфейсы необязательны, хотя могут присутствовать.
В typescript, из-за его возможности динамической типизации (и отсутствия по понятным причинам проверки типов в рантайме), интерфейсы не обязательны. Можно прям в рантайме подставлять нужный тип (с некоторыми манипуляциями) и работать.

Вот вам и ответ: там нет интерфейсов, потому что там есть динамическая типизация. А теперь попробуйте добиться того же эффекта в языке со статической типизацией и без множественного наследования (привет, C#).

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

В данном случае «интерфейс» можно представить как класс без единой строчки кода (одни декларации функций) и protected конструктор. Это фактически народ писал руками на С++ 90х годов :)
В данном случае интерфейс будет как «синтаксический сахар» для базового класса, который декларирует некоторый функционал.
Тут lair намекал, что интерфейсы это компромисс при отсутствии множественного наследования.

Вот как раз в typescript это самая интересная часть. Без них не получится правильно типизировать библиотеки которые поставляются по принципу «ядро + плагины». Например, jquery или rx.
Без них не получится правильно типизировать библиотеки

Если бы библиотеки были бы переписаны на typescript, то интерфейсы не нужны были, а так это просто связующее звено между легаси.

Ну-ну, покажите как вы будете это делать. Вот представьте, что вы пишите аналог jquery на typescript и вам нужно добавить метод attr.


Ну и как это делать без интерфейсов, но с сохранением выбранной архитектуры (ядро + плагины)?

но с сохранением выбранной архитектуры

Вы мне предлагаете с сохранением архитектуры построенной на интерфейсах переделать на такую же без интерфейсов? Это абсурдное требование. Очевидно, что система без интерфейсов будет выглядеть совершенно по другому, но полезную работу будет выполнять одну и туже.
А не приведете ли Вы, как архитектор, четкое и понятное определение ООП (которого лично Вы придерживаетесь). Дабы не было разночтений и бестоковых споров.

Я не придерживаюсь никакого четкого определения ООП, потому что я его пока не встретил. Мне ближе всего позиция Кея: "OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things".

Спасибо. Жаль, ибо четкой и предметной дискуссии не получится (ибо есть только «плавающее определение» что же такое ООП). Ок :)
Здесь мы используем одно из чудес, которые дает полиморфизм. Метод принимает любой снаряд. В сигнатуре указан базовый класс, а не дочерние. Но внутри метода, мы можем увидеть, что за снаряд прилетел – какого типа. И в зависимости от этого, реализуем ту или иную логику.

Типичный такой ад с нарушением инкапсуляции. А вот представьте, что у вас появился четвертый и пятый вид снарядов — что случится во всех классах брони?


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

передавали ему необходимые параметры

Передача параметров это не есть разве взаимодействие? Или вы тупо про инициализацию, тогда причем тут полиморфизм?
Передача параметров это не есть разве взаимодействие?

Передавали параметры снаряду, не зная его тип. Полиморфизм (тот, который subtyping) — он именно про это.

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

Это называется "инкапсуляция".


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

А это называется subtyping polymorphism и не предполагает, что вы потом делаете if по типу.


Я, собственно, в самом начале спросил: вы понимаете, чем плох такой подход?

, что вы потом делаете if по типу.

Такой код у меня 100% не пройдет ревью на работе.

Извините, случайно перепутал вас с автором поста.

три способа организации взаимодействия между классами

А вот шаблон проектирования Фабрика например, это какой способ организации?
Конечно их больше, а ваш код следует улучшать дальше, к примеру классы брони — обычная копипаста, добавление новой брони усложнено, а при изменении интерфейса класса нужно делать много работы. Эта проблема может быть решена как мета-программированием, так и в рамках ООП.
Для меня ключевое понимание наследования и отличие его от композиции пришло после понимания механизма их работы. Приведу пример. Допустим у нас есть класс DBConnection объект которого представляет собой соединение с базой данных и базовые операции работы с базой. Применяя композицию обычно создают отдельный класс Repository который представляет собой crud-операции с базой, который в конструкторе создает объект соединения и сохраняет его каком-то поле и использует потом его для взаимодействия c базой данных в crud-методах. А вот применяя наследование вместо композиции класс Repository отнаследуется от DBConnection и добавит нужный код работы с crud. И здесь принципиальное отличие — в случае композиции при создании объекта Repository будет создано два объекта в рантайме (сам Repository и объект DBConnection) а применяя наследование — только один объект. А в случае если у нас будет цепочка из 10 различных сущностей которые что-то добавляют и переопределяют то с композицией это будет уже 10 объектов а с наследованием только один вне зависимости от длины этой цепочки (да хоть тысячу сущностей). В этом и суть наследования — оно позволяет вынести в compile-time декораторную логику экономя cpu-циклы и память
UFO just landed and posted this here
A «owns» B = Composition: B has no meaning or purpose in the system without A
A «uses» B = Aggregation: B exists independently (conceptually) from A
насколько это всё не нужные ограничения, показывает следующий пример стирания типов без вредных эффектов.
Пусть код должен поддерживать разные виды танков бронетехники, колёсные и гусеничные, с разным количеством пушек, моторов и т.п.
пусть просто класс Tank имеет Map<TypeID, List>, не Object, чтобы туда дураки не кидали любые обьекты. TankPart имеет integer или enum поле TypeID уникальную для каждого типа или может быть заменено на Class смотря что в каком языке есть. При добавлении новой детали проводится довольно простая и очевидная операция:
map.get(part.typeId).add(part).
Интересно, особо внимательные заметили что описанное выше — питоний стиль??
с танком можно тогда делать более умные вещи такие как приказать стрелять из всех, либо из определённого числа орудий, либо разделить орудия в этой Map по более мелким классам, либо одни и те же детали добавить в разные классы чтоб вызывать для разных дел, причём это всё можно динамически менять с малым оверхедом на содержание карты.

… и как бишь в этой конструкции определить, что именно умеет конкретный TankPart — стрелять или ездить?

по typeID. Если не хочешь запоминать соответствие число -> что часть умеет, есть старый но проверенный способ это битовые флаги

Т.е. потребитель должен знать список всех возможных TypeID, а поставщик — не забывать его обновлять, а если не дай бог поведение поменялось, то отследить это можно только тестами?

В общем случае — да.
Не найдет ожидаемого в 'Run time' — сгенерится ошибка типа «NULL Reference exception» / «Application Exception» и т.д. в зависмости от архитектуры

Ну и зачем нам такое счастье без особых на него причин?

Банально — экономия ресурсов, поскольку в RunTime ничего не проверяется.
Все проверяется на этапе компиляции статическими анализаторами (в лучшем случае).
Например, COM с такими спецэффектами работал, если руками править IDL не понимая всех последствий :)
Если еще древнее и нагляднее — динамическая загрузка DLL и ручной поиск указателей на функции :) В Pascal overlay механизм.
Это сейчас ресурсов дофига — и памяти и процессора, а раньше это был дефицит. Выравнивали побайтово, что бы поменьше места занимало.
Банально — экономия ресурсов, поскольку в RunTime ничего не проверяется.

Вы думаете, при вызове несуществующего метода ничего не проверяется? Не говоря уже о том, что в C# — о котором идет речь в статье — это немножко невозможно без дополнительных прыжков?

Вы думаете, при вызове несуществующего метода ничего не проверяется?

Я не думаю, я знаю. При не корректном указателе — будет попытка исполнения не корректной операции процессора или обращения по некорректному указателю.
Дальше ОС отловит ошибку и прибьет процесс. Насмотрелся :)
Например: исполнить код в области данных. Сейчас такие операции контролируются на уровне ОС).

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

Вопрос был другой и С# не касался.
А насчет С# есть стандарт:
New types—value types and reference types—are introduced into the CTS via type declarations expressed in metadata. In addition, metadata is a structured way to represent all information that the CLI uses to locate and load classes, lay out instances in memory, resolve method invocations, translate CIL to native code, enforce security, and set up runtime context boundaries.
Например: исполнить код в области данных. Сейчас такие операции контролируются на уровне ОС

Это ваше "контролируется на уровне ОС" — это не проверка, по-вашему?


Вопрос был другой и С# не касался.
Да нет, вопрос был в контексте поста, а пост — про C#.
Это ваше «контролируется на уровне ОС» — это не проверка, по-вашему?

Это все таки не зависит от языка или среды исполнения. Одинаково будет «ловить ошибки» что на С++ что на Java или .Net

К примеру:
Overview of the Protected Mode Operation of the Intel Architecture

If we look back at the segment descriptor you will see information in the descriptor that relates to more than just its base address in memory (Figure 2 & Table 1). The additional information provided is primarily for the implementation of a protected system:
• How programs can access different types of segments,
• ensuring accesses within the limits of the segment (limit checking),
• maintaining privilege levels or who has access to a segment,
• and controlling access to privileged instructions.


Возвращаясь к С#
Т.е. потребитель должен знать список всех возможных TypeID, а поставщик — не забывать его обновлять, а если не дай бог поведение поменялось, то отследить это можно только тестами?

Следует ответ «Да должен знать всегда», поскольку стандарт говорит следующее:
Signatures are the part of a contract that can be checked and automatically enforced. Signatures are formed by adding constraints to types and other signatures. A constraint is a limitation on the use of or allowed operations on a value or location. Example constraints would be whether a location can be overwritten with a different value or whether a value can ever be changed.
All locations have signatures, as do all values.

Type safety and verification
Since types specify contracts, it is important to know whether a given implementation lives up to these contracts. An implementation that lives up to the enforceable part of the contract (the named signatures) is said
to be type-safe. An important part of the contract deals with restrictions on the visibility and accessibility of named items as well as the mapping of names to implementations and locations in memory.
Type-safe implementations only store values described by a type signature in a location that is assignment-compatible (§8.7) with the location signature of the location (see §8.6.1).
Type-safe implementations never apply an operation to a value that is not defined by the exact type of the value. Type-safe implementations only access locations that are both visible and accessible to them. In a type-safe implementation, the exact type of a value cannot change.
Verification is a mechanical process of examining an implementation and asserting that it is type-safe.
Verification is said to succeed if the process proves that an implementation is type-safe. Verification is said to fail if that process does not prove the type safety of an implementation. Verification is necessarily conservative:
it can report failure for a type-safe implementation, but it never reports success for an implementation that is not type-safe.
For example, most verification processes report implementations that do pointer-based arithmetic as failing verification, even if the implementation is, in fact, type-safe.

Это все таки не зависит от языка или среды исполнения.

ОС — это "среда исполнения". Так что зависит. И даже процессор — это "среда исполнения".


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


Следует ответ «Да должен знать всегда», поскольку стандарт говорит следующее:

А какое отношение этот стандарт имеет к самописным идентификаторам?


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

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

В общем проверка бесплатна, поскольку аппаратная и уже есть.
Срабатывает один раз, а не постоянно.
Скажем так — это неизбежное «константное» зло :) Скорее всего, обработка ошибок такого типа будет значительно дороже.

А какое отношение этот стандарт имеет к самописным идентификаторам?

Это в общем-то идентификаторы типов .Net.
И да, они самописны… компилятором :)

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

И тут Вы правы, ибо этот же стандарт отдает реализацию на усмотрение разработчика :)


The choice of a particular verification process is thus a matter of engineering, based on the resources available to make the decision and the importance of detecting the type safety of different programming constructs.

Например:
When a class is loaded at runtime, the CLI loader imports the metadata into its own in-memory data structures, which can be browsed via the CLI Reflection services. The Reflection services should be considered as similar to a compiler; they automatically walk the inheritance hierarchy to obtain information about inherited methods and fields, they have rules about hiding by name or name-and-signature, rules about inheritance of methods and
properties, and so forth.
В общем проверка бесплатна, поскольку аппаратная и уже есть.

Эээ… нет же. Не бесплатна.


Срабатывает один раз, а не постоянно.

На каждом вызове же.


Это в общем-то идентификаторы типов .Net.

То, что предлагают в комментарии в начале треда? Нет, это не они.


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

В общем проверка бесплатна, поскольку аппаратная и уже есть.

Эээ… нет же. Не бесплатна.

Транзисторы есть и с этой точки зрения да «уплочено». Влияет ли это на латентность или пропускную способность памяти с учетом того, что все эти расчеты делаются на уровне стека и аккумулятора процессора?
Срабатывает один раз, а не постоянно

На каждом вызове же.

Если на первом вызове выяснится, что не корректный адрес функции и приложение попробует вызвать эту функцию, то приложение будет «ломиться до посинения»? Или ОС просто прибьет не корректное приложение и на этом все закончится?
Да и после формирования пространства процесса (выделение и распределения памяти, загрузки кода и данных) ОС не занимается перетасовкой адресов функций «потому что скучно», это ответсвенность самого процесса, в рамках предоставленых прав «сегмент кода»/«сегмент данных».
Если, конечно не задаться целью наваять такое «забавное» приложение.
Влияет ли это на латентность или пропускную способность памяти с учетом того, что все эти расчеты делаются на уровне стека и аккумулятора процессора?

По сравнению с отсутствием этих расчетов — конечно, влияет.


Если на первом вызове выяснится, что не корректный адрес функции и приложение попробует вызвать эту функцию, то приложение будет «ломиться до посинения»? Или ОС просто прибьет не корректное приложение и на этом все закончится?

А если адрес корректный, сколько раз будет выполняться проверка?


Давайте с другой стороны посмотрим. Вот исходное предложение:


Пусть код должен поддерживать разные виды танков бронетехники, колёсные и гусеничные, с разным количеством пушек, моторов и т.п.
пусть просто класс Tank имеет Map<TypeID, List>, не Object, чтобы туда дураки не кидали любые обьекты. TankPart имеет integer или enum поле TypeID уникальную для каждого типа

Очевидно, что каждый TankPart имеет разные операции (пушка — стреляет, фара — светит, мотор — крутится, и так далее). Предположим, что нам надо сказать "включить все фары". Как это сделать в рамках исходного предложения, и чем это выгоднее стандартного ICollection<Light>? Напомню, что контекст — C#, .net.

Влияет ли это на латентность или пропускную способность памяти с учетом того, что все эти расчеты делаются на уровне стека и аккумулятора процессора?

По сравнению с отсутствием этих расчетов — конечно, влияет.

Да в общем-то нет. Как было X операций в N единиц времени, так и будет. Эти битовые маски расчитываются и контролируются аппаратно процессором в процессе исполнения комманд.

Например согласно "“Protection” руководства “Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A & 3B):System Programming Guide”

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

Следующий список дает примеры типичных операций в которых выполняется проверка типов (список не исчерпывающий):

В процессе определенных внутренних операций,
При вызове или переходе через call gate (или при прерывании или обработчике исключения через trap или interrupt gate) процессор автоматически проверяет что сегментный дескриптор указанный в gate является кодовым сегментом.
Когда операнд инструкции содержит селектор сегмента
определенным инструкциям разрешен доступ к сегментам или шлюзам (gates) только определенного типа, например:
Дальнему вызову CALL или инструкции JMP разрешен доступ к сегментному дескриптору содержащему «conforming code segment», «nonconforming code segment», call gate, task gate или TSS.

и
Program-Error Exceptions
Процессор генерирует одно или более исключений при обнаружении программных ошибок в процессе выполнении приложения, кода операционной системы или executive. Архитектуры Intel64 и IA-32 определяют vector number для каждого processor-detectable exception.
Исключения подразделяются на faults, traps и aborts.

Программно-генерируемые исключения
Инструкции INTO, INT3 и BOUND позволяют программную генерацию исключений. Эти инструкции позволяют выполнение проверки условий в местах выполнения потока инструкций. Например INT 3 вызывает генерацию breakpoint exception.

Machine-Check Exceptions
Процессоры семейств P6 family и Pentium предоставляют внутренние и внешние machine-check механизмы для проверки операций внутреннего аппаратного чипа и транзакций шины. Эти механизмы — implementation dependent (непереносимы). Когда процессор обнаруживает machinecheck
ошибку, процессор сигнализирует об ошибке с помощью machine-check exception (vector 18) и возвращает код ошибки.


Что касается «Предположим, что нам надо сказать „включить все фары“. Как это сделать в рамках исходного предложения, и чем это выгоднее стандартного ICollection? Напомню, что контекст — C#, .net.»
Ответ прост — кто-то должен будет сделать эту работу по выяснению наличия возможности «включить».
Например:
if (unit is IOnOff)
{
((IOnOff)unit).OFF();
}
else
{
throw new Exception();
}

или
*p->Off();

Или это сделает программист руками, или ОС внутреними функциями или CPU аппаратными возможостями. Просто обработка ошибок более дорогое удовольствие, чем нормальны код.
*p->Off();

Я что-то не думаю, что в C# можно так написать. Фиг с ним, с указателем, но вам же надо знать, что такое Off, а вы этого не сделаете без операции приведения типа.


Так что ответа на мой вопрос я так и не вижу.


И это не говоря о том, что сравниваю я с вариантом ICollection<ITankLight>.ForEach(l => l.Off()), в котором как раз операций приведения нет.

Я что-то не думаю, что в C# можно так написать. Фиг с ним, с указателем, но вам же надо знать, что такое Off, а вы этого не сделаете без операции приведения типа.

Да ладно ?! :)
using System;

struct Point
{
public int x;
public int y;

public override string ToString() {
return "(" + x + "," + y + ")";
}
}

class Test
{
static void Main() {
Point point;
unsafe {
Point* p = &point;
p->x = 10;
p->y = 20;
Console.WriteLine(p->ToString());
}
}
}

в котором как раз операций приведения нет.

Проверка на усмотрение разрабочика и среды исполнения. Но, полагаю некоторые механизмы оптимизации должны быть. Глупо постоянно проверять неизменяемые вещи. Проще проверить на этапе компиляции и/или загрузки сборки.
Да ладно ?! :)

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


Проверка на усмотрение разрабочика и среды исполнения.

Зачем там какая-либо проверка, если там все типы явные?


Проще проверить на этапе компиляции и/или загрузки сборки.

Ну то есть как раз то, что делает статическая типизация: проверят на этапе компиляции, что метод есть у типа, а переменная — нужного типа.

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

Нет. В этом и нюанс.
Вы декларируете в runtime, то это указатель типа Х. Cреда исполения доблестно дергает функцию по адресу A, а там все что угодно. Хотя бы и функция с другой сигнатурой, приведет к не корректному заполнению стека вызова функции.
Типичные грабли цепочки приведения типов:
*typeX -> *void ->*typeY.
После дергаем *typeY->FnY() и получаем «грабли в лоб».
Проверка на усмотрение разрабочика и среды исполнения.

Зачем там какая-либо проверка, если там все типы явные?

Типовые проблемы в runtime: не корректный вычисленный адрес или размер данных, адрес функции.
Например переполнение стека, выход за границы массива, нулевой указатель.
Вы декларируете в runtime, то это указатель типа Х. Cреда исполения доблестно дергает функцию по адресу A, а там все что угодно.

Вы код-то на C# покажите, который это делает. Мне вот не удалось даже уговорить C# сделать указатель на интерфейс или класс.


(и это мы еще не трогаем того факта, что просто не надо использовать unsafe для таких вещей)


Типовые проблемы в runtime: не корректный вычисленный адрес или размер данных, адрес функции.
Например переполнение стека, выход за границы массива, нулевой указатель.

Эти "типовый проблемы" не имеют отношения к дискуссии (и все проверяются .net, btw).

Привет «грабли», которые отлично компилируются:
class TypeX
{
public void FnX()
{
return;
}
}

class TypeY
{
public void FnY()
{
return;
}
}

class Program
{
static void Main(string[] args)
{

TypeX instX = new TypeX();
instX.FnX();

((TypeY)((object)instX)).FnY();

}
}

А в результате отлично «грабли прилетают в лоб» в runtime: System.InvalidCastException: 'Unable to cast object of type 'ConsoleTest.TypeX' to type 'ConsoleTest.TypeY'.'

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


QED.

BTW, вызова метода у вас не получилось, у вас упало-то ощутимо раньше.

Исходя из декларации назначения механизмов защиты упоминаемых выше, их назначение в уменьшение ущерба в случае человеческих ошибок, намеренных действий или аппартных ошибок.
Стратегия строится на как можно раннем этапе предотвращения ошибок такими методами:
  • язык разработки
  • инструменты анализа
  • внедрение «защитного» кода на этапе компиляции (example: Check for arithmetic overflow/underflow, Buffer Security Check, Run-Time Error Checks) & etc
  • аппаратные, как последняя линия обороны
в стандартном решении со статической типизацией.

Это превентивный мехнизм, но он не помогает в процессе исполнения. В Runtime приходится применять другие механизмы. .Net предлагает свои механизмы, Java свои. А С++ например практически таких механизмов не предлагает и полагается в этом вопросе на разработчика.
BTW, вызова метода у вас не получилось, у вас упало-то ощутимо раньше.

Да. Это сработал один из защитных механизмов .Net в Runtime (проверил описание типов и цепочки наследования, согласно приведенным выше цитатам из стандартов для .Net). А на уровне спецификации языка «Object references are not blittable.» и соответсвующая ошибк компилятора: error CS0208: Cannot take the address of, get the size of, or declare a pointer to a managed type
Для С++ будет ошибка типа «Unhandled exception in BlaBlaBla.exe 0xC0000005 access violation» или «Unhandled exception at 0x7715A849 (ntdll.dll) in BlaBlaBla.exe: 0xC0000374: A heap has been corrupted (parameters: 0x77195910).» или еще черти что выпадет.
Сейчас с этим значительно лучше стало — средства разработки улучшились и «поумнели»
Исходя из декларации назначения механизмов защиты упоминаемых выше, их назначение в уменьшение ущерба в случае человеческих ошибок, намеренных действий или аппартных ошибок.

Эээ… там нет механизмов защиты, ровно наоборот. Если вы про исходный коммент, конечно, а не про что-то свое.


Это превентивный мехнизм, но он не помогает в процессе исполнения.

Он помогает уменьшить количество проверок на этапе исполнения.

Эээ… там нет механизмов защиты, ровно наоборот. Если вы про исходный коммент, конечно, а не про что-то свое.

Не вижу смысла оспаривать документацию по .Net CLI VM:

The verification algorithm shall attempt to associate a valid stack state with every CIL instruction. The stack state specifies the number of slots on the CIL stack at that point in the code and for each slot a required type that shall be present in that slot.

The verification algorithm shall simulate all possible control flow paths through the code and ensure that a valid stack state exists for every reachable CIL instruction.

Verification simulates the operation of each CIL instruction to compute the new stack state, and any type mismatch between the specified conditions on the stack state and the simulated stack state shall cause the verification algorithm to fail.

The VES ensures that both special constraints and type constraints are satisfied. The constraints can be checked
as early as when a closed type is constructed, or as late as when a method on the constrained generic type is invoked, a constrained generic method is invoked, a field in a constrained generic type is accessed, or an instance of a constrained generic type is created.

A CIL instruction can throw a range of exceptions. The CLI can also throw the general purpose exception called ExecutionEngineException.


и т.д. Там много такого люботыного.

Например открываем документацию для С++ и читаем:
Buffer Security Check

Security Checks

On functions that the compiler recognizes as subject to buffer overrun problems, the compiler allocates space on the stack before the return address. On function entry, the allocated space is loaded with a security cookie that is computed once at module load. On function exit, and during frame unwinding on 64-bit operating systems, a helper function is called to make sure that the value of the cookie is still the same. A different value indicates that an overwrite of the stack may have occurred. If a different value is detected, the process is terminated.


если это все называется "… там нет механизмов защиты ..." боюсь тогда представить тогда что такое "… там есть механизмы защиты ..." :)

p.s.
Предлагаю пока закругляться с этим вопросом и возможно вернуться к нему позже :)
Не вижу смысла оспаривать документацию по .Net CLI VM:

Я же говорю: вы о чем-то своем. В исходном комментарии предлагается положить болт на статическую типизацию, которую приносит .net, и использовать самописный костыль с придуманным TypeId. И это, как мне кажется, никаких достоинств, кроме NIH, не имеет.

А зачем тогда придумали «To fully identify a type, the type name shall be qualified by the scope that includes the type name.»?
Опять же, зачем то сделали:
Type signatures
Type signatures define the constraints on a value and its usage. A type, by itself, is a valid type signature. The type signature of a value cannot be determined by examining the value or even by knowing the class type of the value. The type signature of a value is derived from the location signature (see below) of the location from which the value is loaded or from the operation that computes it. Normally the type signature of a value is the type in the location signature from which the value is loaded.

Не вижу проблем, при загрузке или компиляции генерить уникальные идентификаторы/хеши типов для быстрой проверки. Не парсить же описание типов каждый раз при каждом обращении.
Возможно Type.GUID Property уже служит для этих оптимизаций.
COM такой механизм опознавания типов использовал.
Это не новая идея и уже была реализована.
А зачем тогда придумали «To fully identify a type, the type name shall be qualified by the scope that includes the type name.»?

Не знаю.


Не вижу проблем, при загрузке или компиляции генерить уникальные идентификаторы/хеши типов для быстрой проверки. Не парсить же описание типов каждый раз при каждом обращении.
Возможно Type.GUID Property уже служит для этих оптимизаций.

Это все было бы круто, если бы потом не надо было делать каст, внутри которого .net все равно сделает проверку типа.

Не знаю.

Нашел таки описание в стандарте:
Externally, an assembly is a collection of exported resources, including types. Resources are exported by name.

The identity of a type is its assembly scope and its declared name. A type defined identically in two different assemblies is considered two different types.

New types—value types and reference types—are introduced into the CTS via type declarations expressed in metadata. In addition, metadata is a structured way to represent all information that the CLI uses to locate and load classes, lay out instances in memory, resolve method invocations, translate CIL to native code, enforce security, and set up runtime context boundaries. Every CLI PE/COFF module (see Partition II Metadata – File Format) carries a compact metadata binary
that is emitted into the module by the CLI-enabled development tool or compiler.

Each CLI component carries the metadata for declarations, implementations, and references
specific to that component. Therefore, the component-specific metadata is referred to as component metadata, and the resulting component is said to be self-describing. In object models such as COM or CORBA, this information is represented by a combination of typelibs, IDL files, DLLRegisterServer, and a myriad of custom files in disparate formats and separate from the actual executable file. In contrast, the metadata is a fundamental part of a CLI component.

When a class is loaded at runtime, the CLI loader imports the metadata into its own in-memory data structures, which can be browsed via the CLI Reflection services. The Reflection services should be considered as similar to a compiler; they automatically walk the inheritance hierarchy to obtain information about inherited methods and fields, they have rules about hiding by name or name-and-signature, rules about inheritance of methods and properties, and so forth.

A metadata token is an implementation-dependent encoding mechanism. Partition II describes the manner in which metadata tokens are embedded in various sections of a CLI PE/COFF module. Metadata tokens are embedded in CIL and native code to encode method invocations and field accesses at call sites; the token is used by various infrastructure services to retrieve information from metadata about the reference and the type on which it was scoped in order to resolve the reference.
A metadata token is a typed identifier of a metadata object (such as type declaration and member declaration). Given a token, its type can be determined and it is possible to retrieve the specific metadata attributes for that metadata object. However, a metadata token is not a persistent identifier. Rather it is scoped to a specific metadata binary. A metadata token is represented as an index into a metadata data structure, so access is fast and direct.



Глянул в 'WebAssembly Core Specification':
5. Binary Format
5.3. Types
5.3.1. Value Types

Value types are encoded by a single byte.
​valtype​::=​0x7F => i32
valtype​::=​0x7E => i64
valtype​::=​0x7D => f32
valtype​::=​0x7C => f64


Так что насчет "… должен знать типы… если поменялось то обнаружить только тестами..." и тут ответ похоже «да должен знать» и «да только тестами проверить»

Это все было бы круто, если бы потом не надо было делать каст, внутри которого .net все равно сделает проверку типа.

Тут согласен — не очень рационально использовать велосипед вместо уже сделанного и отпимизированного.
Так что насчет "… должен знать типы… если поменялось то обнаружить только тестами..." и тут ответ похоже «да должен знать» и «да только тестами проверить»

Для прикладного кода под .net это, очевидно (и демонстрируемо), не так.

Sign up to leave a comment.

Articles

Change theme settings