TJS2中对象的表示方法,其代表的运行时环境,与闭包的关系

对一个对象实例调用(string)转换时,可能会看到这样的结果: (object 0x01AAD840:0x01A9EEC4)
object到string的转换可以通过显式或隐式方式调用.这个转换在tjsVariant.cpp中实现.

\kirikiri2\src\core\tjs2\tjsVariant.cpp
void tTJSVariant::ToString()
{
switch(vt)
{
case tvtVoid:
String=NULL;
vt=tvtString;
break;

case tvtObject:
{
tTJSVariantString * string = TJSObjectToString(*(tTJSVariantClosure*)&Object);
ReleaseObject();
String = string;
vt=tvtString;
break;
}

case tvtString:
break;

case tvtInteger:
String=TJSIntegerToString(Integer);
vt=tvtString;
break;

case tvtReal:
String=TJSRealToString(Real);
vt=tvtString;
break;

case tvtOctet:
TJSThrowVariantConvertError(*this, tvtString);
break;
}
}

tTJSVariantString * TJSObjectToString(const tTJSVariantClosure &dsp)
{
if(TJSObjectTypeInfoEnabled())
{
// retrieve object type information from debugging facility
tjs_char tmp[256];
TJS_sprintf(tmp, TJS_W("(object 0x%p"), dsp.Object);
ttstr ret = tmp;
ttstr type = TJSGetObjectTypeInfo(dsp.Object);
if(!type.IsEmpty()) ret += TJS_W("[") + type + TJS_W("]");
TJS_sprintf(tmp, TJS_W(":0x%p"), dsp.ObjThis);
ret += tmp;
type = TJSGetObjectTypeInfo(dsp.ObjThis);
if(!type.IsEmpty()) ret += TJS_W("[") + type + TJS_W("]");
ret += TJS_W(")");
tTJSVariantString * str = ret.AsVariantStringNoAddRef();
str->AddRef();
return str;
}
else
{
tjs_char tmp[256];
TJS_sprintf(tmp, TJS_W("(object 0x%p:0x%p)"), dsp.Object, dsp.ObjThis);
return TJSAllocVariantString(tmp);
}
}

注意到,上面例子(object 0x01AAD840:0x01A9EEC4)中的两个数字,前者是dsp.Object,后者是dsp.ObjThis. tTJSVariantClosure的这两个成员是由tTJSVariantClosure_S继承而来.

tTJSVariantClosure_S的定义如下:
struct tTJSVariantClosure_S
{
iTJSDispatch2 *Object;
iTJSDispatch2 *ObjThis;
};

由此得知,两个数字皆是地址(指针自身的内容都是地址).
我现在还不是太确定Object成员是不是在编译到中间代码的时候就已经构造出了实例,但从多个同类型的对象实例共享一个Object成员的实例来看,应该不是到脚本执行的时候才实例化的.现在虽然找到了几个关键点(T_NEW, VM_NEW, CreateNew, TJS_GET_VM_REG等),但面向对象程序的代码就是很难通过静态分析完全确定动态行为,我仍然理不出运行时行为的头绪.糟糕的是我暂时没办法以debug模式把TJS2的解释器正确编译出来,要调试也很麻烦.

Anyway,还不确定的就先放一边,来看看我确定的部分.上面提到了,每个tTJSVariantClosure实例都会有[b]Object[/b]和[b]ObjThis[/b]指针为成员.并且,tTJSVariantClosure拥有由iTJSDispatch2接口类型所拥有的各方法.在调用这些方法时,需要经由tTJSVariantClosure来访问,以确保执行时的上下文的正确性.

