北京
本书的思路
从目前软件开发的就业形势来看,.NET程序员的就业率相当不错,因为使用.NET开发项目的公司特别多,微软公司对.NET的支持也比较完善。下面的图给出了从2008年到2013年人们对ASP.NET招聘职位的关注度,从最初的30多万提高到了目前的100多万。本书的目的就是让更多学习.NET的开发者能轻松就业。
本书的特色
本书的考题实例都具有较强的实践性,读者可以根据考题的具体情况,轻松完成实例的制作。本书的特点主要体现在以下几个方面。
本书的编排采用循序渐进的方式,适合初级、中级.NET 学习人员逐步掌握.NET Framework的基本原理。
本书精选大量典型的面试考题,在解题的过程中,不但分析题目所考查的知识点,还提供了解题思路及相关知识点的详细讲述。
本书在介绍.NET 技术时,采用了较简洁的例子,大多数考题的实例,读者可以快速完成编写并立即见到成效。
本书在介绍ASP.NET 技术时,以ASP.NET 的编译过程、无状态特性为起点,避免了.NET基础知识的重复学习,使读者能够快速掌握ASP.NET的程序编写。
本书有大量实际项目的开发方法提示,使读者可以轻松将知识应用到实际项目中。
本书的所有实例源代码都附在随书光盘中,方便读者使用。
本书的内容安排
本书内容突出了在.NET面试中或者是.NET项目开发中,必须掌握的技能和容易忽略的内容,对.NET面试者来说,可以快速掌握面试过程中考察的知识点,减少面试准备时间,提高面试成功率。本书共分为11章180余道面试题。
第1章 .NET 概念题
本章内容包含面向对象程序设计和.NET的基础概念题,由于这部分题目涉及的范围比较广,因此在很多公司的面试题中占了比较大的比重。
第2章 .NET 编程语言基础题
本章内容包含C#语言各方面的题目,涉及部分容易忽略的C#基本语法,如值类型和引用类型、装箱和拆箱操作、枚举和结构等,还有.NET平台的内存管理和异常处理机制。
第3章 基类、接口和泛型
本章包含基类应用(如System.DataTime、System.IO)、接口和集合、泛型方面的知识,相对前两章的内容,本章侧重实际应用,会用一些小示例来配合讲解每个面试题的解析。
第4章 .NET 高级特性
本章的内容包含了.NET框架中的高级特性,主要有委托、事件、反射、线程、序列化、LINQ、匿名方法等。
第5章 Windows 窗体编程
本章内容以窗体程序中控件及 GDI+相关的编程知识为主,.NET 提供了大量的控件以提升程序开发效率,读者可以通过这些面试题来重新认识控件。
第6章 ADO.NET 编程
本章包括ADO.NET在数据库方面的应用题目,涉及ADO.NET连接式访问数据库和断开式访问数据库的知识,其中会涉及SQL Server 数据库。
第7章 SQL查询及LINQ
本章讲述数据库的一些基本概念题,以及各种SQL查询语句相关的基础题目。本章还涉及.NET Framework 中的LINQ 技术,其相关的题目应用了LINQ 中比较基础同时也比较典型的几个知识点,其中包括LINQ to SQL 技术以及LINQ to XML 技术。
第8章 ASP.NET 程序开发1
本章讲述 ASP.NET 程序开发的相关题目。ASP.NET 是.NET 重要的组成部分,所以ASP.NET的题目在面试题目中占的比例较大。本章包括一些Code-Behind技术、@Page指令等常见的ASP.NET题目。
第9章 ASP.NET 程序开发2
本章的内容仍然和ASP.NET程序相关,主要分为Web.config配置文件相关题、ASP.NET数据相关题和建站技巧。本章的题目更贴近实际项目开发的知识需要和实践经验的考查,这要求面试者善于对开发经验的积累和归纳。
第10章 Web Service 和远程处理
.NET 技术可以应用多种远程应用技术,其中,Web Service逐渐被企业应用大量采用。本章先以简单的实例说明Web Service 的基本概念,这些概念在面试中或多或少都有涉及,理解了基本概念后编写实际项目的难度并不大。
第11章 算法和趣味题
本章讲述编程中的算法题目。算法是程序设计的核心,在笔试中出现的机会很多,面试者应做好充分的准备。对于同样一个问题可以用多种方法完成,面试应该选择最有效率的方法,这就是考官所要考查的内容,所以这种题目也充满了趣味性。
本书读者群
希望进入程序开发行业的新手。
希望快速转向.NET 平台进行程序开发的开发人员。
希望从事.NET 开发岗位的开发人员。
希望进一步提高.NET 开发水平的中级开发人员。
对.NET 平台技术感兴趣的爱好者。
ASP.NET 和C#入门人员。
编者
本章内容包含面向对象程序设计和.NET的基础概念题,由于这部分题目涉及的范围比较广,因此在很多公司的面试题中占了比较大的比重。很多.NET程序员在编写代码时非常熟练,但往往缺乏对基础知识的深刻理解,从而导致面试失败。这类知识是程序设计的基础,如果不加以重视,程序编写就没有创造性,只能学一步,做一步。
面向对象编程是当前流行的编程方式,被大多数高级语言支持。.NET程序同样是基于面向对象的设计,只有深刻理解面向对象的编程理念,才可以开发出结构良好的、更易维护的.NET程序。
说明:本书采用C#语言编写.NET程序示例。
【考点】面向对象程序设计思想概念的理解,面向对象设计的应用范围,用C#实现面向对象设计方法。
【出现频率】★★★★☆
【解答】
面向对象是程序开发的一种机制,其特征为封装、继承、多态及抽象。以面向对象的方式编写程序时,需要将复杂的项目抽象为多个对象互相协作的模型;然后编写类型的结构,声明或实现类型的成员,即描述对象的特征和行为。在项目中编写不同的类型完成不同的功能,最后在程序中通过类的实例处理实际的问题。
说明:此处类型不仅仅指类,还可能包括接口、结构等。
【分析】
面向对象编程简称为OOP,其“对象”的含义类似于生活中的“物体”。相对于以前的编程方式,面向对象编程更注重于人的思维方式,可极大地提高程序的开发效率。面向对象编程将程序看作多个互相分离的功能模块,各个模块之间分工合作,并且有着极低的耦合度。面向对象编程中最重要的概念是类(即class),类是描述各个功能模块的模板,而程序中工作的模块实体被称为对象(即object)。
类和对象的概念好比动物学的分类,猫科动物就是一个类,老虎也是一个类,并且属于猫科动物类,动物园中某只老虎的实体则是一个对象。老虎类拥有猫科动物类的所有特征和行为,但有自己独特的特征和行为。而某只老虎符合老虎类特征描述和行为定义,可能还有部分独特的特征。所以类可以继承另一个类,如老虎类继承了猫科动物类。类是产生对象的一个模板,对象拥有类的一切特征和行为。关于类和对象的基本关系如图1.1所示。
面向对象编程的重点是类的设计,面试者应能熟练地编写简单的类并创建对象,展示基本的OOP语法。以图1.1为例,编写图中相应的类,并通过虎类创建一只体重为100kg、长度为 200cm 的老虎对象。新建一个.cs 文件,并命名为 OopTest.cs,编写代码如程序1.1所示。
程序1.1 老虎对象的创建:OopTest.cs
01 using System;
02 class OopTest
03 {
04 static void Main(String[] args)
05 {
06 Console.WriteLine("请输入老虎对象的体重和长度(逗号分隔的整数):");
07 string input= Console.ReadLine();
08 int pos = input.IndexOf(",");
09 string w = input.Substring(0, pos);
10 string l = input.Substring(pos + 1);
11 Tiger chinatiger = new Tiger(w, l);
12 Console.WriteLine("老虎已经创建完成!");
13 Console.WriteLine("这只老虎的重量是:"+chinatiger.weight+"kg");
14 Console.WriteLine("这只老虎的长度是:" + chinatiger.length + "cm");
15 Console.WriteLine("老虎的特征是:" + Tiger.msg);
16 Console.WriteLine("老虎一般生存在:" + Tiger.habitat );
17 Console.WriteLine("老虎像猫吗?" + Tiger.cat());
18 }
19 }
20 class Mammal //哺乳动物类名称
21 {
22 protected static bool viviparous = true; //有胎生的特征
23 protected static bool Feeding = true; //有喂养的特征
24 }
25 class Felid : Mammal //猫科动物类名称(继承于哺乳动物类)
26 {
27 protected static bool catlike = true; //类似猫的特征
28 protected static bool sensitivity = true; //有敏感的特征
29 }
30 class Tiger : Felid //虎类名称(继承于猫科动物类)
31 {
32 //有各种特征
33 internal static string msg = "凶猛、会游泳、会爬树,有漂亮的花纹,被称为“兽中之王”。";
34 internal static string habitat = "森林"; //栖息地在森林
35 internal string weight; //体重
36 internal string length; //长度
37 internal Tiger(string w, string l) //构造函数,直接给体重和长度赋值
38 {
39 this.weight = w;
40 this.length = l;
41 }
42 internal static bool cat() //通过静态方法获取继承的属性
43 {
44 return Tiger.catlike;
45 }
46 }
在命令行下编译OopTest.cs后,执行OopTest程序,其效果如图1.2所示。
当用户首先输入了“50,100”,程序创建了一只老虎的对象,并访问了部分数据字段和方法。
说明:下文所有当前章的程序示例将在当前章编号的目录下创建并运行,最后进行归档,例如第1章每个示例文件归档到ch01目录下,而第2章每个示例文件归档到ch02目录下,依此类推。
【考点】理解类和对象的关系,在实际应用中类和对象的作用。
【出现频率】★★★★★
【解答】
类(即class)指一类事物,对象(即object)指属于这一类事物的实体。类定义了对象的相关数据和方法,类通过构造函数生成对象,对象实现了类的定义,且拥有具体的数据。在ch01目录下新建一个程序文件,并命名为ClassObj.cs,编写代码如程序1.2所示。
程序1.2 类和对象:ClassObj.cs
01 using System;
02 class ClassObj
03 {
04 static void Main(String[] args)
05 {
06 Console.WriteLine("请输入动物对象的类别(pig,dog,cat):");
07 string animal= Console.ReadLine(); //读取用户输入到animal变量
08 Console.WriteLine("请输入动物对象的品种(如“波斯”):");
09 string tp = Console.ReadLine(); //读取用户输入到tp变量
10 Console.WriteLine("请输入动物对象的体重:");
11 string w = Console.ReadLine(); //读取用户输入到w变量
12 switch (animal.ToLowerInvariant()) //判断animal变量值
13 {
14 case "pig":
15 Pig pg = new Pig(tp, w); //根据tp和w的值创建Pig类的对象
16 Console.Write("\n你创建了一只" + Pig.name + "!");
17 Console.WriteLine("它的品种是:" + pg.type + "!");
18 Console.WriteLine("这只" + Pig.name + "的重量是:" + pg.weight + "kg");
19 Console.WriteLine(Pig.name + "的特征是:" + Pig.msg );
20 break;
21 case "dog":
22 Dog dg = new Dog(tp, w); 根据tp和w的值创建// Dog类的对象
23 Console.Write("\n你创建了一只" + Dog.name + "!");
24 Console.WriteLine("它的品种是:" + dg.type + "!");
25 Console.WriteLine("这只" + Dog.name + "的重量是:" + dg.weight + "kg");
26 Console.WriteLine(Dog.name + "的特征是:" + Dog.msg );
27 break;
28 default:
29 Cat ct = new Cat(tp, w); //根据tp和w的值创建Cat类的对象
30 Console.Write("\n你创建了一只" + Cat.name + "!");
31 Console.WriteLine("它的品种是:" + ct.type + "!");
32 Console.WriteLine("这只" + Cat.name + "的重量是:" + ct.weight + "kg");
33 Console.WriteLine(Cat.name + "的特征是:" + Cat.msg );
34 break;
35 }
36
37 }
38 }
39 class Pig //猪类名称
40 {
41 internal static string name = "小猪";
42 internal static string msg = 肥胖、迟钝";" //有各种特征
43 internal string type; //品种
44 internal string weight; //体重
45 internal Pig(string tp, string w) //构造函数, 接给品种和体重赋值直
46 {
47 this.type = tp;
48 this.weight = w;
49 }
50 }
51 class Dog //狗类名称
52 {
53 internal static string name = "小狗";
54 internal static string msg = "属于犬科动物,是人类的好朋友"; //有各种特征
55 internal string type; //品种
56 internal string weight; //体重
57 internal Dog(string tp, string w) //构造函数,直接给品种和体重赋值
58 {
59 this.type = tp;
60 this.weight = w;
61 }
62 }
63 class Cat //猫类名称
64 {
65 internal static string name = "小猫";
66 internal static string msg = "安静、敏感"; //有各种特征
67 internal string type; //品种
68 internal string weight; //体重
69 internal Cat(string tp, string w) //构造函数,直接给品种和体重赋值
70 {
71 this.type = tp;
72 this.weight = w;
73 }
74 }
例子比较简单,但可以充分说明类和对象的关系,3个动物类的描述如图1.3所示。
图1.3为3个类的定义,在主程序中用户输入动物的类别、品种和体重数据后,即根据以上类为模板创建了一只小动物对象,并输出其数据。在命令行下编译ClassObj.cs后,执行ClassObj程序,其效果如图1.4所示。
【分析】
简单来说,类是用于描述对象的特征、状态和行为的模板,是抽象的概念。例如虎类可以用于描述所有老虎个体的共同特征及行为,但是虎类不能指定某一只老虎的个体。而对象指拥有具体特征、状态和行为的实体,例如某只老虎就是虎类的一个对象,它遵从类所描述的一切特征和行为。
【考点】理解访问权限存在的意义,各种访问权限的作用,在代码中灵活应用访问权限。
【出现频率】★★★☆☆
【解答】
1.private修饰符
private 修饰符用于设置类或类成员的访问权限仅为所属类的内部,private 也被称为私有修饰符。某些时候需要访问私有类成员时,可通过get和set访问器读取或修改。本例通过对类的私有成员及私有类的访问,展示private修饰符的保护作用。在ch01目录下新建一个程序文件,并命名为ModPrivate.cs,编写代码如程序1.3所示。
程序1.3 private修饰符示例:ModPrivate.cs
01 using System;
02
03 class ModPrivate
04 {
05 static void Main(String[] args)
06 {
07 Dog dg = new Dog("可卡");
08 Console.WriteLine("一只小狗创建成功!");
09 Console.WriteLine("这只"+dg.name + "的品种是:" + dg.type);
10 //由于参数类型为字符串的构造函数是私有的,这里不能直接创建实例对象
11 //Tiger tg = new Tiger("华南虎");
12 //由于Tiger类所嵌套的ZooTiger类是私有类,所以无法直接访问
13 //Tiger.ZooTiger tz = new Tiger.ZooTiger();
14 Tiger tg = new Tiger(true); //参数类型为布尔型的构造函数可创建Tiger类的对象
15 Console.WriteLine("\n一只老虎创建成功!");
16 Console.WriteLine("这只" + tg.name + "的品种是华南虎吗?" + tg.ischinatiger);
17
18 }
19 }
20 class Dog //狗类名称
21 {
22 internal string name = "小狗";
23 private string _type; //品种
24 internal Dog(string tp) //构造函数,直接给品种赋值
25 {
26 this._type = tp;
27 }
28 internal string type //type变量,get访问器获取私有成员_type的值
29 {
30 get { return this._type; }
31 }
32 }
33 class Tiger //虎类名称
34 {
35 internal string name = "老虎";
36 private string _type; //品种
37 private bool _ischinatiger; //是否为华南虎
38 private Tiger(string tp) //构造函数,直接给品种赋值
39 {
40 this._type = tp;
41 }
42 internal Tiger(bool chinatiger) //构造函数,直接给_ischinatiger赋布尔型值
43 {
44 this._ischinatiger = chinatiger;
45 }
46 internal string ischinatiger //ischinatiger变量,get访问器获取返回值
47 {
48 get
49 {
50 //由于同属于Tiger类的内部,所以可创建ZooTiger私有类的对象
51 ZooTiger zt = new ZooTiger();
52 //返回字符串,内容为私有成员_ischinatiger的值和ZooTiger的对象的name值
53 return this._ischinatiger + "(" + zt.name + ")";
54 }
55 }
56 private class ZooTiger
57 {
58 internal string name;
59 internal ZooTiger()
60 {
61 this.name = "动物园的老虎";
62 }
63 }
64 }
2.protected修饰符
protected 修饰符用于设置类或类成员的访问权限仅为所属类及子类的内部。本例通过对类的私有成员及私有类的访问,展示private修饰符的保护作用。在ch01目录下新建一个程序文件,并命名为ModProtected.cs,编写代码如程序1.4所示。
程序1.4 protected 修饰符示例:ModProtected.cs
01 using System;
02
03 class ModProtected
04 {
05 static void Main(String[] args)
06 {
07 Console.WriteLine("请输入所需创建老虎对象的品种(如东北虎、华南虎、孟加拉虎等):");
08 string input = Console.ReadLine(); //读取用户输入,并存储于input变量
09 string nm, tp;
10 if (input != "华南虎") //如果input变量存储的不是"华南虎"字符串
11 {
12 Tiger tg = new Tiger(input); //创建Tiger类的对象,并传递input变量的值
13 nm = tg.name; //将tg对象的name属性赋值给nm变量
14 tp = tg.type; //将tg对象的type属性赋值给tp变量
15
16 }
17 else
18 {
19 ChinaTiger tg = new ChinaTiger(); //创建ChinaTiger类的对象
20 //将tg对象的name属性(继承于父类中声明为protected的属性)赋值给nm变量
21 nm = tg.name;
22
23 //将tg对象的type属性(继承于父类中声明为protected的属性)赋值给tp变量
24 tp = tg.type;
25 }
26 Console.WriteLine("\n一只{0}创建成功!",nm);
27 Console.WriteLine("这只{0}的品种是:{1}" ,nm,tp);
28 }
29 }
30 class Tiger //虎类名称
31 {
32 protected string _name = "老虎";
33 protected string _type; //品种
34 internal Tiger() //无参数构造函数
35 {
36 }
37 internal Tiger(string t) //构造函数,直接给品种赋值
38 {
39 this._type = t;
40 }
41 internal string name //name变量,get访问器获取返回值
42 {
43 get
44 {
45 return this._name; //返回字符串,内容为私有成员_name的值
46 }
47 }
48 internal string type //type变量,get和set访问器获取返回值或写入值
49 {
50 get
51 {
52 return this._type; //返回字符串,内容为私有成员_type的值
53 }
54 set
55 {
56 this._type = value; //为私有成员_type赋值
57 }
58 }
59 }
60 class ChinaTiger : Tiger //华南虎类名称
61 {
62 internal ChinaTiger() //构造函数,直接给品种赋值
63 {
64 this._type = "华南虎"; //直接赋值"华南虎"字符串给父类中继承的_type属性
65 }
66 }
3.internal修饰符
internal修饰符修饰的类或类成员的访问权限为同一程序集内部,C#默认的类访问修饰符即为internal。前面两个示例中,需要供类外部代码访问的成员都使用了internal修饰符,因为这些类都处于同一程序集中。
4.public修饰符
public修饰符则为公共访问权限,对代码的访问没有任何限制。大多数情况下须谨慎使用public修饰符,因为滥用将影响类的封装性,并且带来安全隐患。
以下为代码的运行结果:
(1)在命令行下编译ModPrivate.cs后,执行ModPrivate程序,其效果如图1.5所示。
从本例代码中可知,ZooTiger类无法在类的外部直接访问,所有的私有成员只能在类的内部访问,本例采用了get访问器访问了小狗和老虎的品种,并创建了ZooTiger私有类的对象。有的读者可能会迷惑,如果同时使用get和set访问器private,修饰符意义何在?其实很多程序中确实有这样的做法,这样做向类的外部屏蔽了私有成员的实现方法,同时也隐藏了私有成员的实际名称,有利于封装性。例如本例g, et访问器中有两步操作,而外界无法获知。
(2)在命令行下编译 ModProtected.cs 后,执行 ModProtected 程序,其效果如图1.6所示。
本例接收用户的输入,当输入值为“华南虎”时,创建 ChinaTiger 类的对象,并通过构造函数赋值“华南虎”字符串给_type字段。_type字段声明中使用了protected修饰符,所以在Tiger类的子类(ChinaTiger类)中可被直接访问。
注意:即使派生类和基类在不同程序集中,派生类仍可访问基类的 protected修饰符成员。读者必须清楚的一点是,派生类继承了所有基类的成员,只是无法直接访问基类的private修饰符成员,但可访问protected修饰符成员。
【分析】
面向对象编程的特征之一就是封装性,而类就是封装性的典型体现。在生活中,人们不需要知道电器的内部构造,但是能很容易地使用电器,这就是封装性。在项目中需要编写很多功能类,在程序运行时只需要使用类所提供的功能,大部分类内部的代码实现需要封装,拒绝外部访问。这样大大增加了类的内部代码安全性和稳定性,同时多个功能类之间也减少了互相干扰的可能。访问权限修饰符即可将类及类的成员划分多种安全级别,根据不同需要设置访问权限。
internal和public访问修饰符是需要谨慎选择的,多数情况下应该尽量使用internal访问修饰符。
还有一种访问修饰符,即protected internal 修饰符,可在子类中或同一程序集内被访问。如果要声明成员只能被同一程序集内的派生类(子类)访问,则应首先设置所属类为internal,成员设置为protected。
【考点】对属性(Property)的理解,C#中get和set访问器的编写方法,理解自动实现的属性。
【出现频率】★★☆☆☆
【解答】
本例通过属性操作类中声明的私有字段_username,请注意在Name属性的get和set访问器中的逻辑操作。本例还有一个自动实现的属性,可读取用户输入的数据。在ch01目录下新建一个程序文件,并命名为GetSet.cs,编写代码如程序1.5所示。
程序1.5 属性示例:GetSet.cs
01 using System;
02
03 class GetSet
04 {
05 static void Main(String[] args)
06 {
07 Console.Write("请输入用户名:");
08 Detail dt = new Detail(); //创建Detail的对象
09 dt.Name = Console.ReadLine();
10 Console.WriteLine("Name属性写入完成!");
11 Console.WriteLine("\n您的用户名为{0}(读取Name属性)", dt.Name);
12 Console.Write("\n请输入密码:");
13 dt.Password = Console.ReadLine();
14 Console.WriteLine("Password属性写入完成!");
15 Console.WriteLine("\n您的密码为{0}(读取Password属性)", dt.Password);
16 }
17 }
18 class Detail
19 {
20 private string _username;
21 internal string Name
22 {
23 get
24 {
25 //从_username值的第6位开始截取并返回(字符串索引值从0开始)
26 return _username.Substring(5);
27 }
28 set
29 {
30 _username = "user-" + value; //在传入值前面加上"user-"
31 }
32 }
33 internal string Password //Password属性内部为自动属性实现
34 {
35 get;
36 set;
37 }
38 }
在命令行下编译GetSet.cs后,执行GetSet程序,其效果如图1.7所示。
本例中,首先创建Detail类的对象dt,将用户第1次输入数据赋值给dt对象的Name属性。在Name属性的set访问器中,用户输入的字符串前面拼接了“user-”字符串,然后赋值给_username 字段。当读取 Name 属性时, get 访问器将_username 字段的值执行Substring(5)方法,将“user-”字符串去掉后返回。从整个过程来看,用户无法知道属性中数据经过了什么处理及数据最终存储在何处。而用户输入第2次数据,其值被写入Password属性,自动属性实现可将值写入匿名后备字段,读取时亦可直接返回其值。
说明:本例仅展示了get和set访问器在属性中的使用,在索引器中使用方法是一样的。
【分析】
在前面的例子中使用了get和set访问器,通过访问器可以很方便地访问私有成员。其对外部暴露的部分可以为属性或索引器,属性比较常用。类体中的属性(Property)在使用时和一般的类成员没有区别,只是在内部实现的方法通过get和set访问器完成,这样更灵活、更隐蔽、更安全。其编写格式如以下代码所示:
访问修饰符 数据类型 属性名称
{
get
{
[访问修饰符2] 相关数据操作;
}
[访问修饰符3] set
{
和value关键字有关的数据操作;
}
}
当直接读取属性名称时,将会使用 get 访问器,执行“相关数据操作”的内容,相当于执行一个返回值为“数据类型”的方法。当直接赋值给属性名称时,被赋予的新值将替换隐式参数value,执行相关的数据操作。从代码中可知,get或set可添加访问修饰符,不过必须在get和set同时存在的情况下,且不能同时添加。当没有set访问器时,代表该属性为只读。
最常用的数据操作是读取和写入类中的私有字段(被称为后备字段),如果 get 和 set访问器中不需要逻辑,仅仅通过属性完成赋值和写入值的功能,可以使用自动实现的属性。自动实现的属性可以提供比较简洁的格式,并且编译器将自动创建一个匿名后备字段(在类体中没有声明),其编写方法如以下代码所示:
访问修饰符 数据类型 属性名称{get;set}
访问修饰符 数据类型 属性名称{get;private set}
第1种自动实现的属性可直接读取和写入,使用者并不知道数据读取或写入了哪个字段(匿名后备字段)。第2种自动实现的属性为只读,无法写入。
注意:属性提供了比较灵活的数据访问方法,读者编写代码时注意显式声明的set访问修饰符必须比属性修饰符更严格。get 或 set 没有显式访问修饰符时,其默认访问限制和属性一致。
【考点】sealed修饰符的意义,密封类和抽象类的关系,sealed修饰符的用法。
【出现频率】★★★☆☆
【解答】
在ch01目录下新建一个程序文件,并命名为Sealed.cs,编写代码如程序1.6所示。
程序1.6 sealed修饰符示例:Sealed.cs
01 using System;
02
03 class Sealed
04 {
05 static void Main(String[] args)
06 {
07 Console.WriteLine("请输入电脑CPU的名称:");
08 string c = Console.ReadLine();
09 Computer lenovo = new Computer(c); //创建电脑对象
10 Console.WriteLine("\n{0}所采用的CPU为{1}", lenovo.name, lenovo.cpu);
11 Phone nokia = new Phone(); //创建手机对象
12 nokia.display(); //调用重写后的密封方法display()
13 }
14 }
15 class Product
16 {
17 internal string name;
18 internal virtual void display()
19 {
20 Console.WriteLine("这是产品类的方法!");
21 }
22 }
23 sealed class Computer:Product //声明电脑类,继承于Product类
24 {
25 internal string cpu ;
26 internal Computer(string c)
27 {
28 this.cpu = c;
29 this.name = "电脑";
30 }
31 }
32 /*
33 class NoteBook : Computer
34 {
35 }
36 *声明本类将导致错误,因为Computer类为密封类,无法被继承
37 */
38 class Phone : Product //声明手机类,继承于Product类
39 {
40 internal sealed override void display()
41 {
42 Console.WriteLine("\n这是手机类的方法!");
43 }
44 }
45 class Phone3G : Phone //声明手机类,继承于Product类
46 {
47 /*internal override void display()
48 {
49 Console.WriteLine("\n这是3G手机类的方法!");
50 }
51 *声明此方法将导致错误,由于 display方法在其父类Phone中为密封方法,所以无法再重写
52 */
53 }
在命令行下编译Sealed.cs后,执行Sealed程序,其效果如图1.8所示。
从本例代码中可知,密封类一般情况下的使用方法和其他类一样,只是无法被继承。代码中Product类的虚方法display方法被Phone类重写,而被重写的display方法前面也加了sealed修饰符。在这种情况下,密封的display方法无法被所属类的子类继续重写,如本例代码中的Phone3G类,无法重写display方法。
说明:密封类可用于单一功能的实现,并且防止被意外地继承,产生非预期的效果,这也是封装性的体现。
【分析】
sealed修饰符用于修饰类、实例方法和属性。sealed用于修饰类时,该类无法被继承,所以该类也被称为密封类。而abstract类(抽象类)必须被继承才有意义,所以,sealed修饰符和abstract修饰符是互相排斥的,无法共存。密封方法会重写基类的方法,sealed用于修饰实例被重写的虚方法或虚属性时,表示该方法或属性无法再被重写。
注意:sealed修饰实例方法或属性时,必须和override一起使用。
【考点】静态类和静态类成员的理解,static在应用中的特殊性。
【出现频率】★★☆☆☆
【解答】
在ch01目录下新建一个程序文件,并命名为Static.cs,编写代码如程序1.7所示。
程序1.7 静态类及静态类成员示例:Static.cs
01 using System;
02
03 class Static
04 {
05 static void Main()
06 {
07 Console.Write("请输入电脑的CPU和内存规格,用英文逗号分隔:");
08 string input = Console.ReadLine(); //获取用户输入值并赋值给input变量
09 int pos = input.IndexOf(","); //获取input字符串中英文逗号的索引
10 //根据索引获取逗号前面部分的字符串并赋值给PC类的静态字段cpu
11 PC.cpu = input.Substring(0, pos);
12 //根据索引获取逗号后面部分的字符串并赋值给PC类的静态字段memory
13 PC.memory = input.Substring(pos + 1);
14 Console.Write("请输入电脑的单价和数量,用英文逗号分隔:");
15 string input2 = Console.ReadLine();
16 int pos2 = input2.IndexOf(",");
17 int p = Int32.Parse(input2.Substring(0, pos2));
18 int n = Int32.Parse(input2.Substring(pos2 + 1));
19 PC ibm = new PC(p, n); //将p和n值转换为int类型后传递给构造函数
20 Console.WriteLine("\n(1)你选择电脑的CPU是{0},内存是{1},总价是{2}元", PC.cpu,PC.memory, ibm.count());
21 CpuMsg.getmsg(); //调用静态类的静态方法
22 }
23 }
24 class PC
25 {
26 internal static string cpu="";
27 internal static string memory="";
28 private static int price;
29 private static int num;
30 internal PC(int p, int n) //构造函数接收2个参数并为2私有字段赋值
31 {
32 price = p;
33 num = n;
34 }
35 internal int count() //用于计算总价格的方法,返回值为int类型
36 {
37 return price * num;
38 }
39 }
40 static class CpuMsg
41 {
42 private static string _name="CPU";
43 private static string _comp = "Intel";
44 internal static void getmsg()
45 {
46 Console.WriteLine("(2)静态类包含的CPU名称为:{0};生产厂家为:{1}",_name,_comp);
47 }
48 }
在命令行下编译Static.cs后,执行Static程序,其效果如图1.9所示。
本例的Main方法中,首先将用户第1次输入的值通过逗号分隔为2个字符串,再分别给PC类的赋值静态字段,即cpu和memory;然后将用户的第2次输入也分隔为2个字符串,并转换为整数类型赋值给p和n。通过传递参数p和n给PC的构造函数创建了PC类的对象ibm,并进行了初始化。在用户输入值后的第1行访问了PC类的2个静态字段,并通过实例方法计算了总价。在第2行直接调用了静态类CpuMsg的静态方法getmsg(),静态类CpuMsg完全不需要实例化,可以很方便地直接在程序中使用。在.NET的类库中有很多类似的静态类,可以在程序中直接使用其方法,例如Math类。
【分析】
static是比较特殊的修饰符,它所修饰的类或类成员被称为静态类或静态类成员。
当类中某些成员不需要创建实例实现,则可将其声明为静态类成员。静态成员在访问类名而非对象名,同样,“this”关键字也无法访问静态成员时直接引用。这些成员可用作该类的多个对象共享的数据,因为静态类成员不依赖某个对象。声明静态类成员如以下代码所示:
访问修饰符 static 数据类型 类成员;
当类中没有和对象实例相关的成员时,即类体中只有静态成员,可声明该类为静态类。静态类无法用new 创建对象,所以并不能编写构造函数,并且该类是密封类(即无法被继承)。静态类的声明方法如以下代码所示:
访问修饰符 static class 类名称
{
静态类成员1;
静态类成员2;
静态类成员3;
...
}
必须注意的是,类中的常数声明和类型声明默认为静态,如类体中声明 1 个类,这个类默认为static,即无法被所属类的对象访问。
注意:声明静态类时,必须保证其内含成员全部为静态成员。
【考点】各种形态构造函数的理解,派生类的构造函数,构造函数的重载。
【出现频率】★★★★☆
【解答】
构造函数用于创建类的实例,并对实例进行初始化操作,通过不同的参数传递,可进行不同的实例初始化操作。本例通过多种不同形式的构造函数创建实例,并输出初始化的结果。在 ch01 目录下新建一个程序文件,并命名为 Constructor.cs,编写代码如程序1.8所示。
程序1.8 构造函数示例:Constructor.cs
01 using System;
02
03 class Constructor
04 {
05 static void Main()
06 {
07 Console.Write("请输入篮球比赛的选手人数:");
08 int inputA = Int32.Parse(Console.ReadLine()); //将用户输入值转换为int类型(这里没有作异常处理)
09 Console.Write("请输入篮球比赛的MVP:");
10 string inputB = Console.ReadLine();
11 Basketball bb = new Basketball(); //用Basketball类的默认构造函数创建实例bb
12 bb.getmsg(); //实例bb调用getmsg方法
13 Basketball bbb = new Basketball(inputA, inputB); //用Basketball类带2个参数的构造函数创建bbb
14 bbb.getmsg(); //实例bbb调用getmsg方法
15 Football fb = new Football(); //用Football类的默认构造函数创建实例fb
16 fb.getmsg(); //实例fb调用getmsg方法
17 Console.WriteLine("\n本次游泳比赛的冠军是{0}队", Swim.champ); //直接访问Swim类的静态字段
18 //Shoot sh = new Shoot();此处代码将会被编译器报错,因为其默认构造函数为私有的
19 }
20 }
21 class Basketball
22 {
23 private int _playernum;
24 private string _mvp;
25 internal Basketball()
26 {
27 }
28 internal Basketball(int n, string m)
29 {
30 _playernum = n;
31 _mvp = m;
32 }
33 internal void getmsg()
34 {
35 Console.WriteLine("\n这场篮球比赛的选手有{0}个,最有价值球员是{1}!", _playernum,_mvp);
36 }
37 }
38 class Football
39 {
40 private string _star = "Henry";
41 internal void getmsg()
42 {
43 Console.WriteLine("\n这场足球比赛的明星是{0}!", _star);
44 }
45 }
46 class Swim
47 {
48 internal static string champ;
49 static Swim() //静态构造函数,用于初始化静态成员
50 {
51 champ = "中国";
52 }
53 }
54 class Shoot
55 {
56 internal static string champ=null;
57 private Shoot() //私有构造函数,无法在类外部创建实例
58 {
59 }
60 }
在命令行下编译Constructor.cs后,执行Constructor程序,其效果如图1.10所示。
【分析】
前面的所有示例中都使用了构造函数,因为构造函数用于创建类的实例(对象)。在类中声明构造函数可对新实例(对象)进行初始化的操作,其编写方法如以下代码所示:
class 类名称
{
访问修饰符 类名称()
{
初始化操作;
}
}
可见,构造函数和类中的方法类似,也是一种函数,不过构造函数的名称必须和类名称相同。并且构造函数没有返回值,所以其函数签名和一般的函数有区别。没有参数的构造函数被称为默认构造函数,如果非静态类的类体中没有声明构造函数,类将自动提供一个默认构造函数,并将类成员初始化为默认值。
说明:结构类型(Struct)是值类型,不需要显式声明默认构造函数,编译器将自动生成默认构造函数。当用new运算符实例化时默认构造函数才被调用,将成员初始化为默认值。
通过不同的参数传递,在类体中可声明多个构造函数,即实现构造函数的重载。其编写方法如以下代码所示:
class 类名称
{
访问修饰符 类名称()
{
初始化操作1;
}
访问修饰符 类名称( 参数类型1 参数1......)
{
初始化操作2;
}
访问修饰符 类名称( 参数类型2 参数1......)
{
初始化操作3;
}
}
在程序中创建该类的实例(对象)时,通过传递参数的不同,调用不同的构造函数进行不同的初始化操作。程序中创建实例(对象)的方法如以下代码所示:
类型名称对象名称 = new构造函数(); //默认构造函数
类型名称对象名称 = new构造函数(参数列表);
一般情况下,构造函数是实例构造函数,即可通过该构造函数在类外部创建类的实例。反之,如果需要阻止创建类的实例,可在声明私有的默认构造函数,这种情况一般用于无实例成员的类中。如果需要完成只执行 1 次的操作,可以声明静态构造函数。这种构造函数在创建实例前或引用静态成员前自动调用,一般用于对静态成员的操作。
说明:无实例成员的类可声明为静态类,即无须声明私有的默认构造函数。
【考点】对类体内函数的深刻理解,对重载机制的应用,对override的理解。
【出现频率】★★★★☆
【解答】
方法的重载和重写容易被混淆,重载是方法的名称相同,函数签名不同,进行多次重载以适应不同的需要。而重写(override)是进行基类中函数的扩展或改写,其签名必须与被重写函数保持一致。
本例通过多种不同形式的构造函数创建实例,并输出初始化的结果。在ch01目录下新建一个程序文件,并命名为Override.cs,编写代码如程序1.9所示。
程序1.9 方法的重载和重写示例:Override.cs
01 using System;
02
03 class Override
04 {
05 static void Main()
06 {
07 PC ibm = new PC(); //创建PC类的实例ibm
08 Console.WriteLine("调用无参数的getmsg方法");
09 ibm.getmsg();
10 Console.WriteLine("\n调用1个字符串参数的getmsg方法");
11 ibm.getmsg("金士顿");
12 Console.WriteLine("\n调用2个参数的getmsg方法");
13 ibm.getmsg("金士顿",5000);
14
15 IntelCpu core = new IntelCpu(); //创建IntelCpu类的实例core
16 Console.WriteLine("\n被重写的getnameA方法(抽象)和getnameB方法(虚)\n");
17 core.getnameA();
18 core.getnameB();
19 }
20 }
21 class PC
22 {
23 private string _cpu;
24 private string _memory;
25 private int _price;
26 internal PC() //默认构造函数
27 {
28 _cpu = "英特尔";
29 }
30 internal void getmsg() //声明无参数的getmsg方法
31 {
32 Console.WriteLine("\n【1】电脑的CPU厂家为:{0}", _cpu);
33 }
34 internal void getmsg(string m) //声明1个字符串参数的getmsg方法
35 {
36 _memory = m;
37 Console.WriteLine("\n【2】电脑的内存厂家为:{0}", _memory);
38 }
39 internal void getmsg(string m, int p) //声明2个参数的getmsg方法
40 {
41 this.getmsg(); //调用无参数的getmsg方法
42 this.getmsg(m); //调用1个字符串参数的getmsg方法
43 _price = p;
44 Console.WriteLine("\n【3】电脑的价格为:{0}", _price);
45 }
46 }
47 abstract class CPU //声明抽象类CPU
48 {
49 abstract internal void getnameA(); //声明抽象方法getnameA
50 internal virtual void getnameB() //声明虚方法getnameB
51 {
52 Console.WriteLine("CPU主打品牌是?");
53 }
54 }
55 class IntelCpu : CPU //继承CPU抽象类
56 {
57 internal override void getnameA() //重写抽象方法getnameA
58 {
59 Console.WriteLine("【A】Intel CPU的主打品牌是酷睿");
60 }
61 internal override void getnameB() //重写虚方法getnameB
62 {
63 Console.WriteLine("【B】Intel CPU原来的主打品牌是奔腾");
64 }
65 }
在命令行下编译Override.cs后,执行Override程序,其效果如图1.11所示。
本例的PC类中,以3种不同的参数列表声明了3个getmsg方法,甚至第3个getmsg方法调用了前2个方法。
【分析】
方法的重载一般指通过对类中同名函数使用不同的签名,以声明多个函数体。简单地说,给函数定义不同的参数个数或不同的参数类型,可以声明不同的同名函数(返回值也可以不同)。简单的函数重载(在类中即为方法重载)如以下代码所示:
访问修饰符 返回类型1 函数名称(参数列表1)
{
函数体代码1;
}
访问修饰符 返回类型2 函数名称(参数列表2)
{
函数体代码1;
}
以上代码声明了 2 个函数,虽然函数名相同,但函数的签名不同,所以可以视做 2 个不同的函数。程序中调用时,通过不同参数传递执行不同的函数。
而override方法被称为重写方法,即在派生类中将所继承的方法进行扩展或改写,要求重写后的方法签名与被重写的方法签名一致。派生类中只有继承的虚方法或抽象方法可以被重写,并且静态方法不能被重写。其使用方法如以下代码所示:
访问修饰符返回类型 override 函数名称(参数列表)
{
函数体代码;
}
注意:派生类所继承的非密封override方法也可重写,因为该方法是被重写过的。
【考点】接口类型的理解,接口在程序中的意义。
【出现频率】★★★☆☆
【解答】
接口在程序设计中的作用为充当类或结构的功能界面,接口的属性、方法等属于抽象描述必须通过类或结构的实现才能使用。接口是使用者只知道接口有些什么功能,却不知道功能如何实现、由谁实现,这给程序的设计留下了很大的灵活性。例如某个项目由多个功能模块组成,每个模块由一个程序员完成,程序员只需编写完模块功能的实现后,留下该模块的接口供其他人使用。其他人在程序中只需直接使用接口的功能,而不必了解接口的功能如何实现等问题,其关系模型如图1.12所示。
说明:使用者指在程序中使用接口功能的代码编写者。
当功能模块能力无法满足需要或功能模块的需求有变更时,程序员只需将该功能模块的实现代码部分进行修改或扩展,其他调用接口的程序无须变动。接口的这种应用模式可称为Bridge模式,Bridge模式即为分离意图和实现,以得到更好的扩展性。本例以Computer为接口,通过PCA类和PCB类实现该接口的功能。在ch01目录下新建一个程序文件,并命名为Interface.cs,编写代码如程序1.10所示。
程序1.10 接口示例:Interface.cs
01 using System;
02
03 class Interface
04 {
05 static void Main()
06 {
07 Computer a = new PCA(); //创建Computer接口类型a,并引用PCA类的实例
08 Console.WriteLine("【A】第一台电脑的CPU是:{0}",a.getcpu());//调用接口a的getcpu方法
09 Console.WriteLine("这台电脑的显卡芯片是:{0}", a.videocard); //访问接口a的videocard属性
10 a = new PCB(); //Computer接口类型a改为引用PCB类的实例
11 Console.WriteLine("\n【B】第二台电脑的CPU是:{0}", a.getcpu()); //调用接口a的getcpu方法
12 Console.WriteLine("这台电脑的显卡芯片是:{0}", a.videocard); //访问接口a的videocard属性
13
14 Console.Write("\n【请输入你喜欢的显卡芯片型号】:");
15 //a.videocard = Console.ReadLine();无法完成操作,因为Computer接口声明中属性无set访问器
16 PCB b = new PCB();
17 b.videocard = Console.ReadLine(); //PCB类的实例可以写入videocard属性
18 Console.WriteLine("\n【C】第三台电脑的显卡芯片是:{0}", b.videocard);
19 }
20 }
21 interface Computer //声明接口类型Computer
22 {
23 string getcpu(); //声明接口方法getcpu
24 string videocard //声明接口只读属性videocard
25 {
26 get;
27 }
28
29 }
30 class PCA : Computer //声明类PCA,实现Computer接口
31 {
32 private string _vc = "Nvidia-Geforce 9600GT";
33 public string getcpu() //实现接口的getcpu方法
34 {
35 return "Intel-Core 2 Duo";
36 }
37 public string videocard //实现接口的videocard属性,并增加set访问器
38 {
39 get
40 {
41 return _vc;
42 }
43 set
44 {
45 _vc = value;
46 }
47 }
48 }
49 class PCB : Computer //声明类PCB,实现Computer接口
50 {
51 private string _vc = "AMD-Radeon 3690";
52 public string getcpu() //实现接口的getcpu方法
53 {
54 return "AMD-Athlon X2";
55 }
56 public string videocard //实现接口的videocard属性,并增加set访问器
57 {
58 get
59 {
60 return _vc;
61 }
62 set
63 {
64 _vc = value;
65 }
66 }
67 }
在命令行下编译Interface.cs后,执行Interface程序,其效果如图1.13所示。
本例程序运行时,创建PCA类的实例,并将引用赋给接口类型a变量。第1行输出时,接口类型的a直接调用getcpu方法,在第2行输出中,a直接访问videocard属性。其属性和方法的操作实际为PCA类的实例所执行。接下来将PCB类的实例引用赋给a,再次输出的内容中,同样为a调用getcpu方法并访问videocard属性,其操作实际为PCB类的实例执行。由于接口的定义中videocard属性是只读的,所以无法进行写入操作,但是在PCB类中的videocard属性可写,所以创建PCB类型的实例操作videocard属性时,可直接写入用户输入值。
【分析】
接口是面向对象编程中一个非常重要的类型,和抽象类非常相似。接口类型无法被实例化,只能被其派生类或结构实现,其编写方法如以下代码所示:
interface 接口名称 [: 接口名称1,接口名称2]
{
属性声明;
方法声明;
索引器声明;
事件声明;
}
从以上代码可得知,接口可以继承多个接口,而类只能继承一个基类(单继承)。接口可描述属性、方法、索引器和事件,不过接口只能作声明,无法实现,所有声明必须由继承此接口的类或结构实现。必须要注意的是,接口的访问权限为public,类或结构实现接口的成员必须保持public,并且实现方法的签名必须和接口方法签名一致。
【考点】抽象类的理解,抽象类和接口的区别。
【出现频率】★★★★★
【解答】
接口和抽象类非常相似,两者都无法实例化,并且未实现部分都由派生类实现,其应用模型如图1.14所示。
结合图1.14可知,接口与抽象类的主要区别有以下几点:
(1)抽象类只能派生类,而接口可以派生类和结构。
(2)抽象类的派生类也可以是抽象类,即抽象成员在派生类中不一定被完全实现。而接口要求其派生类或结构必须完全实现其成员。
(3)抽象类可以包含已经实现的成员,可以包含字段,而接口只包含未实现的成员,不能包含字段。并且接口及所含成员必须为public访问级别。
(4)类只能继承一个抽象类,但可以继承(实现)多个接口。
在具体的程序设计中,抽象类和接口的取舍应视程序的需要而定。抽象类可以用于归纳一组相似的、有共同特性的类,然后将这些类共同的成员提取到抽象类中,使抽象类作为这组类的基类。这样做到了代码的复用,不但节约了代码量,也减轻了维护的复杂度。然后将这组类中相似的方法或属性提取到抽象类中,成为抽象类的抽象成员,不提供具体实现,由这组类自己完成不同的实现。
说明:抽象类的应用非常类似于网页制作中的CSS外部样式文件,大量风格相同的页面可以共用这个CSS文件,并且在页面中可以对部分CSS属性进行改写。
而接口是一组类的功能集合,也可以说是一组类的协定集合,这组类负责实现这些功能,可以说接口内含的成员都是抽象的。类可以实现多个接口,这样可将意图和实现分离,接口可以暴露给其他程序直接使用,并且可以很方便地进行功能的扩展。两者的应用对比如图1.14所示。
本例以Computer为接口,通过PCA类和PCB类实现该接口的功能。在ch01目录下新建一个程序文件,并命名为Abstract.cs,编写代码如程序1.11所示。
程序1.11 接口和抽象类示例:Abstract.cs
01 using System;
02
03 class Abstract
04 {
05 static void Main()
06 {
07 Jacky a = new Jacky(); //创建Jacky类的实例a
08 //访问实例a的_msg字段
09 Console.WriteLine("【A】Jacky实例继承的_msg字段是:{0}", a._msg);
10 //调用实例a的getname方法
11 Console.WriteLine("【B】Jacky实例实现的getname方法是:{0}", a.getname());
12 //访问实例a的ismale属性
13 Console.WriteLine("【C】Jacky实例的ismale属性是:{0}", a.ismale);
14
15 Mariah b = new Mariah(); //创建Mariah类的实例b
16 //访问实例b的_msg字段
17 Console.WriteLine("\n【D】Mariah实例继承的_msg字段是:{0}", b._msg);
18 //调用实例b的getname方法
19 Console.WriteLine("【E】Mariah实例实现的getname方法是:{0}", b.getname());
20 //访问实例b的ismale属性
21 Console.WriteLine("【F】Mariah实例的ismale属性是:{0}", b.ismale);
22 }
23 }
24 abstract class Person //声明抽象类Person
25 {
26 //声明字段,作为所有派生类的共用字段
27 internal string _msg = "属于哺乳动物,是地球上的有智慧的高级生物";
28 abstract internal string getname(); //声明抽象方法getname
29 abstract internal bool ismale //声明抽象布尔类型属性ismale
30 {
31 get;
32 }
33 }
34 class Jacky : Person //声明类Jacky,继承Person类
35 {
36 internal override string getname() //实现getname方法
37 {
38 return "我是Jacky";
39 }
40 internal override bool ismale //实现ismale属性
41 {
42 get
43 {
44 return true;
45 }
46 }
47 }
48 abstract class Female : Person //声明类Female,继承Person类
49 {
50 internal override bool ismale //实现ismale属性
51 {
52 get
53 {
54 return false;
55 }
56 }
57 }
58 class Mariah : Female //声明类Mariah,继承Female类
59 {
60 internal override string getname() //实现getname方法
61 {
62 return "我是Mariah";
63 }
64 }
在命令行下编译Abstract.cs后,执行Abstract程序,其效果如图1.15所示。
本例代码中声明了名为 Person 的抽象类,类体中声明了两个抽象成员(1 个方法和 1个属性),Jacky类和Femal类继承了Person类。而Femal类只实现了抽象属性,所以Female必须仍然是抽象类,并且编写了Mariah类继承Female类,Mariah类实现了所继承的抽象方法。而Jacky类完全实现了Person类的抽象成员,所以Jacky类可以不是抽象类,可以创建实例。
程序运行时,创建Jacky类的实例a,并直接输出其_msg字段,还调用了a的getname方法,并访问了ismale属性。然后创建Mariah类的实例b,并进行相同的操作。从程序结果中可看出,Person抽象类的_msg字段为所有派生类的可复用字段,是派生类共同的部分。只有完全实现了 Person 类抽象成员的类才可以不是抽象类,如 Jacky 类,而没有完全实现的类如Female类仍然为抽象类。
【分析】
抽象类是一种用abstract关键字修饰的类,这种类仅用于被继承。类似于接口,抽象类无法创建实例,而类体可以声明多个未实现的抽象成员,这些成员由继承此类的派生类实现。其编写方法如以下代码所示:
abstract 类名称
{
abstract 方法声明;
abstract 属性声明;
其他类成员声明及实现;
}
可见,抽象类的类体中可包含实现的成员,而未实现的成员为抽象成员。抽象方法或属性本身就是隐性的virtual,所以派生类实现抽象方法或属性必须使用override关键字。继承抽象类的类如果没有完全实现抽象成员,仍然只能是抽象类,即派生的非抽象类必须完全实现抽象成员。抽象类也可以实现接口,这时抽象类必须实现所有的接口成员,也可以将继承的接口成员映射至抽象成员,并由其派生类来实现。
说明:抽象类的抽象成员不能使用virtual或static修饰。
【考点】工厂模式的理解,工厂模式在实际应用中的编写。
【出现频率】★★★☆☆
【解答】
在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时由于需求的变化,往往存在着更多系列对象的创建工作。为了绕过常规对象的创建方法(new运算符创建实例),工厂模式提供一种“封装机制”来减少使用程序和这种“多系列具体对象创建工作”的耦合性。
说明:这里的程序指客户程序之类的使用者。
简单工厂模式可以用于封装创建实例的实现部分,在应用接口的程序中被广泛使用,其应用模型如图1.16所示。
为了处理更加复杂的情况,可以将图中的产品进行再次细分为多个大类,用抽象类进行归纳,完成同大类产品共用代码的复用。然后将工厂类也相应地分为多个大类,用抽象类进行归纳。将图1.16改良后如图1.17所示。
说明:本图假设将A产品和B产品作为两大类产品(即将看作产品的具体实现类再次细分),每大类产品有两个产品,如A产品有A1和A2。
为了说明工厂模式在应用程序中的具体表现,在ch01目录下新建一个程序文件,并命名为Factory.cs,编写代码如程序1.12所示。
程序1.12 工厂模式示例:Factory.cs
01 using System;
02
03 class Factory
04 {
05 static void Main()
06 {
07 Console.WriteLine("男性中有2种人可以选择,编号如下所示:");
08 Console.WriteLine("【1】男孩\t【2】男人");
09 Console.Write("请输入你的选择:");
10 int input1 = Int32.Parse(Console.ReadLine()); //读取用户输入并转换为整型数据
11 Factory1 f1 = new Factory1(); //创建Factory1类的实例f1
12 Ihuman h1 = f1.gethuman(input1); //调用f1的gethuman方法,并传递用户输入参数
13 h1.getfav(); //调用h1的getfav方法
14 Console.WriteLine("【身份描述】我的身份是:{0}", h1.getstatus());
15 for(int j=0;j<50;j++) //输出一行虚线
16 {
17 Console.Write("-");
18 }
19 Console.WriteLine("\n女性中有2种人可以选择,编号如下所示:");
20 Console.WriteLine("【1】女孩\t【2】女人");
21 Console.Write("请输入你的选择:");
22 int input2 = Int32.Parse(Console.ReadLine()); //读取用户输入并转换为整型数据
23 Factory2 f2 = new Factory2(); //创建Factory2类的实例f2
24 Ihuman h2 = f2.gethuman(input2); //调用f2的gethuman方法,并传递用户输入参数
25 h2.getfav(); //调用h2的getfav方法
26 Console.WriteLine("【身份描述】我的身份是:{0}", h2.getstatus());
27 }
28 }
29 interface Ihuman //声明接口Ihuman
30 {
31 void getfav(); //声明未实现的getfav方法
32 string getstatus(); //声明未实现的getstatus方法
33 }
34 abstract class Children : Ihuman //声明抽象类Children,并实现接口Ihuman
35 {
36 protected string _status="孩子"; //定义_status字段,并赋予初值
37 public string getstatus() //实现Ihuman接口的getstatus方法
38 {
39 return _status; //getstatus方法可返回_status字段的内容
40 }
41 abstract public void getfav(); //将Ihuman接口的getfav方法映射到抽象方法实现
42 }
43 class Boy : Children //声明Boy类,并继承Children类
44 {
45 public override void getfav() //实现(重写)抽象的getfav方法
46 {
47 Console.WriteLine("【男孩】我喜欢玩电子游戏,还喜欢恶作剧。");
48 }
49 }
50 class Girl : Children //声明Girl类,并继承Children类
51 {
52 public override void getfav() //实现(重写)抽象的getfav方法
53 {
54 Console.WriteLine("【女孩】我喜欢洋娃娃、小宠物。");
55 }
56 }
57 abstract class Adult : Ihuman //声明抽象类Adult,并实现接口Ihuman
58 {
59 protected string _status = "成年人"; //定义_status字段,并赋予初值
60 public string getstatus() //实现Ihuman接口的getstatus方法
61 {
62 return _status; //getstatus方法可返回_status字段的内容
63 }
64 abstract public void getfav(); //将Ihuman接口的getfav方法映射到抽象方法实现
65 }
66 class Man : Adult //声明Man类,并继承Adult类
67 {
68 public override void getfav() //实现(重写)抽象的getfav方法
69 {
70 Console.WriteLine("【男人】我喜欢阅读、编程。");
71 }
72 }
73 class Woman : Adult //声明Woman类,并继承Adult类
74 {
75 public override void getfav() //实现(重写)抽象的getfav方法
76 {
77 Console.WriteLine("【女人】我喜欢听音乐、逛街。");
78 }
79 }
80 abstract class HumanFactory //声明HumanFactory抽象类
81 {
82 protected Ihuman _h1=new Boy(); //定义Ihuman接口类型的字段,分别创建相应的类实例
83 protected Ihuman _h2 = new Man();
84 protected Ihuman _h3 = new Girl();
85 protected Ihuman _h4 = new Woman();
86 abstract public Ihuman gethuman(int i);
87 }
88 class Factory1 : HumanFactory //声明Factory1类,并继承HumanFactory类
89 {
90 public override Ihuman gethuman(int i) //重写继承的gethuman抽象方法
91 {
92 switch (i) //对参数进行判断,并返回不同的字段值
93 {
94 case 1:
95 return _h1;
96 case 2:
97 return _h2;
98 default:
99 return _h1;
00 }
101 }
102 }
103 class Factory2 : HumanFactory //声明Factory2类,并继承HumanFactory类
104 {
105 public override Ihuman gethuman(int i) //重写继承的gethuman抽象方法
106 {
107 switch (i) //对参数进行判断,并返回不同的字段值
108 {
109 case 1:
110 return _h3;
111 case 2:
112 return _h4;
113 default:
114 return _h3;
115 }
116 }
117 }
在命令行下编译Factory.cs后,执行Factory程序,其效果如图1.18所示。
本例声明了多个类,代码略显复杂,但是只要理解了图 1.17,其实也容易掌握。在代码中,首先声明了一个接口,即Ihuman,其中含有两个未实现的方法。实现接口的类是两个抽象类,即Children(孩子)类和Adult(成年人)类,这两个类分别归纳了Boy类和Girl类,以及Man和Woman类。接口的getstatus方法成员在抽象类中实现,而getfav方法则映射为抽象方法,被抽象类的派生类实现。最后通过HumanFactory抽象类的两个派生类,根据不同的参数传递,创建不同的实例引用,并返回接口类型。
在主程序中,分别创建Factory1类和Factory2类的实例(f1和f2),并调用其gethuman方法。根据用户的输入决定创建哪个类的实例引用,并返回一个接口类型引用变量(h1 和h2)。接口类型的引用变量调用两个方法时,使用者无法知道方法如何实现、由谁来实现。
【分析】
前面讲解接口的时候,着重分析了Bridge模式,接口可以简单地完成意图与实现的分离,以实现Bridge模式。由于类可以实现多个接口,所以类可以通过多个接口向外界提供多组不同的功能。接口反映了面向对象编程的特征之一,即多态,多态指通过相同方法得到不同的表现。接口也反映了面向对象编程的另一个特征,即封装,使用者并不清楚接口成员实现的细节,如以下代码所示:
interface Ibook //声明接口Ibook
{
void read(); //声明未实现的read方法
}
class BookA : Ibook
{
public void read() //实现read方法
{
Console.WriteLine("你在看A书。");
}
}
class BookB : Ibook
{
public void read() //实现read方法
{
Console.WriteLine("你在看B书。");
}
}
//使用接口的程序代码部分:
Ibook abc = new BookB(); //创建Ibook类的BookB实例引用
abc.read(); //调用read方法
接口类型的abc可以引用不同类的实例,以致相同的read方法可以有不同表现。但是以上代码的程序部分中,使用者仍然需要用 new运算符进行相应的实例化,同时,使用者还是知道read 方法由哪个派生类实现。为了进一步分离意图和实现,并对实现部分更好地封装,简单工厂模式可以进行一定的改良,添加代码如下所示:
class Factory
{
private Ibook _bka = new BookA();
private Ibook _bkb = new BookB();
public Ibook getbook(int i)
{
switch (i)
{
case 1:
return _bka;
case 2:
return _bkb;
default:
return _bka;
}
}
}
//使用接口的程序代码部分修改如下:
Factory f = new Factory();
Ibook abc = f.getbook(2); //调用f的getbook方法,并传递参数2
abc.read(); //调用read方法
Factory 类封装了将接口各个派生类实例化的代码,这样,使用者只需要创建 Factory类的实例,并调用getbook方法即可。向getbook方法传递不同的整数类型参数,可以创建不同的实例引用,而这些实例都是Ibook接口类型。如本例中,传递1将创建BookA类的实例引用(Ibook接口类型),传递2将创建BookB类的实例引用(Ibook接口类型)。使用者只知道传递数字来使用接口提供的不同功能,对内部实现却一无所知。Factory 类则用于实例化各种类,相当于生产产品的工厂,其产品供接口类型的实例引用,这也是称其为工厂模式的原因。
说明:本例中工厂类的getbook方法使用switch条件分支判断,然后返回相应的实例引用,在实例种类很多的情况下不大适用。根据具体情况不同,可以考虑利用反射、泛型等方法进行改进。
【考点】this的理解,base的理解。
【出现频率】★★☆☆☆
【解答】
this关键字用于引用类的当前实例。base关键字用于派生类访问基类成员。
为了说明this和base在类中的具体应用,在ch01目录下新建一个程序文件,并命名为This.cs,编写代码如程序1.13所示。
程序1.13 this 和base示例:This.cs
01 using System;
02
03 class This
04 {
05 static void Main()
06 {
07 Console.WriteLine("请输入书名:");
08 string inputA = Console.ReadLine();
09 Console.WriteLine("请输入作者:");
10 string inputB = Console.ReadLine();
11 //将用户输入的2个字符串传递给构造函数,创建Book类的实例bk
12 Book bk = new Book(inputA, inputB);
13 //调用bk实例的getbook方法,并输出
14 Console.WriteLine(bk.getbook());
15 //创建PCBook类的实例pcbk
16 PCBook pcbk = new PCBook();
17 //调用pcbk实例的words方法
18 pcbk.words();
19 }
20 }
21
22 class Book //声明Book类
23 {
24 private string _name;
25 private string _author;
26 internal Book() //编写默认构造函数
27 {
28 Console.WriteLine("\n书是人类进步的阶梯!");
29 }
30 internal Book(string n, string a) //编写重载构造函数
31 {
32 this._name = n;
33 this._author = a;
34 }
35 internal string getbook() //定义getbook方法
36 {
37 //拼接字符串,访问当前实例的Name属性,并调用Tool类的静态add方法,当前实例作为参数
38 string booktxt = "\n【BookName】" + this.Name + "【Author】"+ Tool.add(this);
39 return booktxt;
40 }
41 internal string Name
42 {
43 get
44 {
45 return _name;
46 }
47 }
48 internal string Author
49 {
50 get
51 {
52 return _author;
53 }
54 }
55 internal virtual void words() //定义words虚方法
56 {
57 Console.WriteLine("\n知识就是力量!");
58 }
59 }
60 static class Tool
61 {
62 internal static string add(Book b) //定义静态方法add,接收参数为Book类型
63 {
64 return b.Author;
65 }
66 }
67 class PCBook : Book //声明PCBook类,继承Book类
68 {
69 internal PCBook() : base() //默认构造函数预先调用基类默认构造函数
70 {
71 Console.WriteLine("来买计算机书籍吧!");
72 }
73 internal override void words()
74 {
75 base.words(); //调用基类的words虚方法
76 Console.WriteLine("计算机知识也是力量!");
77 }
78 }
在命令行下编译This.cs后,执行This程序,其效果如图1.19所示。
本例展示了this和base在类中的应用,其程序工作步骤如下所示。
(1)主程序中接收了用户输入的两个值(书名和作者),然后将这两个值传递给 Book类的构造函数,创建实例bk。这个步骤中,bk对象的_name字段和_author字段被赋予了新值,可见,this的作用即引用当前的实例对象,其代码如下:
this._name = n; //n为构造函数接收的第1个字符串参数
this._author = a; //a为构造函数接收的第2个字符串参数
(2)调用bk对象的getbook方法,其方法体调用了Tool类的静态方法add静态方法,并通过this向其传递当前实例。getbook方法实际执行代码如下:
//Tool.add(this)返回当前实例的Author属性
string booktxt = "\n【BookName】" + this.Name + "【Author】"+ this.Author;
//本方法最终返回booktxt变量
return booktxt;
(3)创建PCBook类的实例pcbk,由于其默认构造函数将通过base调用基类的默认构造函数,所以创建pcbk的实例将执行以下代码:
Console.WriteLine("\n书是人类进步的阶梯!"); //通过base()调用基类的默认构造函数
Console.WriteLine("来买计算机书籍吧!"); //PCBook类的默认构造函数的函数体
(4)pcbk调用words方法,PCBook类的words方法继承并重写了Book类的words方法。PCBook类的words方法体中通过base.words(),调用了基类的words方法(未被重写)。所以pcbk对象的words方法实际执行代码如下:
Console.WriteLine("\n知识就是力量!"); //通过base.words()调用基类的words方法
Console.WriteLine("计算机知识也是力量!");
【分析】
在面向对象的编程中,this 访问关键字使用非常频繁,其中文意思为“这个”,非常形象地描述了this关键字的作用。类通过创建实例执行具体的任务,而类体代码中的this用于引用类的当前实例。相应地,静态成员和实例无关,所以静态成员中不能使用this。
注意:this仅限于构造函数和方法成员中使用。
base访问关键字可用于访问基类成员,即基类被重写的方法和基类的构造函数。由于派生类继承了所有的基类成员,所以一般的基类成员可直接访问,但是基类被重写的虚方法只能通过 base访问。同样,如果创建派生类的实例,其构造函数可通过 base 访问基类的构造函数,复用基类构造函数体的代码。这两种情况下,base的使用方法如以下代码所示:
base.方法名([参数列表]); //用于派生类访问基类被重写的方法
派生类名称([参数列表]) : base(参数列表) //派生类构造函数预先调用基类构造函数
{
}
注意:base关键字访问基类的成员时,必须保证基类成员有相应的访问权限。
【考点】索引器的理解,this在索引器中的作用。
【出现频率】★★☆☆☆
【解答】
索引器可以使客户程序很方便地访问类中的集合或数组,访问方法类似于通过索引访问数组,并且索引器向客户程序隐藏了内部的数据结构。索引器和属性同样使用 get 和 set访问器读取、写入值,不过索引器的get和set访问器必须具有与索引器相同的形参表。但是属性可以为静态成员,而索引器必须为实例成员。索引器不支持类似于属性的自动实现的语法。
说明:形参表即为声明索引器时接收的形式参数。
本例描述了索引器在类中的具体应用,其参数可为整型、字符串等类型,也可为多个(如索引多维数组)。在 ch01 目录下新建一个程序文件,并命名为 Index.cs,编写代码如程序1.14所示。
程序1.14 索引器示例:Index.cs
01 using System;
02
03 class Index
04 {
05 static void Main()
15 }
06 {
07 Console.WriteLine("我的暑假安排:");
08 Plan p = new Plan(); //创建Plan类型的实例p
09 for (int i = 0; i < p.Length; i++) //从0到days数组元素个数的循环
10 {
11 Console.Write("+" + p[i] + "\t+"); //输出days数组的元素值
12 Console.Write(p[p[i]] + "\n"); //输出content数组的元素值
13 }
14 }
16
17 class Plan //声明Plan类
18 {
19 //声明2个字符串类型的数组字段
20 private string[] days = new string[7] { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日" };
21 private string[] content = new string[7] { "看书", "游泳", "打篮球", "踢足球", "和朋友一起玩游戏", "参加计算机培训班", "做暑假作业"};
22 //用this声明属性,并接收1个整数类型的参数,只有get访问器,表明只读
23 internal string this[int i]
24 {
25 get
26 {
27 return days[i];
28 }
29 }
30 //用this声明属性,并接收1个字符串类型的参数,只有get访问器,表明只读
31 internal string this[string s]
32 {
33 get
34 {
35 return content[getindex(s)];
36 }
37 }
38 //声明getindex方法,接收1个字符串参数,可返回参数在days数组中的索引值
39 private int getindex(string s)
40 {
41 int j = 0; //用于days属性值的索引计数
42 foreach (string t in days) //遍历days数组元素
43 {
44 if (s == t) //判断所接收字符串参数是否等于days数组
45 {
46 return j; //如果参数等于days数组某个元素值,则返回这个元素的索引值(整型)
47 }
48 j++;
49 }
50 return -1;
51 }
52 //声明Length属性,用于访问days数组的元素个数
53 internal int Length
54 {
55 get
56 {
57 return days.Length;
58 }
59 }
60 }
除了在类中应用索引器,在接口中也可以声明索引器,并被其他类实现,在ch01目录下新建一个程序文件,并命名为IndexInterface.cs,编写代码如程序1.15所示。
程序1.15 接口索引器示例:IndexInterface.cs
01 using System;
02
03 class IndexInterface
04 {
05 static void Main()
06 {
07 Iplan p = new Plan(); //创建Iplan类型的p,引用Plan类的实例
08 for (int i = 0; i < p.Length; i++) //从0到days数组元素个数的循环
09 {
10 if (i % 3 == 0) //判断计数器i是否为3的倍数或i是否为0
11 {
12 Console.Write("\n+" + p[i] + "\t"); //输出days数组的元素值,前面加换行
13 }
14 else
15 {
16 Console.Write("+" + p[i] + "\t"); //输出days数组的元素值
17 }
18 }
19 }
20 }
21 interface Iplan //声明Iplan接口
22 {
23 string this[int i] //声明未实现的索引器,接收1个整型参数
24 {
25 get;
26 }
27 int Length //声明未实现的Length属性
28 {
29 get;
30 }
31 }
32 class Plan : Iplan //声明Plan,并实现接口
33 {
34 //声明2个字符串类型的数组字段
35 private string[] days = new string[7] { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日" };
36 //实现接口的索引器
37 public string this[int i]
38 {
39 get
40 {
41 return days[i];
42 }
43 }
44 //实现接口的属性
45 public int Length
46 {
47 get
48 {
49 return days.Length;
50 }
51 }
52 }
在命令行下编译Index.cs后,执行Index程序,其效果如图1.20所示。
在索引器示例中,声明了一个Plan类(代表计划),存储了一个学生在暑假中每个礼拜周一到周日的活动。类中定义了两个字符串类型的数组字段,days数组用于存储周几,content数组则相应地存储了每天安排的活动内容。类中定义了两个索引器,接收整型参数的索引器可返回days数组相应的元素值,而接收字符串参数的索引器则返回content数组相应的元素。整个程序工作步骤如下所示:
(1)创建Plan类的实例p,通过p实例可以直接使用索引器访问集合。
(2)建立一个for循环,以i变量为计数器,i初始值为0,终止值为p的Length属性减去1。在类中,p的Length属性可返回days数组的元素个数,本例中p的Length属性值为7,则for循环次数为7次。
(3)在循环体中,依次输出两个索引器的返回值。
说明:本例的重点在于字符串参数在索引器中的使用,通过 getindex 方法,判断字符串参数是否等于days中的元素,如果相等则返回索引值(整型)。最后通过索引值查询content数组相应索引的元素,这样就将两个数组联系起来了。
在命令行下编译IndexInterface.cs后,执行IndexInterface程序,其效果如图1.21所示。
接口的索引器和类中的索引器差别不大,只是类似于接口方法的声明,接口中索引器不能提供实现,必须由实现接口的类实现这个索引器。具体在本例中,声明了Iplan接口,接口中声明了索引器和Lenth属性,并且索引器和属性都是只读的。Plan类实现了这个接口,其索引器返回days数组的元素值,Length属性返回days数组的元素个数。程序工作的步骤如下:
(1)创建Iplan类型的引用变量p,并引用Plan类的实例。
(2)建立1个for循环,以i变量为计数器,i初始值为0,终止值为p的Length属性减去1。本例中p的Length属性值为7,则for循环次数为7次。
(3)在循环体中,首先判断i是否为3的倍数或i是否为0,如果是则换行输出索引器的返回值,否则直接输出索引器的返回值。
【分析】
类似于属性,访问类或结构的集合或数组类型可用索引器实现,索引器用 this 关键字声明。其声明方法如下代码所示:
数据类型 this[参数列表]
{
get
{
以 参数为索引返回集合或数组数据;
}
set
{
分配值到参数索引的元素;
}
}
可见,索引器的访问器和属性类似,都是使用 get 和 set,不过索引器的访问器不能添加修饰符。通过get和set访问器的选用,可确定索引器可读写、只读和只写的功能。索引器的参数类型和参数个数是索引器的签名,在同一个类中,多个索引器必须有不同的签名。
【考点】.NET程序集的知识,.NET应用程序域的理解,.NET应用程序域与程序集的简单应用。
【出现频率】★★★☆☆
【解答】
.NET的程序集用于解决DLL HELL,DLL HELL是指与DLL有关的问题。程序集是自我描述的安装单元,这是一个逻辑单元,而非一个文件。程序集可以是包含元数据的多个文件,也可以是一个DLL或EXE文件。简而言之,程序集是作为整体发布的.NET可执行程序或.NET可执行程序的一部分,包含了程序的文件集或资源文件。
程序集分为私有程序集和共享程序集,私有程序集是创建.NET项目时默认的,也是比较常用的方式。私有程序集以可执行文件或库的形式提供应用程序,库中的代码只服务于这个应用程序。而共享程序集是一个公共库,可服务于系统中所有程序。共享程序集必须安装到.NET的特别目录中,而其他被服务的程序则不需要知道安装的地方。
【分析】
任何.NET 程序均由程序集构成,程序集是包含已编译的、面向.NET Framework 的代码的逻辑单元。当程序集存储于多个文件当中时,则一定有一个主文件包含程序集主程序的入口,这个包含入口的主文件描述了位于相同程序集的其他文件。程序集包含描述自身的元数据,这种元数据位于程序集清单中,可用于检查版本以及自身的完整性。
说明:动态程序集位于内存中,而非存储于文件中。
应用程序域是.NET中的程序的“边界”。相对于进程边界,应用程序域边界范围更小。在Windows 7/XP 中,进程可以有效保证不同的程序安全地运行,当某个程序出错时并不会影响其他程序。但是进程对多程序运行的系统性能作出了妥协,因为进程之间不允许共享内存。可以使用基于DLL的组件解决这个问题,将所有的DLL组件在一个地址空间中运行,不过当某个组件出错时将会影响其他组件。使用应用程序域可以分离组件,并且不会导致类似于进程的性能问题。
说明:进程有独立的虚拟内存,以保证进程之间的内存无法互写。
在一个进程内可容纳多个应用程序域,这样,.NET中的应用程序域可使多个应用程序运行于同一个进程。在没有代理的情况下,不同的应用程序域中的实例和静态成员无法共享,这样也保证了安全性。
说明:程序集的代码只需加载一次,以减少内存消耗。
本例展示了程序集和应用程序域的简单应用。在VS环境(VS2008或VS2010)中创建一个新控制台应用程序,名称为AppA,在Program.cs中编写代码如程序1.16所示。
程序1.16 AppA 项目:Program.cs
01 using System;
02 using System.Collections.Generic;
03 using System.Text;
04
05 namespace AppDomainTest
06 {
07 class Program
08 {
09 static void Main(string[] args)
10 {
11 //创建AppDomain类型的current,用于引用AppDomain.CurrentDomain
12 AppDomain current = AppDomain.CurrentDomain;
13 //输出程序的应用程序域的名称
14 Console.WriteLine("大家好,我是程序集AppA,我所在的应用程序域是{0}。",current.FriendlyName);
15 }
16 }
17 }
执行本程序,可得到结果如图1.22所示。
可见,当应用程序运行时,默认所在的应用程序域的名称为程序名称。现在 AppA 项目已经编译了最简单的私有程序集,位于ch01\AppA\AppA\bin\Debug目录下。接下来用同样的方法在VS环境中创建新项目,名称为AppB,在其Program.cs中编写代码如程序1.17所示。
程序1.17 AppB 项目:Program.cs
01 using System;
02 using System.Collections.Generic;
03 using System.Text;
04
05 namespace AppDomainTest
06 {
07 class Program
08 {
09 static void Main(string[] args)
10 {
11 //创建AppDomain类型的one,用于引用AppDomain.CurrentDomain
12 AppDomain one = AppDomain.CurrentDomain;
13 //输出one的基目录以及名称、上下文信息
14 Console.WriteLine("大家好,我是程序集B,我的基目录:{0}\n我所在的应用程序域的名称及上下文策略:{1}", one.BaseDirectory, one.ToString());
15 //在当前的应用程序域中加载程序集AppA.exe
16 one.ExecuteAssembly("AppA.exe");
17 Console.WriteLine("\n上面的AppA程序集和下面的AppA程序集位于不同的应用程序域\n");
18 //创建AppDomain类型的tow,用于引用AppDomain类创建的新应用程序域,其名称为ROOM-A
19 AppDomain two = AppDomain.CreateDomain("ROOM-A");
20 //two应用程序域装载(执行)AppA.exe程序集
21 two.ExecuteAssembly("AppA.exe");
22 //输出two的基目录以及名称、上下文信息
23 Console.WriteLine("AppA程序集的基目录:{0}\nAppA程序集所在的应用程序域的名称及上下文策略:{1}", two.BaseDirectory, two.ToString());
24 }
25 }
26 }
执行程序前,首先需添加程序集引用,在VS中的菜单栏中单击“项目|添加引用”,在添加引用的对话框中单击“浏览”选项,如图1.23所示。
浏览AppA项目的程序集路径,即ch01\AppA\AppA\bin\Debug目录,选中AppA.exe,单击“确定”按钮即可。这步的操作实际上把AppA程序集直接复制到AppB项目程序集的相同目录下,即复制到ch01\AppB\AppB\bin\Debug目录下。这也充分展示了私有程序集安装的便捷性,现在,AppB应用程序执行时即可将AppA程序集装载到新建的应用程序域了。执行AppB项目的程序,可得到结果如图1.24所示。
如图所示, AppA 程序集首先被加载到当前应用程序域,然后被加载到名称为“ROOM-A”的应用程序域中。从任务管理器中观察,AppA 程序集并没有创建新的进程。AppB程序集所在的应用程序域被称为进程中的主应用程序域,这是运行时自动创建的。
说明:实际上AppA程序集已加载到AppB.exe进程中运行,这就达到了多个应用程序在同一个进程中运行的目的。
本节问题相对比较靠近.NET的底层,编程者必须理解程序基本的运行过程,才能写出更高效的程序。从本节的代码中可知,AppDomain 类用于创建和中断应用程序域,加载和卸载程序集合类等功能,另外AppDomain类还可以枚举应用程序域中的程序集和线程。由于程序集包含了元数据,其中含有所有定义的类型以及这些类型成员的细节,所以可通过反射技术来获取这些数据。
本节示例主要展示了应用程序域的简单使用,其“边界”作用不同于进程,不同应用程序域中的应用程序有自己独立的内存空间。在默认情况下,这些程序互相隔离,保证程序安全运行。示例中AppB.exe程序运行情况如图1.25所示。
【考点】CLR的知识,中间语言的知识。
【出现频率】★★★★★
【解答】
.NET的程序可由多种高级语言编写,如 C++、Visual Basic、 C#、 J# 等,但是最后将会被各自的编译器编译为一致的中间语言(IL)。最后由CLR提供运行环境,将中间语言编译为机器码,供CPU执行,其编译过程如图1.26所示。
为了尽量减少中间代码编译为机器代码的性能损失,中间语言采用即时编译,也被称为JIT编译。这种编译方式只编译调用的代码部分,而并非完全编译程序中所有的代码,编译过的部分将存储在内存中,下次执行时不需重复编译。当退出程序时,已编译部分的代码才会被清除。这种策略极大地降低了中间代码的性能损失,是程序灵活性和性能相权衡的较佳方案。
【分析】
在系统中运行.NET 程序必须安装相应版本的.NET Framework,目前最新版本为4.5。.NET程序不是已经编译过了么?为什么还要依赖.NET Framework 呢?这和.NET程序的运行机制有关,传统上程序分为源代码层和编译后的本机代码层(机器码)。而.NET 提供了对多种编程语言以及多重平台的支持,所以在其中添加了中间代码层,中间代码被称为IL或MSIL。由于多了中间语言代码,使.NET程序有了更好的灵活性,有运行于多个平台的可能性(如Linux系统)。
.NET Framework 的核心是CLR,即公共语言运行时,CLR 是.NET 程序的运行库环境。中间语言需要在CLR中运行并转换为机器码,所以.NET 程序必须依赖.NET Framework 才能运行。以C# 语言为例,C# 编译器编译的程序只是由中间语言构成,无法直接运行,必须由CLR执行。
.NET这种编译运行的机制和Java、ActionScript比较相似,Java第一次编译为字节码,而Java良好的移植性得益于此。只要客户机安装了Java虚拟机(JVM),就可以直接运行Java程序(JVM将字节码编译为机器码)。类似地,ActionScript同样被第一次编译为字节码,并存放于swf文件中。只要客户机安装了FlashPlayer,swf即可运行,因为FlashPlayer含有AS虚拟机(AVM)。可见,.NET的CLR和JVM、AVM是殊途同归。
说明:客户机应尽量安装新版本的.NET Framework。
本节问题主要考察面试者对于.NET Framework 编译的认识,特别是对于中间语言的理解。.NET程序的中间语言(IL)也被称为托管代码,其优点总结如下所示:
(1)平台无关性。例如MONO项目,可以使.NET程序运行于Windows以外的平台。
(2)JIT性能优化。及时编译需要调用的代码,尽可能提高程序运行速度。
(3)语言互操作性。支持多种语言编写程序,并编译为中间语言。通过这个特性,可以使多种语言编写的程序交互操作,以提升团队合作的融洽性。
【考点】.NET 的命名空间的基本理解,自定义命名空间的知识,在程序中使用命名空间的各种技巧。
【出现频率】★★★☆☆
【解答】
使用命名空间的方法可以反映程序中的逻辑关系,并且可以有效避免类名冲突。命名空间就是各种类或其他类型名称的逻辑组织方式,而不代表物理组织方式。例如以下代码:
System.Windows.Forms.MessageBox.Show("文本内容");
在执行以上代码时,将跳出一个带有“确定”按钮的对话框,并停止程序的运行。其中,System.Windows.Forms是命名空间,调用了MessageBox类的Show静态方法。这种将命名空间和类名称用点符号相连使用时,类名被称为全饰类名,用同样的方法可以调用.NET基类库的所有类型。不过,这种方法比较麻烦,当程序中多处使用相同命名空间时会明显增加代码量。所以常常在程序代码页顶部使用using关键字引入命名空间,相同的命名空间可被一次性引入,这样将大大减少重复劳动。在 ch01 目录下新建一个文件,名称为Namespace.cs,在其Namespace.cs中编写代码如程序1.18所示。
程序1.18 命名空间示例:Namespace.cs
01 using System;
02 using System.Windows.Forms;
03
04 class Namespace
05 {
06 static void Main()
07 {
08 Console.WriteLine("程序开始运行。");
09 MessageBox.Show("Hello!");
10 MessageBox.Show("你好,欢迎学习.NET!");
11 MessageBox.Show("下次再见!");
12 Console.WriteLine("程序运行结束。");
13 }
14 }
在命令行下编译Namespace.cs后,执行Namespace程序,其效果如图1.27所示。
原来,编译过程中,遇到如MessageBox的类型名称时,C#编译器将在using引入的命名空间中寻找该类型。如果C# 编译器找到了就把MessageBox 类标识为全饰名称,即在前面加上命名空间,并用点符号分隔。反之,C# 编译器将报错。在顶部用 using 引入System.Windows.Forms命名空间后,可以直接使用MessageBox的类名并调用方法,Console类也是同样的原理(对应System命名空间)。
不仅可以通过访问命名空间调用基类库提供的类型,编程者也可以自己组织命名空间,以方便多个类型的访问。用namespace关键字可自定义命名空间,并使用大括号将类型包含, namespace前面不需要加访问修饰符。因为命名空间不是文件物理的组织分类,所以一个程序集可以有多个命名空间甚至多层命名空间,多个程序集也可以使用同一个命名空间。
本例定义两个类,分别存储于两个文件中,但这两个类定义相同的命名空间,其中一个类的字段存储了用户名信息,另一个类创建时将输出信息。然后创建第三个文件作为主程序,用using引入自定义命名空间,即可实现直接访问两个自定义类。在ch01目录下新建3个文件,分别名称为Username.cs、Mymsg.cs和Login.cs,在Username.cs中编写代码如程序1.19所示。
程序1.19 用户名信息类:Username.cs
01 using System;
02
03 //自定义LoginSystem命名空间
04 namespace LoginSystem
05 {
06 public class Username
07 {
08 //创建字符串类型的字段name
09 public string name = "wangxiaoming";
10 }
11 }
在Mymsg.cs中编写代码如程序1.20所示。
程序1.20 信息输出类:Mymsg.cs
01 using System;
02
03 //自定义LoginSystem命名空间
04 namespace LoginSystem
05 {
06 public class Mymsg
07 {
08 public Mymsg()
09 {
10 //构造函数中输出信息
11 Console.WriteLine("我的信息:我身高180公分,喜爱篮球。");
12 }
13 }
14 }
在Login.cs中编写代码如程序1.21所示。
程序1.21 主程序类:Login.cs
01 using System;
02
03 //引入LoginSystem命名空间
04 using LoginSystem;
05
06 class Login
07 {
08 static void Main()
09 {
10 Console.WriteLine("请输入正确的用户名(wangxiaoming):");
11 //接收用户输入,并赋值给input字符串变量
12 string input = Console.ReadLine();
13 //创建Username类型的实例a
14 Username a = new Username();
15 //判断用户输入值和a对象的name字段值是否相等
16 if (input == a.name)
17 {
18 //输出成功信息
19 Console.WriteLine("【{0}】,你已经登录成功。", a.name);
20 //创建Mymsg类的实例b
21 Mymsg b = new Mymsg();
22 }
23 else
24 {
25 //如果用户输入值和用户名不匹配,则输出失败信息
26 Console.WriteLine("登录失败。");
27 }
28 }
29 }
本节举例说明了.NET已有命名空间和自定义命名空间,在运行第二个示例时,须首先将Username.cs和Mymsg.cs编译为dll程序集,然后编译Login.cs时引用这两个dll程序集。用csc编译器完成如图1.28所示。
编译完成后,直接执行Login.exe程序,并输入正确的用户名信息如图1.29所示。
本例的Login.cs代码中,通过引入自定义的LoginSystem命名空间,直接在主程序中访问了Username类和Mymsg类。进一步分析自定义命名空间,可以在同一个程序集中定义多个命名空间,并且其间可以嵌套,如以下代码所示:
namespace A
{
namespace B
{
class ClassNameOne { }
}
namespace C
{
}
class ClassNameTwo { }
}
namespace D.E
{
class ClassNameThree { }
}
以上代码中,命名空间A包含了命名空间B和命名空间C,而命名空间D包含了命名空间E,只是用点符号简写了嵌套的格式。从本质上来看,命名空间只是有组织地编写长类型名的方式,通过点符号表明类型名称的含义及关联性。一般情况下提倡使用using在程序顶部导入将要使用的命名空间,但是当不同命名空间中有相同的类名时,则不能使用这种方法,应该使用全饰类名代替。为了避免全饰类名过长导致程序编写混乱,可以使用using定义命名空间的别名,如以下代码所示:
namespace A
{
ce namespa B
{
namespace C
{
class ClassName { }
}
}
}
//指定别名
using d = A.B.C;
namespace D.E
{
class ClassNameThree
{
//命名空间由A.B.C缩短为d
d.ClassName obj =new ABC.ClassName();
}
}
【分析】
.NET Framework由公共语言运行时(CLR)和基类库(BCL)组成,前者提供运行库环境,而后者则提供丰富的类库,适用于全部.NET编程语言调用。基类库不仅封装了各种类型,而且还支持很多服务。基类库在物理上表现为多个共享程序集,其中最重要的程序集是mscorlib.dll,这些程序集中包含了大量核心数据类型以及常见的编程任务。共享程序集安装于.NET特别指定的目录中,Windows操作系统中一般为%windir%\assembly目录,如图1.30所示。
这个目录也被称为全局程序集缓存(GAC),用户自己创建的共享程序集也必须安装于此目录,才可以在系统级别得到共享。丰富的基类库被存于这些程序集中,.NET编程语言可以通过命名空间使用这些类库,命名空间是程序集内相关类型的分组。如图1.30所示的程序集,一个程序集也可以包含多个命名空间,如mscorlib.dll包含了很多命名空间,并且每个命名空间可以包含多种类型。命名空间实际上是类名的扩展,如常用的命名空间System。在程序需要命令行输出时需使用System命名空间中Console类的WriteLine静态方法,编写代码如下:
System.Console.WriteLine("输出内容");
可见,需要使用Console类必须先引入其所属命名空间,并用点符号分隔。不仅仅调用基类库需要访问命名空间,编程者也可以定义自己的命名空间。
说明:命名空间只是.NET组织各种相关类型的逻辑形式,方便编程者的理解。
图书在版编目(CIP)数据
.NET程序员面试秘笈/张云翯编著.--北京:人民邮电出版社,2014.3
ISBN 978-7-115-34048-1
Ⅰ.①N… Ⅱ.①张… Ⅲ.①计算机网络—程序设计 Ⅳ.①TP393
中国版本图书馆CIP数据核字(2013)第301966号
内容提要
随着微软公司对Visual Studio 系统工具的力推,使用.NET 进行开发的企业越来越多,为了让读者从面试中脱颖而出,笔者特意编写了本书。
本书是一本解析.NET面试题的书,可以帮助求职者更好地准备面试。全书共11章,囊括了目前企业中常见的面试题类型和考点,包括.NET语言基础、基类、接口和泛型、.NET高级特性、Windows窗体编程、ADO.NET编程、SQL查询及LINQ、ASP.NET程序开发和算法趣味题等。本书通过技术点解析、代码辅佐的方式使读者能深刻理解每个考点背后的技术。
本书紧扣面试焦点,对各种技术剖析一针见血,是目前想找工作的.NET 程序员和刚毕业学生的面试宝典。
◆编著 张云翯
责任编辑 陈冀康
责任印制 程彦红 焦志炜
◆人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
北京昌平百善印刷厂印刷
◆开本:800×1000 1/16
印张:34.75
字数:680千字 2014年3月第1版
印数:1-3000册 2014年3月北京第1次印刷
定价:59.00元
读者服务热线:(010)81055410 印装质量热线:(010)81055316
反盗版热线:(010)81055315