1. 主页
  2. 文档
  3. 学习君土脚本
  4. 君土脚本对象入门
  5. 接口

接口

介绍

君土脚本的核心原则之一是, 对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在君土脚本里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

接口初探

下面通过一个简单示例来观察接口是如何工作的:

务 打印标签(标签对象: { 标签: 文 }) {
  控制台.日志(标签对象.标签);
}

定 象0 = { 尺寸: 10, 标签: "尺寸为10的对象" };
打印标签(象0);

类型检查器会查看打印标签的调用。 打印标签有一个参数,并要求这个对象参数有一个名为标签类型为的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。 然而,有些时候君土脚本却并不会这么宽松,我们下面会稍做讲解。

接下来, 我们重写上面的例子,这次使用接口来描述:必须包含一个标签属性且类型为

型 型标签值 {
  标签: 文;
}

务 打印标签(标签对象: 型标签值) {
  控制台.日志(标签对象.标签);
}

定 象0 = { 尺寸: 10, 标签: "尺寸为10的对象" };
打印标签(象0);

型标签值接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个标签属性且类型为的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给打印标签的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在使用“可选包”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

下面是使用了“可选包”的例子:

型 矩形配置 {
  颜色?: 文;
  宽?: 数;
}

务 建矩形(配置: 矩形配置): { 颜色: 文; 面积: 数 } {
  定 新矩形 = { 颜色: "白", 面积: 100 };
  若 (配置.颜色) {
    新矩形.颜色 = 配置.颜色;
  }
  若 (配置.宽) {
    新矩形.面积 = 配置.宽 * 配置.宽;
  }
  回 新矩形;
}

定 矩形0 = 建矩形({ 颜色: "黑" });

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。 比如,我们故意将建矩形里的颜色属性名拼错,就会得到一个错误提示:

型 矩形配置 {
  颜色?: 文;
  宽?: 数;
}

务 建矩形(配置: 矩形配置): { 颜色: 文; 面积: 数 } {
  定 新矩形 = { 颜色: "白", 面积: 100 };
  若 (配置.颜色) {
    新矩形.颜色 = 配置.颜; // 错误:类型“矩形配置”上不存在属性“颜
  }
  若 (配置.宽) {
    新矩形.面积 = 配置.宽 * 配置.宽;
  }
  回 新矩形;
}

定 矩形0 = 建矩形({ 颜色: "黑" });

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用固/*readonly*/来指定只读属性:

  型 点 {
    固 横: 数;
    固 竖: 数;
  }

你可以通过赋值一个对象字面量来构造一个。 赋值后,再也不能被改变了。

  定 点1: 点 = { 横: 10, 竖: 20 };
  点1.横 = 5; // 错误!

君土脚本具有固组/*ReadonlyArray*/<T>类型,它与组/*Array*/<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:

  定 组0: 数[] = [1, 2, 3, 4];
  定 固组0: 固组<数> = 组0;
  固组0[0] = 12; // 错误!
  固组0.压(5); // 错误!
  固组0.长 = 100; // 错误!
  组0 = 固组0; // 错误!

上面代码的最后一行,可以看到就算把整个固组赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:

  组0 = 固组0 作 数[];

固对常

固和常都是让它们修饰的内容变成只读, 什么时候使用哪种呢? 最简单判断该用还是的方法是, 看要把它做为变量使用还是做为一个属性。 做为变量使用的话用,若做为属性则使用

额外的属性检查

我们在第一个例子里使用了接口,君土脚本让我们传入{ 尺寸: 数; 标签: 文; }到仅期望得到{ 标签: 文; }的函数里。 我们已经学过了可选属性,并且知道他们在“可选包”模式里很有用。

然而,天真地将这两者结合的话就会像在爪哇脚本(JavaScript)里那样搬起石头砸自己的脚。 比如,拿建矩形例子来说:

型 矩形配置 {
  颜色?: 文;
  宽?: 数;
}
务 建矩形(配置: 矩形配置): { 颜色: 文; 面积: 数 } {
// ...
}

定 矩形0 = 建矩形({ 颜: "黑", 宽: 100 });  // 错误

注意传入建矩形的参数拼写为而不是颜色

你可能会争辩这个程序已经正确地类型化了,因为属性是兼容的,不存在颜色属性,而且额外的属性是无意义的。

然而,君土脚本会认为这段代码可能存在错误。 对象字面量会被特殊对待而且会经过额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。

// 错误:类型“{ 颜: 文; 宽: 数; }”的参数不能赋给类型“矩形配置”的参数。
// 对象文字可以只指定已知属性,并且“颜”不在类型“矩形配置”中。
定 矩形0 = 建矩形({ 颜: "黑", 宽: 100 });

绕开这些检查非常简单。 最简便的方法是使用类型断言:

