跳到主要内容

使用 self 参数的异同

  在使用 TypeScriptToLua (TSTL) 进行 TypeScript 编写并编译为 Lua 时,由于两者在语言特性上的差异,开发者需要注意一些关键的行为变化和约定。本文将重点介绍 self 参数的处理. 回调函数的上下文参数. 以及如何避免常见的类型错误等内容。

self 参数存在的意义

  在 TSTL 的默认配置中,所有函数默认会包含一个 self 参数,以保持与 JavaScript 中 this 关键字的行为一致。这个 self 参数用于指代调用函数的对象上下文。对于大部分 Lua 用户来说,这与 Lua 的 :(冒号)调用方法类似。

1. 注意事项

  在 Dora SSR 使用的 TSTL 中,则通过启用了 noImplicitSelf 配置选项。使得所有的普通函数不再默认包含一个 self 参数,以进行 JavaScript 的 this 关键字的功能模拟。

示例:

在 Dora SSR 中:

输入 (TypeScript)

function f() {}
function f2(this: any) {}
const a = () => {};
class C {
method() {}
};
interface Item {
method(): void;
};
const item: Item = {
method() {}
};

输出 (Lua)

function f() end
function f2(self) end
local a = function() end

local C = __TS__Class()
C.name = "C"
function C.prototype.____constructor(self) end
function C.prototype.method(self) end -- 类成员依然有 self 参数

local item = {
method = function(self) end -- 对象成员函数默认依然有 self 参数
}

  注意:即使启用了 noImplicitSelf,类方法和对象成员函数依然会默认带有 self,除非你明确声明 this: void

2. 如何显式移除不想要的 self 参数

  当你希望主动声明移除 self 参数,尤其是为了与 Lua 代码进行交互时,可以使用 TypeScript 的 this: void 语法。这会告诉编译器在当前上下文中不允许使用 this,从而消除 self 参数。

2.1 在类方法中使用 this: void

  在类的定义中,如果希望某些方法不使用 self,可以明确声明 this: void

示例:

输入 (TypeScript)

declare class Class {
colon(arg: string): void;
dot(this: void, arg: string): void;
}

const c = new Class();
c.colon("foo"); // 使用冒号调用
c.dot("foo"); // 使用点号调用

输出 (Lua)

local c = __TS__New(Class)
c:colon("foo")
c.dot("foo")

  通过这种方式,你可以根据需求在类方法中控制是否生成 self 参数。

2.2 处理回调函数中的 self 参数

  在常见的 Lua 库中,回调函数通常不会使用 self 参数。在编写与这些库交互的 TypeScript 代码时,务必确保回调函数的声明中不包含 self 参数。

示例:

输入 (TypeScript)

type Callback = (this: void, arg: string) => void;

declare function useCallback(this: void, callback: Callback): void;

useCallback(arg => {
print(arg);
});

输出 (Lua)

useCallback(function(arg)
print(arg)
end)

  在这个示例中,我们明确声明了回调函数不包含 self 参数,这样编译后的 Lua 代码中也不会生成多余的上下文参数。

2.3 使用 @noSelf 注解

  如果希望某个类. 接口中的所有函数都不带上下文参数,可以使用 @noSelf 注解。这样可以避免在每个函数中手动指定 this: void

输入 (TypeScript)

/** @noSelf **/
interface Item {
foo(arg: string): void;
};
const item: Item = {
foo(arg) {}
};

输出 (Lua)

local item = {
foo = function(arg) end
}

  你也可以通过在单个函数中指定 this 参数来覆盖 @noSelf 注解。例如:

输入 (TypeScript)

/** @noSelf **/
interface Item {
foo(arg: string): void;
bar(this: any, arg: string): void;
};
const item: Item = {
foo(arg) {},
bar(arg) {}
};

输出 (Lua)

local item = {
foo = function(arg) end,
bar = function(self, arg) end
}

3. 赋值错误与解决方案

  在 TSTL 中,带有 this: void 的函数和普通函数之间是不可互相赋值的。如果你试图将一个带有上下文参数的函数赋值给一个不带上下文参数的函数类型,TSTL 会抛出错误。

错误示例:

declare function useCallback(cb: (this: void, arg: string) => void);

function callback(arg: string) {}
useCallback(callback); // ❌ 错误

  这种错误可以通过将函数包裹在箭头函数中解决:

修正后:

useCallback((arg) => callback(arg));

  这样,TSTL 将不会尝试生成 self 参数,从而避免类型不匹配的问题。

4. 避免重载函数的上下文不一致

  在 TypeScript 中,函数允许重载多个不同的签名。但是,在 TSTL 中,如果多个重载函数的签名的上下文类型不同(例如一个带 this: void,一个不带),将会导致编译错误。

示例:

declare function useCallback(f: () => {}): void;

declare function callback(this: void, s: string, n: number): void;
declare function callback(s: string);

useCallback(callback); // ❌ 错误:上下文类型不一致

  为避免这些错误,最好避免上下文类型不一致的函数过载。