为什么解释型语言大多是鸭子类型,而编译型语言则具有强类型?

46

我不知道这是否有技术原因?使用弱类型语言实现编译器是否更加困难?这是什么情况?


2
看起来你混淆了一些术语。例如,Python是一种具有鸭子类型的动态类型语言,但它是强类型的;C具有静态类型,但是它是弱类型的。 - mipadi
是的,我想我是。谢谢你的提示,我会去谷歌一些定义并澄清这些术语。 - orion3
9个回答

212

问题的前提有点不可信。解释型语言并非大多数为鸭子类型,编译型语言也不大多是强类型。 类型系统是一种语言特性编译还是解释是一个实现的特性

例如:

  • 编程语言Scheme是动态类型(又称鸭式类型),它有许多几十个解释器实现,但也有一些良好的本地代码编译器,包括Larceny,Gambit和PLT Scheme(其中包括同时具备解释器和JIT编译器的过渡)。

  • 编程语言Haskell是静态类型的;最著名的两个实现是解释器HUGS和编译器GHC。还有其他几个光荣的实现,分为编译成本机代码(yhc)和解释(Helium)。

  • 编程语言Standard ML是静态类型的,它有许多本地代码编译器,其中最好且最活跃的之一是MLton,但最有用的实现之一是解释器Moscow ML。

  • 编程语言Objective Caml是静态类型的。它只带有一个实现(来自法国INRIA),但此实现包括同时具备解释器和本地代码编译器

  • 编程语言Pascal是静态类型的,但由于在UCSD构建的优秀实现基于P-code解释器而变得流行。后来可用了更好的本地代码编译器,例如IBM Pascal/VS compiler for the 370 series of computers。

  • 编程语言C是静态类型的,今天几乎所有实现都是编译的,但在1980年代,我们运气好的使用Saber C时正在使用解释器。

尽管你的问题有些言过其实,但是它确实有一定的道理,因此你应该得到一个更为深思熟虑的答案。事实是,动态类型语言似乎与解释执行的实现相关联。为什么会这样呢?

  • 许多新语言是通过实现定义的。构建解释器比构建编译器更容易。在运行时动态检查类型比静态检查类型更容易。如果你正在编写解释器,则静态类型检查很少有性能优势。

  • 除非你正在创建或调整一个非常灵活的多态类型系统,否则静态类型系统可能会妨碍程序员的工作。但是,如果你正在编写解释器,一个原因可能是要创建一个小型、轻量级的实现,以避免妨碍程序员的工作。

  • 在一些解释型语言中,许多基本操作非常昂贵,以至于在运行时检查类型的额外开销并不重要。PostScript就是一个很好的例子: 如果你想随时随地运行和光栅化贝塞尔曲线,那么在这里或那里检查类型标记就没有问题。

顺便提一句,请谨慎使用“强类型”和“弱类型”这些术语,因为它们没有普遍认可的技术含义。相比之下,“静态类型”意味着程序在执行之前被检查,程序可能会在启动之前被拒绝。 “动态类型”意味着在执行期间检查值的类型,并且不良类型操作可能会导致程序停止或以其他方式在运行时发生错误。静态类型的主要原因是排除可能具有此类“动态类型错误”的程序。(这也是编写解释器的人对静态类型不太感兴趣的另一个原因; 在类型检查之后立即执行,因此区分和保证的性质并不明显。)

强类型通常意味着类型系统中没有漏洞,而弱类型意味着类型系统可以被破坏(从而使任何保证失效)。这些术语经常被错误地用来表示静态和动态类型。
要看到区别,请考虑 C:该语言在编译时进行类型检查(静态类型),但有很多漏洞。你几乎可以将任何类型的值强制转换为相同大小的另一种类型,特别是指针类型自由转换。Pascal 是一种旨在强类型化的语言,但其有一个出乎意料的漏洞:没有标记的变体记录。
强类型语言的实现通常随着时间推移而获得漏洞,通常是为了在高级语言中实现运行时系统的部分。例如,Objective Caml 有一个名为 Obj.magic 的函数,在运行时产生的效果只是简单地返回其参数,但在编译时它将任何类型的值转换为任何其他类型之一。我最喜欢的例子是 Modula-3,其设计者称其类型转换结构为 LOOPHOLE
总之:
  • 静态 vs 动态是语言

  • 编译 vs 解释是实现

  • 原则上,这两个选择可以和被正交地做出,但由于技术上的充分理由,动态类型通常与解释相关联


