整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

谁说 Vim 不好用?送你一个五彩斑斓的编辑器

谁说 Vim 不好用?送你一个五彩斑斓的编辑器

创:良许 源:良许Linux

相信大家在使用各种各样强大的 IDE 写代码时都会注意到,代码中各种类型的关键字会用独特的颜色标记出来,然后形成一套语法高亮规则。这样不仅美观,而且方便代码的阅读。

而在上古神器 Vim 中,我们通常看到的是黑底白字,没有任何其它颜色。难道我们的 Vim 就这么枯燥乏味吗?当然不是,Vim 隐藏了非常多非常实用的功能与技巧,不是老司机根本就不知道。下面来详细介绍。

在开始之前,我们要先检查一下系统中是否安装了 Vim 编辑器。一般来说 Ubuntu 系统是默认自带的,没有的小伙伴请先自行安装。

$ sudo apt-get install vim

然后我们运行以下命令来查看安装好的 Vim 编辑器的版本。

$ vim -version

接下来,我们就可以为 Vim 编辑器添加配色方案。为了能看出效果,我们准备了一个名为 login.sh 的 bash 文件,它的内容如下:

login.sh

!/bin/bash
echo "Type your username"
read username
echo "Type your password"
read password
if [[ ( $username=="admin" && $password=="secret" ) ]]; then
echo "Authorized user"
else
echo "Unauthorized user"
fi

运行以下命令使用 Vim 编辑器打开该文件:

$ vim login.sh

启用/禁用语法高亮显示

有些发行版的 Vim 编辑器已经帮你默认启用语法高亮,而有些发行版却没有。如果没有默认打开语法高亮,那么我们就需要自行打开。

打开的方法其实也很简单。在 Vim 编辑器中打开 login.sh 文件后,按 ESC 键并键入 :syntax on ,语法高亮显示就打开了,非常方便。

同样,关闭语法高亮也很简单,只需按 ESC 键并键入 :syntax off即可。

永久启用或禁用语法高亮显示

刚刚那种打开语法高亮的方法只是暂时性的,一旦当前文件关闭,下次打开文件就需要重新打开语法高亮。

如果想要永久启用或禁用语法高亮显示,就需要修改 .vimrc 文件。首先,我们使用 Vim 打开 .vimrc 文件。

$ sudo vim ~ / .vimrc

然后,在打开的文件中添加一句 syntax on 命令,代表永久启用语法高亮显示。最后再输入 :wq 保存并关闭文件。

如果想要永久禁用语法高亮,则添加 syntax off 命令即可。

改变配色方案

在默认情况下,打开语法高亮后 Vim 为你应用了一套默认的配色方案,而实际上 Vim 还有很多套配色方案可供我们使用,我们可以自行去修改。

安装 Vim 后,颜色方案文件位于 /usr/share/vim/vim*/colors/中。我们可以运行以下命令来查看 Vim 配色方案文件的列表。

$ ls -l /usr/share/vim/vim*/colors/

可以看出 Vim 为我们提供了非常多的配色方案,我们可以根据自己的需要自由选择。假设,我们有一个 hello.html 的 html 文件,我们想要将它的的配色方案改为 morning 类型。

hello.html

<html>
<head>
<title> Welcome </title>
</head>
<body>
<p> Hello Everybody, Welcome to our site </p>
</body>
</html>

我们首先运行以下命令在 Vim 中打开这个 html 文件。

$ vim hello.html

按 ESC 并键入 :colorscheme morning ,这样我们就更改了文件当前的颜色方案。

应用新颜色方案后,效果如下图显示。但这种更改依然是暂时的,关闭编辑器后配色方案将被重置。

如果想要永久设置特定的配色方案及背景,那么我们需要再次打开 .vimrc 文件并添加以下命令文本。下面配置的是夜晚配色方案,会将背景颜色设置为深色。

color evening
set background=dark

之后再重新打开刚才的文件,效果就会变成如下图所示。

根据语言选择配色方案

Vim 编辑器支持多种语言的语法,例如 PHP,Perl,Python,awk 等。根据不同语言,它会应用一套相应语言的配色方案。

比如现在有一个名为 average.py 的 Python 文件,我们用 Vim 编辑器打开它。通过 Vim 编辑器的默认配色方案,文件的显示效果如下图所示。

这是一个 Python 文件,但如果我们想以 Perl 的语法风格来显示,要怎么操作?我们只需输入 :set syntax=perl ,按 Enter 键就可以了,效果如下图所示。

:set syntax=perl

自定义配色方案

