动态类型语言与静态类型语言

207

10
这个问题过于主观。 - Jason Dagit
7
我不会称之为主观,但它可能会引起争议。不过,有一些客观事实与此相关。 - Vinko Vrsalovic
2
同意:太主观了。比较和对比这两种方法很有趣,但它危险地悬在论坛末日的边缘。 - Daniel Spiewak
17
动态语言适用于快速开发演示/丢弃应用,因为如果你打错字谁都不在乎,网页仍然可以加载,可能只是这里或那里有几个数据元素错误。我无法想象除此之外还有哪种情况会将能够打错变量而不会得到编译器错误的能力视为“优势”。 - Alex R
4
这种错误通常会使JavaScript停止运行,我认为这是一件非常好的事情。至少它会抛出错误,我也认为这很有价值。不知为何总是来自静态类型编程范式的人想用空的try/catch语句隐藏他们的javascript错误,这在我的经验中是一种现象。那是什么?无论如何,当我们运行我们的代码时,我们总会得到反馈。 - Erik Reppen
显示剩余2条评论
9个回答

140

解释器能够推断类型和类型转换,使得开发时间更快,但也可能引发运行时错误。相比之下,静态类型语言在编译时就能检测到这些错误。然而,哪种方法更好(甚至是否总是正确)已经成为社区内的热门讨论话题(长期以来如此)。

对于这个问题的一个很好的看法来自于微软的Erik Meijer和Peter Drayton所写的《Static Typing Where Possible, Dynamic Typing When Needed: The End of the Cold War Between Programming Languages》,可以从这里获得。

支持静态类型的人认为,静态类型的优点包括更早地发现编程错误(例如防止将整数添加到布尔值)、更好的文档形式(例如在解析名称时合并参数的数量和类型)、更多的编译器优化机会(例如,在静态知道接收方的确切类型时用直接调用替换虚拟调用)、增加运行时效率(例如不需要所有值都携带动态类型)以及更好的设计时开发者体验(例如,知道接收器的类型,IDE 可以呈现所有适用成员的下拉菜单)。支持静态类型的狂热者试图让我们相信“类型正确的程序不会出错”。虽然这听起来令人印象深刻,但它是一个相当空洞的陈述。静态类型检查是您程序运行时行为的编译时抽象,因此它必然只是部分正确且不完整的。这意味着由于类型检查器未跟踪的属性,程序仍可能出错,并且有些程序虽然不会出错,但无法进行类型检查。让静态类型变得不那么局限和更加完整的冲动会导致类型系统变得过于复杂和奇特,正如“幻影类型”[11]和“摇晃类型”[10]的概念所示。这就像试图拴上一只球和铁链来跑马拉松,并在第一英里后大喊大叫,即使你放弃了也几乎做到了。
动态类型语言的支持者认为静态类型过于严格,而动态语言的柔性使其非常适合原型系统,因为系统需要不断变化或还不确定需求,或者需要与其他无法预测地发生变化的系统(数据和应用程序集成)互动。当然,动态类型语言对于处理真正动态的程序行为(如方法拦截、动态加载、移动代码、运行时反射等)是不可或缺的。在 scripting 的母亲论文[16]中,John Ousterhout 认为,相对于动态类型的脚本语言,静态类型的系统编程语言使代码更不可重用、更冗长、不更安全,而且表达能力也更差。许多支持动态类型的脚本语言的支持者从字面上重复这个论点。我们认为这是一种谬论,并属于那些认为声明式编程的实质是消除 赋值 的人的同一类别。或者像 John Hughes 所说[8],通过省略功能来使语言更加强大是一种逻辑上的不可能。为了捍卫将所有类型检查延迟到运行时的好处,这是对事实掩耳盗铃,错误应尽早在开发过程中被捕捉。

37
"方法拦截,动态加载,移动代码,运行时反射",这些在Java中都可以实现,只是为了提一下。 - Will Hartung
13
"幽灵类型并不是过于复杂的。" - J D
12
截至2010年5月16日,Meijer论文的链接已经失效。 - jchadhowell
5
@jchadhowell,你可以在这里找到它 http://research.microsoft.com/en-us/um/people/emeijer/Papers/RDL04Meijer.pdf - Srinivas Reddy Thatiparthy
4
使用类型推断和多态的静态语言非常适合快速原型设计。它们提供了像动态语言一样的便利性和静态语言的安全性。 - Edgar Klerks
显示剩余12条评论

