吃点东西有什么意义
类型不可变而其他类型不可变?
如果没有一些可变类型,您将不得不全力以赴进行纯函数式编程——与目前最流行的 OOP 和过程方法完全不同的范式,虽然非常功能强大,显然对许多程序员来说非常具有挑战性(当您确实需要在没有什么是可变的语言中产生副作用时会发生什么,而在实际编程中你当然不可避免地会这样做,这是挑战——例如,Haskell 的 Monads 是一种非常优雅的方法,但是您知道有多少程序员完全自信地理解它们并且可以使用它们以及典型的 OOP 构造?-)。
如果您不了解提供多种范式的巨大价值(FP 之一 和 都非常依赖可变数据),我建议您学习 Haridi 和 Van Roy 的杰作,Concepts, Techniques, and Models of Computer Programming - - “21 世纪的SICP”,正如我曾经描述的那样;-)。
大多数程序员,无论是否熟悉 Haridi 和 Van Roy,都会欣然承认至少有 一些 可变数据类型对他们来说很重要。尽管我从您的 Q 中引用了上面的句子,它采取了完全不同的观点,但我相信这也可能是您困惑的根源:不是“为什么每个”,而是“为什么一些不可变的”。
“彻底可变”的方法曾经(偶然地)在 Fortran 实现中获得。如果你有,比如说,
SUBROUTINE ZAP(I)
I = 0
RETURN
然后一个程序sn-p在做,例如,
PRINT 23
ZAP(23)
PRINT 23
会打印 23,然后是 0 -- 数字 23 已经发生了变异,所以程序其余部分中对 23 的所有引用实际上都将引用 0。这不是编译器中的错误,从技术上讲:Fortran 在将常量与变量传递给分配给其参数的过程时,对您的程序是什么以及不允许做什么有一些微妙的规则,而这个 sn-p 违反了那些鲜为人知的、非编译器可执行的规则,所以它是a 但在程序中,不在编译器中。当然,在实践中,这种方式导致的错误数量高得令人无法接受,因此典型的编译器很快就会在这种情况下转向破坏性较小的行为(如果操作系统支持,将常量放在只读段中以获得运行时错误;或者, 传递一个新的 copy 常量而不是常量本身,尽管有开销;等等)即使在技术上它们是允许编译器非常“正确”地显示未定义行为的程序错误;-) .
在其他一些语言中强制执行的替代方案是添加多种参数传递方式的复杂性——最明显的可能是在 C++ 中,按值、按引用、通过常量引用、通过指针、通过常量指针, ...然后你当然会看到程序员被 const foo* const bar 之类的声明所迷惑(如果 bar 是某个函数的参数,那么最右边的 const 基本上是无关紧要的......但如果 bar 是一个局部变量...!-).
实际上 Algol-68 可能沿着这个方向走得更远(如果你可以有一个值和一个引用,为什么不引用一个引用?或者引用引用到引用?&c -- Algol 68 对此没有任何限制,并且定义正在发生的事情的规则可能是在“旨在实际使用”的编程语言中发现的最微妙、最难的组合)。早期的 C(只有按值和按显式指针——没有const,没有引用,没有复杂性)无疑是对它的部分反应,就像原始的 Pascal 一样。但是const 很快就进来了,并发症又开始增加了。
Java 和 Python(以及其他语言)以强大的简单大砍刀穿过这个丛林:所有参数传递,和所有赋值,都是“通过对象引用”(从不引用变量或其他参考,绝不是语义上的隐含副本,&c)。将(至少)数字定义为语义上不可变的数字可以避免像上面的 Fortran 代码那样出现“错误”,从而保持程序员的理智(以及语言简单这一宝贵方面)。
将字符串视为原语就像数字一样与语言预期的高语义级别非常一致,因为在现实生活中我们确实需要像数字一样简单易用的字符串;诸如将字符串定义为字符列表 (Haskell) 或字符数组 (C) 等替代方法对编译器(在这种语义下保持高效性能)和程序员(有效地忽略这种任意结构以使字符串的使用变得简单)都提出了挑战原语,就像现实生活中的编程经常需要的那样)。
Python 更进一步,添加了一个简单的不可变容器 (tuple) 并将 散列 绑定到“有效的不变性”(这避免了程序员在 Perl 中发现的某些意外情况) ,它的哈希允许可变字符串作为键)——为什么不呢?一旦你有了不变性(一个宝贵的概念,它使程序员不必学习 N 种不同的语义来进行赋值和参数传递,N 会随着时间的推移而增加;-),你不妨充分利用它;-) .