前面所介绍的那些配色方案,其实都是系统自带的,我们只是选择一下而已。如果我们想要个性化一点,自定义一套配色方案,要怎么操作?强大的 Vim 也是支持你这个需求的!

一般而言,语法需要高亮的无非就是 变量、注释、常量 之类的,Vim 编辑器支持的语法高亮组有如下 9 个:

Group Name

Description

Identifier

变量

Statement

关键字,例如 if,else,do,while等

Comment

注释

Type

数据类型,如 int、double、string等

PreProc

预处理器语句,比如 #include

Constant

常量,如数字、带引号的字符串、真/假等

Special

特殊符号,如“”、“”等

Underlined

带下划线的文本

Error

错误

下面举个例子。在 Vim 编辑器中打开名为 leap.py 的 Python 脚本文件。默认情况下,该文件的语法高亮显示如下图所示。

如果我们想把关键字变为红色,该怎么操作?只需输入 :hi Statement ctermfg=red 即可。这时,if 和 else 的颜色将变为红色。

:hi Statement ctermfg=red

当然,对于注释、常量、数据类型,我们也可以使用同样的方法来改变颜色。这样一来,你就会形成一套自己的配色方案,感觉是不是很酸爽?

最后,我自己是一名从事了多年开发的Python老程序员,辞职目前在做自己的Python私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的Python学习干货,可以送给每一位喜欢Python的小伙伴,想要获取的可以关注我的头条号并在后台私信我:01,即可免费获取。

、abbr

abbr 全称是 abbreviations,意思是缩写。应用场景也很简单,为一些文章中的缩写增加注释。

以前在文章中对于缩写的解释经常会这么做:

DAU(Daily Active User),日活跃用户数 ......

那我们用 abbr 标签呢?

<abbr title="Daily Active User">
    DAU
</abbr>
<span>,日活跃用户数 ......</span>

展示的效果如下:

这个标签就可以把全称隐藏掉,弱化信息量,让真正不知道该缩写的用户主动去获取缩写的具体意思,这个在 markdown 里经常会出现。

二、mark

<mark/> 在 markdown 中也是很常用的,用于将包裹的文本高亮展示。

<mark>高亮文本</mark>

效果如下:

如果全文统一高亮样式,可以专门对 mark 标签进行样式重置,这样就不用对你用的每个 div 加一个 highlight 的类名了,又不语义化,又徒增文档大小。

三、sup、sub

<sup/><sub/>分别表示上标和下标,在 markdown 中出现得也很频繁,比如数学公式和引用。

<div>3<sup>[2]</sup></div>
<div>4<sub>2</sub></div>

效果如下:

上标和下标的样式原理也比较简单,主要就是利用了 vertical-aligntopsub 属性值,然后将字号缩小,不过有现成的标签,干嘛不用呢?

四、figure

figure 是用于包裹其它标签的内容的,然后再利用另一个标签 figcaption ,可以对包裹的内容进行一个文本描述,例如:

<figure>
    <img src="/media/cc0-images/elephant-660-480.jpg"
         alt="大象">
    <figcaption>这是一张大象的照片</figcaption>
</figure>

效果如下:

那要是图片挂了呢?

再友好点处理,我们把 img 标签的 alt 属性去掉。

漂亮,终于把我一直厌烦的图裂 icon 给干掉了,样式还巨好看。

当然不止能包裹 img 标签,其它任何都是可以的。

嘿嘿,给大家在本文来个实战,下面这个可以点击,样式也是利用了 figure 这个标签。

我是figure标签产生的

五、progress

说到 <progress/> 这个标签就很有意思了,去年有段时间我做的业务里涉及到了进度条,当时是前同事做的,然后有一些性能问题,我就在研究如何优化,减少进度条改变带来的性能问题。

虽然最后问题是解决了,但是也有幸收到了张鑫旭大佬的评论,他告诉我 progress 这个标签就足够了,既有语义化,又有进度条的功能,性能还好,兼容性也很不错。后来经过一番尝试,还真是,当时是我孤陋寡闻了,也安利给大家。

<!-- 进度条最大值为100,当前进度为60,即60% -->
<progress max="100" value="60"/>

浏览器自带的样式就已经很好看了,效果如下:

业务中我们也就可以通过控制 value 属性,来改变进度条的进度了。

六、area