1
我不是专家。有大量的文献关于将Scheme和Lisp编译成本地机器代码。发表问题,我会尽力回答 :-) - Norman Ramsey
1
我希望你不介意我的询问。如果“动态类型意味着在执行期间检查值的类型”,那么任何解释型语言怎么可能是静态类型的呢? - Venemo
@Venemo:你运行一个生成字节码的编译器,然后进行解释。例如Objective Caml、Moscow ML、Saber C(令人怀念的)、UCSD Pascal(古老的传说)等等。 - Norman Ramsey
2
@Norman,那么按照这个逻辑,每一种没有编译成字节码的解释型语言都是动态类型的,对吗? - Venemo
动态类型 != 鸭子类型。Scheme 不是鸭子类型。 - Coderino Javarino
显示剩余4条评论

10

进行早期绑定(强类型)的原因是性能。 通过早期绑定,您可以在编译时找到方法的位置,因此在运行时它已经知道它的位置。

然而,使用晚期绑定,您必须搜索一个看起来像客户端代码调用的方法。 当然,对于许多程序中的方法调用,这就是动态语言“缓慢”的原因。

但是,您可以创建一个静态编译的语言,并进行后期绑定,这将抵消许多静态编译的优势。


那么你的意思是,具有延迟绑定的编译语言会使程序在运行时变慢?那么它比解释器更慢吗? - orion3
1
Travis 过于简化了。C++ 的虚成员函数具有后期绑定特性,但编译器还有许多有用的工作要做。Self 语言具有真正的后期绑定,但由于 Urs Hoelzle 出色的动态编译器,它获得了很好的性能。难怪 Urs 现在已经成为 Google 的大人物 :-) - Norman Ramsey
1
是的,这有点过于简化了,而且确实有很多方法可以加速后期绑定方法,例如查找缓存等(因此在“slow”周围加上单引号)。 - tsimon
1
早期/晚期绑定与强/弱类型无关。 - Wei Hu

6
几乎可以说是因为编写和使用解释语言的人更喜欢鸭子类型,而开发和使用编译语言的人则更喜欢强制显式类型。(我认为,这种情况的共识原因在于90%是为了错误预防,10%是为了性能)。对于今天编写的大多数程序而言,速度差异都是微不足道的。微软的Word已经在p-code(未编译)上运行了15年了吧?
最好的例子,我能想到的就是经典的Visual Basic(VB6/VBA等)。同样的程序可以用VB编写,并且无论是编译还是解释执行都可以得到相同的结果和可比较的速度。此外,您还可以选择是否进行类型声明(实际上是变量声明)。大多数人更喜欢类型声明,通常是为了预防错误。至少在硬件速度和容量有两个数量级之前,我从未听说过或看到过要使用类型声明来提高速度。
谷歌最近因为在JavaScript上开发JIT编译器而备受关注——这将不需要对语言进行任何更改,也不需要程序员额外考虑。在这种情况下,唯一的好处就是速度。

5

编译型语言在编译时需要考虑使用的内存量。

当你看到像这样的内容:

int a

在C++中,编译器会生成代码来预留四个字节的内存,并将局部符号“a”指向该内存。如果你有像JavaScript这样的无类型脚本语言,解释器在幕后会分配所需的内存。你可以执行以下操作:
var a = 10;  // a is probably a four byte int here
a = "hello world"; // now a is a 12 byte char array

