最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

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

第 96 講:C# 3 之?dāng)U展方法

2022-03-21 08:46 作者:SunnieShine  | 我要投稿

前文我們介紹了第一個 C# 3 對方法語法的拓展功能,下面來看看第二個:擴(kuò)展方法(Extension Method)。

Part 1 引例

考慮一種情況。我現(xiàn)在有一個 int 的數(shù)據(jù),我想獲取這個 int 的所有比特位是 1 的位置,比如 13 的二進(jìn)制是 1101,我想通過某個方法得到 0、2、3 構(gòu)成的數(shù)組,表示的是 13 的二進(jìn)制表達(dá) 1101B 的第 0、2、3 位上都是 1。

其實實現(xiàn)起來并不難。

從代碼看出,其實邏輯也不復(fù)雜。不過這里用到三個處理技巧:BitOperations.PopCount 方法、>>= 1 操作和 & 1 操作。這也不必多說了,>>= 1/= 2 是一個意思;& 1% 2 是一個意思;而 BitOperations.PopCount 方法是獲取一個整數(shù)有多少個比特位是 1。

那么,我們調(diào)用這個方法就先得比較輕松了:

我們保證了方法處理過程是嚴(yán)謹(jǐn)?shù)?,因此我們可以留給別的人使用,因此我們可以通過這樣的機(jī)制來調(diào)用這個方法。可問題在于,這里我們的“類型”我們并不關(guān)心是什么。我們要想包裝這個方法,我們必須使用類型來包裝起來。C# 要求所有的方法必須都分門別類放在相同或不同的數(shù)據(jù)類型里,因此,我們必須給這個方法規(guī)劃一個類型而存儲起來。

假定這個類型叫 Int32Extensions,專門放的是關(guān)于 int 數(shù)據(jù)類型的一些拓展操作的方法:

這么做也是有意義的,不過我們總不能用一次就寫一遍 Int32Extensions 吧,這太不友好了。于是,擴(kuò)展方法就誕生了。

Part 2 語法

擴(kuò)展方法需要我們做兩處修改和變動。第一處是方法本身。我們給參數(shù) digit 修飾 this 關(guān)鍵字。是的,this 關(guān)鍵字在這里當(dāng)成修飾符來用:

這是第一處修改。接下來是第二處。我們嘗試將 GetAllSets 就當(dāng)成是 int 類型的一個實例方法那樣去書寫:

是的,a.GetAllSets()。我們試著將 a 這個變量當(dāng)作是 int 類型的實例,而 GetAllSets 雖然是靜態(tài)方法,但我們也可以通過此語法改成類似實例方法的調(diào)用方式,而由于 a 按照實例方法的寫法而被提前了,因此小括號里原本傳入 a 現(xiàn)在就會變?yōu)榭盏男±ㄌ?。這就是擴(kuò)展方法:帶有 this 關(guān)鍵字修飾在第一個參數(shù)上的靜態(tài)方法。

啰嗦一點。這個語法叫擴(kuò)展方法,不叫拓展方法。擴(kuò)展和拓展在 C# 是有明顯的語義區(qū)分的。擴(kuò)展指的是將功能、代碼內(nèi)容和一些別的東西進(jìn)行推廣,而拓展則指的是將原有的內(nèi)容進(jìn)行進(jìn)一步地優(yōu)化和翻新。說白了就是,擴(kuò)展用的是新的東西來推廣原來的東西,而拓展用的是原來的東西,就地變更內(nèi)容來推廣原本的東西。顯然,這個語法用到了一個單獨的工具類型,存儲了這些方法,它明顯在語義上是將類型進(jìn)行的推廣,因此稱為擴(kuò)展方法。

Part 3 使用約定和規(guī)范

既然用法我們知道了,那么肯定得有一些內(nèi)容需要我們注意。

3-1 this 修飾符只能在位于靜態(tài)類的靜態(tài)方法上

我們觀察擴(kuò)展方法的語法,可以得知,實際上擴(kuò)展方法就是標(biāo)記上了其中一個參數(shù),然后將其當(dāng)成實例的調(diào)用規(guī)則。這樣的方法可以從上面的引例看出,它一般都用作類型的擴(kuò)展。也就是說,有一些 .NET 庫就提供了的封裝完好的數(shù)據(jù)類型,我們無法直接在封裝好了的這些數(shù)據(jù)類型上加上東西,那么我們只有自己寫地方裝上它們,然后達(dá)到擴(kuò)展的目的。

