【问题标题】:Why should casting be avoided? [closed]为什么要避免铸造? [关闭]
【发布时间】:2011-05-09 05:07:38
【问题描述】:

我通常会尽可能避免强制转换类型,因为我认为这是一种糟糕的编码实践,并且可能会导致性能损失。

但如果有人让我解释为什么会这样,我可能会像头灯下的鹿一样看待他们。

那么为什么/什么时候选角不好?

它对于 java、c#、c++ 是通用的还是每个不同的运行时环境都按照自己的方式处理它?

欢迎任何语言的具体说明,例如为什么它在 c++ 中不好?

【问题讨论】:

  • 这个印象是从哪里来的?
  • 我有这样的印象,因为我从来没有读过一本书,也没有遇到过说“CASTING SOOOO GOOOOD!!!”的程序员
  • C++ 的答案不可避免地与 C# 的答案不同。这是非常特定于语言的。迄今为止的答案针对特定语言回答了这个问题,并且在某些情况下没有说明他们在谈论什么语言。
  • 坏是一个相对术语。避免强制转换是最佳实践,但有时程序员必须做程序员必须做的事情。 (特别是如果您编写一个使用为 1.4 编写的库的 java 1.5+ 程序)也许重命名这个问题,“为什么要避免强制转换?”
  • 这是一个很好的问题.... 它被 5 个用户关闭,每个用户的声望都低于 10k!显然是过分热心地使用新的权力。已投票重新开放。

标签: c# java c++ casting


【解决方案1】:

Java、c# 和 c++ 是强类型语言,尽管强类型语言可以被视为不灵活,但它们的好处是在编译时进行类型检查,并保护您免受由于某些错误类型导致的运行时错误操作。

基本上有两种类型的转换:转换为更一般的类型或转换为其他类型(更具体)。转换为更通用的类型(转换为父类型)将使编译时检查保持不变。但是转换为其他类型(更具体的类型)将禁用编译时类型检查,并将由编译器替换为运行时检查。这意味着你不太确定你编译的代码是否能正确运行。由于额外的运行时类型检查(Java API 充满了强制转换!),它对性能的影响也可以忽略不计。

【讨论】:

  • 在 C++ 中并非所有强制类型转换“绕过”类型安全。
  • 并非所有在 C# 中强制“绕过”类型安全。
  • 并非所有在 Java 中都强制“绕过”类型安全。
  • 并非所有的 Casting 中都“绕过”类型安全。哦等等,那个标签不是指一种语言...
  • 在 C# 和 Java 的几乎所有情况下,强制转换都会降低性能,因为系统将执行运行时类型检查(这不是免费的)。在 C++ 中,dynamic_cast 通常比 static_cast 慢,因为它通常需要进行运行时类型检查(有一些警告:转换为基本类型很便宜,等等)。
【解决方案2】:

在 java 中,转换错误总是被报告为运行时错误。使用泛型或模板会将这些错误转化为编译时错误,从而更容易检测到您何时犯了错误。

正如我上面所说的。这并不是说所有的选角都是不好的。但如果可以避免,最好还是这样做。

【讨论】:

  • 这假设所有的演员都是“坏的”;然而,这并不完全正确。以 C# 为例,它同时支持隐式和显式转换。当执行转换(意外地)删除信息或类型安全(这因语言而异)时,问题就出现了。
  • 实际上,这在 C++ 中是完全错误的。也许需要进行编辑以包含此信息所针对的语言。
  • @Erick - 我会,但我不知道关于 Java 的第一件事,也没有足够的 C# 细节来确保信息正确。
  • 对不起,我是Java人,所以我的c++知识有限。我可以把“模板化”这个词删掉,因为它本来就含糊不清。
  • 不是这样。 Java 编译器会捕获一些转换错误,而不仅仅是通用错误。考虑 (String)0;
【解决方案3】:

选角本质上并不是坏事,只是它经常被误用作实现某些真正不应该做的事情的手段,或者做得更优雅。

如果它普遍不好,语言就不会支持它。像任何其他语言功能一样,它也有它的位置。

我的建议是专注于您的主要语言,并了解其所有类型以及相关的最佳实践。这应该为其他语言的旅行提供信息。

相关的 C# 文档是 here

有一个关于 C++ 选项的精彩总结at a previous SO question here

