25.1 概述
在本章中,我会介绍使用C#时的一些重要而不适合放到其他章节的其他主题。它们包括字符串操作、可空类型、Main方法、文档注释以及嵌套类型。
25.2 字符串
BCL提供了很多能让字符串操作变得更简单的类。
C#预定义的string类型代表了.NET的System.String类。对于字符串,最需要理解的概念如下:
- 字符串是Unicode字符串数组。
- 字符串是不可变的(immutable)——它们不能被修改。
string类有很多有用的字符串操作成员,包括允许我们进行诸如检测长度、改变大小写、连接字符串等一些有用的操作。下表列出了其中一些最有用的成员。
string类型的有用成员 成员 类型 意义 Length 属性 返回字符串长度 Concat 静态方法 返回连接参数字符串后的字符串 Contains 方法 返回指示参数是否是对象字符串的子字符串的bool值 Format 静态方法 返回格式化后的字符串 Remove 方法 从对象字符串中移除一组字符 Replace 方法 替换对象字符串中的字符 SubString 方法 获取对象字符串的子字符串 ToUpper 方法 返回对象字符串的副本,其中所有字母的字符都为大写 ToLower 方法 返回对象字符串的副本,其中所有字母的字符都为小写
从上表中的大多数方法的名字来看,好像它们都会改变字符串对象。其实,它们不会改变字符串而是返回了新的副本。对于一个string,任何“改变”都会分配一个新的恒定字符串。
使用StringBuilder类
StringBuilder类可以产生能被修改的字符串。
- StringBuilder类是BCL的成员,位于System.Text命名空间中。
- StringBuilder对象是Unicode字符的可变数组。
当创建了StringBuilder对象之后,类分配了一个比当前字符串长度更长的缓冲区。只要缓冲区能容纳对字符串的改变就不会分配新的内存。如果对字符串的改变需要的空间比缓冲中的可用空间多,就会分配更大的缓冲区,并把字符串拷贝到其中。和原来的缓冲区一样,新的缓冲区也有额外的空间。
要获取StringBuilder对应的这符串内容,我们只需要简单调用它的ToString方法即可。
格式化数字字符串
例:
Console.WriteLine("The value:{0}.",500); //输出数字
Console.WriteLine("The value:{0:C}.",500);//格式为货币
两条语句的不同之处在于,格式项以格式说明符形式包括了额外的信息。大括号内的格式化说明符的语法由三个字符组成:索引号、对齐说明符和格式说明符。语法如下:
格式项首先需要索引号。现在我们已经知道,索引指定了之后的格式化字符串应该格式化列表中的哪一项。索引号是必需的,并且列表项的数字必须从0开始。
1.对齐说明符
对齐说明符表示了字段中字符的最小宽度。对齐说明符有如下特性:
- 它是可选的,并且用逗事情来和索引号分离。
- 它由一个正整数或负整数组成。
- 整数表示了字段使用字符的最少数量。
- 符号表示了右对齐或左对齐。正数表示右对齐,负数表示左对齐。
例: Consoloe.WriteLine("{0,10}",500);
值的实际表示可能会比对齐说明符指定的字符数多一些或少一些:
- 如果要表示的字符数比对齐说明符中指定的字符数少,那么其余字符会使用空格填充。
- 如果要表示的字符数多于指定的字符数,对齐说明符会被忽略,并且使用所有字符进行表示。
2.格式组件
格式组件指定了数字应该以哪种形式表示。命名旭,应该被当作货币、十进制数字、十六进制数字还是定点符号来表示?
格式组件有两部分,如下图所示:
- 格式说明符是一个字母的字符,是9个内置字符格式之一。字符可以是大写或小写形式。大小写对于某些说明符来说比较重要,而对于另一些说明符来说则不重要。
- 精度说明符是可选的,由1-2位数字组成。它的实际意义取决于格式说明符。
如下代码是格式字符串组件语法的一个示例:
Console.WriteLine("{0:F4}",12.345678) //F4=> 格式组件——四位小数的定点数。
3.标准数字格式说明符
Windows控制面板的区域和语言选项程序会影响一些说明符的最终格式。例如,指定国家或区域的货币符号会被货币格式说明符所使用。
标准数字格式说明符 名字和字符 意义 货币 C、c 使用货币符号把值格式化为货币。
精度说明符:小数位数十进制 D、d 十进制数字字符串,需要的情况下有负数符号。只能和整数类型配合使用。
精度说明符:输出字符串中的最少位数。如果实际数字的位数更少则在左边以0填充。
例:Console.WriteLine("{0:D4}",12); 输出:0012定点 F、f 带有小数点的十进制数字字符串。如果需要也可以有负数符号。
精度说明符:小数的位数
精度说明符:根据值常规 G、g 在没有指定说明符的情况下,会根据值转会根据值转换为定点或科学记数法表示的紧凑形式。
精度说明符:根据值十六制数X、x 十六进制数字的字符串。十六进制数字A-F会匹配说明符的大小写形式。
精度说明符:输出字符串中的最少位数。如果实际数的位数更少,则在左边以0填充。
示例:Console.WriteLine("{0:x}",180026); 输出:2bf3a数字 N、n 和定点表示法相似,但是在每三个数字的一组中间有分隔符。从小数点开始往左数
精度说明符:小数的位数百分比 P、p 表示百分比的字符串。数字会乘以100
精度说明符:小数的位数往返过程R、f 保证输出字符串后如果使用Parse方法将字符串转化为数字,那么该值和原始值一样。
精度说明符:忽略
示例:Console.WriteLine("{0:R}",1234.21897); 输出:1234.21897科学记数法E、e
区分大小写具有尾数和指数的科学记数法。指数前面加字母E。E的大小写和会一致。
精度说明符:小数的位数。
示例: console.WriteLine("{0:e4}",12.3456789); 输出:1.2346e+001
25.3 把字符串解析为数据值
字符串都是Unicode字符的数组。例如,字符串"25.873"是6个字符而不是一个数字。尽管它看上去像数字,但是我们不能对它使用数学函数。把两个字符串进行“相加”只会串联他们。
- 解析允许我们接受表示值的字符串,并且把它转换为实际值。
- 所有预定义的简单类型都有一个叫做Parse的静态方法,它接受一个表示这个类型的字符串值,并且把它转换为类型的实际值。
以下语句给出一个使用Parse方法语法的示例。注意,Parse是静态4 ,所以我们需要通过目标类型名为调用它。
double d1 = double.Parse("25.873");
如果字符串不能被解析,系统会抛出一个异常。还有另外一个静态方法TryParse,如果是字符串解析成功,则返回true,其余情况返回false。如果解析失败,它不会抛出异常。
说明:关于Parse有一个常见的误解,由于它是在操作字符串,会被认为是string类的成员。其实不是,Parse不是一个方法,而是由目标类型实现的很多个方法。
25.4 可空类型
在某些情况下,特别是在使用数据库时,我们可能会希望表示一个当前没有保存有效值的变量。对于引用类型,这很容易实现,只要把变量设置为null就可以了。然而,如果我们的变量定义为值类型,不管它的内容是否是有效的含义,内存都已经分配了。
在这种情况下,我们需要一个关联到变量的布尔指示器,当值是有效时,指示器是true;而当值是无效时,指示器为false。
C#2.0引入的可空类型允许我们创建一个值类型的变量并标注它为有效或无效。这样,我们就可以在使用它之前确保变量是有效的。普通值类型称作不可空(non-nullable)类型。
创建可空类型
可空类型总是基于一个叫做基础类型(underlying type)的已经被声明的类型。
- 可以从任何值类型创建可空类型,包括预定义的简单类型。
- 不能从引用类型或其他可空类型创建可空类型。
- 不能在代码中显式声明可空类型,只能声明可空类型的变量。之后我们会看到,编译器会使用泛型隐式地创建可空类型。
要创建可空类型的变量,只需要在变量声明中的基础类型的名字后面加一个问号。不幸的是,这种语法看上去好像你的代码有很多问题一样。
例如,以下代码声明了一个可空int类型的变量。注意,后缀附加到类型名——而不是变名称。
int? myNInt = 28;
有了这样的声明语句,编译器就会产生可空类型并关联变量类型。可空类型的结构如图25-3所示。它包含如下:
- 基础类型的实例。
- 几个重要的只读属性。
- HasValue属性是bool类型,并且指示值是否有效。
- Value属性是和基础类型相同的类型并且返回变量的值——如果变量是有效的话。
我们可以按如下方式显式使用两个只读属性:
int? myInt1 = 15;
if (MyInt1.HasValue)
Console.WriteLine("{0}",myInt1.value);
然而,更好的方式是使用快捷形式,如以下代码所示。
- 要检测可空类型是否有值,我们可以把它和null比较。
- 和其他变量一样,要获取它的值,可以只用它的名字。
if (myInt1 !=null)Console.WriteLine("{0}",myInt1);
读取可空类型的变量会返回它的值。然而,你必须确保变量不是null。尝试读取null值的变量会产生一个异常。
我们可以简单地在可空类型和它对应的不可空类型之间转换。
- 不可空类型到它的可空形式之间有隐式转换。也就是说,不需要强制转换。
- 可空类型到它的不可空形式之间可以显式转换。
为可空类型赋值
有三种方式可以为可空类型的变量赋值
- 基础类型的值
- 同样可空类型的值
- 值null
使用运算符以及空接合运算符
标准算术运算符和比较运算符同样也能处理可空类型。还有一个新的运算符叫做空接合运算符 (null coalescing operator),它允许我们在可空类型变量为null时返回一个值给表达式。
空接合运算符由两个连续的问号(??)组成,它有两个操作数:
- 第一个操作数是可空类型的变量。
- 第二个是相同基础类型的不可空值。
- 在运行时,如果第一个操作数运算后为null,那么第二个操作数就会被返回作为运算结果。
对于相等比较运算符==和!=,我们需要知道其中的一个有趣的特性。如果你比较两个相同可空类型的值,并且都设置为null,那么相等比较运算符会认为它们是相等的。
使用可空用户自定义类型
至此,我们已经看到了预定义的简单类型的可空形式。我们还可以创建用户自定义值类型的可空形式。这就引出了在使用简单类型时候没有遇到的其他问题。
主要问题是访问封装的基础类型的成员。一个可空类型不直接暴露基础类型的任何成员。例如,来看看下面的代码和图中它的表示形式。代码声明了一个叫做MyStruct的结构(值类型),它有两个公共字段。
- 由于结构的字段是公共的,所以它可以被结构的任何实例所访问到,如图左部分所示。
- 然而,结构的可空形式只通过Value属性暴露基础类型,它不直接暴露它的任何成员。尽管这些成员对结构来说是公共的,但是它们对要空类型来说不是公共的。
struct MyStruct { public int X; public int Y; public MyStruct(int xVal, int yVal) { X = xVal; Y = yVal; } } class Program { static void Main(string[] args) { MyStruct? mSNull = new MyStruct(5, 10); } }
例如,以下代码使用之前声明的结构并创建了结构和它对应的可空类型的变量。在代码的第三行和第四行中,我们直接读取结构变量的值。在第五行和第六行中,就必须从可空类型的Value属性返回的值中进行读取。
MyStruct mSStruct = new MyStruct(6, 11); //结构变量 MyStruct? mSNull = new MyStruct(5, 10); //可空类型的变量 Console.WriteLine("mSStruct.X:{0}",mSStruct.X); //结构类型访问 Console.WriteLine("mSStruct.Y:{0}", mSStruct.Y); Console.WriteLine("mSNull.X:{0}", mSNull.Value.X); //可空类型访问 Console.WriteLine("mSNull.Y:{0}", mSNull.Value.Y);
Nullable<T>
可空类型通过一个叫做System.Nullable<T>的.NET类型来实现,它使用了C#的泛型特性。
C#可空类型的问号语法是创建Nullable<T>类型变量的快捷语法,在这里T就是基础类型。Nullable<T>接受了基础类型并把它嵌入结构中,同时给结构提供可空类型的属性、方法和构造函数。
我们可以使用Nullable<T>这种泛型语法,也可以使用C#的快捷语法。快捷语法更容易书写和理解,并且也减少了出错的可能性。
以下代码使用Nullable<T>语法为之前示例中声明的MyStruct结构创建一个叫做mSNull的Nullable<MyStruct>类型:
Nullable<MyStruct> mSNull = new Nullable<MyStruct>(); 完全等同于 MyStruct? mSNull = new MyStruct();
25.5 Main方法
每一个C#程序都必须有一个入口点—— 一个必须叫做Main的方法。
一共有4种形式的Main可以作为程序的入口点。这些形式如下:
- static void Main {...}
- static void Main(string[] args) {...}
- static int Main() {...}
- static int Main(string[] args) {...}
前面两种形式在程序终止后都不返回值给执行环境。后面两种形式则返回int值。如果使用返回值,通常用于报告程序的成功或失败,0通常用于表示成功。第二种和第四种形式允许我们在程序启动时从命令行向程序传入实参,也叫做参数。命令行参数的一些重要特性如下:
- 可以有0个或多个命令行参数。即使没有参数,args参数也不会是null,而是一个没有元素的数组。
- 参数由空格或制表符隔开。
- 每一个参数都被程序解释为是字符串,但是你无须在命令行中为参数加上引号。
其他需要了解的有关Main的重要事项如下:
- Main必须声明为static。
- Main可以被声明为类或结构。
一个程序只可以包含Main的4种可用入口点形式中的一种声明。当然,如果你声明其他方法的名称为Main,只要它们不是4种入口点形式中的一种就是合法的——但是,这样做是大小写容易混淆的。
Main的可访问性
Main可以被声明为public或private:
- 如果Main被声明为private,其他程序集就不能访问它,只有执行环境才能启动程序。
- 如果Main被声明为public,其他程序集就可以调用它。
然而,无论Main声明的访问级或所属类或结构的访问级别是什么,执行环境总是能访问Main。默认情况下,当Visual Studio创建了一个项目时,它就创建了一个程序框,其中的Main是隐式private。如果需要,你随时可以添加public修饰符。
25.6 文档注释
文档注释特性允许我们以XML元素的形式在程序中包含文档。Visual Studio会帮助我们插入元素,以及从源文件中读取它们并拷贝到独立的XML文件中。本小节不介绍XML的主题,但是会介绍使用文档注释的大致过程。
下图给出了一个使用XML注释的概要。这包括如下步骤:
- 你可以使用Visual Studio来产生带有嵌入了XML的源文件。Visual Studio会自动插入大多数重要的XML元素。
- Visual Studio从源文件中读取XML并且拷贝XML代码到新的文件。
- 另外一个叫做文档编译器的程序可以获取XML文件并且从它产生各种类型的文件 。
之前的Visual Studio版本包含了基本的文档编译器,但是它在Visual Studio 2005发布之前被删除了。微软公司正在开发一个叫做Sandcastle的新文档编译器,它已经被用来生成.NET框架的文档。你可以从“微软开发者网络”的网站(http://msdn.microsoft.com)上下载这个软件。
插入文档注释
文档注释从三个连续的正斜杠开始。
- 前两个斜杠指示编译器这是一行注释,并且需要从程序的解析中忽略。
- 第三个斜杠指示这是一个文档注释。
使用其他XML标签
文档代码XML标签 标签 意义 <code> 格式化内部的行,用看上去像代码的字体 <example> 将内部的行标为一个示例 <param> 为方法或构造函数标注参数,并允许描述 <remarks> 描述类型的声明 <returns> 描述返回值 <seealso> 在输出文档中创建SeeAlso一项 <summary> 描述类型或类型成员 <value> 描述属性
25.7 嵌套类型
我们通常直接在命名空间中声明类型。然而,我们还可以在类或结构中声明类型。
- 在另一个类型声明中声明的类型叫做嵌套类型。和所有类型声明一样,嵌套类型是类型实例的模板。
- 嵌套类型像封闭类型(enclosing type)的成员一样声明。
- 嵌套类型可以是任意类型。
- 嵌套类型可以是类或结构。
例如,以下代码显示了MyClass类,其中有一个叫做MyCounter的嵌套类
class MyClass //封闭类{
class MyCounter //嵌套类{}
}
如果一个类型只是作为帮助方法并且只对封闭类型有意义,可能就需要声明嵌套类型了。
不要被嵌套这个术语弄混淆。嵌套指声明的位置——而不是任何实例的位置。尽管嵌套类型的声明在封闭类型的声明之内,但嵌套类型的对象并不一定封闭在封闭类型的对象之内。嵌套类型的对象——如果创建了的话——和它没有在另一个类型中声明时所在的位置一样。
class MyClass { class MyCounter //嵌套类 { private int _Count = 0; public int Count { get //只读属性 { return _Count; } } public static MyCounter operator ++(MyCounter current) { current._Count++; return current; } } private MyCounter counter; //嵌套类的字段 public MyClass() { counter = new MyCounter(); } public int Incr() //增量方法 { return (counter++).Count; } public int GetValue() //获取counter的值 { return counter.Count; } } class Program { static void Main(string[] args) { MyClass mc = new MyClass(); mc.Incr(); mc.Incr(); mc.Incr(); mc.Incr(); mc.Incr(); mc.Incr(); Console.WriteLine("Total:{0}", mc.GetValue()); } }
可见性和嵌套类型
我们已经了解到类和类型通常有public和internal的访问级别。然而,嵌套类型的不同之处在于,它们有成员访问级别而不是类型访问级别。因此,下面的命题是成立的:
- 在类内部声明的嵌套类型可以有5种类成员访问级别中的任何一种:public、protected、private、internal或protected internal。
- 在结构内部声明的嵌套类型可以有3种结构成员访问级别中的任何一种:public、internal或private。
在这两种情况下,嵌套类型的默认访问级别都是private,也就是说不能被封闭类型以外的对象所见。
封闭类和嵌套类的成员之间的关系是很容易理解的,不管封闭类型的成员声明了怎样的访问级别,包括private和protected,嵌套类型都能访问这些成员。
然而,它们之间的关系不是对称的。尽管封闭类型的成员总是可见嵌套类型的声明并且能创建它的实例,但是它们不能完全访问嵌套类型的成员。相反这种访问权限受限于嵌套类成员声明的访问级别——就好像嵌套类型是一个独立的类型一样。也就是说,它们可以访问public或internal的成员,但是不能访问嵌套类型的private或protected成员。
我们可以把这种关系总结如下:
- 嵌套类型的成员对封闭类型的成员总是有完全访问权限。
- 封闭类型的成员:
- 总是可以访问嵌套类型本身。
- 只能访问声明了有访问权限的嵌套类型成员。
嵌套类型的可见性还会影响基类成员的继承。如果封闭类型有子类,嵌套类型就可以通过使用相同的名字来隐藏基类成员。可以在嵌套类型的声明上使用new修饰符来显式隐藏。
嵌套类型中的this引用指的是嵌套类型的对象——不是封闭类型的对象。如果嵌套类型的对象需要访问封闭类型,它必须持有封闭类型的引用。
class SomeClass //封闭类 { int Field1 = 15, Field2 = 20; //封闭类的字段 MyNested mn = null; //嵌套类的引用 public void PrintMyMembers() { mn.PrintOuterMembers(); //调用嵌套类中的方法 } public SomeClass() { mn = new MyNested(this); //创建嵌套类的实例 } class MyNested //嵌套类声明 { SomeClass sc = null; //封闭类的引用 public MyNested(SomeClass SC) { sc = SC; } public void PrintOuterMembers() { Console.WriteLine("Field1:{0}",sc.Field1); Console.WriteLine("Field2:{0}",sc.Field2); } } //嵌套类结束 } class Program { static void Main(string[] args) { SomeClass MySC = new SomeClass(); MySC.PrintMyMembers(); } }