寻求对弱类型语言的明显矛盾的澄清

我认为我理解 很强的打字能力,但是每次我寻找弱类型的例子时,我最终都会找到一些简单地自动强制/转换类型的编程语言的例子。

例如,在这篇名为 类型: 强与弱,静态与动态的文章中指出,Python 是强类型的,因为如果尝试:

巨蟒

1 + "1"
Traceback (most recent call last):
File "", line 1, in ?
TypeError: unsupported operand type(s) for +: 'int' and 'str'

然而,这种情况在 Java 和 C # 中是可能发生的,我们并不认为它们仅仅是弱类型化的。

爪哇咖啡

  int a = 10;
String b = "b";
String result = a + b;
System.out.println(result);

C #

int a = 10;
string b = "b";
string c = a + b;
Console.WriteLine(c);

在另一篇名为 弱类型语言的文章中,作者说 Perl 是弱类型的,因为我可以将一个字符串连接到一个数字,反之亦然,而不需要进行任何显式转换。

Perl

$a=10;
$b="a";
$c=$a.$b;
print $c; #10a

所以同样的例子使 Perl 弱类型化,但不是 Java 和 C # 。

天啊,这太让人困惑了enter image description here

作者似乎暗示,阻止对不同类型的值应用某些操作的语言是强类型语言,而相反的意思是弱类型语言。

因此,在某种程度上,我觉得如果一种语言提供了很多类型之间的自动转换或强制转换(比如 perl) ,那么它最终可能会被认为是弱类型,而其他只提供少量转换的语言最终可能会被认为是强类型。

我倾向于相信,虽然,我必须在这个解释是错误的,我只是不知道为什么或如何解释它。

所以,我的问题是:

  • 真正弱类型的语言到底意味着什么?
  • 你能举出一些弱键入的好例子吗? 这些弱键入与语言的自动转换/自动强制无关?
  • 一种语言可以同时具有弱类型和强类型吗?
12729 次浏览

更新: 这个问题是2012年10月15日我博客的主题。谢谢你的问题!


“弱类型”语言的真正含义是什么?

它的意思是“这种语言使用一种我不喜欢的类型系统”。相比之下,“强类型”语言是一种具有类型系统的语言,我觉得这种语言令人愉快。

这些术语本质上是没有意义的,你应该避免使用它们。维基百科十一种不同的意思列为“强类型”,其中几个是相互矛盾的。这表明,在任何涉及“强类型”或“弱类型”的会话中,产生混淆的几率都很高。

您可以确定的是,讨论中的“强类型”语言在类型系统中有一些额外的限制,无论是在运行时还是编译时,这是讨论中的“弱类型”语言所没有的。如果没有进一步的上下文,就无法确定这种限制可能是什么。

不要使用“强类型”和“弱类型”,而应该详细描述所指的类型安全。例如,C # 是一种 静态输入语言、一种 打字保险箱语言和一种 记忆安全语言 大部分是。C # 允许违反所有这三种形式的“强”类型。强制转换操作符违反了静态类型; 它告诉编译器“我比您更了解这个表达式的运行时类型”。如果开发人员错误,则运行库将引发异常以保护类型安全。如果开发人员希望破坏类型安全或内存安全,他们可以通过制造一个“不安全”块来关闭类型安全系统。在不安全的块中,您可以使用指针魔法将 int 视为 float (违反类型安全)或写入您不拥有的内存。(违反记忆安全)

C # 强加了在编译时和运行时都会检查的类型限制,因此与编译时检查较少或运行时检查较少的语言相比,它是一种“强类型”语言。C # 还允许您在特殊情况下绕过这些限制执行最终运行,与不允许执行这种最终运行的语言相比,C # 使其成为一种“弱类型”语言。

到底是什么?这是不可能说的,它取决于说话人的观点和他们对各种语言特征的态度。

我喜欢 @ Eric Lippert 的回答,但是为了解决这个问题——强类型语言通常在程序的每个点都有变量类型的外显知识。弱类型语言则不会,因此它们可以尝试执行对于特定类型可能无法执行的操作。 它认为最简单的方法是在函数中看到这一点。 C + + :

void func(string a) {...}

已知变量 a是字符串类型的,任何不兼容的操作都将在编译时被捕获。

巨蟒:

def func(a)
...

变量 a可以是任何东西,我们可以有调用无效方法的代码,这只会在运行时被捕获。

