@的行为

@的行为

> LeekScript 教程

本页只适用于 LS1,从 LS2 开始就不应该再使用 @ 了

在 LeekScript 中,如果你还没注意到的话:每回合的操作数是有上限的,上限取决于韭葱的 核心 数量。因此,优化是取胜的关键之一。本文将深入讲解 @ 这个广受欢迎的工具的行为。

它的主要用途接近于“引用”,也就是说可以避免值的复制。

精简说明

下面的说明是为了让你对 @ 符号背后的机制有个直观的认识。它并不是精确的技术描述,因为当前所用 LeekScript 版本的源码并不公开。

0; ""; []; function(x) { return x; };

赋值

当你直接用字面量创建一个对象时(见上文),它会占用一块内存。要使用它,就得知道去哪里找它。我们把这个位置称为对象的地址。当你把一个对象赋给一个变量时,程序会先复制这个对象,再把副本的地址交给变量。我们也可以要求直接使用原对象的地址,而不是副本的地址,从而省下一次复制:只要在等号右边的表达式前面加上一个 @ 符号即可。(main_gauche =@ main_droite)

var w = 0; // 复制了一份 0,并把这份副本的地址交给变量。 var x =@ 0; // 直接交出 0 的地址。(代码中出现的每个字面量都是不同的对象。) var y = w; // 把 0 的那份副本又复制了一份,并交出这份新副本的地址。 var z =@ w; // 直接交出 0 的那份副本的地址。

@ 符号的优先级最高。因此 x =@ y + z 并不会做你可能以为的事情,它等价于 x = (@x) + y。如果你想赋的是地址,就得加上括号,明确 @ 作用在什么上面:x =@ (x + y)。

特殊情况

--- 数字上的运算不会就地改变对象。当你使用 += 这类运算符时,结果是一个新对象。数组和字符串则不同,因为这两者是结构,被修改的是它们的内容。把表达式展开一点,会更容易理解其中的原理:x += 1 相当于 x = x + 1,可以看到这里发生了重新赋值;而 x += [1] 相当于 x[count(x)] = 1,或者说 push(x, 1)/pushAll(x, [1]),显然变量 x 并没有被重新赋值。

参数

同样地,当你把一个对象作为实参传给函数时,程序会复制一份,并把副本的地址交给形参。不过,和赋给变量不同,在形参前面加上 @ 符号,函数调用时的行为要复杂一些:

在少见的情况下,如果你想传的是一份副本而不是变量的地址,可以在实参前面加上 @ 符号:at(@zero)。

作为参数

它最稳定、最实用的用法是在函数的参数上:

function with_a (@ param1 ) { } // 我们拿它来和下面这个作对比: function whitout_a ( param1 ) { }

注意: 如果你在函数里修改了这个参数,调用方程序里的变量也会跟着改变,因为交给函数的是你的变量本身,而不是它的值,例如:

function add_1 (@param) { param += 1; }

var a = 5; add_1(a); debug (a); // 显示:'6'

注意: 调用一次函数要花 1 次操作。下面的结果里已经扣掉了这项开销。

直接传入原始数据 通过变量传入 作为参数传入的数据 不带 @ 带 @ 变量赋值的开销(见“赋值”一节) 不带 @ 带 @ 42 2 1 2 2 0 "a string" 2 1 2 2 0 function () {} 2 1 2 2 0 [42, -42, 42] 116 34 116 83 0 ["un autre string", -42, [42]] 154 45 154 110 0

顺带一提,想让一次赋值少花几次操作,可以写成 a = @[42, -42, 42]; 这样的形式(见“赋值”一节)。用这种写法,得到的结果和带 @ 把原始数据传进函数时(即函数调用)一样:[42, -42, 42] 的赋值要 34 次操作,["un autre string", -42, [42]] 要 45 次操作,另外三种各要 1 次操作。

'''结论'''

使用 @ 是非常推荐的做法 只用一次的场合专门建一个变量,没有任何区别(操作数不多也不少) 除非你要把同一个原始值反复作为参数传进去,这时候就定义常量 => 更划算 请注意,@ 是按引用传递,也就是说变量的改动会反映到调用这个函数的程序里

赋值

在第二部分里,我们来深入看看 @ 在赋值中的用处。它这样使用:

a = @( 42 ); // 这里的 'a' 是一个变量

注意: 如果只有一个元素,括号可以省略,比如 =@ 0 或 =@ variable,但 =@ 1 + 1 不行。

直接传入原始数据 通过变量传入 作为参数传入的数据 不带 @ 带 @ 不带 @ 带 @ 42 2 1 2 1 "a string" 2 1 2 1 function () {} 2 1 2 1 [42, -42, 42] 116 34 83 1 ["un autre string", -42, [42]] 154 45 110 1

结论

非常建议在赋值时使用 @ 把变量当作常量来用更省操作 注意:使用 @ 并非没有风险,某些情况下会出现“反向影响”,详见下一部分

反向影响

我把“赋值时连被引用的那个变量的值也一起改掉”这件事称为“反向影响”。这正是函数中按引用传参时发生的情况:如果在函数里修改了值,作为参数传进来的变量本身也会被修改:

function add1 (@a) { a++; }

b = 0; add1(b); debug(b); // 显示:1

在某些赋值的场合,这种现象同样会发生……

