为什么我不应该在 Smalltalk 中存储到文字数组中?

Why shouldn't I store into literal arrays in Smalltalk?

一些风格指南和习语建议您不要改变文字数组,例如在这种情况下:

MyClass>>incrementedNumbers

    | numbers |
    numbers := #( 1 2 3 4 5 6 7 8 ).
    1 to: numbers size: do: [:index |
        numbers at: index put: (numbers at: index) + 1].
    ^ numbers

为什么我不应该那样做?

注意:以下是依赖于实现的。 ANSI Smalltalk Standard 定义:

It is unspecified whether the values of identical literals are the same or distinct objects. It is also unspecified whether the values of separate evaluations of a particular literal are the same or distinct objects.

也就是说,您不能指望两个(相等的)文字相同或不同。然而,下面是一个常见的实现

Squeak 和 Pharo 中的文字数组

至少在 Squeak 和 Pharo 中,文字数组是在保存(=编译)方法时构造的,并存储在内部方法对象(CompiledMethod)。这意味着更改文字数组会更改存储在方法对象 中的值 。例如:

MyClass>>example1

    | literalArray |
    literalArray := #( true ).
    literalArray first ifTrue: [
       literalArray at: 1 put: false.
       ^ 1].
    ^ 2

此方法 returns 1 仅在第一次调用时:

| o p |
o := MyClass new.
o example1. "==> 1"
o example1. "==> 2"
o example1. "==> 2"
p := MyClass new.
p example1. "==> 2"

这甚至与接收器无关。

但是,你不能依赖它,它在其他 Smalltalks 中可能会有所不同。

不同的方法

  1. 正在复制(始终安全)
    为了克服这个问题,您可以在使用前简单地复制文字数组。你的例子:

    MyClass>>incrementedNumbers
    
        | numbers |
        numbers := #( 1 2 3 4 5 6 7 8 ) copy. "<====== "
        1 to: numbers size: do: [:index |
            numbers at: index put: (numbers at: index) + 1].
        ^ numbers
    

    这总是安全的,不会改变方法对象中的数组。

  2. 支撑阵列(主要是便携式)
    虽然没有在标准中定义,但大多数实现都支持像这样的大括号数组表达式:

    { 1 . 'foo' . 2 + 3 }. 
    

    相当于:

    Array with: 1 with: 'foo' with: 2 + 3.
    

    这些数组是在执行时构造的(与文字数组相反),因此可以安全使用。又是你的例子:

    MyClass>>incrementedNumbers
    
        | numbers |
        numbers := { 1 . 2 . 3 . 4 . 5 . 6 . 7 . 8 }. "<====== "
        1 to: numbers size: do: [:index |
            numbers at: index put: (numbers at: index) + 1].
        ^ numbers
    

(Ab)使用文字数组

有时有理由实际改变文字数组(或更普遍的任何方法文字,坦率地说)。例如,如果你有静态信息,如图像或二进制数据,它们根本不会改变但并不总是被使用,但你不能(无论出于何种原因)使用实例或 class 变量,你 可能 在第一次使用时将对象存储在文字数组中:

MyClass>>staticInformation

    | holder |
    holder := #( nil ).
    holder first ifNil: [ holder at: 1 put: self generateBinaryData ].
    ^ holder first

ifNil: 检查只会在第一次执行该方法时为真,后续执行只会 return 期间 self generateBinaryData 编辑的值 return第一次调用。

这个模式被一些框架使用了一段时间。 然而,特别是对于二进制数据,大多数 Smalltalks(包括 Squeak 和 Pharo)现在支持 形式的 #[ … ] 文字字节数组 。该方法可以简单地写成

MyClass>>staticInformation

    ^ #[42 22 4 33 4 33 11 4 33 0 0 0 0 
        4 33 18 4 33 4 33 9 0 14 4 33 4 
        33 7 4 33 0 0 9 0 7 0 0 4 33 10
        4 33 4 33 7 4 33 0 0 9 0 7 0 0 4 
        " ... "
        33 10 4 33 4 33 17 0 11 0 0 4 33
        4 33 0 0 17 0 7 0 0 4 33 13 0]

在过去,当某些方法将文字数组(或字符串)分发给意外修改它的人时,它一直是一些混乱的根源。很难找到,因为源代码没有反映文字数组的内容。

因此,一些 Smalltalks(VisualWorks、Smalltalk/X 和其他)使文字不可变,并且会引发异常,当文字被写入时 (Smalltalk/X 允许将其关闭编译时间,以防您真的需要该功能以实现向后兼容性)。

我们公司多年来一直在使用这两种方案,我们确实不会错过或需要可变数组。我敢打赌,在不太未来的 Squeak 版本中,情况也会如此(如果尚未在队列中或某些更改文件中)。