area 这个标签也非常有意思,它的作用是为图片提供点击热区,可以自己规定一张图的哪些区域可点击,且点击后跳转的链接,也可以设置成点击下载文件,我们来举个例子:

  <img src="example.png" width="100" height="100" alt="" usemap="#map">

  <map name="map">
    <area shape="rect" coords="0,0,100,50" alt="baidu" href="https://www.baidu.com">
    <area shape="rect" coords="0,50,100,100" alt="sougou" href="https://www.sogou.com/">
  </map>

area 一般要搭配 map 标签一起使用,每个 area 标签表示一个热区,例如上面代码中,我们定义了两个热区,热区形状都为rect(矩形),他们的热区分别是:

  • 坐标 (0,0) 到坐标 (100,50) 的一个矩形
  • 坐标 (0,50) 到坐标 (100,100) 的一个矩形

我们都知道,默认的坐标轴是这样的:

因此,我们划分的两个热区就是:

最后再来看一下我们的实际效果:

i

七、details

details 字面意思是 "详情",在 markdown 里也经常用,用该标签包裹了的内容默认会被隐藏,只留下一个简述的文字,我们点击以后才会展示详细的内容。

<details>
  <p>我是一段被隐藏的内容</p>
</details>

效果如下:

这还没有加任何一行的 js 代码,我们点击后,details 标签上会多一个 open 的属性,被隐藏的内容就展示出来了。

默认情况下,简要文字为 "详情",想要修改这个文字,要搭配 summary 标签来使用。

<details>
  <summary>点击查看更多</summary>
  <p>我是一段被隐藏的内容</p>
</details>

就搞定了!

八、dialog

浏览器自带弹窗方法 alertconfirmprompt,样式固定且每个浏览器不同,同时还会阻塞页面运行,除了这个还提供了一个 dialog 标签,它的使用方式有点类似于现在各大组件库的 Modal 组件了,浏览器还为该标签提供了原生的 dom 方法:showModalclose,可以直接控制弹窗的展示和隐藏。

<dialog id="dialog">
    <input type="text">
    <button id="close">ok</button>
</dialog>
<button id="openBtn">打开弹框</button>

<script>
    const dialog=document.getElementById('dialog')
    const openBtn=document.getElementById('openBtn')
    const closeBtn=document.getElementById('close')
  
    openBtn.addEventListener('click', ()=> {
        // 打开弹框
        dialog.showModal()
    })
    closeBtn.addEventListener('click', ()=> {
        // 隐藏弹框
        dialog.close()
    })
</script>

效果如下:

细心的你有没有发现,这原生的弹框还自带背景蒙层,点击是关闭不掉的,但起码它不会阻塞页面。

然后我们在弹窗展示时,也可以通过 esc 键来关闭弹窗。

九、datalist

datalist 是用于给输入框提供可选值的一个列表标签,类似咱们常用的 Select 组件。

我可以用其实现一个 "输入联想" 的功能。

<label> 输入C开头的英文单词:</label>
<input list="c_words"/>

<datalist id="c_words">
  <option value="China">
  <option value="Click">
  <option value="Close">
  <option value="Const">
  <option value="Count">
</datalist>

来试一试:

刚点击时会把所有推荐的选项都列出来,然后根据后面输入的内容,会过滤掉不匹配的选项,比如我输入 cl,会过滤掉不是 cl 开头的单词,最后只剩下 ClickClose 了。

最后我发现,他这个下拉框有点好看啊?为啥这原生的 input 框默认样式那么丑,啥时候改改。

十、fieldset

fieldset 标签是用于分组管理 form 表单内的元素的,若 fieldset 设置了 disabled 属性,则被其包裹的所有表单元素都会被禁用置灰,且不会随着表单一起提交上去,是的就成了摆设。

什么意思呢?看个例子:

<form action="/example">
  <fieldset disabled>
    <legend>被禁用区域</legend>
    <label>ID:</label>
    <input type="text" name="id" value="1">
    <label>邮箱:</label>
    <input type="text" name="email" value="1234567@163.com">
  </fieldset>
  <label>名字:</label>
  <input type="text" name="name">
  <button type="submit">提交</button>
</form>

这里我们把 ID邮箱 的表单包裹了起来,且设置了 disabled,只开放了一个 name 的输入控件,此时界面如下:

可以看到除了 name 输入框,其它的两个输入框都被禁用了,此时点提交会是什么样子呢?

嗯,只提交了 name 字段。

十一、noscript

这个标签是在浏览器不支持或禁用了 javascript 时才展示的,大多用于对 js 强依赖的应用,比如现在大部分的 SPA 页面,一旦不支持 javascript,页面基本上什么内容都没了,此时可以靠这个标签做友好提示。

