C#的变迁史02 - C# 2.0篇
在此重申一下,本文僅代表個人觀點,如有不妥之處,還請自己辨別。
第一代的值類型裝箱與拆箱的效率極其低下,特別是在集合中的表現,所以第二代C#重點解決了裝箱的問題,加入了泛型。
1. 泛型 - 珍惜生命,遠離裝箱
集合作為通用的容器,為了兼容各種類型,不得已使用根類Object作為成員類型,這在C#1.0中帶來了很大的裝箱拆箱問題。為了C#光明的前途,這個問題必須要解決,而且要解決好。
C++模板是一個有用的啟迪,雖然C++模板的運行機制不一樣,但是思路確實是正確的。
帶有形參的類型,也就是C#中的泛型,作為一種方案,解決了裝箱拆箱,類型安全,重用集合的功能,防止具有相似功能的類泛濫等問題。泛型最大的戰場就是在集合中,以List<T>,Queue<T>,Stack<T>等泛型版本的集合基本取代了第一代中非泛型版本集合的使用場合。當然除了在集合中,泛型在其他的地方也有廣泛的用途,因為程序員都是懶的,重用和應對變化是計算機編程技術向前發展最根本的動力。
為了達到類型安全(比如調用的方法要存在),就必須有約定。本著早發現,早解決的思路,在編譯階段能發現的問題最好還是在編譯階段就發現,所以泛型就有了約束條件。泛型的約束常見的是下面幾種:
a. 構造函數約束(使用new關鍵字),這個約束要求實例化泛型參數的時候要求傳入的類必須有公開的無參構造函數。
b. 值類型約束(使用struct關鍵字),這個約束要求實例化泛型參數的類型必須是值類型。
c. 引用類型約束(使用class關鍵字),這個約束要求實例化泛型參數的類型必須是引用類型。
d. 繼承關系約束(使用具體制定的類或接口),這個約束要求實例化泛型參數的類型必須是指定類型或是其子類。
當然了,泛型參數的約束是可以同時存在多個的,參看下面的例子:
public class Employee {} class MyList<T, V> where T: Employee, IComparable<T>where V: new() { }如果不指定約束條件,那么默認的約束條件是Object,這個就不多講了。
當使用泛型方法的時候,需要注意,在同一個對象中,泛型版本與非泛型版本的方法如果編譯時能明確關聯到不同的定義是構成重載的。例如:
public void Function1<T>(T a); public void Function1<U>(U a); 這樣是不能構成泛型方法的重載。因為編譯器無法確定泛型類型T和U是否不同,也就無法確定這兩個方法是否不同public void Function1<T>(int x); public void Function1(int x); 這樣可以構成重載public void Function1<T>(T t) where T:A; public void Function1<T>(T t) where T:B; 這樣不能構成泛型方法的重載。因為編譯器無法確定約束條件中的A和B是否不同,也就無法確定這兩個方法是否不同使用泛型就簡單了,直接把類型塞給形參,然后當普通的類型使用就可以了。例如:
List<int> ages = new List<int>(); ages.Add(0); ages.Add(1); ages.Remove(1);2. 匿名函數delegate
在C# 2.0中,終于實例化一個delegate不再需要使用通用的new方式了。使用delegate關鍵字就可以直接去實例化一個delegate。這種沒有名字的函數就是匿名函數。這個不知道是不是語法糖的玩意兒使用起來確實比先定義一個函數,然后new實例的方式要方便。不過最方便的使用方式將在下一版中將會到來。
delegate void TestDelegate(string s); static void M(string s) {Console.WriteLine(s); }//C# 1.0的方式 TestDelegate testDelA = new TestDelegate(M);//C# 2.0 匿名方法 TestDelegate testDelB = delegate(string s) { Console.WriteLine(s); };談到匿名函數,不得不說說閉包的概念。
如果把函數的工作范圍比作一個監獄的話,函數內定義的變量就都是監獄中的囚犯,它們只能在這個范圍內工作。一旦方法調用結束了,CLR就要回收線程堆棧空間,恢復函數調用前的現場;這些在函數中定義的變量就全部被銷毀或者待銷毀。但是有一種情況是不一樣的,那就是某個變量的工作范圍被人為的延長了,通俗的講就像是某囚犯越獄了,它的工作范圍超過了劃定的監獄范圍,這個時候它的生命周期就延長了。
閉包就是使用函數作為手段延長外層函數中定義的變量的作用域和生命周期的現象,作為手段的這個函數就是閉包函數。看一個例子:
class Program{static void Main(string[] args){List<Action> actions = getActions();foreach (var item in actions){item.Invoke();}}static List<Action> getActions(){List<Action> actions = new List<Action>();for (int i = 0; i < 5; i++){Action item = delegate() { Console.WriteLine(i); };actions.Add(item);}return actions;}}你可以試試運行這個例子,結果和你預想的一致嗎?這個例子會輸出5個5,而不是0到4,出現這個現象的原因就是閉包。getActions函數中的變量i被匿名函數引用了,它在getActions調用結束后還會一直存活到匿名函數執行結束。但是匿名函數是后面才調用的,執行它們的時候,i早就循環完畢,值是5,所以最終所有的匿名函數執行結果都是輸出5,這是由閉包現象導致的一個bug。
要想修復這個由閉包導致的問題,方法基本上是破壞閉包引用,方式多種多樣,下面是簡單的利用值類型的深拷貝實現目的。
第一個方法:讓閉包引用不再指向同一個變量
for (int i = 0; i < 5; i++) {int j = i;Action item = delegate() { Console.WriteLine(j); };actions.Add(item); }第二個方法:包上一層函數來構造新的作用域
static List<Action> getActions() {List<Action> actions = new List<Action>();for (int i = 0; i < 5; i++){Action item = ActionMethod(i);actions.Add(item);}return actions; }static Action ActionMethod(int p) {return delegate(){Console.WriteLine(p);}; }閉包現象提醒我們使用匿名函數和3.0中的Lambda表達式時都要時刻注意變量的來源。
3. 迭代器
在C# 1.0中,集合實現迭代器模式是需要實現IEnumerable的,這個大家還記得吧,這個接口的核心就是GetEnumerator方法。實現這個接口主要是為了得到Enumerator對象,然后通過其提供的方法遍歷集合(主要是Current屬性和MoveNext方法)。自己去實現這些還是比較麻煩的,先需要定義一個Enumerator對象,然后在自定義的集合對象中還需要實現IEnumerable接口返回定義的Enumerator對象,于是一個新的語法糖就出現了: yield關鍵字。
在C# 2.0中,只需要在自定義的集合對象中還需要實現IEnumerable接口返回一個Enumerator對象就行了,這個創建Enumerator對象的工作就由編譯器自己完成了。看一個簡單的小例子:
public class Stack<T>:IEnumerable<T> {T[] items;int count;public void Push(T data){...}public T Pop(){...}public IEnumerator<T> GetEnumerator(){for(int i=count-1;i>=0;--i){yield return items[i];}} }使用yield return創建一個Enumerator對象是不是很方便?編譯器遇到yield return會創建一個Enumerator對象并自動維護這個對象。
當然了,多數時候foreach必要遍歷集合中的每一個元素,這個時候使用yield return配合for循環枚舉每個元素就可以了,但是有時候只需要返回滿足條件的部分元素,這個時候就要結合yield break中斷枚舉了,看一下:
//使用yield break中斷迭代: for(int i=count-1;i>=0;--i) {yield return items[i];if(items[i]>10){yield break;} }4. 可空類型
這個特性我覺得又是把值類型設計成引用類型Object類子類后,微軟生產的怪語法。空值是引用類型的默認值,0值是值類型的默認值。那么在某些場合,比如從數據庫中的記錄取到內存中以后,沒有值代表的是空值,但是字段的類型卻是值類型,怎么搞呢?于是整出了可空類型。當然了,這個問題可以通過在設計表的時候給字段設計一個默認值來解決,但是有的時候某些字段的設置默認值是沒有意義的,比如年齡,0有意義嗎?
可空類型的概念很簡單,沒什么可說的,不過一個相似的語法卻讓我感到很舒服:那就是"??"操作符。這是一個二元操作符,如果第一個操作數是空值,則執行第二個操作數代表的操作,并返回其結果。例如:
static void Main(string[] args) {int? age = null;age = age ?? 10;Console.WriteLine(age); }5. 部分類與部分方法
這一特性還是比較好用的,終于不用把所有的內容擠到一起了,終于可以申明和實現相分離了,雖然好像以前也可以做到,但是現在這項權利也下放給人民群眾了。partial關鍵字帶來了這一切,也帶來了一定的擴展性。這個特性也比較簡單,就是使用partial關鍵字。編譯的時候,這些文件中定義的部分類會被合并。部分方法是3.0的特性,不過沒什么新意,就放到2.0一起說吧。
使用這個特性的時候需要注意:
a. 部分方法只能在部分類中定義。
b. 部分類和部分方法的簽名應該是一致的。
c. partial用在類型上的時候只能出現在緊靠關鍵字 class、struct 或 interface 前面的位置。
d. public等訪問修飾符必須出現在partial前面。
f. partial定義的東西必須是在同一程序集和模塊中。
看一個簡單的例子:
// File1.cs namespace PC {partial class A{int num = 0;void MethodA() { }partial void MethodC();} }// File2.cs namespace PC {partial class A{void MethodB() { }partial void MethodC() { }} }這里需要注意一下MethodC的申明和定義是分開的就可以了。
還有一點,很多人認為partial破壞了類的封裝性,實際上談不上。因為一個類能分部,就說明類的設計者認為是需要保留這個擴展性的,所以后面的人才可以給這個類添加一些新的東西。
6. 靜態類
這個特性也是比較符合實際情況的,很多情況下,某些對象只需要實例化一次,然后到處使用,單件模式是可以實現這個目的,現在靜態類也是一個新的選擇。
靜態類只能含有靜態成員,所以構造函數也是靜態的,既然是靜態的,那么它與繼承就沒什么關系了。靜態類從首次調用的時候創建,一直到程序結束時銷毀。
簡單看一個小例子:
public static class A {static string message = "Message";static A(){Console.WriteLine("Initialize!");}public static void M(){Console.WriteLine(message);} }不過據經驗講,有沒有靜態構造函數對靜態類的構造時間是有影響的,一個是出現在首次使用對象成員的時候,一個是程序集加載的時候,不過分清這個實在沒什么意義,有興趣的同學自己研究吧。
C#2.0的新特性絕不止這幾個,但是對程序猿們影響比較大的都在這了,更多的就參看微軟的MSDN吧。
總結
以上是生活随笔為你收集整理的C#的变迁史02 - C# 2.0篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微软IE浏览器退役:日本受影响最大
- 下一篇: 消息称百度全额出售爱奇艺股份!官方回应: