C# 篇基础知识4——.NET的基础概念
C#语言是与微软的.NET框架紧密地联系在一起的,而.NET框架是微软.NET战略的核心,为了更好的理解C#语言,我们必须了解一些.NET框架的基本知识。.NET框架是为开发应用程序推出的一个编程平台,它主要为编写应用程序提供两方面的支持,一是它管理代码的执行过程,二是它为代码提供类库支持。
(1)公共语言运行时CLR
.NET平台下编写的程序一般都在公共语言运行时(Common Language Runtime,CLR)的管理下运行,它负责运行代码,确保代码的安全性和准确性,又负责内存管理、线程调度等核心服务,通常把在CLR控制下运行的代码称为托管代码(Managed Code)。
(2)FCL类库
.NET为我们提供了一个内容丰富的.NET框架基础类库(Framework Class Library,FCL),如果将C#程序比喻为一座大厦,那么大厦的设计思想就是面向对象编程,而建筑大厦的材料则来自于.NET框架基础类库,我们可以像使用钢筋、水泥、砖块一样使用FCL中的类构建应用程序大厦。因此学习C#的一个重点就是学习FCL类库中的常用类。
1. C#为何采用两次编译
(1)为了提高性能
因为JIT编译中程序运行时发生,这时编译器已经知道系统使用何种类型的CPU,可以针对该CPU特性在JIT编译过程中进行代码优化(比如可以更高效地利用CPU 的寄存器,在适当的情况下实施低级代码优化(常量重叠、拷贝复制、取消范围检查、取消常规副表达式以及方法内联等),可以在代码执行期间监控当前的物理和虚拟内存需求从而更高效地利用内存等),另外,虽然程序运行代码首次由中间语言编成为机器语言时,性能会稍有损失,但由于中间语言可以非常快速地转换为机器语言,并且被优化的程序代码往往被成百上千次使用,第一次编译造成的这点性能损失显得微不足道,C#两次编译的优势就显现出来了。
(2)语言互操作性
MSIL 为不同编程语言的互操作性提供了可能,不同语言编写的组件最终都编译为中间语言,然后组成一个完整的程序。这样就能让使用C#、Visual Basic、Visual C++的开发人员一起完成同一项目。
(3)平台无关性
Mono项目,以及现在正式推出的.NET Core,使基于.NET程序能够运行在各种操作系统平台上。
2.强数据类型
中间语言是基于强数据类型的,即每一个变量都有明确的数据类型。虽然有时强数据类型会降低性能,但会在语言的互操作性、垃圾回收、安全性、应用程序域等方面获得更多的好处。
(1)通用类型系统
由于不同编程语言的数据类型和语法都有所不同,由此.NET为中间语言制定了一个通用类型系统(Common Type System,CTS),它定义了一系列标准的基本数据类型,不管你用什么语言编写程序,程序中的变量最终都被转换为这些基本数据类型。
通用类型系统不仅指定了基本数据类型,还定义了一个内容丰富的类型层次结构,允许用户通过这些基本类型构造自己需要的类型,比如数组、结构、类等。如图所示。
(2)公共语言规范
除了通用类型系统外,.NET 框架还制定了一套公共语言规范(Common Language Specification,CLS)。每种语言都有一套自己的语法规则集,而恰恰CLS 就是这些集合的交集,所以它能被.NET 上的所有语言支持。通俗地说就是公共语言规范中的语法规则在所有.NET 语言中都成立。如此一来,符合CLS 规范的代码转换为中间语言后就可以被.NET 上任何语言访问,从而确保不同语言的互操作性。编写不符合CLS 规范的代码是完全可以的,但这时就不能保证不同语言间的互操作性。所以在编写用于共享的类时,一般只用完全兼容CLS 的代码,以增强类的交互性;在私有类中,可以编写非CLS 代码,因为私有类不用来交互。C#中不兼容CLS 的特性非常少。
3. 类型的判定、命名空间和装箱拆箱
(1)类型的判定
通过下面几种方法,可以轻易获取变量的类型信息:
Sizeof()运算符,通过它可获知数据类型在内存中占用几个字节。如sizeof(int)。
typeof(),通过它可以获取数据类型的通用数据类型名,此运算符的参数只能是类型。如typeof(int)得到System.Int32。
GetType(),如果想要获取某个变量的类型,需要使用该变量的GetType()方法,此方法继承自Object类,例如对于int t;string str=t.GetType();。
结合 GetType()方法和typeof 运算符,我们就可以检验变量是否为某种类型。kitty.GetType() == typeof(Cat))。is 运算符,is 运算符检验某个对象是否为某种类型,而且用法更为简洁,例如if(kitty is Cat){}。这两种判断变量类型的方式有一点区别,即当对象和类型之间是继承关系时,is运算符仍然返回true。
(2)命名空间
命名空间是用来组织类的,它避免了重名的问题。System是.Net预定义的一个命名空间,它包含了大量常用类。如果在文件开头使用了using 语句,在使用相应命名空间中的类时,就不必添加命名空间前缀了。.NET 建议在大多数情况下,都至少要提供两个嵌套的命名空间,第一个是公司名,第二个是技术名或软件名。这么做可以尽量保证不与其它组织编写的命名空间冲突。例如Microsoft.Win32. Registry类。
(3)装箱拆箱
在C#中,一切变量都可以看作对象,所有值类型数据都可以通过隐式的装箱操作转换为引用型对象,装箱操作的优点在于可以像操作对象那样操作值类型变量。操作方法例如:int n=3;Object obj=n;。对于装入箱中的值类型数据,可以通过拆箱(Unboxing)操作释放出来,拆箱操作要用显式转换,例如int i=(int)obj;。当一个方法的参数类型不能确定时,装箱操作就非常有用,例如一个用来存储各种物品的仓库类:
class Storehouse
{ public Object [] items;
private int count;
public Storehouse(int size)
{ items=new Object[size];count=0;}
public void Add(Object obj)
{
if(count<items.Length)
{ items[count]=obj;
count++;}
else
{Console.WriteLine(“仓库已满”);}
}
}
3.异常
异常(Exception)就是程序执行期间发生的问题,原因并不总在程序员,比如用户输入了非法数据,要读取的文件不存在等等。如果不处理这些异常,程序可能会崩溃,但如果在程序中过多的处理这些异常,会使程序结构不清晰。为此,C#为我们提供了一套完美的方案,在程序主线之外处理异常,不但使程序更加健壮、更加容错,而且保持了程序结构的清晰。
(1)捕获异常——try-catch结构
一般情况下我们需要对可能的异常情况进行处理,这时就需要捕获并处理异常。在C#中用try-catch 结构捕获并处理异常。把可能出现异常的语句放在 try 块中,把异常发生后的处理代码放在catch 块中。产生异常后CLR会在try 块后面寻找匹配的catch 块,并执行该块中的语句。
(2)收尾工作——try-catch-finally结构
在某些问题中,不论是否出现异常,都要进行一些收尾工作,这些收尾工作常放在finally 块中。可能发生异常的代码放在try 块中,异常处理代码放在catch 块中,不管是否发生异常,
程序都要执行finally 块中的代码。
例如,程序经常动态申请资源,比如打开某个文件,并从中读写数据。操作系统通常不允许多个程序同时读写同一个文件,所以读写完文件后,应及时关闭文件(即释放资源),以便其它程序使用。如果不关闭文件,就会发生资源泄露。这些释放资源的代码通常放在finally 块,不管是否发生异常,资源均被释放。
注意:try 块和catch 块、finally 块一起构成try 语句,try 块是关键字try 后用大括号括起来的语句块。如果 try 块后面有catch 块,则finally 块是可选的;如果try 块后面没有catch 块,则必须跟一个finally 块;如果既有catch 块又有finally 块,finally 块必须放在最后。
(3)抛出异常——throw语句
.NET 可以自动检测并抛出常见异常,但有时我们需要人工抛出异常。
try
{
Console.Write("请输入一个0 到10 之间的整数:");
int number = Convert.ToInt32(Console.ReadLine());
if (number < 0 || number > 10)
{
throw new ArgumentOutOfRangeException();
}
else
{
Console.WriteLine("你输入的整数是:{0}", number);
}
}
catch (ArgumentOutOfRangeException)
{
Console.WriteLine("你输入的整数超出范围!");
}
finally
{ Console.WriteLine("谢谢!"); } }
在C#中使用thow 语句抛出异常,其一般格式如下图所示,该语句先通过new 运算符创建异常类ArgumentOutOf RangeException 的一个对象,然后通过throw 语句抛出。当程序遇到throw 语句时,会立即停止执行try 块中的语句,转而执行匹配的catch 块中的语句。
4.NET中的异常类
(1)常见异常类
.NET 提供了丰富的异常类型,所有的异常类型都派生于Exception 类。下图列举出了一些常见的异常。
类大多在System 命名空间中,而IOException 类及其派生类在System.IO命名空间中(这个命名空间用于处理数据的读写)。一般情况下,异常没有特定的命名空间,哪个类生成异常,异常就放在哪个类所在的命名空间。Exception 类有两个非常重要的派生类——SystemException 和ApplicationException。.NET 中预定义的异常类都派生于SystemException 类,而用户自定义的异常类都应派生于ApplicationException 类。由于派生类对象属于基类,所以当有多个 catch 块时,要按从具体到一般的顺序由上往下排列,基类必须放在最后,否则捕捉派生类异常的catch 块永远没机会被执行。
(2)获取方法的异常帮助信息
还可以不给catch 块指定异常类型,这时它可以捕获任何异常。如想查找方法Convert.ToInt32(String)的异常,可以直接在vs ide中按F1,即可在帮助中找到Convert.ToInt32 (String)方法的文档,其中会有一节用来描述和该方法相关的异常。
(3)异常类的属性
Exception类和其他一般类一样,有几个公有属性,通过这些属性可以非常方便的了解异常信息。其中比较重要的两个属性是Message和StackTrace。属性Message用于描述异常的原因,属性StackTrace用于描述异常的堆栈信息,即发生异常位置。例如
catch (DivideByZeroException e)
{ Console.WriteLine("Message:" + e.Message);
Console.WriteLine("StackTrace:" + e.StackTrace); }
(4)自定义异常
大多数情况下,我们使用.Net 预定义的异常类,必要时我们也可以针对程序中的问题创建新的异常类。用户定义的异常类都直接或间接继承于ApplicationException 类。例如:
Class NegativeNumberException:ApplicatioinException
{
//属性Message 被直接初始化为字符串"对负数进行非法操作"
public NegativeNumberException():base(“对负数进行非法操作”){}
public NegativeNumberException(string message):base(message){}
}
异常类的名称最好和故障相关联,并以“Exception”结尾。