接着上一章节
Qt脚本中的this对象
当Qt脚本函数在脚本中被调用时,当执行函数体时,调用它的方式确定该对象,如下面的脚本示例所示:
var getProperty = function(name) { return this[name]; };
name = "Global Object"; // creates a global variable
print(getProperty("name")); // "Global Object"
var myObject = { name: 'My Object' };
print(getProperty.call(myObject, "name")); // "My Object"
myObject.getProperty = getProperty;
print(myObject.getProperty("name")); // "My Object"
getProperty.name = "The getProperty() function";
getProperty.getProperty = getProperty;
getProperty.getProperty("name"); // "The getProperty() function"
要注意的是,在Qt脚本中,与C++和Java不同,这个对象不是执行范围的一部分。这意味着成员函数(即对此进行操作的函数)必须始终使用此关键字来访问对象的属性。例如,下面的脚本可能不做你想要的:
var o = { a: 1, b: 2, sum: function() { return a + b; } };
print(o.sum()); // reference error, or sum of global variables a and b!!
您将得到一个引用错误,表示“a未定义”,或者,更糟糕的是,两个完全不相关的全局变量a和b将用于执行计算(如果它们存在的话)。相反,脚本应该是这样的:
var o = { a: 1, b: 2, sum: function() { return this.a + this.b; } };
print(o.sum()); // 3
对于C++和Java的范围规则的程序员来说,意外省略这个关键字是一个典型的错误来源。
封装本地函数
Qt脚本提供QScriptEngine::newFunction()作为包装C++函数指针的方法;这使您能够在C++中实现一个函数并将其添加到脚本环境中,以便脚本可以调用您的函数,就像它是一个“正常”脚本函数一样。下面是前面的getProperty()函数如何用C++编写的:
QScriptValue getProperty(QScriptContext *ctx, QScriptEngine *eng)
{
QString name = ctx->argument(0).toString();
return ctx->thisObject().property(name);
}
调用QScriptEngine::newFunction()来包装函数。这将产生一种特殊类型的函数对象,它在内部携带指向C++函数的指针。一旦生成的包装器被添加到脚本环境中(例如,通过将其设置为全局对象的属性),脚本就可以调用该函数,而不必知道也不必关心它是本机函数。
请注意,C++函数的名称在脚本意义上并不重要;脚本调用函数的名称仅取决于您所称的脚本对象属性,在其中存储函数包装器。
目前不可能包装成员函数,即需要这个对象的C++类的方法。
the QScriptContext Object
QScriptContext上下文包含与函数的特定调用相关联的所有状态。通过QScript上下文,你可以:
- 获取传递给函数的参数。
- 获取this对象。
- 了解函数是否new操作符调用(其意义将在后面解释)。
- 抛出脚本错误。
- 获取正在调用的函数对象。
- 获取激活对象(用于保存局部变量的对象)。
以下各节说明如何使用此功能。
处理函数的参数
有两个关于函数参数的重要信息
- 任何脚本函数(包括本机函数)都可以用任意数量的参数调用。这意味着,如果需要,由函数本身检查参数计数,并相应地操作(例如,如果参数数量太大,则抛出错误,或者如果数量太小,则准备默认值)。
- 任何类型的值可以作为参数提供给任何函数。这意味着,根据需要检查参数的类型,并相应地采取行动(例如,如果参数不是特定类型的对象,则抛出错误)。
总之:Qt Script不会自动对函数调用中涉及的参数的数量或类型强制执行任何约束。
形式参数和参数对象
本机Qt Script函数类似于没有定义正式参数并且只使用内置参数变量来处理其参数的脚本函数。为了了解这一点,我们首先考虑脚本通常如何定义一个add()函数,该函数接受两个参数,将它们相加并返回结果:
function add(a, b) {
return a + b;
}
当用形式参数定义脚本函数时,可以将其名称视为参数对象的属性的别名;例如,在add(a,b)定义的函数体中,a和arguments[0]引用相同的变量。这意味着add()函数可以等效地写成这样:
function add() {
return arguments[0] + arguments[1];
}
后一种形式与本地实现的典型匹配非常相似:
QScriptValue add(QScriptContext *ctx, QScriptEngine *eng)
{
double a = ctx->argument(0).toNumber();
double b = ctx->argument(1).toNumber();
return a + b;
}
检查参数的数目
再一次,请记住,函数定义中形式参数名称的存在(或缺乏)并不影响如何调用函数;add(1,2,3)是引擎允许的,add(42)也是如此。对于add()函数,函数确实需要两个参数才能执行一些有用的操作。这可以由脚本定义表示如下:
function add() {
if (arguments.length != 2)
throw Error("add() takes exactly two arguments");
return arguments[0] + arguments[1];
}
如果脚本使用除两个参数之外的任何参数调用add(),则将引发错误。可以修改本机函数以执行相同的检查:
QScriptValue add(QScriptContext *ctx, QScriptEngine *eng)
{
if (ctx->argumentCount() != 2)
return ctx->throwError("add() takes exactly two arguments");
double a = ctx->argument(0).toNumber();
double b = ctx->argument(1).toNumber();
return a + b;
}
检查参数的类型
除了要求一定数量的参数之外,函数还可能要求这些参数是某些类型的(例如,第一个参数是数字,第二个参数是字符串)。这样的函数应该显式地检查参数的类型和/或执行转换,或者如果参数的类型不兼容则抛出错误。
实际上,上面所示的add()的本地实现不具有与脚本对应的完全相同的语义;这是因为Qt Script 的+操作符的行为取决于其操作数的类型(例如,如果其中一个操作数是字符串,则执行字符串连接)。)为了给脚本函数提供更严格的语义(即,它应该只添加数值操作数),可以测试参数类型:
function add() {
if (arguments.length != 2)
throw Error("add() takes exactly two arguments");
if (typeof arguments[0] != "number")
throw TypeError("add(): first argument is not a number");
if (typeof arguments[1] != "number")
throw TypeError("add(): second argument is not a number");
return arguments[0] + arguments[1];
}
然后,像add("foo", new Array())这样的调用将引发错误。
C++版本可以调用QScriptValue::isNumber()来执行类似的测试:
QScriptValue add(QScriptContext *ctx, QScriptEngine *eng)
{
if (ctx->argumentCount() != 2)
return ctx->throwError("add() takes exactly two arguments");
if (!ctx->argument(0).isNumber())
return ctx->throwError(QScriptContext::TypeError, "add(): first argument is not a number");
if (!ctx->argument(1).isNumber())
return ctx->throwError(QScriptContext::TypeError, "add(): second argument is not a number");
double a = ctx->argument(0).toNumber();
double b = ctx->argument(1).toNumber();
return a + b;
}
一种不太严格的使用+运算符的方式是,在运算之前执行显式的数字转换:
function add() {
if (arguments.length != 2)
throw Error("add() takes exactly two arguments");
return Number(arguments[0]) + Number(arguments[1]);
}
在本地实现中,这相当于不首先执行任何类型测试而调用QScriptValue::toNumber(),因为如果必要,QScriptValue::toNumber()将自动执行类型转换。
为了检查一个参数是否属于某个对象类型(类),脚本可以使用instanceof操作符(例如,“arguments[0] instanceof Array”计算为true,如果第一个参数是Array对象);本地函数可以调用QScriptValue::instanceOf()。
若要检查参数是否为自定义C++类型,通常使用qscriptvalue_cast(),并检查结果是否有效。对于对象类型,这意味着对指针进行强制转换并检查它是否非零;对于值类型,类应该使用isNull()、isValid()或类似的方法。或者,由于大多数自定义类型在QVariants中传输,所以可以使用QScriptValue::isVariant()检查脚本值是否为QVariant,然后使用QVariant::canConvert()检查QVariant是否可以转换为您的类型。
具有可变数量参数的函数
由于内置参数对象的存在,实现具有可变数量的参数的函数非常简单。事实上,正如我们所看到的,在技术意义上,所有Qt Script函数都可以看作变量参数函数。例如,考虑一个concat()函数,它接受任意数量的参数,将参数转换为其字符串表示并连接结果;例如,concat(“Qt”、“Script”、“101”)将返回“Qt Script 101”。concat()的脚本定义可能是这样的:
function concat() {
var result = "";
for (var i = 0; i < arguments.length; ++i)
result += String(arguments[i]);
return result;
}
这里是一个等价的本机实现:
QScriptValue concat(QScriptContext *ctx, QScriptEngine *eng)
{
QString result = "";
for (int i = 0; i < ctx->argumentCount(); ++i)
result += ctx->argument(i).toString();
return result;
}
变量数目可变的第二个用例是实现可选参数。下面是脚本定义的定义:
function sort(comparefn) {
if (comparefn == undefined)
comparefn = fn; /* replace fn with the built-in comparison function */
else if (typeof comparefn != "function")
throw TypeError("sort(): argument must be a function");
// ...
}
这里是本地等价函数:
ScriptValue sort(QScriptContext *ctx, QScriptEngine *eng)
{
QScriptValue comparefn = ctx->argument(0);
if (comparefn.isUndefined())
comparefn = /* the built-in comparison function */;
else if (!comparefn.isFunction())
return ctx->throwError(QScriptContext::TypeError, "sort(): argument is not a function");
...
}
变量数目可变的第三个用例是模拟C++重载。这包括在函数体的开头(如已经显示的)检查参数的数量和/或它们的类型,并相应地进行操作。在这样做之前,可能值得再三考虑,而倾向于使用唯一的函数名;例如,具有单独的processNumber(number)和processString(string)函数,而不是一般的process(anything)函数。在调用端,这使脚本更难意外地调用错误的重载(因为它们不知道或不理解您自定义的复杂重载解析规则),而在被调用端,您避免了可能复杂的(读:容易出错的)检查来解决最大限度。
访问参数对象
大多数本地函数使用QScriptContext::arguments()函数来访问函数参数。但是,通过调用QScriptContext::argumentsObject()函数,也可以访问内置的参数对象本身(在脚本代码中由参数变量引用的那个)。这有三个主要的应用:
- 参数对象可以用来轻松地将函数调用转发给另一个函数。在脚本代码中,这是它通常看起来的样子:
function foo() {
// Let bar() take care of this.
print("calling bar() with " + arguments.length + "arguments");
var result = bar.apply(this, arguments);
print("bar() returned" + result);
return result;
}
例如,foo(10,20,30)将导致foo()函数执行与bar(10,20,30)等效的函数,如果希望在调用函数(例如,在不需要修改bar()函数本身的情况下记录对bar()的调用,如上面的示例)时执行一些特殊的预处理或后处理,或者如果希望从具有确切功能的原型函数调用“基本实现”,则这非常有用。同样的“签名”。在C++中,转发函数可能是这样的:
QScriptValue foo(QScriptContext *ctx, QScriptEngine *eng)
{
QScriptValue bar = eng->globalObject().property("bar");
QScriptValue arguments = ctx->argumentsObject();
qDebug() << "calling bar() with" << arguments.property("length").toInt32() << "arguments";
QScriptValue result = bar.apply(ctx->thisObject(), arguments);
qDebug() << "bar() returned" << result.toString();
return result;
}
- 参数对象可以作为QScriptValueIterator的输入,提供对参数进行迭代的通用方法。例如,调试器可以使用它来在通用的“Qt脚本对象资源管理器”中显示参数对象。
- 参数对象可以被序列化(例如,使用JSON)并传输到另一个实体(例如,在另一个线程中运行的脚本引擎),其中对象可以被反序列化并作为参数传递到另一个脚本函数。