一般我们不需要特地去使用,大多都是在打包过程中自动插入到 html 静态文件里去的,例如:

// init.js
const root=document.getElementById('root')
const button=document.createElement('button')
button.innerText='点击出弹窗'
root.appendChild(button)
<!-- index.html -->
<script defer src="./init.js"></script>

<noscript>
  不好意思,你的浏览器不支持或禁用了 JavaScript,请更换浏览器或启用 JavaScript
</noscript>

<div id="root"></div>

未禁用 javascript 时,页面是这样的:

禁用了 javascript 时,是这样的:

文要点

  • 作为一项预览特性,Java SE 13(2019 年 9 月)引入了文本块,旨在减轻在 Java 中声明和使用多行字符串字面量的痛苦。随后,第二个预览版本对它做了一些细微的改进,并计划在 Java SE 15(2020 年 9 月)中成为 Java 语言的一个永久特性。
  • Java 程序中的字符串字面量不限于“yes”和“no”这样的短字符串;它们经常对应于结构化语言(如 HTML、SQL、XML、JSON,甚至 Java)中的整个“程序”。
  • 文本块是可以包含多行文本的字符串字面量,使用三重引号(""")作为开始和结束分隔符。
  • 文本块可以看作是嵌入在 Java 程序中的二维文本块。
  • 如果能够保留嵌入的那个程序的二维结构,又不必使用转义字符和其他会造成干扰的语法,那就可以降低出错的可能,开发出可读性更好的程序。

在 QCon 纽约大会的演讲“ Java 的未来”中,Java 语言架构师 Brian Goetz 带我们快速浏览了 Java 语言近期和未来的一些特性。在本文中,他深入探讨了文本块。

作为一项预览特性,Java SE 13(2019 年 9 月)引入了文本块,旨在减轻在 Java 中声明和使用多行字符串字面量的痛苦。

随后,第二个预览版本对它做了一些细微的改进,并计划在 Java SE 15(2020 年 9 月)中成为 Java 语言的一个永久特性。

文本块是可以包含多行文本的字符串字面量,如下所示:

复制代码

String address="""                 25 Main Street                 Anytown, USA, 12345                 """;

在下面这个简单的示例中,变量 address 是一个两行的字符串,每行后面都有行终止符。如果没有文本块,我们必须这样写:

复制代码

String address="25 Main Street\n" +                 "Anytown, USA, 12345\n";                 

复制代码

String address="25 Main Street\nAnytown, USA, 12345\n";

每个 Java 开发人员都知道,这些写法都非常麻烦。但是,更重要的是,它们更容易出错(很容易忘记\n 并且发现不了),也更难阅读(因为语言语法与字符串的内容混在一起)。由于文本块通常没有转义字符和其他语法干扰,读者更容易看明白字符串的内容。

字符串字面量中最常见的转义字符是换行符(\n),文本块支持直接表示多行字符串而不需要换行符。除了换行符之外,另一个最常用的转义字符是双引号("),这个必须转义,因为它与字符串字面量分隔符冲突。文本块不需要这样,因为单引号与三引号文本块分隔符并不冲突。

为什么起了这样一个名字?

有人可能会说,这个特性应该叫“多行字符串字面量”(很多人可能会这样称呼它)。但是,我们选择了一个不同的名称文本块,为的是强调:文本块不是不相关行的集合,而是嵌入到 Java 程序中的二维文本块。为了说明这里所说的“二维”是什么意思,我们举一个稍微结构化一些的例子,在这个例子中,文本块是一个 XML 片段。(同样的考量也适用于其他“语言”的“程序”片段,如 SQL、HTML、JSON,甚至 Java,作为字面量嵌入到 Java 程序中。)

复制代码

void m() {    System.out.println("""                       <person>                           <firstName>Bob</firstName>                           <lastName>Jones</lastName>                       </person>                       """);}

作者希望这段代码打印什么?虽然我们无法读取他们的想法,但似乎不太可能是想让 XML 块缩进 21 个空格;更可能是,这 21 个空格只是用来将文本块与周围的代码对齐。另一方面,几乎可以肯定,作者的意图是输出的第二行应该比第一行多缩进四个空格。此外,即使作者确实需要缩进 21 个空格,当程序修改、周围代码的缩进发生变化时,又会发生什么呢?我们不希望输出的缩进仅仅因为源代码被重新格式化而改变,也不希望文本块因为没有合理对齐而看起来“格格不入”。

从这个例子中,我们可以看到,嵌入在程序源代码中的多行文本块的自然缩进来自于文本块各行行之间预期的相对缩进,以及文本块与周围代码之间的相对缩进。我们希望字符串字面量与代码对齐(因为如果不对齐就会显得格格不入),我们希望字符串的行可以反映出行之间的相对缩进,但是这两个缩进来源(我们可以称之为附带的和必需的)在源程序的表示中必然混杂在一起。(传统的字符串字面量没有这个问题,因为它们不能跨行,所以不用为了使内容对齐而在文本中添加额外的前导空格。)

解决这个问题的一种方法是使用库方法,我们可以将它应用于多行字符串字面量,比如 Kotlin 的 trimIndent 方法,而 Java 确实也提供了这样一个方法: String::stripIndent 。但是,因为这是很常见的问题,所以 Java 更进一步,会在编译时自动去除附带的缩进。

为了理顺附带的和必需的缩进,我们可以想象下,在包含整个代码段的 XML 代码段周围绘制一个最小的矩形,并将这个矩形里的内容视为一个二维文本块。这个“魔法矩形”是文本块的内容,它反映了文本块行之间的相对缩进,但是忽略了它所在的程序是如何缩进的。

这个“魔法矩形”的类比或许可以帮助我们理解文本块的工作方式,但是细节要更微妙一些,因为我们可能会希望更好地控制哪些缩进是附带的,哪些是必需的。可以使用结尾分隔符相对于内容的位置来平衡附带缩进和必需缩进。

详情

文本块使用三引号(""")作为其开始和结束分隔符,开始分隔符所在行的其余部分必须为空白。文本块的内容从下一行开始,一直延续到结束分隔符。块内容的编译时处理分为三个阶段:

  • 标准化行终止符。用 LF(\u000A)字符替换所有行终止符。这可以防止文本块的值受最后编辑它的平台的换行约定所影响。(Windows 使用 CR + LF 作为结束行;Unix 系统只使用 LF,甚至还有其他使用中的方案。)
  • 从每行中删除附带的前导空格和所有的尾随空格。附带空格通过以下方式确定:计算出一组确定行,这些行是上个步骤得出的所有非空行,以及最后一行(包含结束分隔符的行),即使它是空的;计算出所有确定行的公共空格前缀;从每个确定行中删除公共空格前缀。
  • 解释内容中的转义序列。文本块使用与字符串和字符字面量相同的转义序列集。执行上面这些操作意味着像\n、\t、\s 和 <eol> 这样的转义字符不会影响空格处理。( JEP 368 中新增了两个转义序列:显式空格\s,续行标记 <eol>。)

