從零開始獨(dú)立游戲開發(fā)學(xué)習(xí)筆記(十)--Unity學(xué)習(xí)筆記(四)--微軟C#指南(一)

學(xué)微軟的東西特別好的一點(diǎn)就是,文檔指南?特別用心。
0. 如何看文檔
對不起,打臉了,微軟這 C# 文檔結(jié)構(gòu)寫的過于意義不明,結(jié)構(gòu)混亂(文檔結(jié)構(gòu),不是文檔本身),幽幽子看了都會(huì)絕食。
為什么這么說呢?
首先我們看一下結(jié)構(gòu):

你們猜一下正確的教程在哪里?你是不是以為是那個(gè)大大的"教程"兩個(gè)字?
然而如果你點(diǎn)開會(huì)發(fā)現(xiàn):

很明顯看標(biāo)題就知道這不是給初學(xué)者看的東西,那么正確的第一個(gè)教程在哪里呢?在這里:

那么你猜第二個(gè)教程在哪里呢?沒錯(cuò),在"基礎(chǔ)"版塊下的教程里。

那么正確的全教程順序是什么呢?
首先看完"入門"版塊下的教程。
然后看完"基礎(chǔ)"版塊下的教程。
然后看完"C#中的新增功能"版塊下的教程。
然后看完最外層那個(gè)光禿禿的"教程"版塊。
沒錯(cuò),最外層的那個(gè)教程,實(shí)際上指的是最后一個(gè)教程。
相信任何人都會(huì)覺得這結(jié)構(gòu)意義不明。
本篇文章前 5 節(jié)(從 C# 語言介紹 到 程序結(jié)構(gòu))來自"入門"版塊的簡介。看完"簡介"后我發(fā)現(xiàn)下一個(gè)子版塊,也就是"類型" 根本不是給人學(xué)習(xí)看的。經(jīng)過苦苦尋找我才找到真正的初學(xué)者應(yīng)該看的,也就是剛剛上面介紹的順序。真的太難了。微軟罪大滔天。
1. C# 語言介紹
C# 是一門面向?qū)ο?,面向組件的語言。沒別的好說的了。
2. .NET 介紹
C# 在 .NET 上運(yùn)行。
介紹 .NET 之前應(yīng)該先介紹一下 CLI(不是命令行那個(gè) CLI 哦,是 Common Language Infrastructure),CLI 是一個(gè)標(biāo)準(zhǔn),定義了一個(gè)跨語言的運(yùn)行環(huán)境。
然后我們才有 CLR(Common Language Runtime),CLR 是微軟開發(fā)的一個(gè) CLI 的實(shí)現(xiàn)。也就是說,CLR 是一個(gè)跨語言的運(yùn)行環(huán)境。(CLI 是標(biāo)準(zhǔn),定義了一個(gè)跨語言的運(yùn)行環(huán)境;CLR 是實(shí)現(xiàn),它就是一個(gè)跨語言的運(yùn)行環(huán)境)
而 .NET 則是一個(gè)開發(fā)平臺,其中包含了 CLR,同時(shí)還有一堆類庫,以及一些亂七八糟的東西??梢哉f .NET 是一個(gè)統(tǒng)稱。
既然 .NET 包括了 CLR,也就是說 .NET 并不是和 C# 綁定的。你可以使用任何 CLR 支持的語言在 .NET 上編寫程序,例如 F# 等。
在 .NET 上寫完源代碼后,會(huì)被編譯成 IL 語言。IL 代碼和資源會(huì)被儲存在擴(kuò)展名通常為 dll 的程序集中。
執(zhí)行 C# 程序時(shí),這些程序集會(huì)被加載到 CLR 上(因此想要運(yùn)行 C# 程序必須得有 .NET,這也是為什么很多程序尤其是游戲,經(jīng)常會(huì)讓你去下載 .Netframework 第幾幾幾版本)。然后 CLR 會(huì)對這些 IL 代碼執(zhí)行實(shí)施編譯(JIT,Just-In-Time),編譯成本機(jī)指令。CLR 提供像是垃圾回收,異常處理,資源管理等等這些功能。
IL 是一種中間語言,因此可以和其他 .NET 版本的 F#,C++ 等語言編譯出來的 IL 代碼進(jìn)行交互。一個(gè)程序集中可以有很多模塊,這些模塊由不同語言編寫而成,但是它們之間卻能夠互相引用。
除了 CLR,.NET 還提供了類庫(class libraries)。如果熟悉其他語言的話,很容易理解??傊烁鞣N像是輸入輸出,web框架,字符串控制等等類庫,都被分成對應(yīng)的 namespace。當(dāng)然,除了官方提供的,還有別人或自己寫的第三方庫。
可以看這張圖(如有錯(cuò)誤/模糊不清,歡迎指出):

3. Hello World
先上代碼:
using System;class Hello { ? ?static void Main() { ? ? ? ?Console.WriteLine("Hello, World!"); ? ?} }
雖然只是一個(gè) HelloWorld,但是還是有很多說法的:
第一行的 using System; 表示使用 System 這個(gè) namespace。namespace 里面可以包含類型,也可以包含其他 namespace。例如第五行里的 Console,其實(shí)就是 System 里包含的類?
System.Console
,此外 System 里也有其他 namespace 諸如?IO
?和?Collections
。當(dāng)引用了 namespace 就可以簡寫,比如第五行本來應(yīng)該寫?System.Console.WriteLine
,但是由于有了第一行的引用,所以簡寫成?Console.WriteLine
?也是可以的。我們有一個(gè)類叫做 Hello,然后里面有一個(gè)方法叫做 Main,被 static 修飾符修飾,這個(gè)叫做靜態(tài)方法。靜態(tài)方法不需要實(shí)例就能使用。實(shí)例方法則需要在實(shí)例上運(yùn)行,實(shí)例方法中可以通過 this 來引用實(shí)例。此外,Main 是 C# 規(guī)定的程序入口。程序運(yùn)行的時(shí)候會(huì)自動(dòng)去找這個(gè)方法來運(yùn)行,所以必須有一個(gè)這個(gè)方法。
Console 是一個(gè)類,里面的 WriteLine 是其靜態(tài)方法。Cosole 類由標(biāo)準(zhǔn)庫提供,默認(rèn)情況下編譯器會(huì)自動(dòng)引用標(biāo)準(zhǔn)庫。
4. 類型和變量
C# 有兩種類型。值類型和引用類型。值類型的變量直接包含數(shù)據(jù),而引用類型的變量則儲存對數(shù)據(jù)的引用。
以下是類型的細(xì)分:
值類型:
(T1, T2, T3...) 格式的用戶自定義類型
其他所有值類型的擴(kuò)展,包含那個(gè)值類型的所有值和 null
struct S {...} 格式的用戶自定義類型
enum E {...} 格式的用戶自定義類型。
有符號整型:sbyte, short, int, long
無符號整型:byte, ushory, uint, ulong
Unicode 字符:char,代表 UTF-16 代碼單元
IEEE 二進(jìn)制浮點(diǎn)數(shù):float,double
高精度十進(jìn)制浮點(diǎn)數(shù):decimal
布爾值:bool
簡單類型:
枚舉類型:
結(jié)構(gòu)類型:
可以為 null 的值類型
元祖值類型:
引用類型:
delegate int D(...) 格式的用戶自定義類型
一維,多維,或交錯(cuò):int[], int[,] 或 int[][]
interface I {...} 格式的用戶自定義類型
所有類型(包括值類型)的基類:object
Unicode 字符串:String,表示 UTF-16 代碼單元序列
class C {...} 格式的用戶自定義類型
類類型:(有點(diǎn)拗口,斷句為 類 類型)
接口類型:
數(shù)組類型:
委托類型:
struct 類型和 class 類型都可以包含數(shù)據(jù)成員和函數(shù)成員。區(qū)別在于 struct 是值類型,不需要堆分配。此外,struct 不支持用戶指定的繼承。(為什么并不直接說不支持繼承,而要說不支持用戶指定的既傳承呢?因?yàn)樗?struct 均隱式繼承 object 類,事實(shí)上所有類型都繼承自 object)
interface 可以繼承多個(gè)其他 interface。class 和 struct 也可以同時(shí)實(shí)現(xiàn)多個(gè) interface。
delegate 可以讓方法賦值給變量,甚至作為函數(shù)的參數(shù)。類同于函數(shù)式語言提供的函數(shù)類型。
class,struct,interface,delegate 全部支持泛型。
可以為 null 的類型無需定義。對于所有沒有 null 值的類型 T,都可以加一個(gè) ? 變成 T? 類型。這樣這個(gè)變量就可以為 null 了。
C# 采用統(tǒng)一的類型系統(tǒng),因此所有的類型包括值類型都可以看做 object 類型,所有類型都直接或間接派生自 object 類型。
可以通過裝箱,拆箱來把值類型當(dāng)引用類型來使用。
int i = 123;object o = i; // 裝箱int j = (int)o; // 拆箱
裝箱后,值會(huì)被復(fù)制到箱里。拆箱的時(shí)候會(huì)檢查這個(gè)箱里是不是有正確的值類型。
C# 的統(tǒng)一類型系統(tǒng)實(shí)際上意味著按需將值類型當(dāng)引用類型來引用。如果有庫使用 object,那么實(shí)際上無論是值類型還是引用類型都可以使用。
5. 程序結(jié)構(gòu)
C# 中的關(guān)鍵程序結(jié)構(gòu)包括 程序,命名空間,類型,成員,程序集。程序聲明類型,類型包含成員,并被整理進(jìn)命名空間。類型包括 類,接口,結(jié)構(gòu)。成員包括 字段,方法,屬性,事件。編譯完的 C# 實(shí)際會(huì)被打包進(jìn)程序集,視打包成應(yīng)用還是庫,程序集后綴可能為 exe 或 dll。
6. 字符串的用法
這里開始正式教程。
6.1 基本屬性
string 可以用 + 拼接。
字符串內(nèi)插。類似于其他語言的模板字符串。格式為?
$"hello {yourName}"
,其中 yourName 是變量。字符串有屬性 Length,可以得到字符串的長度。格式為?
yourName.Length
。
6.2 字符串操作方法
對字符串進(jìn)行一些修改操作。
Trim, TrimStart, TrimEnd 可以去掉頭尾的空格。
Replace 方法可以替換字符串。替換所有的。
ToUpper 和 ToLower 則是全部轉(zhuǎn)成大寫和小寫。
操作字符串方法返回的都是新的字符串。
6.3 搜索字符串
Contains 方法返回該字符串是否包含作為參數(shù)的子字符串。返回值是布爾值。
StartsWith 和 EndsWith 則返回字符串是否以子字符串開頭或結(jié)尾。
7. 數(shù)字
int.MaxValue 和 int.MinValue 分別表示 int 類型所能承載的最大值和最小值(也就是負(fù)最大值)。
int.MaxValue + 1 == int.MinValue,給最大值 + 1 變成了負(fù)最大值。
小數(shù)一般都用 double 而不用 float(最好的例子,編譯器遇到小數(shù)常數(shù),會(huì)假定為 double)。double 的范圍極大。高達(dá) 10 的 308 次方(也是一樣通過 double.MaxValue 來查看)。比 long 還長(而且長很多)。
但是 double 的精度仍然不夠。小數(shù)點(diǎn)后還是只能有 15 位。想要更多的小數(shù)點(diǎn)后位數(shù),就要使用 decimal 類型。decimal 類型的最大值最小值范圍比 double 低很多,但是精度更高。
decimal a = 1.0M;// 常數(shù)的時(shí)候,如果不加 M,編譯器會(huì)將其當(dāng)做 double 類型Console.WriteLine(1.0/3); // 0.333333333333333Console.WriteLine(1.0M/3); // 0.3333333333333333333333333333
8. 分支和循環(huán)
沒什么好說的。
9. 列表
因?yàn)橐f的東西挺多,因此從一串代碼開始看起:
var names = new List<string> {"<name>", "asshole", "joe"};foreach (var name in names) { ? ?Console.WriteLine($"Hello {name}"); }
輸出為:

List<T>
?類型,此類型儲存類型為 T 的一系列元素。foreach 提供了很方便的遍歷列表的方式。(當(dāng)然,這里 var 完全可以換成 string,當(dāng)你不確定類型的時(shí)候才使用 var)
9.1 修改列表
Add 添加元素。
Remove 刪除元素。
Count 屬性可以查看列表元素個(gè)數(shù)。(不是 Length)
實(shí)際上,列表長度有兩種方式。
9.2 Count 屬性 VS Count() 方法。
這兩這種方法表現(xiàn)是一致的。硬要說的的話,Count 由于不會(huì)進(jìn)行類型檢查,所以會(huì)比 Count() 快一些。
9.3 搜索和排序列表
IndexOf() 方法搜索元素所在的索引位置,如果元素不存在則返回 -1。
name.Sort() 會(huì)對列表進(jìn)行排序。(會(huì)改變原來的列表??!這個(gè)方法不返回值?。。?/p>
10. 如何顯示命令行參數(shù)
其實(shí)如果做游戲的話,這個(gè)其實(shí)不用學(xué)。但是反正教程就那么幾個(gè)字,看看也不虧。
命令行參數(shù)會(huì)作為一個(gè)數(shù)組被傳進(jìn) Main 方法。如下:
static void Main(string[] args) { }
就這樣,沒了。
11. 類簡介
作為一個(gè) OOP 語言,類必然是最重要的概念之一。
所以現(xiàn)在我們就建立一個(gè)類試試,來表示一個(gè)銀行賬號。
using System;namespace classes { ? ?public class BankAccount { ? ? ? ?public string Number {get;} ? ? ? ?public string Owner {get; set;} ? ? ? ?public decimal Balance {get;} ? ? ? ? ? ? ? ?public void MakeDeposit (decimal amount, DateTime date, string note) ? ? ? ?{ ? ? ? ? ?} ? ? ? ? ? ? ? ?public void MakeWithdrawal (decimal amount, DateTime date, string note) ? ? ? ?{ ? ? ? ?} ? ? ? ? ? ?} }
namespace 用于組織代碼,像是我們現(xiàn)在寫的小代碼,一個(gè) namespace 即可。
public 表示能否被其他文件引用。
BanckAccount 類里:
前三行是屬性,屬性可以定義一些驗(yàn)證和其他規(guī)則。get,set 表示讀和寫的權(quán)限,如果想讓屬性只讀可以不加 set。
后兩行是方法。
11.1 添加賬戶
我們給其加一個(gè)添加賬戶的功能。也就是實(shí)例化。那么這里要介紹 constructor 了。constructor 是一個(gè)和類名同名的成員。用于初始化實(shí)例(其實(shí) this 可以省略) :

constructor 和其他方法的區(qū)別,一個(gè)是名字和類名相同,還有一個(gè)就是沒有返回值,void 都不能寫,加了會(huì)被當(dāng)做方法,從而報(bào)錯(cuò),因?yàn)榉椒ú荒芎皖惷?/p>
試了一下效果:

11.1.1 賬號號碼(static 修飾符)
我們有一個(gè)屬性是 Number,這個(gè)屬性不應(yīng)該由用戶提供,而是代碼生成。
那么一個(gè)簡單地方法是先弄個(gè)初始的 10 位的數(shù)字,然后每添加一個(gè)賬戶就給它 + 1。
那么我們用這么個(gè)代碼來做到:

細(xì)節(jié)很多,來解釋:
private,因?yàn)橹挥羞@個(gè)類里會(huì)用到,只是作為一個(gè)初始值。
static,這樣這個(gè)數(shù)字就會(huì)被共享了。有沒有想過,如果沒有加 static,那么每次新賬號都是 1000000001,因?yàn)檫@個(gè)數(shù)會(huì)重新生成。但是如果加上 static,那么這個(gè)屬性就是和 class 綁定的,而不是和實(shí)例綁定的。換句話說就是 static 修飾的屬性可以讓我們在實(shí)例之間共享數(shù)據(jù)。
然后把構(gòu)造函數(shù)改成這樣既可(其實(shí) this 可以省略,但為了區(qū)分 static 和別的區(qū)別還是加上):

由于現(xiàn)在在類內(nèi),所以直接調(diào)用 initialAccountNumber 即可,如果是在外面的話,就要用 BankAccount.initialAccountNumber。當(dāng)然了,由于我們是 private,就算在外面也調(diào)用不了就是了。
測試一下效果:

11.2 創(chuàng)建存款和取款
單純地改變 Balance 沒意思,也不合理。我們先創(chuàng)建一個(gè)類叫做 transaction。用這個(gè)來記錄 Balancce 的變化。

然后我們創(chuàng)建一個(gè)屬性用于存儲每個(gè)賬號所有的歷史 transaction。

然后我們修改 Balance 屬性。讓其等于所有 transaction 計(jì)算后的結(jié)果。

然后終于開始寫方法,這個(gè)時(shí)候會(huì)引入 exception:

然后我們需要對 constructor 進(jìn)行修改,因?yàn)槌跏蓟?balance 也需要改變了(此時(shí) Balance 可以去掉 set 變成只讀了):

Date.Now 獲取當(dāng)前時(shí)間,注意是屬性不是方法。
11.3 測試

使用 try catch 可以捕獲錯(cuò)誤:

看起來差不多,但是沒有說未捕獲的異常了。而且這個(gè)錯(cuò)誤是我自己主動(dòng)打印出來的。
11.4 log 所有 transaction
看代碼:

StringBuilder 類型的 AppendLine 方法會(huì)自動(dòng)在每一行后面加空格。然后我們再用制表符調(diào)整縮進(jìn)。 測試效果如下:

12. 繼承和多態(tài)
OOP 有四個(gè)重要的特性。Abstraction,Encapsulation,Inheritance, Polymorphism。前面兩個(gè)在上一節(jié)已經(jīng)講過了,這一節(jié)講后兩個(gè)。
依舊拿剛才的銀行賬號類來講解。
12.1 繼承
我們要新建 3 種銀行賬號類型。儲蓄卡,信用卡和禮品卡。首先我們先將它們創(chuàng)建,并繼承之前的類:
每一個(gè)新的類都已經(jīng)有和 BankAccount 相同的屬性和方法了。
可以看到報(bào)錯(cuò)了,因?yàn)槲覀冃枰袠?gòu)造函數(shù)。而且這個(gè)構(gòu)造函數(shù)不止要初始化現(xiàn)在這個(gè)類,還要初始化基類。
可以看下面的代碼:
構(gòu)造函數(shù)里也要和繼承一樣的格式,但是繼承的類直接用 base 代替,而不是寫基類的名字。
base 里參數(shù)不寫類型,因?yàn)檫@是在調(diào)用而不是在聲明。
原理就是,這個(gè)構(gòu)造函數(shù)會(huì)調(diào)用基類的構(gòu)造函數(shù),傳參則直接從當(dāng)前構(gòu)造函數(shù)里拿。
有些時(shí)候基類可能有多個(gè)構(gòu)造函數(shù),這個(gè)語法可以選擇用哪種構(gòu)造函數(shù)。
12.2 多態(tài)
假設(shè)每一張卡都會(huì)在月底做一些事情,但是每張卡做的事情不一樣。那么我們使用多態(tài)來完成這件事。
首先我們?nèi)サ紹ankAccount 類,加上以下 virtual 方法:
virtual 關(guān)鍵字表示這個(gè)方法之后可能會(huì)被繼承類重新實(shí)現(xiàn)。既然是可能,也就是說你也可以在基類里寫一些代碼,然后不覆蓋。
在繼承類里使用 override 關(guān)鍵字來覆蓋掉方法。
也可以把 virtual 換成 abstract,來讓繼承類強(qiáng)制覆蓋這個(gè)方法。(不過那樣的話基類本身也得是 abstract 類型,具體之后再講吧)
繼承類里也可以調(diào)用基類的方法:
不過我們現(xiàn)在不需要用,前兩張卡實(shí)現(xiàn)代碼如下:
對于禮品卡,規(guī)則比較多,因此需要更改一些別的,比如我們先修改構(gòu)造函數(shù):
這里因?yàn)槲覀冎挥幸痪洌虼耸∪チ舜罄ㄌ?,使用?=> 也就是 lambda 運(yùn)算符。
然后我們再改寫 MonthlyTask 方法:
測試一下 禮品卡 和 儲蓄卡:
但是當(dāng)測試信用卡的時(shí)候,出問題了:
這很 resonable,因?yàn)?BankAccount 限制不能取走比 Banlance 更多的錢。于是我們需要修改 BankAccount 了:
readonly 屬性和只給一個(gè) get 的屬性的區(qū)別在于。前者只能在構(gòu)造函數(shù)階段賦值,一旦初始化之后就無法更改了。而后者則可以在 class 內(nèi)部進(jìn)行更改。不過對外都是作為只讀屬性來看。
這里我們有兩個(gè)構(gòu)造函數(shù),第一個(gè)構(gòu)造函數(shù)接受 3 個(gè)參數(shù);第二個(gè)接受兩個(gè)參數(shù),同時(shí)使用?
:this()
?語法來調(diào)用上一個(gè)構(gòu)造函數(shù)。然后這里我們只需要調(diào)用第一個(gè)構(gòu)造函數(shù)(以第三個(gè)參數(shù)為 0 的形式),不需要做別的事,所以就用?:this() {}
?了。
然后我們修改 MakeWithdrawal:
然后我們在 CreditCardAccount 里可以修改成這樣:
12.2.1 protocted
現(xiàn)在想讓信用卡能夠超過 creditLimit,可以用 virtual 方法。在 BankAccount 里,更改 MakeWithdrawal 方法:
public void MakeWithdrawal(decimal amount, DateTime date, string note){ ? ?if (amount <= 0) ? ?{ ? ? ? ?throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive"); ? ?} ? ?var overdraftTransaction = CheckWithdrawalLimit(Balance - amount < minimumBalance); ? ?var withdrawal = new Transaction(-amount, date, note); ? ?allTransactions.Add(withdrawal); ? ?if (overdraftTransaction != null) ? ? ? ?allTransactions.Add(overdraftTransaction); }protected virtual Transaction? CheckWithdrawalLimit(bool isOverdrawn) { ? ?if (isOverdrawn) ? ?{ ? ? ? ?throw new InvalidOperationException("Not sufficient funds for this withdrawal"); ? ?} ? ?else ? ?{ ? ? ? ?return default; ? ?} }
我寫在這里是因?yàn)槲倚陆ǖ捻?xiàng)目用的是 C# 7.3,而只有 C# 8.0 才有"可以為null的引用類型"的支持。
這里除了修改 MakeWithdrawal 方法以外,還添加了一個(gè)新的方法。這個(gè)方法前的修飾符是 protected 和 virtual。
protected 和 public,private 是一類的,代表著這個(gè)方法只能被繼承類調(diào)用。
virtual 代表著這個(gè)方法可以被繼承類覆蓋。
加在一起就是只能被繼承類調(diào)用,同時(shí)可以被覆蓋。
于是我們在 CreditCardAccount 里覆蓋它:
public override Transaction? CheckWithdrawalLimit(bool isOverdrawn) ? ?=> isOverdrawn ? ?? new Transaction(-20, DateTime.Now, "Apply overdraft fee") ? ?: default; ? ?
通過這樣的方式,把代碼分離出成一個(gè) virtual 方法,然后 override,就可以做出不同的行為了。