猫史档案馆


【Box3】开发避坑,深究类型:文档质量不高导致的坑怎么避?

用户:Fox_AwaFox_Awa查看:4 回复:6 评论:4 创建时间:2022-08-16T19:09:15


本文转自“凌”的个人手记。

TL;DR(“太长了,不读”)见下

handler的返回值应当改为void | Promise<void>。

entity.player.dialog等异步函数应强调其异步性,同时也应强调返回值可能为null。

(略)null和undefined不一样,或许应当为undefined | Box3DialogResponse。

今天闲着没事干读岛3的API,偶然看到有这么一个示例: 

// entity.player.dialog的官方示例
/* 玩家进入游戏时,弹出一个欢迎对话框 */
world.onPlayerJoin(({entity}) => {
    entity.player.dialog({
        type: Box3DialogType.TEXT,
        title: "吉吉喵",
        content: `你好,${entity.player.name},很高兴认识你。`,
    });
})
然后我就感觉特别不对劲。你这相当于阻塞游戏,怎么写的。于是我又调查了entity.player.dialog的类型。  
// 在游戏中显示一个对话框。
// 目前支持3种对话框样式:文本框 Text / 选项框 Select / 输入框 Input

type Box3DialogCall =
((params:Box3TextDialogParams) => Promise<Box3DialogResponse | null> & Box3DialogCancelOption) |
((params:Box3SelectDialogParams) => Promise<DialogSelectResponse | null> & Box3DialogCancelOption) |
((params:Box3InputDialogParams) => Promise<Box3DialogResponse | null> & Box3DialogCancelOption)
好家伙,异步的啊。也就是你这返回Promise直接给扬了,实在逆天。我感觉越来越不对劲,光速去调查world.onPlayerJoin的类型。   结果我们得到了如下定义:  
// world.onPlayerJoin 的类型
type Box3EventChannel‹EventType› = (handler:(event: EventType) => void) => Box3EventHandlerToken;
  其中handler参数的类型为:  
// 我从函数的参数列表抽出了类型。
type Handler<EventType> = (event: EventType) => void;
而若希望在语义上传入异步的handler,则应有额外的类型定义:  
// 支持传入async function的Handler
type AsyncHandler<EventType> = (event: EventType) => Promise<void>;
或者我们可以使用联合类型来同时允许传入同步和异步函数:  
// 使用联合类型来表示返回值,这样同时兼容同步和异步函数。
type Handler2<EventType> = (event: EventType) => Promise<void> | void;
但是handler既不是Handler | AsyncHandler,也不是Handler2,故Promise<void>的返回值在语义上讲是不被允许的(实际上,因为返回值是void,Promise<void>会被丢弃) 而我们知道,async function的返回值必然是Promise<unknown>(unknown代指返回类型)故在此处传入async function会令人很感到很奇怪(Promise<void>被丢掉了,但又没被丢掉)。   举个生动的例子如下:
world.onPlayerJoin(({entity}) => {
  // 这样是OK的。函数返回void。
})
world.onPlayerJoin(async ({entity}) => {
  // 这样也OK但并不推荐,因为async函数返回Promise<void>,但Promise<void>被丢弃了。
})

// 示例
type fn = () => void
const a: fn = async () => 1
;(async () => {
  const d = await a() // 认为这里的await是无效的。
  console.log(d) // 实际上输出1,因为Promise<number>没有被实际丢弃。
})()
接下来我将展示几段错误代码。在2020-2021时,因此类错误导致的项目漏洞不在少数。 之所以有这类错误,实际上是因为文档并未对异步函数进行强调,导致部分初学者可能出现错误。
// 错误代码1
/* 玩家进入游戏时,弹出一个欢迎对话框 */
world.onPlayerJoin(({entity}) => {
    const result = entity.player.dialog({
        type: Box3DialogType.TEXT,
        title: "凌",
        content: `首先,${entity.player.name},这是一段错误代码。`,
    });
    // 下一步处理...
    entity.player.dialog({
        type: Box3DialogType.TEXT,
        title: "凌",
        content: `比如,这样就错了。`,
    });
})
 

