news 2026/9/10 21:51:15

Carbon 字符串字面量全面解析:转义序列、块字符串、原始字符串与编码语义

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Carbon 字符串字面量全面解析:转义序列、块字符串、原始字符串与编码语义

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

导读

本文以 Carbon Language 的字符串字面量提案为核心,结合当前仓库中的词法规范与 lexer 源码实现,系统梳理 Carbon 字符串字面量的完整语法体系:包括单行字符串与块字符串、完整的转义序列表、以#定制的原始字符串、缩进剥离规则以及 UTF-8 编码语义。读完本文,你将掌握 Carbon 中四种字符串形态(普通/原始 × 单行/多行)的写法与取舍,理解其与 C++、Rust、Swift、Python 的异同,并能从源码与测试用例层面验证每条规则的实际行为。

注意:本文所述语法以当前仓库实际实现为准。原始提案 p000199 曾提议以"""作为块字符串分隔符,但该语法随后被提案 p001360 变更,当前实现采用'''(三个单引号)作为块字符串定界符,详见下文"语法演进"一节。

问题与背景:为什么要专门定义字符串字面量

提案的出发点很朴素:为"人类可读文本"提供一种书写语法。这里的"文本"含义宽泛——正则表达式、程序源码、C++ mangled name 等最终要交给计算机解释的内容同样属于"文本",它与任意二进制数据的区别在于:文本是由字符序列构成的,通常需要经过某种编码(encoding)才能存储与传输。编码是"文本中的字符序列"与"有界整数值序列(即码元 code units)"之间的双向映射。例如俄语单词 углерод(carbon)在 UTF-8 编码下对应D1 83 D0 B3 D0 BB D0 B5 D1 80 D0 BE D0 B4这一组字节序列。

在考察各语言既有实践时,提案总结了字符串字面量常见的三类扩展能力:

  • 转义序列(escape sequences):让字符串能够包含难以输入、对读者有歧义或会导致渲染问题的字符(空白、不可见字符、改变后续字符渲染的字符),也能包含任意码元。C++ 与 Python 支持\a\b\f\n\r\t\v;JavaScript 去掉\a;Java 再去掉\v;Rust 与 Swift 只保留\n\r\t
  • 原始字符串(raw string literals):不识别转义序列,适合嵌入与宿主语言共享转义约定的另一种机器可读语言。Python 用r前缀,C++ 用r"DELIM(...)DELIM"自定义定界符,Rust 用r#...#,Swift 用#"..."#且以\#引入转义,Java 则用反引号定界。
  • 多行字符串(multiline string literals):C++/Rust/Java 用原始字符串兼作多行;Python 用"""''';Swift 用"""但内容不能与定界符同行;JavaScript 用反引号。

至于字符串插值(interpolation),例如"Hello, $planet.",明确不在本提案范围内

提案核心:四个正交的字符串形态

提案的设计要点可以概括为:

  1. 单行字符串由一对"定界:"hello"
  2. 多行(块)字符串由'''引入并换行,以一行开头的'''结束,结束行的缩进会从前面所有行中剥离(当前实现的定界符;提案原文为""");
  3. '''之后可跟随文件类型指示符(file type indicator),它只辅助工具理解字符串意图,不影响程序语义;
  4. 转义序列以\引入,支持最常见的 C/C++ 转义,移除八进制转义(\177)、\a\b\f\v,并把\uNNNN/\U00NNNNNN统一为\u{NNNNNN}形式;块字符串中还允许\<newline>转义(不产生任何内容);
  5. 原始字符串遵循 Swift 惯例:开定界符前缀一个或多个#,闭定界符后缀相同数量的#,如#"foo\s*bar"##"foo"bar"#;在原始字符串中,\后跟匹配数量的#可重新引入转义,例如#"foo\#nbar"#包含一个换行符;
  6. 与 C/C++ 不同,相邻字符串字面量不会隐式拼接。

"普通/原始"与"单行/多行"两个维度正交,组合出全部四种形态,这正是设计上刻意追求的结果——参见提案的 Rationale。

