瓜瓜派的瓜瓜
分类: C/C++
2012-01-09 16:57:52
说明: 我正在撰写《面向对象的艺术——.NET Framework 4.0技术剖析与应用》(暂名)一书,会陆续将一些章节发到我的博客上。 作者本人拥有所有的版权。允许自由阅读和转载这些文章,但任何个人与机构不能将其用于商业目的。 金旭亮 ================================================== 《面向对象的艺术》 之 3.1 直观理解类和对象3.1.1 类、类的实例和对象 面向对象编程中,最基本的概念就是“类”和“对象”,深刻把握这两个概念,在编程中时刻具备“面向对象”的意识,对于一个.NET工程师而言非常重要。 请看示例程序UseForm(图 3‑1)。 图 3‑1 示例程序UseForm 如图 3‑1所示,示例程序UseForm运行时,每点击一次主窗体上的按钮,会在屏幕上出现一个“一模一样”的从窗体,而这些从窗体彼此之间是独立的,每个从窗体都可以在屏幕上自由移动和修改大小,都不会对其他任何一个窗体有着直接的影响。 这个简单的Windows Form示例程序的背后,隐藏着.NET面向对象编程的最基本特性。 打开UseForm项目的源码,我们可以看到其中定义了两个窗体类:frmMain(代表主窗体)和frmOther(代表从窗体)。而主窗体上按钮单击事件的响应代码如下: private void btnShowOtherForm_Click(object sender, EventArgs e) { //创建从窗体对象 frmOther frm = new frmOther(); //显示从窗体 frm.Show(); } 上述代码包容了整个示例程序的“核心机密”: 程序运行时我们看到的从窗体,其实都是frmOther类的对象。你看到了多少个从窗体,实际中就有多少个frmOther对象存在。 你所编写的代码全部放在frmMain或frmOther类中,但在程序运行时,“类”根本就不存在,存在的仅仅是“对象”。 为什么你所看到的“从窗体”都一模一样? 因为它们是从同一个模子里倒出来的! 所以,对象是以类为模板而创建出来的实例。 打个比方更易于理解:可以把类想象为一个印章,而对象则是印章沾上印泥之后盖出的印。这些盖出的印可以有颜色深浅,所盖位置等差别,但其内容则是由印章所确定的。 图 3‑2 一个印章可以盖出无数个印,类似地,,除非类的设计者做了特殊的限制,否则一个类可以创建无数个对象(或实例)。 在面向对象领域,“对象”这个概念与“类的实例”是等同的,它们指代同一事物。 在示例程序中,我们看到有一个主窗体(frmMain对象)和多个从窗体(frmOther)对象,而它们之间的“地位”是不同的,主窗体负责创建从窗体,关闭从窗体对其他窗体没有影响,而关闭主窗体,将导致屏幕上现有的所有窗体全部“消失”。由此可知,对象在程序中可以拥有不同的地位、作用和角色。 从中我们可以得出另外一个结论: 面向对象的程序在运行时,会创建多个对象,这些对象之间可能有着复杂的关联,相互协作,共同完成应用程序所提供的各项功能。 可以将上述结论以一个简单的公式来表达 正在运行的面向对象的程序= 对象 + 对象之间的相互协作关系 在面向对象程序的开发阶段,“类”是核心,而在面向对象程序运行之后,“对象”是核心。 3.1.2 创建自己的对象 我们可以通过编写一个类来定义好对象的模板,然后,就可以使用它来创建对象。 使用C#编写一个类非常简单,简单到只用一句话就可以讲清楚: 将函数和变量用大括号包起来,给它起个名字,再加上一个class关键字就行了。 以下代码定义了一个MathOpt类,它完成两数相加的功能(参见示例代码UseMathOpt)。 class MathOpt { public int Add(int x, int y) { return x + y; } } 以下代码创建了一个MathOpt对象,并且使用它来计算两整数之和: 1 static void Main(string[] args) 2 { 3 MathOpt mathobj; //定义MathOpt对象变量 4 mathobj = new MathOpt(); //创建对象 5 int result; 6 result=mathobj.Add(100, 200); //调用类的方法 7 Console.WriteLine("100+200=" + result); //输出结果 8 Console.ReadKey(); //敲任意键结束整个程序 9 } 上述的示例非常简单,然而“麻雀虽小,五脏俱全”,这个看似简单的例子中却蕴含着面向对象编程的基本原理。 首先,可以看到所有功能代码都是放在Main()和Add()两个函数中,而这两个函数又分别放在类Program和MathOpt中(放在类中的函数改称为“方法(method)”),可由此总结出一个要点: (1)类是面向对象程序中最基本的可复用软件单元,类中可包含多个方法。 多懂一点: 这里有一个需要强调的地方: 在面向对象程序的源码中,不存在独立于类之外的方法。 但这只是C#编程语言的限制,并非CLR限制,如果你使用MSIL(微软中间语言)编程,完全可以定义一个“全局”的函数,而此函数并不归属于某个类。 那么,编好了一个类,是否就可以直接使用呢? 仔细看看Main()函数中的代码,第3句定义了一个MathOpt类型的变量mathobj,第4句使用new关键字创建了一个MathOpt对象,并用mathobj变量来引用这一对象。之后第6句调用MathOpt类的Add()方法完成两数相加的功能。由此,总结出另外一个要点: (2)外界通过对象变量来调用使用类中的实例方法。 不允许直接访问已归属于某个类的方法,是面向对象程序不同于结构化程序的特点之一。 现在修改Main()函数中的代码,注释掉第4句创建对象的代码,再次编译程序,Visual Studio会报告: 使用了未赋值的局部变量“mathobj” 这说明C#编译器要求变量必须显式初始化后才能使用。 修改第3句代码,初始化mathobj变量为null值,再次编译可以成功。 修改后的代码如下: static void Main(string[] args) { MathOpt mathobj= null; //定义MathOpt对象变量 //mathobj = new MathOpt(); //创建对象 int result; result=mathobj.Add(100, 200); //调用类的方法 Console.WriteLine("100+200=" + result); //输出结果 Console.ReadKey(); //敲任意键结束整个程序 } 运行修改后的程序,Visual Studio这次报告出现了一个“未处理NullReferenceException”的错误。 当某个.NET程序在运行过程中引发了一个错误时,CLR会创建一个异常对象,将程序出错的信息封装到此对象中,NullReferenceException就是这样的一个异常对象,NullReferenceException对象包含的基本信息之一就是:未将对象引用设置到对象的实例。 交叉链接: CLR拥有一个异常处理子系统,向所有.NET应用程序(不论其编程语言如何)“一视同仁”地提供异常处理功能。有关这方面的内容,请看本书第9章——《CLR内部运行机理剖析》 现在分析一下:为何示例程序的运行会引发NullReferenceException异常? 关键是因为我们没有使用new关键字创建MathOpt对象就直接使用了变量mathobj。不创建类的对象就直接使用是编程中的一种常见错误。程序改起来很简单,恢复被注释掉的一句即可。 由此得到面向对象编程的第3个要点: (3)创建完类的对象并赋值给相同类型(或相兼容类型)的变量之后,可以通过此变量使用对象。 示例代码中mathobj这个变量用于引用一个真实的MathOpt对象,因此被称为“对象变量”,对象变量是面向对象编程中另一个非常重要的概念。 如前文所述,我们已经知道: 类是印章,对象是这个印章所盖出的印。 而对象变量可以用于代表任何一个印章盖出来的“印”,对象变量与对象(印)之间的关联是通过赋值语句确定的,一旦确立了这种关联,我们就可以说此对象变量就代表了这个“印”(即对象)本身。 一般用更专业的术语“引用(reference)”来代替上面的“代表”一词。即:对象变量用于“引用”某个真实对象。所以,对象变量是一种引用类型的变量。有关引用类型变量的相关内容,后面章节中还会有更深入的讨论。 现在回到本节示例,在MathOpt.cs文件中再次修改代码,将Add()方法前的public关键字删除后再编译程序,此时Visual Studio报告: “UseMathOpt.MathOpt.Add(int, int)”不可访问,因为它受保护级别限制 由此得到了面向对象编程的第4个要点: (4)只有声明为public的方法可以被外界调用。 要点4说明,虽然一个类中可以有多个方法,但不是所有的方法都可以被外界调用的,只有声明为公有(public)的方法才行,对外界而言,类中的非公有方法等于不存在一样。这就体现出面向对象程序的一个重要特性——封装。 只要保持类中声明为public的成员定义不变,程序员可以在类的内部添加新的私有成员,这种改变不会影响到外界调用代码的运行。这种“封装”特性使得在开发大规模的系统时多个软件工程师可以相互独立地工作,只需相互协商好类的对外接口(指类中声明为public的成员),就不必担心他们的工作成果无法协同工作。 3.1.3 不定义类而直接创建对象 在C#4.0中,我们可以使用var关键字定义一种奇怪的“没有类型”的变量(称为“隐式类型的局部变量”): var Sum = 100; Console.WriteLine(Sum * 2); 上述代码运行时,可以得到一个正确的结果:200。 貌似CLR很聪明地可以动态推断出Sum是一个int类型的变量。 事实上,CLR没那么聪明,C#编译器完成了类型推断的工作,知道Sum是一个int32类型的变量,并直接生成了将常量“100”赋值给它的IL代码,CLR仅仅只是机械地执行罢了。 当使用var定义隐式类型的局部变量时,必须保证编译器能推断得出变量类型,否则,不能通过编译。 由此可知,var只能用于方法内部定义局部变量,不能成为类的字段。例如,以下代码将无法通过编译: class A { var Value=100; } 之所以在这里介绍var关键字,是因为我们可以利用它实现“不定义类而直接创建一个对象”的目的。 请看以下代码(示例程序UseAnonymousType): var v = new { Amount = 108, Message = "Hello" }; 上述代码创建了一个“匿名对象”v,它拥有两个字段:Amount为int型,而Message为string型。 以下代码使用此对象v: Console.WriteLine("Amount:{0},Message:{1}", v.Amount, v.Message); 上述代码输出的结果是: Amount:108,Message:Hello 在这个例子中,我们好象违背了面向对象编程的基本原则了,没有定义类怎么就可以创建对象啦? 其实一切都是C#编译器在后面玩的魔术。 使用ildasm工具查看示例程序生成的程序集(UseAnonymousType.exe),可以看到有一个名字非常奇怪的类型生成(图3‑3)。 图 再打开Main()方法对应的IL代码,一切都真相大白。 原来C#编译器动态创建了一个匿名类型(它的名字如图 3‑3所示),然后将Main方法中的变量v设置为此类型的局部变量,它引用一个创建好了的此类型的对象。 v对象的两个字段Amount和Message的值,是通过直接调用匿名类型所定义的get_Amount()方法和get_Message()方法得到的。 所以,先定义好类再创建对象是面向对象编程的铁律。 对于匿名对象比较有趣的是我们可以写出这样的代码: var v = new { Amount = 108, Message = "Hello" }; Console.WriteLine(v); 上述代码输出: { Amount = 108, Message = Hello } 一连串的问题: (1)这里输出的结果是从哪儿来的? (2)Console.WriteLine()怎么可以直接接收一个临时创建出来的对象v?它怎么知道这个对象有几个字段? 如果您掌握了面向对象的基础知识,并且会使用ildasm和Reflector这两个工具,那么解释这个现象一点也不难。 首先,从Main()方法生成的IL代码中可以知道,“Console.WriteLine(v);”实际上调用的是Console.WriteLine()方法的以下重载[1]形式: public static void WriteLine(object value); 现在使用Reflector工具查看Console.WriteLine(object)方法的实现代码,会发现上述方法在内部调用参数value的ToString()方法生成一个字串,然后再输出此字串。 现在回到ildasm,找到C#编译器生成的匿名类型中的ToString()方法,打开它,看看里面的IL代码,就知道此匿名对象的toString()方法返回值是什么了。 现在剩余最后两个问题,涉及到C#为何要引入这样“隐晦”的语法特性: (1)为什么不直接指定数据类型而要使用隐式类型来定义变量? (2)匿名类型的对象到底有什么用? 回答是: 隐式类型变量和匿名对象主要用于LINQ中。 请看以下代码: //从数据库中提取产品信息 var productQuery = from prod in products select new { prod.Color, prod.Price }; //显示找到的产品信息 foreach (var v in productQuery) { Console.WriteLine("Color={0}, Price={1}",v.Color, v.Price); } 上述代码使用了匿名对象来生成数据库查询的结果。隐式类型的局部变量productQuery其真实类型为“IEnumerable<编译器自动生成的自定义匿名类型>”。 您读完了本节,是否体会到了了解面向对象的基础知识在掌握.NET技术时的重要性了吗? [1] 3.2节将介绍方法重载的基础知识 3.1.4 无所不在的对象 “类”和“对象”渗透到了.NET的每个技术领域。如果不深刻地理解这两个概念的内涵,在.NET世界中几乎寸步难行。 举个例子,在使用ASP.NET开发的Web应用程序中,每个.aspx网页其实就是一个类。当用户使用浏览器向Web服务器发出一个访问特定.aspx网页的HTTP请求时,ASP.NET运行时(ASP.NET runtime)会依据请求的URL找到相应的.aspx网页, “装配”出一个完速的页面类,然后,以此页面类为模板创建一个页面对象,调用此页面的ProcessRequset()方法生成HTML代码,然后发回给客户端浏览器。 由此可知,HTTP请求的响应过程就是一个以页面对象的创建为核心的过程: 浏览器发出HTTP请求-->ASP.NET Runtime生成页面类-->ASP.NET Runtime创建页面对象-->生成HTML代码-->发回给浏览器 再举一个例子,在使用WCF开发的分布式系统中,客户端可以在本地创建一个代理对象(proxy),此代理对象其实对应着远程服务器上的一个“WCF服务对象”,两者拥有一致的访问接口,而客户端对此本地“代理对象”的访问请求,将会被转发到远程的服务器上,由相应的“WCF服务对象”负责响应。 在上面举的两个例子中,涉及到了.NET两个重要的技术领域:ASP.NET和WCF,从前文“点到即止”的介绍中,您一定对“无所不在的对象”有了印象。很明显,如果不深刻地理解“类”和“对象”这两个基本概念,并且能灵活应用之,诸如ASP.NET和WCF这类复杂的技术,是根本无法把握的。 在.NET世界里,在一个“活着”的.NET应用程序中,“对象”无所不在! |