但早期的 C# 語法里只能讓我們使用所謂的“工具類型”的概念去存儲它們,但現(xiàn)在我們有了這樣的語法規(guī)則,因此我們可以這樣去更加流暢地寫出代碼來。那么,既然是一個工具類型里提供的方法,那么我們之前就說過,工具類型是不求任何別的人去實例化的,因此這樣的類型我們往往都定義為靜態(tài)類型,而在這個靜態(tài)類型里,我們定義的方法也肯定就只能是靜態(tài)方法了,因為靜態(tài)類里不能聲明任何實例成員。

正是因為大家寫代碼得到的這些經(jīng)驗總結(jié)和約定俗成的內(nèi)容,因此 C# 3 的擴(kuò)展方法有這兩條限制:

  1. 擴(kuò)展方法只能放在靜態(tài)類里(因為靜態(tài)類才是最適合作為工具類型的類型);

  2. 擴(kuò)展方法只能是靜態(tài)的(因為靜態(tài)類要求存儲的成員也都是靜態(tài)的)。

3-2 this 修飾符只能修飾在方法的第一個參數(shù)上

this 修飾符作為擴(kuò)展方法使用的特殊修飾符類型,它的目的是用來擴(kuò)展一個數(shù)據(jù)類型,使得調(diào)用的時候顯得更為靈活和美觀,畢竟少了一個無意義的靜態(tài)類型名稱來說,代碼看起來確實要好看一些,也要簡短一些。

this 修飾符正是考慮到用來擴(kuò)展類型的目的,它只能用在第一個參數(shù)上。假設(shè)有兩個參數(shù),比如這樣的東西:

我們這樣的方法也是允許使用的,雖然有三個參數(shù),但我們只要符合 this 在第一個參數(shù)上,那么這個方法就可以用來改寫為實例寫法的擴(kuò)展方法:

此時我們需要傳參的是兩個參數(shù)而不是方法要求的三個。傳入的兩個參數(shù)分別對應(yīng)的是這個 Slice 方法的 startlength,而第一個參數(shù) s,已經(jīng)被我們這樣的語法改寫而前置當(dāng)作實例去了。這種現(xiàn)象也叫擴(kuò)展方法的實例前置(Prepositional Instance)。

如果允許 this 修飾符放在別處的話,那么就亂套了:

就拿這個來舉例,假設(shè)我放在最后去了,但因為擴(kuò)展方法語法的嚴(yán)謹(jǐn)性和編譯器的方便處理,我們放在第一個顯然就會更好一些,因為每一個擴(kuò)展方法總是在第一個是 this 參數(shù),這就非常好用了,也方便編譯器讀懂我們使用的代碼和語法規(guī)則。而寫在末尾的話,每一個方法有或多或少有不同個數(shù)的參數(shù),這樣就不方便編譯器處理和閱讀我們的代碼。

3-3 this 修飾符在同一個方法里只能用一次

顯然,this 修飾符的出現(xiàn)對編譯器那是有特殊的用途。試想一下,如果下面的代碼是可以的的話,那么這樣的代碼是啥意思:

這肯定是不可能讀得懂的代碼。這要是翻譯為實例寫法的話,那到底是 a 是實例調(diào)用還是 b 是實例調(diào)用呢?這不就說不清楚了。

C# 3 的擴(kuò)展方法要求,this 修飾符專門用作實例調(diào)用的寫法,所以它只能在一個方法里出現(xiàn)一次,多了就不行了。

3-4 擴(kuò)展方法可以是任何訪問修飾級別

擴(kuò)展方法的用途和作用的是代換工具類型,改用實例的方式來優(yōu)化調(diào)用過程,進(jìn)而讓代碼更好看。那么,訪問修飾符對于這個特性來說,就沒有任何影響了。哪怕它是私有的,但是只要你在類型里使用,不也不影響?所以,擴(kuò)展方法可以用任何的訪問修飾符,只要你愿意。

還有一個原因是,擴(kuò)展方法只是多了一個參數(shù)用了 this 修飾符,但它的基本語法都和普通的方法是一樣的,所以沒有意義也沒有必要對擴(kuò)展方法的訪問修飾符單純做一次限制。

3-5 除了指針,任何數(shù)據(jù)類型都可以用來當(dāng)作擴(kuò)展方法的擴(kuò)展類型

C# 的擴(kuò)展方法基本上能將所有的類型都進(jìn)行擴(kuò)展,所以是一個非常強(qiáng)大的語言特性。但問題是,唯一一種類型,擴(kuò)展方法是不能用的;換句話說,就是這個類型是不能使用 this 修飾符的。這就是指針類型了。

比如這樣的代碼就不合理。有人會問,為什么指針不行呢?下面我從兩個角度給大家說明一下為什么。

