這是一個多變的時代,一次又一次的浪潮將不同的人推上了巔峰。新的人想搭上這一波,同時老的人也不想死在沙灘上。這些年新的浪潮又一次推開,歷史不停地重復上演,那便是移動互聯網。它的興起無人抗拒,而在這一波浪潮中移動互聯網游戲更是重中之重。
在這個混沌時期,手機游戲以幾倍的速度經歷着客戶端游戲曾經走過的道路,從輕度到重度,從弱交互到強交互,從簡單到復雜。來到2014,我們正處於一個中間時期,由於門檻低,渠道單一,手機游戲依然是渠道為王。當然這並不是這篇隨筆的重點。
同時手機游戲由於目前的環境,需要更快的速度推上市場,這便帶來了快速開發的需求。而unity3d搭上了這一波,同時也將.net更深入地推入游戲開發人員的手中。這便是這篇隨筆的由來。之前對.net有一定了解,但沒有系統學習過。最近讀了Essential C#這本書,作為新手學習的話,個人十分推薦,中文版應該叫C#本質論。后面記述的便是讀后的一些筆記,若有理解上的偏差,還望指正。
1 System.Environment.NewLine.
跨平台換行符,由於平台差異,windows換行使用\r\n,而Unix上使用\n。所以當需要保持輸出的跨平台性,可以使用Console.WriteLine,或者在字符串后添加System.Environment.NewLine
2 String
和AS,以及很多腳本語言一樣,C#中的String也是不可改變的。相同字符串在內存中只會有一份內存,而不同的字符串變量都引用該內存。修改String會生成一份新的內存來保存新的字符串,而不會修改其本身。如果由於構建一個字符串需要很多步驟,因此修改之前創建的字符串是不可避免的。這時可以使用System.Text.StringBuilder,它提供了如String類似的函數,區別在於System.Text.StringBuilder的函數是修改其本身的內存,而不是新生成一個。
3 @逐字解釋字符串
在字符輸出時,可以使用@來直接輸出@后面的字符串。@后面除了\"的轉移字符起作用外,其他都不會有作用。簡單來說就是輸出即見即所得。
4 nullable修飾符
一般來說,C#中的值類型是不能賦值為null的,但在一些特殊情況下,如果輸入是一個魔數,比如說數據庫中某一列允許null,那么當這個數據輸入到C#中,就會產生問題。所以我們在這種情況下可以使用nullable修飾符來使得值類型接受null。例如int? account = null
5 數組以及多維數組
數組聲明Type[] varName。多維數組通過在[]添加“,”來聲明。比如Type[,] varName是聲明一個二維數組。數組的維度數等於","的數量+1。數組的Length是不會改變的。它的Clear函數並不會減少數組的Length大小,只是將數組的每個元素的值設置成默認值。
6 ??操作符號
在C#2.0中加入判斷null的快捷操作符,很方便。expression1??expression2。如果1不為null,表達式的值為1,反之,為2。比如說name = filename??"default.txt"。這個用來做空的保護太方便了。
7 nowarn:<warn list>
除了使用#pragma指令來關閉某條warning外,比如說#pragma warning disable 1030。還可以在編譯器命令行中使用nowarn:<warn list>中關閉warning。與前者不同的是前者只影響單個cs文件,后者影響整個編譯過程。例如:csc /nowarn:1591
8 using與別名
using除了可以引用命名空間,還可以設置別名。比如using StringFormat = System.String;
9 params函數變參
C#中的函數變參通過params關鍵字和數組來聲明。傳入函數的每一個參數分別為數組的每一個元素。比如:static string combine(params string[] paths)。我們可以foreach(string s in paths)。當不傳入參數時,數組大小為0.
10 對象初始化列表與集合初始化列表
在C#3.0中加入了對象初始化列表,可以在構造函數之后,加入列表,對對象的可見域和屬性進行賦值。這個也是吸入像js這種動態語言的特性,方便書寫代碼。在IL中,它被翻譯為構造函數調用后,后面跟着列表中每個屬性的賦值操作,且順序與列表中的順序一致。比如:A a = new A(){Title = "ABC", Sa = 1}; 而集合初始化列表與其類似,不同在於它用來在初始化時給集合添值。比如說List<A> a = new List<A>(){ new A(),new A(),new A()};
11 Finalizers
它定義了對象在被垃圾回收前執行的行為。finailzers不會在對象被標記為垃圾回收時立即執行,而是在對象被標記后與程序結束之前的這段時間內執行。更詳細來說,GC發現回收對象帶有finailzer,就不會立即回收內存,而是將其加入到一個隊列中去。然后用另外一個線程來執行隊列中的對象finailzer函數,執行完畢后,再將其移除隊列,告知GC可以回收該對象。這個和lua對userdata的處理有一定類似,不過lua的userdata始終是由lua_state所在的線程來處理的。感覺lua要更輕量級一點。
12 匿名變量與匿名類型
C#3.0加入了匿名類型。例如:var p1 = new{title = "avb", year = 1};IL會通過你的屬性賦值在編譯期自動生成該類型。這又是一項動態語言的特性,類似json與lua table。不過C#的感覺只是語法糖,在IL層面依然是強類型語言。
13 靜態構造函數
C#支持靜態構造函數,用來初始化類。他會在第一次訪問類的時候被調用,具體來說就是比如第一次調用構造,第一次訪問靜態函數或者成員變量。我們在靜態構造函數中對靜態變量賦值,如果該變量在聲明處賦值,那么最后生效的是靜態構造函數里的賦值。
14 靜態類
public static class SimpleMesh。靜態類,他有兩個作用:一,防止程序員實例化這個類;二,防止在該類中聲明實例變量和函數。在IL中,靜態類被加入了abstract和sealed兩個標識。
15 const,readonly
const常量標識符,一個標識為常量的變量,編譯器會自動為其添加static,因為沒有實例對象能改變他的值。如果我們手動為其添加static標識,編譯器會報錯。需要注意的是如果一個assembly引用了另一個assembly的常量,這個常量值會被編譯到引用方的assembly里面去。如果這時被引用的assembly里的常量值變化了,引用方不重新編譯的話,引用方里的值是不會改變的。因此被標識為const的變量應該是永遠不會改變的,包含初始化的值。如果在未來具有可變性,應該使用readonly代替。
readonly與const不同在於:一,readonly只能用於成員變量,const還可以用於局部變量;二,readonly可以在構造函數和聲明處修改。三,不同對象之間readonly的變量可以不同,所以readonly不會自動添加static;他可以用於靜態變量和實例變量。四,readonly的賦值在運行時,而const在編譯時。最后當數組被標識為readonly時,只是數組的個數不能改變,而不是數組的元素不能改變。因為readonly只是使得該變量不能指向新的實例。
16 Partial類和Partial函數
Partial類允許將類定義在不同的cs文件中。一般在我們需要對工具生成的代碼類中添加自己的變量和函數時需要用到它。在C#3.0中,添加了Partial函數,它提供了更細節的修改生成類的方式。我們可以在生成類中聲明Partial函數,比如:partial void OnNameChanging()。然后生成類中函數可以調用這個函數,而這個函數的實現在另一個cs文件中的Partial類中,這個類是我們手動編寫的類,為這個生成類提供補充。因此Partial函數只能在Partial類中出現,並且並不要求所有的函數都有實現,所以這種函數的返回值只能為void,也不能使用out。因為如果沒有這個設定,那么當你調用一個沒有實現的partial函數時,其返回值是不確定的。C#的設計者避免了這種情況。最后,編譯器在編譯時發現某個調用的partial函數沒有實現,那么IL中將不會存在這個函數。
17 Struct
Struct與C++中的Struct有很大不同,與Class是引用類型相比,Struct在C#里的一種值類型。並且Struct不能擁有默認構造,由於C#中對成員變量聲明初始化也是被編譯器放入到默認構造函數中執行的,所以Struct也不擁有這項功能。雖然不支持默認構造,但是Struct卻支持帶參數的構造函數,但該構造函數必須初始化所有的成員變量。同時,Struct也沒有finalizers函數,雖然它也能夠使用new操作符來生成實例。new在CIL層面,Class的指令是newobj.new,而Struct是initobj。最后在繼承方面,所有值類型都是sealed,並且都繼承於System.ValueType,但是值類型可以執行接口。由於這些特性,在C#中盡可能將Struct作為不可變類型來使用,比如配置,或者說少用。這與C,C++有很大不同。
18 boxing和unboxing
boxing和unboxing發生在值類型與引用類型相互轉換的時候。比如說當一個值類型要轉化到他執行的接口或者他的基類時,由於這些類型屬於引用類型,這個時候就會出現boxing的操作。boxing具體分為3步:第一,在heap上分配包含值類型的數據以及一些其他信息的內存;第二,將值類型的數據從stack拷貝到上一步分配的內存中去;第三,賦值對象或者接口引用這個heap內存。反之,將引用類型轉換回值類型就是unboxing。可見頻繁的boxing和unboxing會十分影響性能,需要盡力避免,而且由於存在拷貝的過程,那么在boxing后,對之前的值類型修改,不會影響到boxing之后的引用類型數據。反之unboxing也是一樣。
19 gc和weak reference
.net的gc使用的mark-and-compact的算法,mark部分和很多gc算法一樣,從root開始,然后一直向下mark。最后得到所有的reachable的對象。接着下來是compact的部分,不是通過reachable對象去篩選unreachable,而是將reachable的對象在內存中相鄰排列,這個過程也同時覆蓋了unreachable的內存。最后除開reachable的內存,剩下的就是可用內存了,從而完成了gc的過程。
由於mark和移動reachable對象需要一個一致的runtime state,所以在gc時,所有的managed thread都會掛起等待gc結束。因此我們需要避免gc發生在關鍵代碼時間里,System.GC提供了Collect函數,他會立即進入gc流程,從而降低gc在關鍵代碼時間里出現的可能性。
.net的gc還有一個特性,就是剛創建的對象更容易被gc,而生命周期長的對象被gc的概率相對較低。這個特性是符合一般程序開發情景的。在一個函數的執行過程中,我們通常會產生很多臨時的小變量。為了實現這個特性,.net在內部將對象分為三代,每當一個對象存活了一個gc周期,就會被放入到下一代中,直到最后一代。然后gc運行的頻率在0代上最高,2代上最低。
最后是weak reference:和lua類似,弱引用保持了對對象的引用關系,但不會影響對象是否被gc。一般我們可以用它來引用一些創建消耗高,維持占用高的數據,比如一個很大的數據庫表。這時weak reference利用了對象被解引用,但gc還不一定到來這個機會。如果這段時間又需要訪問這塊數據,我們就可以首先判定弱引用是否為null,如果沒有為null,說明對象還在,那么就可以再次使用。值得注意的是,這個過程需要先將弱引用賦值給強引用再判斷是否為null,防止在判定與賦值之間對象被gc。
20 finalizer函數和IDisposable接口
finalizer函數與c++的類析構函數語法表達一致,他是在gc時被調用來釋放與對象相關聯的一些資源的接口,比如文件,數據庫連接等。我們不能直接調用它,它會在對象被gc時,加入到一個叫f-reachable的隊列中去,然后在加入隊列到程序結束之間的某一個時間點在另一個線程中觸發。因此finalizer具有不確定性因素,而IDisposable接口提供了顯示調用的Dispose接口,配合using語句,可以主動調用。
21 Lambda表達式和閉包
在C#3.0中加入了Lambda表達式。如:(int a, int b) => {a < b},由於C#的強類型要求,所以編譯器如果能夠通過他的賦值delegate的參數類型以及表達式的返回值推導出參數類型,那么參數的類型可以省去。如(a, b) => {a < b}。他也是一層語法糖,在IL層面通過有無upvalue,表現為兩種形式。無upvalue的為一個普通函數,有upvalue的為一個seal類,upvalue為他的成員變量。所以在C#中,函數依然不是第一類值,但他也具有了一些函數式語言的特性。不過由於強類型要求,使用時個人感覺還是不如動態語言方便,比如說Lambda表達式的參數個數,返回類型,還是受到聲明的delegate的約束。
