名企面试官精讲典型编程题之C#篇
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");
Aa2 = 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#。
《ProfessionalC#》。这本书最大的特点是在附录中有几章专门写给已经有其他语言(如VB、C++和Java)经验的程序员,它详细讲述了C#和其他语言的区别,看了这几章之后就不会把C#和之前掌握的语言相混淆。
JeffreyRichter的《CLR Via C#》。该书不仅深入地介绍了C#语言,同时对CLR及.NET做了全面的剖析。如果能够读懂这本书,那么我们就能深入理解装箱卸箱、垃圾回收、反射等概念,知其然的同时也能知其所以然,通过C#相关的面试自然也就不难了。
面试题2:实现Singleton模式
题目:设计一个类,我们只能生成该类的一个实例。
只能生成一个实例的类是实现了Singleton(单例)模式的类型。由于设计模式在面向对象程序设计中起着举足轻重的作用,在面试过程中很多公司都喜欢问一些与设计模式相关的问题。在常用的模式中,Singleton是唯一一个能够用短短几十行代码完整实现的模式。因此,写一个Singleton的类型是一个很常见的面试题。
不好的解法一:只适用于单线程环境
由于要求只能生成一个实例,因此我们必须把构造函数设为私有函数以禁止他人创建实例。我们可以定义一个静态的实例,在需要的时候创建该实例。下面定义类型Singleton1就是基于这个思路的实现:
public sealed class Singleton1
{
private Singleton1()
{
}
private static Singleton1 instance = null;
public static Singleton1 Instance
{
get
{
if (instance == null)
instance = new Singleton1();
return instance;
}
}
}
上述代码在Singleton的静态属性Instance中,只有在instance为null的时候才创建一个实例以避免重复创建。同时我们把构造函数定义为私有函数,这样就能确保只创建一个实例。
不好的解法二:虽然在多线程环境中能工作但效率不高
解法一中的代码在单线程的时候工作正常,但在多线程的情况下就有问题了。设想如果两个线程同时运行到判断instance是否为null的if语句,并且instance的确没有创建时,那么两个线程都会创建一个实例,此时类型Singleton1就不再满足单例模式的要求了。为了保证在多线程环境下我们还是只能得到类型的一个实例,需要加上一个同步锁。把Singleton1稍做修改得到了如下代码:
public sealed class Singleton2
{
private Singleton2()
{
}
private static readonly object syncObj = new object();
private static Singleton2 instance = null;
public static Singleton2 Instance
{
get
{
lock (syncObj)
{
if (instance == null)
instance = newSingleton2();
}
return instance;
}
}
}
我们还是假设有两个线程同时想创建一个实例。由于在一个时刻只有一个线程能得到同步锁,当第一个线程加上锁时,第二个线程只能等待。当第一个线程发现实例还没有创建时,它创建出一个实例。接着第一个线程释放同步锁,此时第二个线程可以加上同步锁,并运行接下来的代码。这个时候由于实例已经被第一个线程创建出来了,第二个线程就不会重复创建实例了,这样就保证了我们在多线程环境中也只能得到一个实例。
但是类型Singleton2还不是很完美。我们每次通过属性Instance得到Singleton2的实例,都会试图加上一个同步锁,而加锁是一个非常耗时的操作,在没有必要的时候我们应该尽量避免。
可行的解法:加同步锁前后两次判断实例是否已存在
我们只是在实例还没有创建之前需要加锁操作,以保证只有一个线程创建出实例。而当实例已经创建之后,我们已经不需要再做加锁操作了。于是我们可以把解法二中的代码再做进一步的改进:
public sealed class Singleton3
{
private Singleton3()
{
}
private static object syncObj = new object();
private static Singleton3 instance = null;
public static Singleton3 Instance
{
get
{
if (instance == null)
{
lock (syncObj)
{
if (instance == null)
instance = newSingleton3();
}
}
return instance;
}
}
}
Singleton3中只有当instance为null即没有创建时,需要加锁操作。当instance已经创建出来之后,则无须加锁。因为只在第一次的时候instance为null,因此只在第一次试图创建实例的时候需要加锁。这样Singleton3的时间效率比Singleton2要好很多。
Singleton3用加锁机制来确保在多线程环境下只创建一个实例,并且用两个if判断来提高效率。这样的代码实现起来比较复杂,容易出错,我们还有更加优秀的解法。
强烈推荐的解法一:利用静态构造函数
C#的语法中有一个函数能够确保只调用一次,那就是静态构造函数,我们可以利用C#这个特性实现单例模式如下:
public sealed class Singleton4
{
private Singleton4()
{
}
private static Singleton4 instance = new Singleton4();
public static Singleton4 Instance
{
get
{
return instance;
}
}
}
Singleton4的实现代码非常简洁。我们在初始化静态变量instance的时候创建一个实例。由于C#是在调用静态构造函数时初始化静态变量,.NET运行时能够确保只调用一次静态构造函数,这样我们就能够保证只初始化一次instance。
C#中调用静态构造函数的时机不是由程序员掌控的,而是当.NET运行时发现第一次使用一个类型的时候自动调用该类型的静态构造函数。因此在Singleton4中,实例instance并不是第一次调用属性Singleton4.Instance的时候创建,而是在第一次用到Singleton4的时候就会被创建。假设我们在Singleton4中添加一个静态方法,调用该静态函数是不需要创建一个实例的,但如果按照Singleton4的方式实现单例模式,则仍然会过早地创建实例,从而降低内存的使用效率。
强烈推荐的解法二:实现按需创建实例
最后的一个实现Singleton5则很好地解决了Singleton4中的实例创建时机过早的问题:
public sealed class Singleton5
{
Singleton5()
{
}
public static Singleton5 Instance
{
get
{
return Nested.instance;
}
}
class Nested
{
static Nested()
{
}
internal static readonly Singleton5 instance = new Singleton5();
}
}
在上述Singleton5的代码中,我们在内部定义了一个私有类型Nested。当第一次用到这个嵌套类型的时候,会调用静态构造函数创建Singleton5的实例instance。类型Nested只在属性Singleton5.Instance中被用到,由于其私有属性他人无法使用Nested类型。因此当我们第一次试图通过属性Singleton5.Instance得到Singleton5的实例时,会自动调用Nested的静态构造函数创建实例instance。如果我们不调用属性Singleton5.Instance,那么就不会触发.NET运行时调用Nested,也不会创建实例,这样就真正做到了按需创建。
解法比较
在前面的5种实现单例模式的方法中,第一种方法在多线程环境中不能正常工作,第二种模式虽然能在多线程环境中正常工作但时间效率很低,都不是面试官期待的解法。在第三种方法中我们通过两次判断一次加锁确保在多线程环境能高效率地工作。第四种方法利用C#的静态构造函数的特性,确保只创建一个实例。第五种方法利用私有嵌套类型的特性,做到只在真正需要的时候才会创建实例,提高空间使用效率。如果在面试中给出第四种或者第五种解法,毫无疑问会得到面试官的青睐。
源代码:
本题完整的源代码详见02_Singleton项目。
本题考点:
考查对单例(Singleton)模式的理解。
考查对C#的基础语法的理解,如静态构造函数等。
考查对多线程编程的理解。
本题扩展:
在前面的代码中,5种单例模式的实现把类型标记为sealed,表示它们不能作为其他类型的基类。现在我们要求定义一个表示总统的类型President,可以从该类型继承出FrenchPresident和AmericanPresident等类型。这些派生类型都只能产生一个实例。请问该如何设计实现这些类型?
本文选自《剑指Offer——名企面试官精讲典型编程题》一书
图书详细信息:http://blog.csdn.net/broadview2006/article/details/7043805