浅析转义字符

Posted on 2022-12-26

TL;DR

转义字符不是天然存在的,它的实现需要各个应用程序自行实现字符串转义解释器来进行解释、替换。

概述

转义字符(Escape Character)是指在ASCII码和Unicode等字符集中的无法被键盘录入的字符被当作特殊用途而需要转换回它原来的意义的字符。而转义字符转义是指字符已经被转换了意义。

一直到前段时间我都认为转义字符是公共的、统一的、规范的约定,然而当我深入的去探索后发现:转义字符不是天然存在的,它的实现需要各个引用程序自行实现字符串转义解释器来进行解释、替换。

作用

转义字符的作用是在声明的时候,通过特别的前缀把一些不可见的的字符转变成可见的、可读的字符。

比如制表符TAB是不可见字符,如果我们声明的字符串中有制表符,因为制表符和空格在显式层面的结果是一样的,都是空白,如果不使用转义字符来替换,肉眼很难区分出字符串中的空白到底是空格还是其他意义的字符。

因此,业界对于常见的不可见字符,约定了一些转义序列。通过可读可见的转义序列,我们可以区分出不可见字符,避免使用时出错。

实现

转义字符是给人使用的。通过约定的方式定义了转义序列比如\t\n后,我们在使用时可以更加便捷、可读。但是操作系统底层渲染是不认识应用层代码定义的转义字符的。如果不加处理,最终渲染输出的还是\t\n字符,则达不到我们想要的效果。

因此应用层软件需要对用户输入做特别的适配,将读取到的用户的输入中的转义字符解释、替换成真正的字符编码。

以Java中正则表达式的实现为例,我们在编写代码使用正则表达式时,其实是经过了两次解释、替换的过程,这也是Java正则表达式里面会使用很多/的原因。

public static void main(String[] args) {
    // 用于匹配\的正则表达式
    Pattern pattern = Pattern.compile("\\\\");
    // 和"\"匹配
    Matcher matcher = pattern.matcher("\\");
    System.out.println(matcher.find());
}

如上述代码示例,该段代码会在编译时,成为Java编译器javac的输入。javac在处理用户输入、解析、编译源码时,本身会处理一次转义字符,主要是处理由""确定边界的字符串字面量。
javac会匹配、替换由/开头的转义字符。详见javac源码

经过javac解释替换后,字节码中存在的两个字符串的值分别为:\\\

转义字符的由支持转义字符的软件自行匹配、替换。替换的结果体现在输出中。javac的输出就是字节码文件。

javac处理完一次转义字符后,将处理后的结果固定在字节码中。后续代码运行时,正则表达式编译器会再次进行一次转义字符的替换。

上述示例中的Pattern.compile("\\\\")在运行中实际跑的应该是Pattern.compile("\\")

同理,正则编译器也会进行转义字符的处理。

java.util.regex.Pattern#escape

img

img

处理后,\\被映射成内存中的、字符集中包含的\以便进行后续的匹配。

案例

同样的处理过程在很多地方都有体现。

Properties解析

java.util.Properties#loadConvert

img

img

如图,Properties文件中的转义字符,也是由Properties文件解析程序自行处理的。

源码中的\t、\n等字符会被javac替换成正式的单字符。这是最容易迷惑的地方,希望读者自行好好体会。

Json解析

com.fasterxml.jackson.core.json.ReaderBasedJsonParser#_decodeEscaped

img

jackson在解析json的时候,也会自行处理转义字符。

echo程序

echo是linux上常用的输出命令,用于输出指定的字符串。

img

在使用时,可以通过-E-e参数提示echo命令是否需要解释字符串参数中的转义字符。

img

如上图,echo程序默认会解释、替换参数中的转义字符,但是可以通过参数屏蔽转义字符。屏蔽转义字符的核心其实就是不遍历、替换参数中的转义序列。

这个示例就是想说明:转义字符的解释、替换是应用程序级别的代码逻辑,可以由引用程序自行控制是否解释转义字符、解释哪些转义字符。

总结

  1. 如果一个应用程序的输入是字符串,那么从易用性的角度考虑,它应该要提供转义字符解释、替换的功能。
  2. 但是我们需要理解:转义字符解释、替换的能力不是操作系统提供的,是应用程序自行解析、替换的。
  3. 每个应用程序支持的转义字符数量、格式都可能不一样。但是出于惯例,大家一般都是用\前缀来标识转义字符,一般都会支持基础的\n\t等。这是惯性,是程序员心智;而不是因为\在客观上有什么特别的优势。
  4. Java领域常见的以字符串为输入的应用程序,在处理输入时,都自行解释、处理了转义字符。
  5. 最重要的一点:javac(java源码编译器)也是处理字符串输入的应用程序,因此源码中也可以使用转义字符。
    当我们用这种心智来理解Java正则表达式中的那么多\时,整体感受就不会那么别扭了。
  6. 为什么javascript的正则表达式写起来不需要那么多\呢?
    这是因为首先javascript没有编译的过程,然后是因为javascript中,正则是一等公民,有特殊的语法,和普通的字符串字面量没有共用声明语法。