关于C#:寻求对弱类型语言明显矛盾的澄清

Seeking clarification on apparent contradictions regarding weakly typed languages

我想我理解强类型,但是每次我查找弱类型的示例时,我都会找到一些编程语言的示例,这些编程语言可以简单地自动强制/转换类型。

例如,在本文中,名为"typing:strong vs.weak"的文章中,静态vs.dynamic表示python是强类型的,因为如果您尝试:

Python

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

但是,在Java和C语言中,这样的事情是可能的,我们不认为它们仅是弱类型的。

爪哇

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

C.*

1
2
3
4
int a = 10;
string b ="b";
string c = a + b;
Console.WriteLine(c);

在另一篇名为弱类型语言的文章中,作者说Perl是弱类型的,这仅仅是因为我可以将字符串连接到数字和viceversa,而无需任何显式转换。

珀尔

1
2
3
4
$a=10;
$b="a";
$c=$a.$b;
print $c; #10a

因此,同样的例子使Perl弱类型化,而不是Java和C?.

哎呀,这让enter image description here很困惑。

作者似乎暗示了一种阻止对不同类型的值应用某些操作的语言是强类型的,反之则意味着弱类型的。

因此,在某种程度上,我认为,如果一种语言提供了大量的自动转换或类型之间的强制转换(如Perl),最终可能会被认为是弱类型,而其他仅提供少数转换的语言最终可能会被认为是强类型。

不过,我倾向于相信,在这个有趣的描述中,我一定是错的,我只是不知道为什么或如何解释。

所以,我的问题是:

  • 对于一种语言来说,真正的弱类型化意味着什么?
  • 你能提到一些与语言的自动转换/自动强制无关的弱类型的好例子吗?
  • 一种语言能同时被弱类型化和强类型化吗?


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

What does it really mean for a language to be"weakly typed"?

它的意思是"这种语言使用了一种我觉得讨厌的类型系统"。相比之下,"强类型"语言是一种类型系统让我感到愉悦的语言。

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

您可以肯定地说,讨论中的"强类型"语言在类型系统中有一些额外的限制,无论是在运行时还是编译时,讨论中的"弱类型"语言都缺乏这种限制。如果没有进一步的背景,这个限制可能无法确定。

不要使用"强类型"和"弱类型",您应该详细描述您所指的类型安全性。例如,在大多数情况下,C是一种静态类型语言、一种类型安全语言和一种内存安全语言。C允许所有这三种形式的"强"类型都被破坏。cast运算符违反了静态类型;它对编译器说,"我比您更了解这个表达式的运行时类型"。如果开发人员是错误的,那么运行时将抛出一个异常以保护类型安全。如果开发人员希望破坏类型安全或内存安全,可以通过设置"不安全"块来关闭类型安全系统来实现。在不安全的块中,可以使用指针魔术将int视为float(违反类型安全),或者写入不属于您的内存。(违反记忆安全。)

C实施了在编译时和运行时都要检查的类型限制,因此与编译时检查较少或运行时检查较少的语言相比,它是一种"强类型"语言。C还允许您在特殊情况下围绕这些限制执行结束运行,与不允许执行此类结束运行的语言相比,它是一种"弱类型"语言。

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


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

要点是,在Perl中,没有"整型变量"、"浮点变量"、"字符串变量"或"布尔变量"这样的东西。事实上,就用户(通常)所知,甚至没有整数、浮点、字符串或布尔值:您所拥有的都是"scalars",它们都是同时存在的。例如,你可以写:好的。

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

当然,正如您正确指出的,所有这些都可以看作是类型强制。但关键是,在Perl中,类型总是被强制的。事实上,对于用户来说,很难判断变量的内部"类型":在上面的示例中,在第2行,询问EDOCX1的值(0)是字符串"9"还是数字9几乎没有意义,因为就Perl而言,它们是相同的。实际上,Perl标量甚至可以同时在内部同时具有字符串和数值,例如,上面第2行后面的$foo的情况。好的。

这一切的另一方面是,由于Perl变量是非类型化的(或者更确切地说,不要向用户公开其内部类型),因此不能重载运算符来为不同类型的参数执行不同的操作;不能只说"此运算符将对数字执行X操作,对字符串执行Y操作",因为运算符无法(不会)分辨哪种类型的val它的论点是。好的。