维基百科上关于强打字的文章提供了一个完美的例子:

通常,强类型意味着编程语言对允许发生的混合设置了严格的限制。

弱打字

a = 2
b = "2"


concatenate(a, b) # returns "22"
add(a, b) # returns 4

强打字

a = 2
b = "2"


concatenate(a, b) # Type Error
add(a, b) # Type Error
concatenate(str(a), b) #Returns "22"
add(a, int(b)) # Returns 4

注意,弱类型语言可以混合使用不同的类型,而不会出现错误。强类型语言要求输入类型为预期类型。在强类型语言中,类型可以转换(str(a)将整数转换为字符串)或强制转换(int(b))。

这完全取决于对类型的解释。

除了 Eric 所说的,考虑下面的 C 代码:

void f(void* x);


f(42);
f("hello");

与 Python、 C # 、 Java 之类的语言相比,上述语言的类型是弱类型的,因为我们使用的是 输了类型信息。Eric 正确地指出,在 C # 中,我们可以通过强制转换绕过编译器,有效地告诉它“我比你更了解这个变量的类型”。

但即使这样,运行时仍然会检查类型!如果强制转换无效,运行时系统将捕获它并引发异常。

使用类型擦除,这种情况不会发生——类型信息被丢弃。C 语言的 void*模式就是这样。在这方面,上面的声明与诸如 void f(Object x)之类的 C # 方法声明有着根本的不同。

