问题描述
我对以下两个代码的结果感到困惑:
代码 1:
>> f: func [x][head insert x 1]
== func [x][head insert x 1]
>> a: [2 3]
== [2 3]
>> f a
== [1 2 3]
>> a
== [1 2 3] ;; variable a is destroyed
代码 2:
>> g: func [x y][x: x + y]
== func [x y][x: x + y]
>> c: 1 d: 2
== 2
>> g c d
== 3
>> c
== 1
>> d
== 2
;;variable c and d keep their original values
我的问题是:Red/Rebol 中的函数如何通过值或引用获取它们的参数?
解决方法
这是个好问题。答案是:参数是按值传递的,但有些参数中可能包含引用。
Rebol/Red 中的每个值都表示为大小一致的盒装结构,通常称为 slot 或 cell。该结构在逻辑上分为 3 部分:header、payload 和 extra。
┌─────────┐
│ header │
├─────────┤ Historically,the size of the value slot is 4 machine pointers in size,│ payload │ 1 for header,1 for extra,and 2 for payload.
│ │ E.g. on 32-bit systems that's 128 bits,or 16 bytes.
├─────────┤
│ extra │
└─────────┘
- 标头包含各种元信息,有助于识别有效负载包含的值,最重要的部分是类型标记,或者用 Rebol 的话说,是数据类型 ID。
- 有效载荷包含一些值的数据表示,例如数字、字符串、字符等。
- 额外的部分用作为优化(例如缓存)和存储不适合负载的数据保留的空间。
现在,值槽具有统一的大小,自然,有些数据根本无法完全放入其中。为了解决这个问题,值槽可以包含对外部缓冲区的引用(基本上是一个带有额外间接层的指针,以使数据可以垃圾回收并在多个槽之间共享)。
┌─────────┐
│ header │
├─────────┤
│ payload │ --→ [buffer]
│ │
├─────────┤
│ extra │
└─────────┘
适合值槽的值(例如scalar!
)称为直接,而不适合的值(例如series!
)称为间接:因为其中的引用在值槽和实际数据之间引入了一个间接级别。例如,here 是在 Red 中定义各种插槽布局的方式。
值槽的内容只是一堆字节;运行时如何解释它们取决于标头中的数据类型 ID。一些字节可能只是文字,而其他字节可能是指向数据缓冲区的间接指针。将参数传递给函数只会复制这些字节,而不管它们的含义。因此,在这方面,文字和引用的处理方式相同。
所以,如果你有一个内部看起来像这样的值槽:
┌────────┐
│DEADBEEF│ header
├────────┤
│00000000│ payload
│FACEFEED│
├────────┤
│CAFEBABE│ extra
└────────┘
那么,比如说,FACEFEED
可以是一个有符号整数 -87097619
,或者打包在一起的不同大小的位域,或者它可以是一个机器指针:这取决于标头中的数据类型 ID (例如 EF
字节)归因于它。
当值槽作为参数传递给函数时,它的所有字节都将简单地复制到计算堆栈上,无论它们编码或表示什么。对于直接值,逻辑很简单:如果在函数内修改了参数,则原始值保持不变,因为它只是一个副本。这就是您的第二个示例的全部内容。
Parameter Stack
┌────────┐ ┌────────┐
│DEADBEEF│ │DEADBEEF│
├────────┤ ├────────┤
│00000000│ │00000000│ Both represent the same integer -87097619.
│FACEFEED│ │FACEFEED│ ← You modify this one,with no effect on the other.
├────────┤ ├────────┤
│CAFEBABE│ │CAFEBABE│
└────────┘ └────────┘
但是对于间接值,它更有趣。它们也被逐字复制,但这会使两个副本共享对单个缓冲区的相同引用(请记住,两个插槽中表示引用的字节是相同的)。因此,当您通过一个(例如,头部的 insert
元素)修改缓冲区时,另一个也会反映更改。
Parameter Stack
┌────────┐ ┌────────┐
│DEADBEEF│ │DEADBEEF│
├────────┤ ├────────┤
│00000000│ │00000000│ Both refer to the same buffer (same machine pointers!)
│FACEFEED│──┐───│FACEFEED│
├────────┤ │ ├────────┤
│CAFEBABE│ │ │CAFEBABE│
└────────┘ │ └────────┘
↓
[b u f f e r] ← You modify the content of the buffer.
回到第一个例子:
>> f: func [x][head insert x 1]
== func [x][head insert x 1]
>> a: [2 3]
== [2 3]
>> f a
== [1 2 3]
>> a
== [1 2 3] ;; variable a is destroyed
简化很多,这就是它在引擎盖下的样子:
value slot buffer value slot (parameter on stack)
<word a in global context> --→ [1 2 3] ←-- <word x in function's context>
当然,有多种方法可以克隆值槽和一个它所引用的缓冲区:这就是 copy
所做的。
>> f: func [x][head insert x 1]
== func [x][head insert x 1]
>> a: [2 3]
== [2 3]
>> f copy a
== [1 2 3]
>> a
== [2 3]
图解(再次简化):
value slot buffer
<x> --→ [1 2 3]
<a> --→ [2 3]
系列值(例如块)在其有效负载中还包含另一条数据:索引。
>> at [a b c d] 3 ; index 3,buffer → [a b c d]
== [c d]
当将块作为参数传递时,它的索引也会被复制,但与数据缓冲区不同的是,它不会在两个值槽之间共享。
Parameter Stack
┌────────┐ ┌────────┐
│DEADBEEF│ │DEADBEEF│
├────────┤ ├────────┤
│00000000│ │00000000│ Suppose that 0's here are an index.
│FACEFEED│──┐───│FACEFEED│ Modifying this one won't affect the other.
├────────┤ │ ├────────┤
│CAFEBABE│ │ │CAFEBABE│
└────────┘ │ └────────┘
↓
[b u f f e r]
所以:
>> foo: func [x][x: tail x] ; tail puts index,well,at the tail
== func [x][x: tail x]
>> y: [a b c]
== [a b c]
>> foo y
== [] ; x is modified
>> y
== [a b c] ; y stays the same