在我们的 XML 示例中,第一行和最后一行中的所有空格都将被删除,而中间两行会缩进四个空格,因为在这个例子中有五个确定行(包括四行 XML 代码和结束分隔符所在的行),行的缩进都至少和第一行内容的缩进相同。通常情况下,这种缩进就是我们期望的,但有时我们可能不希望去掉所有的前导缩进。例如,如果我们想让整个块缩进 4 个空格,那么我们可以通过将结束分隔符向左移动 4 个空格来实现:

复制代码

void m() {    System.out.println("""                       <person>                           <firstName>Bob</firstName>                           <lastName>Jones</lastName>                       </person>                   """);}

因为最后一行也是一个确定行,所以公共空格前缀现在是块的最后一行中结束分隔符之前的空格数量,每一行都按这个数量删除空格,整个块缩进 4 个空格。我们也可以通过编程方式管理缩进,通过实例方法 String::indent,它接受一个多行字符串(不管它是否来自文本块),并按固定数量的空格缩进每一行:

复制代码

void m() {    System.out.println("""                       <person>                           <firstName>Bob</firstName>                           <lastName>Jones</lastName>                       </person>                       """.indent(4));}

在极端情况下,如果不需要删除空格,则可以将结束分隔符一直移动到左边界:

复制代码

void m() {    System.out.println("""                       <person>                           <firstName>Bob</firstName>                           <lastName>Jones</lastName>                       </person>""");}

或者,我们可以通过将整个文本块移到左边界来达到同样的效果:

复制代码

void m() {    System.out.println("""<person>    <firstName>Bob</firstName>    <lastName>Jones</lastName></person>""");}

乍听起来,这些规则可能有些复杂,但选用这种规则是为了平衡各种竞争性问题(既希望能够相对于周围的程序缩进文本块,又不会产生可变数量的附带前导空格),同时,如果默认的算法不是想要的结果,还可以提供一个简单的方法来调整或选择不删除空格。

嵌入式表达式

