C# 中的转义:字符、字符串、字符串格式、关键字、标识符






4.90/5 (58投票s)
转义字面量和名称/关键字的各种可能性。
引言
每个人都知道如何在 C# 字符串中转义特定字符。那么,为什么还要费心呢?
这篇提示展示了 C# 中转义涉及的细微差别
- 字符字面量转义 | 例如:'\'' 、'\n' 、'\u20AC' (欧元 € 货币符号)、'\x9' (等同于 \t ) |
- 字面字符串转义 | 例如:"...\t...\u0040...\U000000041...\x9..." |
- 逐字字符串转义 | 例如:@\"...""...\" |
- string.Format 转义 | 例如:\"...{{...}}...\" |
- 关键字转义 | 例如:@if (将 if 用作标识符) |
- 标识符转义 | 例如:i\u0064 (用于 id ) |
目录
转义 - 为了什么?
同样,每个人都知道这一点——或者至少有所感觉。尽管如此,我还是想提醒一下转义的好处。
转义为“正常”含义提供了替代含义。“正常”取决于常用的内容。对于什么是“正常”,没有绝对的参考,因此,每种转义机制都定义了什么“正常”,以及其转义方式。
例如,字符串字面量用双引号 "..."
括起来。双引号的含义是包围字符串字面量——这是双引号对字符串的正常含义。如果您现在想在字符串字面量中包含一个双引号,您必须说明该双引号不具有正常含义。例如,"..."..."
会在第二个双引号处终止字符串,而 \"...\"
会转义第二个双引号,使其不被解释为终止字符串字面量。
有各种成熟的转义机制。转义的动机也各不相同。使用转义的一些动机
- 在字符串和字符字面量中
- 必须能够嵌入终止符,如单引号或双引号。
- 需要输入没有字符符号的特殊字符,例如水平制表符。
- 需要输入键盘上没有直接按键的字符,例如日元货币符号(¥)。
- 等等。
- 在标识符中
- 需要输入键盘上没有对应按键的字符名称,例如德语的变音字母 Ä(Unicode 0x00C4)。
- 需要生成 C# 代码,其中可能使用与 C# 关键字冲突的标识符,例如
yield
。 - 等等。
- 在字符串格式化中
- 必须能够在
string.Format(...)
中输入字面量的{
或}
,例如在
Console.WriteLine("...{...", ...)
.
- 必须能够在
- 在正则表达式中
- 必须匹配那些具有控制含义的字符,例如匹配字符
[
等。
- 必须匹配那些具有控制含义的字符,例如匹配字符
- 等等。
那么,让我们开始讨论几种转义正常行为的各种机制。
字符和字符串字面量中的转义
首先我们来看字符串。字符串是字符序列。字符是一种保存 UTF-16[^] 编码值的类型。因此,字符是两个字节的值。
例如,UTF-16 十进制码 64(十六进制 40)是 @
字符。
注意:有少数“字符”无法直接用这两个字节编码。这些字符占用 4 个字节,即一对 UTF-16 值。这些称为 UTF-16:代理项对[^](搜索“代理项对”)。
因此,字符串是双字节字符的序列。
例如,字符串 "abc"
在执行程序中会变成 UTF-16 值序列 0x0061
、0x0062
、0x0063
。或者欧元货币符号是 Unicode 字符 0x20AC
( €),日元货币符号是 Unicode 字符 0x00A5
(¥)。
如何在 C# 中编写它?
char euro = '\u20ac';
char yen = '\u00a5';
\uxxxx
项表示一个 UTF-16 码。
作为替代,可以将 \u....
写成 \x
,后跟一到四个十六进制字符。上面的示例也可以写成
char euro = '\x20ac';
char yen = '\xa5';
注意: \x
序列会尝试匹配尽可能多的字符,即 "\x68ello"
会得到 "ڎllo"
而不是 "hello"
(\x68e
在三个字符后终止,因为下一个字符不是十六进制字符。因此,\u...
比使用 \x...
更安全,因为前者给定了长度,而后者取最长匹配,这可能会误导您。
注释
- 请注意,大写
\Uxxxxxxxx
项表示代理项对。由于代理项对需要一对 UTF-16 字符,因此它不能存储在一个 C# 字符中。 \u
后面必须精确地跟四个十六进制字符\U
后面必须精确地跟八个十六进制字符\x
后面必须跟一到四个十六进制字符
啊,是的,由于这是常识,我差点忘了提供一些常用特殊字符的短字符转义符号,例如 \n
等。
短符号 | UTF-16 字符 | 描述 |
---|---|---|
\' | \u0027 | 允许在字符字面量中输入 ' ,例如 '\'' |
\" | \u0022 | 允许在字符串字面量中输入 " ,例如 "this is the double quote (\") character" |
\\ | \u005c | 允许在字符或字符串字面量中输入 \ 字符,例如 '\\' 或 "this is the backslash (\\) character" |
\0 | \u0000 | 允许输入码为 0 的字符 |
\a | \u0007 | 警报(通常是硬件蜂鸣声) |
\b | \u0008 | 退格 |
\f | \u000c | 换页(下一页) |
\n | \u000a | 换行(下一行) |
\r | \u000d | 回车(移到行首) |
\t | \u0009 | (水平)制表符 |
\v | \u000b | 垂直制表符 |
摘要
- 字符是双字节 UTF-16 码
- UTF-16 代理项对存储在一对 C# 字符中
- 转义字符
\
引入了转义 \
字符后面跟着的是- 短符号字符之一(
\\
、\"
、\'
、\a
等) - Unicode 字符码(
\u20a5
、\u00a5
等) - 代理项对(
\Ud869ded6
等),只能存储在字符串中,但不能存储在单个字符中。 - 1 到 4 个十六进制字符的十六进制序列(
\xa5
等)
- 短符号字符之一(
逐字字符串中的转义
什么是逐字字符串?这是一种 语法糖[^],用于在 C# 中输入字符串。
例如,存储 Windows 文件路径
string path = "C:\\Program Files\\Microsoft Visual Studio 10.0\\";
可能被认为是尴尬或难看的。更方便的版本是逐字字符串
string path = @"C:\Program Files\Microsoft Visual Studio 10.0\";
逐字字符串(@"..."
)按原样获取内容,不解释任何字符。嗯,几乎;只有一个字符可以转义:嵌入的 "
必须转义为 ""
。例如
string xml = @"<?xml version=""1.0""?>
<Data>
...
<Data>";
注意:如上所述,逐字字符串字面量是 C# 中输入字符串字面量的便捷方式。内存中的字符串图像是相同的。例如,这些都是相同的字符串内容
string v1 = "a\r\nb";
string v2 = "\u0061\u000d\u000a\u0062";
string v3 = @"a
b";
Console.WriteLine("v1 = \"{0}\"\nv2 = \"{1}\"\nsame = {2}", v1, v2, v1 == v2);
Console.WriteLine("v1 = \"{0}\"\nv3 = \"{1}\"\nsame = {2}", v1, v3, v1 == v3);
结果是
v1 = "a
b"
v2 = "a
b"
same = True
v1 = "a
b"
v3 = "a
b"
same = True
摘要
- 逐字字符串字面量和普通字符串字面量是定义字符串内容的两种方式
- 逐字字符串按原样接收所有给定字符,包括换行符等
- 逐字字符串字面量中唯一的转义序列是
""
,用于表示嵌入的"
字符
string.Format 转义
格式字符串是在运行时(而不是编译时)解释以替换 {...}
为相应的参数。例如。
Console.WriteLine("User = {0}", Environment.UserName);
但是,如果您想在格式字符串中嵌入 {
或 }
怎么办?是 \{
还是 {{
?想想看!
显然是后者。为什么?让我们详细说明一下。
- 格式字符串和其他字符串一样。您可以输入为
string.Format("...", a, b); string.Format(@"...", a, b); string.Format(s.GetSomeFormatString(), a, b);
- 如果 C# 允许输入
\{
或\}
,它将作为{
和}
分别存储在字符串中。 string.Format
函数然后读取此字符串以决定是否解释格式说明,例如{0}
。由于\{
导致字符串中出现{
字符,string.Format
函数无法确定这应该被视为格式说明还是字面量{
。- 另一种选择是一些其他转义。既成方法是加倍要转义的字符。
- 因此,
string.Format
将{{
视为字面量{
。类似地,}}
用于}
。
摘要
string.Format(...)
转义仅在运行时解释,不在编译时解释- 在
string.Format(...)
中具有特殊含义的两个字符是{
和}
- 这两个字符的转义是加倍该特定字符:
{{
和}}
(例如,Console.WriteLine("{{{0}}}", "abc");
的控制台输出为 {abc})
奖励
以下代码会扫描 C# 字符串格式文本并返回所有参数 id
public static IEnumerable<int> GetIds(string format)
{
string pattern = @"\{(\d+)[^\}]*\}";
var ids = Regex.Matches(format, pattern, RegexOptions.Compiled)
.Cast<Match>()
.Select(m=>int.Parse(m.Groups[1].Value));
}
foreach (int n in GetIds("a {0} b {1 } c {{{0,10}}} d {{e}}")) Console.WriteLine(n);
传递 "a {0} b {1 } c {{{0,10}}} d {{e}}"
会得到
0
1
0
标识符转义
为什么有人会转义标识符?我猜这实际上并不打算日常使用。它可能只对自动生成的 C# 代码有用。尽管如此,有两种机制可以转义标识符。
- 定义一个会与关键字冲突的标识符
- 定义一个包含键盘上没有对应按键的字符的标识符
选项 A:在标识符前加上 @
前缀,例如
int @yield = 10;
选项 B:使用上面字符串字面量中描述的 UTF-16 转义序列,例如
int \u0079ield = 10;
注释
- 关键字必须保持未转义,即,如果一个标识符写成 @xxx,它将始终是一个标识符(即永远不是关键字)。
- 包含 UTF-16 转义序列的标识符也一样
- 您可以混合搭配转义的标识符,例如以下是相同的
while (@a > 0) \u0061 = a - 1; while (a > 0) a = a - 1;
摘要
- C# 中提供了标识符转义
- 可以通过在标识符前加上
@
来避免关键字冲突 - 可以使用 UTF-16 字符转义序列对标识符字符进行编码
- 转义的标识符仍必须来自合法的字符集——您不能定义包含点的标识符等。
- 数字、运算符和标点符号不能转义(例如,1.0f 等不能转义)
- 我的观点:转义标识符并非用于日常使用——例如,永远不要尝试在任何标识符前加上
@
!这仅用于自动生成的代码,即用户永远不应该看到这样的标识符……
正则表达式中的转义
Regex 模式字符串也像 string.Format(...)
一样在运行时进行解释。Regex 语法包含以 \
开头的指令。例如,\d
表示 0...9
集合中的单个字符。这篇提示不深入讲解 Regex 语法,而是讲解如何方便地将这种 Regex 模式放入 C# 字符串中。
由于 Regex 模式很可能包含一些 \
,因此将 Regex 模式写成逐字字符串会更方便。结果是模式中的 \
不需要转义。例如,以下模式对于 Regex 模式 \d+|\w+
来说是相同的(自己决定哪种更方便)
var match1 = Regex.Matches(input, "\\d+|\\w+");
var match2 = Regex.Matches(input, @"\d+|\w+");
有一个陷阱:在逐字字符串中输入双引号看起来有点奇怪。最后,由您选择输入模式的方式,作为普通字符串字面量或作为逐字字符串字面量。
摘要
- Regex 模式方便地以逐字字符串
@"..."
的形式输入
奖励
以下代码显示了 C# 的标记化。请尝试理解转义
string strlit = @"""(?:\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}|\\x[0-9a-fA-F]{1,4}|\\.|[^""])*""";
string verlit = @"@""(?:""""|[^""])*"""; // or: "@\"(?:\"\"|[^\"])*\""
string charlit = @"'(?:\\u[0-9a-fA-F]{4}|\\x[0-9a-fA-F]{1,4}|\\.|[^'])'";
string hexlit = @"0[xX][0-9a-fA-F]+[ulUL]?";
string number1 = @"(?:\d*\.\d+)(?:[eE][-+]?\d+)?[fdmFDM]?";
string number2 = @"\d+(?:[ulUL]?|(?:[eE][-+]?\d+)[fdmFDM]?|[fdmFDM])";
string ident = @"@?(?:\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}|\w)+";
string[] op3 = new string[] {"<<="};
string[] op2 = new string[] {"!=","%=","&&","&=","*=","++","+=","--","-=","/=",
"::","<<","<=","==","=>","??","^=","|=","||"};
string rest = @"\S";
string skip = @"(?:"+ string.Join("|", new string[]
{
@"[#].*?\n", // C# pre processor line
@"//.*?\n", // C# single line comment
@"/[*][\s\S]*?[*]/", // C# block comment
@"\s", // white-space
}) + @")*";
string pattern = skip + "(" + string.Join("|", new string[]
{
strlit, // C# string literal
verlit, // C# verbatim literal
charlit, // C# character literal
hexlit, // C# hex number literal
number1, // C# real literal
number2, // C# integer or real literal
ident, // C# identifiers
string.Join("|",op3.Select(t=>Regex.Escape(t))), // C# three-letter operator
string.Join("|",op2.Select(t=>Regex.Escape(t))), // C# two-letter operator
rest, // C# one-letter operator and any other one char
}) + @")" + skip;
string f = @"..."; // enter your path to the C# file to parse
string input = File.ReadAllText(f);
var matches = Regex.Matches(input, pattern, RegexOptions.Singleline|RegexOptions.Compiled).Cast<Match>();
foreach (var token in from m in matches select m.Groups[1].Value)
{
Console.Write(" {0}", token);
if ("{};".Contains(token)) Console.WriteLine();
}
玩得开心!
链接
以下链接可能提供其他信息
- C# 参考 (MSDN)[^]
- 标准 ECMA-334:C# 语言规范[^]
- UTF-16[^]
- EBNF C# 语法描述[^]
- C# 中有哪些字符转义序列? (MSDN)[^]
- String.Format 方法 (MSDN)[^]
- 正则表达式对象模型 (MSDN)[^]
- 正则表达式语言 - 快速参考 (MSDN)[^]
历史
V1.0 | 2012-04-23 |
初始版本。 |
V1.1 | 2012-04-23 |
修复了格式错误。 |
V1.2 | 2012-04-25 |
修复了拼写错误,添加了更多链接,修复了文本中的 HTML Unicode 字面量,更新了一些摘要。 |
V1.3 | 2012-08-21 |
修复了 \x... 的描述。创建了更多 ArticleTable 类表(看起来更美观) |