【讨论】:

    【解决方案4】:

    详细说明KDeveloper's answer,它本质上不是类型安全的。使用强制转换,无法保证您的转换来源和转换目标会匹配,如果发生这种情况,您将获得运行时异常,这总是一件坏事。

    对于 C#,由于它包含 isas 运算符,因此您有机会(在大多数情况下)确定强制转换是否成功。因此,您应该采取适当的步骤来确定操作是否会成功并正确进行。

    【讨论】:

      【解决方案5】:

      在 C# 的情况下,由于在处理值类型时涉及装箱/拆箱开销,因此在强制转换时需要更加小心。

      【讨论】:

        【解决方案6】:

        我在这里主要代表 C++,但其中大部分可能也适用于 Java 和 C#:

        C++ 是一种静态类型语言。语言允许你在这方面有一些余地(虚拟函数,隐式转换),但基本上编译器在编译时知道每个对象的类型。使用这种语言的原因是可以在编译时捕获错误。如果编译器知道ab 的类型,那么当您执行a=b 时,它会在编译时捕获您,其中a 是一个复数,b 是一个字符串。

        每当你进行显式转换时,你都会告诉编译器闭嘴,因为你认为你知道得更多。万一你错了,你通常只会在运行时发现。在运行时发现的问题是,这可能是在客户的。

        【讨论】:

          【解决方案7】:

          某些类型的铸造是如此安全和高效,以至于通常甚至根本不被视为铸造。

          如果您从派生类型转换为基类型,这通常非常便宜(通常 - 取决于语言、实现和其他因素 - 它是零成本)并且是安全的。

          如果你从像 int 这样的简单类型转换为像 long int 这样的更广泛的类型,那么它通常也很便宜(通常不会比分配与转换相同的类型贵多少)并且再次是安全的。

          其他类型更令人担忧和/或更昂贵。在大多数语言中,从基类型转换为派生类型要么很便宜,但存在严重错误的高风险(在 C++ 中,如果您从基类型转换为派生类型会很便宜,但如果基础值不是派生类型,则行为未定义并且可能非常奇怪)或相对昂贵并且有引发异常的风险(C++ 中的 dynamic_cast、C# 中的显式基到派生转换等)。 Java 和 C# 中的装箱是另一个例子,而且花费更大(考虑到它们所改变的不仅仅是底层值的处理方式)。

          其他类型的转换可能会丢失信息(长整数类型到短整数类型)。

          这些风险(无论是异常还是更严重的错误)和费用情况都是避免强制转换的原因。

          一个更概念化但可能更重要的原因是,每种类型转换的情况都是您推理代码正确性的能力受到阻碍的情况:每种情况都是另一个可能出错的地方,以及方法它可能出错的情况增加了推断整个系统是否会出错的复杂性。即使每次都证明演员是安全的,证明这是推理的额外部分。

          最后,转换的大量使用可能表明在创建对象模型、使用对象模型或两者时未能很好地考虑对象模型:频繁地在相同的几个类型之间来回转换几乎总是未能考虑关系使用的类型之间。这里并不是说演员阵容不好,因为他们是坏事的征兆。

          【讨论】:

            【解决方案8】:

            您已用三种语言对此进行了标记,而这三种语言的答案确实大不相同。对 C++ 的讨论或多或少也意味着对 C 类型转换的讨论,这(或多或少)给出了第四个答案。

            由于这是您没有明确提及的那个,我将从 C 开始。C 转换有很多问题。一是他们可以做许多不同的事情。在某些情况下,强制转换只不过是告诉编译器(本质上):“闭嘴,我知道我在做什么”——也就是说,它确保即使您进行可能导致问题的转换,编译器不会就这些潜在问题向您发出警告。例如,char a=(char)123456;。定义的这个实现的确切结果(取决于char 的大小和符号),除了在相当奇怪的情况下,可能没有用。 C 转换在它们是仅在编译时发生的事情(即,您只是告诉编译器如何解释/处理某些数据)还是在运行时发生的事情(例如,从 double 到长)。

            C++ 尝试通过添加一些“新”转换运算符来至少在某种程度上解决这个问题,每个运算符都仅限于 C 转换功能的一个子集。这使得(例如)意外进行您真正不打算的转换变得更加困难 - 如果您打算放弃对象上的 constness,您可以使用const_cast,并且确保它唯一会影响一个对象是constvolatile还是不是。相反,static_cast 不允许影响对象是const 还是volatile。简而言之,您拥有大多数相同类型的功能,但它们被分类,因此一次转换通常只能进行一种转换,而单个 C 样式转换可以在一次操作中进行两次或三次转换。主要的例外是您可以至少在某些情况下使用dynamic_cast 代替static_cast,尽管写成dynamic_cast,但它最终会变成@ 987654332@。例如,您可以使用dynamic_cast 向上或向下遍历类层次结构——但是“向上”转换层次结构始终是安全的,因此可以静态完成,而“向下”转换层次结构不一定安全,所以它是动态完成的。

            Java 和 C# 更相似。特别是,对于他们来说,铸造(实际上?)总是一个运行时操作。就 C++ 强制转换运算符而言,就实际所做的而言,它通常最接近 dynamic_cast —— 即,当您尝试将对象强制转换为某种目标类型时,编译器会插入运行时检查以查看是否允许转换,否则抛出异常。确切的细节(例如,用于“bad cast”异常的名称)各不相同,但基本原理仍然大致相似(尽管,如果记忆有用,Java 确实将强制转换应用于少数非对象类型,如 int much更接近 C 类型转换——但这些类型很少使用,以至于 1) 我不记得了,而且 2) 即使它是真的,它也无关紧要)。

            从更一般的角度来看,情况非常简单(至少在 IMO):演员表(显然足够)意味着您正在将某物从一种类型转换为另一种类型。何时/如果您这样做,就会提出“为什么?”的问题。如果您真的希望某物成为特定类型,为什么不将其定义为一开始的那种类型呢?这并不是说从不有理由进行这种转换,但无论何时发生这种转换,它都会提示您是否可以重新设计代码以便始终使用正确的类型的问题。即使是看似无害的转换(例如,整数和浮点之间的转换)也应该比常见的更仔细地检查。尽管它们看起来相似,但整数确实应该用于“计数”类型的事物,而浮点数则应该用于“测量”类型的事物。忽略这种区别会导致一些疯狂的说法,比如“美国家庭平均有 1.8 个孩子”。尽管我们都可以看到这是如何发生的,但事实是 no 家庭有 1.8 个孩子。他们可能有 1 个,也可能有 2 个,或者可能有更多——但绝不会是 1.8。

            【讨论】:

            • 看起来你在这里正遭受“注意力不集中的死亡”,这是一个不错的答案。
            • 一个强有力的答案,但一些“我不知道”部分可以收紧以使其全面。 @Dragontamer5788 这是一个很好的答案,但并不全面。
            • 一个完整的孩子和一个缺腿的孩子将是 1.8 个孩子。但是非常好的答案。 :)
            • 一个非常好的答案,不排除有没有个孩子的夫妇。
            • @Roger:不排除,只是忽略。
            【解决方案9】:

            不确定是否有人已经提到过这一点,但在 C# 中可以以相当安全的方式使用强制转换,并且通常是必要的。假设您收到一个可以有多种类型的对象。使用is 关键字,您可以首先确认该对象确实是您将要转换的类型,然后直接将该对象转换为该类型。 (我没有经常使用 Java,但我确信在那里也有一种非常简单的方法)。

            【讨论】:

              【解决方案10】:

              如果满足两个条件,您只能将对象转换为某种类型:

              1. 你知道是那种类型
              2. 编译器没有

              这意味着并非您拥有的所有信息都在您使用的类型结构中得到了很好的体现。这很糟糕,因为您的实现应该语义上包含您的模型,而在这种情况下它显然不是。

              现在,当您进行演员表时,可能有两个不同的原因:

              1. 您在表达类型关系方面做得不好。
              2. 语言类型系统根本不足以表达它们。

              在大多数语言中,您经常会遇到第二种情况。 Java 中的泛型有一点帮助,C++ 模板系统更有帮助,但它很难掌握,即便如此,有些事情可能是不可能的,或者只是不值得努力。

              所以你可以说,演员表是一种肮脏的技巧,可以规避你的问题,用某种特定的语言表达某种特定的类型关系。应该避免肮脏的黑客攻击。但你永远不能没有他们。

              【讨论】:

                【解决方案11】:

                通常模板(或泛型)比强制类型转换更安全。在这方面,我会说强制转换的一个问题是类型安全。然而,还有另一个更微妙的问题,尤其是与向下转换相关:设计。至少从我的角度来看,向下转换是一种代码味道,表明我的设计可能有问题,我应该进一步调查。为什么很简单:如果你“得到”了正确的抽象,你根本就不需要它!顺便问下好问题...

                干杯!

                【讨论】:

                  【解决方案12】:

                  这里有很多好的答案。这是我的看法(从 C# 的角度来看)。

                  铸造通常意味着以下两种情况之一:

                  • 我知道这个表达式的运行时类型,但编译器不知道。编译器,我告诉你,在运行时对应于这个表达式的对象确实是这种类型。到目前为止,您知道该表达式将被视为这种类型。生成假定对象属于给定类型的代码,或者,如果我错了,则抛出异常。

                  • 编译器和开发人员都知道表达式的运行时类型。还有一个不同类型的值与此表达式在运行时将具有的值相关联。生成从给定类型的值生成所需类型的值的代码;如果不能这样做,则抛出异常。

                  请注意,它们是相反的。有两种类型的演员表!在某些类型的转换中,您正在向编译器提供关于 realityhint - 嘿,这个对象类型的东西实际上是 Customer 类型 - 并且在您告诉的地方有类型转换编译器执行从一种类型到另一种类型的映射 - 嘿,我需要对应于这个 double 的 int。

                  这两种类型的演员都是危险信号。第一种类型的转换提出了一个问题“为什么开发人员知道编译器不知道的东西?”如果您处于这种情况,那么更好的做法通常是更改程序,以便编译器确实能够处理现实。那你就不需要演员了;分析是在编译时完成的。

                  第二种类型的转换提出了一个问题“为什么不首先在目标数据类型中进行操作?”如果您需要整数的结果,那么为什么首先要持有双精度数?你不应该持有一个 int 吗?

                  这里有一些额外的想法:

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

                  【讨论】:

                  • 我不认为这些本质上是危险信号。如果您有一个从Bar 继承的Foo 对象,并且您将其存储在List<Bar> 中,那么如果您想要返回Foo,您将需要强制转换。也许它表明了架构级别的问题(为什么我们存储Bars 而不是Foos?),但不一定。并且,如果 Foo 也具有对 int 的有效转换,它还会处理您的其他评论:您存储的是 Foo,而不是 int,因为 int 并不总是合适的。
                  • @Mike Caron - 显然,我无法为 Eric 回答问题,但对我来说,红旗意味着“这是要考虑的事情”,而不是“这是有问题的”。将Foo 存储在List<Bar> 中也没有问题,但在演员阵容中,您正试图用Foo 做一些不适合Bar 的事情。这意味着子类型的不同行为是通过虚拟方法提供的内置多态性以外的机制完成的。也许这是正确的解决方案,但更多时候是危险信号。
                  • 确实如此。如果你从动物列表中取出东西,然后你需要告诉编译器,哦,顺便说一句,我碰巧知道第一个是老虎,第二个是狮子,第三个是熊,那么您应该使用 Tuple,而不是 List
                  • 我同意你的观点,但我认为如果没有对Tuple<X,Y,...> 元组更好的语法支持,C# 中不太可能广泛使用。这是语言可以更好地推动人们走向“成功深渊”的一个地方。
                  • @kvb:我同意。我们考虑为 C# 4 采用元组语法,但它不符合预算。也许在 C# 5 中;我们还没有制定出完整的功能集。太忙于将 CTP 放在一起以进行异步。或者也许在一个假设的未来版本中。
                  【解决方案13】:

                  要真正简洁,一个很好的理由是因为可移植性。都适应相同语言的不同架构可能具有不同大小的整数。因此,如果我从 ArchA 迁移到具有更窄 int 的 ArchB,我最多可能会看到奇怪的行为,而最坏的情况可能会出现段错误。

                  (我显然忽略了与架构无关的字节码和 IL。)

                  【讨论】:

                    【解决方案14】:

                    程序员越来越倾向于坚持关于语言特性使用的教条规则(“永远不要使用 XXX!”、“XXX 被认为有害”等),其中 XXX 的范围从 gotos 到指向 @987654322 的指针@ 数据成员到单例以按值传递对象。

                    按照我的经验,遵循这样的规则可以确保两件事:你不会成为一个糟糕的程序员,也不会成为一个伟大的程序员。

                    更好的方法是挖掘并揭示这些全面禁令背后的真相核心,然后明智地使用这些特征,并理解许多情况下它们是工作的最佳工具。

                    “我通常尽可能避免强制转换类型”是这种过度概括规则的一个很好的例子。在许多常见情况下,演员表是必不可少的。一些例子:

                    1. 与第三方代码互操作时(尤其是当该代码充斥着typedefs 时)。 (例如:GLfloat double Real。)
                    2. 从派生类指针/引用转换为基类指针/引用:这是非常常见和自然的,编译器会隐式执行此操作。如果明确表示会增加可读性,那么演员表就是向前迈进了一步,而不是倒退!
                    3. 从基类转换为派生类指针/引用:同样常见,即使在设计良好的代码中也是如此。 (例如:异构容器。)
                    4. 在二进制序列化/反序列化或其他需要访问内置类型的原始字节的低级代码中。
                    5. 任何时候只要使用其他类型更自然、更方便、更易读。 (例如:std::size_type --> int。)

                    在很多情况下适合使用演员表,学习这些也很重要;我不会详细介绍,因为上面的答案已经很好地指出了其中的一些。

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 2013-04-16
                      • 1970-01-01
                      • 2021-06-25
                      • 1970-01-01
                      • 1970-01-01
                      • 2018-11-13
                      • 2011-11-15
                      相关资源
                      最近更新 更多