Java 的字符串不支持表达式插值,而其他一些语言则支持;文本块也不支持。(在某种程度上,我们将来可能会考虑这个特性,不限于文本块,而是同样适用于字符串。)过去,参数化的字符串表达式是通过普通的字符串连接(+)构建的;Java 5 添加了 String::format 以支持“printf”样式的字符串格式。

由于需要对周围的空格进行全面分析,当通过字符串连接组合成文本块时,要想正确地缩进可不是一件简单的事。但是,文本块的处理结果是一个普通的字符串,所以我们仍然可以使用 String::format 来参数化字符串表达式。此外,我们可以使用新的 String::formatted 方法,它是 String::format 的实例版本。

复制代码

String person="""                <person>                    <firstName>%s</firstName>                    <lastName>%s</lastName>                </person>                """.formatted(first, last));

遗憾的是,这个方法也不能命名为 format,因为我们不能重载具有相同名称和参数列表的静态实例方法。

先例与历史

虽然从某种意义上说,字符串字面量是一个“微不足道”的特性,但它们的使用频率如此之高,足以让小烦恼累积成大烦恼。因此也就不奇怪,为什么缺乏多行字符串是近年来对 Java 最常见的抱怨之一,而许多其他语言都有多种形式的字符串字面量来支持不同的用例。

这或许令人惊讶,在各种流行的语言中,这一特性有许多不同的表示方式。说“想要多行字符串”很容易,但是在研究其他语言时,我们发现,它们的语法和目标各不相同。(当然,对于哪一种才是“正确”的方法,开发人员的观点也很不一样。)虽然没有任何两种语言是相同的,但是对于大多数许多语言都具有的特性(例如 for 循环),通常有一些通用的方法可供选择;在 15 种语言中找到同一特性的 15 种不同解释很罕见,但是,我们发现,多行原始字符串字面量就是这样。

下表显示了各种语言中字符串字面量的部分选项。其中,…是字符串字面量的内容,对于转义序列和嵌入式插值,可能会处理,也可能不会处理,xxx 表示一个分隔符,用户可以任意定义,但不能与字符串的内容冲突,而##表示数量可变的#号(可能是零个)。

语言语法说明Bash‘…’[span]Bash$’…’[esc] [span]Bash“…”[esc] [interp] [span]C“…”[esc]C++“…”[esc]C++R"xxx(…)xxx"[span] [delim]C#“…”[esc]C#$"…"[esc] [interp]C#@"…"Dart‘…’[esc] [interp]Dart“…”[esc] [interp]Dart‘’’…’’’[esc] [interp] [span]Dart“”"…"""[esc] [interp] [span]Dartr’…’[prefix]Go“…”[esc]Go…[span]Groovy‘…’[esc]Groovy“…”[esc] [interp]Groovy‘’’…’’’[esc] [span]Groovy“”"…"""[esc] [interp] [span]Haskell“…”[esc]Java“…”[esc]Javascript‘…’[esc] [span]Javascript“…”[esc] [span]Javascript…[esc] [interp] [span]Kotlin“…”[esc] [interp]Kotlin“”"…"""[interp] [span]Perl‘…’Perl“…”[esc] [interp]Perl<<‘xxx’[here]Perl<<“xxx”[esc] [interp] [here]Perlq{…}[span]Perlqq{…}[esc] [interp] [span]Python‘…’[esc]Python“…”[esc]Python‘’’…’’’[esc] [span]Python“”"…"""[esc] [span]Pythonr’…’[esc] [prefix]Pythonf’…’[esc] [interp] [prefix]Ruby‘…’[span]Ruby“…”[esc] [interp] [span]Ruby%q{…}[span] [delim]Ruby%Q{…}[esc] [interp] [span] [delim]Ruby<<-xxx[here] [interp]Ruby<<~xxx[here] [interp] [strip]Rust“…”[esc] [span]Rustr##"…"##[span] [delim]Scala“…”[esc]Scala“”"…"""[span]Scalas"…"[esc] [interp]Scalaf"…"[esc] [interp]Scalaraw"…"[interp]Swift##"…"##[esc] [interp] [delim]Swift##"""…"""##[esc] [interp] [delim] [span]

说明:

  • esc:某种程度的转义序列处理,转义符通常由 C 语言样式衍生而来(例如,\n);
  • interp:变量或任意表达式插值的部分支持;
  • span:多行字符串可以通过简单地跨越多个源代码行来表示;
  • here:在 "here-doc" 样式中,后面的行,一直到用户定义的分隔符所在的行,都被视为字符串字面量;
  • prefix:前缀形式对所有其他形式的字符串字面量也都是有效的,只是为了简洁起见而省略了;
  • delim:在某种程度上可定制分隔符,比如通过包含 nonce(C++)、不同数量的#字符(Rust、Swift),或者将花括号替换为其他匹配的括号(Ruby)。
  • strip:在某种程度上支持删除附带缩进。