语法演进:从"""'''

历史提案 p000199 原文使用"""作为块字符串定界符。但后续实践发现,[#]*"""会被用户想当然地认为是块字符串,实则存在歧义:例如"""abc"""会被解析为三个相邻字面量"""abc""",而#"""#等价于"\""。为此提案 p001360 将语法明确为:[#]*"表示单行字符串,[#]*'''表示块字符串,同时禁止相邻字符串字面量"""abc"""因此成为非法写法)。当前词法实现中,'''是唯一合法的多行引入符,而"""会被 lexer 以MultiLineWithDoubleQuotes形态识别并报错:

error: use `'''` delimiters for a multi-line string literal, not `"""`

该诊断的发射点在 toolchain/lex/string_literal.cpp,并由 toolchain/lex/testdata/multiline_string_literals.carbon 中的fail_quotes.carbon用例验证。

简单字符串字面量(非原始、单行)

简单字符串字面量由以下序列构成,并整体用"括起来:

  • 除反斜杠、双引号和垂直空白以外的字符;
  • 转义序列。

每个转义序列都会被替换为对应的字符序列或码元序列:

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

另外有几个重要约束:

  • 字符串内只允许**空格(U+0020)**这种空白;制表符等其他水平空白被禁止,但会"解析进"字符串以支持错误恢复;
  • 垂直空白不会成为单行字符串的一部分(单行字符串不能跨行);
  • 无效转义(如\z)同样为错误恢复目的解析进字符串;
  • 相邻字符串字面量被禁止——"""abc"""会因三个相邻字面量"""abc"""而被拒绝并诊断。

这些规则在 toolchain/lex/testdata/string_literals.carbon 中有大量对应用例:fail_unknown_escape.carbon"\w"unrecognized escape sequence)、fail_literal_tab_in_string.carbon"<TAB>"whitespace other than plain space must be expressed with an escape sequence)、fail_unterminated.carbon等。

转义序列

下表是 Carbon 字符串字面量中完整可识别的转义序列:

转义含义
\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 的码元
\xHH值为 HH16的码元
\u{HHHH...}Unicode 码点 U+HHHH...
\<换行>不产生任何字符串内容(仅限块字符串)

需要注意的细节:

  • 十六进制字符必须大写H只能是0-9A-F(大小写敏感),即\xAA合法而\xaa不合法;\x必须且只能跟恰好两位十六进制数字(与 Python 一致,与 C++ 的任意长度不同)。源码中HexadecimalEscapeMissingDigits诊断正是据此触发,见 string_literal.cpp。
  • Unicode 码点用\u{...}大括号形式(同 JavaScript/Rust/Swift),接受 1 到 8 个十六进制字符,可表达 016-D7FF16与 E00016-10FFFF16范围内的任意码点(即排除代理区)。超出0x10FFFF或落入代理区的码点会分别触发UnicodeEscapeTooLarge/UnicodeEscapeSurrogate错误,见 string_literal.cpp 及 string_literals.carbon 的测试。
  • \0不得紧跟十进制数字;若需要在空字节后写数字,请用\x00"foo\x00123"\0跟数字会触发DecimalEscapeSequence诊断(string_literal.cpp),保留未来引入十进制转义的可能。

与 C++ 相比移除了哪些转义\?(历史上用于 trigraph,已无用途);\ooo八进制转义(Carbon 不支持八进制字面量,但保留\0特例以服务 C 互操作);\uABCD\U0010FFFF(统一改为\u{...}形式);\a(响铃)、\b(退格)、\v(垂直制表)、\f(换页)。需要这些字符时可分别用\x07\x08\x0B\x0C表达。整体上,Carbon 的转义集合与 Swift、Rust 一致,额外多出 Swift 所没有的\xHH