一旦在下一步处理后面写逻辑就大错特错了。此处需要await才可以正确使用。但也有抄示例2抄错的情况存在。

// 错误代码1
/* 玩家进入游戏时,弹出一个欢迎对话框 */
world.onPlayerJoin(({entity}) => {
    const result = await entity.player.dialog({
        type: Box3DialogType.TEXT,
        title: "凌",
        content: `首先,${entity.player.name},这是一段错误代码。`,
    });
    // 下一步处理...
    await entity.player.dialog({
        type: Box3DialogType.TEXT,
        title: "凌",
        content: `比如,这样就错了。`,
    });
})

错误的地方在于,忘记加async而直接使用await,导致问题发生。

一部分喜欢探知的青少年创作者会去了解Promise,然后使用Promise.then完成任务。虽然也可以这样做,但是代码并不优雅。

// 正确但不推荐的代码
/* 玩家进入游戏时,弹出一个欢迎对话框 */
world.onPlayerJoin(({entity}) => {
    entity.player.dialog({
        type: Box3DialogType.TEXT,
        title: "凌",
        content: `首先,${entity.player.name},这是一段错误代码。`,
    }).then(result => {
      // 下一步处理...
      entity.player.dialog({
          type: Box3DialogType.TEXT,
          title: "凌",
          content: `比如,这样就错了。`,
      });
    });
})

还有一个大家不怎么注意到的错误是忘记判断Promise返回的result是否为null(因为result为null | Box3DialogResponse)。这个问题在Javascript中是很难被发现的,相反在Typescript中就比较简单。若不判断result为null,则代码就有报错的可能性。相反,如果一个异步函数绝对不返回null,则在Promise中使用联合类型就是不恰当的。


回复

上一页1 页 / 共 1下一页
SKQASKQA

这真的是JavaScript吗?

点赞1


评论


Fox_AwaFox_Awa

对类型的解释不可避免地需要用到Typescript。但猫站并没有Typescript 语法高亮,故只能使用Javascript语法高亮代替。

点赞0


评论


星星上的雪花星星上的雪花

废话

点赞0


评论


指令者指令者

为什么这代码点了没有反应

world.onPlayerJoin(({ entity: { player } }) => {
    // 当玩家按下按键时,触发交互
    player.onPress(({ button }) => {
        if (button === Box3ButtonType.ACTION1) {
            if (!player.crouchButton) return;
            const result = player.dialog({
                type: Box3DialogType.SELECT,
                title: "滤镜",
                titleTextColor: new Box3RGBAColor(0, 0, 0, 1),
                titleBackgroundColor: new Box3RGBAColor(0.968, 0.702, 0.392, 1),
                content: "请选择滤镜",
                options: ['无', '反色', '热成像'],
                contentTextColor: new Box3RGBAColor(0, 0, 0, 1),
                contentBackgroundColor: new Box3RGBAColor(1, 1, 1, 1),
            })
            // 如果玩家点击了屏幕其他区域,取消了对话框。
            if (!result || result === null) {
                player.directMessage('你取消了对话框。');
                return;
            }

            // 判断玩家选了什么选项。
            switch (result.index) {
                case 0:
                    //如果选择了第三项,即:'无'
                    player.directMessage('你取消了滤镜');
                    player.colorLUT = '';
                    break;
                case 1:
                    // 如果选择了第二项,即:'反色'
                    player.directMessage('你切换了反色');
                    player.colorLUT = 'luts/Inverted.lut';
                    break;
                case 2:
                    //如果选择了第三项,即:'无'
                    // 如果选择了第一项,即:'热成像'
                    player.directMessage('你切换了热成像');
                    player.colorLUT = 'lut/Psychedelic.lut';
                    break;
                default:
                // 注意,使用 switch 分支的时候,不要漏了后面的 break; 
            }
        }
    })
});

点赞0


评论


凌cloud凌cloud

emmm......

点赞0


评论


145a145a

我看不懂,这是JS吗

点赞0


评论