虽然这个表说明了字符串字面量方法的多样性,但它实际上只触及了表面,因为语言解释字符串字面量的方法有各种细节的差异,不可能通过这样一个简单的表格完全说明。虽然大多数语言受 C 语言启发,使用一个转义字符,但它们支持的转义字符不同,是否支持以及如何支持 Unicode 转义(例如,\unnnn),不完全支持转义语言的形式是否仍然支持一些有限形式的分隔符字符转义(比如,对于嵌入式引用,使用两个引号而不是结束字符串。)简单起见,该表还省略了一些其他形式(如 C++ 中用于控制字符编码的各种前缀)。

语言之间最明显的差别是分隔符的选择,而不同的分隔符意味着不同形式的字符串字面量(有或没有转义字符,单行或多行,有或没有插值,字符编码选择,等等),但从中我们可以看到,这些语法的选择常常反映了该语言在设计哲学上的差异——如何平衡等各种目标,如简洁性、表现力和用户便利性。

毫不奇怪,脚本语言(bash、Perl、Ruby、Python)已经将“用户选择权”放在最高优先级,它们提供多种形式的字面量,可以用多种方式表示相同的东西。但是,一般来说,在鼓励用户如何看待字符串字面量、提供多少种形式以及这些形式的正交性方面,各种语言都不相同。我们还看到了一些关于多行字符串的理论。有一些(比如 JavaScript 和 Go)只是将行结束符看作另一个字符,允许所有形式的字符串字面量跨越多个行,有一些(比如 C++)把它们作为一种特殊的“原始”字符串,其他语言(比如 Kotlin)则将字符串分成“简单”和“复杂”两类,并将多行字符串归为“复杂”这一类,还有一些提供的选项太多,甚至都无法这样简单的分类。同样,它们对“原始字符串”的解释也各不相同。真正的原始需要某种形式的用户可控制的分隔符(如 C++、Swift 和 Rust 所具有的形式),尽管其他语言也号称其字符串是“原始的”,但它们仍然为其结束(固定)分隔符保留着某种形式的转义。

尽管有各种各样的方法和观点,但是从平衡原则性设计和表现力的角度来看,这项调查中有一个明显的“赢家”: Swift 。它通过一种灵活的机制(单行变体和多行变体)提供转义、插值和真正的原始字符串。在这些语言中,最新的语言拥有最动听的故事,这并不奇怪,因为它有后见之明,可以从其他人的成功和错误中汲取经验教训。(这里,关键的创新是,虽然转义分隔符各不相同,但都与字符串分隔符保持一致,可以避免在“熟(cooked)”和“生(raw)”模式之间进行选择,同时仍然跨所有形式的字符串字面量共享转义语言——这种方法“事后看来”是很值得赞扬的。)由于现有的语言限制,Java 无法大规模地采用 Swift 的方法,但是,Java 已尽可能地从 Swift 社区所做的优秀工作中汲取灵感——并为将来开展进一步的工作预留了空间。

差点采用的做法

文本块并不是该特性的第一次迭代;第一次迭代是原始字符串字面量。与 Rust 的原始字符串一样,它使用了一个大小可变的分隔符(任意数量的反单引号字符),并且完全不解释其内容。在完成了全部设计和原型之后,这个提议被撤回了。当时的判断是,这种做法虽然足够合理,但总让人觉得太过于死板——与传统字符串字面量没什么共同之处,因此,如果我们将来想扩展这个特性,就没法把它们一起扩展。(由于现在的发布节奏很快,这只是让该特性推迟了 6 个月,但我们会得到一个更好的特性。)

JEP 326 方法的一个主要缺点是,原始字符串的工作方式与传统的字符串字面量完全不同:不同的分隔符字符、变长分隔符 vs 固定分隔符、单行 vs 多行、转义 vs 非转义。总会有人想要一些不同选项的组合,想要更多不同的形式,因此,我们走了 Bash 所走的路。最重要的是,它没有解决“附加缩进”的问题,这显然会成为 Java 程序脆弱性的根源。基于此,文本块与传统的字符串字面量(分隔符语法、转义语言)有非常多的相似之处,只在一个关键方面有所不同——字符串是一维字符序列还是二维文本块。

风格指南

