從零開始獨立游戲開發(fā)學習筆記(十六)--Unity學習筆記(五)--微軟C#指南(二)

0. 為什么這次更新這么久
上周六,周日,周一:電腦掉在出租車上,而我已經(jīng)坐上回家的火車。
并且,上周六到這周三:一個非常非常巨大的變故。
最后,這周四到昨天發(fā)高燒, 38.8 度。
不過既然你們看到這一篇,說明之后會慢慢恢復更新速度了。我盡量不讓變故影響到自己。
1. 繼承
繼承本身是個什么就不說了,實在是 OOP 太常用的概念了。 C# 只允許繼承一個基類(base class)。不過具有傳遞性(transitive),B 繼承 A,C 繼承 B 這樣。
1.1 不會繼承下去的成員
以 B 繼承 A 為例。A 中的一些成員不會被 B 繼承。這些成員有:
靜態(tài)構(gòu)造函數(shù)。用于對靜態(tài)數(shù)據(jù)進行初始化。
實例構(gòu)造函數(shù)。也就是我們常說的構(gòu)造函數(shù)。(雖然不繼承,但可以調(diào)用)
終結(jié)器。和垃圾回收相關。
1.2 類成員的可訪問性
這里我們只討論繼承的情況。
1.2.1 private 成員
即使是繼承類,也無法訪問基類的 private 成員。
但是也有例外,私有變量可以被嵌套繼承類所訪問。如下面代碼中的 B 就可以訪問,但 C 不行。
public class A{ ? ?private int value = 3; ? ?public class B : A
? ?{ ? ? ? ?public int GetValue()
? ? ? ?{ ? ? ? ? ? ?return this.value;
? ? ? ?}
? ?}
}public class C : A{
}class Program{ ? ?static void Main(string[] args)
? ?{
? ? ? ?A.B b = new A.B(); // 因為 B 在 A 里面,因此可以通過方法來訪問。
? ? ? ?C c = new C(); // 雖然 C 繼承自 A,但無法訪問 value
? ? ? ?Console.WriteLine(b.GetValue()); // 可以獲取 value
? ? ? ?
? ?}
}
即使把 getValue 方法放到 C 里也無法訪問。
話說我突然安意識到 C# 語法一直讓我難以區(qū)分的一點,就是函數(shù)根本沒有專有關鍵詞,不存在什么 def,func 之類的東西。直接就是 int GetValue() ,前面加一個返回值就是函數(shù)了,把 int 換成 class 就是類。把括號去掉就變成變量。
1.2.2 protected 成員
相比 private,protected 可以被繼承的類所訪問了。不過實例代碼所在的地方也會影響能否使用,如夏例子:

可以發(fā)現(xiàn)并不能訪問 Method1(),因為實例化的地方在繼承類之外,所以得通過注釋里的代碼間接訪問:

1.2.3 internal 成員
像是上面的問題,在 internal 和 public 里就不會出現(xiàn),可以直接通過 b.Method1() 訪問。
可以被同一程序集中的繼承類訪問。在不同程序集里,繼承類照樣無法訪問該成員。
1.2.4 public 成員
public 可以被繼承的類訪問。
1.3 override 和 virtual
如果想要在繼承類里改寫基類的某個成員,就需要在基類里給該成員加上 virtual 關鍵字,并給繼承類加上 override 關鍵字。
如果沒加上 virtual 關鍵字,override 會報錯。 只加上 virtual,不加 overrride 則沒有問題。因為只是給繼承類可以覆蓋的可能。
1.4 abstract
如果我們想要讓繼承類必需覆蓋基類成員的話,則要用到 abstract 關鍵字。
abstract 比較特殊:
類和成員必需同時加上 abstract 關鍵字才行。不然會報錯。
成員不能有 implementation。這個很合理,既然是必定會被覆蓋的,那么寫 implementation 也沒有意義。
abstract 類并不代表方法也都要是 abstract 的。完全可以在 abstract 類里寫 virtual 方法或者不能被覆蓋的普通方法。abstract 加在 class 前面表示這個類是個抽象類不能被實例化,但可以在繼承類里訪問這些方法。

