2.11 自定义字符串插值器
问题
你想要创建自定义的字符串插值器,就像Scala自带的s、f和raw字符串插值器一样。
解决方案
要创建自定义的字符串插值器,你需要知道的是,当程序员写下foo"a b c"这样的代码时,这段代码会被转换为StringContext类上的foo方法调用。具体来说,这段代码:
会被翻译成:
因此,要创建一个自定义的字符串插值器,需要将foo创建为StringContext上的Scala 3扩展方法。此外还有一些额外的细节需要了解,我将在后面的一个例子中展示这些细节。
假设你想创建一个名为caps的字符串插值器,将字符串中的每个字都大写,像这样:
要创建caps,需要将其定义为StringContext的一个扩展方法。因为要创建一个字符串插值器,所以该方法会返回一个字符串,所以解决方案应该是类似下面这样:
因为一个预插值字符串可以包含任何类型的多个表达式,所以需要定义caps接受一个Any类型的varargs参数,所以可以这样写:
要定义caps的函数体,接下来要知道的是,原始字符串以两个不同的变量形式出现:
·sc是StringContext的实例,通过迭代器提供其数据。
·args.iterator是Iterator[Any]的实例。
下面这段代码展示了使用上面所说的迭代器将字符串中每一个字符都变成大写的方法:
以下是对这段代码的简单描述:
1.为两个迭代器创建变量。strings变量包含了输入字符串中的所有字符串字面量,而expressions则包含了代表输入字符串中所有表达式的值,比如$a变量。
2.通过在while循环中对这两个迭代器进行迭代来填充一个StringBuilder。这就开始把字符串重新组合起来,包括所有的字符串字面量和表达式。
3.StringBuilder被转换回一个字符串,然后调用一系列转换函数,将字符串中的每个字符变成大写。
当然还有其他方法来实现该功能,但这里使用这种方法是为了明确所涉及的步骤,特别是当创建了像caps"a $b c ${d*e}"这样的插值器时,则需要使用两个迭代器来重建字符串。
讨论
理解解决方案有助于理解字符串插值的工作原理,换句话说就是有助于理解在IDE中输入的Scala代码是如何转换为其他Scala代码的。通过字符串插值,你的方法的调用者会写出这样的代码:
在这段代码里:
·id是字符串插值方法的名称,在前面的例子中是caps。
·textN片段是输入(预插值)字符串中的字符串常量。
·exprN片段是输入字符串中用$expr或${expr}语法编写的表达式。
编译时,编译器将这段代码翻译成类似下面这样的代码:
如上所示,字符串的常量部分也就是字符串的字面量会被提取出来作为参数传递给StringContext构造函数。包含在初始字符串中的所有表达式会被作为参数传递给StringContext实例的id方法,对于前一个例子来说就是caps方法。
下面来看一个具体的例子,假设有一个名为yo的插值器和这段代码:
在编译阶段的第一步,最后一行被转换为这样:
现在,yo方法需要像解决方案中所示的caps方法那样处理这两个迭代器:
更多的插值器
关于更多的细节,在本书的GitHub(https://github.com/alvinj/ScalaCook-bookV2Examples)项目中展示了几个插值器的例子,包括我的Q插值器,它可以对这种多行字符串输入进行转换:
得到这样的结果:
另见
·本节使用的扩展方法将在8.9节中讨论。
·关于字符串插值的Scala官方文档(https://oreil.ly/A3hqn)。