\<换行>转义:反斜杠后跟换行符(LF)是一个产生"无内容"的转义,属于实验性特性,只能出现在块字符串中。它按"先统一换行再处理转义"的顺序执行:\后跟水平空白再跟行终止符时,会删除该空白直到并包括行终止符。与 Rust 不同、与 Swift 类似:被转义换行的下一行前导空白不会被额外删除(仅删除与终止'''缩进匹配的部分)。示例如下:

// 该字符串不包含任何换行符。 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. ''';

空白与无效转义的处理:以反斜杠开头但不匹配任何已知转义的字符序列是无效的。除空格(以及块字符串中的换行、可选的 CR+LF)外,其他空白字符一律禁止。其余所有字符(包括不可打印字符)原样保留。由于 Carbon 源文件必须是合法 Unicode 字符序列,非法 UTF-8 码元序列只能通过\x转义产生

块字符串字面量(非原始、多行)

块字符串字面量以'''开头,随后是可选的文件类型指示符,再跟一个换行;它在下一个"第一个'不属于\'转义"的'''处结束。关闭的'''必须是该行的首个非空白字符。开行与闭行之间(不含两端)的各行称为内容行(content lines),内容行中不得出现不属于转义序列的\字符。

缩进剥离规则是块字符串最核心的语义:块字符串的缩进定义为闭行'''之前的水平空白序列。每个非空内容行都必须以该缩进开头,然后按如下步骤构造内容:

  1. 从每个非空内容行移除闭行缩进;
  2. 每行末尾的所有尾部空白(包括行终止符)替换为单个换行符(U+000A);
  3. 将处理后的各行拼接;
  4. 将每个转义序列替换为对应的字符序列或码元序列。

只含空白字符的内容行视为空行。因此块字符串默认带尾部换行(开头的换行不属于内容),第一个字符通常是首个内容行的首个非缩进字符:

var String: w = ''' This is a string literal. Its first character is 'T' and its last character is a newline character. It contains another newline between 'is' and 'a'. '''; // 该字符串字面量非法:'closing' 后的 ''' 终止了字面量,但它不在行首。 var String: invalid = ''' error: closing ''' is not on its own line. ''';

缩进不匹配同样会被诊断。lexer 的CheckIndent会检查闭行后是否还有内容(ContentBeforeStringTerminator),ExpandEscapeSequencesAndRemoveIndent则逐行验证缩进一致性,不匹配时报MismatchedIndentInString(string_literal.cpp 与 multiline_string_literals.carbon 的fail_indent_mismatch.carbon用例)。

文件类型指示符(File Type Indicator)

文件类型指示符是引入行'''之后、去除周边空白后的文本,它不得包含'#";其后的//尾部注释属于普通注释,不进入指示符。指示符对 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. ''';

指示符甚至可以携带超出文件类型本身的语义信息,例如指示代码格式化器对该代码块禁用格式化。提案留下的开放问题是:目前没有一套公认的指示符集合,未来最好在最佳实践指南中非正式地规定一组常见指示符,使各工具对指示符含义有共同理解(string_literal.cpp 的Introducer::Lex实现了指示符的裁剪与校验)。

原始字符串字面量

当字符串内容大量包含\"时,可以通过给定界符加前缀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. '''#; // But the preceding line does end the string. // OK, final character is \ var String: y = #"Hello\"#; var String: z = ##"Raw strings #"nesting"#"##; var String: w = #"Tab is expressed as \t. Example: '\#t'"#;

#"""的歧义消解:注意"单行原始字符串"与"多行原始字符串"都可以以#'''开头,两者的区分依据是同一行后面是否还有更多":若同一行稍后存在"加一个或多个#来终止,则是单行原始字符串;否则该行其余部分是文件类型指示符(不能含"#)。当前语法下('''为块字符串),原始字符串词法完全由hash_level(开定界符前的#数量)驱动,见 string_literal.h。

原始字符串的一个重要优势是不损失任何功能:因为\#n这种形式在原始字符串中依然可以引入转义,维护中需要"在原始字符串里嵌入转义内容"时无需退回普通字符串。例如在原始块字符串中保留行尾空白,可以用更啰嗦的\#n\#终止符实现。

编码:UTF-8 与 8 位字节

字符串字面量求值结果是一串8 位字节序列。与 Carbon 源文件一样,字符串字面量以 UTF-8 编码,且本提案不提供请求其他编码的机制——如果确实需要其他编码,期望在编译期间从 UTF-8 转码。但这并不意味着字符串保证是合法 UTF-8:由于\xHH可以插入任意字节,字符串内容可能是任意字节序列。

这一决策目前是实验性的:如果出现直接以其他编码表达字符串字面量的充分动机,应重新审视。同时,随着库对字符串类型支持的演进,未来应考虑提供(也许作为默认)能保证字符串内容为合法 UTF-8 的字面量语法,让类型系统能够区分"合法 UTF-8"与"任意字符串";在这种字面量中,可考虑像 Rust 一样拒绝 HH 大于 7F16\xHH转义。

源码实现佐证

  • 词法入口与形态枚举:toolchain/lex/string_literal.h 定义了StringLiteral::KindChar(字符字面量也经由字符串词法处理)、SingleLineMultiLineMultiLineWithDoubleQuotes(错误恢复用)。hash_level_记录#数量,content_needs_validation_标记是否需要转义展开(有转义或制表符时为真)。
  • 转义展开与缩进剥离:toolchain/lex/string_literal.cpp 的ExpandEscapeSequencesAndRemoveIndent是核心实现:逐行消费缩进、追加普通文本、处理换行与转义;它同时实现了"行尾空白替换为单个换行"与\<换行>转义逻辑。
  • Unicode 展开:string_literal.cpp 的ExpandUnicodeEscapeSequence校验码点范围(拒绝 >0x10FFFF 与代理区),再用 LLVM 的ConvertUTF32toUTF8转成 UTF-8 字节。
  • 测试验证:toolchain/lex/testdata/string_literals.carbon 覆盖了无效转义、缺失十六进制数字、\0后接数字、字符串内裸制表符、Unicode 代理区与越界码点等错误路径;toolchain/lex/testdata/multiline_string_literals.carbon 覆盖缩进不匹配、"""误用与引入行尾部注释。
  • 类型层面:字符串字面量的类型为Core.String,其前导类型定义位于 core/prelude/types/string.carbon,由ptr: Char*size: i64构成;toolchain/check/testdata/primitives/string_literals.carbon 展示了字面量如何被转换为string_literal指令及长度/超长诊断。

备选方案与设计取舍

提案对若干关键决策进行了充分的替代方案比较,理解这些取舍有助于把握语法设计动机:

  • 块字符串的替代:可以完全不做块字符串、用拼接构造多行文本,但那样更啰嗦且源码表达偏离程序员意图;也不宜像 C++ 那样用原始字符串兼作多行,因为那会把"是否识别转义"与"是否跨行"两个正交维度耦合。例如 C++ 的 Makefile 规则字符串:

    std::string make_rule = "%s: %s\n\t$(CC) -c -o $@ $< $(CFLAGS)\n\n" "main:\n\t$(CC) %s -o %s\n";

    在 Carbon 中可以写成可读得多且制表符可见的块字符串:

    var String: make_rule = '''make %s: %s \t$(CC) -c -o $@ $< $(CFLAGS) main: \t$(CC) %s -o %s ''';
  • 前导空白剥离方式:曾考虑用显式字符(如|)标记要剥离的缩进量,这样在见到开'''后的第一行时即可确定缩进,但会增加词法复杂度并损害"把字符串内容拷贝到其他上下文"的能力,因此未采纳。

  • 尾部换行的取舍:Swift 不包含尾部换行,而 Carbon 选择包含尾部换行、不包含开头换行。这样在用多个块字符串拼接构造一个大字符串时,"每行都以换行结尾"的规则能让源码层级与结果字符串对齐最佳——示例可见原提案中分段打印 C++ 类的Run()函数。

  • 转义序列的取舍:不支持八进制转义(与不支持八进制数字字面量保持一致,且调查显示许多 C++ 程序员不知道\123是八进制);\x固定两位十六进制(C++ 的任意长度需要显式终止机制,属"不必要的发明");不引入 Python 的\N{unicode character name};曾讨论过\e(U+001C ESCAPE,常用于硬编码 ANSI 终端序列如"\e[32mgreen text\e[0m"),提案未明确拒绝,但认为未来一个终端操作库设施可能比该转义更有价值;\u{...}采用 Swift/Rust 的单一大括号形式而非 JavaScript 的\uNNNN短形式,因为预期常规可打印且 NFC 归一化的 Unicode 字符会直接写在源码里,显式\u转义主要用于测试数据或方向性标记等特殊字符。

  • 原始字符串的取舍:采用 Swift 式#方案而非 C++ 式自定义定界符(嵌套一两层时#"..."###"..."##足够,无需程序员做任意选择);在@#$)]}\.等可用字符中选#\与转义冲突、闭括号可能用于未来语法、.太像设计符);不采用 Rust 式r前缀 + 禁转义方案,因为 Swift 式是普通字符串的推广(非原始只是#数量为零的特例),且保留转义能力意味着维护时不会被迫放弃原始字符串。已知弱点(如"\\################"这类极端字符串难以用原始字符串表达)可通过"\后跟 N+1 个以上#原样保留"的规则修补。

  • 内部空白:字符串中不允许裸制表符,鼓励在可用处用\t(原始字符串中则用更啰嗦的\#t),以保护程序可读性。

总结

Carbon 的字符串字面量设计以"可读、可工具化、表达自然"为目标,用普通/原始单行/块两组正交维度覆盖几乎所有字符串内容场景:块字符串的缩进剥离让多行文本融入代码缩进结构,#级联定界让含大量\"的文本免于转义地狱,\#n机制又保证原始字符串不损失转义能力,\xHH保留了非 UTF-8 字节序列的表达通道,文件类型指示符则为语法高亮、格式化乃至静态分析工具提供了嵌入式代码的识别线索。其整体哲学是避免不必要的发明,沿袭 Rust 与 Swift 的成熟做法——这正是它在转义集合、\u{...}形式与原始字符串语法上与之高度一致的原因。如需查阅完整规范,请以词法约定设计文档为准,它基于本提案并在语法细节上持续演进。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 21:49:47

怀化快手AI短视频:下沉市场的内容策略

来源&#xff1a;唐sirAI&#xff08;www.tangsir.cc&#xff09; | 电话&#xff1a;18874530691━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━怀化的商家朋友们&#xff0c;如果您正在寻找在快手平台上进行AI短视频营销的方法&#xff…

作者头像 李华
网站建设 2026/9/10 21:44:13

OFDM系统核心原理与Matlab实现详解

1. OFDM系统概述与核心原理 OFDM&#xff08;正交频分复用&#xff09;作为现代无线通信系统的核心技术&#xff0c;其核心思想是将高速数据流分配到多个相互正交的子载波上并行传输。这种设计有效对抗多径效应带来的符号间干扰&#xff08;ISI&#xff09;&#xff0c;在4G/5G…

作者头像 李华
网站建设 2026/9/10 21:40:47

怀化短视频制作价格对比:AI vs 传统拍摄

来源&#xff1a;唐sirAI&#xff08;www.tangsir.cc&#xff09; | 电话&#xff1a;18874530691━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━在怀化&#xff0c;越来越多的商家开始关注怀化短视频制作价格。而价格&#xff0c;无疑是大…

作者头像 李华
网站建设 2026/9/10 21:39:06

鹤城区AI短视频:怀化主城区营销阵地

来源&#xff1a;唐sirAI&#xff08;www.tangsir.cc&#xff09; | 电话&#xff1a;18874530691━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━怀化鹤城区的企业和商家们&#xff0c;您是否还在为高昂的短视频制作费用而烦恼&#xff1f…

作者头像 李华