五月天青色头像情侣网名,国产亚洲av片在线观看18女人,黑人巨茎大战俄罗斯美女,扒下她的小内裤打屁股

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊

第 38 講:面向?qū)ο缶幊蹋ㄊ簩?duì)象的多態(tài)

2021-05-16 08:06 作者:SunnieShine  | 我要投稿

前面,我們介紹了相當(dāng)多的繼承有關(guān)的有趣語法,比如繼承語法,比如抽象類(abstract class)和密封類(sealed class),有抽象成員、重寫成員(override 修飾的成員)和密封的重寫成員(sealed override 修飾的成員)等等。

下面我們繼續(xù)介紹面向?qū)ο蟮睦^承機(jī)制的一種特殊現(xiàn)象:多態(tài)。多態(tài)這個(gè)詞語是直接翻譯的 Polymorphism 這個(gè)單詞,因?yàn)檫@個(gè)詞語不屬于基本單詞,所以很少普通人知道這個(gè)詞語。對(duì)于我們初學(xué)面向?qū)ο蟮呐笥褌兌?,也不是很好理解。多態(tài)我們理解成“多種狀態(tài)”。

Part 1 基類型的實(shí)例

我們還是使用前面的 Shape 的例子。Shape 類型因?yàn)楸欢x為抽象類,因此無法使用實(shí)例化的 new 語句??墒?,我們這么寫代碼:

請(qǐng)看左右兩側(cè)的東西。左邊的 s 變量用的是 Shape 類型,而右側(cè)的實(shí)例化卻是用的 Circle 類型的構(gòu)造器。這個(gè)語法是正確的。為什么呢?Circle 表示“一個(gè)圓”,而 Shape 表示“一個(gè)形狀”,而圓就是一種形狀,所以為什么不行呢?

當(dāng)然,這是從邏輯上說的,所以道理應(yīng)該都明白。現(xiàn)在我們從代碼上這么考慮一下。

假設(shè),我們把 Shape 看成一個(gè)箱子。這個(gè)箱子里包含了你想要的東西(成員);當(dāng)你從 Shape 類型派生后,Circle 類型又是一個(gè)新的箱子。不過這個(gè)新的箱子顯然要和原來的 Shape 是一樣的,因?yàn)榛镜某蓡T都是一致的。不過,因?yàn)槟銊?chuàng)建的 Circle 類型是你可以人為控制的,因?yàn)樗梢詭в幸恍┬碌摹⒉皇?Shape 里的東西,于是你可以給箱子改裝一下。

現(xiàn)在,我們要把這個(gè)新的箱子丟給 s 變量(賦值給左邊)。sShape 類型的,因此它只需要檢測你給的這個(gè)箱子到底是不是包含 Shape 這些應(yīng)有的東西。顯然,繼承下來就一定包含它們,因此箱子你怎么改裝,肯定這些成員是不會(huì)丟失的,那么,自然就允許和通過檢測了,賦值是成功的。

我們把這種賦值現(xiàn)象(基類型變量用子類型實(shí)例賦值)稱為變量的多態(tài)性。因?yàn)樽兞勘旧砜梢允?Circle 類型的,也可以是 Shape 類型的,所以它這不就是兩個(gè)“狀態(tài)”了嘛。

那么這種賦值現(xiàn)象有什么用呢?用來把作變量類型的合取。假設(shè)我們有一個(gè)輸出一個(gè)形狀的方法:

這顯然可以奏效,可以輸出一個(gè)正常的、正確的正方形的面積。可問題就在我如果需要輸出圓的面積、長方形的面積、梯形的面積和別的什么圖形的面積的時(shí)候,這個(gè)方法不就得抄幾遍嗎?這就很麻煩了是吧。

所以,我們只需要把參數(shù)改成 Shape 類型的:

可以看到,只需要這么改變一下之后,我們什么圖形都可以調(diào)用這個(gè)方法了,只要這個(gè)變量是 Shape 類型的:

既然都是 Shape 類型的,那么賦值是成功的,傳參的時(shí)候,s、tu 也都是 Shape 類型的,也符合參數(shù)的類型規(guī)則,因此一個(gè)方法支持多個(gè)執(zhí)行,當(dāng)然就是沒問題的了。

