.NET程序员面试秘笈

978-7-115-34048-1
作者: 张云翯
译者:
编辑: 陈冀康
分类: .NET

图书目录:

详情

本书涉及了.NET平台中最常用的知识点,从面向对象思想到.NET平台的各种特性应用、从Windows窗体程序到ASP.NET,再到常用算法分析以及设计模式,并且包括了大量.NET典型特色。本书不仅可以作为.NET初学者的学习书籍和.NET程序员面试者的参考书籍,也可以作为.NET开发者的程序参考手册。

图书摘要

.NET程序员面试秘笈
张云翯 编著
人民邮电出版社

北京

前言

本书的思路

从目前软件开发的就业形势来看,.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#入门人员。

编者

第1章 .NET概念题

本章内容包含面向对象程序设计和.NET的基础概念题,由于这部分题目涉及的范围比较广,因此在很多公司的面试题中占了比较大的比重。很多.NET程序员在编写代码时非常熟练,但往往缺乏对基础知识的深刻理解,从而导致面试失败。这类知识是程序设计的基础,如果不加以重视,程序编写就没有创造性,只能学一步,做一步。

面向对象编程是当前流行的编程方式,被大多数高级语言支持。.NET程序同样是基于面向对象的设计,只有深刻理解面向对象的编程理念,才可以开发出结构良好的、更易维护的.NET程序。

说明:本书采用C#语言编写.NET程序示例。

面试题1 简述面向对象的程序设计思想

【考点】面向对象程序设计思想概念的理解,面向对象设计的应用范围,用C#实现面向对象设计方法。

【出现频率】★★★★☆

【解答】

面向对象是程序开发的一种机制,其特征为封装、继承、多态及抽象。以面向对象的方式编写程序时,需要将复杂的项目抽象为多个对象互相协作的模型;然后编写类型的结构,声明或实现类型的成员,即描述对象的特征和行为。在项目中编写不同的类型完成不同的功能,最后在程序中通过类的实例处理实际的问题。

说明:此处类型不仅仅指类,还可能包括接口、结构等。

【分析】

面向对象编程简称为OOP,其“对象”的含义类似于生活中的“物体”。相对于以前的编程方式,面向对象编程更注重于人的思维方式,可极大地提高程序的开发效率。面向对象编程将程序看作多个互相分离的功能模块,各个模块之间分工合作,并且有着极低的耦合度。面向对象编程中最重要的概念是类(即class),类是描述各个功能模块的模板,而程序中工作的模块实体被称为对象(即object)。

类和对象的概念好比动物学的分类,猫科动物就是一个类,老虎也是一个类,并且属于猫科动物类,动物园中某只老虎的实体则是一个对象。老虎类拥有猫科动物类的所有特征和行为,但有自己独特的特征和行为。而某只老虎符合老虎类特征描述和行为定义,可能还有部分独特的特征。所以类可以继承另一个类,如老虎类继承了猫科动物类。类是产生对象的一个模板,对象拥有类的一切特征和行为。关于类和对象的基本关系如图1.1所示。

图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所示。

图1.2 OopTest的执行结果

当用户首先输入了“50,100”,程序创建了一只老虎的对象,并访问了部分数据字段和方法。

说明:下文所有当前章的程序示例将在当前章编号的目录下创建并运行,最后进行归档,例如第1章每个示例文件归档到ch01目录下,而第2章每个示例文件归档到ch02目录下,依此类推。

面试题2 用代码描述类和对象的区别

【考点】理解类和对象的关系,在实际应用中类和对象的作用。

【出现频率】★★★★★

【解答】