第一,指針沒有單獨的類型聲明。在 C# 的世界里,所有的數(shù)據(jù)類型都需要我們自己獨立給出類型,存儲到一個文件或多個文件里去。但是,指針是沒有自己的類型聲明的。這說起來不好理解。我這么舉個例吧。int? 對應(yīng) Nullable<T> 類型,而 int 對應(yīng) Int32 類型,int[] 則對應(yīng) Array 類型。所有 C# 里的類型,不論它寫成啥樣,最終我們都能找到一個合理的基本類型來表達(dá)和表示它,但指針類型不行。C# 的指針類型是一種“奇怪”的類型,它甚至不走 object 的派生體系,因此你無法這么寫代碼:

void*object 是兩種不同的類型,object 以及子類型是一個派生類型體系,而 void* 則不是派生體系的一員。所以,你擴(kuò)展方法實現(xiàn)了這個 void* 這種指針類型的擴(kuò)展,有什么意義呢?它擴(kuò)展了什么?

第二,指針具有 .-> 運(yùn)算,允許指針使用擴(kuò)展方法將導(dǎo)致兩個運(yùn)算符的推算不再嚴(yán)謹(jǐn)??紤]下面的代碼。

倘若有這樣的方法,我們在使用的時候有如下的寫法:

而我們知道,指針類型變量在 C# 里只能使用指針成員訪問運(yùn)算符 ->。因此,這破壞了 C# 對這個行為的基本規(guī)則和約定。

所以,不論如何,C# 沒有允許擴(kuò)展方法使用指針類型當(dāng)擴(kuò)展。

Part 4 泛型擴(kuò)展方法

是的,擴(kuò)展方法甚至支持泛型??紤]下面的代碼:

那么,這樣的代碼如果允許 C# 使用,那么咋用?

答案很簡單。T 是泛型類型,這意味著任何一種數(shù)據(jù)類型(這里指針仍然除外,因為指針類型不在泛型處理的考慮范疇里)都支持和兼容這個方法的使用和調(diào)用。因此,不論你傳入一個什么類型,都可以執(zhí)行這里給出的代碼邏輯。哪怕 T 僅僅是一個值類型。值類型雖然沒有 null 一說,但 T 是泛型的,所以它并不知道來者何人,而且再加上但凡它有數(shù)值,即使它是值類型,那么被裝箱后也會有對應(yīng)的裝箱實例的地址,因此怎么說也不可能為 null。因此 ReferenceEquals(instance, null) 是合理的代碼。

用法很簡單。但凡在這里給一個實例,只要不是指針,就能使用上這個擴(kuò)展方法:

唯一需要注意的是,帶泛型約束的時候。

其實,問題也不大。調(diào)用的時候,只需要看看你給的實例是不是滿足泛型約束即可。如果不滿足,那么就不能作為擴(kuò)展方法來用,僅此而已。

Part 5 重載方法的調(diào)用優(yōu)先級規(guī)則推廣

由于這個語法和普通的實例方法的調(diào)用語法是完全相同的,所以它牽扯到了語義處理的機(jī)制,而不單單只是語法上的新規(guī)則。正是因為它和實例方法的調(diào)用規(guī)則寫法完全一致,因此方法的重載在 C# 3 擴(kuò)展方法誕生后有所推廣。

什么意思呢?你想想看,我在一個類型 T 里有一個 F 方法,而我也對 T 類型寫了一個擴(kuò)展方法,名字也叫 F。那么這樣兩個方法由于不在一起,但也構(gòu)成重載。這就有一個比較奇特的現(xiàn)象:我完全可以實現(xiàn)一個擴(kuò)展方法,讓它的調(diào)用寫法和我直接調(diào)用實例方法的寫法是完全一樣的。比如 new T().F() 可以指代 T 類型的實例方法 F(無參的),也可以指代給 T 類型寫了一個擴(kuò)展方法,比如它是 TExtensions 類型下的 F 方法,那么完全可以對應(yīng)上去的是 TExtensions.F(this T instance); 這樣靜態(tài)方法的簽名。由于方法存放的地方不一樣,所以它們不會沖突;而正是因為這樣的特殊性,因此擴(kuò)展方法和普通實例方法就會交織在一起構(gòu)成新的重載規(guī)則。對于這種重載規(guī)則,我們應(yīng)該如何去處理和理解呢?

你始終記住,擴(kuò)展方法是最接近兼容類型的實例方法。意思是說,除了 T 類型自帶的實例方法,跟 T 類型相關(guān)(就是寫成 this T 的意思)的這些擴(kuò)展方法也會被視為這個 T 類型的真正存在的實例方法,只是唯一一個區(qū)別是,優(yōu)先先看 T 類型自身的方法。如果這個類型沒有這個方法,則會去匹配擴(kuò)展方法。

