论编程界的日经问题:到底如何区分静态类型和动态类型、强类型和弱类型?
我发现在我加的一些编程交流群里,几乎每半个月就会产生这样的一些争论:"Python 到底是强类型语言还是弱类型语言","为什么 JavaScript 是弱类型语言","动态类型语言和静态类型语言的区别是什么"...... 这些争吵喋喋不休,大多很难有一个确定的共识。
其实大家很难争吵出共识是很正常的,因为对于静态类型和动态类型,强类型和弱类型这些概念来说,他们本身就没有什么确定的概念,大家基于一个模糊的概念各说各的,自然得不出一个确切的答案。
但是如果我们按照一些已有的共识来重新规范一下两对类型的概念,那么其实还是很容易得出答案的。
值得一提的是,无论是静态类型和动态类型,还是强类型和弱类型,这些概念都是基于语言的语法这一层次来定义的,而不是语言的内部设计,否则我们大可以说:"所有语言最后都是由 0 和 1 组成的",那么就没有办法再谈什么"类型"了。
强类型和弱类型
有关强类型和弱类型的定义大都比较模糊,这里我采用 Wikipedia 上的一个结论:
强类型的语言遇到函数参数类型和实际调用类型不符合的情况经常会直接出错或者编译失败;而弱类型的语言常常会实行隐式转换,或者产生难以意料的结果。
先说结论,以下语言属于强类型:C#
, Java
, Scala
, Kotlin
, Groovy
, rust
, go
, Python
, TypeScript
,而这些语言则属于弱类型:C
, C++
, JavaScript
, PHP
。
我相信一部分人看到这个分类的时候一定已经开始有一些疑问了,别急,让我们慢慢道来......
Python 为什么是强类型
很多人觉得 Python 不是一个强类型的语言,因为其在变量声明时不需要指定类型,也很少见到 Python 提及"类型"这一概念。但其实,Python 是一门强类型的动态类型语言 ,虽然在变量声明时我们不需要显式指定类型,但是"类型"这一概念是实际存在的,举个例子,以下 Python 代码会获得一个 TypeError
:
arduino
>>> 1+""
Traceback (most recent call last):
File "<pyshell#0>", line 1, in <module>
1+""
TypeError: unsupported operand type(s) for +: 'int' and 'str'
这是因为我们将 int
类型和 str
类型相加导致的,Python 不知道应该如何将这两种类型相加。但是反观经典弱类型语言 JavaScript 会如何处理:
arduino
> 1+""
< '1'
很显然,JavaScript 愉快的为这两种不同类型的变量做了隐式的类型转换,而此类类型转换在 JavaScript 中屡见不鲜,甚至沦为笑谈,而这一切都是弱类型的锅。
C, C++ 为什么是弱类型
有些人看到 C 和 C++ 是弱类型的时候可能会大吃一惊,怎么可能,C 和 C++ 明明拥有严格的变量类型标注才对!
但是想想 void*
,想想数组传参时的指针弱化,他们都证明了 C 和 C++ 会随时进行隐式类型转换,而这种隐式类型转换在 C 和 C++ 中仍然是无处不在,这也是它们被称为弱类型语言最好的佐证。
警惕!语法糖不是弱类型
经过上面的介绍,你可能会联想到 Java 在字符串连接时可以由不同的类型,例如:
ini
String a = 1 + "" // "1"
或者在 Python 中,也可以在流程控制表达式中使用非 bool
类型:
bash
if 1:
print("hit")
else:
print("not hit")
但他们实际上都是语法糖而已,Java 的字符串连接是自动装箱和 StringBuilder 的语法糖,而 Python 则是为所有类型隐式调用了 __bool__
属性得到 bool
类型而已。这都不能说明这个语言的类型系统是弱类型。
静态类型和动态类型
我们一般认为以下语言是静态类型语言:C
, C++
, C#
, Java
, Scala
, Kotlin
, rust
, go
,而这些语言则属于动态类型:Python
, JavaScript
, TypeScript
, PHP
, Groovy
。
其实动态类型语言和静态类型语言的区别主要是:变量类型是在编译期确定还是在运行时确定。如何理解?在 Python 中尝试以下代码:
ini
a = 1
a = ""
显而易见的,这段代码可以正常被运行,但是注意到了吗,a
变量的类型从 int
变为了 str
(这同时也佐证了 Python 是一门强类型的语言,虽然其不需要显式声明变量类型,但是强类型定义的系统是内部存在的),那么这样的代码在 Java 中能否正确运行呢?尝试在 jshell 中执行试试:
ini
| Welcome to JShell -- Version 17.0.2
| For an introduction type: /help intro
jshell> var a = 1
a ==> 1
jshell> a = ""
| Error:
| incompatible types: java.lang.String cannot be converted to int
| a = ""
| ^^
jshell>
在这里我特意用了 var
关键字来声明一个变量,而不是显式声明变量类型,是想表明一个观点:动态类型和变量类型推断是完全不同的两个东西,虽然 Java 提供了 var
关键字让我们可以无须显式指定一个变量的类型,但是该变量类型依然在编译期就会被确定下来;上例 a
变量的类型被推断为 int
,因此就不能再被赋值为 java.lang.String
对象,所以产生了编译错误。
当然,这里我们还需要讨论两个边界情况:
C# 的 dynamic
关键字
C# 存在一个 dynamic
关键字,使用 dynamic
关键字标注的变量的类型推断和函数调用检查都会被从编译期推迟到运行时,以下代码在 C# 中会引发报错:
go
C# > var a = 1;
C# > a = "";
❌ Microsoft.DotNet.Interactive.CodeSubmissionCompilationErrorException: (1,5): error CS0029: Cannot implicitly convert type 'string' to 'int'
但是以下代码可以正常运行:
shell
C# > dynamic b = 1;
C# > b = ""
当然,即便如此,我们仍然认为 C# 是一个静态类型语言。
rust 的 variable shadowing
在 rust
中,你可以在同一作用域中重复声明多个名称相同的变量,后者则会代替前者:
ini
let spaces = " "; // &str
let spaces = spaces.len(); // usize
仔细看,这可不是什么动态类型!两个变量的名字虽然相同,但是并没有进行重新赋值,而是后者作为一个新的变量代替了前者。如果转用赋值,则会得到报错:
ini
let spaces = " "; // &str
spaces = spaces.len(); // usize
error[E0308]: mismatched types
--> src\main.rs:3:14
|
2 | let spaces = " "; // &str
| ----- expected due to this value
3 | spaces = spaces.len(); // usize
| ^^^^^^^^^^^^ expected `&str`, found `usize`
Python 的 type hint
Python 在其 3.5 版本引入了一个名为 typing
的功能,可以为 Python 函数提供函数参数和返回值类型声明:
python
def greeting(name: str) -> str:
return 'Hello ' + nam
你可能会认为在这种情况下 Python 成为了一个静态类型的语言,但是实际上这种 type hint 只是一个暗示(正如 hint
的意思),可以被其他第三方工具采用,但并不会被 Python 运行时强制使用(Note: The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc. ),这也就是说以下代码完全可以正常运行:
python
>>> def print_me(me: int):
... print(me)
...
>>> print_me("Hello, World")
Hello, World
最后
之所以想写这篇文章是因为今天某个群又因为这个问题吵起来了,但好在最后大家还是达成了一个很好的共识的。讨论之末,有人问了一个很有意思的问题:"我一直想知道了解语言的 typing system 分类对工程应用有什么帮助",这确实引发了我的一些思考,即使我们争论的喋喋不休,又或者终于达成了某种共识,那么这种结果对我们的工程开发有什么实际的意义吗?
经过简单的思考后,我给出了一个同样简单的答案:
屁用不顶,说是八股也不是不行。
所以这种讨论还是少点好吧(