再說了,這么用,在 PrintArea 方法里,我們也只是用了 Area 屬性。顯然 Shape 自帶 Area 屬性,s、tu 肯定也都因?yàn)槔^承機(jī)制而自動(dòng)配了 Area 屬性的,所以憑什么我們又不讓它參與運(yùn)行呢?

這就是多態(tài)的好處。多態(tài)可以一勞永逸。當(dāng)然了,你也可以這么寫:

這么寫也可以。

這樣也可以。

Part 2 類型匹配

既然有多態(tài),那么如果我們類型是基類型的,那么我們咋知道這個(gè)類型具體是什么類型呢?這個(gè)時(shí)候我們就需要用到兩個(gè)運(yùn)算符了:isas 運(yùn)算符。

2-1 is 運(yùn)算符

is 運(yùn)算符用來檢測類型是否正確。is 的左邊寫變量,右邊寫類型名稱。整個(gè)表達(dá)式結(jié)果是一個(gè) bool 類型的數(shù)值,表示是不是變量就是這個(gè)類型的。

大概這么用。

2-2 as 運(yùn)算符

as 運(yùn)算符可以轉(zhuǎn)換數(shù)據(jù)的類型,它等價(jià)于 obj is T ? (T)obj : null 這個(gè)表達(dá)式。其中的 T 就是寫在 is 后面的這個(gè)類型名稱。

比如這樣用。

2-3 強(qiáng)制轉(zhuǎn)換運(yùn)算符

前面我們用到了一個(gè)新鮮的類型轉(zhuǎn)換。如果一個(gè) Shape 類型原本就是從 Rectangle 這邊變過來的類型的話,我們可以使用 (Rectangle)obj 的語法。這是面向?qū)ο蟮睦^承里的一大特性。

  • 如果從子類型轉(zhuǎn)基類型的話,因?yàn)槭且欢梢赞D(zhuǎn)換成功的,因此是隱式轉(zhuǎn)換的(即前面 Part 1 講的東西);

  • 如果從基類型轉(zhuǎn)子類型的話,因?yàn)轭愋筒灰欢ǔ晒D(zhuǎn)換,因此是使用強(qiáng)制轉(zhuǎn)換的。

這就是繼承的轉(zhuǎn)換機(jī)制。當(dāng)然了,任何類型的變量,o is object 都應(yīng)該是為 true 的,因?yàn)樗蓄愋投紡?object 類型派生。

Part 3 object 類型:所有引用類型的根

下面我們來說一個(gè)新鮮玩意兒:object 類型。和 string 類型一樣,object 類型也是分關(guān)鍵字寫法和 BCL 名稱寫法兩種的。關(guān)鍵字 object 對(duì)應(yīng)了它的 BCL 名稱 Object。

object 類型是所有類型的基類型。換句話說,如果你不寫繼承機(jī)制的語句:: 類名,那么這個(gè)類型自動(dòng)從 object 類型派生;如果這個(gè)類型寫了繼承語句 : 類型 的話,那么這個(gè)類型就會(huì)從這個(gè)寫的這個(gè)類型派生;而寫出來的這個(gè)基類型,如果沒有繼承語句,那么它也是自動(dòng)從 object 派生的。

另外,就算是之前接觸的那些值類型,intdouble 這些,它們是從一個(gè)叫做 ValueType 的類型派生下來的;但 ValueType 是從 object 派生的。因此,不論你發(fā)現(xiàn)到什么東西,都是從 object 派生下來的。

說這個(gè)有什么用呢?下面我們來說一些 object 類型的成員。

3-1 Equals 方法

object 類型里有一個(gè)叫做 Equals 的方法,這個(gè)方法和之前我們提到過的 ReferenceEquals 方法一點(diǎn)區(qū)別都沒有:

這是兩種寫法。但執(zhí)行的效果是一樣的。

可能你會(huì)問我,既然是一樣的,為什么要定義倆寫法不同的方法呢?因?yàn)?Equals 是被 virtual 修飾過的實(shí)例方法,而 ReferenceEquals 是靜態(tài)方法。

既然被 virtual 修飾過的,那么就意味著這方法可以重寫。因?yàn)樗窍到y(tǒng)自動(dòng)繼承的,因此這個(gè)方法不管你寫不寫繼承語句,都是自動(dòng)可以使用的方法。不過,如果你要比較內(nèi)部的數(shù)據(jù)的話,你可以重寫 Equals 方法。

