Carbon 语言字符串字面量完全指南:简单、块与原始字符串的语法、转义与词法实现解析
2026/9/11 6:31:20 网站建设 项目流程

Carbon 语言字符串字面量完全指南:简单、块与原始字符串的语法、转义与词法实现解析

【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang

本文以 docs/design/lexical_conventions/string_literals.md 为骨架,结合 Carbon 词法分析器源码与词法测试数据,系统讲解 Carbon 字符串字面量的三种形态(单引号包围的简单字符串、'''包围的块字符串、以#定界的原始字符串)、完整的转义序列集合、UTF-8 编码约定,以及编译器在词法阶段的实际处理逻辑。读完本文,你将能够准确书写各类 Carbon 字符串字面量,理解块字符串缩进剥离、文件类型指示符与原始字符串哈希定界的工作机制,并掌握词法层面的错误诊断行为。

概述:Carbon 字符串字面量的三种形态

Carbon 同时支持单行与多行两类字符串字面量,并各自拥有"普通"与"原始(raw)"两种形态:

  • 简单字符串字面量(simple string literal):使用一个双引号"定界,单行书写。
  • 块字符串字面量(block string literal):使用三个单引号'''定界,可跨多行;在起始'''之后还可以附带可选的文件类型指示符(file type indicator),该指示符不影响字符串本身的内容,仅供外部工具(如语法高亮器、格式化器)识别内容结构。
  • 原始字符串字面量(raw string literal):通过为定界符添加一个或多个#前缀来定制定界方式,便于在字符串中直接书写字面意义的\"。简单与块字符串均有原始形态。

以下是一个三种形态的直观对比示例(摘自设计文档):

// 简单字符串字面量: var simple: String = "example"; // 块字符串字面量: var block: String = ''' The winds grow high; so do your stomachs, lords. How irksome is this music to my heart! When such strings jar, what hope of harmony? I pray, my lords, let me compound this strife. -- History of Henry VI, Part II, Act II, Scene 1, W. Shakespeare '''; // 带文件类型指示符的块字符串字面量: var code_block: String = '''cpp #include <iostream> int main() { std::cout << "Hello world!"; return 0; } '''

块字符串字面量会将终止行(closing line)的前导缩进从所有前导行中移除。因此在上面的code_block示例中,最终字符串里只有std::coutreturn两行各保留了 4 个空格的缩进。

转义序列由反斜杠\引入,用于表达特殊字符或代码单元序列,例如\n表示换行。原始字符串字面量则额外以若干#定界:其中的转义序列需要\后跟等量的#才会被识别。例如:

// 含换行转义序列的简单字符串: var newline: String = "line one\nline two"; // 字面意义的 `\n`(两个字符),不是换行: var raw: String = #"line one\nstill line one"#; // 原始字符串中的换行转义序列(\ 后跟一个 #): var raw_newline: String = #"line one\#nline two"#;

简单字符串字面量

一个简单字符串字面量由以下序列构成,并被"包围:

  • \"之外的字符。
    • 字符串字面量中合法的空白只有空格字符(U+0020)
    • 其他水平空白(包括制表符 tab)都是不允许的;但出于错误恢复(error recovery)的目的,它们仍会作为字符串的一部分被解析。
    • 垂直空白(如换行)不会被解析为简单字符串字面量的一部分。
  • 转义序列。每个转义序列都会被替换为对应的字符序列或代码单元序列。
    • 与非法空白类似,\z这类非法转义序列也会作为字符串的一部分被解析(用于错误恢复)。

例如:

var String: lucius = "The strings, my lord, are false.";

相邻字符串字面量是被禁止的

Carbon 与 C/C++ 不同,不会将相邻的字符串字面量隐式拼接。例如下面的写法是无效的:

// 三个相邻的简单字符串字面量 `""`、`"abc"` 与 `""` 是无效的。 var String: block = """abc""";

以三个双引号"""开头的字符串字面量属于"相邻字符串字面量",必须被拒绝并给出诊断。从词法实现看,toolchain/lex/string_literal.cpp中的Introducer::Lex会识别以"""开头的输入并标记为MultiLineWithDoubleQuotesKind枚举之一,见 string_literal.h),随后在计算字符串值时发出错误诊断:

use'''delimiters for a multi-line string literal, not"""``(多行字符串字面量应使用'''定界符,而不是"""

对应实现见 string_literal.cpp 与词法测试 toolchain/lex/testdata/multiline_string_literals.carbon 中的fail_quotes用例。

块字符串字面量

块字符串字面量'''开始。同一行'''之后、换行之前的文本是可选的文件类型指示符。字面量在下一个"其首个'不属于\'转义序列"的三单引号处结束。需要注意:

  • 闭合的'''必须是该行第一个非空白字符
  • 起始行与闭合行之间的各行称为内容行(content lines)
  • 内容行中不允许出现不构成转义序列组成部分的\字符。

缩进剥离规则

块字符串字面量的**缩进(indentation)**定义为闭合'''之前的那段水平空白。每条非空内容行必须以该缩进开头。字面量的内容按如下方式形成:

  1. 将闭合行的缩进从每条非空内容行中移除。
  2. 每一行末尾的所有尾随空白(包括行终止符)都被替换为一个换行符(U+000A)。
  3. 将得到的各行拼接起来。
  4. 每个转义序列被替换为对应的字符序列或代码单元序列。

其中,"空内容行"指仅包含空白字符的行。

// 所有块字符串字面量默认都以一个换行结尾。 var String: newline_example = ''' This is a block string literal. Its first character is 'T' and its last character is a newline. It contains another newline character between 'is' and 'a'. '''; // 可以用转义字符 '\' 抑制换行。 var String: suppressed_newlines = ''' This is another block string literal. The newline character here \ is suppressed, along with the trailing newline here.\ '''; // 下面的块字符串字面量是无效的,因为 'closing' 后的 ''' 虽然终止了 // 字面量,却不在行首。 var String: invalid = ''' error: closing ''' is not on its own line. ''';

实现层面,缩进检测与剥离由 string_literal.cpp 中的ExpandEscapeSequencesAndRemoveIndent完成:它对每一行尝试contents.consume_front(indent);若某行的缩进与闭合行不一致,会发出MismatchedIndentInString诊断("indentation does not match that of the closing'''in multi-line string literal"),对应测试用例见 multiline_string_literals.carbon 中的fail_indent_mismatch。同时,lex.cpp 会更新行信息:块字符串字面量所跨越的每一行,其缩进列都被设为闭合定界符的列宽,以保证后续令牌(如尾随注释)的行列定位正确。

文件类型指示符与尾随注释

文件类型指示符'''之后、引入行上的文本,去除两侧空白后得到;它不得包含'#"。在文件类型指示符之后可以跟一个尾随注释:它是一条普通注释,工具将其视作普通注释处理,不属于指示符本身,因此可以包含指示符所不允许的字符(如'#")。

文件类型指示符对 Carbon 编译器没有语义意义,但部分指示符会被语言工具链(如语法高亮器、代码格式化器)理解,用于识别字符串内容的结构。例如:

// 这是一个块字符串字面量。其前两个字符是空格,最后一个字符是换行。 // 它的文件类型是 'c++'。 var String: starts_with_whitespace = '''c++ int x = 1; // This line starts with two spaces. int y = 2; // This line starts with two spaces. ''';

文件类型指示符还可以携带文件类型之外的语义信息,例如向代码格式化器发出"对这段代码块禁用格式化"的指令。

开放问题:目前还没有一个具体的、被公认的文件类型指示符集合。设计文档指出,正式地给出一个"知名指示符"集合将有助于工具对指示符含义达成共识(例如写在最佳实践指南中)。

词法实现印证:Introducer::Lex在解析引入行时会查找//并剥离开头的尾随注释(要求//后跟空白或行尾,见 string_literal.cpp),再对剩余文本去除空白并检查是否含'#\"三者之一。该行为有专门的测试用例introducer_trailing_comment(见 multiline_string_literals.carbon)。

'''与字符字面量的消歧

由于字符字面量永不空,''只能作为'''(块字符串字面量的起始或结束)的开头出现。形如'''foo'''的单行写法是错误,绝不会被解释为一串字符字面量。词法器会对这种"引入行形式不合法"的情况返回一个覆盖整个引入行的无效字面量,并发出MultiLineStringInvalidIntroducer诊断("invalid multi-line string literal introducer; a file type indicator may not contain',#, or", and the content must begin on a new line"),见 lex.cpp。

转义序列详解

在字符串字面量内部,以下转义序列是被识别的:

转义序列含义
\tU+0009 CHARACTER TABULATION(水平制表)
\nU+000A LINE FEED(换行)
\rU+000D CARRIAGE RETURN(回车)
\"U+0022 QUOTATION MARK("
\'U+0027 APOSTROPHE('
\\U+005C REVERSE SOLIDUS(\
\0值为 0 的代码单元
\0D非法,为将来演进保留
\xHH值为 HH₁₆ 的代码单元
\u{HHHH...}Unicode 码点 U+HHHH...
\<newline>不产生任何字符串内容(仅限块字符串字面量)

十六进制字符(H必须大写\xAA合法,\xaa非法)。

与 C++ 转义序列的差异

这套转义序列覆盖了除以下情形之外的所有 C++ 转义序列:

  • \?:历史上用于在字符串字面量中转义三字符组(trigraphs),现已无用途,故不支持。
  • \ooo八进制转义:被移除,因为 Carbon 不支持八进制字面量;\0作为特例保留,预期对 C 互操作很重要。
  • \uABCD:被\u{ABCD}取代。
  • \U0010FFFF:被\u{10FFFF}取代。
  • \a(响铃)、\b(退格)、\v(垂直制表)、\f(换页):\a\b已过时,\f\v基本废弃。如有需要,可以分别用\x07\x08\x0B\x0C表达。

值得注意的是,这套转义序列与 Swift 和 Rust 支持的集合相同,唯一的区别是 Carbon 额外支持\xHH(Swift 不支持)。

八进制转义预计将长期不被允许(即便\0D被保留),而不支持\1..\7、更一般地不支持\DDDD这一决定目前标记为实验性(experimental)。

\x\u的数字规则

  • 上表中H代表任意十六进制字符:0-9A-F(区分大小写)。
  • 与 C++ 不同、与 Python 相同,\x恰好要求两个十六进制数字。
  • 与 JavaScript、Rust、Swift 相同,Unicode 码点可以用\u{10FFFF}记法按数字表达;它接受1 到 8 个十六进制字符(这一限制由提案 p002040-unicode-escape-code-length.md 确立,理由是对齐 Swift 的 8 位限制、简化解析器,并避免\u{000...000E9}这类可写任意前导零的混乱情况)。
  • 可表达的码点数值范围为 0₁₆–D7FF₁₆ 与 E000₁₆–10FFFF₁₆(即排除了代理区 D800–DFFF)。

开放问题:部分语言(尤其是 Python)支持\N{unicode character name}语法。Carbon 未来可能增加该转义序列;设计文档提醒,相关提案应关注 C++ Unicode 研究小组在此领域的工作。

\0与后续十进制数字

\0转义序列后面不得紧跟十进制数字。当需要在空字节后跟一个十进制数字时,应改用\x00:例如"foo\x00123"。这样做的意图是为将来允许十进制转义序列保留可能性。词法实现会在\0后紧跟十进制数字时发出DecimalEscapeSequence诊断,提示改用\x00(见 string_literal.cpp),测试用例fail_decimal_escape.carbon(输入"\01")验证了这一点:

error: decimal digit follows `\0` escape sequence. Use `\x00` instead of `\0` if the next character is a digit [DecimalEscapeSequence]

转义换行:\<newline>

反斜杠后跟换行符是一种不产生任何字符串内容的转义序列。该特性是实验性的,且只能出现在块字符串字面量中。处理时机在"尾随空白被替换为换行符"之后,因此"\+ 水平空白 + 行终止符"会移除直到并包括该行终止符在内的空白。与 Rust 不同、与 Swift 相同:被转义换行之后那一行的前导空白不会被移除,除非这些空白与终止'''的缩进匹配。

非法转义与空白限制

  • 以反斜杠开头、但不匹配任何已知转义序列的字符序列是非法的。词法器会发出UnknownEscapeSequence诊断(例如\w→ "unrecognized escape sequencew"),并在错误恢复时将该转义"展开为自身"(即去掉引入符,\qq),见 string_literal.cpp。
  • 除空格(以及块字符串字面量中可选的"回车+换行")之外的其他空白字符均被禁止。具体而言,词法器在检测到 tab 或其它水平空白时会发出InvalidHorizontalWhitespaceInString诊断:"whitespace other than plain space must be expressed with an escape sequence in a string literal"(见 string_literal.cpp),测试用例fail_literal_tab_in_string.carbon验证了这一点。
  • 其余所有字符(包括不可打印字符)原样保留
  • 由于所有 Carbon 源文件都必须是合法的 Unicode 字符序列,非合法 UTF-8 的代码单元序列只能通过\x转义序列产生

不允许字符串字面量中出现字面 tab 字符这一决定同样是实验性的。

以下示例综合展示了上述规则:

var String: fret = "I would 'twere something that would fret the string,\n" + "The master-cord on's \u{2764}\u{FE0F}!"; // 该字符串包含两个字符(在编码为 UTF-8 之前): // U+1F3F9 (BOW AND ARROW) 后跟 U+0032 (DIGIT TWO) var String: password = "\u{1F3F9}2"; // 该字符串不包含任何换行字符。 var String: type_mismatch = ''' Shall I compare thee to a summer's day? Thou art \ more lovely and more temperate.\ '''; var String: trailing_whitespace = ''' This line ends in a space followed by a newline. \n\ This line starts with four spaces. ''';

词法层的转义校验与诊断汇总

从源码实现看,\x\u的处理位于ExpandAndConsumeEscapeSequence(string_literal.cpp):

  • \x要求紧跟两个大写十六进制数字,否则发出HexadecimalEscapeMissingDigits(错误信息示例:"escape sequence\xmust be followed by two uppercase hexadecimal digits, for example\x0F")。
  • \u要求形如\u{HHHH...},否则发出UnicodeEscapeMissingBracedDigits
  • \u内的数值超过 0x10FFFF 时发出UnicodeEscapeTooLarge;落在代理区(0xD800–0xDFFF)时发出UnicodeEscapeSurrogate

这些诊断均有对应的词法测试,见 toolchain/lex/testdata/string_literals.carbon(fail_unknown_escapefail_unicode_escape_missing_braced_digitsfail_hex_escape_missing_digitsfail_unicode_surrogatefail_unicode_length等用例)。

原始字符串字面量

为了让字符串内容可以自由包含\",字符串字面量的定界符可以通过在起始定界符前加N#字符来定制。规则如下:

  • 只有在后跟N#时,才被识别为闭合定界符。
  • 同理,只有\后跟N#时,才被识别为转义序列。
  • 未后跟N#\"'''没有特殊含义
起始定界符转义序列引入符闭合定界符
"/'''\(例如\n"/'''
#"/#'''\#(例如\#n"#/'''#
##"/##'''\##(例如\##n"##/'''##
###"/###'''\###(例如\###n"###/'''###
.........

示例:

var String: x = #''' This is the content of the string. The 'T' is the first character of the string. ''' <-- This is not the end of the string. '''#; // 但上一行确实结束了字符串。 // OK,最后一个字符是 \ var String: y = #"Hello\"#; var String: z = ##"Raw strings #"nesting"#"##; var String: w = #"Tab is expressed as \t. Example: '\#t'"#;

从上例可见:原始字符串可以逐级嵌套(#"..."###"..."#####"..."###),且可以通过\#n这类写法在原始字符串中按需使用转义序列。该机制源于 Swift 的 raw strings 设计,被提案 p000199-string-literals.md 采纳——普通字符串可视为"N = 0 个#"的特例,从而"普通/原始"与"单行/多行"两对正交特性可以自由组合出全部 4 种字面量形式。

在词法实现中,StringLiteral::Lex会先统计开头的#数量作为hash_level(string_literal.cpp),随后据此构造带等量#的终止符与转义引入符(string_literal.cpp),并在扫描内容时仅匹配带#的闭合符与转义序列(string_literal.cpp)。

编码

一个字符串字面量最终产生一串 8 位字节(byte)。与 Carbon 源文件一样,字符串字面量以UTF-8编码。然而,由于可以通过\xHH转义插入任意字节序列,无法保证字符串一定是合法的 UTF-8

这一决定是实验性的:如果未来出现直接在字符串字面量中表达其他编码的充分动机,应当重新评估。同样地,随着库对字符串类型的支持不断演进,应当考虑引入一种(或许作为默认的)保证字符串内容为合法 UTF-8 编码的字符串字面量语法,以便在类型系统中区分合法 UTF-8 与任意字符串;对于此类字面量,还应考虑像 Rust 那样拒绝\xHH中 HH 大于 7F₁₆ 的转义。

编译器实现视角:词法阶段如何识别字符串字面量

在 Carbon 工具链中,字符串字面量的词法分析集中在 toolchain/lex/string_literal.h 与 toolchain/lex/string_literal.cpp 两个文件中,核心类为StringLiteral

  • Kind枚举区分四种形态(string_literal.h):Char(字符字面量,仍经由字符串字面量词法路径处理)、SingleLine"<content>")、MultiLine'''<content>''')、MultiLineWithDoubleQuotes(错误的"""<content>"""写法,用于错误恢复)。
  • StringLiteral::Lex(string_literal.cpp)负责从源码文本中提取字面量:先统计#前缀得到hash_level,再通过Introducer::Lex解析引入符,最后用查找表快速扫描"有趣字符"(\、换行、引号、tab)以定位终止符与转义序列。返回std::nullopt表示未发现字面量;返回未终止(is_terminated_ == false)的字面量则表示"发现了前缀但形式有误",用于辅助构造错误。
  • ComputeStringValue(string_literal.cpp)在BumpPtrAllocator上展开转义序列并剥离缩进;当内容无需校验且无缩进时可直接返回原始内容引用。展开过程保证只会缩短内容(CARBON_CHECK(result.size() <= content_.size())),因此无需重分配。
  • 调用入口Lexer::LexStringLiteral位于 lex.cpp:字符字面量由ComputeCharLiteralValue计算码点值,字符串字面量则把计算出的值存入string_literal_values存储并生成StringLiteral令牌;遇到未终止字面量时发出UnterminatedString诊断。

用词法测试验证行为

上述诊断与行为均由基于文件测试(file test)框架的测试数据锁定,可通过以下命令单独运行(以简单字符串为例,见 string_literals.carbon 文件头部的 TIP):

bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/lex/testdata/string_literals.carbon

或转储输出:

bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/lex/testdata/string_literals.carbon

多行字符串的对应测试位于 multiline_string_literals.carbon,覆盖缩进不匹配(fail_indent_mismatch)、"""误用(fail_quotes)以及引入行尾随注释(introducer_trailing_comment)等场景。

设计演进与相关提案

字符串字面量的设计演进可以沿以下提案追溯:

  • 基础设计:提案 p000199-string-literals.md 确立了简单/块字符串、转义序列集合、原始字符串与编码规则,其"Alternatives considered"部分详细记录了块字符串的前导空白移除与终止换行取舍、\x位数、\u{NNNN}记法(而非保留\uNNNN/\U00NNNNNN)、基于 Swift 而非 Rust 的原始字符串机制(#定界而非r前缀)以及内部空白(拒绝字面 tab)等设计决策。
  • Unicode 转义长度:提案 p002040-unicode-escape-code-length.md 将\u{...}的位数限定为 1 到 8,以简化解析器并尽早失败于明显非法的输入。
  • 尾随注释:文件类型指示符后可跟尾随注释的规则与提案 p007441-trailing-comments.md 相关。
  • 字符字面量:字符串字面量与字符字面量共享同一套转义序列(字符字面量中\xHH被限制为 ≤0x7F以避免字节值与码点的歧义),且''永不构成字符字面量、只作为'''的开头出现,从而保证'''不会被误读。

【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询