Oracle Java 团队的 Jim Laskey 和 Stuart Marks 发布了一份程序员指南,详细介绍了文本块,并提供了样式建议。

在可以提高代码清晰度时使用文本块。连接、转义换行和转义引号分隔符使字符串字面量的内容变得混乱;文本块解决了这个问题,内容更清晰,但在语法上,它们比传统的字符串字面量更重量级。务必要在好处大于额外成本的地方使用文本块;如果一个字符串可以放在一行中,没有转义换行,那么最好还是使用传统的字符串字面量。

避免复杂表达式中的内联文本块。文本块是字符串值表达式,因此,可以在任何需要字符串的地方使用,但将文本块嵌套在复杂的表达式中有时候并不好,把它放到一个单独的变量中会更好。当阅读下面的代码时,其中的文本块会打断代码流,迫使读者转换思维:

复制代码

String poem=new String(Files.readAllBytes(Paths.get("jabberwocky.txt")));String middleVerses=Pattern.compile("\n\n")                             .splitAsStream(poem)                             .match(verse -> !"""                                   ’Twas brillig, and the slithy toves                                   Did gyre and gimble in the wabe;                                   All mimsy were the borogoves,                                   And the mome raths outgrabe.                                   """.equals(verse))                             .collect(Collectors.joining("\n\n"));

如果我们把文本块放入它自己的变量中,读者就更容易理解计算流程:

复制代码

String firstLastVerse="""    ’Twas brillig, and the slithy toves    Did gyre and gimble in the wabe;    All mimsy were the borogoves,    And the mome raths outgrabe.    """;String poem=new String(Files.readAllBytes(Paths.get("jabberwocky.txt")));String middleVerses=Pattern.compile("\n\n")                             .splitAsStream(poem)                             .match(verse -> !firstLastVerse.equals(verse))                             .collect(Collectors.joining("\n\n"));

避免在文本块缩进中混合使用空格和制表符。删除附加缩进的算法会计算公共空格前缀,因此,如果混合使用空格和制表符进行缩进,该算法仍然有效。不过,这显然更容易出错,所以最好避免混合使用它们,而只使用其中的一种。
将文本块与相邻的 Java 代码对齐。由于附加空格会被自动删除,所以我们应该利用这一点使代码更易于阅读。虽然我们可能会忍不住写成下面这样:

复制代码

void printPoem() {    String poem="""’Twas brillig, and the slithy tovesDid gyre and gimble in the wabe;All mimsy were the borogoves,And the mome raths outgrabe.""";    System.out.print(poem);

因为我们不想字符串中有任何前导缩进,但大多数时候,我们应该写成下面这样:

复制代码

void printPoem() {    String poem="""        ’Twas brillig, and the slithy toves        Did gyre and gimble in the wabe;        All mimsy were the borogoves,        And the mome raths outgrabe.        """;    System.out.print(poem);}

因为这样读者更容易理解。

不是一定要将文本与开始分隔符对齐。不过,我们可以选择将文本块内容与开始分隔符对齐:

复制代码

String poem="""              ’Twas brillig, and the slithy toves              Did gyre and gimble in the wabe;              All mimsy were the borogoves,              And the mome raths outgrabe.              """;

这样看起来很漂亮,但是如果行很长,或者分隔符从距离左边距很远的地方开始,就会很麻烦,因为现在文本会一直插入到右边距。但是,这样的缩进并不是必需的;我们可以使用任何缩进方式,只要前后一致:

复制代码

String poem="""    ’Twas brillig, and the slithy toves    Did gyre and gimble in the wabe;    All mimsy were the borogoves,    And the mome raths outgrabe.    """;

当文本块中嵌入了三重引号时,只转义第一个引号。虽然可以每个引号都转义,但这没必要,并且会降低可读性;只需要转义第一个引号:

复制代码

String code="""    String source=\"""        String message="Hello, World!";        System.out.println(message);        \""";    """;

使用\分割非常长的行。除了文本块之外,我们还获得了两个新的转义序列,\s(空格字面量)和 <newline>(续行指示符)。如果文本的行非常长,可以使用 <newline> 在源代码中放一个换行符,它会在字符串编译时转义处理期间被删除。

小结

Java 程序中的字符串字面量并不局限于像“yes”和“no”这样的短字符串;它们通常对应于结构化语言(如 HTML、SQL、XML、JSON 甚至 Java)的整段“程序”。保留嵌入的那段程序的二维结构,而又不引入转义字符和其他语言上的干扰,这样程序更不容易出错,而且更易于阅读。