字符串和字符串字面量

字符串是值为文本的 String 类型对象。 文本在内部存储为 Char 对象的依序只读集合。 在 C# 字符串末尾没有 null 终止字符;因此,一个 C# 字符串可以包含任何数量的嵌入的 null 字符 ('\0')。 字符串的 Length 属性表示其包含的 Char 对象数量,而非 Unicode 字符数。 若要访问字符串中的各个 Unicode 码位,请使用 StringInfo 对象。

string 与System.String

在 C# 中,string 关键字是 String 的别名。 因此,Stringstring 是等效的(虽然建议使用提供的别名 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# 运算符实际上都是在新的字符串对象中返回结果。 在下面的示例中,当 s1s2 的内容被串联在一起以形成单个字符串时,两个原始字符串没有被修改。 += 运算符创建一个新的字符串,其中包含组合的内容。 这个新对象被分配给变量 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 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."

原始字符串文本

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

  • 以至少三个双引号字符序列 (""") 开头和结尾。 可以使用三个以上的连续字符开始和结束序列,以支持包含三个(或更多)重复引号字符的字符串字面量。
  • 单行原始字符串字面量需要左引号和右引号字符位于同一行上。
  • 多行原始字符串字面量需要左引号和右引号字符位于各自的行上。
  • 在多行原始字符串字面量中,会删除右引号左侧的任何空格。

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

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"
  ]
}
""";

将该文本与 JSON 序列化示例中的等效文本(没有使用此新功能)进行比较。

字符串转义序列

转义序列 字符名称 Unicode 编码
\' 单引号 0x0027
\" 双引号 0x0022
\\ 反斜杠 0x005C
\0 null 0x0000
\a 警报 0x0007
\b Backspace 0x0008
\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# 6.0 及更高版本中提供,内插字符串$ 特殊字符标识,并在大括号中包含内插表达式。 如果不熟悉字符串内插,请参阅字符串内插 - 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.

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

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

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.

复合格式设置

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 类型格式的详细信息,请参阅 .NET 中的格式设置类型

子字符串

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

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"

如果 String 方法不提供修改字符串中的各个字符所需的功能,可以使用 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 和字符串