Почему следует избегать кастинга?

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

Но если кто-то попросил меня объяснить, почему именно это, я бы, вероятно, посмотрел на них как олень в фарах.

Так почему/когда бросает плохо?

Является ли он общим для java, С#, С++ или использует ли каждая среда среды выполнения на нем собственные термины?

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

Ответ 1

Вы отметили это с тремя языками, и ответы действительно очень разные между этими тремя. Обсуждение С++ более или менее подразумевает обсуждение C casts, а также дает (более или менее) четвертый ответ.

Так как это тот, который вы не указали явно, я начну с C. C casts имеет ряд проблем. Один из них заключается в том, что они могут делать какие-то разные вещи. В некоторых случаях актер делает не что иное, как рассказывать компилятору (по сути): "Заткнись, я знаю, что я делаю" - т.е. Он гарантирует, что даже когда вы делаете преобразование, которое может вызвать проблемы, компилятор не предупредит вас об этих потенциальных проблемах. Например, char a=(char)123456;. Точный результат этой реализации определен (зависит от размера и подписи char), и, за исключением довольно странных ситуаций, вероятно, не полезен. C casts также различаются в зависимости от того, являются ли они тем, что происходит только во время компиляции (т.е. Вы просто говорите компилятору, как интерпретировать/обрабатывать некоторые данные) или что-то, что происходит во время выполнения (например, фактическое преобразование из double в долго).

С++ пытается справиться с этим, по крайней мере, в некоторой степени, добавив несколько "новых" операторов трансляции, каждая из которых ограничена только подмножеством возможностей C cast. Это затрудняет (например) случайное преобразование, которое вы действительно не намеревались - если вы только намерены отбросить константу на объект, вы можете использовать const_cast и быть уверенным, что единственное, что он может влияет ли объект const, volatile или нет. И наоборот, a static_cast не может влиять на объект const или volatile. Короче говоря, у вас есть одни и те же типы возможностей, но они классифицируются таким образом, что один актер может, как правило, выполнять только один вид конверсии, при котором одиночный C-стиль может делать два или три преобразования за одну операцию. Основное исключение состоит в том, что вы можете использовать dynamic_cast вместо static_cast по крайней мере в некоторых случаях, и, несмотря на то, что он написан как dynamic_cast, он действительно окажется в виде static_cast. Например, вы можете использовать dynamic_cast для перемещения вверх или вниз по иерархии классов, но приведение "вверх" в иерархию всегда безопасно, поэтому это можно сделать статически, в то время как "вниз" иерархия не обязательно безопасный, чтобы он выполнялся динамически.

Java и С# гораздо более похожи друг на друга. В частности, при каждом из них кастинг (фактически?) Всегда выполняется во время выполнения. В терминах операторов спредов С++ он обычно ближе всего к dynamic_cast с точки зрения того, что действительно сделано, т.е. Когда вы пытаетесь передать объект некоторому типу цели, компилятор вставляет проверку времени выполнения, чтобы увидеть, преобразование разрешено, а исключение - исключение. Точные детали (например, имя, используемое для исключения "плохого приведения" ), различаются, но основной принцип остается в основном похожим (хотя, если память используется, Java делает прикладывание к нескольким не-объектным типам, таким как int ближе к C-кастам - но эти типы используются достаточно редко, чтобы 1) я не помню это точно, и 2) даже если это правда, это не имеет большого значения в любом случае).

Глядя на вещи в более общем плане, ситуация довольно проста (по крайней мере, IMO): литье (очевидно, достаточно) означает, что вы конвертируете что-то из одного типа в другой. Когда/если вы это сделаете, возникает вопрос: "Почему?" Если вы действительно хотите, чтобы что-то было определенным типом, почему вы не определили его, чтобы начать этот тип? Это не означает, что никогда не было причин делать такое преобразование, но в любом случае это должно спросить о том, можно ли перепроектировать код, чтобы правильный тип использовался повсюду. Даже кажущиеся безобидными преобразования (например, между целыми и плавающими точками) следует исследовать гораздо ближе, чем обычно. Несмотря на кажущееся сходство, целые числа действительно должны использоваться для "подсчитанных" типов вещей и с плавающей точкой для "измеренных" видов вещей. Игнорирование различия заключается в том, что приводит к некоторым сумасшедшим заявлениям типа "средняя американская семья имеет 1,8 детей". Хотя мы все можем видеть, как это происходит, факт состоит в том, что ни одна семья не имеет 1,8 детей. У них может быть 1 или 2, или они могут иметь больше, чем это, но не 1.8.

Ответ 2

