蒂姆·霍利的回答非常正确,但我会详细说明一下。首先,让我们定义一些术语 - 你可能不同意我的定义,但至少你会知道我在说什么。在我看来,静态语言和动态语言之间的主要区别在于:在静态语言中,表达式具有类型;在动态语言中,值具有类型。
在静态语言中,有规则来确定程序中每个表达式的类型。表达式的类型决定程序的行为。不能为每个表达式确定一致类型的程序被认为是不正确的,并且无法编译。在存在多态性的情况下,表达式的类型可能不是单个具体类型:参数多态性可以被视为让相同的代码描述一组具体类型算法的方法,这些算法由类型的参数索引;子类型多态性可以被视为在否则静态语言中引入有限数量的动态行为。
另一方面,动态语言没有为表达式分配类型的规则:类型是通过程序执行时数据流方式暗示的。一般来说,表达式可能产生任何类型的值。因此,类型理论家有时将动态语言描述为“单类型”-即从静态的角度来看,“类型”本质上是表达式的属性,在动态语言中,所有表达式的类型都是Any。当然,这是将静态类型的概念(仅对表达式有意义)应用于类型仅对值有意义的语言。
Julia明确地位于动态阵营中:类型是值而不是表达式的属性。代码的结果类型是通过值在执行时如何流动来确定的;语言不包括在执行之前为表达式分配类型的任何规则。但与许多动态语言不同,Julia具有相当复杂的用于谈论类型的语言,并且您可以使用类型注释来注释表达式。例如,x::T是一个断言,即x是类型为T的值;如果这是真的,则x::T计算为x的值,否则会引发错误并且该表达式不返回值。方法签名中的类型注释具有稍微不同的含义:它们不是断言现有值的类型,而是指示仅当相应参数为指定类型时才适用该方法。无论哪种情况,以下代码都可以安全地假定x的值是类型T。
[旁白:在一些具有“渐进”或“可选”类型的语言中,类型注释将语言从动态模式切换到静态模式:没有类型注释的方法是动态的;有类型注释的方法是静态的。在静态代码中,有规则将所有表达式分配给类型,并且代码必须满足这些规则。这不是Julia的工作方式-具有类型注释的代码仍然是动态的,并且具有与没有类型注释的代码相同的语义。]
F#、OCaml 或 Haskell 等语言中的类型推断是确定表达式类型的一部分。如果编译器无法推断任何表达式的类型,则您的程序将无法编译。这些语言都使用某种形式的 Hindley-Milner 类型推断,这是一种非常巧妙的方法,可以从代码结构中推导出表达式的类型,而不必编写显式类型(与动态语言相比,动态语言的类型是由代码执行隐含的)。大多数情况下根本不需要任何类型注释,这与像 C++、C# 和 Java 这样需要冗长类型声明的语言相比非常愉快。然而,这与像 Julia 和 Python 这样的动态语言非常不同,其中不需要类型注释仅仅是因为表达式没有预定的类型是完全可接受的。在 Hindley-Milner 语言中,您可能不必像在 C++ 或 Java 中那样编写许多类型,但每个表达式都必须具有编译器可以计算的预定类型。
Julia 的编译器也进行类型推断,但它非常不同:并不是每个表达式都需要有可推断的类型。编译器分析代码以尝试预测表达式的类型,并使用该信息生成更高效的机器代码。但是,如果它无法确定表达式的类型,那没关系:编译器只会发出通用代码,该通用代码将使用运行时类型信息仍能正常工作。在 Julia 中,大部分情况下类型推断只是一种优化 - 您的代码将以相同方式工作,无论是否进行类型推断 - 但是通过成功的类型推断,它将运行得更快。