【问题标题】:Why shouldn't I store into literal arrays in Smalltalk?为什么我不应该在 Smalltalk 中存储到文字数组中?
【发布时间】:2015-04-30 09:45:48
【问题描述】:

一些风格指南和习语建议你不应该改变文字数组,就像在这种情况下:

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

为什么我不应该这样做?

【问题讨论】:

    标签: smalltalk pharo squeak


    【解决方案1】:

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

    未指定相同文字的值是相同还是不同的对象。特定文字的单独评估的值是相同还是不同的对象也未指定。

    也就是说,您不能依赖两个(相等的)文字相同或不同。但是,下面是一个常见的实现

    Squeak 和 Pharo 中的文字数组

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

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

    此方法仅在第一次调用时返回 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)使用文字数组

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

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

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

    这种模式被一些框架使用了一段时间。 但是,特别是对于二进制数据,大多数 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]
    

    【讨论】:

    • 请注意,除了改变数组之外,您还可以做更多的意图揭示^ numbers collect: [ :each | each + 1 ]
    • 正确且更好。我这样写只是为了展示效果:)
    【解决方案2】:

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

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

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

    【讨论】:

    • 这是个好主意。然而,存储到文字中目前是一种让系统在改变类形状的同时保持工作的方式,例如。
    • 当然,但是 ST/X 和 VW 都有一个 makeMutable / beMutable,你可以在这些事情之前使用它(然后让它可变)。这具有记录您的意图的额外好处,您可以搜索它的发件人;-)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-11
    • 2017-02-23
    • 2023-03-18
    • 1970-01-01
    相关资源
    最近更新 更多