Здесь много хороших ответов. Вот как я смотрю на это (с точки зрения С#).

Кастинг обычно означает одну из двух вещей:

  • Я знаю тип выполнения этого выражения, но компилятор этого не знает. Компилятор, я вам говорю, во время выполнения объект, соответствующий этому выражению, действительно будет иметь этот тип. На данный момент вы знаете, что это выражение должно рассматриваться как относящееся к этому типу. Сгенерируйте код, предполагающий, что объект будет иметь данный тип, или, если я ошибаюсь, исключил исключение.

  • И компилятор, и разработчик знают тип выполнения выражения. Существует другое значение другого типа, связанное со значением, которое это выражение будет иметь во время выполнения. Сгенерировать код, который выдает значение требуемого типа из значения данного типа; если вы не можете этого сделать, затем выполните исключение.

Обратите внимание, что это противоположности. Есть два вида бросков! Есть броски, где вы даете подсказку компилятору о реальности - эй, эта вещь типа типа на самом деле имеет тип Customer - и есть отливки, в которых вы говорите компилятору, чтобы выполнить сопоставление от одного типа к другому - эй, Мне нужен int, который соответствует этому двойнику.

Оба вида приведения - это красные флаги. В первом типе броска возникает вопрос: "Почему именно разработчик знает то, чего нет в компиляторе?" Если вы находитесь в такой ситуации, то лучше всего это изменить программу, чтобы компилятор действительно справился с реальностью. Тогда вам не нужен бросок; анализ выполняется во время компиляции.

Во втором типе броска возникает вопрос: "Почему в первую очередь не выполняется операция в целевом типе?" Если вам нужен результат в ints, то почему вы держите двойной в первую очередь? Разве вы не должны держать int?

Некоторые дополнительные мысли здесь:

http://blogs.msdn.com/b/ericlippert/archive/tags/cast+operator/

Ответ 3

Ошибки каста всегда сообщаются как ошибки времени выполнения в java. Использование generics или templating превращает эти ошибки в ошибки времени компиляции, что значительно облегчает обнаружение, когда вы допустили ошибку.

Как я сказал выше. Это не значит, что все касты плохие. Но если это можно избежать, лучше всего это сделать.

Ответ 4

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

Если это было повсеместно плохо, языки не поддержали бы его. Как и любая другая языковая функция, у нее есть свое место.

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

Соответствующие документы С# здесь.

В предыдущем SO-вопросе здесь есть большое резюме на опциях С++ .

Ответ 5

В основном я использую С++, но большинство из них, вероятно, относится и к Java и С#:

С++ - это статически типизированный язык. Есть некоторые ограничения, которые язык позволяет вам в этом (виртуальные функции, неявные преобразования), но в основном компилятор знает тип каждого объекта во время компиляции. Причина использования такого языка заключается в том, что ошибки могут быть обнаружены во время компиляции. Если компилятор знает типы a и b, то он поймает вас во время компиляции, когда вы выполните a=b, где a - комплексное число, а b - строка.

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

Ответ 6

Java, С# и С++ являются строго типизированными языками, хотя строго типизированные языки можно рассматривать как негибкие, они имеют преимущество при проверке типов во время компиляции и защите от ошибок времени выполнения, вызванных неправильным типом операции.

Существует несколько базовых двух типов: литье в более общий тип или приведение к другим типам (более конкретным). Приведение к более общему типу (приведение к родительскому типу) оставит тайм-ауты компиляции неповрежденными. Но приведение к другим типам (более конкретные типы) отключит проверку типа времени компиляции и будет заменено компилятором проверкой времени выполнения. Это означает, что у вас меньше уверенности, что вы скомпилированный код будет работать правильно. Это также оказывает незначительное влияние на производительность, из-за дополнительной проверки типа времени выполнения (Java API полон отливок!).

Ответ 7

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

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

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

Другие типы более чреваты и/или более дорогими. В большинстве языков литье из базового типа в производный тип либо дешево, но имеет высокий риск серьезной ошибки (в С++, если вы статично-каскад от базы до производного, это будет дешево, но если базовое значение не относится к производному типу, поведение undefined и может быть очень странным) или относительно дорогостоящим и с риском создания исключения (dynamic_cast в С++, явный базовый-на-производный листинг на С# и т.д.). Бокс в Java и С# - еще один пример этого и еще больший расход (учитывая, что они меняются больше, чем просто обрабатываются базовые значения).

Другие типы приведения могут потерять информацию (длинный целочисленный тип для короткого целочисленного типа).

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

Более концептуальная, но, возможно, более важная причина заключается в том, что каждый случай кастинга - это случай, когда ваша способность рассуждать о правильности вашего кода затуманивается: каждый случай - это другое место, где что-то может пойти не так, и способы в котором он может пойти не так, добавьте сложность вывода, будет ли система в целом ошибаться. Даже если бросок доказуемо безопасен каждый раз, доказывая, что это дополнительная часть рассуждений.

Наконец, тяжелое использование отбросов может указывать на неспособность хорошо рассмотреть объектную модель либо при ее создании, либо в ее использовании, либо в том и другом: частое отбрасывание между одними и теми же немногими типами почти всегда является неспособностью рассматривать отношения между используемыми типами. Здесь не так много, что касты плохие, так как они являются признаком чего-то плохого.

Ответ 8

Существует тенденция к тому, что программисты придерживаются догматических правил использования языковых функций ( "никогда не используйте XXX!", "XXX считается вредным" и т.д.), где XXX варьируется от goto до указателей до protected данных в одиночные точки для передачи объектов по значению.

Следуя таким правилам, по моему опыту, обеспечивается две вещи: вы не будете страшным программистом и не будете отличным программистом.

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

"Я вообще избегаю типов литья, насколько это возможно" является хорошим примером такого чрезмерно обобщенного правила. Броски важны во многих распространенных ситуациях. Некоторые примеры:

  • При взаимодействии с сторонним кодом (особенно, когда этот код распространен с typedef s). (Пример: GLfloatdoubleReal.)
  • Кастинг из указателя/ссылки производного на базовый класс: это настолько распространено и естественно, что компилятор сделает это неявно. Если сделать это явным, повышает удобочитаемость, бросок - это шаг вперед, а не назад!
  • Кастинг с указателя/ссылки базового класса на производный класс: также распространен даже в хорошо продуманном коде. (Пример: разнородные контейнеры.)
  • Внутри двоичной сериализации/десериализации или другого низкоуровневого кода, которому необходим доступ к необработанным байтам встроенных типов.
  • В любое время, когда он просто более естественный, удобный и читаемый для использования другого типа. (Пример: std::size_typeint.)

Конечно, есть много ситуаций, когда нецелесообразно использовать бросок, и важно также изучить их; Я не буду вдаваться в подробности, поскольку приведенные выше ответы проделали хорошую работу, указав некоторые из них.

Ответ 9

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

С особым отношением к С#, поскольку он включает is и as, у вас есть возможность (по большей части) принять решение о том, удастся ли сделать трансляция. Из-за этого вы должны предпринять соответствующие шаги, чтобы определить, будет ли операция успешной и надлежащим образом действовать.

Ответ 10

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

Ответ 11

Не уверен, что кто-то уже упомянул об этом, но в С# casting можно использовать довольно безопасно, и это часто необходимо. Предположим, вы получаете объект, который может быть нескольких типов. Используя ключевое слово is, вы можете сначала подтвердить, что объект действительно относится к типу, к которому вы собираетесь его бросить, и затем непосредственно передать объект в этот тип. (Я не очень много работал с Java, но я уверен, что там очень простой способ сделать это).

Ответ 12

Вы передаете объект только одному типу, если выполняются 2 условия:

  • вы знаете, что он относится к этому типу
  • компилятор не выполняет

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

Теперь, когда вы делаете актерский состав, это может иметь две разные причины:

  • Вы плохо работали над выражением отношений типа.
  • система типов языков просто недостаточно выразительна для их обозначения.

В большинстве языков вы часто сталкиваетесь со второй ситуацией. Дженерики, как и в Java, немного помогают системе шаблонов С++, но ее трудно освоить, и даже тогда некоторые вещи могут быть невозможны или просто не стоят усилий.

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

Ответ 13

Обычно шаблоны (или генерики) более безопасны для типов, чем приводы. В этом отношении я бы сказал, что проблема с литьем - это безопасность типов. Тем не менее, есть еще одна более тонкая проблема, связанная особенно с downcasting: design. По крайней мере, с моей точки зрения, подавление - это запах кода, признак того, что что-то может быть неправильно с моим делом, и я должен исследовать дальше. Почему просто: если вы "правильно" получаете абстракции, вам это просто не нужно! Хороший вопрос, кстати...

Ура!

Ответ 14

Чтобы быть очень кратким, хорошая причина связана с переносимостью. Разная архитектура, в которой размещаются одинаковые языки, может иметь, скажем, разные размеры. Поэтому, если я перехожу из ArchA в ArchB, у которого есть более узкий int, я мог бы увидеть нечетное поведение в лучшем случае и seg, в худшем случае.

(я явно игнорирую независимый от архитектуры байт-код и IL.)