1.5 只有類和接口有繼承的概念。
struct,enum,delegate 等是沒有繼承的概念的。
1.6 隱性繼承
實際上,所有的類型都直接或間接繼承自 Object。Object 的所有特性,所有類型都可以使用。
比如我們先定義一個空類:
public class SimpleClass { }
然后我們通過反射(以后再講)查看這個類的成員可以看到其包含 9 個成員,其中 1 個是默認構(gòu)造函數(shù),另外 8 個則是從 Object 里繼承而來:

ToString 返回字符串表現(xiàn)形式,此例中返回類名:"SimpleClass"
接下來 3 個方法都是為了測試兩個對象是否相等。一般這些方法測試的是兩個變量是否引用相等。也就是說被比較的變量們必須指向同一個對象。
GetHashCode 方法,會計算出一個值,用于 hashed collection
GetType 方法,獲取一個 Type Object,本例中為 SimpleClass 類型。和 ToString 不一樣,這個返回的不是字符串,只不過用 Console.WriteLine 打印的時候會變成字符串罷了。
Finalize 方法,用于垃圾回收。
MemberwiseClone,會創(chuàng)建一個當前對象的淺克隆。

可以在類定義里 overrride 掉 ToString 方法來改寫返回值。
2. 設計基類和繼承類
由于 oop 的靈活性,以及 C# 提供了這么多關鍵字。導致設計成為了一個比較重要的一環(huán)。 比如現(xiàn)在就舉一個例子,我們有一個 Publication 的基類,然后衍生出 Book,然后 Magazine 等。
2.1 設計思路
2.1.1 綜觀
我們要設計的地方有很多,比如說這個基類應該包含哪些成員,然后一些方法成員是否提供 implementation,以及這個基類是否應該作為一個 abstract 基類。
非 abstract 方法的一個好處是可以復用代碼。避免在多個繼承類里寫同樣的代碼,也可以避免很多 bugs 的產(chǎn)生。
2.1.2 繼承關系層數(shù)
oop 設計是很靈活的。比如說我們的例子。雖然我們確定了基類就是 Publication,但是之后我們既可以直接從 Publication 中衍生出 Magazine,也可以先衍生出 Periodical,再衍生下去。
我們的例子中,我們是 Publication->Book->Magazine 這種。
2.1.3 實例化是否 make sense
如果不 make sense,那么直接換成 abstract 類即可。
如果 make sense,那么就用構(gòu)造函數(shù)來實例化。當然你會發(fā)現(xiàn)即使你不寫構(gòu)造函數(shù)也不會報錯也可以實例化。那是因為編譯器幫你生成了一個無參數(shù)構(gòu)造函數(shù)(上一節(jié)已經(jīng)講過 .ctor 那個)
在我們的例子中,由于 Publication 實例化不 make sense,因此我們將其設為 abstract class。但是不包含 abstract method。?像這種無 abstract 方法的 abstract 類,一般是一個抽象概念,這個概念被一些具體類(后面的 Book 等)共享。
2.1.4 繼承類里是否有部分成員需要覆蓋基類方法
如果有的話,得用 virtual 和 override 配合。
2.1.5 某個繼承類是否為層級的最后一級
任何繼承類都可以作為其他類的基類。不過必要的時候,也可以加上 sealed 關鍵字表示該類為最后一層,無法被作為基類了。
2.2 例子-Publication
直接給代碼:
using System;public enum PublicationType { Misc, Book, Magazine, Article };public abstract class Publication{ ? private bool published = false; ? private DateTime datePublished; ? private int totalPages; ? public Publication(string title, string publisher, PublicationType type)
? { ? ? ?if (String.IsNullOrWhiteSpace(publisher)) ? ? ? ? throw new ArgumentException("The publisher is required.");
? ? ?Publisher = publisher; ? ? ?if (String.IsNullOrWhiteSpace(title)) ? ? ? ? throw new ArgumentException("The title is required.");
? ? ?Title = title;
? ? ?Type = type;
? } ? public string Publisher { get; } ? public string Title { get; } ? public PublicationType Type { get; } ? public string CopyrightName { get; private set; } ? public int CopyrightDate { get; private set; } ? public int Pages
? { ? ? get { return totalPages; } ? ? set
? ? { ? ? ? ? if (value <= 0) ? ? ? ? ? ?throw new ArgumentOutOfRangeException("The number of pages cannot be zero or negative.");
? ? ? ? totalPages = value;
? ? }
? } ? public string GetPublicationDate()
? { ? ? ?if (!published) ? ? ? ? return "NYP"; ? ? ?else
? ? ? ? return datePublished.ToString("d");
? } ? public void Publish(DateTime datePublished)
? {
? ? ?published = true; ? ? ?this.datePublished = datePublished;
? } ? public void Copyright(string copyrightName, int copyrightDate)
? { ? ? ?if (String.IsNullOrWhiteSpace(copyrightName)) ? ? ? ? throw new ArgumentException("The name of the copyright holder is required.");
? ? ?CopyrightName = copyrightName; ? ? ?int currentYear = DateTime.Now.Year; ? ? ?if (copyrightDate < currentYear - 10 || copyrightDate > currentYear + 2) ? ? ? ? throw new ArgumentOutOfRangeException($"The copyright year must be between {currentYear - 10} and {currentYear + 1}");
? ? ?CopyrightDate = copyrightDate;
? } ? public override string ToString() => Title;
}
明明是 abstract,為什么還有構(gòu)造函數(shù)。當然可以有。只是無法用這個構(gòu)造函數(shù)來實例化 Publication 實例罷了。但是可以在繼承類里使用這個構(gòu)造函數(shù)。這個在上一篇文章里也提到過。
2.3 例子 Book
using System;public sealed class Book : Publication{ ? public Book(string title, string author, string publisher) : ? ? ? ? ?this(title, String.Empty, author, publisher)
? { } ? public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
? { ? ? ?// isbn argument must be a 10- or 13-character numeric string without "-" characters.
? ? ?// We could also determine whether the ISBN is valid by comparing its checksum digit
? ? ?// with a computed checksum.
? ? ?//
? ? ?if (! String.IsNullOrEmpty(isbn)) { ? ? ? ?// Determine if ISBN length is correct.
? ? ? ?if (! (isbn.Length == 10 | isbn.Length == 13)) ? ? ? ? ? ?throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string."); ? ? ? ?ulong nISBN = 0; ? ? ? ?if (! UInt64.TryParse(isbn, out nISBN)) ? ? ? ? ? ?throw new ArgumentException("The ISBN can consist of numeric characters only.");
? ? ?}
? ? ?ISBN = isbn;
? ? ?Author = author;
? } ? public string ISBN { get; } ? public string Author { get; } ? public Decimal Price { get; private set; } ? // A three-digit ISO currency symbol.
? public string Currency { get; private set; } ? // Returns the old price, and sets a new price.
? public Decimal SetPrice(Decimal price, string currency)
? { ? ? ? if (price < 0) ? ? ? ? ?throw new ArgumentOutOfRangeException("The price cannot be negative.");
? ? ? Decimal oldValue = Price;
? ? ? Price = price; ? ? ? if (currency.Length != 3) ? ? ? ? ?throw new ArgumentException("The ISO currency symbol is a 3-character string.");
? ? ? Currency = currency; ? ? ? return oldValue;
? } ? public override bool Equals(object obj)
? {
? ? ?Book book = obj as Book; ? ? ?if (book == null) ? ? ? ? return false; ? ? ?else
? ? ? ? return ISBN == book.ISBN;
? } ? public override int GetHashCode() => ISBN.GetHashCode(); ? public override string ToString() => $"{(String.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
}
兩個構(gòu)造函數(shù):當參數(shù)數(shù)量不同的時候,使用不同的構(gòu)造函數(shù)??梢钥吹降谝粋€構(gòu)造函數(shù)使用 :this 來調(diào)用第二個構(gòu)造函數(shù),第二個構(gòu)造函數(shù)則是調(diào)用基類的構(gòu)造函數(shù)。少參數(shù)的構(gòu)造函數(shù)會去調(diào)用多參數(shù)的構(gòu)造函數(shù)并提供默認值,這種方式叫做構(gòu)造函數(shù)鏈。
不僅改寫了 ToString,甚至還改寫了 Equals。因為如果沒有被 overrriden,Equal 測試的只是引用相等。改寫 Equals 的同時應該改寫 GetHashCode。GetHashCode 應該和 Equals 保持一致,本例因為比較的是 ISBN 號,因此 GetHashCode 也用 ISBN 的該方法。