假設(shè)我們還是用 Shape 類來舉例。

現(xiàn)在,我們把 Shape 類改裝成這樣。請(qǐng)注意第 5 行代碼,我們使用了一種新的語法 abstract override 組合關(guān)鍵字。這里,我們用到的是 object 類里自帶的 Equals 方法。那么,為什么我們可以這么組合呢?因?yàn)槲覀冞@里的 abstractoverride 都有作用:abstract 是說這個(gè)方法是抽象的,那么在派生類里就必須給我實(shí)現(xiàn)這個(gè)方法;override 關(guān)鍵字則是表示這個(gè)方法是從基類型 object 里直接拿下來的。

因?yàn)闆]有提供實(shí)現(xiàn)代碼,因此大括號(hào)就不能寫了;相反,使用分號(hào)結(jié)尾就可以了。

接著,我們在繼承 Shape 類的時(shí)候,就需要同時(shí)實(shí)現(xiàn) Area 屬性和 Equals 方法了。

比如我們拿 Rectangle 類型舉例。我們使用之前學(xué)的知識(shí)點(diǎn)來完善例子。再等下次,我們?nèi)绻褂玫?Equals 方法后,方法就會(huì)自動(dòng)定位到這里 Rectangle 里的 Equals 而不是 objectEquals 了。這樣,就和 ReferenceEquals 不再一致了。

3-2 GetHashCode 方法

要想明白這個(gè)方法為什么得以存在,地位還那么高(放在了 object 里),就得先知道一個(gè)概念:哈希碼(Hash Code)。

哈希碼,在 C# 里用一個(gè) int 類型的數(shù)值表示。任何世間萬物都通過一個(gè)公式(不論是系統(tǒng)自帶的,還是你自己寫的)來計(jì)算得到一個(gè)哈希碼。這個(gè)哈希碼用于直接區(qū)分對(duì)象是不是一致。換句話說,如果兩個(gè)對(duì)象的哈希碼一致,我們大概率認(rèn)為這兩個(gè)對(duì)象包含相同的數(shù)值;反之,如果哈希碼不同,那么我們大概率認(rèn)為這兩個(gè)對(duì)象可能有個(gè)別數(shù)據(jù)成員的數(shù)值不同,甚至是完全不同。

為什么說是“大概率”,而不是“一定相同”或者“一定不相同”呢?世間萬物都用公式計(jì)算的話,顯然是不合適的;另一方面,公式也不能夠完全區(qū)分兩個(gè)對(duì)象是不是相同。舉個(gè)例子,我有一個(gè)超長字符串(100 個(gè)字符的那種)和另外一個(gè)超長字符串(也是 100 個(gè)字符)。我如果要比較兩個(gè)字符串是否一致,顯然就是逐字符比較。遇到不同的字符就說明兩個(gè)字符串不同。

但是,如果通過哈希碼計(jì)算的話,就有一點(diǎn)問題。首先,在 C# 里的一個(gè)字符可以表示非常多的情況(大概?2%5E%7B16%7D?種不同的字符);那么 100 個(gè)字符就有?%7B2%5E%7B16%7D%7D%5E%7B100%7D 種情況。很顯然這個(gè)數(shù)已經(jīng)是天文數(shù)字了。要想每一種情況都得配好一個(gè)哈希碼來的話,這肯定是不可能的事情,畢竟 GetHashCode 的默認(rèn)返回值是 int 的,這個(gè)你是改不了的。

所以,我們只能盡量做到“哈希碼數(shù)值不同能夠表達(dá)的對(duì)象不同”,而永遠(yuǎn)不可能找到一種辦法可以唯一表示任何一個(gè)字符串的通用計(jì)算哈希碼的公式。當(dāng)然,別的數(shù)據(jù)類型也是一樣,因?yàn)楫吘勾笮《疾灰粯勇铩?/span>

至于 C# 的 char(字符類型)為什么有?2%5E%7B16%7D 種情況,這一點(diǎn)你可能需要參考一下 UTF-16 編碼,這里我們就不展開說明了。