119
静态类型系统旨在消除某些错误,通过检查程序而不运行它并尝试在某些方面证明其正确性。一些类型系统能够捕获比其他类型系统更多的错误。例如,C#可以在正确使用时消除空指针异常,而Java没有这样的功能。Twelf具有一种类型系统,实际上保证证明将终止,"解决"了停机问题
然而,没有完美的类型系统。为了消除特定类别的错误,它们必须拒绝违反规则的某些完全有效的程序。这就是为什么Twelf实际上并没有解决停机问题,它只是通过丢弃大量以奇怪方式终止的完全有效的证明来避免它。同样,Java的类型系统由于Clojure的PersistentVector实现使用异构数组而拒绝它。它在运行时工作,但类型系统无法验证它。
因此,大多数类型系统提供"逃生",即覆盖静态检查器的方法。对于大多数语言,这些采用强制转换的形式,但一些语言(如C#和Haskell)具有标记为"不安全"的整个模式。
主观上,我喜欢静态类型。如果实现得当(提示:不是 Java),静态类型系统可以在崩溃生产系统之前帮助我们清除错误。动态类型语言往往需要更多的单元测试,这在最好的情况下也很繁琐。此外,静态类型语言可以具有某些特性,在动态类型系统中要么不可能,要么不安全(隐式转换 会出现)。这完全是一个需求和主观品味的问题。我不会用 Ruby 建立下一个 Eclipse,就像我不会尝试使用汇编语言编写备份脚本或使用 Java 打补丁内核一样。

哦,还有,那些说 "x 类型比 y 类型高效 10 倍" 的人只是在吹牛。在许多情况下,动态类型可能会“感觉”更快,但一旦你实际尝试使你的花哨应用程序运行,它就会失去优势。同样,静态类型可能看起来像是完美的安全网,但是看看 Java 中一些更复杂的通用类型定义,大多数开发人员都会拿到眼罩。即使有类型系统和生产力,也没有银弹。

最后注意:在比较静态和动态类型时不必担心性能。现代 JIT(例如 V8 和 TraceMonkey)已经接近静态语言的性能。此外,Java 实际上编译成一个固有动态中间语言的事实应该是一个提示,对于大多数情况来说,动态类型并不是一些人所说的巨大性能杀手。


5
关于性能。在普通情况下不会有太大的差异,但在高紧张的数学等情况下,有真正的区别。测试已经证明,在调用函数方面,ipy和C#之间的区别达到了一千个循环。这是因为前者必须确保方法存在。 - Dykam
29
你能详细说明一下“当使用得当时,C#可以消除空指针异常,而Java则没有这种能力。”的观点吗?提供一个示例或引用将会非常感激。 - Srinivas Reddy Thatiparthy
13
有些在Java中比较复杂的泛型类型定义会令许多开发者感到头疼。如果这已经是你最糟糕的情况,那显然你还没有使用过C++ ;-) - bacar
3
此外,Java实际上编译成固有的动态中间语言这一事实应该暗示着,在大多数情况下,动态类型并不像某些人所说的那样是巨大的性能杀手。当你将Java作为“性能良好”的语言示例时,你可能需要重新考虑。 - Yakk - Adam Nevraumont
Java编译成一个固有的动态中间代码,这点被忽略了。静态检查已经提前完成,因此不需要额外的运行时检查来弥补这些检查,编译器选择像dadd这样的指令是因为它提前知道操作数是double类型。 - Michael Beer
此外,在我的软件项目中需要处理实时要求,我同意Yakk的观点:与C++或C相比,Java速度较慢。这两种语言都不会(C的情况)或只部分地进行运行时类型检查(C++有点)。在C++中,由于性能问题,您会尝试避免运行时类型操作。 - Michael Beer