(从技术上讲,C # 还允许通过不安全的代码或编组进行类型擦除。)

这个 是弱类型的。其他一切都只是静态和动态类型检查的问题,也就是检查 什么时候类型的时间。

正如其他人所指出的,术语“强类型”和“弱类型”有如此多的不同含义,以至于你的问题没有单一的答案。但是,既然您在问题中特别提到了 Perl,那么让我试着解释一下 Perl 在什么意义上是弱类型的。

关键是,在佩尔,没有所谓的“整数变量”、“浮点变量”、“字符串变量”或“布尔变量”。事实上,就用户所知(通常) ,甚至没有整数、浮点数、字符串或布尔值 价值观: 所有的都是“标量”,它们同时都是这些东西。例如,你可以这样写:

$foo = "123" + "456";           # $foo = 579
$bar = substr($foo, 2, 1);      # $bar = 9
$bar .= " lives";               # $bar = "9 lives"
$foo -= $bar;                   # $foo = 579 - 9 = 570

当然,正如您正确地指出的,所有这些都可以被看作只是类型强制。但问题是,在佩尔,类型是 ABc4强制的。事实上,用户很难判断变量的内部“类型”是什么: 在我上面的例子的第2行,询问 $bar的值是字符串 "9"还是数字 9是没有意义的,因为就 Perl 而言,这是一回事是没有意义的。事实上,Perl 标量甚至可以在内部同时具有 都有字符串和数值,例如上面第2行之后的 $foo

另一方面,由于 Perl 变量是无类型的(或者,更确切地说,不向用户公开它们的内部类型) ,操作符不能被重载来为不同类型的参数做不同的事情; 你不能只说“这个操作符将为数字做 X,为字符串做 Y”,因为操作符不能(不会)告诉它的参数是哪种类型的值。

因此,例如,Perl 同时具有并需要一个数值加法运算符(+)和一个字符串连接运算符(.) : 正如您在上面看到的,添加字符串("1" + "2" == "3")或连接数字(1 . 2 == 12)是完全可以的。类似地,数值比较操作符 ==!=<><=>=.0比较它们参数的数值,而字符串比较操作符 .1、 .2、 .3、 .4、 .5、 .6和 .7按字符串的形式比较它们。所以 .8,但是 .9(但是 "1" + "2" == "3"0,而 "1" + "2" == "3"1)。(请注意,某些 "1" + "2" == "3"3语言,比如 JavaScript,试图在 "1" + "2" == "3"4做运算符重载的时候适应类似 Perl 的弱键入。这通常会导致丑陋,比如 +失去联想性。)

(这里的美中不足之处在于,由于历史原因,Perl 5确实有一些角落情况,比如按位逻辑运算符,它们的行为取决于其参数的内部表示。这些通常被认为是一个恼人的设计缺陷,因为内部表示可能因为令人惊讶的原因而发生变化,所以预测这些操作符在给定情况下会做什么是很棘手的。)

所有这些都说明,Perl是的具有强类型; 它们只是不是您可能期望的那种类型。具体来说,除了上面讨论的“标量”类型之外,Perl 还有两种结构化类型: “ array”和“ hash”。这些是不同于标量的 非常,以至于 Perl 变量有不同的 印记来表示它们的类型(标量为 $,数组为 @,散列为 %) @0。这些类型之间存在 @1强制规则,所以 @2编写比如 %foo = @bar,但是其中许多是相当有损耗的: 例如,$foo = @bar将数组 @bar@3分配给 $foo,而不是它的内容。(此外,还有一些其他奇怪的类型,比如类型 globs 和 I/O 句柄,您通常不会看到暴露。)

此外,这个漂亮设计中的一个小缺陷是引用类型的存在,它是一种特殊的标量(使用 ref运算符将 可以与普通标量区分开来)。可以将引用用作普通标量,但它们的字符串/数值并不特别有用,如果使用普通标量操作修改它们,它们往往会失去特殊的引用性。此外,任何 Perl 变量 2都可以被 bless化为一个类,将其转换为该类的一个对象; Perl 中的 OO 类系统在某种程度上与上面描述的基本类型(或无类型)系统正交,尽管在遵循 鸭子打字范式的意义上它也是“弱”的。一般的观点是,如果你发现自己在佩尔检查一个对象的类,那么你就做错了。


1 实际上,这个符号表示被访问的值的类型,例如,数组 @foo中的第一个标量表示 $foo[0]。有关详细信息,请参阅 Perlfaq4

2 Perl 中的对象(通常)通过对它们的引用来访问,但实际上获得 blessed 的是引用指向的(可能是匿名的)变量。然而,祝福确实是变量的一个属性,它的值是 没有,例如,将实际的祝福变量赋给另一个变量只是给你一个浅表的,不祝福的副本。有关详细信息,请参阅 Perlobj

我想以我自己对这个主题的研究来参与讨论,当其他人评论和贡献时,我一直在阅读他们的答案,跟踪他们的参考资料,并且我发现了有趣的信息。正如所建议的那样,这些内容中的大部分可能会在程序员论坛中得到更好的讨论,因为它们似乎更多的是理论上的,而不是实际上的。

从理论的角度来看,我认为卢卡 · 卡德利和彼得 · 韦格纳关于 理解类型、抽象化和多态性的文章是我读过的最好的论据之一。

一种类型可以被视为一套衣服(或一套盔甲) 保护底层 无法打印表示不受任意或 它提供了一个保护性的覆盖物来隐藏 基底形式和约束对象可能的交互方式 在无类型系统中,无类型对象是 裸体 因为基底形式暴露在众目睽睽之下。 违反类型系统包括移除 直接对裸体表现进行操作。

该语句似乎表明,弱类型可以让我们访问类型的内部结构,并像操作其他类型(另一种类型)一样操作它。也许我们可以使用不安全的代码(Eric 提到过)或者 Konrad 提到的 c 类型擦除指针。

文章继续..。

调用所有表达式都为 类型一致的语言 强类型语言。如果一种语言是强类型的,它的编译器 可以保证它接受的程序将不使用类型执行 一般来说,我们应该努力强打字,并采用 静态类型。请注意,每个静态类型 语言是强类型的,但反之则不一定正确。

因此,强类型意味着没有类型错误,我只能假设弱类型意味着相反的情况: 可能存在类型错误。在运行时还是编译时?看起来无关紧要。

有趣的是,根据这个定义,像 Perl 这样具有强大类型强制的语言会被认为是强类型语言,因为系统并没有失败,而是通过将类型强制转换为适当且定义良好的等价物来处理类型。

另一方面,我是否可以说 ClassCastExceptionArrayStoreException(在 Java 中)以及 InvalidCastExceptionArrayTypeMismatchException(在 C # 中)的允许值表明了弱类型的级别,至少在编译时是这样的?埃里克的回答似乎同意这一点。

在这个问题的一个答复中提供的一个参考资料中提供的名为 类型化编程的第二篇文章中,Luca Cardelli 深入探讨了类型违反的概念:

大多数系统编程语言允许任意类型冲突, 有些是不分青红皂白的,有些只是在程序的限制部分。 涉及类型冲突的操作称为 unsound 侵权行为分为几类[其中我们可以提到] :

基本值强制 : 这些强制包括整数、布尔值、字符、集合等之间的转换 因为可以提供内置接口来执行 以类型合理的方式进行胁迫。

因此,由运算符提供的类型强制可以被认为是类型冲突,但是除非它们破坏了类型系统的一致性,否则我们可以说它们不会导致弱类型系统。

基于此,Python、 Perl、 Java 或 C # 都不是弱类型。

Cardelli 提到了两种类型的错误,我认为这两种类型是真正的弱类型:

如果需要,应该有一个内置的(不健全的)接口,提供足够的地址操作 和类型转换。各种情况都涉及到指向 堆(重新定位收集器非常危险) ,指向 堆栈、指向静态区域的指针以及指向其他地址的指针 有时数组索引可以取代地址算术。 内存映射。这涉及到将一个内存区域看作一个非结构化数组,尽管它包含结构化数据。这才是 典型的内存分配器和收集器。

这种事情在 C 语言(Konrad 提到过)或者在。Net (由 Eric 提到)确实意味着弱键入。

我相信到目前为止最好的答案是 Eric 的,因为这个概念的定义是非常理论化的,当涉及到一种特定的语言时,对所有这些概念的解释可能会导致不同的有争议的结论。

正如许多其他人所表达的,“强”与“弱”类型的整个概念是有问题的。

作为原型,Smalltalk 是非常强类型化的——如果两个对象之间的操作不兼容,它将引发异常。然而,我怀疑在这个列表中很少有人会称 Smalltalk 为强类型语言,因为它是 动态类型化。

我发现“静态”与“动态”类型的概念比“强”与“弱”更有用静态类型语言具有在编译时指定的所有类型,如果不是这样,程序员必须显式声明。

与在运行时执行类型化的动态类型化语言形成对比。这通常是多态语言的一个要求,因此关于两个对象之间的操作是否合法的决定不必由程序员事先决定。

在多态的动态类型语言(如 Smalltalk 和 Ruby)中,将“类型”看作“与协议的一致性”更为有用如果一个对象和另一个对象遵守同样的协议——即使这两个对象不共享任何继承、 Mixin 或其他巫术——它们被运行时系统认为是相同的“类型”。更准确地说,这类系统中的对象是自治的,可以决定是否有必要响应引用任何特定参数的任何特定消息。

想要一个对象,可以作出一些有意义的响应消息“ +”与对象参数,描述颜色蓝色?您可以在动态类型的语言中这样做,但是在静态类型的语言中这是一个痛苦的过程。

弱类型确实意味着很高比例的类型可以被隐式强制,试图猜测编码者的意图。

强类型化意味着类型不是被强制的,或者至少不是被强制的。

静态类型意味着变量的类型是在编译时确定的。

最近许多人把“明显类型”和“强类型”混为一谈。“清单类型化”意味着显式声明变量的类型。

Python 主要是强类型的,尽管您可以在布尔型上下文中使用几乎任何东西,布尔型可以在整型上下文中使用,并且您可以在浮点型上下文中使用整型。它不是显式类型化的,因为您不需要声明类型(除了 Cython,它不完全是 python,尽管它很有趣)。它也不是静态类型化的。

C 和 C + + 是显式类型、静态类型和某种强类型,因为你声明你的类型,类型是在编译时确定的,你可以混合整数和指针,或整数和双精度,甚至将指向一种类型的指针强制转换为指向另一种类型的指针。

Haskell 是一个有趣的例子,因为它不是显式类型化的,但它也是静态的和强类型化的。

强 < = > 弱类型不仅关系到一种数据类型的语言自动将多少值强制转换为另一种数据类型,而且关系到实际 价值观的类型是强还是弱。在 Python 和 Java 中,主要是在 C # 中,值的类型是固定的。在 Perl 中,情况并非如此——实际上只有少数几种不同的值类型可以存储在一个变量中。

让我们一个一个地打开这些箱子。


巨蟒

在 Python 示例 1 + "1"中,+操作符为 int类型调用 __add__,并给出字符串 "1"作为参数——然而,这会导致 Not實现:

>>> (1).__add__('1')
NotImplemented

接下来,解释器尝试 str 的 __radd__:

>>> '1'.__radd__(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute '__radd__'

当它失败时,+操作符失败,结果为 TypeError: unsupported operand type(s) for +: 'int' and 'str'。因此,这个异常并没有说明什么强类型,但是操作符 + 不会强迫的参数自动指向相同类型的事实,是一个指针,表明 Python 不是连续体中最弱类型的语言。

另一方面,在 Python 中实现了 'a' * 5 :

>>> 'a' * 5
'aaaaa'

就是,

>>> 'a'.__mul__(5)
'aaaaa'

操作不同的事实需要一些强类型化——然而,与 *在乘法之前将值强制转换为数字相反,仍然不一定会使值弱类型化。


爪哇咖啡

在 Java 示例中,String result = "1" + 1;之所以能够工作,是因为为了方便起见,操作符 +对于字符串是重载的。Java+操作符用创建一个 StringBuilder来替换序列(参见 这个) :

String result = a + b;
// becomes something like
String result = new StringBuilder().append(a).append(b).toString()

这是一个非常静态类型的例子,没有实际的强制—— StringBuilder有一个 append(Object)方法,这里专门使用。文件说明如下:

追加 Object参数的字符串表示形式。

整体效果与将参数转换为 方法 String.valueOf(Object)的字符串,以及 这个字符串随后被附加到这个字符序列。

那么 String.valueOf在哪里

返回 Object 参数的字符串表示形式。 如果参数为 null,则返回等于 "null"的字符串; 否则返回 obj.toString()的值。

因此,这是一个绝对没有强制的情况下的语言-委派的每一个关注对象本身。


C #

根据 乔恩 · 斯基特,请回答,对于 string类,操作符 +甚至没有重载——类似于 Java,这只是由编译器生成的便利,这归功于静态类型和强类型。


Perl

Perldata解释道,

Perl 有三种内置的数据类型: 标量、标量数组和标量的关联数组,称为“散列”。标量是一个单独的字符串(任何大小,仅受可用内存的限制)、数字或对某些内容的引用(将在 perlref 中讨论)。普通数组是按数字索引的标量的有序列表,从0开始。哈希是标量值的无序集合,标量值由其相关的字符串键索引。

然而,Perl 并没有单独的数据类型,比如数字、布尔值、字符串、空值、 undefined、对其他对象的引用等等——它只有一种类型,即标量类型; 0是标量值,也就是“0”。被设置为字符串的标量 变量可以真正地变成一个数字,从那时起,它的行为就与“只是一个字符串”如果它是在数字上下文中访问的不同了。在佩尔,标量可以容纳任何东西,它既是系统中存在的对象,也是系统中存在的对象。而在 Python 中,名称只是指对象,而在 Perl 中,名称中的标量值是可更改的对象。此外,面向对象类型系统也是如此: perl 标量、列表和散列中只有3种数据类型。Perl 中用户定义的对象是指向一个包的 bless引用(指向前面3个对象中的任何一个)——您可以在任何时候获取任何这样的值并将其保佑到任何类。

Perl 甚至允许您随心所欲地更改值的类——这在 Python 中是不可能的,因为在 Python 中,要创建某个类的值,您需要用 object.__new__或类似的方法显式构造属于该类的值。在 Python 中,在创建之后你不能真正改变对象的本质,在 Perl 中,你可以做很多事情:

package Foo;
package Bar;


my $val = 42;
# $val is now a scalar value set from double
bless \$val, Foo;
# all references to $val now belong to class Foo
my $obj = \$val;
# now $obj refers to the SV stored in $val
# thus this prints: Foo=SCALAR(0x1c7d8c8)
print \$val, "\n";
# all references to $val now belong to class Bar
bless \$val, Bar;
# thus this prints Bar=SCALAR(0x1c7d8c8)
print \$val, "\n";
# we change the value stored in $val from number to a string
$val = 'abc';
# yet still the SV is blessed: Bar=SCALAR(0x1c7d8c8)
print \$val, "\n";
# and on the course, the $obj now refers to a "Bar" even though
# at the time of copying it did refer to a "Foo".
print $obj, "\n";

因此类型标识弱绑定到变量,并且可以通过任何动态引用对其进行更改。事实上,如果你想的话

my $another = $val;

\$another没有类标识,即使 \$val仍然会给出受祝福的引用。


DR

对于 Perl 而言,弱类型化不仅仅是自动强制,而且更多的是关于值本身的类型不是固定的,与 Python 不同,Python 是动态但非常强类型化的语言。Python 在 1 + "1"上给出的 TypeError表明这种语言是强类型的,即使与此相反,在 Java 或 C # 中做一些有用的事情并不排除它们是强类型语言。