扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
作者:李建忠 来源:MSDN 2007年10月29日
关键字: Microsoft.NET C# Linux
抛开Microsoft.NET平台去谈C#是没有意义的,C#之“Sharp”也正在其后端强大的平台。仅仅拘泥于语法层面是体验不了C#的锐利之处的,C#程序很多诡秘之处必须依靠Microsoft.NET平台才能深度的掌握和运用。简单的讲,Microsoft.NET平台是一个建立在开放互联网络协议和标准之上,采用新的工具和服务来满足人们的计算和通信需求的革命性的新型XML Web智能计算服务平台。它允许应用程序在因特网上方便快捷地互相通信,而不必关心使用何种操作系统和编程语言。
从技术层面具体来说,Microsoft.NET平台主要包括两个内核,即通用语言运行时(Common Language Runtime,简称CLR)和Microsoft.NET框架类库,它们为Microsoft.NET平台的实现提供了底层技术支持。通用语言运行时是建立在操作系统最底层的服务,为Microsoft.NET平台的执行引擎。Microsoft.NET框架包括一套可被用于任何编程语言的类库,其目的是使得程序员更容易地建立基于网络的应用和服务。在此之上是许多应用程序模板,这些模板为开发网络应用和服务提供高级的组件和服务。Microsoft.NET平台之浩瀚绝非这里的几千字能够廓清,我们下面将着重体验那些对我们用C#开发应用程序至关重要的平台基础构造。
通用语言运行时是整个Microsoft.NET框架赖以建构的基础,它为Microsoft.NET应用程序提供了一个托管的代码执行环境。它实际上是驻留在内存里的一段代理代码,负责应用程序在整个执行期间的代码管理工作,比较典型的有:内存管理,线程管理,安全管理,远程管理,即时编译,代码强制安全类型检查等。这些都可称得上Microsoft.NET框架的生命线。
实际上我们可以看出来,CLR代理了一部分传统操作系统的管理功能。在CLR下的代码称之为托管代码,否则称为非托管代码。我们也可将CLR看作一个技术规范,无论程序使用什么语言编写,只要能编译成微软中间语言 (MSIL),就可以在它的支持下运行,这使得应用程序得以独立于语言。目前支持CLR的编程语言多达二三十种。微软中间语言是我们在Microsoft.NET平台下编译器输出的PE文件的语言。它是Microsoft.NET平台最完整的语言集,非常类似于PC机上的汇编语言。即时编译器在运行时将中间语言编译成本地二进制代码。它为Microsoft.NET平台提供了多语言的底层技术支持。另外根据需要,Microsoft.NET即时编译器提供了特殊情况下的经济型即时编译和安装时编译技术。
CLR的设计目的便是直接在应用程序运行环境中为基于组件的编程提供第一等的支持。正如在Windows中添加了对窗口、控件、图形和菜单的直接支持,为基于消息的编程添加了底层结构,为支持设备无关性添加了抽象内容一样,CLR直接支持组件(包括属性和事件)、对象、继承性、多态性和接口。对属性和事件的直接支持使得基于组件的编程变得更简单,而不需要特殊的接口和适配设计模式。在组件运行时,CLR负责管理内存分配、启动和中止线程和进程、强化安全系数,同时还调整任何该组件涉及到的其他组件的附属配置。序列化支持允许以多种格式操作存储在磁盘上的组件,包括基于业界标准XML的SOAP。CLR提供了处理错误条件的有力、协调的方式。每个模块都具有内置的完整的元数据,这意味着诸如动态创建和方法调用之类的功能更容易,也更安全。映射甚至允许我们灵活地创建和执行代码。我们可以控制应用程序使用的组件的版本,这使应用程序更加可靠。组件代码是与处理器无关的和易于验证的中间语言 ( IL),而不是某一种特定的机器语言,这意味着组件不但可以在多种计算机上运行,而且可以确保组件不会覆盖它们不使用的内存,也不会潜在地导致系统崩溃。CLR根据托管组件的来源(例如来自因特网,企业局域网,本地机)等因素对他们判定以适当的信任度,这样CLR会根据他们的信任度来限定他们执行如读取文件,修改注册表等某些敏感操作的权限。借助通用类型系统(Common Type System,简称CTS)对代码类型进行严格的安全检查避免了不同组件之间可能存在的类型不匹配的问题。CLR下的编程全部是围绕组件进行的。
值得指出的是CLR通常寄宿在其他高性能的服务器应用程序中,比如:因特网信息服务器(IIS),Microsoft SQL Server。这使得我们可以充分利用通用语言运行时诸多的安全,高效的优点来部署自己的商业逻辑。
CLR对程序员影响最大的就是它的内存管理功能,以至于我们很有必要单独把它列出来阐述。它为应用程序提供了高性能的垃圾收集环境。垃圾收集器自动追踪应用程序操作的对象,程序员再也用不着和复杂的内存管理打交道。这在某些喜欢张口闭口底层编程的所谓的高手来说,自动内存管理从来都是他们嘲笑的对象。的确,为通用软件环境设计的自动化内存管理器永远都抵不上自己为特定程序量身订制的手工制作。但现代软件业早已不再是几百行代码的作坊作业,动辄成千上万行的代码,大量的商业逻辑凸现的已不再是算法的灵巧,而是可管理性,可维护性的工程代码。.NET/C#不是为那样的作坊高手准备的,C语言才是他们的尤物。在Microsoft.NET托管环境下,CLR负责处理对象的内存布局,管理对象的引用,释放系统不再使用的内存(自动垃圾收集)。这从根本上解决了长期以来困扰软件的内存泄漏和无效内存引用问题,大大减轻了程序员的开发负担,提高了程序的健壮性。实际上我们在托管环境下根本找不到关于内存操作或释放的语言指令。值得指出的是Microsoft.NET应用程序可以使用托管数据,也可以使用非托管数据,但CLR并不能判断托管数据与非托管数据。
垃圾收集器负责管理.NET应用程序内存的分配和释放。当用new操作符创建新的对象时,垃圾收集器在托管堆(Managed Heap)中为对象分配内存资源。只要托管堆内的内存空间可用,垃圾收集器就为每一个新创建的对象分配内存。当应用程序不再持有某个对象的引用,垃圾收集器将会探测到并释放该对象。值得注意的是垃圾收集器并不是在对象引用无效时就立即开始释放工作,而是根据一定算法来决定什么时候进行收集和对什么对象进行收集。任何一个机器的内存资源总是有限的,当托管堆内的内存空间不够用时,垃圾收集器启动收集线程来释放系统内存。垃圾收集器根据对象的存活时间,对象历经的收集次数等来决定对哪些对象的内存进行释放。宏观的看,我们并不知道垃圾收集的确切行为,但Microsoft.NET类库为我们提供了控制垃圾收集行为的部分功能,在某些特殊情况下,我们有必要进行一些受限的操作。
垃圾收集器并不意味着程序员从此可以一劳永逸,如果正在操作一个包装了如文件,网络连接,Windows句柄,位图等底层操作系统资源的对象,我们还是需要明确地释放这些非托管资源的。这在“第五讲 构造器与析构器”里有详细的阐述。
Microsoft.NET框架类库是一组广泛的,面向对象的可重用类的集合,为应用程序提供各种高级的组件和服务。它将程序员从繁重的编程细节中解放出来专注于程序的商业逻辑,为应用程序提供各种开发支持--不管是传统的命令行程序还是Windows图形界面程序,拟或是面向下一代因特网分布式计算平台的ASP.NET或XML Web服务。下面是对这些组件和服务的一个概括。
• |
系统框架服务 服务框架包括一套开发人员希望在标准语言库中存在的基类库,例如:集合、输入/输出,字符串及数据类。另外,基类库提供访问操作系统服务如图画、网络、线程、全球化和加密的类。服务框架也包括数据访问类库,及开发工具,如调试和剖析服务,能够使用的类。 |
• |
ADO.NET组件 ADO.NET为基于网络的可扩展的应用程序和服务提供数据访问服务。ADO.NET不仅支持传统的基于连接指针风格的数据访问,同时也为更适合于把数据返回到客户端应用程序的无连接的数据模板提供高性能的访问支持。 |
• |
XML数据组件 所有的数据都可被看作XML,开发人员可以通过XML为任何数据使用转换,传输和确认服务。系统框架对XML数据提供第一等的操作支持。系统也支持ADO.NET数据与XML数据之间的通用转换。 |
• |
Windows表单组件 Windows表单组件为开发人员提供了强大的Windows应用程序模型和丰富的Windows用户接口,包括传统的ActiveX控件和Windows XP的新界面,如透明的、分层的、浮动窗口。对设计时的强大支持也是Windows表单组件令人兴奋的地方。 |
• |
ASP.NET应用服务 ASP.NET的核心是高性能的用于处理基于低级结构的HTTP请求的运行语言。编译运行方式大大提高了它的性能。ASP.NET使用基于构件的Microsoft .NET框架配制模板,因此它获得了如XCOPY配制、构件并行配制、基于XML配制等优点。它支持应用程序的实时更新,提供高速缓冲服务改善性能。 |
• |
ASP.NET Web表单 ASP.NET Web表单把基于VB的表单的高生产性的优点带到了网络应用程序的开发中来。ASP.NET Web表单支持传统的将HTML内容与角本代码混合的ASP语法,但是它提出了一种将应用程序代码和用户接口内容分离的更加结构化的方法。ASP.NET提供了一套映射传统的HTML用户接口部件(包括列表框,文本框和按钮)的ASP.NET Web表单控件和一套更加复杂强大的网络应用控件(如日历和广告转板)。 |
• |
XML Web服务 ASP.NET应用服务体系架构为用ASP.NET建立XML Web服务提供了一个高级的可编程模板。虽然建立XML Web服务并不限定使用特定的服务平台,但是它提供许多的优点将简化开发过程。使用这个编程模型,开发人员甚至不需要理解HTTP、SOAP或其它任何网络服务规范。 ASP.NET XML Web服务为在Internet上绑定应用程序提供了一个利用现存体系架构和应用程序的简单的、灵活的、基于产业标准的模型。 |
组件编程不是对传统面向对象的抛弃,相反组件编程正是面向对象编程的深化和发展。类作为面向对象的灵魂在C#语言里有着相当广泛深入的应用,很多非常“Sharp”的组件特性甚至都是直接由类包装而成。对类的深度掌握自然是我们“Sharp XP”重要的一环。
C#的类是一种对包括数据成员,函数成员和嵌套类型进行封装的数据结构。其中数据成员可以是常量,域。函数成员可以是方法,属性,索引器,事件,操作符,实例构建器,静态构建器,析构器。我们将在“第五讲 构造器与析构器”和“第六讲 域 方法 属性与索引器”对这些成员及其特性作详细的剖析。除了某些导入的外部方法,类及其成员在C#中的声明和实现通常要放在一起。
C#用多种修饰符来表达类的不同性质。根据其保护级C#的类有五种不同的限制修饰符:
1. |
public可以被任意存取; |
2. |
protected只可以被本类和其继承子类存取; |
3. |
internal只可以被本组合体(Assembly)内所有的类存取,组合体是C#语言中类被组合后的逻辑单位和物理单位,其编译后的文件扩展名往往是“.DLL”或“.EXE”。 |
4. |
protected internal唯一的一种组合限制修饰符,它只可以被本组合体内所有的类和这些类的继承子类所存取。 |
5. |
private只可以被本类所存取。 |
如果不是嵌套的类,命名空间或编译单元内的类只有public和internal两种修饰。
new修饰符只能用于嵌套的类,表示对继承父类同名类型的隐藏。
abstract用来修饰抽象类,表示该类只能作为父类被用于继承,而不能进行对象实例化。抽象类可以包含抽象的成员,但这并非必须。abstract不能和new同时用。下面是抽象类用法的伪码:
abstract class A { public abstract void F(); } abstract class B: A { public void G() {} } class C: B { public override void F() { //方法F的实现 } }
抽象类A内含一个抽象方法F(),它不能被实例化。类B继承自类A,其内包含了一个实例方法G(),但并没有实现抽象方法F(),所以仍然必须声明为抽象类。类C继承自类B,实现类抽象方法F(),于是可以进行对象实例化。
sealed用来修饰类为密封类,阻止该类被继承。同时对一个类作abstract和sealed的修饰是没有意义的,也是被禁止的。
类与对象的区分对我们把握OO编程至关重要。我们说类是对其成员的一种封装,但类的封装设计仅仅是我们编程的第一步,对类进行对象实例化,并在其数据成员上实施操作才是我们完成现实任务的根本。实例化对象采用MyClass myObject=new MyClass()语法,这里的new语义将调用相应的构建器。C#所有的对象都将创建在托管堆上。实例化后的类型我们称之为对象,其核心特征便是拥有了一份自己特有的数据成员拷贝。这些为特有的对象所持有的数据成员我们称之为实例成员。相反那些不为特有的对象所持有的数据成员我们称之为静态成员,在类中用static修饰符声明。仅对静态数据成员实施操作的称为静态函数成员。C#中静态数据成员和函数成员只能通过类名引用获取,看下面的代码:
using System; class A { public int count; public void F() { Console.WriteLine(this.count); } public static string name; public static void G() { Console.WriteLine(name); } } class Test { public static void Main() { A a1=new A(); A a2=new A(); a1.F(); a1.count=1; a2.F(); a2.count=2; A.name="CCW"; A.G(); } }
我们声明了两个A对象a1,a2。对于实例成员count和F(),我们只能通过a1,a2引用。对于静态成员name和G()我们只能通过类型A来引用,而不可以这样a1.name,或a1.G()。
在上面的程序中,我们看到在实例方法F()中我们才用this来引用变量count。这里的this是什么意思呢?this 关键字引用当前对象实例的成员。在实例方法体内我们也可以省略this,直接引用count,实际上两者的语义相同。理所当然的,静态成员函数没有 this 指针。this 关键字一般用于从构造函数、实例方法和实例访问器中访问成员。
在构造函数中this用于限定被相同的名称隐藏的成员,例如:
class Employee { public Employee(string name, string alias) { this.name = name; this.alias = alias; } }
将对象作为参数传递到其他方法时也要用this表达,例如:
CalcTax(this);
声明索引器时this更是不可或缺,例如:
public int this [int param] { get { return array[param]; } set { array[param] = value; } }
C#中所有的类都直接或间接继承自System.Object类,这使得C#中的类得以单根继承。如果我们没有明确指定继承类,编译器缺省认为该类继承自System.Object类。System.Object类也可用小写的object关键字表示,两者完全等同。自然C#中所有的类都继承了System.Object类的公共接口,剖析它们对我们理解并掌握C#中类的行为非常重要。下面是仅用接口形式表示的System.Object类:
namespace System { public class Object { public static bool Equals(object objA,object objB){} public static bool ReferenceEquals(object objA,object objB){} public Object(){} public virtual bool Equals(object obj){} public virtual int GetHashCode(){} public Type GetType(){} public virtual string ToString(){} protected virtual void Finalize(){} protected object MemberwiseClone(){} }
我们先看object的两个静态方法Equals(object objA,object objB),ReferenceEquals(object objA,object objB)和一个实例方法Equals(object obj)。在我们阐述这两个方法之前我们首先要清楚面向对象编程两个重要的相等概念:值相等和引用相等。值相等的意思是它们的数据成员按内存位分别相等。引用相等则是指它们指向同一个内存地址,或者说它们的对象句柄相等。引用相等必然推出值相等。对于值类型关系等号“= =”判断两者是否值相等(结构类型和枚举类型没有定义关系等号“= =”,我们必须自己定义)。对于引用类型关系等号“= =”判断两者是否引用相等。值类型在C#里通常没有引用相等的表示,只有在非托管编程中采用取地址符“&”来间接判断二者的地址是否相等。
静态方法Equals(object objA,object objB)首先检查两个对象objA和objB是否都为null,如果是则返回true,否则进行objA.Equals(objB)调用并返回其值。问题归结到实例方法Equals(object obj)。该方法缺省的实现其实就是{return this= =obj;}也就是判断两个对象是否引用相等。但我们注意到该方法是一个虚方法,C#推荐我们重写此方法来判断两个对象是否值相等。实际上Microsoft.NET框架类库内提供的许多类型都重写了该方法,如:System.String(string),System.Int32(int)等,但也有些类型并没有重写该方法如:System.Array等,我们在使用时一定要注意。对于引用类型,如果没有重写实例方法Equals(object obj),我们对它的调用相当于this= =obj,即引用相等判断。所有的值类型(隐含继承自System.ValueType类)都重写了实例方法Equals(object obj)来判断是否值相等。
注意对于对象x,x.Equals(null)返回false,这里x显然不能为null(否则不能完成Equals()调用,系统抛出空引用错误)。从这里我们也可看出设计静态方法Equals(object objA,object objB)的原因了--如果两个对象objA和objB都可能为null,我们便只能用object. Equals(object objA,object objB)来判断它们是否值相等了--当然如果我们没有改写实例方法Equals(object obj),我们得到的仍是引用相等的结果。我们可以实现接口IComparable(有关接口我们将在“第七讲 接口 继承与多态”里阐述)来强制改写实例方法Equals(object obj)。
对于值类型,实例方法Equals(object obj)应该和关系等号“= =”的返回值一致,也就是说如果我们重写了实例方法Equals(object obj),我们也应该重载或定义关系等号“= =”操作符,反之亦然。虽然值类型(继承自System.ValueType类)都重写了实例方法Equals(object obj),但C#推荐我们重写自己的值类型的实例方法Equals(object obj),因为系统的System.ValueType类重写的很低效。对于引用类型我们应该重写实例方法Equals(object obj)来表达值相等,一般不应该重载关系等号“= =”操作符,因为它的缺省语义是判断引用相等。
静态方法ReferenceEquals(object objA,object objB)判断两个对象是否引用相等。如果两个对象为引用类型,那么它的语义和没有重载的关系等号“= =”操作符相同。如果两个对象为值类型,那么它的返回值一定是false。
实例方法GetHashCode()为相应的类型提供哈希(hash)码值,应用于哈希算法或哈希表中。需要注意的是如果我们重写了某类型的实例方法Equals(object obj),我们也应该重写实例方法GetHashCode()--这理所应当,两个对象的值相等,它们的哈希码也应该相等。下面的代码是对前面几个方法的一个很好的示例:
using System; struct A { public int count; } class B { public int number; } class C { public int integer=0; public override bool Equals(object obj) { C c=obj as C; if (c!=null) return this.integer==c.integer; else return false; } public override int GetHashCode() { return 2^integer; } } class Test { public static void Main() { A a1,a2; a1.count=10; a2=a1; //Console.Write(a1==a2);没有定义“= =”操作符 Console.Write(a1.Equals(a2));//True Console.WriteLine(object.ReferenceEquals(a1,a2));//False B b1=new B(); B b2=new B(); b1.number=10; b2.number=10; Console.Write(b1==b2);//False Console.Write(b1.Equals(b2));//False Console.WriteLine(object.ReferenceEquals(b1,b2));//False b2=b1; Console.Write(b1==b2);//True Console.Write(b1.Equals(b2));//True Console.WriteLine(object.ReferenceEquals(b1,b2));//True C c1=new C(); C c2=new C(); c1.integer=10; c2.integer=10; Console.Write(c1==c2);//False Console.Write(c1.Equals(c2));//True Console.WriteLine(object.ReferenceEquals(c1,c2));//False c2=c1; Console.Write(c1==c2);//True Console.Write(c1.Equals(c2));//True Console.WriteLine(object.ReferenceEquals(c1,c2));//True } }
如我们所期望,编译程序并运行我们会得到以下输出:
True False False False False True True True False True False True True True
实例方法GetType()与typeof的语义相同,它们都通过查询对象的元数据来确定对象的运行时类型,我们在“第十讲 特征与映射”对此作详细的阐述。
实例方法ToString()返回对象的字符串表达形式。如果我们没有重写该方法,系统一般将类型名作为字符串返回。
受保护的Finalize()方法在C#中有特殊的语义,我们将在“第五讲 构造器与析构器”里详细阐述。
受保护的MemberwiseClone()方法返回目前对象的一个“影子拷贝”,该方法不能被子类重写。“影子拷贝”仅仅是对象的一份按位拷贝,其含义是对对象内的值类型变量进行赋值拷贝,对其内的引用类型变量进行句柄拷贝,也就是拷贝后的引用变量将持有对同一块内存的引用。相对于“影子拷贝”的是深度拷贝,它对引用类型的变量进行的是值复制,而非句柄复制。例如X是一个含有对象A,B引用的对象,而对象A又含有对象M的引用。Y是X的一个“影子拷贝”。那么Y将拥有同样的A,B的引用。但对于X的一个“深度拷贝”Z来说,它将拥有对象C和D的引用,以及一个间接的对象N的引用,其中C是A的一份拷贝,D是B的一份拷贝,N是M的一份拷贝。深度拷贝在C#里通过实现ICloneable接口(提供Clone()方法)来完成。
对对象和System.Object的把握为类的学习作了一个很好的铺垫,但这仅仅是我们锐利之行的一小步,关乎对象成员初始化,内存引用的释放,继承与多态,异常处理等等诸多“Sharp”特技堪为浩瀚,让我们继续期待下面的专题!
查看本文来源如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者