44
好的,两者都非常容易被误解,它们是两个完全不同的东西,但并不互相排斥。
静态类型是语言语法的一种限制。可以严格地说,静态类型语言不属于上下文无关语言。简单来说,如果使用上下文无关语法表达语言会变得很麻烦,因为它不能将所有数据都只视作位向量。如果有的话,静态类型系统是语言语法的一部分,但它比上下文无关语法更加严格,因此语法检查实际上需要对源代码进行两次检查。静态类型对应于数学中类型理论的概念,类型理论通常只是限制某些表达式的合法性。例如,在数学中我无法说 3 + [4,7] ,这是由其类型理论造成的。
从理论角度来看,静态类型并不是防止出错的方法,而是语法的限制。事实上,如果去掉类型系统,对于 3 + [4,7] 这样的表达式是有一个明确定义的结果的,即仍然是一个集合。理论上不存在“运行时类型错误”,类型系统在实践中的作用是防止操作对人类来说毫无意义。当然,这些操作仍然只是位的移动和操作。
然而,静态类型系统面临的问题在于,它无法决定是否将运行此类操作。即,准确地将所有可能程序分为那些会有“类型错误”的程序和那些不会有的程序。它只能做两件事:
1. 证明程序中会出现类型错误。 2. 证明程序中不会出现类型错误。
这可能看起来像自相矛盾。但 C 或 Java 类型检查器所做的是,如果不能成功实现第二点,就将程序拒绝为“不合法”,或者称之为“类型错误”。它不能证明这样的错误不会发生,这并不意味着这样的错误不会发生,只是它无法证明它不会发生。很可能会拒绝一些不会产生类型错误的程序,仅因为编译器无法一般化证明这些情况。一个简单的例子是 if(1) a = 3; else a = "string";,显然,由于它总是为真,在程序中else-branch将永远不会被执行,也不会发生类型错误。但它无法以一般方式证明这些情况,因此它被拒绝了。这就是许多静态类型语言的主要缺点,在保护你免受自己的错误时,你也必然会在不需要保护的情况下受到保护。

与普遍观念相反,也有按照原则1工作的静态类型语言。它们简单地拒绝所有可以证明会导致类型错误的程序,并通过所有不能证明会导致类型错误的程序。因此,它们允许具有类型错误的程序存在,一个很好的例子是 Typed Racket,它是动态和静态类型的混合体。有人认为这种系统获得了两个世界最好的优点。

静态类型的另一个优势是在编译时已知类型,因此编译器可以使用它。如果我们在 Java 中执行 "string" + "string"3 + 3,文本中的两个 +令牌代表完全不同的操作和数据,但编译器仅从类型中就知道选择哪一个。

现在,我要说出一个非常有争议的观点,请耐心听我说:'动态类型'不存在

听起来非常有争议,但事实如此,从理论上讲,动态类型的语言是无类型的。它们只是具有一种类型的静态类型语言。或者简单地说,它们是由上下文自由语法在实践中生成的语言。

为什么它们没有类型?因为每个操作都被定义并允许在每个操作数上执行。什么是'运行时类型错误'?从理论的角度来看,它只是一个副作用。如果 print("string") 是一个打印字符串的操作,那么 length(3) 也是一个操作,前者的副作用是将string写入标准输出,而后者只是error: function 'length' expects array as argument. ,就这样。从理论上讲,不存在动态类型语言。它们是无类型的。

好了,'动态类型'语言的明显优点是表达能力强,类型系统只是表达能力的限制。通常情况下,具有类型系统的语言确实会对所有不允许的操作具有定义的结果,如果忽略类型系统,则结果对人类来说毫无意义。许多语言在应用类型系统后失去了图灵完备性。

显而易见的缺点是操作可能会产生对人类来说无意义的结果。为了防止这种情况发生,动态类型语言通常重新定义那些操作,而不是产生无意义的结果,它们重新定义为具有写出错误的副作用,并可能完全停止程序。从理论上讲,这根本不是一个“错误”,事实上,语言规范通常意味着这一点,从理论上讲,这是语言的一种行为,就像打印字符串一样。类型系统因此强制程序员思考代码的流程,以确保这种情况不会发生。或者,确保这种情况发生也可以在某些调试点非常有用,显示这根本不是一个“错误”,而是语言的一个明确定义的属性。实际上,大多数语言都保护了一种遗留的“动态类型”:防止除零。这就是动态类型的含义,没有类型,除了零是与所有其他数字不同的不同类型之外,没有更多的类型。人们所谓的“类型”只是数据的另一个属性,如数组的长度或字符串的第一个字符。许多动态类型语言还允许您编写诸如“错误:此字符串的第一个字符应该是‘z’”之类的内容。
另一件事是,动态类型语言在运行时具有类型可用性,通常可以检查它并处理它并从中做出决定。当然,在理论上,它与访问数组的第一个字符并查看其内容没有什么不同。实际上,您可以制作自己的动态C,只使用一种类型,如long long int,并在其中使用前8个位来存储“类型”,然后编写相应的函数来检查它并执行浮点或整数加法。您有一个带有一个类型的静态类型语言或动态语言。
实践中表明,静态类型语言通常用于编写商业软件,而动态类型语言倾向于用于解决某些问题和自动化某些任务。在静态类型语言中编写代码往往需要很长时间且很繁琐,因为您不能做一些您知道会成功的事情,但类型系统仍然保护您免受错误的影响。许多编程人员甚至没有意识到这一点,因为它在他们的系统中,但是当您在静态语言中编码时,您经常绕过类型系统无法让您做无法出错的事情,因为它不能证明它不会出错。
正如我所指出的,“静态类型”通常意味着情况2,即蓄意谋杀。但是,一些不从类型理论派生其类型系统的语言使用规则1:无罪推定,直到被证明有罪,这可能是理想的混合体。因此,也许Typed Racket适合您。