這個(gè)方法,Visual Studio 會(huì)提示你在重寫了 Equals 方法的時(shí)候重寫它;或者是如果你不重寫 GetHashCode 方法的時(shí)候,Visual Studio 會(huì)告訴你“Equals 方法需要重寫”。雖然不能絕對(duì)保證數(shù)據(jù)不同,但 GetHashCode 確實(shí)可以用來比較數(shù)據(jù)。因?yàn)樵谝恍﹫龊舷?,哈希碼計(jì)算結(jié)果一定可以唯一表示一個(gè)數(shù)據(jù),且不同的數(shù)據(jù)產(chǎn)生的哈希碼一定不一樣。比如說我有一個(gè)叫做 Cell 的類,它包含兩個(gè)字段 RowColumn。Cell 類型的對(duì)象表達(dá)的是一個(gè)格子的第幾行第幾列。那么假設(shè)整個(gè)網(wǎng)格最多只能 10 行 10 列的話,我們的哈希碼計(jì)算公式就可以這么寫:

是的,通過這個(gè)公式,我們就可以得到這個(gè)格子的哈希碼,而且因?yàn)槲覀兗僭O(shè)的網(wǎng)格最多只能 10 行 10 列,所以我們無法超過這個(gè)規(guī)格的話,用 Row * 10 就是合適的。

那么,如果我們要比較兩個(gè) Cell 類型的對(duì)象是不是一樣,現(xiàn)在就有兩種比較辦法:

第一種就是純粹比較兩個(gè)對(duì)象的 RowColumn 數(shù)值是不是都一樣。而第二種判別方式就比較簡單了:因?yàn)楣4a能夠唯一確定數(shù)據(jù),所以我們直接通過哈希碼就可以比較兩個(gè)對(duì)象是不是一致。

而且可以看到哈希碼計(jì)算公式相當(dāng)簡單,因此我們直接上手寫逐數(shù)據(jù)成員比較的話,就顯得代碼很臃腫。畢竟,有簡單的比較辦法我們肯定不會(huì)用復(fù)雜的,因?yàn)閮蓚€(gè)比較辦法都能得到一致的、正確的結(jié)論。這就是哈希碼的存在的意義。

3-3 ToString 方法

很明顯,從這個(gè)名字上就可以看出這個(gè)玩意兒用來干嘛了。ToString 方法用來把對(duì)象用字符串形式表達(dá)呈現(xiàn)出來。因?yàn)橐敵鲲@示一個(gè)對(duì)象的信息,我們就不得不擁有一個(gè)機(jī)制,來把對(duì)象呈現(xiàn)出來。那么,只要我們重寫了 ToString 方法的話,就可以直接這么寫代碼:

就非常方便了。

一般通常,我們實(shí)現(xiàn) ToString 的辦法都是,把需要呈現(xiàn)的數(shù)據(jù)成員給提出來,然后用字符串拼接的方式把它們拼接起來,最后輸出。比如,假設(shè)我們要顯示一個(gè)形狀,那么代碼可能是這樣的:

首先,這是在 Shape 里的代碼。我們追加一個(gè)抽象屬性 ShapeKindName 用來顯示輸出這個(gè)形狀到底是什么。在從 Shape 類派生后,我們就不得不重寫掉這個(gè)屬性,比如重寫的數(shù)值可以是 "Rectangle",那么就寫成 public override string ShapeKindName { get { return "rectangle"; } }。

寫好這個(gè)屬性后,我們就可以在 Shape 類里的 ToString 方法里直接使用 ShapeKindNameArea 來顯示具體的數(shù)值信息。這里用到了 string.Format 這個(gè)靜態(tài)方法,雖然沒有講過,但是可以告訴你的是,這個(gè)方法和 Console.WriteLine 的傳參方式是完全一樣的,所以不必考慮和擔(dān)心參數(shù)列表到底如何書寫的問題,照搬過來就可以了。只是,string.Format 方法返回的是一個(gè)字符串,而 Console.WriteLine 方法是直接把字符串結(jié)果顯示出來了,它倆在呈現(xiàn)機(jī)制上有所不同。

稍微提一下的是,這里的 ToString 被我用 sealed 標(biāo)記了,這表示我在添加別的類的繼承的時(shí)候,就不許再次重寫 ToString 了,你只能用這個(gè)方法,而不能改內(nèi)部的執(zhí)行邏輯。

