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


97

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

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

Так почему и когда плохо кастинг?

Является ли это общим для java, c #, c ++ или каждая другая среда выполнения справляется с этим на своих условиях?

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


3
Откуда у вас такое впечатление?
Oded

8
У меня сложилось такое впечатление, потому что я никогда не читал книгу и не встречал программиста, который сказал бы: "КАСТИНГ ТАКУМУ !!!"
LoudNPossablyWrong

15
Ответ для C ++ неизбежно отличается от ответа для C #. Это очень зависит от языка. Ответы на данный момент отвечают на этот вопрос для конкретных языков и в некоторых случаях не указывают, о каком языке они говорят.
Джеймс МакНеллис,

2
Плохое - понятие относительное. Лучше всего избегать приведения типов, но иногда программист должен делать то, что должен делать программист. (особенно если вы пишете программу java 1.5+, которая использует библиотеку, написанную для 1.4) Возможно, переименуйте вопрос: «Почему следует избегать преобразования типов?»
Майк Миллер,

2
Это отличный вопрос .... Его закрыли 5 пользователей, у каждого из которых менее 10к репутации !! Ясно, что чрезмерное использование новых возможностей. Проголосовали за повторное открытие.
rich4thelasers

Ответы:


141

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

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

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

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

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


10
Похоже, здесь вы страдаете от «смерти от концентрации внимания», это хороший ответ.
Стив Таунсенд,

2
Сильный ответ, но некоторые из «я не знаю» можно было бы сократить, чтобы сделать его исчерпывающим. @ Dragontamer5788 это хороший ответ, но не исчерпывающий.
M2tM

5
Один целый ребенок и один ребенок с отсутствующей ногой - это 1,8 ребенка. Но все же очень хороший ответ. :)
Оз.

1
Очень хороший ответ, нисколько не помешавший исключить пары без детей.

@ Роджер: не исключая, просто игнорируя.
Джерри Коффин,

48

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

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

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

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

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

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

Второй тип приведения поднимает вопрос «почему операция не выполняется в целевом типе данных в первую очередь?» Если вам нужен результат в int, то зачем вам вообще дабл? Разве вы не должны держать int?

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

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


2
Я не думаю, что это по своей сути красные флажки. Если у вас есть Fooобъект, который наследуется от Bar, и вы храните его в a List<Bar>, вам потребуются приведения, если вы хотите Fooвернуть его. Возможно, это указывает на проблему на архитектурном уровне (почему мы сохраняем Bars вместо Foos?), Но не обязательно. И, если у него Fooтакже есть допустимое приведение int, это также касается вашего другого комментария: вы сохраняете, а Fooне int, потому что intне всегда подходит.
Майк Кэрон,

6
@Mike Caron - Я не могу отвечать за Эрика, очевидно, но для меня красный флаг означает «это то, о чем нужно подумать», а не «что-то не так». И нет проблем с сохранением a Fooв a List<Bar>, но в момент преобразования вы пытаетесь сделать что-то, Fooчто не подходит для a Bar. Это означает, что различное поведение подтипов осуществляется с помощью механизма, отличного от встроенного полиморфизма, обеспечиваемого виртуальными методами. Возможно, это правильное решение, но чаще всего это красный флаг.
Джеффри Л. Уитледж

14
На самом деле. Если вы вытаскиваете что-то из списка животных и позже вам нужно сказать компилятору, о, кстати, я знаю, что первый - тигр, второй - лев, а третий - является медведем, то вам следовало использовать Tuple <Lion, Tiger, Bear>, а не List <Animal>.
Эрик Липперт,

5
Я согласен с вами, но я думаю, что без улучшенной синтаксической поддержки Tuple<X,Y,...>кортежи вряд ли увидят широкое применение в C #. Это то место, где язык мог бы лучше подталкивать людей к «яме успеха».
kvb

4
@kvb: согласен. Мы рассматривали возможность принятия синтаксиса кортежей для C # 4, но он не укладывался в бюджет. Возможно, в C # 5; мы еще не разработали полный набор функций. Слишком занят сборкой CTP для async. Или, возможно, в гипотетической будущей версии.
Эрик Липперт

36

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

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


4
Это предполагает, что все приведение "плохое"; однако это не совсем так. Возьмем, к примеру, C # с неявной и явной поддержкой приведения типов. Проблема возникает, когда выполняется приведение, которое (случайно) удаляет информацию или безопасность типов (это зависит от языка).

5
На самом деле, в C ++ это совершенно неправильно. Возможно, необходимо отредактировать, чтобы включить язык (языки), на который направлена ​​эта информация.
Стив Таунсенд,

@Erick - Я бы хотел, но я ничего не знаю о Java и не знаю достаточно подробностей о C #, чтобы быть уверенным, что информация верна.
Стив Таунсенд,

Извините, я парень Java, поэтому мои знания C ++ достаточно ограничены. Я мог исключить из него слово «шаблон», так как оно должно было быть расплывчатым.
Майк Миллер

Не так. Некоторые ошибки приведения перехватываются компилятором Java, и не только общие. Рассмотрим (String) 0;
Marquis of Lorne

18

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

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

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

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

Здесь есть отличное резюме по параметрам C ++ в предыдущем вопросе SO .


9

Я в основном говорю здесь о C ++ , но большая часть этого, вероятно, применима и к Java и C #:

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

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


5

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

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


5
Не все приведения типа "обходят" безопасность типа в C ++.
Джеймс Макнеллис,

4
Не все приводит к типу безопасности "обхода" в C #.

5
Не все приведения типа "обходят" безопасность типов в Java.
Эрик Робертсон

11
Не все приведения типа "обходят" безопасность типа в Casting. Ой, подождите, этот тег не относится к языку ...
SBI

2
Практически во всех случаях в C # и Java приведение типов приводит к снижению производительности, поскольку система выполняет проверку типа во время выполнения (что не является бесплатным). В C ++, dynamic_castкак правило, медленнее, чем static_cast, поскольку он обычно должен выполнять проверку типа во время выполнения (с некоторыми оговорками: приведение к базовому типу дешево и т. Д.).
Travis Gockel

4

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

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

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

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

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

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

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

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


2

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

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

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

«Я обычно стараюсь избегать типов приведения в максимально возможной степени» - хороший пример такого чрезмерно обобщенного правила. Приводы необходимы во многих распространенных ситуациях. Некоторые примеры:

  1. При взаимодействии со сторонним кодом (особенно когда этот код изобилует typedefs). (Пример: GLfloat<--> double<--> Real.)
  2. Приведение указателя / ссылки производного к базовому классу: это настолько распространено и естественно, что компилятор сделает это неявно. Если сделать его явным, улучшится читаемость, приведение будет шагом вперед, а не назад!
  3. Преобразование из базового в производный указатель / ссылку на класс: также часто встречается даже в хорошо разработанном коде. (Пример: разнородные контейнеры.)
  4. Внутри двоичной сериализации / десериализации или другого низкоуровневого кода, которому требуется доступ к необработанным байтам встроенных типов.
  5. В любое время, когда это просто более естественно, удобно и читабельно, использовать другой тип. (Пример: std::size_type-> int.)

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


1

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

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


1

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


1

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


1

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

  1. вы знаете, что это такого типа
  2. компилятор не

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

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

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

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

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


0

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

Ура!


0

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.