5-1 擴(kuò)展方法和實例方法在一起時候的重載

下面我們來看一個例子。

E 類型里包含兩個擴(kuò)展方法 F,都是針對于 object 的擴(kuò)展。不過傳參有所不同,一個是 int,而另外一個是 string。而 A、BC 是三個不同的數(shù)據(jù)類型,其中 A 類型里沒有實例方法,BC 類型則有實例方法,也都叫 F。不過 B 傳參是 int 類型,C 類型的這個方法傳參則是 object 類型。

接著 X 類型里則是執(zhí)行和調(diào)用這些方法。試問一下,這些方法分別都對應(yīng)什么方法?(答案已經(jīng)寫在上面了,我希望你先自己思考了然后看答案。)

下面我們針對上面給的答案解釋一下原因。

  • a.F(1):這個調(diào)用下,aA 類型的實例,但問題是 A 類型沒有自己的實例方法,于是就只能去看擴(kuò)展方法。擴(kuò)展方法里有一個 object 類型的擴(kuò)展方法,這意味著所有 object 類型的實例都可以使用此擴(kuò)展方法。但問題是它是 A 類型的實例,那么能不能用呢?當(dāng)然可以啦。因為 A 是自己定義的類型,它是 class 關(guān)鍵字定義的,因此屬于類,那么類的最終基類型就是 object。因此,只要屬于這個 object 類型派生鏈條上的所有類型(包括它自己)都是可以用這種擴(kuò)展方法的。接著,可以發(fā)現(xiàn) E 類型給了兩個擴(kuò)展方法,都是 F,參數(shù)類型換了一下。而 1 是 int 類型的字面量,顯然第一個方法就可以是完美匹配的。因此,a.F(1) 對應(yīng)調(diào)用的方法是 E 類型里的第一個 F 擴(kuò)展方法(即代碼第 3 行的這個方法);

  • a.F("hello"):顯然這就沒辦法在 A 類型里去找到匹配了。因為 A 類型里只有一個 F 的重載,而這個重載只能去看擴(kuò)展方法里有沒有了。顯然,擴(kuò)展方法里是有的,這個 F(this object, string) 的擴(kuò)展方法就非常對味。所以,這個方法調(diào)用的是 E 這個靜態(tài)類里的 F 擴(kuò)展方法(代碼里的第二個,即代碼第 4 行這個方法);

  • b.F(1)B 類型里有自帶的實例,它調(diào)用的 F 應(yīng)該是哪一個呢?答案看的是“就近原則”。因為 B 類型自身就有一個 F 方法,傳參是完全匹配的(參數(shù) 1 恰好是 int 類型的字面量,而 B 類型的 F 方法也確實是傳參 int 類型,因此外部存在的所有擴(kuò)展方法都是無效的,因為沒有機(jī)會匹配上它們;

  • b.F("hello"):不多說,因為 B 類型沒有 string 類型的重載,所以只得去看擴(kuò)展方法。擴(kuò)展方法里有兼容,所以它就調(diào)用這個;

  • c.F(1):這個稍微麻煩一些。顯然擴(kuò)展方法里有一個完全兼容的方法重載版本,它可以要求傳入 int 當(dāng)參數(shù),而我們這里頁恰好只需要一個 int 當(dāng)參數(shù)??蓡栴}是,C 類型自己有一個 F 實例方法,而且要求傳入的參數(shù),類型兼容的范圍太廣了——它能到 object。那么 c.F(1) 調(diào)用誰呢?當(dāng)然就是這個類型內(nèi)部的這個方法了,因為它最近嘛。雖然有一個擴(kuò)展方法,參數(shù)是完美兼容的,但是傳入的 this 參數(shù)是 object 類型,這里做了一次隱式轉(zhuǎn)換相當(dāng)于繞了一步;而它又在別的類型里放著,所以要想發(fā)現(xiàn)它又需要繞一步,所以要想調(diào)用到 c.F(1),需要繞兩步;但 C 類型的這個 F 方法,只需要轉(zhuǎn)換參數(shù)類型做一次隱式轉(zhuǎn)換即可,所以只繞一步。所以,c.F(1) 調(diào)用的是這個類型自己帶的這個方法;

  • c.F("hello"):也不必多說,都兼容完了,所以不管傳入啥,都只看 C 類型自己,不看外面的擴(kuò)展方法。

5-2 命名空間距離的概念

上面我們說了一下類型自身的方法和擴(kuò)展方法的重載,下面我們來說一下,命名空間不同導(dǎo)致的不同擴(kuò)展方法之間的重載。