另外,为了举一个更荒谬和极端的例子,我目前正在实现一种语言,在这种语言中,“类型”真正作为数组的第一个字符出现,它们是数据,“类型”的数据,即“类型”本身是一种类型和数据,是唯一的数据,它以自身为类型。类型不是静态有限或有界的,但根据运行时信息可以生成新类型。


1
许多编程语言在应用类型系统后失去了图灵完备性,但这并不适用于通常的编程语言,对吧?从我所读的内容来看,常规语言并不是图灵完备的。 - Răzvan Flavius Panda
1
@RăzvanPanda:Lajla可能是指的类型λ演算的变体或一些定理证明器中使用的编程语言。其中许多只能表达保证停机的程序,因此不是图灵完备的。基于这些类型系统的实用函数式编程语言通过扩展具有递归类型的核心演算来解决这个限制。 - hugomg
1
“听起来非常有争议,但事实是动态类型语言从理论上来看是未经类型检查的。”-- ……瞬间,我知道你不知道你在说什么。动态类型只意味着类型属于值而不是标识符。这使得程序更难以证明,但并非不可能。内联和参数多态性已经导致了链接时优化的开发;它解决了与编译最佳动态类型语言相同类型的问题:即知道所有可能的输入和输出。 - DeftlyHacked

24

动态类型最大的“好处”可能是学习曲线较浅。没有需要学习的类型系统,也没有像类型约束这样的非常规语法问题。这使得动态类型对更多人来说更加易于接受,也使其成为许多无法使用复杂静态类型系统的人可行的选择。因此,在教育(例如麻省理工学院的Scheme/Python)和面向非程序员的特定领域语言方面(例如Mathematica),动态类型已经被广泛采用。动态语言也在很少或没有竞争的领域上获得了成功(例如JavaScript)。

最简洁的动态类型语言(例如Perl、APL、J、K、Mathematica)是面向特定领域的,并且在它们设计的领域内可以比最简洁的通用静态类型语言(例如OCaml)更加简洁。

动态类型的主要缺点包括:

  • 运行时类型错误。

  • 实现相同程度的正确性可能非常困难,甚至实际上是不可能的,需要进行更多的测试。

  • 没有编译器验证的文档。

  • 性能较差(通常是在运行时,但有时也会在编译时,例如Stalin Scheme),并且由于依赖复杂的优化,性能很难预测。

个人而言,我从小就使用动态语言,但作为职业人士,除非没有其他可行的选择,否则不会碰它们。


2
我会说入门门槛较低,但掌握也同样需要学习曲线。 - Erik Reppen
学习曲线不是更低吗,因为你没有需要学习的类型系统吗? - J D
仍然存在类型系统。当你将布尔值和字符串相加时,你可以做出合理的猜测,但了解动态类型语言中类型强制转换的一些实际细节通常会有很大帮助。这就是许多只坚持严格类型的人所不理解的。我们实际上学习了这些东西。 - Erik Reppen
@ErikReppen:我们对“类型系统”的定义不同。我指的是不需要学习静态类型系统,例如代数数据类型、泛型等。而你所说的“类型”只是数据而已。一些函数在运行时拒绝某些数据的事实是普遍存在的。 - J D

12

根据Artima的文章Typing: Strong vs. Weak, Static vs. Dynamic:

强类型防止了不同类型之间混合操作,如果要混合类型,则必须使用显式转换

弱类型意味着您可以在不进行显式转换的情况下混合使用不同类型

Pascal Costanza在他的论文 Dynamic vs. Static Typing — A Pattern-Based Analysis (PDF)中指出,在某些情况下,静态类型比动态类型更容易出错。一些静态类型语言要求您手动模拟动态类型才能做到“正确”。这在Lambda the Ultimate上讨论过。