定 矩形1 = 建矩形({ 宽: 100, 模糊: 0.5 } 作 矩形配置);

然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果矩形配置带有上面定义的类型的颜色属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:

  型 矩形配置 {
    颜色?: 文;
    宽?: 数;
    [属性名: 文]: 化;
  }

我们稍后会讲到索引签名,但在这我们要表示的是矩形配置可以有任意数量的属性,并且只要它们不是颜色,那么就无所谓它们的类型是什么。

还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量: 因为矩形配置不会经过额外属性检查,所以编译器不会报错。

  定 配置 = { 颜: "红", 宽: 100 };
  定 矩形2 = 建矩形(配置);

要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的错误。 就是说你遇到了额外类型检查出的错误,比如“可选包”,你应该去审查一下你的类型声明。 在这里,如果支持传入颜色属性到建矩形,你应该修改矩形配置定义来体现出这一点。

函数类型

接口能够描述君土脚本中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

  型 搜索函数 {
    (源文: 文, 子文: 文): 两;
  }

这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

  定 搜索0: 搜索函数;
  搜索0 = 务(源文: 文, 子文: 文) {
    定 结果 = 源文.搜(子文);
    回 结果 > -1;
  }

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 比如,我们使用下面的代码重写上面的例子:

  定 搜索0: 搜索函数;
  搜索0 = 务(源: 文, 子: 文): 两 {
    定 结果 = 源.搜(子);
    回 结果 > -1;
  }

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,君土脚本的类型系统会推断出参数类型,因为函数直接赋值给了搜索函数类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是)。 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与搜索函数接口中的定义不匹配。

  定 搜索0: 搜索函数;
  搜索0 = 务(源, 子) {
    定 结果 = 源.搜(子);
    回 结果 > -1;
  }

可索引的类型

与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如甲[10]文章对应图["中庸"]。 可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。 让我们看一个例子:

  型 字符串数组 {
    [索引: 数]: 文;
  }

  定 数组0: 字符串数组;
  数组0 = ["小明", "小花"];

  定 名0: 文 = 数组0[0];

上面例子里,我们定义了字符串数组接口,它具有索引签名。 这个索引签名表示了当用去索引字符串数组时会得到类型的返回值。

共有支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用来索引时,君土脚本会将它转换成然后再去索引对象。 也就是说用100(一个)去索引等同于使用"100"(一个)去索引,因此两者需要保持一致。

  类 动物 {
    名: 文 = '';
  }
  类 狗 承 动物 {
    品种: 文 = '';
  }

  型 工作 {
    [序: 数]: 狗;
    [序: 文]: 动物;
  }

  // 错误:使用'文'索引,有时会得到 动物!
  型 不工作 {
    [序: 数]: 动物;
    [序: 文]: 狗;
  }

字符串索引签名能够很好的描述字典模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了对象.属性对象["属性"]两种形式都可以。 下面的例子里,的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

  型 数型字典 {
    [序: 文]: 数;
    长: 数;    // 可以,长 是 数 类型
    名: 文       // 错误,`名`的类型与索引类型返回值的类型不匹配
  }

最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:

  型 只读字串数组 {
    固 [序: 数]: 文;
  }
  定 数组0: 只读字串数组 = ["小明", "小花"];
  数组0[2] = "小华"; // 错误!

你不能设置数组0[2],因为索引签名是只读的。

类类型

实现接口

与一些编程语言里接口的基本作用一样,君土脚本也能够用它来明确的强制一个类去符合某种契约。

  型 时钟接口 {
    当前时间: 历;
  }

  类 时钟 具 时钟接口 {
    当前时间: 历;
    构(时: 数, 分: 数) {
      此.当前时间 = 启 历();
     }
  }

你也可以在接口中描述一个方法,在类里实现它,如同下面的置时间方法一样:

  型 时钟接口 {
    当前时间: 历;
    置时间(时间: 历): 无;
  }

  类 时钟 具 时钟接口 {
    当前时间: 历;
    构(时: 数, 分: 数) {
      此.当前时间 = 启 历();
    }
    置时间(时间: 历): 无 {
      此.当前时间 = 时间;
    }
  }

接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。

继承接口

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

  型 形状 {
    颜色: 文;
  }

  型 矩形 承 形状 {
    边长: 数;
  }

  定 矩形0 = <矩形>{};
  矩形0.颜色 = "蓝";
  矩形0.边长 = 10;

一个接口可以继承多个接口,创建出多个接口的合成接口。

  型 形状 {
    颜色: 文;
  }
  型 画笔 {
    笔宽: 数;
  }

  型 矩形 承 形状, 画笔 {
    边长: 数;
  }

  定 矩形0 = <矩形>{};
  矩形0.颜色 = "蓝";
  矩形0.边长 = 10;
  矩形0.笔宽 = 5.0;