不同擴(kuò)展方法的重載,看的是,調(diào)用的地方距離哪一個方法更近。換句話說,每一個方法的調(diào)用都會層層使用到命名空間名稱,然后才是類型,最后是這個方法。假如引用擴(kuò)展方法 1 需要是 C.F,而引用擴(kuò)展方法 2 則需要是 N1.D.F,那么顯然 CN1.D 就有不同:C 類型下直接就能被看到,但 N1.D 還需要進(jìn)入 N1 命名空間下,然后才能在 D 類型里發(fā)現(xiàn)它。因此相當(dāng)于多繞了一步。所以,不同地方的擴(kuò)展方法重載起來的話,看的是相對于調(diào)用方的“距離”。下面我們來舉個例子。

考慮這樣的代碼。CN1.D 類型都帶有 int 實例的重載,下面給出了 1.F()、2.G()3.H() 三個調(diào)用,請問它們分別調(diào)用的都是哪一個方法?

  • 1.F():由于我們使用了 using N1 指令,因此我們在 Test 類型里調(diào)用 F 方法應(yīng)該可以看到三個重載版本:C.F、D.FE.F。不過,因為 E.F 距離 Main 方法調(diào)用最近,因此這里我們優(yōu)先考慮的是 E.F 方法(即代碼的第 23 行),C.FD.F 不論如何都是多了一層命名空間的間接引用;

  • 2.G():我們使用的 using N1 指令使得我們可以看到 N1 命名空間下的 D.G 方法,因此我們這次可以看到兩處的重載:C.GD.G。由于我們要發(fā)現(xiàn) C.G 需要走出 N2 命名空間,但 D.G 是我們通過 using 指令已經(jīng)導(dǎo)入的內(nèi)容,所以它會被優(yōu)先發(fā)現(xiàn)到。你可以類比理解為一個“蟲洞”。從 Main(假設(shè)看成“地球”)往 D.G 前進(jìn),你只需要穿越蟲洞(蟲洞幾乎不消耗能力)就可以到達(dá);但你要去看 C.G,你需要走出地球所在的太陽系,然后去別的星系才能看到。所以 using 指令導(dǎo)入了的會被優(yōu)先選擇和讀取到,所以這個地方應(yīng)該調(diào)用的是 D.G 方法(即代碼的第 12 行代碼);

  • 3.H():這個不多說,因為只有一個擴(kuò)展方法,它沒有重載版本,因此直接調(diào)用即可,所以 3.H() 調(diào)用的是 C.H 方法(即代碼的第 5 行)。

5-3 同距離或同轉(zhuǎn)換步驟數(shù)的重載咋辦?

再極端一些,如果你遇到了同樣距離訪問的擴(kuò)展方法的話,那么編譯器肯定是區(qū)分不了調(diào)用誰的。此時,編譯器會直接生成編譯器錯誤告訴你不要這么去使用。

同理,如果轉(zhuǎn)換次數(shù)也是一樣的的話,編譯器也會直接告訴你,它也不知道調(diào)用哪一個,于是編譯也不通過。因此一定要規(guī)避這樣的現(xiàn)象(雖然這樣的極端情況也基本上遇不到)。

Part 6 總結(jié)

總的來說,擴(kuò)展方法是一個相當(dāng)有趣的語言特性,雖然它在重載的優(yōu)先級調(diào)用和匹配上理解起來比較難,但也不是不能理解,而且這種情況也不常遇到。盡量規(guī)避出現(xiàn)這樣的問題就可以了。

下面我們將要介紹的是 C# 3 里最后一個新語言特性,也是目前接觸到的最復(fù)雜的語言語法體系:集成語言查詢(Language Integrated Query,簡稱 LINQ),它可能會有 10+ 講解的篇目介紹這個體系,請做好心理準(zhǔn)備。


第 96 講:C# 3 之?dāng)U展方法的評論 (共 條)

分享到微博請遵守國家法律
齐齐哈尔市| 彭阳县| 南部县| 双桥区| 桓台县| 保亭| 镇平县| 丹东市| 彭山县| 静宁县| 兴安盟| 三原县| 凭祥市| 高雄市| 巧家县| 渑池县| 贞丰县| 丰原市| 康马县| 阿克陶县| 杭锦后旗| 名山县| 甘肃省| 平顺县| 巩留县| 健康| 游戏| 新建县| 开原市| 潞城市| 全椒县| 峨山| 绥棱县| 临潭县| 湘乡市| 瑞安市| 灵台县| 长海县| 类乌齐县| 清丰县| 永胜县|