1
"静态类型比动态类型更容易出错" - 是的,是的,绝对正确!我在两种类型的语言中都有很多经验,在每种情况下,动态语言“只是工作”,而静态语言需要2倍的调试时间(例如C++和Delphi)。这通常是由于类型问题引起的,特别是在具有疯狂类型的模块和函数之间传递数据时。尽管动态语言据说可能会导致各种理论上的错误,但实际上,除非您是滥用动态类型的差劲程序员,否则我很少遇到由类型强制引起的错误。" - dallin
8
几年前我读过一篇Costanza的论文草稿。他在每个地方都写着“static”,但实际上他特指“Java”。我给他提供了很多反例,这些反例用OCaml等语言证明了他的观点是错误的,但他仍然发表了那篇论文。从那篇论文的样子来看,他仍在发表同样的胡言乱语。例如,在那篇论文中,他声称“C#通常是Java的劣质复制品”。这在科学论文中没有任何地位... - J D
@dallin 我的经验完全相反:我在C、C++、Java、Python、Perl等语言中进行了大量编程,除非被迫,否则我永远不会在动态类型语言中开始任何比小修补程序更大的项目。在Python中,当我想到一个WSGI项目时,我仍然会感到颤抖:我必须超负荷地处理回调函数,这些回调函数是通过对象引用传递的,而且代码似乎都很好,但最终崩溃了,因为有时传递的不是对象而是一些基本类型。这样容易创建错误代码的语言非常危险。 - Michael Beer
@MichaelBeer 你也可以说像C/C++这样的语言直接管理内存是非常危险的!我曾经为了解决内存错误而苦苦挣扎了数小时。庞大的Java项目也不是什么好玩意儿。在任何语言中,你都必须理解语言的危险性和良好的实践方法。我曾经参与过一些没有结构的PHP团队项目,那是我做过的最糟糕的项目,但我也曾经参与过使用良好框架和编程实践的动态语言项目,那是一个美梦。 - dallin
@dallin 同意,每种语言都有其缺陷。但我所指的缺陷是任何动态类型语言固有的,直接操作内存的可能性不是静态类型语言的固有属性。你可以想象一些动态类型语言让你直接操作内存。我同意 C++ 是一个灾难,语言发明人自己认为这个星球上没有一个人能够知道语言的所有部分。然而,这不能归咎于 C++ 是静态类型的,而是一个已经成长了30年的怪物... - Michael Beer
@MichaelBeer 说实话,我觉得大多数人对于静态类型语言或动态类型语言的赞成与反对,其实本质上都与静态和动态类型无关,而是这些语言类型中常见的设计决策。例如,静态类型语言不一定需要冗长或编译,但大多数确实如此。我还认为,通常与动态语言相关的绝大多数类型错误,实际上并非与其动态性有关,而是可以通过两个变化来修复:要求变量声明,并使0和"" != false。 - dallin

3
这取决于上下文。动态类型系统和强类型系统都有很多适用的优点。我认为动态类型语言的流程更快。动态语言不受类属性和编译器对代码的思考的限制,你有某种自由。此外,动态语言通常更具表现力,可以减少代码量,这是好的。尽管如此,它更容易出错,这也是值得怀疑的,并且更依赖于单元测试覆盖。使用动态语言易于原型设计,但维护可能会变成噩梦。
与静态类型系统相比,主要的收获是IDE支持和静态代码分析器。每次代码更改后,你会更加自信。使用这些工具可以轻松维护代码。

0

关于静态语言和动态语言有很多不同之处。 对我而言,主要的区别在于,在动态语言中,变量没有固定类型; 相反,类型与值相关联。 因此,在运行时确定执行的确切代码。

在早期或天真的实现中,这会对性能造成巨大的影响,但现代JIT非常接近优化静态编译器的最佳效果(在某些边缘情况下,甚至更好)。


0

这一切都与选用适当工具有关。没有哪种方法100%更好。两个系统都是人类创造的,而且都有缺陷。抱歉,我们不能制作完美的东西。

我喜欢动态类型,因为它不会干扰我的方式,但是是的,单元错误可能会悄悄地蔓延开来,这是我没有计划的。

相反,静态类型可能会修复前面提到的错误,但会让初学者(在有类型的语言中)疯狂地尝试将一个常量字符和字符串转换。


-3

静态类型: 像Java和Scala这样的语言是静态类型的。

变量必须在代码中定义和初始化后才能使用。

例如: int x; x = 10;

System.out.println(x);

动态类型: Perl 是一种动态类型的语言。

变量在使用之前不需要初始化。

y=10; 在代码的后面部分使用这个变量。


5
这与类型系统无关。 - Thiago Negri

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