C# 内部(第二版):字符串处理和正则表达式(第一部分)






4.94/5 (64投票s)
2002 年 5 月 15 日
17分钟阅读

736328

2213
本文将探讨 String 类、它的一些简单方法以及它的各种格式说明符。
![]() |
|
字符串处理和正则表达式,第一部分
这个分为两部分的字符串处理和正则表达式系列基于《C# 内部(第二版)》的第 10 章。由于该章被分成了两节,我在这里也进行了相同的划分,以便于阅读。请注意,虽然每篇文章都主要介绍一个类(分别是 String
和 Regex
)以及一组辅助类,但两者之间有大量的重叠——在大多数情况下,您可以选择使用所有字符串方法、所有正则表达式操作,或者两者结合使用。另外值得注意的是,基于字符串功能编写的代码通常更容易理解和维护,而使用正则表达式编写的代码则通常更灵活、更强大。
话虽如此,本文将首先探讨 String
类、它的一些简单方法以及它的各种格式说明符。然后,我们将研究字符串与其他 .NET Framework 类(包括 Console、基本数值类型和 DateTime)之间的关系,以及区域性信息和字符编码如何影响字符串格式化。我们还将了解 StringBuilder
支持类,并深入研究字符串的驻留。
使用 C# 和 .NET 进行字符串处理
.NET Framework 的System.String
类(或其别名 string)代表一个不可变的字符序列——不可变是因为其值一旦创建就不能被修改。看起来修改字符串的方法实际上会返回一个包含修改的新字符串。除了 String 类之外,.NET Framework 类还提供 StringBuilder
、String.Format
、StringCollection
等。它们共同提供了比较、追加、插入、转换、复制、格式化、索引、连接、拆分、填充、修剪、删除、替换和搜索等方法。考虑这个使用 Replace
、Insert
和 ToUpper
的示例
public class TestStringsApp
{
public static void Main(string[] args)
{
string a = "strong";
// Replace all 'o' with 'i'
string b = a.Replace('o', 'i');
Console.WriteLine(b);
string c = b.Insert(3, "engthen");
string d = c.ToUpper();
Console.WriteLine(d);
}
}
此应用程序的输出将是
string
STRENGTHENING
String
类具有一系列比较方法,包括 Compare
和重载的运算符,正如上一个示例的续集所示
if (d == c) // Different
{
Console.WriteLine("same");
}
else
{
Console.WriteLine("different");
}
此附加代码块的输出是
different
请注意,倒数第二个示例中的字符串变量 a 未因 Replace
操作而改变。但是,您总是可以选择重新分配一个字符串变量。例如
string q = "Foo";
q = q.Replace('o', 'i');
Console.WriteLine(q);
输出是:
Fii
您可以将字符串对象与常规的 char 数组组合,甚至可以以常规方式访问字符串中的元素
string e = "dog" + "bee";
e += "cat";
string f = e.Substring(1,7);
Console.WriteLine(f);
for (int i = 0; i < f.Length; i++)
{
Console.Write("{0,-3}", f[i]);
}
这是输出
ogbeeca
o g b e e c a
如果您想要一个 null 字符串,请声明一个并为其分配 null。之后,您可以将其重新分配给另一个字符串,如下面的示例所示。由于对 g
从 f.Remove
的赋值是在条件块中进行的,因此编译器会拒绝 Console.WriteLine(g)
语句,除非 g
已被赋值为 null
或某个有效字符串值。
string g = null;
if (f.StartsWith("og"))
{
g = f.Remove(2,3);
}
Console.WriteLine(g);
这是输出
ogca
如果您熟悉 Microsoft Foundation Classes (MFC) CString
、Windows Template Library (WTL) CString
或 Standard Template Library (STL) string 类,那么 String.Format
方法应该不会让您感到意外。此外,Console.WriteLine
使用与 String
类相同的格式说明符,如下所示
int x = 16;
decimal y = 3.57m;
string h = String.Format(
"item {0} sells at {1:C}", x, y);
Console.WriteLine(h);
这是输出
item 16 sells at £3.57
如果您有 Microsoft Visual Basic 的经验,您不会对可以使用加号 (+) 将字符串与任何其他数据类型连接感到惊讶。这是因为所有类型至少都继承了 object.ToString
。以下是语法
string t =
"item " + 12 + " sells at " + '\xA3' + 3.45;
Console.WriteLine(t);
这是输出
item 12 sells at £3.45
String.Format
与 Console.WriteLine
有很多共同之处。这两种方法都包含一个重载,该重载接受一个开放式 (params) 对象数组作为最后一个参数。以下两个语句现在将产生相同的输出
// This works because last param is a params object[].
Console.WriteLine(
"Hello {0} {1} {2} {3} {4} {5} {6} {7} {8}",
123, 45.67, true, 'Q', 4, 5, 6, 7, '8');
// This also works.
string u = String.Format(
"Hello {0} {1} {2} {3} {4} {5} {6} {7} {8}",
123, 45.67, true, 'Q', 4, 5, 6, 7, '8');
Console.WriteLine(u);
输出如下
Hello 123 45.67 True Q 4 5 6 7 8
Hello 123 45.67 True Q 4 5 6 7 8
字符串格式化
String.Format
和 WriteLine 格式化都受相同的格式化规则支配:格式参数嵌入其中,包含零个或多个格式说明符,形式为 "{ N [, M ][: formatString ]}", arg1, ... argN,
其中N
是一个零基整数,表示要格式化的参数。M
是一个可选整数,表示包含格式化值的区域的宽度,用空格填充。如果 M 为负数,则格式化值左对齐;如果M
为正数,则值右对齐。formatString
是一个可选的格式代码字符串。argN
是在字符串中相应位置使用的表达式。
如果 argN
为 null
,则使用空字符串代替。如果省略 formatString
,则由 N
指定的参数的 ToString
方法提供格式化。例如,以下三个语句产生相同的输出
public class TestConsoleApp
{
public static void Main(string[] args)
{
Console.WriteLine(123);
Console.WriteLine("{0}", 123);
Console.WriteLine("{0:D3}", 123);
}
}
这是输出
123
123
123
我们使用 String.Format
直接也可以获得完全相同的结果
string s = string.Format("123");
string t = string.Format("{0}", 123);
string u = string.Format("{0:D3}", 123);
Console.WriteLine(s);
Console.WriteLine(t);
Console.WriteLine(u);
因此:
- 逗号
(,M)
确定字段宽度和对齐方式。 - 冒号
(:formatString)
确定如何格式化数据——例如货币、科学记数法或十六进制——如下所示Console.WriteLine("{0,5} {1,5}", 123, 456); // Right-aligned Console.WriteLine("{0,-5} {1,-5}", 123, 456); // Left-aligned Console.WriteLine("{0,-10:D6} {1,-10:D6}", 123, 456);
输出是:123 456 123 456
当然,您可以将它们组合起来——先放逗号,然后放冒号
Console.WriteLine("{0,-10:D6} {1,-10:D6}", 123, 456);
这是输出
000123 000456
我们可以使用这些格式化功能以适当的对齐方式输出数据到列中——例如
Console.WriteLine("\n{0,-10}{1,-3}", "Name","Salary");
Console.WriteLine("----------------");
Console.WriteLine("{0,-10}{1,6}", "Bill", 123456);
Console.WriteLine("{0,-10}{1,6}", "Polly", 7890);
这是输出
Name Salary
----------------
Bill 123456
Polly 7890
格式说明符
标准数字格式字符串用于以常用格式返回字符串。它们的格式为X
0,其中 X
是格式说明符,0 是精度说明符。格式说明符可以是九个内置格式字符之一,它们定义了最常用的数字格式类型,如表 10-1 所示。表 10-1 - String 和 WriteLine 格式说明符
字符 |
解释 |
C 或 c |
货币 |
D 或 d |
十进制(十进制整数——不要与 .NET |
E 或 e |
指数 |
F 或 f |
定点 |
G 或 g |
左键单击 gadget 并拖动以移动它。左键单击 gadget 的右下角并拖动以调整其大小。右键单击 gadget 以访问其属性。 |
N 或 n |
货币 |
P 或 p |
百分比 |
R 或 r |
往返(仅适用于浮点值);保证数值转换为字符串后可以解析回相同的数值 |
X 或 x |
十六进制 |
让我们看看当我们将整数值格式化为字符串时,使用每个格式说明符会发生什么。以下代码中的注释显示了输出。
public class FormatSpecApp
{
public static void Main(string[] args)
{
int i = 123456;
Console.WriteLine("{0:C}", i); // £123,456.00
Console.WriteLine("{0:D}", i); // 123456
Console.WriteLine("{0:E}", i); // 1.234560E+005
Console.WriteLine("{0:F}", i); // 123456.00
Console.WriteLine("{0:G}", i); // 123456
Console.WriteLine("{0:N}", i); // 123,456.00
Console.WriteLine("{0:P}", i); // 12,345,600.00 %
Console.WriteLine("{0:X}", i); // 1E240
}
}
精度说明符控制小数点右边的有效数字或零的个数
Console.WriteLine("{0:C5}", i); // £123,456.00000
Console.WriteLine("{0:D5}", i); // 123456
Console.WriteLine("{0:E5}", i); // 1.23456E+005
Console.WriteLine("{0:F5}", i); // 123456.00000
Console.WriteLine("{0:G5}", i); // 1.23456E5
Console.WriteLine("{0:N5}", i); // 123,456.00000
Console.WriteLine("{0:P5}", i); // 12,345,600.00000 %
Console.WriteLine("{0:X5}", i); // 1E240
R(往返)格式仅适用于浮点值:该值首先使用通用格式进行测试,对于 Double 为 15 位精度,对于 Single 为 7 位精度。如果该值成功解析回相同的数值,则使用通用格式说明符进行格式化。另一方面,如果该值未能成功解析回相同的数值,则使用 17 位精度(对于 Double)或 9 位精度(对于 Single)进行格式化。尽管可以将精度说明符附加到往返格式说明符,但它会被忽略。
double d = 1.2345678901234567890;
Console.WriteLine("Floating-Point:\t{0:F16}", d); // 1.2345678901234600
Console.WriteLine("Roundtrip:\t{0:R16}", d); // 1.2345678901234567
如果标准格式说明符不够用,您可以使用图片格式字符串来创建自定义字符串输出。图片格式定义使用占位符字符串来标识使用的数字的最小和最大位数、负号的位置或外观,以及数字中任何其他文本的外观,如表 10-2 所示。
表 10-2 - 自定义格式说明符
格式字符 |
目的 |
描述 |
0 |
显示零占位符 |
如果数字的位数少于格式中的零位数,则显示无意义的零 |
# |
显示数字占位符 |
仅用数字替换井号(#) |
. |
小数点 |
显示句点(.) |
, |
分组分隔符 |
分隔数字组,例如 1,000 |
% |
百分比表示法 |
显示百分号(%) |
E+0 |
指数表示法 |
格式化指数表示法的输出 |
\ |
文字字符 |
与传统格式化序列一起使用,例如“\n”(换行符) |
'ABC' |
文字字符串 |
字面量显示引号或撇号内的任何字符串 |
; |
节分隔符 |
指定数字值是要格式化的正数、负数还是零时的不同输出 |
让我们看看使用一组自定义格式生成字符串的结果,首先使用正整数,然后使用该整数的负值,最后使用零
int i = 123456;
Console.WriteLine();
Console.WriteLine("{0:#0}", i); // 123456
Console.WriteLine("{0:#0;(#0)}", i); // 123456
Console.WriteLine("{0:#0;(#0);<zero>}", i); // 123456
Console.WriteLine("{0:#%}", i); // 12345600%
i = -123456;
Console.WriteLine();
Console.WriteLine("{0:#0}", i); // -123456
Console.WriteLine("{0:#0;(#0)}", i); // (123456)
Console.WriteLine("{0:#0;(#0);<zero>}", i); // (123456)
Console.WriteLine("{0:#%}", i); // -12345600%
i = 0;
Console.WriteLine();
Console.WriteLine("{0:#0}", i); // 0
Console.WriteLine("{0:#0;(#0)}", i); // 0
Console.WriteLine("{0:#0;(#0);<zero>}", i); // <zero>
Console.WriteLine("{0:#%}", i); // %
对象和 ToString
回想一下,在 .NET Framework 中,所有数据类型——预定义和用户定义的——都继承自System.Object
类,它被别名为 objectpublic class Thing
{
public int i = 2;
public int j = 3;
}
public class objectTypeApp
{
public static void Main()
{
object a;
a = 1;
Console.WriteLine(a);
Console.WriteLine(a.ToString());
Console.WriteLine(a.GetType());
Console.WriteLine();
Thing b = new Thing();
Console.WriteLine(b);
Console.WriteLine(b.ToString());
Console.WriteLine(b.GetType());
}
}
这是输出
1
1
System.Int32
objectType.Thing
objectType.Thing
objectType.Thing
从上述代码可以看出,语句
Console.WriteLine(a);
等同于Console.WriteLine(a.ToString());
之所以等同,是因为 Int32
类型重写了 ToString
方法以生成数值的字符串表示。然而,默认情况下,ToString
将返回对象类型的名称——与 GetType
相同,即一个由包含的命名空间或命名空间和类名组成的名称。当我们调用我们 Thing 引用上的 ToString
时,这种等同性就很明显了。我们可以——也应该——为任何非平凡的用户定义类型重写继承的 ToString
。
public class Thing
{
public int i = 2;
public int j = 3;
override public string ToString()
{
return String.Format("i = {0}, j = {1}", i, j);
}
}
此修改后代码的相关输出是
i = 2, j = 3
i = 2, j = 3
objectType.Thing
数字字符串解析
所有基本类型都有一个ToString
方法,它继承自 Object
类型,并且所有数值类型都有一个 Parse
方法,该方法接受数字的字符串表示形式并返回其等效的数值。例如public class NumParsingApp
{
public static void Main(string[] args)
{
int i = int.Parse("12345");
Console.WriteLine("i = {0}", i);
int j = Int32.Parse("12345");
Console.WriteLine("j = {0}", j);
double d = Double.Parse("1.2345E+6");
Console.WriteLine("d = {0:F}", d);
string s = i.ToString();
Console.WriteLine("s = {0}", s);
}
}
此应用程序的输出在此处显示
i = 12345
j = 12345
d = 1234500.00
s = 12345
默认情况下,输入字符串中的某些非数字字符是被允许的,包括前导和尾随空格、逗号和小数点,以及加号和减号。因此,以下 Parse
语句是等效的
string t = " -1,234,567.890 ";
//double g = double.Parse(t); // Same thing
double g = double.Parse(t,
NumberStyles.AllowLeadingSign ¦
NumberStyles.AllowDecimalPoint ¦
NumberStyles.AllowThousands ¦
NumberStyles.AllowLeadingWhite ¦
NumberStyles.AllowTrailingWhite);
Console.WriteLine("g = {0:F}", g);
此附加代码块的输出如下所示
g = -1234567.89
请注意,要使用 NumberStyles
,您必须添加对 System.Globalization
的 using 语句。然后,您可以使用各种 NumberStyles
枚举值的组合,或者使用 NumberStyles.Any
来包含所有这些。如果您还想支持货币符号,则需要第三个 Parse
重载,它接受一个 NumberFormatInfo
对象作为参数。然后,您在将 NumberFormatInfo
对象作为第三个参数传递给 Parse
之前,将其 CurrencySymbol
字段设置为预期的符号,Parse
会修改 Parse
的行为。
string u = "£ -1,234,567.890 ";
NumberFormatInfo ni = new NumberFormatInfo();
ni.CurrencySymbol = "£";
double h = Double.Parse(u, NumberStyles.Any, ni);
Console.WriteLine("h = {0:F}", h);
此附加代码块的输出在此处显示
h = -1234567.89
除了 NumberFormatInf
o,我们还可以使用 CultureInfo
类。CultureInfo
表示关于特定区域性的信息,包括区域性的名称、书写系统和日历,以及访问提供常见操作(如格式化日期和排序字符串)方法的特定于区域性的对象。区域性名称遵循 RFC 1766 标准,格式为 <languagecode2>-<country/regioncode2>
,其中 <languagecode2>
是源自 ISO 639-1 的小写两位字母代码,<country/regioncode2>
是源自 ISO 3166 的大写两位字母代码。例如,美国英语是“en-US”,英国英语是“en-GB”,特立尼达和多巴哥英语是“en-TT”。例如,我们可以为美国英语创建一个 CultureInfo
对象,并根据此 CultureInfo
将整数值转换为字符串
int k = 12345;
CultureInfo us = new CultureInfo("en-US");
string v = k.ToString("c", us);
Console.WriteLine(v);
此示例将生成如下字符串
$12,345.00
请注意,我们使用的是一个 ToString
重载,它将格式字符串作为第一个参数,并将 IFormatProvider
接口实现(在本例中为 CultureInfo
引用)作为第二个参数。这是另一个例子,这次是针对丹麦的丹麦语
CultureInfo dk = new CultureInfo("da-DK");
string w = k.ToString("c", dk);
Console.WriteLine(w);
输出是:
kr 12.345,00
字符串和 DateTime
DateTime
对象有一个名为 Ticks
的属性,它将日期和时间存储为自公元 1 年 1 月 1 日午夜(格里高利历)以来的 100 纳秒间隔数。例如,刻度值为 31241376000000000L 的字符串表示为“Friday, January 01, 0100 12:00:00 AM”。每增加一个刻度,时间间隔增加 100 纳秒。DateTime
值使用存储在 DateTimeFormatInfo
实例属性中的标准或自定义模式进行格式化。要修改值的显示方式,必须使 DateTimeFormatInfo
实例可写,以便可以将自定义模式保存在其属性中。
using System.Globalization;
public class DatesApp
{
public static void Main(string[] args)
{
DateTime dt = DateTime.Now;
Console.WriteLine(dt);
Console.WriteLine("date = {0}, time = {1}\n",
dt.Date, dt.TimeOfDay);
}
}
此代码将产生以下输出
23/06/2001 17:55:10
date = 23/06/2001 00:00:00, time = 17:55:10.3839296
表 10-3 列出了每个标准模式的标准格式字符以及可以设置以修改标准模式的关联 DateTimeFormatInfo
属性。
表 10-3 - DateTime 格式化
格式字符 |
格式模式 |
关联属性/描述 |
D |
MM/dd/yyyy |
ShortDataPattern |
D |
dddd,MMMM dd,yyyy |
LongDatePattern |
F |
dddd,MMMM dd,yyyy HH:mm |
完整日期和时间(长日期和短时间) |
F |
dddd,MMMM dd,yyyy HH:mm:ss |
FullDateTimePattern (长日期和长时间) |
G |
MM/dd/yyyy HH:mm |
通用(短日期和短时间) |
G |
MM/dd/yyyy HH:mm:ss |
通用(短日期和长时间) |
M,M |
MMMM dd |
MonthDayPattern |
r,R |
ddd,dd MMM yyyy,HH':'mm':'ss 'GMT' |
RFC1123Pattern |
S |
yyyy-MM-dd HH:mm:ss |
SortableDateTimePattern(符合 ISO 8601),使用本地时间 |
T |
HH:mm |
ShortTimePattern |
T |
HH:mm:ss |
LongTimePattern |
U |
yyyy-MM-dd HH:mm:ss |
UniversalSortableDateTimePattern(符合 ISO 8601),使用通用时间 |
U |
dddd,MMMM dd,yyyy,HH:mm:ss |
UniversalSortableDateTimePattern |
y,Y |
MMMM,yyyy |
YearMonthPattern |
DateTimeFormatInfo.InvariantInfo
属性获取独立于区域性的(不变的)默认只读 DateTimeFormatInfo
实例。您也可以创建自定义模式。请注意,InvariantInfo
不一定与当前区域性信息相同:Invariant 等同于美国标准。此外,如果将 null 作为第二个参数传递给 DateTime.Format
,DateTimeFormatInfo
将默认为 CurrentInfo
,如
Console.WriteLine(dt.ToString("d", dtfi));
Console.WriteLine(dt.ToString("d", null));
Console.WriteLine();
这是输出
06/23/2001
23/06/2001
比较选择 InvariantInfo
与选择 CurrentInf
o 的结果
DateTimeFormatInfo dtfi;
Console.Write("[I]nvariant or [C]urrent Info?: ");
if (Console.Read() == 'I')
dtfi = DateTimeFormatInfo.InvariantInfo;
else
dtfi = DateTimeFormatInfo.CurrentInfo;
DateTimeFormatInfo dtfi = DateTimeFormatInfo.InvariantInfo;
Console.WriteLine(dt.ToString("D", dtfi));
Console.WriteLine(dt.ToString("f", dtfi));
Console.WriteLine(dt.ToString("F", dtfi));
Console.WriteLine(dt.ToString("g", dtfi));
Console.WriteLine(dt.ToString("G", dtfi));
Console.WriteLine(dt.ToString("m", dtfi));
Console.WriteLine(dt.ToString("r", dtfi));
Console.WriteLine(dt.ToString("s", dtfi));
Console.WriteLine(dt.ToString("t", dtfi));
Console.WriteLine(dt.ToString("T", dtfi));
Console.WriteLine(dt.ToString("u", dtfi));
Console.WriteLine(dt.ToString("U", dtfi));
Console.WriteLine(dt.ToString("d", dtfi));
Console.WriteLine(dt.ToString("y", dtfi));
Console.WriteLine(dt.ToString("dd-MMM-yy", dtfi));
这是输出
[I]nvariant or [C]urrent Info?: I
01/03/2002
03/01/2002
Thursday, 03 January 2002
Thursday, 03 January 2002 12:55
Thursday, 03 January 2002 12:55:03
01/03/2002 12:55
01/03/2002 12:55:03
January 03
Thu, 03 Jan 2002 12:55:03 GMT
2002-01-03T12:55:03
12:55
12:55:03
2002-01-03 12:55:03Z
Thursday, 03 January 2002 12:55:03
01/03/2002
2002 January
03-Jan-02
[I]nvariant or [C]urrent Info?: C
03/01/2002
03/01/2002
03 January 2002
03 January 2002 12:55
03 January 2002 12:55:47
03/01/2002 12:55
03/01/2002 12:55:47
03 January
Thu, 03 Jan 2002 12:55:47 GMT
2002-01-03T12:55:47
12:55
12:55:47
2002-01-03 12:55:47Z
03 January 2002 12:55:47
03/01/2002
January 2002
03-Jan-02
编码字符串
System.Text
命名空间提供了一个 Encoding 类。Encoding 是一个抽象类,因此您不能直接实例化它。但是,它提供了一系列方法和属性,用于将 Unicode 字符的数组和字符串转换为目标代码页编码的字节数组,以及从字节数组转换回来。这些属性实际上解析为返回 Encoding 类的实现。表 10-4 显示了其中一些属性。表 10-4 - 字符串编码类
属性 |
编码 |
ASCII |
将 Unicode 字符编码为单个 7 位 ASCII 字符。此编码仅支持 U+0000 到 U+007F 之间的字符值 |
BigEndianUnicode |
将每个 Unicode 字符编码为两个连续字节,使用大端(代码页 1201)字节序。 |
Unicode |
将每个 Unicode 字符编码为两个连续字节,使用小端(代码页 1200)字节序。 |
UTF7 |
使用 UTF-7 编码对 Unicode 字符进行编码。(UTF-7 代表 UCS Transformation Format, 7-bit form。)此编码支持所有 Unicode 字符值,并可作为代码页 65000 访问。 |
UTF8 |
使用 UTF-8 编码对 Unicode 字符进行编码。(UTF-8 代表 UCS Transformation Format, 8-bit form。)此编码支持所有 Unicode 字符值,并可作为代码页 65001 访问。 |
例如,您可以将一个简单的字节序列转换为常规的 ASCII 字符串,如下所示
class StringEncodingApp
{
static void Main(string[] args)
{
byte[] ba = new byte[]
{72, 101, 108, 108, 111};
string s = Encoding.ASCII.GetString(ba);
Console.WriteLine(s);
}
}
这是输出
Hello
如果您想转换为 ASCII 以外的内容,只需使用其他 Encoding
属性之一。以下示例与前面的示例具有相同的输出
byte[] bb = new byte[]
{0,72, 0,101, 0,108, 0,108, 0,111};
string t = Encoding.BigEndianUnicode.GetString(bb);
Console.WriteLine(t);
System.Text
命名空间还包含几个从抽象 Encoding
类派生(因此实现)的类。这些类提供了与 Encoding
类本身中的属性类似的行为
- ASCIIEncoding
- UnicodeEncoding
- UTF7Encoding
- UTF8Encoding
使用以下代码可以达到与上一个示例相同的效果
ASCIIEncoding ae = new ASCIIEncoding();
Console.WriteLine(ae.GetString(ba));
UnicodeEncoding bu =
new UnicodeEncoding(true, false);
Console.WriteLine(bu.GetString(bb));
StringBuilder 类
回想一下,对于String
类,看起来修改字符串的方法实际上会返回一个包含修改的新字符串。这种行为有时很麻烦,因为如果您对字符串进行了多次修改,最终会处理原始字符串的多个副本。出于这个原因,Redmond 的开发人员在 System.Text
命名空间中提供了 StringBuilder
类。考虑这个示例,使用 StringBuilder
的 Replace
、Insert
、Append
、AppendFormat
和 Remove
方法
class UseSBApp
{
static void Main(string[] args)
{
StringBuilder sb = new StringBuilder("Pineapple");
sb.Replace('e', 'X');
sb.Insert(4, "Banana");
sb.Append("Kiwi");
sb.AppendFormat(", {0}:{1}", 123, 45.6789);
sb.Remove(sb.Length - 3, 3);
Console.WriteLine(sb);
}
}
这是输出
PinXBananaapplXKiwi, 123:45.6
请注意,与大多数其他类型一样,您可以轻松地将 StringBuilder
转换为 String
string s = sb.ToString().ToUpper();
Console.WriteLine(s);
这是输出
PINXBANANAAPPLXKIWI, 123:45.6
拆分字符串
String
类确实提供了一个 Split 方法,用于将字符串拆分为子字符串,拆分由您提供给方法的任意分隔符字符决定。例如class SplitStringApp
{
static void Main(string[] args)
{
string s = "Once Upon A Time In America";
char[] seps = new char[]{' '};
foreach (string ss in s.Split(seps))
Console.WriteLine(ss);
}
}
输出如下
Once
Upon
A
Time
In
America
String.Split
的 separators 参数是一个 char 数组;因此,我们可以根据多个分隔符拆分字符串。但是,我们必须小心处理反斜杠(\)和单引号(')等特殊字符。以下代码产生的输出与上一个示例相同
string t = "Once,Upon:A/Time\\In\'America";
char[] sep2 = new char[]{ ' ', ',', ':', '/', '\\', '\''};
foreach (string ss in t.Split(sep2))
Console.WriteLine(ss);
请注意,如果我们要按多个字符的实例分隔子字符串,Split
方法非常简单且不太有用。例如,如果我们的字符串中的任何单词之间有一个以上的空格,我们将得到以下结果
string u = "Once Upon A Time In America";
char[] sep3 = new char[]{' '};
foreach (string ss in u.Split(sep3))
Console.WriteLine(ss);
这是输出
Once
Upon
A
Time
In
America
在这一系列文章的第二篇文章中,我们将讨论 .NET Framework 中的正则表达式类,并了解如何解决这个特定问题以及许多其他问题。
扩展字符串
在 .NET 时代之前的库中,将库中现有的String
类扩展为增强功能已成为一种常见的做法。不幸的是,.NET Framework 中的 String
类是密封的;因此,您不能从中派生。另一方面,提供一系列封装的静态方法来处理字符串是完全可能的。例如,String
类确实提供了 ToUpper
和 ToLower
方法,分别用于转换为大写或小写,但该类不提供将单词转换为首字母大写的方法。提供这种功能很简单,如下所示public class StringEx
{
public static string ProperCase(string s)
{
s = s.ToLower();
string sProper = "";
char[] seps = new char[]{' '};
foreach (string ss in s.Split(seps))
{
sProper += char.ToUpper(ss[0]);
sProper +=
(ss.Substring(1, ss.Length - 1) + ' ');
}
return sProper;
}
}
class StringExApp
{
static void Main(string[] args)
{
string s = "the qUEEn wAs in HER parLOr";
Console.WriteLine("Initial String:\t{0}", s);
string t = StringEx.ProperCase(s);
Console.WriteLine("ProperCase:\t{0}", t);
}
}
这将产生此处显示的输出。(在本系列文章的第二部分中,我们将了解如何使用正则表达式实现相同的结果。)
Initial String: the qUEEn wAs in HER parLOr
ProperCase: The Queen Was In Her Parlor
另一个经典的常见操作是回文串测试——即一个字符串正反读起来都一样的字符串
public static bool IsPalindrome(string s)
{
int iLength, iHalfLen;
iLength = s.Length - 1;
iHalfLen = iLength / 2;
for (int i = 0; i <= iHalfLen; i++)
{
if (s.Substring(i, 1) !=
s.Substring(iLength - i, 1))
{
return false;
}
}
return true;
}
static void Main(string[] args)
{
Console.WriteLine("\nPalindromes?");
string[] sa = new string[]{
"level", "minim", "radar",
"foobar", "rotor", "banana"};
foreach (string v in sa)
Console.WriteLine("{0}\t{1}",
v, StringEx.IsPalindrome(v));
}
这是输出
Palindromes?
level True
minim True
radar True
foobar False
rotor True
banana False
对于更复杂的操作——例如条件拆分或连接、扩展解析或标记化,以及 String
类不提供您想要的强大功能的复杂修剪——您可以求助于 Regex
类。这正是我们将在本文的后续文章中要介绍的内容
字符串驻留
字符串被设计为不可变的原因之一是这种安排允许系统对其进行驻留。在字符串驻留过程中,应用程序中的所有常量字符串都存储在内存中的公共位置,从而消除了不必要的重复。这种做法显然节省了运行时空间,但可能会让不了解的人感到困惑。例如,回想一下,相等运算符 (==) 会测试值类型的相等性,以及引用类型的地址(或引用)相等性。因此,在以下应用程序中,当我们比较具有相同内容的同一类的两个引用类型对象时,结果为False
。但是,当我们比较内容相同的两个字符串对象时,结果为 True
class StringInterningApp
{
public class Thing
{
private int i;
public Thing(int i) { this.i = i; }
}
static void Main(string[] args)
{
Thing t1 = new Thing(123);
Thing t2 = new Thing(123);
Console.WriteLine(t1 == t2); // False
string a = "Hello";
string b = "Hello";
Console.WriteLine(a == b); // True
}
}
好吧,但这两个字符串实际上都是常量或字面量。假设我们有另一个是变量的字符串?同样,给定相同的内容,字符串相等运算符将返回 True
string c = String.Copy(a);
Console.WriteLine(a == c); // True
现在,假设我们强制运行时系统将这两个字符串视为对象,而不是字符串,因此使用最基本的引用类型相等运算符。这次我们得到 False
Console.WriteLine((object)a == (object)c);
是时候查看底层的 Microsoft 中间语言 (MSIL) 了,如图 10-1 所示。
图 10-1 - 字符串相等和对象相等的 MSIL。
关键区别如下:对于第一次比较 (t1==t2)
,将两个 Thing
对象引用加载到求值堆栈后,MSIL 使用操作码 ceq
(比较相等),从而明显地比较了引用或地址值。但是,当我们将两个字符串加载到堆栈上进行比较时,ldstr
,第二次比较 (a==b)
的 MSIL 是一个调用操作。我们不仅仅是比较堆栈上的值;相反,我们调用 String
类的相等运算符方法 op_Equality
。对于第三次比较 (a==c)
,也会发生同样的过程。对于第四次比较 (object)a==(object)c
,我们又回到了 ceq
。换句话说,我们比较堆栈上的值——在本例中是两个字符串的地址。
请注意,Inside C# 的**第 13 章** 确切地说明了 String
类如何通过运算符重载拥有自己的相等运算符方法。目前,了解系统处理字符串的方式与其他引用类型不同就足够了。
如果我们比较两个原始字符串常量并强制使用最原始的相等运算符会怎样?看看
Console.WriteLine((object)a == (object)b);
您会发现此输出为 True
。最终证明系统正在驻留字符串——使用的 MSIL 操作码再次是 ceq,但这次它导致相等,因为两个字符串被分配了一个只存储一次的常量字面量值。事实上,通用语言基础结构保证,两个 ldstr
指令引用具有相同字符序列的两个元数据令牌会返回完全相同的字符串对象。
摘要
在本文中,我们探讨了String
类以及一系列支持字符串操作的辅助类。我们研究了 String
类方法在搜索、排序、拆分、连接和以其他方式返回修改后的字符串中的使用。我们还看到了 .NET Framework 中的许多其他类如何支持字符串处理——包括 Console、基本数值类型和 DateTime——以及区域性信息和字符编码如何影响字符串格式化。最后,我们看到了系统如何执行隐蔽的字符串驻留以提高运行时效率。在下一篇文章中,您将发现 Regex
类及其支持类——Match
、Group
和 Capture
——用于封装正则表达式。