名企面試官精講典型程式設計題之C#篇

broadviewbj發表於2011-12-12

名企面試官精講典型程式設計題之C#

C#

C#是微軟在推出新的開發平臺.NET時同步推出的程式語言。由於Windows至今仍然是使用者最多的作業系統,而.NET又是微軟近年來力推的開發平臺,因此C#無論在桌面軟體還是網路應用的開發上都有著廣泛的應用,所以我們也不難理解為什麼現在很多基於Windows系統開發的公司都會要求應聘者掌握C#

C#可以看成是一門以C++為基礎發展起來的一種託管語言,因此它的很多關鍵字甚至語法都和C++很類似。對一個學習過C++程式設計的程式設計師而言,他用不了多長時間學習就能用C#來開發軟體。然而我們也要清醒地認識到,雖然學習C#C++相同或者類似的部分很容易,但要掌握並區分兩者不同的地方卻不是一件很容易的事情。面試官總是喜歡深究我們模稜兩可的地方以考查我們是不是真的理解了,因此我們要著重注意C#C++不同的語法特點。下面的面試片段就是一個例子:

面試官:C++中可以用structclass來定義型別。這兩種型別有什麼區別?

應聘者:如果沒有標明成員函式或者成員變數的訪問許可權級別,在struct中預設的是public,而在class中預設的是private

面試官:那在C#中呢?

應聘者:C#C++不一樣。在C#中如果沒有標明成員函式或者成員變數的訪問許可權級別,structclass中都是private的。structclass的區別是struct定義的是值型別,值型別的例項在棧上分配記憶體;而class定義的是引用型別,引用型別的例項在堆上分配記憶體。

C#中,每個型別中和C++一樣,都有建構函式。但和C++不同的是,我們在C#中可以為型別定義一個FinalizerDispose方法以釋放資源。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的普通建構函式。建構函式先初始化成員變數,再執行函式體內的語句,因此先後列印出a2a4。因此執行上面的程式碼,得到的結果將是列印出4行,分別是a1a3a2a4

我們除了要關注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#》。這本書最大的特點是在附錄中有幾章專門寫給已經有其他語言(如VBC++Java)經驗的程式設計師,它詳細講述了C#和其他語言的區別,看了這幾章之後就不會把C#和之前掌握的語言相混淆。

     Jeffrey Richter的《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中,只有在instancenull的時候才建立一個例項以避免重複建立。同時我們把建構函式定義為私有函式,這樣就能確保只建立一個例項。

不好的解法二:雖然在多執行緒環境中能工作但效率不高

解法一中的程式碼在單執行緒的時候工作正常,但在多執行緒的情況下就有問題了。設想如果兩個執行緒同時執行到判斷instance是否為nullif語句,並且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 = new Singleton2();

            }

 

            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 = new Singleton3();

                }

            }

 

            return instance;

        }

    }

}

Singleton3中只有當instancenull即沒有建立時,需要加鎖操作。當instance已經建立出來之後,則無須加鎖。因為只在第一次的時候instancenull,因此只在第一次試圖建立例項的時候需要加鎖。這樣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,可以從該型別繼承出FrenchPresidentAmericanPresident等型別。這些派生型別都只能產生一個例項。請問該如何設計實現這些型別?

 名企面試官精講典型程式設計題之C#篇

本文選自《劍指Offer——名企面試官精講典型程式設計題》一書

圖書詳細資訊:http://space.itpub.net/?uid-13164110-action-viewspace-itemid-712760

 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/13164110/viewspace-713139/,如需轉載,請註明出處,否則將追究法律責任。

相關文章