类(即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 类和对象程序的执行结果

图1.3为3个类的定义,在主程序中用户输入动物的类别、品种和体重数据后,即根据以上类为模板创建了一只小动物对象,并输出其数据。在命令行下编译ClassObj.cs后,执行ClassObj程序,其效果如图1.4所示。

【分析】

简单来说,类是用于描述对象的特征、状态和行为的模板,是抽象的概念。例如虎类可以用于描述所有老虎个体的共同特征及行为,但是虎类不能指定某一只老虎的个体。而对象指拥有具体特征、状态和行为的实体,例如某只老虎就是虎类的一个对象,它遵从类所描述的一切特征和行为。

图1.4 类和对象程序的执行结果

面试题3 描述private、protected、internal和public修饰符的作用

【考点】理解访问权限存在的意义,各种访问权限的作用,在代码中灵活应用访问权限。

【出现频率】★★★☆☆

【解答】

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访问器中有两步操作,而外界无法获知。

图1.5 private修饰符示例

(2)在命令行下编译 ModProtected.cs 后,执行 ModProtected 程序,其效果如图1.6所示。

图1.6 protected修饰符示例

本例接收用户的输入,当输入值为“华南虎”时,创建 ChinaTiger 类的对象,并通过构造函数赋值“华南虎”字符串给_type字段。_type字段声明中使用了protected修饰符,所以在Tiger类的子类(ChinaTiger类)中可被直接访问。

注意:即使派生类和基类在不同程序集中,派生类仍可访问基类的 protected修饰符成员。读者必须清楚的一点是,派生类继承了所有基类的成员,只是无法直接访问基类的private修饰符成员,但可访问protected修饰符成员。

【分析】

面向对象编程的特征之一就是封装性,而类就是封装性的典型体现。在生活中,人们不需要知道电器的内部构造,但是能很容易地使用电器,这就是封装性。在项目中需要编写很多功能类,在程序运行时只需要使用类所提供的功能,大部分类内部的代码实现需要封装,拒绝外部访问。这样大大增加了类的内部代码安全性和稳定性,同时多个功能类之间也减少了互相干扰的可能。访问权限修饰符即可将类及类的成员划分多种安全级别,根据不同需要设置访问权限。

internal和public访问修饰符是需要谨慎选择的,多数情况下应该尽量使用internal访问修饰符。

还有一种访问修饰符,即protected internal 修饰符,可在子类中或同一程序集内被访问。如果要声明成员只能被同一程序集内的派生类(子类)访问,则应首先设置所属类为internal,成员设置为protected。

面试题4 举例说明属性、get和set访问器的使用

【考点】对属性(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所示。

图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 没有显式访问修饰符时,其默认访问限制和属性一致。

面试题5 描述sealed修饰符的使用

【考点】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所示。

图1.8 sealed修饰符示例

从本例代码中可知,密封类一般情况下的使用方法和其他类一样,只是无法被继承。代码中Product类的虚方法display方法被Phone类重写,而被重写的display方法前面也加了sealed修饰符。在这种情况下,密封的display方法无法被所属类的子类继续重写,如本例代码中的Phone3G类,无法重写display方法。

说明:密封类可用于单一功能的实现,并且防止被意外地继承,产生非预期的效果,这也是封装性的体现。

【分析】

sealed修饰符用于修饰类、实例方法和属性。sealed用于修饰类时,该类无法被继承,所以该类也被称为密封类。而abstract类(抽象类)必须被继承才有意义,所以,sealed修饰符和abstract修饰符是互相排斥的,无法共存。密封方法会重写基类的方法,sealed用于修饰实例被重写的虚方法或虚属性时,表示该方法或属性无法再被重写。

注意:sealed修饰实例方法或属性时,必须和override一起使用。

面试题6 请简述静态类和静态类成员

【考点】静态类和静态类成员的理解,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所示。

图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,即无法被所属类的对象访问。

注意:声明静态类时,必须保证其内含成员全部为静态成员。

面试题7 构造函数有什么作用

【考点】各种形态构造函数的理解,派生类的构造函数,构造函数的重载。

【出现频率】★★★★☆

【解答】

构造函数用于创建类的实例,并对实例进行初始化操作,通过不同的参数传递,可进行不同的实例初始化操作。本例通过多种不同形式的构造函数创建实例,并输出初始化的结果。在 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所示。

图1.10 构造函数示例

【分析】

前面的所有示例中都使用了构造函数,因为构造函数用于创建类的实例(对象)。在类中声明构造函数可对新实例(对象)进行初始化的操作,其编写方法如以下代码所示:

class 类名称

{

访问修饰符 类名称()

{

初始化操作;

}

}

可见,构造函数和类中的方法类似,也是一种函数,不过构造函数的名称必须和类名称相同。并且构造函数没有返回值,所以其函数签名和一般的函数有区别。没有参数的构造函数被称为默认构造函数,如果非静态类的类体中没有声明构造函数,类将自动提供一个默认构造函数,并将类成员初始化为默认值。

说明:结构类型(Struct)是值类型,不需要显式声明默认构造函数,编译器将自动生成默认构造函数。当用new运算符实例化时默认构造函数才被调用,将成员初始化为默认值。

通过不同的参数传递,在类体中可声明多个构造函数,即实现构造函数的重载。其编写方法如以下代码所示:

class 类名称

{

访问修饰符 类名称()

{

初始化操作1;

}

访问修饰符 类名称( 参数类型1 参数1......)

{

初始化操作2;

}

访问修饰符 类名称( 参数类型2 参数1......)

{

初始化操作3;

}

}

在程序中创建该类的实例(对象)时,通过传递参数的不同,调用不同的构造函数进行不同的初始化操作。程序中创建实例(对象)的方法如以下代码所示:

类型名称对象名称 = new构造函数();  //默认构造函数

类型名称对象名称 = new构造函数(参数列表);

一般情况下,构造函数是实例构造函数,即可通过该构造函数在类外部创建类的实例。反之,如果需要阻止创建类的实例,可在声明私有的默认构造函数,这种情况一般用于无实例成员的类中。如果需要完成只执行 1 次的操作,可以声明静态构造函数。这种构造函数在创建实例前或引用静态成员前自动调用,一般用于对静态成员的操作。

说明:无实例成员的类可声明为静态类,即无须声明私有的默认构造函数。

面试题8 方法的重载和override有什么区别

【考点】对类体内函数的深刻理解,对重载机制的应用,对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.11 方法的重载和重写示例

【分析】

方法的重载一般指通过对类中同名函数使用不同的签名,以声明多个函数体。简单地说,给函数定义不同的参数个数或不同的参数类型,可以声明不同的同名函数(返回值也可以不同)。简单的函数重载(在类中即为方法重载)如以下代码所示:

访问修饰符 返回类型1 函数名称(参数列表1)

{

函数体代码1;

}

访问修饰符 返回类型2 函数名称(参数列表2)

{

函数体代码1;

}

以上代码声明了 2 个函数,虽然函数名相同,但函数的签名不同,所以可以视做 2 个不同的函数。程序中调用时,通过不同参数传递执行不同的函数。

而override方法被称为重写方法,即在派生类中将所继承的方法进行扩展或改写,要求重写后的方法签名与被重写的方法签名一致。派生类中只有继承的虚方法或抽象方法可以被重写,并且静态方法不能被重写。其使用方法如以下代码所示:

访问修饰符返回类型 override 函数名称(参数列表)

{

函数体代码;

}

注意:派生类所继承的非密封override方法也可重写,因为该方法是被重写过的。

面试题9 举例描述接口的作用

【考点】接口类型的理解,接口在程序中的意义。

【出现频率】★★★☆☆

【解答】

接口在程序设计中的作用为充当类或结构的功能界面,接口的属性、方法等属于抽象描述必须通过类或结构的实现才能使用。接口是使用者只知道接口有些什么功能,却不知道功能如何实现、由谁实现,这给程序的设计留下了很大的灵活性。例如某个项目由多个功能模块组成,每个模块由一个程序员完成,程序员只需编写完模块功能的实现后,留下该模块的接口供其他人使用。其他人在程序中只需直接使用接口的功能,而不必了解接口的功能如何实现等问题,其关系模型如图1.12所示。

图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所示。

图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,并且实现方法的签名必须和接口方法签名一致。

面试题10 接口和抽象类该如何取舍

【考点】抽象类的理解,抽象类和接口的区别。

【出现频率】★★★★★

【解答】

接口和抽象类非常相似,两者都无法实例化,并且未实现部分都由派生类实现,其应用模型如图1.14所示。

结合图1.14可知,接口与抽象类的主要区别有以下几点:

(1)抽象类只能派生类,而接口可以派生类和结构。

(2)抽象类的派生类也可以是抽象类,即抽象成员在派生类中不一定被完全实现。而接口要求其派生类或结构必须完全实现其成员。

(3)抽象类可以包含已经实现的成员,可以包含字段,而接口只包含未实现的成员,不能包含字段。并且接口及所含成员必须为public访问级别。

(4)类只能继承一个抽象类,但可以继承(实现)多个接口。

图1.14 接口和抽象类示例

在具体的程序设计中,抽象类和接口的取舍应视程序的需要而定。抽象类可以用于归纳一组相似的、有共同特性的类,然后将这些类共同的成员提取到抽象类中,使抽象类作为这组类的基类。这样做到了代码的复用,不但节约了代码量,也减轻了维护的复杂度。然后将这组类中相似的方法或属性提取到抽象类中,成为抽象类的抽象成员,不提供具体实现,由这组类自己完成不同的实现。

说明:抽象类的应用非常类似于网页制作中的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所示。

图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修饰。

面试题11 举例说明简单工厂模式的作用

【考点】工厂模式的理解,工厂模式在实际应用中的编写。

【出现频率】★★★☆☆

【解答】

在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时由于需求的变化,往往存在着更多系列对象的创建工作。为了绕过常规对象的创建方法(new运算符创建实例),工厂模式提供一种“封装机制”来减少使用程序和这种“多系列具体对象创建工作”的耦合性。

说明:这里的程序指客户程序之类的使用者。

简单工厂模式可以用于封装创建实例的实现部分,在应用接口的程序中被广泛使用,其应用模型如图1.16所示。

图1.16 简单工厂模式

为了处理更加复杂的情况,可以将图中的产品进行再次细分为多个大类,用抽象类进行归纳,完成同大类产品共用代码的复用。然后将工厂类也相应地分为多个大类,用抽象类进行归纳。将图1.16改良后如图1.17所示。

图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.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条件分支判断,然后返回相应的实例引用,在实例种类很多的情况下不大适用。根据具体情况不同,可以考虑利用反射、泛型等方法进行改进。

面试题12 访问关键字this和base有什么作用

【考点】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所示。

图1.19 this和base示例

本例展示了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关键字访问基类的成员时,必须保证基类成员有相应的访问权限。

面试题13 举例说明索引器的作用

【考点】索引器的理解,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所示。

图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所示。

图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访问器的选用,可确定索引器可读写、只读和只写的功能。索引器的参数类型和参数个数是索引器的签名,在同一个类中,多个索引器必须有不同的签名。

面试题14 简述程序集和应用程序域

【考点】.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所示。

图1.22 AppA项目运行示例

可见,当应用程序运行时,默认所在的应用程序域的名称为程序名称。现在 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所示。

图1.23 AppB项目添加引用
图1.24 AppB项目运行示例

如图所示, AppA 程序集首先被加载到当前应用程序域,然后被加载到名称为“ROOM-A”的应用程序域中。从任务管理器中观察,AppA 程序集并没有创建新的进程。AppB程序集所在的应用程序域被称为进程中的主应用程序域,这是运行时自动创建的。

说明:实际上AppA程序集已加载到AppB.exe进程中运行,这就达到了多个应用程序在同一个进程中运行的目的。

本节问题相对比较靠近.NET的底层,编程者必须理解程序基本的运行过程,才能写出更高效的程序。从本节的代码中可知,AppDomain 类用于创建和中断应用程序域,加载和卸载程序集合类等功能,另外AppDomain类还可以枚举应用程序域中的程序集和线程。由于程序集包含了元数据,其中含有所有定义的类型以及这些类型成员的细节,所以可通过反射技术来获取这些数据。

本节示例主要展示了应用程序域的简单使用,其“边界”作用不同于进程,不同应用程序域中的应用程序有自己独立的内存空间。在默认情况下,这些程序互相隔离,保证程序安全运行。示例中AppB.exe程序运行情况如图1.25所示。

图1.25 AppB.exe程序运行示意图

面试题15 .NET程序是如何编译的

【考点】CLR的知识,中间语言的知识。

【出现频率】★★★★★

【解答】

.NET的程序可由多种高级语言编写,如 C++、Visual Basic、 C#、 J# 等,但是最后将会被各自的编译器编译为一致的中间语言(IL)。最后由CLR提供运行环境,将中间语言编译为机器码,供CPU执行,其编译过程如图1.26所示。

为了尽量减少中间代码编译为机器代码的性能损失,中间语言采用即时编译,也被称为JIT编译。这种编译方式只编译调用的代码部分,而并非完全编译程序中所有的代码,编译过的部分将存储在内存中,下次执行时不需重复编译。当退出程序时,已编译部分的代码才会被清除。这种策略极大地降低了中间代码的性能损失,是程序灵活性和性能相权衡的较佳方案。

图1.26 .NET程序编译示意图

【分析】

在系统中运行.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)语言互操作性。支持多种语言编写程序,并编译为中间语言。通过这个特性,可以使多种语言编写的程序交互操作,以提升团队合作的融洽性。

面试题16 请简述.NET的命名空间

【考点】.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所示。

图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所示。

图1.28 csc编译命令

编译完成后,直接执行Login.exe程序,并输入正确的用户名信息如图1.29所示。

图1.29 Login程序执行过程

本例的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所示。

图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

相关图书

程序员的制胜技
程序员的制胜技
Dapr与.NET微服务实战
Dapr与.NET微服务实战
精通ASP.NET Core MVC 第7版
精通ASP.NET Core MVC 第7版
深入浅出 ASP.NET Core
深入浅出 ASP.NET Core
ASP.NET Core真机拆解
ASP.NET Core真机拆解
ASP.NET Core与RESTful API 开发实战
ASP.NET Core与RESTful API 开发实战

相关文章

相关课程