3-4 ReferenceEquals 靜態(tài)方法

是的,你的猜想一點(diǎn)都沒有錯(cuò)。之前我們提到的 ReferenceEquals 方法其實(shí)就是來自于 object 類里,只是有所不同的地方是,這個(gè)方法一般都要寫成 object.ReferenceEquals,因?yàn)樗?object 類里;但是實(shí)際上我們都沒有寫它,這是因?yàn)?C# 知道這個(gè)方法是 object 里的,所以不用寫。

3-5 ==!= 運(yùn)算符為什么要重載

實(shí)際上,object 就自帶了 ==!= 這兩個(gè)運(yùn)算符。正是因?yàn)樗亲詭У?,所以我們不重寫的話,C# 就會(huì)自動(dòng)定位到 object==!=。而大家都知道的是,==!= 實(shí)際上就是簡單調(diào)用了一下 ReferenceEquals(這一點(diǎn)之前有說過哦),所以我們要重載運(yùn)算符來避免 C# 定位到這里,只要我們重寫了 Equals 方法,或者 GetHashCode 方法。

另外,順帶一提。我們之前就說過寫代碼要養(yǎng)成好習(xí)慣,如果是引用類型傳入的話,就一定有可能為 null,因此,只要遇到引用類型就一定要先判斷這個(gè)對(duì)象是不是為 null 數(shù)值。判斷方法就是調(diào)用 ReferenceEquals 方法了。

Part 4 重寫(override)、重載和覆蓋(new)的區(qū)別

很高興我們能說到這里。這三個(gè)詞語其實(shí)區(qū)別不大,所以經(jīng)常容易分不清楚。下面我們來說一下這三個(gè)詞語的區(qū)別。

  • 重寫(Override):基于基類型提供的抽象成員(abstract 修飾的)或虛成員(virtual 修飾的),重新修改執(zhí)行邏輯的過程;

  • 重載(Overload):重載有兩層含義:運(yùn)算符重載和方法重載。方法重載是參數(shù)不同構(gòu)成不同重載,所以跟這里關(guān)系不大;而運(yùn)算符重載是避免運(yùn)算符本身在調(diào)用的時(shí)候還定位到基類型(比如指的是 object)的運(yùn)算符去。因?yàn)檫\(yùn)算符重載本身是靜態(tài)的行為,所以根本談不上用 overridevirtual、abstract 或者 sealed 這類只用來修飾實(shí)例成員的修飾符;

  • 覆蓋(Overwrite):覆蓋和重寫的區(qū)別就是是否阻斷了繼承鏈。如果是重寫,那么就是基類型直接拿下來的;而覆蓋則是直接把基類型的成員隱藏掉,而以后所有的繼承都從這里覆蓋掉的地方開始往下算,而基類型的就不再能夠可以訪問了。

Part 5 繼承關(guān)系下的訪問級(jí)別問題

如果我使用了繼承關(guān)系的語法來的話,比如這樣的代碼:

在同一個(gè)項(xiàng)目下,AB 因?yàn)闆]有訪問修飾符修飾,因此默認(rèn)的修飾符應(yīng)該是 internal。而 internal 只在項(xiàng)目里可以隨便使用。如果我試著改變 AB 的訪問修飾符的話,那么一共就有四種情況:

  • public class Apublic class B;

  • public class Ainternal class B

  • internal class Apublic class B;

  • internal class Ainternal class B。

那么,這些寫法都是正確的嗎?從語法上它們都應(yīng)該是對(duì)的,但實(shí)際上在使用的時(shí)候,我們來看一下 B : A 的繼承關(guān)系約束下,B 就必須和 A 是一樣級(jí)別的訪問修飾符,或者比 A 要小。

按道理來說,BA 的派生類型,這就是在說,我可以使用多態(tài)機(jī)制來書寫這樣的代碼:

兩個(gè)代碼在語法上都是可以的。可問題就在于,我兩種寫法都正確的話,就意味著我必須 AB 得是同一個(gè)級(jí)別,或者 BA 的級(jí)別要小,才可以這樣。如果 BA 訪問級(jí)別還要大的話,那么唯一的一組情況就只可能是 public class B(子類型)和 internal class A(父類型)了。從邏輯上來看,我能夠?qū)嵗粋€(gè) B 類型的對(duì)象并暴露寫在代碼里,可我如果多態(tài)使用 A 類型來接收的話,而我此時(shí) B 繼承關(guān)系上又保證了它是從基類型 A 這里拿下來的,但卻又不能使用多態(tài)給 new B() 賦值給 A 類型,因?yàn)?A 我又“看不見”。這不就是矛盾了嗎?

如果你沒有明白這段話的話,我換一個(gè)說法。面向?qū)ο笠馕吨粋€(gè)類型必須得要么走 object 這個(gè)默認(rèn)類型派生,要么就必須給出一個(gè)自定義的引用類型,讓該類型走這個(gè)我自定義的類型派生。那么我自己的訪問級(jí)別能夠被當(dāng)前環(huán)境(或者叫范圍吧)下看得到,那么基類型就必須得也能夠看得到才行,否則我走哪里派生的我不清楚的話,別人說不定還以為我是走 object 派生的,畢竟我現(xiàn)在連一個(gè)基類型都看不到了嘛。這就破壞了面向?qū)ο蟮睦^承機(jī)制。所以,當(dāng)前類型在當(dāng)前范圍下能看得到,那么它的基類型也必須能看得到。因此,我無關(guān)我當(dāng)前類型什么訪問修飾符,但它的基類型的訪問修飾符的級(jí)別至少都得和當(dāng)前類型的訪問級(jí)別得是一樣的,或者說基類型比當(dāng)前類型的訪問修飾級(jí)別還要大。所以 B(子類型)是 publicA(父類型)是 internal 的這組情況是不可能在 C# 里存在的;而其它三種情況均是可以的。

這個(gè)是類的繼承關(guān)系下的訪問修飾級(jí)別的問題。那么,如果是嵌套類型呢?這個(gè)時(shí)候,類型可以嵌套的話,里面的這個(gè)類型就可以使用 private 或者 protected,甚至是 protected internal 來修飾了。這個(gè)情況更為復(fù)雜,這怎么理解呢?

倘若我有一個(gè)這樣的情況:

現(xiàn)在 Nested 類型從 B 類型派生,而 NestedB 是不同的類型。那么這個(gè)時(shí)候,組合情況就非常多了。按照我們剛才的說法,“當(dāng)前類型在當(dāng)前范圍下能看得到,那么它的基類型也得能看得到”,因此至少 B 的訪問級(jí)別不能比 Nested 的低。

比如說,如果 Nestedprivate 修飾的,那么 B 就可以什么都行,因?yàn)槔^承關(guān)系下,訪問修飾級(jí)別是可以相同的,而最低情況下就只有 private,而它也可以,所以,此時(shí) B 是什么修飾符都行;那如果是 protected 呢?internal 呢?

我們這里來看一個(gè)表格。

這個(gè)表格其實(shí)不用去死記硬背,因?yàn)楫吘共皇巧险n,也不是考試。但是有了這個(gè)表格,比較熟悉了的話,寫代碼會(huì)輕松一些;然后,這樣的情況平時(shí)用得也不多,所以大概了解一下即可。特別注意的是,即使派生類型和基類型都是相同的 protected internal 修飾符,這樣的組合也不允許,原因是 protected internal 組合修飾符比較特殊,因?yàn)?protected internalprotectedinternal 兩種級(jí)別都混合在一起的情況,那么我就說不清楚這個(gè)類型到底是走了一個(gè) internal 還是 protected 修飾的類型派生下來的,所以是不允許的。

第 38 講:面向?qū)ο缶幊蹋ㄊ簩?duì)象的多態(tài)的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國家法律
桃园市| 元江| 绥江县| 赣州市| 慈溪市| 安国市| 呼图壁县| 朔州市| 通江县| 遵义市| 永清县| 加查县| 长治县| 永和县| 七台河市| 南皮县| 玛多县| 油尖旺区| 大兴区| 邮箱| 庆城县| 天柱县| 沛县| 威信县| 都昌县| 陆良县| 东平县| 岑巩县| 施秉县| 云浮市| 莆田市| 缙云县| 福安市| 鹿邑县| 隆昌县| 于都县| 饶平县| 射洪县| 乐安县| 肥西县| 睢宁县|