字符串和字符串字面量

字符串是值为文本的 String 类型对象。 文本在内部存储为 Char 对象的依序只读集合。 字符串的 Length 属性表示其包含的 Char 对象数量,而非 Unicode 字符数。 若要访问字符串中的各个 Unicode 码位,请使用 StringInfo 对象。

string 与 System.String

在 C# 中,string 关键字是 String 的别名;因此,String 和 string 是等效的。 使用提供的别名 string,因为即使没有 using System; 它也有效。 String 类提供了安全创建、操作和比较字符串的多种方法。 此外,C# 语言重载了部分运算符,以简化常见字符串操作。 有关关键字的详细信息,请参阅 string。 有关类型及其方法的详细信息,请参阅 String。

声明和初始化字符串

可以使用各种方法声明和初始化字符串,如以下示例中所示:

// Declare without initializing.
string message1;

// Initialize to null.
string? message2 = null;

// Initialize as an empty string.
// Use the Empty constant instead of the literal "".
string message3 = System.String.Empty;

// Initialize with a regular string literal.
string oldPath = "c:\\Program Files\\Microsoft Visual Studio 8.0";

// Initialize with a verbatim string literal.
string newPath = @"c:\Program Files\Microsoft Visual Studio 9.0";

// Use System.String if you prefer.
System.String greeting = "Hello World!";

// In local variables (i.e. within a method body)
// you can use implicit typing.
var temp = "I'm still a strongly-typed System.String!";

// Use a const string to prevent 'message4' from
// being used to store another string value.
const string message4 = "You can't get rid of me!";

// Use the String constructor only when creating
// a string from a char*, char[], or sbyte*. See
// System.String documentation for details.
char[] letters = { 'A', 'B', 'C' };
string alphabet = new string(letters);

不要使用 new 运算符创建字符串对象,除非使用字符数组初始化字符串。

使用 Empty 常量值初始化字符串,以新建字符串长度为零的 String 对象。 长度为零的字符串的字符串字面量表示形式为 ""。 通过使用 Empty 值(而不是 null)初始化字符串,可以减少 NullReferenceException 发生的可能性。 尝试访问字符串前,先使用静态 IsNullOrEmpty(String) 方法验证字符串的值。

字符串的不可变性

字符串对象是不可变的:它们在创建后便无法更改。 看起来是在修改字符串的所有 String 方法和 C# 运算符实际上都是在新的字符串对象中返回结果。 在下面的示例中,当 s1 和 s2 的内容被串联在一起以形成单个字符串时,两个原始字符串没有被修改。 += 运算符创建一个新的字符串,其中包含组合的内容。 该新对象被分配给变量 s1,并且分配给 s1 的原始对象将被释放进行垃圾回收,因为其他变量没有保留对变量的引用。

string s1 = "A string is more ";
string s2 = "than the sum of its chars.";

// Concatenate s1 and s2. This actually creates a new
// string object and stores it in s1, releasing the
// reference to the original object.
s1 += s2;

System.Console.WriteLine(s1);
// Output: A string is more than the sum of its chars.

由于字符串“modification”实际上是一个新创建的字符串,因此,必须在创建对字符串的引用时使用警告。 如果创建对字符串的引用,然后“修改”原始字符串,则引用将继续指向原始对象。 原始对象不反映修改字符串时创建的新对象。 以下代码阐释了此行为:

string str1 = "Hello ";
string str2 = str1;
str1 += "World";

System.Console.WriteLine(str2);
//Output: Hello

有关如何创建基于修改的新字符串的详细信息,例如原始字符串上的搜索和替换操作,请参阅如何修改字符串内容。

带引号的字符串字面量

