2.2.2 C#
C#是微软在推出新的开发平台.NET 时同步推出的编程语言。由于Windows至今仍然是用户最多的操作系统,而.NET又是微软近年来力推的开发平台,因此C#无论在桌面软件还是网络应用的开发上都有着广泛的应用,所以我们也不难理解为什么现在很多基于Windows系统开发的公司都会要求应聘者掌握C#。
C#可以看成是一门以 C++为基础发展起来的一种托管语言,因此它的很多关键字甚至语法都和 C++很类似。对一个学习过 C++编程的程序员而言,他用不了多长时间学习就能用C#来开发软件。然而我们也要清醒地认识到,虽然学习C#与C++相同或者类似的部分很容易,但要掌握并区分两者不同的地方却不是一件很容易的事情。面试官总是喜欢深究我们模棱两可的地方以考查我们是不是真的理解了,因此我们要着重注意C#与C++不同的语法特点。下面的面试片段就是一个例子:
面试官:C++中可以用 struct 和 class 来定义类型。这两种类型有什么区别?
应聘者:如果没有标明成员函数或者成员变量的访问权限级别,在struct中默认的是public,而在class中默认的是private。
面试官:那在C#中呢?
应聘者:C#和C++不一样。在C#中如果没有标明成员函数或者成员变量的访问权限级别,struct和class中都是private的。struct和class的区别是struct定义的是值类型,值类型的实例在栈上分配内存;而class定义的是引用类型,引用类型的实例在堆上分配内存。
在C#中,每个类型中和C++一样,都有构造函数。但和C++不同的是,我们在 C#中可以为类型定义一个 Finalizer 和 Dispose 方法以释放资源。Finalizer 方法虽然写法与 C++的析构函数看起来一样,都是后面跟类型名字,但与C++析构函数的调用时机是确定的不同,C#的Finalizer是在运行时(CLR)做垃圾回收时才会被调用,它的调用时机是由运行时决定的,因此对程序员来说是不确定的。另外,在C#中可以为类型定义一个特殊的构造函数:静态构造函数。这个函数的特点是在类型第一次被使用之前由运行时自动调用,而且保证只调用一次。关于静态构造函数,我们有很多有意思的面试题,比如运行下面的C#代码,输出的结果是什么?
class A { public A(string text) { Console.WriteLine(text); } } class B { static A a1 = new A("a1"); A a2 = new A("a2"); static B() { a1 = new A("a3"); } public B() { a2 = new A("a4"); } } class Program { static void Main(string[] args) { B b = new B(); } }
在调用类型B的代码之前先执行B的静态构造函数。静态构造函数先初始化类型的静态变量,再执行函数体内的语句。因此先打印a1再打印a3。接下来执行B b=new B(),即调用B的普通构造函数。构造函数先初始化成员变量,再执行函数体内的语句,因此先后打印出 a2、a4。因此运行上面的代码,得到的结果将是打印出4行,分别是a1、a3、a2、a4。
我们除了要关注C#和C++不同的知识点之外,还要格外关注C#一些特有的功能,比如反射、应用程序域(AppDomain)等。这些概念还相互关联,要花很多时间学习研究才能透彻地理解它们。下面的代码就是一段关于反射和应用程序域的代码,运行它得到的结果是什么?
[Serializable] internal class A : MarshalByRefObject { public static int Number; public void SetNumber(int value) { Number = value; } } [Serializable] internal class B { public static int Number; public void SetNumber(int value) { Number = value; } } class Program { static void Main(string[] args) { String assambly = Assembly.GetEntryAssembly().FullName; AppDomain domain = AppDomain.CreateDomain("NewDomain"); A.Number = 10; String nameOfA = typeof(A).FullName; A a = domain.CreateInstanceAndUnwrap(assambly, nameOfA) as A; a.SetNumber(20); Console.WriteLine("Number in class A is {0}", A.Number); B.Number = 10; String nameOfB = typeof(B).FullName; B b = domain.CreateInstanceAndUnwrap(assambly, nameOfB) as B; b.SetNumber(20); Console.WriteLine("Number in class B is {0}", B.Number); } }
上述 C#代码先创建一个名为 NewDomain 的应用程序域,并在该域中利用反射机制创建类型A的一个实例和类型B的一个实例。我们注意到类型A是继承自MarshalByRefObject,而B不是。虽然这两个类型的结构一样,但由于基类不同而导致在跨越应用程序域的边界时表现出的行为将大不相同。
先考虑A的情况。由于A继承自MarshalByRefObject,那么a实际上只是在默认的域中的一个代理实例(Proxy),它指向位于NewDomain域中的A的一个实例。当调用a的方法SetNumber时,是在NewDomain域中调用该方法,它将修改NewDomain域中静态变量A.Number的值并设为20。由于静态变量在每个应用程序域中都有一份独立的拷贝,修改NewDomain域中的静态变量 A.Number 对默认域中的静态变量 A.Number 没有任何影响。由于Console.WriteLine是在默认的应用程序域中输出A.Number,因此输出仍然是10。
接着讨论B。由于B只是从Object继承而来的类型,它的实例穿越应用程序域的边界时,将会完整地复制实例。因此在上述代码中,我们尽管试图在NewDomain域中生成B的实例,但会把实例b复制到默认的应用程序域。此时调用方法b.SetNumber也是在缺省的应用程序域上进行,它将修改默认的域上的 A.Number 并设为 20。再在默认的域上调用Console.WriteLine时,它将输出20。
下面推荐两本C#相关的书籍,以方便大家应对C#面试并学习好C#。
● 《Professional C#》。这本书最大的特点是在附录中有几章专门写给已经有其他语言(如VB、C++和Java)经验的程序员,它详细讲述了C#和其他语言的区别,看了这几章之后就不会把C#和之前掌握的语言相混淆。
● Jeffrey Richter的《CLR Via C#》。该书不仅深入地介绍了C#语言,同时对CLR及.NET做了全面的剖析。如果能够读懂这本书,那么我们就能深入理解装箱卸箱、垃圾回收、反射等概念,知其然的同时也能知其所以然,通过C#相关的面试自然也就不难了。