var b = DEFAULT_VALUE; var a = @b; a = OTHER_VALUE; // b 是 DEFAULT_VALUE 还是 OTHER_VALUE?

在这个例子里,程序结束时 b 的值是 DEFAULT_VALUE。实际发生的事情是这样的:

'a' 和 'b' 之间的“引用”被 '=' 号打断了。 反过来说,要保住这个引用,就得修改 'a',而不是重新给它赋值。

不过,并不是所有类型都这样。下面是我的研究结果:

对没听说过指针的人解释一下:指针是一个变量,它保存着另一块内存的地址。可以把它想象成一个箭头(指向某一块内存)……在 LeekScript 中,数组和字符串就是用这套机制管理的。当你像下面这样创建一个变量时:

var tab = []; // 或者 var tab = @[];

程序会分配一块(或一组)内存来存放 [],然后让 'tab' 指向这块内存;'tab' 不过是给某块内存起的一个名字。

var tab1 = [1, 2, 3];

// 1) var tabCopy = tab1; // 2) var tabRef = @tab1;

第一种情况下,数组被复制了:程序分配了一块新空间,把 'tab' 数组的每个值都复制进去,变量 'tabCopy' 指向这块新空间。而第二种情况下,变量 'tabRef' 和 'tab' 指向同一块内存,只要 'tab' 被修改(不是被重新赋值!),'tabRef' 也会跟着变。下面(粗略地)画出了这段代码在内存中的样子:

!TabLS

这样一来,就可以用 @ “玩转”数组了。如果一个数组里又装着数组,可以用 @ 取得指向这个内层数组的引用:

var tab = [1, 2, [3, 4, 5]];

var secondTab = @tab[2]; debug(secondTab); // [3, 4, 5]

push(secondTab, 6); debug(tab); // [1, 2, [3, 4, 5, 6]]

push(tab[2], 7); debug(secondTab); // [3, 4, 5, 6, 7]

注意:没办法让一个数组引用它自己。这样只会产生一份副本,还要搭上相应的操作数开销。

var tab = [1, 2, 3];

tab[0] = @tab; // 产生一份副本

如果你像 'tabRef' 引用 'tab' 那样去引用别的数组,为了不打断这个引用,就要修改 'tabref',而不是重新给它赋值……

// 每个例子都是独立的,不会改变初始值 // 也就是说,每个例子过后,'tabref' 和 'tab' 都恢复成默认值,即: var tab = [1, 2, 3]; var tabref = @tab;

// 1) tabRef = []; // 或者 tabref = @[];

// 2) tabRef += [4];

// 3) push(tabRef, 5);

// 4) remove(tabRef, 3);

tabref = [],tab = [1, 2, 3]

tabref = [1, 2, 3, 4],tab = [1, 2, 3, 4]

tabref = [1, 2, 3, 5],tab = [1, 2, 3, 5]

tabref = [1, 2],tab = [1, 2]

结论

在赋值里,@ 可不能闭着眼睛用 变量里装的是数字或函数时,没有任何风险 数据是数组或字符串时,会产生一个引用 一旦重新赋值,这个引用就断了;要保住它,就得修改对象本身,比如用 '+=' 或 'push'

返回值(return)

这一部分我们来研究 @ 在函数返回值上的用法。 它的用法非常简单,而且大多数时候反向影响不会给你添麻烦,因为返回的变量通常就是在函数内部创建的。来看这段程序:

global _global = DEFAULT_VALUE;

function getGlobal () { return /* @ ? */ _global; }

var local = /* @ ? */ getGlobal();

/* 对 local 的修改 例如:push(local, 0); 或 local += 1; 取决于 DEFAULT_VALUE */

debug ("Local : " + local); debug ("Global : " + _global );

local = return ... local = return @ ... local = @ return ... local = @ return @ ... _global 的值 操作数 反向影响 操作数 反向影响 操作数 反向影响 操作数 反向影响 42 1 否 1 否 1 否 1 否 "a string" 1 否 1 是 1 是 1 否 function () {} 1 否 1 是 1 是 1 否 [42, -42, 42] 82 否 1 是 1 是 83 否 ["un autre string", -42, [42]] 109 否 1 是 1 是 110 否

注意:这里扣掉了赋值给 local 的操作开销,只保留 return 本身的开销

“@ return @ ...”表示赋值处放了一个 @,return 处又放了一个 @。

如你所见,使用 @ 可以避免无谓的复制,不过反向影响出现的情形和赋值时一样(数组和字符串)。还要注意,用两个 @ 反而会打断引用,因为这会产生一份副本。 为了保持一致,要么始终用 'return @...' 来返回,要么记得在接收函数返回值的赋值处加上 @。为了更灵活,建议不要返回引用:因为使用者若想要一份副本,就得写两个 @,这样除了复制的代价外还要多花一次操作,而且也不好读。

结论

使用 @ 同样能省下可能发生的复制 当心别写两个 @,那会产生复制并多花一次操作 因此建议定一个规范,特别是不要返回引用

总结

好了,@ 的方方面面就讲完了。它的行为很受欢迎,可以用来优化代码、省下宝贵的操作数。可以记住下面这几种主要用法,以及该采取的做法:

@ 并不能解决一切,优化对于打造一个 AI 至关重要,不过这是个好的开始。