带引号的字符串字面量会在同一行上以单个双引号字符 (") 开头和结尾。 带引号的字符串字面量最适合匹配单个行且不包含任何转义序列的字符串。 带引号的字符串字面量必须嵌入转义字符,如以下示例所示:

string columns = "Column 1\tColumn 2\tColumn 3";
//Output: Column 1        Column 2        Column 3

string rows = "Row 1\r\nRow 2\r\nRow 3";
/* Output:
    Row 1
    Row 2
    Row 3
*/

string title = "\"The \u00C6olean Harp\", by Samuel Taylor Coleridge";
//Output: "The Æolean Harp", by Samuel Taylor Coleridge

逐字字符串文本

对于多行字符串、包含反斜杠字符或嵌入双引号的字符串,逐字字符串字面量更方便。 逐字字符串将新的行字符作为字符串文本的一部分保留。 使用双引号在逐字字符串内部嵌入引号。 下面的示例演示逐字字符串的一些常见用法:

string title = "\"The \u00C6olean Harp\", by Samuel Taylor Coleridge";
//Output: "The Æolean Harp", by Samuel Taylor Coleridge

string filePath = @"C:\Users\scoleridge\Documents\";
//Output: C:\Users\scoleridge\Documents\

string text = @"My pensive SARA ! thy soft cheek reclined
    Thus on mine arm, most soothing sweet it is
    To sit beside our Cot,...";
/* Output:
My pensive SARA ! thy soft cheek reclined
    Thus on mine arm, most soothing sweet it is
    To sit beside our Cot,...
*/

string quote = @"Her name was ""Sara.""";
//Output: Her name was "Sara."

原始字符串文本

可以使用 原始字符串文本 更轻松地创建多行字符串,或使用任何需要转义序列的字符。 原始字符串字面量无需使用转义序列。 你可以编写字符串,包括空格格式,以及你希望在输出中显示该字符串的方式。 原始字符串字面量:

  • 以至少三个双引号字符序列 (""") 开头和结尾。 可使用三个以上的连续字符来开始和结束序列,从而支持包含三个(或更多)重复引号字符的字符串字面量。
  • 单行原始字符串字面量需要左引号和右引号字符位于同一行上。
  • 多行原始字符串字面量需要左引号和右引号字符位于各自的行上。
  • 在多行原始字符串字面量中,将从原始字符串字面量的所有行中删除右引号左侧的所有空格。
  • 在多行原始字符串字面量中,将忽略同一行开始引号后面的空格。
  • 在多行原始字符字面量中,紧跟在开始引号后面的仅由空格组成的行也被包含在字符串字面量中。

以下示例演示了这些规则:

string singleLine = """Friends say "hello" as they pass by.""";
string multiLine = """
    "Hello World!" is typically the first program someone writes.
    """;
string embeddedXML = """
       <element attr = "content">
           <body style="normal">
               Here is the main text
           </body>
           <footer>
               Excerpts from "An amazing story"
           </footer>
       </element >
       """;
// The line "<element attr = "content">" starts in the first column.
// All whitespace left of that column is removed from the string.

string rawStringLiteralDelimiter = """"
    Raw string literals are delimited 
    by a string of at least three double quotes,
    like this: """
    """";

以下示例演示了基于这些规则报告的编译器错误:

// CS8997: Unterminated raw string literal.
var multiLineStart = """This
    is the beginning of a string 
    """;

// CS9000: Raw string literal delimiter must be on its own line.
var multiLineEnd = """
    This is the beginning of a string """;

// CS8999: Line does not start with the same whitespace as the closing line
// of the raw string literal
var noOutdenting = """
    A line of text.
Trying to outdent the second line.
    """;

前两个示例无效,因为多行原始字符串字面量需要让左引号和右引号序列在其自己的行上。 第三个示例无效,因为文本已从右引号序列中缩进。

使用带引号的字符串字面量或逐字字符串字面量时,如果生成的文本包括需要转义序列的字符,应考虑原始字符串字面量。 原始字符串字面量对你和其他人来说更容易阅读,因为它更类似于输出文本。 例如,请考虑包含格式化 JSON 字符串的以下代码:

string jsonString = """
{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
  "DatesAvailable": [
    "2019-08-01T00:00:00-07:00",
    "2019-08-02T00:00:00-07:00"
  ],
  "TemperatureRanges": {
    "Cold": {
      "High": 20,
      "Low": -10
    },
    "Hot": {
      "High": 60,
      "Low": 20
    }
            },
  "SummaryWords": [
    "Cool",
    "Windy",
    "Humid"
  ]
}
""";

小窍门

Visual Studio 和 C# 开发工具包在原始字符串文本包含 JSON 数据或正则表达式时提供一些验证和语法突出显示。

这些工具分析文本。 如果工具确信文本表示 JSON 或正则表达式,编辑器将提供语法着色。

可以通过在声明上方添加指示格式的注释来改善该体验:

  • // lang=json 指示原始字符串文本表示 JSON 数据。
  • // lang=regex 指示原始字符串文本表示正则表达式。

当原始字符串文本用作参数时,参数使用该 System.Diagnostics.CodeAnalysis.StringSyntaxAttribute 参数来指示格式,这些工具将验证某些格式类型的原始字符串文本。 支持 JSON 和正则表达式。

对于某些格式,注释或属性可以启用代码建议,为基于格式的字符串字面值提供修复建议。

字符串转义序列

转义序列 字符名称 Unicode 编码
\' 单引号 0x0027
\" 双引号 0x0022
\\ 反斜杠 0x005C
\0 零 0x0000
\a 警报 0x0007
\b 退格键 0x0008
\e 逃跑 0x001B
\f 换页 0x000C
\n 换行 0x000A
\r 回车 0x000D
\t 水平制表符 0x0009
\v 垂直制表符 0x000B
\u Unicode 转义序列 (UTF-16) \uHHHH(范围:0000 - FFFF;示例:\u00E7 = "ç")
\U Unicode 转义序列 (UTF-32) \U00HHHHHH(范围:000000 - 10FFFF;示例:\U0001F47D = "👽")
\x 除长度可变外,Unicode 转义序列与“\u”类似 \xH[H][H][H](范围:0 - FFFF;示例:\x00E7 或 \x0E7,或 \xE7 = "ç")

警告

使用 \x 转义序列并指定小于 4 个十六进制数字时,如果紧跟转义序列的字符是有效的十六进制数字(如 0-9、A-F 和 a-f),它们将被解释为转义序列的一部分。 例如,\xA1 会生成“¡”,即码位 U+00A1。 但是,如果下一个字符是“A”或“a”,则转义序列将转而被解释为 \xA1A 并生成“ਚ”(即码位 U+0A1A)。 在此类情况下,如果指定全部 4 个十六进制数字(例如 \x00A1),则可阻止所有潜在的解释出错。

注释

在编译时,逐字字符串和原始字符串会转换为普通字符串,并具有所有相同的转义序列。 因此,如果在调试器监视窗口中查看逐字或原始字符串,则会看到编译器添加的转义字符,而不是源代码中的逐字或原始版本。 例如,逐字字符串 @"C:\files.txt" 在监视窗口中显示为 "C:\\files.txt"。

设置字符串格式

格式字符串是在运行时以动态方式确定其内容的字符串。 格式字符串是通过将内插表达式或占位符嵌入字符串大括号内创建的。 大括号 ({...}) 中的所有内容都将解析为一个值,并在运行时以格式化字符串的形式输出。 有两种方法创建格式字符串:字符串内插和复合格式。

字符串内插

使用 特殊字符来声明。 内插字符串会在大括号中包含内插表达式。 如果你不熟悉字符串 内插,请参阅字符串内插 - C# 教程 ,了解几个示例。

使用字符串内插来改善代码的可读性和可维护性。 字符串内插可实现与 String.Format 方法相同的结果,但更易于使用且改进了内联清晰度。

var jh = (firstName: "Jupiter", lastName: "Hammon", born: 1711, published: 1761);
Console.WriteLine($"{jh.firstName} {jh.lastName} was an African American poet born in {jh.born}.");
Console.WriteLine($"He was first published in {jh.published} at the age of {jh.published - jh.born}.");
Console.WriteLine($"He'd be over {Math.Round((2018d - jh.born) / 100d) * 100d} years old today.");

// Output:
// Jupiter Hammon was an African American poet born in 1711.
// He was first published in 1761 at the age of 50.
// He'd be over 300 years old today.

当用于占位符的所有表达式也是常量字符串时,可以使用字符串内插来初始化常量字符串。

可以将 原始字符串文本 与 字符串插值 组合在一起。 使用三个或更多个连续双引号开始和结束格式字符串。 如果输出字符串应包含 { 或 } 字符,则可以使用额外的 $ 字符来指定开始和结束内插的 { 和 } 字符数。 输出中包含任何更少的 { 或 } 字符序列。 以下示例演示了如何使用该功能来显示点与原点的距离,以及如何将点置于大括号中:

int X = 2;
int Y = 3;

var pointMessage = $$"""The point {{{X}}, {{Y}}} is {{Math.Sqrt(X * X + Y * Y)}} from the origin.""";

Console.WriteLine(pointMessage);
// Output:
// The point {2, 3} is 3.605551275463989 from the origin.

逐字字符串内插

C# 还允许逐字面字符串内插,例如,可使用 $@ 或 @$ 语法在多行中实现。

若要逐字解释转义序列,可使用逐字字符串文本。 内插逐字字符串以 $ 字符开头,后跟 @ 字符。 可以按任意顺序使用 $ 和 @ 标记:$@"..." 和 @$"..." 均为有效的内插逐字字符串。

var jh = (firstName: "Jupiter", lastName: "Hammon", born: 1711, published: 1761);
Console.WriteLine($@"{jh.firstName} {jh.lastName}
    was an African American poet born in {jh.born}.");
Console.WriteLine(@$"He was first published in {jh.published}
at the age of {jh.published - jh.born}.");

// Output:
// Jupiter Hammon
//     was an African American poet born in 1711.
// He was first published in 1761
// at the age of 50.

复合格式设置

String.Format 利用大括号中的占位符创建格式字符串。 此示例产生的输出与前一示例中使用的字符串插值方法类似。

var pw = (firstName: "Phillis", lastName: "Wheatley", born: 1753, published: 1773);
Console.WriteLine("{0} {1} was an African American poet born in {2}.", pw.firstName, pw.lastName, pw.born);
Console.WriteLine("She was first published in {0} at the age of {1}.", pw.published, pw.published - pw.born);
Console.WriteLine("She'd be over {0} years old today.", Math.Round((2018d - pw.born) / 100d) * 100d);

// Output:
// Phillis Wheatley was an African American poet born in 1753.
// She was first published in 1773 at the age of 20.
// She'd be over 300 years old today.

有关详细信息,请参阅 .NET 中的复合格式设置。

子字符串

子字符串是包含在字符串中的任何字符序列。 使用 Substring 方法可以通过原始字符串的一部分新建字符串。 可以使用 IndexOf 方法搜索一次或多次出现的子字符串。 使用 Replace 方法可以将出现的所有指定子字符串替换为新字符串。 与 Substring 方法一样,Replace 实际返回的是新字符串,且不修改原始字符串。 有关详细信息,请参阅 C# 中的搜索字符串 以及如何 修改字符串内容。

string s3 = "Visual C# Express";
System.Console.WriteLine(s3.Substring(7, 2));
// Output: "C#"

System.Console.WriteLine(s3.Replace("C#", "Basic"));
// Output: "Visual Basic Express"

// Index values are zero-based
int index = s3.IndexOf("C");
// index = 7

访问单个字符

可以使用包含索引值的数组表示法来获取对单个字符的只读访问权限,如下面的示例中所示:

string s5 = "Printing backwards";

for (int i = 0; i < s5.Length; i++)
{
    System.Console.Write(s5[s5.Length - i - 1]);
}
// Output: "sdrawkcab gnitnirP"

对于更广泛的字符串操作,可以使用 StringBuilder 对象来修改各个字符“原地”。然后创建一个新字符串,使用 StringBuilder 方法来存储结果。 在下面的示例中,假定必须以特定方式修改原始字符串,然后存储结果以供未来使用:

string question = "hOW DOES mICROSOFT wORD DEAL WITH THE cAPS lOCK KEY?";
System.Text.StringBuilder sb = new System.Text.StringBuilder(question);

for (int j = 0; j < sb.Length; j++)
{
    if (System.Char.IsLower(sb[j]) == true)
        sb[j] = System.Char.ToUpper(sb[j]);
    else if (System.Char.IsUpper(sb[j]) == true)
        sb[j] = System.Char.ToLower(sb[j]);
}
// Store the new string.
string corrected = sb.ToString();
System.Console.WriteLine(corrected);
// Output: How does Microsoft Word deal with the Caps Lock key?

有关索引的详细信息,请参阅 “浏览索引和范围 ”一文。

Null 字符串和空字符串

空字符串是包含零个字符的 System.String 对象实例。 空字符串常用在各种编程方案中,表示空文本字段。 可以对空字符串调用方法,因为它们是有效的 System.String 对象。 对空字符串进行了初始化,如下所示:

string s = String.Empty;

相比较而言,null 字符串并不指 System.String 对象实例,只要尝试对 null 字符串调用方法,都会引发 NullReferenceException。 但是,可以在串联和与其他字符串的比较操作中使用 null 字符串。 以下示例说明了对 null 字符串的引用会引发和不会引发意外的某些情况:

string str = "hello";
string? nullStr = null;
string emptyStr = String.Empty;

string tempStr = str + nullStr;
// Output of the following line: hello
Console.WriteLine(tempStr);

bool b = (emptyStr == nullStr);
// Output of the following line: False
Console.WriteLine(b);

// The following line creates a new empty string.
string newStr = emptyStr + nullStr;

// Null strings and empty strings behave differently. The following
// two lines display 0.
Console.WriteLine(emptyStr.Length);
Console.WriteLine(newStr.Length);
// The following line raises a NullReferenceException.
//Console.WriteLine(nullStr.Length);

// The null character can be displayed and counted, like other chars.
string s1 = "\x0" + "abc";
string s2 = "abc" + "\x0";
// Output of the following line: * abc*
Console.WriteLine("*" + s1 + "*");
// Output of the following line: *abc *
Console.WriteLine("*" + s2 + "*");
// Output of the following line: 4
Console.WriteLine(s2.Length);

使用 StringBuilder 快速创建字符串

.NET 中的字符串操作进行了高度的优化,在大多数情况下不会显著影响性能。 但是,在某些情况下(例如,执行数百次或数千次的紧密循环),字符串操作可能影响性能。 StringBuilder 类创建字符串缓冲区,用于在程序执行多个字符串操控时提升性能。 使用 StringBuilder 字符串,还可以重新分配各个字符,而内置字符串数据类型则不支持这样做。 例如,此代码更改字符串的内容,而无需创建新的字符串:

System.Text.StringBuilder sb = new System.Text.StringBuilder("Rat: the ideal pet");
sb[0] = 'C';
System.Console.WriteLine(sb.ToString());
//Outputs Cat: the ideal pet

在以下示例中,StringBuilder 对象用于通过一组数字类型创建字符串:

var sb = new StringBuilder();

// Create a string composed of numbers 0 - 9
for (int i = 0; i < 10; i++)
{
    sb.Append(i.ToString());
}
Console.WriteLine(sb);  // displays 0123456789

// Copy one character of the string (not possible with a System.String)
sb[0] = sb[9];

Console.WriteLine(sb);  // displays 9123456789

字符串、扩展方法和 LINQ

由于 String 类型实现 IEnumerable<T>,因此可以对字符串使用 Enumerable 类中定义的扩展方法。 为了避免视觉干扰,这些方法已从 String 类型的 IntelliSense 中排除,但它们仍然可用。 还可以使用字符串上的 LINQ 查询表达式。 有关详细信息,请参阅 LINQ 和字符串。