因此,例如,Perl有并且需要一个数字加法运算符(+和一个字符串连接运算符(.):正如您在上面看到的,添加字符串("1" +"2" =="3"或连接数字(1 . 2 == 12是完全可以的。同样,数值比较运算符==!=<>、EDOCX11〔12〕、EDOCX11〔13〕和EDOCX11〔14〕比较其参数的数值,而字符串比较运算符eq和EDOCX11〔16〕和EDOCX11〔17〕和EDOCX11〔18〕和EDOCX11〔10〕和EDOCX11〔11〕和EDOCX11〔12〕12〔12〕和EDOCX11〔13〕和EDOCX11〔14〕比较其参数的数值,而字符串比较运算符EDOCX11〔15〕和EDOCX11〔16〕和EDOCX11〔16〕和edcx11〔17〕和edcx11〔17、edcx11〔18和EDOCX121〕将它们作为字符串在词典上进行比较。因此,2 < 10,但2 gt 10(但"02" lt 10,而"02" == 2)。(请注意,某些其他语言,如javascript,在进行运算符重载的同时,也尝试适应Perl式的弱类型。这通常会导致丑陋,比如+的联想性丧失。)好的。

(这里的好处在于,由于历史原因,Perl5确实有一些角情况,比如位逻辑操作符,其行为取决于其参数的内部表示。这些通常被认为是一个恼人的设计缺陷,因为内部表示可能会因令人惊讶的原因而发生变化,因此预测这些操作员在给定情况下的行为可能很困难。)好的。

综上所述,有人可能会说Perl确实有强类型;它们只是不是您可能期望的类型。具体来说,除了上面讨论的"scalar"类型之外,Perl还具有两种结构化类型:"array"和"hash"。这些变量与scalar非常不同,以至于Perl变量有不同的符号来表示它们的类型(scalars的$,数组的@,hashes的%)1。这些类型之间有强制规则,因此您可以编写例如%foo = @bar,但其中许多都是有损的:例如,$foo = @bar将数组@bar的长度指定给$foo,而不是其内容。(此外,还有一些其他奇怪的类型,如typeglobs和I/O句柄,您不会经常看到它们暴露在外。)好的。

另外,在这个漂亮的设计中,有一个小小的缺陷就是引用类型的存在,这是一种特殊的标量(使用ref操作符,它可以与普通的标量区别开来)。可以将引用用作普通的标量,但它们的字符串/数值并不是特别有用,如果使用普通的标量操作修改它们,它们往往会失去它们的特殊引用性。此外,任何Perl变量2都可以被bless化为一个类,将其变成该类的对象;Perl中的OO类系统与上面描述的原始类型(或无类型)系统有些正交,尽管它在遵循duck类型范式的意义上也是"弱的"。一般的观点是,如果您发现自己在检查Perl中对象的类,那么您做了一些错误的事情。好的。

1实际上,sigil表示正在访问的值的类型,例如数组@foo中的第一个标量表示$foo[0]。有关更多详细信息,请参阅perlfaq4。好的。

Perl中的2个对象(通常)是通过对它们的引用来访问的,但是实际得到的blessed是引用指向的(可能是匿名的)变量。然而,祝福实际上是变量的一个属性,而不是它的值,因此,例如,将实际祝福变量分配给另一个变量,只会给您一个浅浅的、无瑕疵的变量副本。有关详细信息,请参阅Perlobj。好的。好啊。


除了Eric所说的以外,还要考虑以下C代码:

1
2
3
4
void f(void* x);

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

与Python、Cype、Java或WHATNOT等语言不同,上述类型是弱类型的,因为我们丢失了类型信息。埃里克正确地指出,在C中,我们可以通过强制转换来绕过编译器,有效地告诉它"我比你更了解这个变量的类型"。

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

使用类型擦除,不会发生这种情况——类型信息会被丢弃。在C语言中,对void*的强制转换可以做到这一点。在这方面,上述方法与c方法声明(如void f(Object x))有根本不同。

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

这是弱类型的。其他一切都只是静态类型与动态类型检查的问题,即检查类型的时间。


一个很好的例子来自维基百科的一篇关于强力打字的文章:

一般来说,强类型意味着编程语言对允许发生的混合施加了严格的限制。

弱分型

1
2
3
4
5
a = 2
b ="2"

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

强类型

1
2
3
4
5
6
7
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))。

这完全取决于打字的解释。


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

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

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

许多人最近把"明显类型"和"强类型"混淆了。"显式类型化"表示显式声明变量的类型。

虽然可以在布尔上下文中使用几乎任何内容,但python大部分是强类型的,布尔值可以在整数上下文中使用,并且可以在浮点上下文中使用整数。它并没有明显的类型化,因为您不需要声明您的类型(除了cython,它并不完全是python,尽管很有趣)。它也不是静态类型。

C和C++是显式的,静态类型的,并且有强类型的,因为你声明你的类型,类型是在编译时确定的,你可以混合整数和指针,或者整数和双倍,或者甚至将一个类型的指针转换成指向另一个类型的指针。

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


我想通过我自己对这个主题的研究来参与讨论,正如其他人的评论和贡献一样,我一直在阅读他们的答案并遵循他们的参考文献,我发现了有趣的信息。正如所建议的,这其中的大部分很可能会在程序员论坛上得到更好的讨论,因为它看起来更理论化而不是实用化。

从理论上讲,我认为Luca Cardelli和Peter Wegner写的关于理解类型、数据抽象和多态性的文章是我读过的最好的论据之一。

A type may be viewed as a set of clothes (or a suit of armor) that
protects an underlying untyped representation from arbitrary or
unintended use. It provides a protective covering that hides the
underlying representation and constrains the way objects may interact
with other objects. In an untyped system untyped objects are naked
in that the underlying representation is exposed for all to see.
Violating the type system involves removing the protective set of
clothing and operating directly on the naked representation.

此语句似乎表明弱类型可以让我们访问类型的内部结构,并像处理其他类型一样对其进行操作。也许我们可以用不安全的代码(Eric提到)或者用Konrad提到的C类型擦除指针来做什么。

文章继续……

Languages in which all expressions are type-consistent are called
strongly typed languages. If a language is strongly typed its compiler
can guarantee that the programs it accepts will execute without type
errors. In general, we should strive for strong typing, and adopt
static typing whenever possible. Note that every statically typed
language is strongly typed but the converse is not necessarily true.

因此,强类型意味着没有类型错误,我只能假设弱类型意味着相反:可能存在类型错误。在运行时还是编译时?这里似乎无关紧要。

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

另一方面,我能说的是EDCOX1 2和EDCOX1〔3〕(在爪哇)和InvalidCastException的余量,EDCOX1〔5〕((C)中)至少在编译时会显示弱分型的程度。埃里克的回答似乎与此一致。

在第二篇题为"typeful programming provided in one of the references provided in one of the answers in this question"的文章中,Luca Cardelli深入探讨了类型侵犯的概念:

Most system programming languages allow arbitrary type violations,
some indiscriminately, some only in restricted parts of a program.
Operations that involve type violations are called unsound. Type
violations fall in several classes [among which we can mention]:

Basic-value coercions: These include conversions between integers, booleans, characters, sets, etc. There is no need for type violations
here, because built-in interfaces can be provided to carry out the
coercions in a type-sound way.

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

基于此,Python、Perl、Java/Cype都不是弱类型的。

cardelli提到了两种类型的诽谤,我很好地考虑到了真正弱类型的情况:

Address arithmetic. If necessary, there should be a built-in (unsound) interface, providing the adequate operations on addresses
and type conversions. Various situations involve pointers into the
heap (very dangerous with relocating collectors), pointers to the
stack, pointers to static areas, and pointers into other address
spaces. Sometimes array indexing can replace address arithmetic.
Memory mapping. This involves looking at an area of memory as an unstructured array, although it contains structured data. This is
typical of memory allocators and collectors.

在C语言(由Konrad提到)或通过.NET中不安全的代码(由Eric提到)中可能出现这种情况,这确实意味着弱类型。

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


强类型<=>弱类型不仅与语言自动强制一个数据类型到另一个数据类型的值的大小有关,而且与实际值的类型有多强或有多弱有关。在Python和Java中,并且大多在C.*中,这些值具有它们的类型。在Perl中,没有那么多——实际上只有少数几个不同的值类型存储在一个变量中。好的。

让我们一个一个地打开箱子。好的。Python

在python示例1 +"1"中,+运算符为类型int调用__add__,将字符串"1"作为参数,但这导致未实现:好的。

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

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

1
2
3
4
>>> '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:好的。

1
2
>>> 'a' * 5
'aaaaa'

也就是说,好的。

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

操作不同的事实需要一些强类型的输入,但是与*相反,在乘法之前强制值为数字并不一定会使值弱类型化。好的。爪哇

Java示例EDCOX1 OR 11的作用仅仅是因为作为一个方便的事实,运算符EDCOX1(1)对字符串来说是过载的。Java EDCOX1×1操作符替换了EDCOX1×14的序列(参见此):好的。

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

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

Appends the string representation of the Object argument.

Ok.

The overall effect is exactly as if the argument were converted to a
string by the method String.valueOf(Object), and the characters of
that string were then appended to this character sequence.

Ok.

其中String.valueOf则好的。

Returns the string representation of the Object argument.
[Returns] if the argument is null, then a string equal to "null"; otherwise, the value of obj.toString() is returned.

Ok.

因此,这是一种完全没有语言强迫的情况——把所有的关注都委托给对象本身。好的。C.*

根据这里的乔恩Sketa回答,运算符EDCOX1的1Ω对于EDCOX1和19的类,甚至类似于Java,甚至不重载,这只是编译器产生的便利性,这得益于静态和强类型。好的。珀尔

正如Perldata解释的那样,好的。< Buff行情>

Perl有三种内置的数据类型:scalar、scalar数组和scalar的关联数组,称为"hashes"。标量是单个字符串(任何大小,仅受可用内存限制)、数字或对某个对象的引用(将在PerlRef中讨论)。普通数组是按数字索引的标量的有序列表,从0开始。哈希是由关联的字符串键索引的标量值的无序集合。好的。< /块引用>

但是,Perl没有用于数字、布尔值、字符串、nulls、undefineds、对其他对象的引用等的单独数据类型-它只有一种类型,即标量类型;0和"0"一样是标量值。一个被设置为字符串的标量变量实际上可以变成一个数字,如果在数字上下文中访问它,那么它的行为就与"仅字符串"不同。标量可以在Perl中保存任何内容,它与系统中存在的对象一样多。而在python中,名称只是引用对象,在perl中,名称中的标量值是可更改的对象。此外,面向对象的类型系统还粘在上面:Perl-scalars、List和Hash中只有3个数据类型。Perl中的用户定义对象是一个指向包的引用(即指向前面3个对象中的任何一个的指针),您可以获取任何此类值并在任何时候将其祝福给任何类。好的。

Perl甚至允许您随意更改值的类——这在Python中是不可能的,您需要在其中创建某个类的值,从而使用object.__new__或类似的方法显式构造属于该类的值。在python中,创建后不能真正改变对象的本质,在perl中,您可以做很多事情:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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,"
"
;
# all references to $val now belong to class Bar
bless \$val, Bar;
# thus this prints Bar=SCALAR(0x1c7d8c8)
print \$val,"
"
;
# 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,"
"
;
# 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,"
"
;

因此,类型标识弱绑定到变量,并且可以通过动态引用更改它。事实上,如果你这样做的话好的。

1
my $another = $val;

以东十一〔3〕没有阶级身份,即使以东十一〔4〕仍然会给出有福的参考。好的。DR

对Perl的弱类型化远不止是自动强制,更重要的是值本身的类型没有被设置为Stone,而不像python,它是动态的,但是类型非常强的语言。Python在EDCOX1(6)中给出EDCOX1×5的一个表示,表明语言是强类型的,即使做一些有用的相反的操作,如Java或C语言,并不排除它们是强类型语言。好的。好啊。


正如许多其他人所说,"强"和"弱"类型的整个概念都是有问题的。

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

我发现"静态"和"动态"类型的概念比"强"和"弱"类型更有用。静态类型语言在编译时已经计算出所有类型,如果不是,程序员必须显式声明。

与动态类型化语言不同,后者在运行时执行类型化。这通常是对多态语言的一个要求,因此关于两个对象之间的操作是否合法的决定不需要由程序员提前决定。

在多态、动态类型语言(如smalltalk和ruby)中,将"类型"视为"与协议的一致性"更为有用。如果一个对象与另一个对象一样遵循协议,即使这两个对象不共享任何继承、混合或其他voodoo,运行时系统也会将它们视为相同的"类型"。更准确地说,这样的系统中的对象是自主的,并且可以决定对引用任何特定参数的任何特定消息作出响应是否有意义。

想要一个能用描述蓝色的对象参数对消息"+"做出有意义的响应的对象吗?您可以在动态类型语言中这样做,但在静态类型语言中这是一个难题。


我喜欢@eric lippert的答案,但为了解决这个问题,强类型语言通常对程序中每个点的变量类型都有明确的了解。弱类型语言没有,因此它们可以尝试执行特定类型不可能执行的操作。它认为最简单的方法是在函数中看到它。C++:

1
void func(string a) {...}

已知变量a的类型为string,任何不兼容的操作都将在编译时捕获。

Python:

1
2
def func(a)
  ...

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