简单点说,TJS2中每个对象实例都有"两个指针",一个是Object,指向类中的程序代码(如初始化用的代码和函数等),由同类型的所有类型所共享,无状态;另一个是ObjThis,也就是TJS2中以"this"访问可以得到的对象,保存着实例自身的上下文(context,也可以理解为环境environment),不与其它实例共享,用于保持状态信息.在TJS2中,将这个上下文称为闭包(closure).可以参考[url=http://kcddp.keyfc.net/doc/tjs2doc/contents/index.html]KCDDP翻译的TJS2中文文档[/url]中"类(class)"一节.这里"闭包"可以理解为状态(主要是成员变量)与一个由"类"的语法作用域所形成的范围发生绑定的现象.这样,这个范围就对其中的成员变量形成了闭包.注意到函数也同样可以被赋值给变量,并且函数声名与将函数表达式赋值给一个变量近似等价,因此成员函数(作为变量)也是上下文的一部分.拥有一个专门指向上下文的指针,就意味着绑定可以在运行时改变.使用incontextof运算符就能做到"可调用对象"(Callable, 这里具体指类定义或函数)的上下文的显式指定.

下面的代码可以清晰的展示这一特性:
class A {

var value = "A";

function printField() {
System.inform(value);
}

function printFunc() {
print2();
}

function print2() {
System.inform("A.print2");
}
}

class B {

var value = "B";

function print2() {
System.inform("B.print2");
}
}

var a = new A();
var b = new B();
(a.printField incontextof b)(); // 使用incontextof运算符显式改变上下文
(a.printFunc incontextof b)(); // 这两行调用的上下文被替换为与b的相绑定
a.printField(); // 正常的方法调用
a.printFunc(); // 这两行调用的上下文都与a的绑定
// 运行结果: 依次显示B, B.print2, A, A.print2


要注意的是,TJS2里的"闭包"与一般支持嵌套定义函数的编程语言中所指的闭包并不相同;TJS2里"闭包"的用法相当独特,特别不应与JavaScript中所支持的闭包所混淆.
在一般的编程语言中,"闭包"的概念是如何会出现的呢? 下面简单解释一下.下面一段会混用"过程""函数""方法"等几个名词,请自行注意分辨区别.也可以查阅[url=en.wikipedia.org/wiki/Closure_(computer_science)]wikipedia上的相关条目[/url].该条目原本有些描述不太对(缺乏对命令式语言中闭包的考虑),现在已经订正了一些.
根据《编译原理与实践》一书,可以将基于栈(stack)的运行时环境分为三类:
- 没有局部过程的基于栈的环境
- 带有局部过程的基于栈的环境
- 带有过程参数的基于栈的环境

假如一种语言不允许嵌套声明过程/函数/方法,则所有的函数如果不是局部的就一定是全局的.因而很容易为变量分配空间——全局变量可以放在一个全局区域,而所有函数内的局部变量则直接分配在栈上(或寄存器上).函数总是能访问到它的作用域内的所有变量,外加全局变量.一个函数在被调用的时候,会在栈上压入它的活动记录,在函数结束时销毁;因而所有局部变量将随着函数的退出而被销毁.

int global = 1;

void foo() {
int local = 2;
// 此处global与local都能被访问到
}

// 此处就不再能访问到local了

假如一种语言允许嵌套声明过程/函数/方法,但不允许将过程/函数/方法当作参数来传递,前一种运行时环境就无效了.试想下面的伪代码(以C的语义来考虑):
int global = 1;

void foo() {
int local = 2;

void goo(int local) {
hoo();
}

void hoo() {
// 此处能访问到global
// 但是local呢?
// 因为hoo()嵌套于foo()之中,我们希望hoo()也能访问到foo()的local
}
goo(local);
}

void main(void) {
foo();
}

这段代码片段中,对hoo()而言global依然是全局变量因而可以正常访问,但local是非局部非全局的变量,不能再按照前面的方式考虑.按照标准的静态作用域则无法在任何活动记录中找到local的信息;如果接受动态作用域,那么可以通过控制链向上找到在goo()中的local.即使goo()中没有local,还可以继续向上找到foo()的local.但这样每次能找到"local"的偏移量都会随着调用的不同而不同,而我们想要的很可能不是这样的.
要解决这问题,仍然可以实现静态作用域.可以用一个访问链(access link)去记录一个函数的包围(enclosing)函数的活动记录,使内部的函数能访问到外部函数的局部变量.在上面的代码里,也就是说hoo()可以访问到foo()的(而不是goo()的)local变量.

前面两种情况都能通过灵活使用栈而得到顺利解决,但下面的这种情况就很难单独依靠栈来维持运行时环境了.假如一个语言允许将过程/函数/方法当作参数来传递,则编译程序无法再像前一种情况生成代码计算调用点上的访问链.为解决非局部引用的问题,函数应包含一个访问指针对,其中一个是代码指针一个是访问链或环境指针(注意到这里跟TJS2的实现的微妙相似与相异).它们通称为闭包(closure),因为访问链"闭合"了由非局部引用引起的"洞"."闭包"的概念来自微积分中λ算子,这里就不深入了.通过闭包,内部函数可以捕捉到外部函数所能访问到的所有引用,包括全局引用和外部函数的局部引用,外加内部函数自身的局部引用;也就是说,内部函数的作用域"捕捉"到了外部的.然而这个内部函数一旦被作为参数传递(例如说被外部函数作为返回值返回),它所能访问到的作用域不能马上被销毁,而要继续存在到没有任何引用继续指向该内部函数为止.这就与前面使用栈的方式相异,很难只用栈来放置活动记录就维持运行时环境.更一般的做法是把闭包相关的活动记录在堆(heap)上分配,然后由垃圾收集器处理销毁的工作.

参考下面代码(ActionScript3):
function add( lhs : int ) : Function {
return function ( rhs : int ) : int {
return lhs + rhs;
}
}

var addFive = add( 5 );
var twelve = addFive( 7 ); // twelve == 12


总之,要特别引起注意的地方是这里引出"闭包"这改变的问题,来自以遵守[b]静态作用域[/b]为前提,允许嵌套声明的函数对来自包围它的作用域的非局部非全局引用的访问.
在这个意义上,TJS2并没有实现一般的闭包.在TJS2中可以嵌套定义函数,但无论嵌套多少层,一个函数的上下文总是绑定于离它最近的"类"的作用域中;假如一个函数向嵌套的外层数去,数到头都没有类声明,则它的上下文绑定于全局对象(global).下面的代码将展示这点:
function foo() {
function goo() {
this.val = "a string";
}
goo();
}

foo();
System.inform(val); // 或者写为global.val都一样.
// 显示a string


幸好吉里吉里2的作者,W.Dee氏已经意识到了这点.在设计TJS2的接班者Risse时,[url=https://sv.kikyou.info/trac/kirikiri/wiki/documents/kirikiri3/development/risse_spec]他承诺[/url]能实现下面的代码:
function test()
{
var i = 0;
return function inc()
{
return ++i;
}
}

var inc_func = test();
Log.message(inc_func()); // => 1
Log.message(inc_func()); // => 2


TJS2不但没实现一般意义的闭包,同时也没有遵循一般意义的静态作用域.TJS2中incontextof运算符可以在静态作用域的基础上将一个可调用对象的上下文重新绑定到任意对象级别上.TJS2解释器会在编译的时候检查是否可执行代码是否有可能调用了非局部(但是是成员的;"全局"在TJS2中是一个特殊的内建对象,全局变量可以认为是global的成员)的引用.如果有的话,则会通过"this proxy"(对应于%-2寄存器)来寻找那些引用.这么做可以说很灵活,但也很容易写出让人难以理解的代码(因为函数与上下文的绑定可能经常变化).建议慎用.
个人倾向于将这种作用域的做法成为"半自动动态作用域".总之关键就是不要以纯静态作用域的假设去阅读TJS2代码,否则一定会吃到苦头...

===========================================

十分有趣的一点是,TJS2中的==与===运算符,分别是由tTJSVariant::NormalCompare与tTJSVariant::DiscernCompare实现的;其中,前者在比较者类型为tvtObject(object)时,比较的是左右操作数的Object.Object;后者在比较者类型为tvtObject(object)时,比较的是左右操作数的Object.Object与Object.ObjThis.这解释了下面代码行为的原因:
class A {
var someField; // member field

function A() {} // ctor

function foo() {} // member method
}
var a = new A();
System.inform((string)A.foo + (string)a.foo); // 显示两个相同的Object地址
System.inform(A.foo == a.foo); // 1, 即equality相等
System.inform(A.foo === a.foo); // 0, 即identity不相等
System.inform(A.foo === (a.foo incontextof null)); // 1, 即identity也相等
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值