|
| 1 | +#import "mod.typ": * |
| 2 | + |
| 3 | +#show: book.page.with(title: "成员与方法") |
| 4 | + |
| 5 | +// == 复合字面量 |
| 6 | + |
| 7 | +// ,它们是: |
| 8 | +// + #term("array literal", postfix: "。") |
| 9 | +// + #term("dictionary literal", postfix: "。") |
| 10 | + |
| 11 | +Typst提供了一系列「成员」和「方法」访问字面量、变量与函数中存储的“信息”。 |
| 12 | + |
| 13 | +其实在上一节(甚至是第二节),你就已经见过了「成员」语法。<grammar-member-exp>你可以通过「点号」获得代码块的“text”(文本内容): |
| 14 | + |
| 15 | +#code(```typ |
| 16 | +#repr(`OvO`.text), #type(`OvO`), #type(`OvO`.text) |
| 17 | +```) |
| 18 | + |
| 19 | +每个类型有哪些「成员」是由Typst决定的。你需要逐渐积累经验以知晓这些「成员」的分布,才能更快地通过访问成员快速编写出收集和处理信息的脚本。(todo: 建议阅读《参考:XXX》) |
| 20 | + |
| 21 | +当然,为防你不知道,大家不都是死记硬背的:有软件手段帮助你使用这些「成员」。许多编辑器都支持LSP(Language Server Protocol,语言服务),例如VSCode安装Tinymist LSP。当你对某个对象后接一个点号时,编辑器会自动为你做代码补全。 |
| 22 | + |
| 23 | +#figure(image("./IDE-autocomplete.png", width: 120pt), caption: [作者使用编辑器作代码补全的精彩瞬间。]) |
| 24 | + |
| 25 | +从图中可以看出来,该代码片段「对象」上有七个「成员」。特别是“text”成员赫然立于其中,就是它了。除了「成员」列表,编辑器还会告诉你每个「成员」的作用,以及如何使用。这时候只需要选择一个「成员」作为补全结果即可。 |
| 26 | + |
| 27 | +== 方法 <grammar-method-exp> |
| 28 | + |
| 29 | +「方法」是一种特殊的「成员」。准确来说,如果一个「成员」是一个对象的函数,那么它就被称为该对象的「方法」。 |
| 30 | + |
| 31 | +来看以下代码,它们输出了相同的内容,事实上,它们是*同一*「函数调用」的不同写法: |
| 32 | + |
| 33 | +#code(```typ |
| 34 | +#let x = "Hello World" |
| 35 | +#let str-split = str.split |
| 36 | +#str-split(x, " ") \ |
| 37 | +#str.split(x, " ") \ |
| 38 | +#x.split(" ") |
| 39 | +```) |
| 40 | + |
| 41 | +第三行脚本含义对照如下。之前已经学过,这正是「函数调用」的语法: |
| 42 | + |
| 43 | +```typ |
| 44 | +#( str-split( x, " " )) |
| 45 | +// 调用 字符串拆分函数,参数为 变量x和空格 |
| 46 | +``` |
| 47 | + |
| 48 | +与第三行脚本相比,第四行脚本仍然是在做「函数调用」,只不过在语法上更为紧凑。 |
| 49 | + |
| 50 | +第五行脚本则更加简明,此即「方法调用」。约定`str.split(x, y)`可以简写为`x.split(y)`,如果: |
| 51 | ++ 对象`x`是`str`类型,且方法`split`是`str`类型的「成员」。 |
| 52 | ++ 对象`x`用作`str.split`调用的第一个参数。 |
| 53 | + |
| 54 | +「方法调用」即一种特殊的「函数调用」规则(语法糖),在各编程语言中广泛存在。其大大简化了脚本。但你也可以选择不用,毕竟「函数调用」一样可以完成所有任务。 |
| 55 | + |
| 56 | +#pro-tip[ |
| 57 | + 这里有一个问题:为什么Typst要引入「方法」的概念呢?主要有以下几点考量。 |
| 58 | + |
| 59 | + 其一,为了引入「方法调用」的语法,这种语法相对要更为方便和易读。对比以下两行,它们都完成了获取`"Hello World"`字符串的第二个单词的第一个字母的功能: |
| 60 | + |
| 61 | + #code( |
| 62 | + ```typ |
| 63 | + #"Hello World".split(" ").at(1).split("").at(1) |
| 64 | + #array.at(str.split(array.at(str.split("Hello World", " "), 1), ""), 1) |
| 65 | + ```, |
| 66 | + al: top, |
| 67 | + ) |
| 68 | + |
| 69 | + 可以明显看见,第二行语句的参数已经散落在括号的里里外外,很难理解到底做了什么事情。 |
| 70 | + |
| 71 | + 其二,相比「函数调用」,「方法调用」更有利于现代IDE补全脚本。你可以通过`.split`很快定位到“字符串拆分”这个函数。 |
| 72 | + |
| 73 | + 其三,方便用户管理相似功能的函数。不仅仅是字符串可以拆分,似乎内容及其他许多类型也可以拆分。如果一一为它们取不同的名字,那可就太头疼了。相比,`str.split`就简单多了。要知道,很多程序员都非常头痛为不同的变量和函数取名。 |
| 74 | +] |
| 75 | + |
| 76 | +== 数组和字典的成员访问 |
| 77 | + |
| 78 | +为了访问数组,你可以使用`at`方法。“at”在中文里是“在”的意思,它表示对「数组」使用「索引」操作。`at(0)`索引到第1个值,`at(n)`索引到第 $n + 1$ 个值,以此类推。如下所示: |
| 79 | + |
| 80 | +#code(```typ |
| 81 | +#let x = (1, "OvO", [一段内容]) |
| 82 | +#x.at(0), #x.at(1), #x.at(2) |
| 83 | +```) |
| 84 | + |
| 85 | +至于「索引」从零开始的原因,这只是约定俗成。等你习惯了,你也会变成计数从零开始的好程序员。 |
| 86 | + |
| 87 | +为了访问字典,你可以使用`at`方法。但由于「键」都是字符串,你需要使用字符串作为字典的「索引」。 |
| 88 | + |
| 89 | +#code(```typ |
| 90 | +#let cat = (attribute: [kawaii\~]) |
| 91 | +#cat.at("attribute") |
| 92 | +```) |
| 93 | + |
| 94 | +为了方便,Typst允许你直接通过成员方法访问字典对应「键」的值: <grammar-dict-member-exp> |
| 95 | + |
| 96 | +#code(```typ |
| 97 | +#let cat = ("attribute": [kawaii\~]) |
| 98 | +#cat.attribute |
| 99 | +```) |
| 100 | + |
| 101 | +== 数组和字典的「存在谓词」 |
| 102 | + |
| 103 | +为了访问数组,你可以使用`contains`方法。“contain”在中文里是“包含”的意思,如下所示: |
| 104 | + |
| 105 | +#code(```typ |
| 106 | +#let x = (1, "OvO", [一段内容]) |
| 107 | +#x.contains[一段内容] |
| 108 | +```) |
| 109 | + |
| 110 | +因为这太常用了,typst专门提供了`in`语法,表示判断`x`是否*存在于*某个数组中:<grammar-array-in> |
| 111 | + |
| 112 | +#code(```typ |
| 113 | +#([一段内容] in (1, "OvO", [一段内容])) \ |
| 114 | +#([另一段内容] in (1, "OvO", [一段内容])) |
| 115 | +```) |
| 116 | + |
| 117 | +字典也可以使用此语法,表示判断`x`是否是字典的一个「键」。特别地,你还可以前置一个`not`判断`x`是否*不在*某个数组或字典中:<grammar-dict-in> |
| 118 | + |
| 119 | +#code(```typ |
| 120 | +#let cat = (neko-mimi: 2) |
| 121 | +#("neko-kiki" not in cat) |
| 122 | +```) |
| 123 | + |
| 124 | +注意:`x in (...)`与`"x" in (...)`是不同的。例如`neko-mimi in cat`将检查`neko-mimi`变量的内容是否是字典变量`cat`的一个「键」,而`"neko-mimi"`检查对应字符串是否在其中。 |
| 125 | + |
| 126 | +== 总结 |
| 127 | + |
| 128 | +// Typst如何保证一个简单函数甚至是一个闭包是“纯函数”? |
| 129 | + |
| 130 | +// 答:1. 禁止修改外部变量,则捕获的变量的值是“纯的”或不可变的;2. 折叠的对象是纯的,且「折叠」操作是纯的。 |
| 131 | + |
| 132 | +// Typst的多文件特性从何而来? |
| 133 | + |
| 134 | +// 答:1. import函数产生一个模块对象,而模块其实是文件顶层的scope。2. include函数即执行该文件,获得该文件对应的内容块。 |
| 135 | + |
| 136 | +// 基于以上两个特性,Typst为什么快? |
| 137 | + |
| 138 | +// + Typst支持增量解析文件。 |
| 139 | +// + Typst所有由*用户声明*的函数都是纯的,在其上的调用都是纯的。例如Typst天生支持快速计算递归实现的fibnacci函数: |
| 140 | + |
| 141 | +// #code(```typ |
| 142 | +// #let fib(n) = if n <= 1 { n } else { fib(n - 1) + fib(n - 2) } |
| 143 | +// #fib(42) |
| 144 | +// ```) |
| 145 | +// + Typst使用`include`导入其他文件的顶层「内容块」。当其他文件内容未改变时,内容块一定不变,而所有使用到对应内容块的函数的结果也一定不会因此改变。 |
| 146 | + |
| 147 | +// 这意味着,如果你发现了Typst中与一般语言的不同之处,可以思考以上种种优势对用户脚本的增强或限制。 |
| 148 | + |
| 149 | +#todo-box[总结] |
| 150 | + |
| 151 | +== 习题 |
| 152 | + |
| 153 | +#todo-box[习题] |
0 commit comments