一文帶你吃透C#中面向對象的相關知識
基礎必讀: 超快速成,零基礎快速掌握C#開發中最重要的概念
switch和字典
前文提到過,有個遊戲裡面有個著名的屎山,就是跑瞭19億次if,把玩傢憋得不行。而解決這個問題其實非常簡單,隻需用到switch就可以瞭。
比如打牌的時候,正常隻有2-10是數字,1是A,11是J,12是Q,13是K,如果要用if…else if這種方法來判斷,那麼遇到K的時候需要判斷好多次才行,switch則隻需一次
void cardName(int cardNum) { switch (cardNum) { case 1: Console.WriteLine("A"); break; case 11: Console.WriteLine("J"); break; case 12: Console.WriteLine("Q"); break; case 13: Console.WriteLine("K"); break; default: Console.WriteLine(cardNum); break; } }
C#中的switch語句,除瞭用break可以跳出switch之外,還可以用goto case xx來跳轉到第xx個case,這個特性還挺有意思的,所以在這裡多提一嘴,但初學者其實隻要有switch case這個概念就可以瞭。
switch case語句之所以在性能上優於if…else,極有可能是用瞭哈希表,通過計算輸入的方法,來快速鏈接到執行程序的入口,達到常數級別的時間復雜度。
在C#中,提供瞭字典這種數據結構,可以實現類似於switch case的效果。所謂字典,就是一組鍵值對,通過鍵值的一一對應關系,達到通過鍵來索引值的目的,其定義方式如下
Dictionary<int, string> card = new Dictionary<int, string> { {1,"A" }, {11, "J" }, {12, "Q" }, {13, "K" } };
Dictionary為數據類型的名字,<int, string>表示其鍵為整型,值為字符串。後面的new表示創建新對象,這個在數組的時候就已經學過瞭,最後花括號中的四行代碼,用於對Dictionary進行初始化。
有瞭這個,就可以更簡潔地實現抽牌功能
Console.WriteLine(card[1]); //命令行中顯示 A
那麼這個時候可能有人問瞭,那2-10這9張牌咋辦?是需要加一個if來判斷嗎?
答案是當然不用,畢竟字典是一種動態的數據結構,內部元素是可以增長的,隻需跑個循環將其填充上就行瞭
for (int i = 2; i < 11; i++) { card.Add(i, i.ToString()); }
其中,card.Add就是添加元素的方法,i.ToString()可以將整型的i轉化為字符串。
這樣,就有瞭從A到K的撲克牌。
類、成員、方法
撲克牌除瞭面值之外,還要看花色的。換言之,用數字是沒法完全描述撲克牌的所有屬性的。
當然,這種屬性其實可以用字典來實現,例如現在有一個紅桃K,可以表示為
Dictionary<string, string> 紅桃K = new Dictionary<string, string> { {"花色", "紅桃" }, {"數值", "13" }, {"名字", "K" } };
首先,看到紅桃K千萬不要害怕,在C#中,中文也是可以當作變量名的。
其次,字典畢竟不太方便,因為C#中的字典要求指定數據類型,數值這個鍵對應的值,按理說應該是整型才比較合理,但無奈之下,隻能是字符串。
面對這種痛點,就得看Class來大顯身手瞭。
class Card { public int Id { get; set; } public string Name { get; set; } public string Color { get; set; } }
其中,class表示,Card是一個類,後面的花括號裡就是這個類的成員變量。
public表示,這是個公開的屬性,可以被別人調用;{get; set;}表示這個屬性既可以被調用,也可以被賦值。
需要註意的是,目前我們寫下的所有代碼,都是在.NET6中所定義的頂級語句。這種頂級語句,並不符合以往C#代碼的規范,由此也會導致一些問題,即頂級語句必須寫在所有類定義的前面。
所以,如果想創建一個Card類的實例,需要在class Card之前調用,這個很反直覺,但習慣瞭就好。
Card 紅桃A = new Card { Id = 1, Name = "A", Color = "紅桃" };
接下來遇到瞭一個很尷尬的問題,的確是新建瞭一個紅桃A,然後呢?
首先,可以通過.來調用類的屬性,例如
Console.WriteLine(紅桃A.Name);
其次,可以在類中添加成員方法,然後調用一下
class Card { public int Id { get; set; } public string Name { get; set; } public string Color { get; set; } public void introduce() { Console.WriteLine($"我的名字是{Color}{Name}"); } }
調用仍然要在class Card這行的前面,
紅桃A.introduce();
得到的結果為
我的名字是紅桃A
是時候規范一下寫法瞭
頂級語句用起來雖然很爽,但,至少在目前看來,最適合的應用場景是算法原理的快速驗證,而非做開發。因為開發要涉及到團隊合作,涉及到大傢按照相同的規范去分塊做不同的內容,為瞭團隊和整體的效率,不得不犧牲局部代碼的簡潔性,所以作為C#程序員,還是要習慣那種類似Java風格的完全的面向對象寫法。
重新建一個控制臺應用,這次在選擇框架的界面,勾選上不使用頂級語句,這回看到的就是這樣的一個結果
namespace MySecondCS { internal class Program { static void Main(string[] args) { Console.WriteLine("Hello, World!"); } } }
就是前面提到的,Hello World外面有一層Main函數,Main函數外面有一個class,class外面有一個命名空間。
然後把之前寫過的Card類寫在Program前面,並將Main函數中的內容改為
static void Main(string[] args) { Card card = new Card() { Id = 1, Name = "A", Color = "紅桃" }; card.introduce(); }
這樣啟動命令行,會得到上一節同樣的結果。
將類寫在一個文件中當然沒什麼問題,但隨著所開發的應用越來越復雜,涉及到的類也會越來越多,全部堆在一個文件中,缺乏有效的組織,顯然是不成的。
正所謂晴天帶傘、居安思危,全堆一個文件不行,那就把類寫在另一個文件中就是瞭。右鍵解決方案中的項目名,選擇添加類,如下圖
類名取為Card,然後項目中除瞭Program.cs之外,還會出現一個Card.cs,其內容為
namespace MySecondCS { internal class Card { } }
其中,namespace為命名空間,在同一個命名空間中的類可以互相調用。internal是一個用於修飾類的關鍵字,是對可訪問性的一種限制,這個限制並不強,隻要在一個程序集中,就可以訪問。
這種訪問限制,在前面第一次創建Card的時候就有提過,Card類中,修飾成員變量用到的public也是用於訪問限制的。
接下來把之前寫好的Card代碼剪切進internal class Card中,在啟動命令行,程序仍然是可以跑通的。
繼承
之所以要有繼承這個概念,是因為紙牌的玩法太多瞭,比如我小的時候就喜歡搜集小當傢的水滸卡,梁山好漢108將剛好是兩幅撲克牌,但是Card類中並沒有額外給梁山好漢提供位置。
這個時候就會遇到兩難問題,若直接把Card改成小當傢水滸卡,那麼打牌的人會覺得這玩意沒啥用,隻會白白地浪費內存;若是另起爐灶重新寫一個類,那老板會覺得你同樣的內容寫兩遍,是不是欺負我不懂技術?然後說不定就扣工資瞭。
所以,繼承就比較好地解決瞭這個問題,就像這個名字暗示的,在C#中,可以新建一個水滸卡的類,可以在繼承Card類中的各種成員之外,添加自己獨有的成員。
接下來在Card類的下面,新建一個類,就叫UniCard,表示統一小當傢水滸卡,如下所示
class UniCard : Card { public int Order { get; set; } public string PersonName { get; set; } }
其中,UniCard : Card就表示,前者是對後者的繼承,所有在Card中public的功能,都可以在UniCard中無痛調用。
接下來在UniCard中實現一個功能,即根據排名確定其對應的紙牌面額。紙牌大小排序是大小王,然後是AkQJ,再然後是10到2。
那麼2副撲克牌中,有4個大小王,其他諸如AKQJ之類的都有8張。現令大小王是0,A是1,那麼其對應的排序就是0->1->13->12->…->2。
也就是說,排名1, 2, 3, 4對應撲克牌中的大小王,面額為0;5-12對應撲克牌中的A,面額為1,然後接下來,每新增八位,其面額就加1。其函數實現為
int order2Id(uint order){ if(order <= 4) return 0; else if (order <= 12) return 1; else return 14 - (order-4) / 8; }
這個函數是非常簡單的,但接下來要將其嵌入到UniCard類中,實現通過Order自動生成Id這樣的功能
class UniCard : Card { public int Order { get; set; } public string PersonName { get; set; } public void getIdName() { if (this.Order <= 4) this.Id = 0; else if (this.Order <= 12) this.Id = 1; else this.Id = 14 - (this.Order - 4) / 8; switch (this.Id) { case 0: this.Name = "Joker"; break; case 1:this.Name = "A"; break; case 13:this.Name = "K"; break; case 12:this.Name = "Q";break; case 11:this.Name = "J"; break; default: this.Name = this.Id.ToString(); break; } } }
其中,this表示當前的這個class,在不引起歧義的情況下是可以省略的。所謂引起歧義,就是假如這個class外面已經有瞭一個Name,那麼在這個class裡面如果非常突兀地來一個Name=1,可能會導致程序不知道這個Name到底指向誰。
另外,如果if後面跟著的程序塊中隻有一行代碼,那麼花括號可以省略。
除此之外,上面的代碼稍微長瞭一點,但並沒有新的知識點,隻是相當於復習瞭一下switch case。
接下來,在Main函數中創建一個UniCard,並調用其繼承的自我介紹的函數。
static void Main(string[] args) { UniCard uniCard = new UniCard(); uniCard.Order = 6; uniCard.Color = "紅桃"; uniCard.getIdName(); uniCard.introduce(); }
運行之後,命令行輸出我的名字是紅桃A。
6號如果我沒記錯的話,是豹子頭林沖,結果現在變成瞭紅桃A,看來這個繼承還是比較成功的。
枚舉
撲克牌的花色隻有四種,紅桃、黑桃、草花、方片,如果把數據類型限制為字符串,保不準有人會把牌的花色定義為“五彩斑斕黑”之類的,為瞭做一個限制,目前想到比較好的方案是用字典
Dictionary<int, String> CARD_COLOR = new Dictionary<int, string> { {0, "黑桃" }, {1, "紅桃" }, {2, "草花" }, {3, "方片" } };
然後再把花色定義為整型,想要看花色的時候以CARD_COLOR[0]這種形式調用。
這樣一來思路就打開瞭,甚至可以將花色封裝成字符串數組
String[] CARD_COLOR = new string[] { "黑桃", "紅桃", "草花", "方片" };
然而在C#中,其實有更加優雅的解決方案,這個方案就是枚舉
public enum COLOR { 黑桃, 紅桃, 草花, 方片};
上面這行代碼可以寫在internal class Card的外面,然後在Card類中可以把花色定義為
public COLOR Color { get; set; }
枚舉這種數據類型的好處是,既有字符串的特點,又有整型的特點,以COLOR這種類型為例,黑桃對應的是0,紅桃對應的是1,以此類推。
這樣一來,getIdName這個函數,除瞭可以通過排名來算牌的面額,還可以據此計算牌的花色。
public void getIdName() { //... //寫在switch case後面 if (this.Order <= 2) this.Color = COLOR.黑桃; else if (this.Order <= 4) this.Color = COLOR.紅桃; else this.Color = (COLOR)((this.Order - 5) % 8 / 2); }
其中COLOR.黑桃是常用的枚舉類型的調用方法,而後面的(COLOR)相當於把其後面的(this.Order – 5) % 8 / 2這個整數,強制轉化為枚舉類型。
改完這些之後,就會發現Main函數中的uniCard.Color = "紅桃";出現瞭紅色的下劃波浪線,說明出現瞭錯誤。原因也很簡單,現在的Color是枚舉類型,並不能賦值一個字符串。
將這行刪掉之後,再運行程序,命令行輸出為
我的名字是黑桃A
說明introduce中的$"我的名字是{Color}{Name}"仍然發生瞭作用,枚舉類型,通過6 66這個數值,計算得到瞭COLOR.黑桃這個結果,最後又通過$字符串轉化成瞭字符串。
這就是前文所言,枚舉類型,既有整型,又有字符串。
構造函數和方法重載
現在回顧一下Main函數,發現UniCard的創建過程未免太過繁瑣。
static void Main(string[] args) { UniCard uniCard = new UniCard(); uniCard.Order = 6; uniCard.getIdName(); uniCard.introduce(); }
Main函數中的4行代碼中,如果隻保留第一行和最後一行,那就完美瞭,比如寫成這種
UniCard uniCard = UniCard(6); uniCard.introduce();
在這個過程中,UniCard變成瞭一個函數,通過輸入一個排名,便可以初始化花色、牌額等內容,這個函數就叫做構造函數,想要實現,隻需在UniCard中添加
public UniCard(int order) { Order = order; getIdName(); }
需要註意,在Order=order中,前面的Order為類成員,其實可以寫為this.Order,getIdName也可以寫為this.getIdName,由於不會引起歧義,所以將this省略瞭。
這時會發現,Main函數中又出現瞭錯誤:
UniCard uniCard = new UniCard();
這時因為,我們已經為UniCard設定瞭唯一的構造函數,這個構造函數必須要輸入一個整型才能執行,UniCard()的參數卻空空如也,這不報錯才怪,解決方法也很簡單,隻需將其改為我們喜聞樂見的形式就行瞭
static void Main(string[] args) { UniCard uniCard = new UniCard(6); uniCard.introduce(); }
這個時候有人說瞭,那我就想生成一個啥也沒有的UniCard,你這麼改來改去把我想要的改沒瞭,你還我UniCard()。
這個需求也是可以滿足的,這就是所謂的重載。所謂重載,就是在C#中,允許創建一些同名函數,這些同名函數可以有著不同的輸入參數,所以隻需在public UniCard(int order)前面或者後面添加下面的代碼,就可以既滿足帶參數的構造函數,又滿足不帶參數的構造函數瞭。
public UniCard(){}
運算符重載
無論是是打牌,還是梁山好漢,都是有排名的。有排名就可以比大小,比大小,就涉及到瞭大於號等於號小於號之類的東西。
正如字符串可以把加號更改為拼接的意思,Card也應該有重新定義運算符的能力,這種能力就叫做運算符重載。
對於已經建立起函數重載這種概念的人來說,運算符重載並不存在理解上的困難,畢竟運算符也是一種函數。
下面針對UniCard這種數據類型,對比較運算符進行重載,
public static bool operator< (UniCard a, UniCard b){ return a.Order > b.Order; } public static bool operator>(UniCard a, UniCard b){ return a.Order < b.Order; } public static bool operator==(UniCard a, UniCard b){ return a.Order == b.Order; } public static bool operator!=(UniCard a, UniCard b){ return a.Order != b.Order; }
非常直觀,其中static為靜態類的修飾符,所謂靜態類型,表示在類尚未實例化時就可以調用,所有運算符重載函數都必須是static的。
operator<表示重新定義運算符<,bool類型標識作為運算結果的數據類型。
在這種排名中,肯定是數越小的人地位越高,所以排名第一大於排名第5,從而其Order大的反而小。
在進行瞭這些運算符重載之後,就可以在Main函數中進行調用瞭
static void Main(string[] args) { UniCard a = new UniCard(15); UniCard b = new UniCard(25); if (a > b) Console.WriteLine($"{a.Color}{a.Name} > {b.Color}{b.Name}"); else Console.WriteLine($"{a.Color}{a.Name} <= {b.Color}{b.Name}"); }
運行之後,命令行輸出為
紅桃K > 草花Q
以上就是一文帶你吃透C#中面向對象的相關知識的詳細內容,更多關於C#面向對象的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- C#9特性record 類型、模式匹配、init 屬性詳情
- C#實現簡單學生成績管理系統
- .Net中的弱引用字典WeakDictionary和ConditionalWeakTable介紹
- C#實現簡單訂單管理程序
- C#實現簡單的飛行棋小遊戲