1) 值類型與引用類型
下面類型哪些是值類型?哪些是引用類型?
i. class enum struct delegate interface
ii. object int string float DateTime
【答案】
值類型有:enum struct int float DateTime
引用類型有:class delegate interface object string
特别要注意的是:string是引用類型
值類型與引用類型的差別:
1、記憶體配置設定方式不同:
值類型所占的記憶體區域是連續的,至于是配置設定在堆上還是棧上,有以下幾種情況:
1) 如果值類型Struct1用來定義局部變量,那麼它總是配置設定在棧上;
2) 如果值類型Struct1用來定義引用類型Class1的字段,那麼Struct1總是配置設定在堆上的;
3) 如果值類型Struct1用來定義值類型Struct2的字段,那麼Struct1的配置設定方式取決于Struct2是配置設定在堆上還是棧上;
4) 如果把值類型Struct1賦予一個引用類型Class1(這種情況通常見于把一個值類型賦予object變量,或賦予一個接口),這時會在堆上建立一個Struct1的拷貝,此過程稱之為“裝箱”;
引用類型的記憶體區域由兩部分組成:一部分為引用(類似于C++中的指針),另一部分為實際的對象,對象本身總是配置設定在堆上的,而引用配置設定在堆上還是棧上取決于具體的情況:
1) 如果定義局部變量,那麼引用是配置設定在棧上的;
2) 如果用于定義引用類型的字段,那麼引用是配置設定在堆上的;
3) 如果用于定義值類型的字段,那麼引用的配置設定方式取決于該值類型配置設定在堆上還是棧上;
在堆上和棧上的記憶體配置設定有如下的差別:
1) 在棧上定義的對象(或引用),可以在出棧的時候迅速釋放,而不必等待無用記憶體回收器來回收;棧上定義的對象無法持久存儲;
2) 棧的空間有限,而堆的空間比較大;
2、變量傳遞方式不同
引用類型在傳遞的時候,隻傳遞引用本身,而對象隻有一個;
值類型在傳遞的時候會進行對象的拷貝;
3、對象釋放的方式不同
在對象是否有效的判斷上,引用類型是進行引用計數,當引用計數為0時,那麼它就會被标為無效,而等待無用記憶體回收器來收集;
值類型如果配置設定在堆上,則會分情況而定,如果是作為引用類型的字段而被配置設定在堆上,則與該對象同存共亡,如果以裝箱的方式而配置設定在堆上,則也與引用類型一樣進行引用計數;
如果值類型是配置設定在棧上,則它的生存期與進棧出棧有關,一對花括号表示一個域,花括号中嵌套的花括号是它的子域,當值類型在進入
直接包含它的域的時候被配置設定,而出域時被釋放。
值類型和引用類型都有釋構函數,在對象的生存期終止時,值類型會立即執行析構函數,而引用類型是等無用記憶體收集器運作時執行;無用記憶體回收器的運作會引起應用程式的暫停,如果析構函數比較耗時,那麼将會引起應用程式明顯的停頓,這是我們不推薦寫析構函數的原因。(但有的情況下析構函數還是比較有價值的)
4、對象是否相等的比較方式不同
如果沒有重載==運作符,那麼引用類型的相等的判斷,是兩個引用是否引用了同一個對象;值類型如果要進行是否相等的判斷,必須重載==運算符;
5、預設值及構造方式不同
引用類型可以不引用任何對象,此時它的值為null,null是引用類型的預設值;值類型的預設值比較複雜,對于基元類型(int、long、float、double等),預設值為0,bool的預設值為false,複合的值類型,是把各個字段都取其預設值;
在對象的構造上,引用類型預設有一個無參構造函數,但如果定義了其它的構造函數,那麼該無參構造函數被自動删除;值類型也有一個無參構造函數,但如果定義了其它構造函數,該無參構造函數依然存在,我們也不能明确定義一個無參構造函數,并且在定義的其它構造函數中,必須對所有字段進行指派,否則會出現編譯錯誤;
6、類層次結構不同
引用類型可以自由地派生,但值類型具有固定的類層次結構,其中struct類型是派生于System.ValueType,enum類型派生于System.Enum,而System.Enum派生于System.ValueType;并且所有的值類型都是密封的,我們不能把值類型用作其它類型的基類;
7、如何在值類型與引用類型中選擇
如果我們定義一個類型,通常情況下應該定義為引用類型,引用類型在參數傳遞的效率上具有優勢;
值類型由于記憶體結構比較簡單,它在建立和配置設定的效率上具有優勢(不需要引用計數);但由于值類型在傳遞的時候會引起拷貝,是以值類型的尺寸不适合定義得過大;
定義值類型和引用類型的初衷也是不同的,值類型用于資料的簡單存儲,例如int、long、float、double等,它們不會有多少與架構相關的東西;而引用類型的類層次結構的靈活性,決定了它是定義軟體架構的基石;
.NET已經為我們提供了一些值類型,例如int、long、float、double等,這些稱為基元類型,通常情況下,我們是把這些基元類型進行組合,形成自己的更有意義的值類型,例如定義坐标(x,y),定義一個值類型Point比用兩個int來表示更有意義,并且還可以在Point中定義一些有意義的方法,或重載一些運算符等;
2) 非靜态成員與靜态成員
寫出下面代碼的輸出結果:
class MyClass
{
public MyClass() { v1++; v2++; }
public static int v1;
public int v2;
}
// 注意:為了書寫友善,沒有将下面的代碼放在函數中,下同
MyClass mc1 = new MyClass(), mc2 = new MyClass();
Console.WriteLine(“{0} {1} {2}”, MyClass.v1, mc1.v2, mc2.v2);
【答案】
2 1 1
靜态成員與非靜态成員的差別:靜态成員從屬于類,而非靜态成員從屬于對象;
從上題可以看到:v1和v2都被執行了兩次自增,但因為v2是非靜态成員,分别屬于mc1和mc2,是以實際上有兩個v2,mc1.v2和mc2.v2都被從零自增為1;v1是靜态成員,從屬于類MyClass,它被執行了兩次自增,其值為2;
3) 接口
寫出下面代碼的輸出結果
interface IMyInterface
{
void Write();
}
class MyClass1 : IMyInterface
{
public void Write() { Console.WriteLine(“Hello World 1”); }
}
class MyClass2 : MyClass1
{
public new void Write() { Console.WriteLine(“Hello World 2”); }
}
class MyClass3 : MyClass2, IMyInterface
{
public new void Write() { Console.WriteLine(“Hello World 3”); }
}
class MyClass4 : MyClass3, IMyInterface
{
void IMyInterface.Write() { Console.WriteLine(“Hello World 4”); }
public new void Write() { Console.WriteLine(“Hello World 5”); }
}
IMyInterface myIn1 = new MyClass1();
IMyInterface myIn2 = new MyClass2();
IMyInterface myIn3 = new MyClass3();
IMyInterface myIn4 = new MyClass4();
myIn1.Write();
myIn2.Write();
myIn3.Write();
myIn4.Write();
【答案】
Hello World 1
Hello World 1
Hello World 3
Hello World 4
myIn1.Write()的輸出為Hello World1,類MyClass1隐式實作了接口IMyInterface中的成員void Write(),将調用MyClass1.Write()進行輸出;
myIn2.Write()的輸出為Hello World1,類MyClass2雖然也有方法void Write(),但由于MyClass2并未直接派生于接口IMyInterface,是以會認為MyClass1.Write()是IMyInterface.Write()的實作;
myIn3.Write()輸出為Hello World3,類MyClass3具有方法void Write(),并且也直接派生于接口IMyInterface.Write(),是以認為MyClass3.Write()是IMyInterface.Write()的實作;
myIn4.Write()輸出為Hello World4,類MyClass4直接派生于接口IMyInterface,并提供了隐式和顯式實作的兩個void Write(),此時将調用顯式的void Write()方法進行輸出;
4) 傳參方式
寫出下面程式的輸出結果:(如果代碼有文法錯誤請指明)
void MyMethod1(int v) { v ++; }
int v1 = 0;
MyMethod1(v1);
Console.WriteLine(v1);
void MyMethod2(ref int v) { v ++; }
int v2 = 0;
MyMethod2(ref v2);
Console.WriteLine(v2);
void MyMethod3(out int v) { v ++; }
int v3 = 0;
MyMethod3(out v3);
Console.WriteLine(v3);
【答案】
0 1 有錯誤
MyMethod1中,傳參方式為副本傳參,在函數中對形參的改變,不會影響實參的值;
MyMethod2中,傳參方式為引用傳參,将會把形參和實參作為同一個變量;
MyMethod3中,此種傳參方式用于将資料傳回給調用者,而在函數的實作中,對于out參數在使用之前必須要初始化,并且要保證所有的out參數在函數傳回前都要指派;
對于變量是否需要初始化,總結為如下幾點:
1) 字段如果不初始化,它的值将為預設值(有于預設值在前面講述過);
2) 局部變量,在使前必須要初始化;
3) 副本傳參和引用傳參,在參數被傳遞之前需要初始化;
4) 輸出參數,在參數被傳遞之前可以不初始化,但在函數的實作中,該輸出參數在使用之前必須要初始化,并且在函數傳回前必須要保證所有的輸出參數被指派;
5) 繼承、隐藏與重寫
寫出下面程式的輸出結果:
class MyClass1
{
public virtual void Write() { Console.WriteLine(“Hello World 1”); }
}
class MyClass2 : MyClass1
{
public new virtual void Write() { Console.WriteLine(“Hello World 2”); }
}
class MyClass3 : MyClass2
{
public override void Write() { Console.WriteLine(“Hello World 3”); }
}
MyClass1 mc1 = new MyClass2();
MyClass2 mc2 = new MyClass3();
mc1.Write();
mc2.Write();
【答案】
Hello World 1
Hello World 3
mc1.Write()輸出結果為Hello World 1,MyClass1.Write()為虛函數,雖然MyClass2中也有Write()的定義,但由于它并未用override來重寫,并且又用new來聲明,是以它是和基類MyClass1.Write()沒有關系的另一個方法;
mc2.Write()輸出結果為Hello World 3,myClass2.Write()被聲明為virtual,而MyClass3.Write()采用override來重寫,是以将會執行MyClass3.Write();
6) 委托
有如下的代碼:
string MyMethod(object obj){ return obj.ToString(); }
則下面文法正确的有:
a) delegate string MyDelegate(object obj); MyDelegate func = MyMethod;
b) delegate object MyDelegate(object obj); MyDelegate func = MyMethod;
c) delegate string MyDelegate(string obj); MyDelegate func = MyMethod;
d) delegate object MyDelegate(string obj); MyDelegate func = MyMethod;
【答案】
a、b、c、d
通過代理進行方法的調用,其過程如下:
1、 調用者将參數值給代理;
2、 代理将參數傳給所代理的方法;
3、 所代理的方法将傳回值傳給代理;
4、 代理将傳回值傳給調用者;
這意味着,調用者傳給代理的參數的類型,不一定必須是所代理的方法的參數的類型,而可以是其派生類;同樣的道理,所代理的方法的傳回值的類型也不一定必須是代理的傳回值類型,而可以是它的派生類;
舉一個例子,在d)中,參數及傳回值傳遞方式如下:代理把string類型的參數傳給所代理的方法中的參數object obj,由于object是string的基類,是以這種傳參是合法的,這稱之為“逆變”,所代理的方法的傳回值為string型,被傳遞給代理的object型,同樣的道理,這也是合法的,這稱之為“協變”。
注意:協變和逆變是C#2.0新增加的特性,這是為了增加委托的靈活性;
再一點需要說明的是“委托”和“代理”這兩個概念:就像對象是類的執行個體一樣,代理是委托的執行個體。
7) 反射
i. 簡單說明一下您對反射的了解,及什麼情況下使用反射
ii. 簡述C#關鍵字is與as的用法
【答案】
i. 通過在程式集中儲存的中繼資料,反射技術可以使我們在運作時剖析一個程式集:擷取程式集中包括的類型資訊,以及類型中的成員資訊等,并且可以動态地建立類型及調用其成員;這意味着,我們可以通過反射技術來制作插件,還可以在程式集中使用Attribute來标注自定義的中繼資料供運作時分析,使架構設計達到最大程度的松耦合;
ii. C#中用is來判斷一個對象是否能夠轉換為指定的類型,用as來将一個對象轉換為指定的類型,但與強制轉換的差別是:當轉換失敗時as會傳回空引用,但強制轉換會抛出異常,這意味着,用as來轉換的類型必須為引用類型;
8) 裝箱與拆箱
請指出下面代碼中,哪些地方會發生裝箱或拆箱
int a = 10;
object obj = a;
obj = 100;
int b = (int)obj;
Type type = b.GetType();
string str = b.ToString();
Console.Write(obj);
Console.Write(b);
【答案】
object obj = a; 會發生裝箱,把當值類型賦予引用類型時,會發生裝箱;
obj = 100; 會發生裝箱,原因同上;
int b = (int)obj; 會發生拆箱,當把引用類型賦予值類型時,會發生拆箱;
Type type = b.GetType(); 會發生裝箱,類成員函數,實際上把該類的對象作為第一個參數,因為GetType()是object的成員,是以它的第一個參數類型為object,而b為int型,把int型賦予object時會發生裝箱;
注意:最後三句不會發生裝箱
string str = b.ToString(); 雖然ToString()是object的方法,但int已經對它進行了重寫,并且值類型都是密封的,是以将會直接調用int的ToString()方法;
而最後兩句:Console.Write()共有18個重載的方法,其中有Console.Write(object value)及Console.Write(int value)兩個重載方法,是以不會産生裝箱。
9)泛型
i. 如果要限制泛型中的類型參數T為引用類型,可以用什麼樣的文法?
ii. 如果泛型類中,某方法要傳回類型參數T的預設值,可以用什麼樣的文法?
【答案】
i. where T : class
ii. return default(T);
10)運算符重載
i. C#中的運算符重載,哪些運算符要求成對重載?
ii. 實作C#運算符重載的方法,需要有哪些固有的特征?(例如通路修飾符等特征)
【答案】
i. == 與 !=
> 與 <
>= 與 <=
false 與 true
ii. 必須聲明為public static
11)集合類
i. 簡述object類中的兩個虛函數GetHashCode()與Equals()的作用
ii. 簡述接口IEnumerable與IEnumerator的作用
iii. 請列舉一下您所熟悉的.NET集合類,越多越好
【答案】
i. 在集合類中,主要用object類Equals()來比較兩個對象是否相等,而不是用==來比較是否為同一個對象,這是為了增加其靈活性;例如,如果要判斷一個集合List<string>中是否存在某個字元串,是調用Contains(string str)來判斷的,而該函數上的輸入參數的字元串并不一定必須和集合中的某個字元串是同一個對象才算相等,隻要重載Equals來定義的兩個字元串是否相等就可以達到這樣的靈活性;
GetHashCode()用于哈希表中來組織資料,哈希表首先調用鍵的GetHashCode()來決定資料應該放在什麼地方,在通過鍵來擷取值的時候,也是先調用鍵的GetHashCode()來找到其所在的範圍,用來縮小查找範圍;
我們在重寫GetHashCode()時,需要注意它的傳回值應該固定,并且計算必須快速,否則影響查找效率;
ii. .NET的集合類都實作了IEnumerable接口,用于擷取集合的疊代器,實作該接口的類都可以由foreach來周遊;IEnumerator是疊代器的接口,是集合類如何周遊的具體實作;
iii. List<T>、Hashtable、Dictionary<TKey, TValue>、Stack<T>、Queue<T>、LinkedList<T>、SortedList<T>、SortedDictionary<TKey, TValue>
12)異常
以下對異常的解釋正确的有:
a) 異常類必須要派生于System.Exception(或其派生類)
b) 在一個方法中出現的異常,如果該方法沒有對其進行處理(catch),則會沿着調用堆棧尋找可以處理該異常的代碼
c) 如果一個方法有傳回值,那麼即使出現了異常,也應該傳回一個明确的值;
d) 隻要進入了try塊,那麼它會最終執行finally塊;(不考慮突然斷電等導緻程式突然中止的情況)
e) 在finally中也可以使用return語句
f) 如果在finally塊中出現了異常,那麼原先在try或catch塊中抛出的異常會丢失
【答案】
a) b) d) f)
說明:
a) 異常類都必須要直接或間接派生于System.Exception,對于系統的異常,都派生于System.SystemException,而我們自己定義的異常,通常都派生于System.ApplicationException;
b) 異常的抛出有return的效果,但throw具有多重return的效果,直到找到可以處理該異常的catch塊為止,如果最終也沒有找到catch塊,程式則會終止執行;
c) throw已經有return的效果,當函數出現了異常時,說明他沒有執行成功,那麼也就沒有必要傳回一個明确的值;
d) e) f)在try塊或catch塊中,即使出現了異常或return語句,那麼在函數傳回之前,也會首先執行finally語句;是以finally塊中不可以使用return語句,但如果此時finally語句再次抛出異常,那麼在try或catch中抛出的異常就會丢失,是以在finally中應該防止抛出異常;
13)多線程
i. 簡述Monitor、Semaphore、Mutex的作用
ii. 簡述C#關鍵字lock的作用
【答案】
簡述如下:(更詳細的解釋參見MSDN)
i. Monitor(臨界區):通過對象鎖的方式來控制線程的執行;
Semaphore(信号量):限制可同時通路某一資源的線程數;
Mutex(互斥體):用于線程間同步
ii. lock(…){…}關鍵字相當于在大括号開始時執行Monitor.Enter(),在大括号結束時執行Monitor.Exit(),但文法更簡潔,并且能保證在抛出異常時也執行Monitor.Exit(),用這種文法可以建立一個臨界區。
14).NET類庫知識
i. 簡述StringBuilder的用途
ii. 簡述IDisposable接口的用途
【答案】
i. 當大量連接配接字元串時,StringBuilder可以顯顯地提高效率,StringBuilder在連接配接字元串時,隻是将字元串添加到一個集合中,到調用其ToString()方法時才進行連接配接;
ii. C#也像C++那樣可以寫析構函數,但對于配置設定于堆上的對象,析構函數要等待無用記憶體回收器回收時才運作,但無用記憶體回收器在運作時會引起應用程式的停頓,是以不推薦在C#中寫析構函數。為了釋放非托管資源(例如檔案、資料庫連接配接等),可以為其實作IDisposable接口,這樣可以使用using(…){…}這種簡潔的文法,在大括号結束之前調用其IDisposable接口。
15)您是否了解C#3.0,如果了解,簡單說一下它的新特性。(選答)
【答案】
Linq、匿名類型、隐式類型本地變量、對象初始化器、集合初始化器、Lambda表達式、自動屬性、擴充方法
2、 設計模式1) 請列舉一下您所知道的模式,越多越好:
【答案】
設計模式可分為三大類:
建立型模式:工廠方法(Factory Method)、抽象工廠(Abstract Factory)、Prototype(原型模式)、Builder(構造模式)、Singleton(單件模式)
結構模式:外觀模式(Facade)、代理模式(Proxy)、擴充卡模式(Adapter)、組合模式(Composite)、裝飾模式(Decorator)、橋接模式(Bridge)、享元模式(Flyweight)。
行為模式: 模闆方法(Template Method)、備忘錄模式(Memento)、觀察者模式(Observer)、職責鍊模式(Chain of Responsibility)、指令模式(Command)、狀态模式(State)、政策模式(Strategy)、中介者模式(Mediator)、解釋器模式(Interpreter)、通路者模式(Visitor)、疊代器模式(Iterator)
2)請從您所熟悉的模式中,任選兩個模式,并用簡單的代碼描述一下:
【答案】(略,可以參照《設計模式·可複用面向對象軟體的基礎》一書中的示例)
3 、程式設計1) 請任選一種算法,用C#文法實作對一個整型數組的逆序排序
【答案】
可以有多種排序方法,最簡單的是冒泡排序:
int[] array = new int[]{ 2, 4, 5, 6, 7, 2, 1, 9, 0 };
for(int k=0; k<array.Length; k++)
{
for(int i=0; i<k; i++)
{
if(array[k] < array[i])
{
int t = array[k];
array[k] = array[i];
array[i] = t;
}
}
}
2) 假如一個整型數組中有重複的元素,請用C#寫一個算法,将其中重複的元素去除,隻保留一個
【答案】
int[] array = new int[]{ 2, 4, 5, 6, 7, 5, 6, 7, 2, 1, 9, 0 };
List<int> list = new List<int>();
foreach(int item in array)
{
if(!list.Contains(item))
list.Add(item);
}
array = list.ToArray();
還有其它更為高效的剔重方法