在这两行代码之间发生了很多事情。解释器删除了变量a的内存,为字符分配了新的缓冲区,然后将a变量指向了那块新的内存。在强类型语言中,没有解释器来帮你管理这些,因此编译器必须编写能考虑到类型的指令。

int a = 10; // we now have four bytes on the stack.
a = "hello world"; // wtf? we cant push 12 bytes into a four byte variable! Throw an error!

所以编译器会停止编译那段代码,这样CPU就不会盲目地将12个字节写入一个4个字节大小的缓冲区并引起麻烦。编译器为了处理类型而添加的额外开销会显著减慢语言运行速度,并消除像C++这样的语言的优势。
:)
-nelson
编辑:针对评论的回应,我不太了解Python,所以无法多说什么。但是松散的类型会显着降低运行时速度。每个解释器(虚拟机)调用的指令都必须进行求值,如果需要,则要将变量强制转换为预期的类型。如果你有:
mov a, 10
mov b, "34"
div a, b

然后解释器必须确保a是一个变量和数字,然后它必须在处理指令之前将b强制转换为数字。如果VM执行每个指令都添加这样的开销,那么你就会手忙脚乱 :)


1
这种内存管理方式完全可以在像Java或.NET这样的虚拟机中实现,但这并不是因为强类型的其他优势。 - Paul Tomblin
他们不这样做是因为性能问题。更好的问题是,“你为什么想要一种松散类型的语言?” 叫我守旧或精英主义者,但我不觉得我的变量类型没有被解释器强制执行这一点让我感到舒适。 - user19302
哦,而且CLR并不是真正的虚拟机,因为它在运行之前将IL即时编译成机器语言。 - user19302
我写过很多 Perl 和 C/C++/Java。它们都有各自的用途。我不想用弱类型语言编写薪资系统,但也不想为我的文件解析器定义接口和类。 - Paul Tomblin
请问您能否澄清一下:“减速语言”是什么意思?它是在编译时还是运行时减速?据我所知,Python 有一个编译器。那么问题出在哪里呢? - orion3

3
基本上使用静态类型而不是鸭子类型有两个原因:
  1. 静态错误检查。
  2. 性能。
如果你使用解释型语言,则没有编译时间来进行静态错误检查。其中一个优点就这样消失了。此外,如果你已经有了解释器的开销,那么该语言已经不能用于任何性能关键的东西,因此性能论点变得无关紧要。这解释了为什么静态类型的解释型语言很少见。
反过来说,可以在静态类型语言中大部分地模拟鸭子类型,而不必完全放弃静态类型的好处。可以通过以下任何一种方式完成:
  1. 模板。在这种情况下,如果你用于实例化模板的类型支持模板内调用的所有方法,则代码编译并运行。否则,它会给出一个编译时错误。这有点像编译时鸭子类型。
  2. 反射。试图按名称调用一种方法,它要么有效,要么抛出异常。
  3. 带标记的联合。这些基本上是其他类型的容器类,它们包含一些内存空间和描述当前包含的类型的字段。这些用于诸如代数类型之类的事物。当调用某个方法时,它要么有效,要么抛出异常,具体取决于当前包含的类型是否支持该方法。
这解释了为什么动态类型编译语言很少。

1
如果你有一门解释型语言,那么就没有编译时间来进行静态错误检查。这是不正确的。你可以在解析时检查类型。 - darkestkhan

2
我猜测动态(鸭子)类型的编程语言采用惰性计算,这是懒惰程序员所青睐的,并且懒惰程序员不喜欢编写编译器 ;-)


