第三章 C#核心编程结构1
本章将介绍C#的一些基本语法和类型。首先介绍了所有应用程序都必须用到的应用程序对象和它的入口点函数(Main),然后介绍了一些内建数据类型,之后介绍了数据转换的相关概念,最后,介绍了核心运算符、循环和选择结构。
3.1 Main函数
默认情况下,当建立一个项目时,VS会建立一个默认的类,叫做program(可以改名),这个类含有一个静态函数:Main,这个函数就是所有可执行应用程序的入口点(也就是执行程序时,都是从这个Main开始执行的),每一个可执行的应用程序(控制台程序、winform、windows 服务)必须包含定义了Main方法的类!
定义了Main方法的类叫做应用程序对象。需要注意的是,当只有一个应用程序对象时,编译器会自动识别和默认它为入口点函数所在类;同时,是可以有多个应用程序对象的,也就是说可以有多个类含有Main,但是,这会让编译器混乱,不知道从哪个类进行启动,因此这时必须指定其中一个为入口点函数的类,否则编译都不能通过!指定的方法(一个类中如果有多个Main,显然无法通过编译,因为指定也是指定某个类,而不能具体到某个类中的具体哪个Main方法):
若用csc编译器,使用main参数指定;
若利用VS平台,则应该在项目-属性-应用程序中的“启动对象”下拉框中,选择一个应用程序类。
Main函数的签名(方法名称、返回类型、参数列表)可以有四种形式:
static void Main(string[] args) {} //默认 static void Main() {} static int Main(string[] args) {} static int Main() {}
注:如果不提供一个明确的访问修饰符,Main方法自动定义为隐式私有的,确保其他应用程序不能直接调用另一个应用程序的入口点。
总结来说,Main可以返回类型可以为void或int,参数可以有(字符串数组)也可以没有。
关于返回类型:即使使用void,系统也会有返回值(0为正常运行,-1为不正常运行),这是Main所特有的特征,其它普通方法若为void,是没法返回值的。在windows下,应用程序的返回值被保存在%errorlevel%的系统变量中,并且只有当程序结束时,这个值才传递给这个系统变量。因此,程序在运行时无法获取自己的这个值,显然,只能用另外一个程序获取,例如另外一个程序可以启动这个程序,并且需要判断是否这个程序正常的执行了。可以System.Diagnostics.Process.ExitCode的静态属性来获取这个系统变量。
使用方法:
Process myProcess = null; myProcess = Process.Start("NotePad.exe");//启动notepad程序。 myProcess.WaitForExit();//一定要等待进程结束,否则下一句会报错,因为没退出就没有ExitCode Console.WriteLine(myProcess.ExitCode);
关于参数:Main的参数的作用是平时我们运行程序时,例如,在dos下运行 csc /t,前面的csc就是主程序,而后面的/t就是参数(是对输入命令后程序名之后的字符串以空格进行拆分出来的),主程序里应该根据参数的不同,执行不同的代码,实际上,参数的传递不一定非要“-”或“/”,程序对哪些命令行参数进行响应以及参数如何格式化,都取决于开发人员,完全可以让参数为 csc ?t,只要程序中能够理解和进行处理即可。
另外,利用System.Enviroment的静态方法GetCommandLineArgs(),它可以访问用户启动程序时输入的参数哦。即使我们的Main定义为无参格式,也是可以获取到的。这个方法返回的是用户输入参数的字符串数组。第一个索引为应用程序的路径和名称,之后就是命令行参数了。方法如下:
//注意,有无参数都没关系 static int Main() { // 用System.Environment获取参数。 string[] theArgs = Environment.GetCommandLineArgs(); foreach(string arg in theArgs) Console.WriteLine("Arg: {0}", arg); Console.ReadLine(); }
请看我执行这个程序以及返回的结果:
另外,在用VS平台时,为了方便用不同命令行测试程序,只需在下面的Command Line arguments中输入相应参数,则程序启动时,自动输入如下参数。
附:可以看出,System.Enviroment类有许多有用的方法,可以获得大量关于.NET应用程序的操作系统细节。可以查看SDK,非常有用的类哦。
疑问:System.Diagnostics.Process.ExitCode和System.Enviroment.ExitCode有什么区别呢?
3.2 System.ConSole类
这是控制台程序中,最常用的类,它封装了基于控制台应用程序的输入、输出和错误流操作。可以参考SDK,其实主要是write*和read*成员。
在控制台输出时,可以控制其格式,使用的是占位符。.NET引入了一种字符串格式化的新风格,与C语言的printf()语句相似。简而言之,如果需要定义一个字符串文本,其中包含一些要到运行时才能知道其值的数据片段,可以使用这种花括号的语法在文本内部指定占位符。在运行时,值会传入到Console.WriteLine()来替代每一个占位符。这个功能在string.format中也有使用,很普遍。
注:如果大括号占位符的数量和填充的参数数量不一致,则会在运行时收到一个FormatException异常。
如果数值数据需要更精细的格式化,每一个占位符都可以(可选地)包含不同的格式字符,给定的占位符值以冒号为标记,将这些字符作为后缀(例如,{0:C}、{1:d}、{2:X})。
3.3数据类型
C#的关键字不只是简单的编译器可以识别的标记,其实是System命名空间中完整类型的简化符号,例如关键字int,其实是system.int32的简化标记,因此,声明数据类型时,两种方法均可:
int a;
system.int32 b;
这里需要注意的是,默认情况下,在赋值运算符右边的一个实数值字面量被当作double类型,因此,为了初始化一个folat变量,使用后缀f或F。如:
double a=5.3;
folat b=5.3F;
对于任何类型(值类型、引用类型),都必须赋值之后才可使用(否则编译出错),应该有两步骤(可以用一个语句实现),第一步骤声明,如int a;object b;第二步骤,对于值类型的赋值,有两种方法,第一种方法直接赋值,如:a=1;另一种,由于内建数据类型支持默认构造函数,可以用 new来将变量自动设置为默认值,如:a=new a();对于引用类型,可以用new的各种构造函数进行赋值,默认时候,赋值为null:
这样对于一个值类型(引用类型类似),在使用前,应该经历如下过程:
int a=2;//声明,直接赋值
int b=new b();//声明,赋默认值为0;
内建的数据类型支持许多有用的属性和方法:
数值数据类型:所有数值数据类型具有MaxValue和MinValue属性,给定可存储的范围,而double、float还具有epsilon(最小的正数)和无穷大(正、负)的属性。
Console.WriteLine(int.MaxValue);
bool类型:支持TrueString和FalseString属性,可以返回True和False字符串。
string a=bool.TrueString;
char类型:可以通过点运算符,看看char有哪些有用的静态方法(Is*用于判断是否数据某种范围,To*转换为某种等效项)
string类型:给出了许多有用的方法,包括返回字符串长度、查找字符串的子字符串、转换大小写等。需要记住的是,c#中“+”符号会被编译器处理为String.Concat方法的调用,减少了输入量。
c#字符串常量中包含各种转义字符,来限制字符串数据应该怎样被输出。每个转义字符都以一个反斜杠开始,后跟一个特殊的标记,例如"\r”,相当于输入一个回车,其他转义字符,参考相应文档。
另外,C#引入以@为前缀的字符串字面量记法,术语称为逐字字符串,他可以使对一个字面量的转义字符的处理失效并输出字符串。例如上面的,我们就想输出\n这个字符串,不是想把它转义为一个回车,则应该为@"\n",此时,系统就不认为这是个转义字符,而是直接输出这个字符串。
关于字符串的相等性:我们知道,字符串为引用类型,而对于引用类型的比较(==、!=)都是比较他们的引用(指针)是否一致,但是,对于string,不是这样的,他将相等性运算符重定义为比较字符串对象的值,而不是内存中他们引用的对象,这是特殊的一点需要记住哦。
关于字符串的不可变性:也就是说,一旦string的类型被赋值,这个值就不变了。那么有人会说,字符串应该可以变啊,在代码中经常将一个字符串进行改变啊,例如:
string a=“ABC”;
a=a+“DEF”;
看起来a变为了“ABCDEF”,但其实后来的这个a已经是被重新分配地址了(一个新的实例了),而之前的那个a的位置中依然是"ABC",只不过现在他不可用了,会在不确定的时间被垃圾回收。从这可以看出,由于对string类型要经常进行操作,因此,不当的使用会使得程序低效,代码膨胀,尤其涉及字符串改变的操作。
为了解决字符串经常变化导致的性能问题,.NET提供了System.Text命名空间,这里有一个StringBulider类,它的独特之处在于当调用这个类型成员时,都直接修改对象内部的字符数据,而不是获取按修改后格式的数据副本。
要注意的是,stringbuilder类型的默认构造函数时,只能保存16个字符串以下的字符串,可以通过其他构造函数改变这个初值。当追加字符串超过规定限制之后,他将把数据复制到新的实例中,并根据规定的限制来扩大缓冲区。
疑问:在stringbuilder中,“当追加字符串超过规定限制之后,他将把数据复制到新的实例中,并根据规定的限制来扩大缓冲区。”那么重新复制到新的实例,是否意味着就和string在进行改变操作一样了,也就是说,每次操作时若都超过规定限制的最差情况下,和string导致的代码膨胀问题一样啦?另外,根据规定限制,是指多少?
3.4数据转换
简单来说,数据转换分为宽化转换和窄化转换。
宽化转换用于定义隐式“向上转换”,并且不会导致数据丢失,在运算或赋值过程中,系统自动进行这种转换,如:
short a=3;
int b=a;
窄化转换是向下转换,CLR不能应用窄化运算,即使可以推断窄化转换可以成功(例如,将一个在short范围内的int值赋值给short类型),也会导致编译错误,因为c#是类型安全的,要想进行强制窄化转换,必须使用运算符()来进行显示强制转换,如:
int a=2;
short b=(short)a;
要注意的是,即使使用了这个强制转换,编译时不会出错了,但是,不能保证转换后的结果是你想要的,一旦超出目标类型的范围,转换结果是不可预料的。为了保证强制转换的结果是正确的,c#提供了checked和unchecked关键字,他将确保数据丢失肯定会被检测到。
当使用checked关键字时,他所包含的语句(语句块),若出现转换的溢出,则会得到一个运行时异常,从而让你发现转换出现的可能错误(最好用try catch捕获),如:
int a=65530;
shrot b=checked((short)a);//语句
或checked //语句块
{
short b=(short)a;
}
要想在整个项目中都对每一个转换进行溢出检测,可以在项目级别设置,方法是项目属性-生成-高级,选中“检测运算上溢/下溢”。这其实是编译器中csc的参数checked标志的选择。
但是需要注意,整个项目的溢出检测,要消耗一定的性能,因此,在所有溢出异常都解决后(好像比较难吧~~~,对静态代码可以检测,对动态输入,如何完全保证哦),可以禁用这个checked标志。
另外,.NET提供了Convert方法,来进行转换(宽化,窄化均可),好处是提供了语言无关的方式进行数据类型的转换,也提供了Parse方法用于string类型转换为各种基本类型,网上有许多对比括号强制转换、Convert和Parse转换的对比文章,这里不再赘述。
3.4迭代与选择结构
C#提供了四种迭代结构:
- for
- foreach/in
- while
- do/while
有两种条件结构:
- if/else
- switch
基本语法就不说了,说几个注意:
while与do/while的循环类似,区别是后者循环肯定会执行至少一次对应的代码块(相比之下,如果一开始终止条件就是false,很可能while循环就不会执行)。
do/while的while后,别忘了加上分号!
do
{
//something
}
while(xxxx);//注意分号!