我欣赏这个笑话,但你如何处理高于真实答案的情况?也许像我一样:继续阅读所有非负声誉的答案 :) - David Rodríguez - dribeas
我通过笑来处理它。一个有趣的答案 - 具有一定真实性!- 不会阻止出现和/或被投票支持的非有趣答案。 - Steven A. Lowe
3
这句话巧妙地运用了文字游戏,但其中的真相是什么呢?大多数采用动态(鸭子)类型的语言默认情况下都不使用惰性求值,而最著名的惰性求值应该是Haskell语言中应用较多,Haskell有静态类型系统和出色的编译器。 :) - ShreevatsaR
1
鸭子类型确实与后期绑定有关,但惰性求值是完全不同的东西:http://en.wikipedia.org/wiki/Lazy_evaluation http://is.gd/bZbT 等等,类似于Unix管道:http://is.gd/dAtP (当然,还有令人惊叹的“为什么使用FP”论文http://is.gd/dAtY,它真正讲述的是惰性求值) - ShreevatsaR
我认为惰性求值和迟绑定并没有太多关联。惰性求值是指当需要一个表达式的值时才会对其进行规约,而迟绑定是指在将名称绑定到表达式之后再进行求值。例如,在Haskell中,如果你写x = 12 + y,那么 x 立即被绑定到一个thunk上,但是该thunk会被惰性地(稍后)求值。至少这是我对Haskell中惰性求值工作方式的直觉理解。 - Giorgio
显示剩余6条评论

2

弱类型语言也可以编译,例如Perl5和大多数Lisp版本都是编译语言。然而,编译带来的性能优势通常会因为语言运行时需要执行的大量工作与确定特定时间内动态变量的类型有关而丢失。

以Perl中的以下代码为例:

$x=1;
$x="hello";
print $x;

显然,编译器很难在特定时间确定$x$的真实类型。在打印语句时,需要做一些工作来弄清楚这一点。在静态类型语言中,类型是完全已知的,所以可以增加运行时性能。

编译器完全能够提供处理执行时出现的任何类型的代码。Visual Basic 6是一种非常流行的可编译语言,不需要类型声明 - 但如果您喜欢,也可以使用它们。 - dkretz
在VB6中,未指定的变量会被隐式地声明为变体类型。这与Perl中的标量类型大致相似,并且需要具有所有其他动态类型语言具有的幕后运作方式。 - 1800 INFORMATION

0
一个猜测:
在编译语言中,一个系统(编译器)可以看到执行强类型检查所需的所有代码。而解释器通常只能一次看到程序的一小部分,因此无法进行这种跨检查。
但这并不是一个绝对的规则 - 完全有可能创建一种具有强类型检查的解释型语言,但这将违背解释型语言的“松散”一般感觉。

问题是为什么你要这样做?这样C++编写者就不必理解他们的代码对计算机实际执行了什么,从而编写一堆低效代码吗? - user19302
因为一些程序员喜欢晚期绑定的自由,而另一些则喜欢编译器强制执行一些纪律的舒适感。在适当的位置上,我都喜欢。 - Paul Tomblin
即使在松散类型的语言中,将同一变量用于多种运行时类型或编写返回不同类型变量的函数/方法通常被认为是“不良实践”。那么为什么这是一件好事呢? - user19302
我喜欢 Perl 的一件事情是,我可以从文件中解析一个字符串,如果它看起来像一个数字,就可以直接使用它而不需要转换。这比编写 Integer.parseInt/catch NumberFormatException 要快得多。 - Paul Tomblin
一切都很好,直到你得到一个格式错误的文件,而你的程序却在各处愉快地使用零。 - Zan Lynx
@Zan,这就是为什么我有时会用 Perl,有时会用 Java。 - Paul Tomblin

0

有些编程语言旨在在非异常条件下运行完美,但这是以在异常条件下运行时遇到可怕的性能问题为代价的,因此需要非常强的类型。其他语言则是通过额外的处理来平衡它。

有时候,比起类型,还有更多的因素在发挥作用。以ActionScript为例,3.0引入了更强的类型,但ECMAScript又使你能够在运行时随意修改类,而ActionScript支持动态类。非常棒,但事实上,他们声称不应该在“标准”构建中使用动态类,这意味着当你需要保险时,这是不可取的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接