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

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

探索 C# 9 的函數(shù)指針

2021-02-28 12:56 作者:SunnieShine  | 我要投稿

是的,你沒看錯(cuò),現(xiàn)在 C# 也有函數(shù)指針了,下面我們來說一下,C# 里的函數(shù)指針到底怎么使用,究竟怎么實(shí)現(xiàn)的。


Part 1 C 語言的函數(shù)指針

C 語言的指針早已經(jīng)不是新鮮事了,因?yàn)槲覀儗W(xué)編程就知道,C 語言的指針本來就不是人玩的東西。C 語言的指針為什么這么難呢?因?yàn)樗氖褂孟喈?dāng)靈活,甚至靈活到創(chuàng)造世間萬物,理論上都是可以做到的。

指針是用來表示一個(gè)“變量地址”的變量。說白了,它不存儲(chǔ)整數(shù)、小數(shù)、字符、布爾量,而是一個(gè)變量的地址。只要我們知道原始變量的類型,我們就可以通過定義,表達(dá)出這個(gè)指針的數(shù)據(jù)類型。比如,我們存儲(chǔ)的變量本身是 int 類型的,那么我們就可以認(rèn)為,這個(gè)指針是 int* 類型的,其中的 * 則是和普通變量作區(qū)分——它不是一個(gè)普通變量的記號(hào)。

當(dāng)然,指針并非只能用在變量上。C 語言的復(fù)雜程度讓我們覺得 C 語言并不好學(xué),它還可以用在數(shù)組上,于是就有了數(shù)組指針(類似寫成 int(*)[] 這樣的奇葩玩意兒)。

指針甚至可以用在函數(shù)上。換句話說,用一個(gè)指針變量和存儲(chǔ)一個(gè)函數(shù)的地址。是的,函數(shù)也有地址,這個(gè)地址數(shù)值只是我們平時(shí)基本上接觸不到罷了。我們?cè)囍靡幌潞瘮?shù)指針。假設(shè)我們有一個(gè)函數(shù)用來對(duì)一個(gè)數(shù)據(jù)進(jìn)行排序。并有一個(gè)參數(shù),這個(gè)參數(shù)是一個(gè)函數(shù)指針,它表示一個(gè)函數(shù),這個(gè)函數(shù)控制排序的比較關(guān)系是怎么做的。

是的,寫成 int (*comparison)(int, int),左側(cè)的 int 表示這個(gè)函數(shù)原始的返回值類型,而右邊的小括號(hào)表示這個(gè)原始函數(shù)的參數(shù)是傳入兩個(gè) int 數(shù)據(jù)。

咋用這個(gè)排序函數(shù)呢?

這里有三個(gè)知識(shí)點(diǎn)說一下。第一個(gè)是 int (*funcPtr)(int, int) 怎么到這里成變量賦值了?第二個(gè)是,sizeof(arr) / sizeof(int) 是什么?第三個(gè)則是,我們這里的 bubbleSort 函數(shù)既然寫出來了,怎么聲明函數(shù)(沒忘吧,C 語言的函數(shù)是需要聲明的)?

我們一個(gè)一個(gè)回答。第一個(gè)問題。你沒看錯(cuò),這里的 funcPtr 是一個(gè)指針變量,它指向了一個(gè)函數(shù) compareTwoValue。既然是指向這個(gè)函數(shù),那么賦值的右側(cè)必須就是一個(gè)地址數(shù)據(jù),因此我們就得把函數(shù)名當(dāng)成一個(gè)變量來用,因此,“&變量 表示一個(gè)地址”這個(gè)概念,你應(yīng)該沒有忘記吧。得到結(jié)果后,我們就把數(shù)據(jù)往左邊賦值就行了。雖然寫成這種格式,但是它確實(shí)是一個(gè)變量的寫法。你想想這個(gè)道理:一個(gè)函數(shù)的地址如果要當(dāng)成變量來用的話,那么就得有返回值和參數(shù)的格式一起寫到變量上去吧。那么,格式自然就是這樣了。而這里 funcPtr 左邊的星號(hào)是干啥呢?一個(gè)函數(shù)是不能直接拿來用的,那么就必須用到指針,自然這個(gè)符號(hào)就得出現(xiàn)了。那么為啥有括號(hào)呢?因?yàn)?(*ptr) 是一個(gè)整體,否則這個(gè)小括號(hào)去掉后會(huì)被編譯器看成“返回值是 int* 的普通函數(shù)變量”。顯然就不符合我們預(yù)期的理解了。

第二個(gè)問題,這個(gè)是 C 語言計(jì)算數(shù)組元素個(gè)數(shù)的辦法:sizeof(數(shù)組) 總是返回整塊數(shù)組占據(jù)內(nèi)存的字節(jié)數(shù),而 sizeof(int) 就是每一個(gè)元素的所占字節(jié)數(shù),因此除法得到的結(jié)果就是總元素?cái)?shù)。

第三個(gè)問題。函數(shù)聲明是吧,抄一遍函數(shù)頭就可以了;當(dāng)然,也可以去掉參數(shù)名:

我相信你更喜歡少寫點(diǎn)字。但是后面這種就不好看了,因?yàn)?(*) 初學(xué)就是理解不了這個(gè)寫法;特別是一個(gè)星號(hào)括起來后還有倆 int 小括號(hào)括起來。

那么整體代碼就比較好理解了:我們用參數(shù)表示一個(gè)比較函數(shù),它專門表示我們到底怎么在冒泡排序法里比較兩個(gè)數(shù)據(jù)的。這個(gè)被指向函數(shù)(Function Pointee)里直接是兩數(shù)相減,那么嵌入到冒泡排序法里就好比是把 arr[j]arr[j + 1] 作差,得到的結(jié)果如果大于等于 0,就交換這兩個(gè)數(shù)據(jù)。這不就是表示 arr[j] >= arr[j + 1] 的時(shí)候交換嗎?被指向函數(shù)還可以改成 return b - a;,這樣就表示結(jié)果反過來減,于是這里的比較就成了“當(dāng)左側(cè)的數(shù)比右側(cè)的數(shù)小的時(shí)候,交換變量”,那么冒泡排序法最終得到的序列就是降序的。因此,被指向函數(shù)的功能就顯得格外重要:我們不需要提供 bubbleSort 的實(shí)現(xiàn),而是通過函數(shù)聲明暴露給 C 語言使用的用戶,這樣用戶就可以在不知曉函數(shù)怎么實(shí)現(xiàn)的時(shí)候就可以更靈活地控制排序的邏輯,而得到靈活的排序結(jié)果,這也是一種良好的封裝過程。

Part 2 C# 的函數(shù)指針

C# 的函數(shù)指針由于會(huì)兼容 C 語言和 C++ 的函數(shù),因此會(huì)有托管函數(shù)托管方法,Managed Function)和非托管函數(shù)非托管方法,Unmanaged Function)的概念。

  • 托管函數(shù):函數(shù)由 C# 語法實(shí)現(xiàn),底層也是用的 C# 提供的 CLR 來完成的。

  • 非托管函數(shù):函數(shù)并不由 C# 實(shí)現(xiàn),它不受 C# 語法控制,而是通過 DLL 文件交互使用。

2-1 托管函數(shù)的函數(shù)指針

那么,我們先來說一下 C# 函數(shù)內(nèi)部的函數(shù)指針(托管函數(shù)的函數(shù)指針):

就是把 C 里的 int (*ptr)(int, int) 改成 delegate* managed<int, int, int>。先寫函數(shù)記號(hào) delegate 關(guān)鍵字,然后帶一個(gè)星號(hào)。這兩個(gè)東西是函數(shù)指針聲明的頭部,是固定不變的語法規(guī)則。接著,指針符號(hào)后面寫上 managed 關(guān)鍵字,這也是 C# 9 里提供的一個(gè)新關(guān)鍵字,它在這里的語義表示一個(gè)“托管函數(shù)”。然后使用委托的類似語法:尖括號(hào)里寫類型參數(shù)表列,最后一個(gè)類型參數(shù)是整個(gè)函數(shù)的返回值類型。如果一個(gè)函數(shù)沒有參數(shù),返回值為 void 就寫成 delegate* managed<void>,如果有多個(gè)參數(shù),就把參數(shù)挨個(gè)寫上去,然后返回值上追加一個(gè)類型參數(shù)在末尾就可以了。

另外,managed 默認(rèn)可以不寫,因?yàn)?C# 的函數(shù)指針默認(rèn)是指向托管函數(shù)的,于是,記號(hào)就簡(jiǎn)化成了 delegate*<int, int, int>。當(dāng)然,你得注意一下,函數(shù)指針是不安全的,所以需要先寫 unsafe 才能用。

接著,我們來說一下非托管函數(shù)是怎么用的。

如果你對(duì) C# 的交互性(也叫互操作性,Interoperability)不了解或不太了解的話,這一段文字我是講不明白的,因?yàn)橐膊恢匾?,因此你在看到這里的話,就可以撤退了,或者直接跳到結(jié)尾。

2-2 非托管函數(shù)的函數(shù)指針

首先我們來說一下函數(shù)調(diào)用約定(Calling Convention,注意這里是 Convention 不是 Conversion)的概念。函數(shù)在底層是有很多實(shí)現(xiàn)模式的,而一旦出現(xiàn)大量函數(shù)混用的話,C 語言、C++ 甚至 C# 這樣的編程語言就得考慮,如何管理這么多函數(shù);函數(shù)在調(diào)用的時(shí)候,應(yīng)如何遵守約定。

因?yàn)楹瘮?shù)可以通過指針進(jìn)行調(diào)用,那么問題來了:我們?cè)趺磸?C# 里找到 C 和 C++ 里寫的函數(shù),并調(diào)用它們呢?DLL 文件陳列了很多函數(shù)提供給大型項(xiàng)目使用,但函數(shù)的調(diào)用方式不同,就會(huì)意味著函數(shù)有不同的執(zhí)行辦法。

2-2-1 __cdecl

最基礎(chǔ)的就是 C 語言聲明的函數(shù)(C Declaration,簡(jiǎn)稱 Cdecl,在代碼里記作 __cdecl_cdecl),它在函數(shù)被得到調(diào)用的時(shí)候,從右往左反向壓棧參數(shù),并由調(diào)用方(Caller)清除被調(diào)用方(Callee)棧幀。這種機(jī)制好處就在于,變長(zhǎng)參數(shù)可以通過這種模式來實(shí)現(xiàn)。


為什么呢?因?yàn)樽冮L(zhǎng)參數(shù)需要借助一個(gè)叫 va_list、va_end 這樣的東西來輔助實(shí)現(xiàn)。如果函數(shù)執(zhí)行完畢就清棧的話,變長(zhǎng)參數(shù)由于存儲(chǔ)在堆內(nèi)存里,因而得不到內(nèi)存釋放,而且棧幀也被清除,所以就無法再找到它們的內(nèi)存空間了,這就是我們俗稱的內(nèi)存泄漏(Memory Leak)問題。

當(dāng)然,__cdecl 是 C 語言和 C++ 默認(rèn)的調(diào)用約定,因而可以省略;換句話說,缺省時(shí)默認(rèn)函數(shù)就是這種調(diào)用約定的。

2-2-2 __fastcall

這是一種函數(shù)調(diào)用模型,再來說一個(gè):__fastcall。這個(gè)和前文的調(diào)用模型有所不同的地方是,函數(shù)在執(zhí)行完畢后立馬清棧,然后才會(huì)返回到被調(diào)用方。顯然,變長(zhǎng)參數(shù)就無法使用這種調(diào)用模型了。而之所以名字帶 fast,是因?yàn)樗幸粋€(gè)寄存器存儲(chǔ)的處理:它會(huì)把參數(shù)表列從左往右看的前兩個(gè)不大于 4 字節(jié)(兩個(gè) DWORD)的參數(shù)直接丟進(jìn)寄存器 ECX 和 EDX 里。學(xué)過計(jì)算機(jī)組成原理的朋友們都知道,寄存器是 CPU 里的一個(gè)部件,它離 ALU(運(yùn)算器)最近,因此即存即取的操作使得運(yùn)算代碼會(huì)相當(dāng)快。但是,因?yàn)榧拇嫫骱苄?,因此不能放很多?shù)據(jù)進(jìn)去。

2-2-3 __stdcall

這個(gè)是專門用在 Win32 API 里的一種調(diào)用模型,就不介紹那么多了(它也是從右到左反向壓棧參數(shù))。

2-2-4 用法

常見的調(diào)用約定一共有 __cdecl、__fastcall__stdcall 這樣三種,C 語言里把這三個(gè)修飾符放在返回值和函數(shù)名中間,比如前面的 int __cdecl addRange 就是這樣的寫法。

2-2-5 函數(shù)指針和調(diào)用約定

下面說一下函數(shù)指針和這些調(diào)用約定怎么進(jìn)行混用。我們拿 C 語言一個(gè)常見的排序函數(shù) qsort 舉例。這個(gè)函數(shù)最適合這里解釋和介紹函數(shù)指針的內(nèi)容,因?yàn)樗牡谒膫€(gè)參數(shù)就是一個(gè)必須指向 __cdecl 這樣調(diào)用約定的函數(shù)指針。

我們先看看它在 C 語言里的聲明:

首先,我們要注意的是 size_t。這個(gè) size_t 是一個(gè)類型別名,在64位系統(tǒng)中為 long long unsigned int,非64位系統(tǒng)中為 long unsigned int。需要注意的是,C# 里的 int 和 C 的 long 一樣大;而 C 里的 int 是不定長(zhǎng)的,因而不能直接和 C# 的 int 進(jìn)行大小比較。那么,既然這么說了,那么 long long 就等價(jià)于 C# 的 long,而 unsigned 就是 C# 里類型的 u 前綴,故就是 ulong 類型;而同理可得,非 64 位系統(tǒng)則是 uint 了。是的,這是不定長(zhǎng)的數(shù)據(jù)表達(dá);當(dāng)然,C# 9 里提供了一種新類型 nuint 來專門表達(dá)這種平臺(tái)不定長(zhǎng)的數(shù)據(jù)類型,因此可以直接在 C# 代碼里體現(xiàn)和代換成 nuint

最后一個(gè)參數(shù) int (__cdecl *compare)(const void *, const void *) 就是我們熟知的函數(shù)指針了。它指向一個(gè)聲明格式是 int func(const void *a, const void *b) 的函數(shù),即帶兩個(gè) const void *,并返回 int 類型的函數(shù)。

稍微注意下,函數(shù)指針上有 __cdecl 修飾符,這表示,被指向函數(shù)必須用 __cdecl 這種調(diào)用約定才可以;換句話說,隨意一個(gè)函數(shù)傳過去都是不允許的。

問題來了。我們要想調(diào)用帶這個(gè)函數(shù)的 DLL 文件,我們寫到 C# 里必然是使用 extern 關(guān)鍵字修飾,并使用 DllImportAttribute 的。那么,我們?cè)趺磳懘a呢?

我們假設(shè) MSVCP60.dll 文件里包含這個(gè)函數(shù),我們?cè)趯?C# 代碼的時(shí)候就需要這么寫:

請(qǐng)注意第四個(gè)參數(shù)的格式。我們需要把前文講到的 managed 關(guān)鍵字替換為 unmanaged,因?yàn)檫@里的函數(shù)指針要契合 qsort 函數(shù)來排序,而 qsort 函數(shù)聲明里帶 __cdecl 修飾符,因而這個(gè)函數(shù)指針僅能使用到 __cdecl 這樣調(diào)用轉(zhuǎn)換的函數(shù)上。在寫成 C# 的時(shí)候,這個(gè)函數(shù)指針需要寫成 unmanaged[Cdecl]Cdecl 是 C# 里一個(gè)叫做 CallConvCdeclAttribute 的特性的簡(jiǎn)寫。因?yàn)槊诌^長(zhǎng),所以 C# 就讓你去掉 CallConv 前綴和 Attribute 后綴,最后就只保留了 Cdecl 這一截。所以,你不必問我為什么 __cdecl 在 C# 里寫成 Cdecl 了,因?yàn)檫@就是原因。

另外,函數(shù)除了函數(shù)指針這個(gè)參數(shù)用了 Cdecl 修飾外,函數(shù)本身也是 C 語言里的庫函數(shù),因此它本身也是滿足 __cdecl 調(diào)用轉(zhuǎn)換模型的,不過這里不是函數(shù)指針,因此沒有前文那樣的語法。這里因?yàn)樾枰?DLL 文件,因此有了 DllImport 特性的修飾。首先,第一個(gè)參數(shù)傳入的是文件名。前文假設(shè)是 MSVCP60.dll 文件里包含這個(gè)函數(shù),因此這里寫 MSVCP60.dll 或者 MSVCP60。接著,調(diào)用約定需要手動(dòng)指明,因?yàn)椴徽f的話,C# 不知道是什么調(diào)用約定,這里的調(diào)用約定是用的枚舉,因此寫成 CallingConvention.Cdecl 的格式。

接著,我們使用這個(gè)函數(shù)。說一下四個(gè)參數(shù)的作用。第一個(gè)參數(shù)就是指向的數(shù)組,因?yàn)?qsort 是支持任意數(shù)據(jù)類型的數(shù)據(jù)參與比較的(比較的操作就從第四個(gè)參數(shù)來指明),所以是 void* 類型;第二個(gè)參數(shù)表示數(shù)據(jù)有多少個(gè);第三個(gè)參數(shù)表示數(shù)據(jù)的每一個(gè)元素都占據(jù)多少個(gè)內(nèi)存空間大小。這里剛好可以用 C 語言里的 sizeof 來表示,所以不用擔(dān)心怎么手動(dòng)計(jì)算;最后一個(gè)參數(shù)前文說了,指明比較到底是怎么操作的。

說明完畢后,我們可以開始調(diào)用這個(gè)函數(shù)。

首先,我們定義一個(gè)需要排序的數(shù)組,然后,使用 fixed 語句固定數(shù)組,將數(shù)組轉(zhuǎn)成地址形式表示,然后傳入函數(shù)指針 p。

這個(gè) p 的類型有點(diǎn)長(zhǎng),但是看得出來它就是一個(gè)函數(shù)指針。需要注意的是,整體函數(shù)指針類型不需要用 unmanaged 修飾符修飾,因?yàn)樗F(xiàn)在已經(jīng)寫在 C# 里面了,即使寫了調(diào)用約定,也是如此,因此聲明并非是

而是

然后,我們?cè)谧钕路蕉x比較函數(shù) cmp,函數(shù)專門用來比較兩個(gè)數(shù)字的大小。先將數(shù)字轉(zhuǎn)成普通類型(因?yàn)楝F(xiàn)在還是 void*,無法參與比較),然后再相減。差值大于 0,則表示左邊比右邊大;差值小于 0,則表達(dá)右邊比左邊大;如相同,就表示兩個(gè)數(shù)一樣大。

最后,我們傳參的時(shí)候,把 cmp 傳過去就可以了。

不過,這樣會(huì)報(bào)錯(cuò),提示函數(shù) cmp 不兼容聲明的 qsort 的函數(shù)指針參數(shù)。這是為啥呢?我們沒設(shè)定 cmp 函數(shù)的調(diào)用約定。是的,這個(gè)函數(shù)僅給非托管函數(shù) qsort 用,所以我們需要先標(biāo)記一個(gè) C# 9 里帶來的新 API:UnmanagedCallersOnlyAttribute。這個(gè)特性標(biāo)記出來有兩個(gè)目的:

  • 告訴編譯器,這個(gè)函數(shù)僅提供給非托管函數(shù)調(diào)用;

  • 告訴編譯器,這個(gè)函數(shù)的調(diào)用約定是 __cdecl 模式的。

是的,我們?cè)谠撎匦陨现付ㄕ{(diào)用約定 Cdecl,怎么指定呢?寫上 CallConvs = new[] { typeof(CallConvCdecl) } 就可以了。賦值方寫的是調(diào)用約定的模型在 C# 里規(guī)定的寫法。這里傳入 typeof(CallConvCdecl) 表示我這里是用的 Cdecl 模式的調(diào)用約定,而我們無法直接傳入類名當(dāng)參數(shù),所以這里用到了 typeof 關(guān)鍵字。

寫上這個(gè)之后,編譯器也知道這個(gè)函數(shù)無法隨便用了,因此這樣一來,我們就可以成功傳參到上面的函數(shù)里當(dāng)函數(shù)指針了,這一次就沒有編譯器錯(cuò)誤了。

另外,我們其實(shí)都知道,這個(gè)根據(jù)調(diào)用平臺(tái)來確定函數(shù)的調(diào)用約定,因此我們其實(shí)可以省略這里的 __cdecl,所以,代碼整體這么寫其實(shí)也沒問題:

注意去掉的三處地方。第一個(gè)是函數(shù)指針 p 的類型,第四個(gè)參數(shù)的 [Cdecl] 沒了;第二個(gè)是 [UnmanagedCallersOnly]CallConv 屬性賦值表達(dá)式?jīng)]了;第三個(gè)是下方函數(shù) qsort 聲明的第四個(gè)參數(shù)里,[Cdecl] 沒了。去掉這些,編譯器就會(huì)自動(dòng)檢測(cè)和確定,這里 qsort 屬于什么平臺(tái),用什么平臺(tái)下的默認(rèn)調(diào)用轉(zhuǎn)換了。之前就說過,C 語言默認(rèn)的函數(shù)就是 __cdecl 的,所以我們就不必寫這些東西了,缺省就是 __cdecl

2-2-6 啰嗦一下 UnmanagedCallersOnlyAttribute

這個(gè)特性前文已經(jīng)說到了它的標(biāo)記目的,那么下面來說一下這個(gè)特性到底有哪些需要注意的地方。

第一,被標(biāo)記方法只能是 static。思考一點(diǎn),要想用函數(shù)指針,就必須方法能夠兼容底層 C/C++ 的代碼。顯然,C 是沒有實(shí)例方法這種概念的,這是面向?qū)ο蟮臇|西。雖然 C++ 里有此概念,但它的實(shí)現(xiàn)和 C# 的依舊不同。如果我們?cè)试S實(shí)例方法作為函數(shù)指針使用的話,這必然會(huì)帶來調(diào)用和兼容的問題。因此,C# 目前限制函數(shù)指針僅用于靜態(tài)方法。

第二,被標(biāo)記函數(shù)的返回值和參數(shù)類型都必須是 C/C++ 里原生支持的基本數(shù)據(jù)類型。換句話說,你不能使用 C# 里定義的數(shù)據(jù)類型作為這個(gè)函數(shù)的參數(shù),這樣 C/C++ 找不到這個(gè)玩意兒,于是就沒辦法兼容。另外,我們可以把這一點(diǎn)簡(jiǎn)化一下說法。C/C++ 底層支持的這些基本數(shù)據(jù)類型因?yàn)槭?/span>由機(jī)器類型來確定的(Platform-specific),因此也被稱為本機(jī)類型(Blittable Type);當(dāng)然,C# 里自定義的結(jié)構(gòu)體啊、類啊、接口之類的東西就稱為非本機(jī)類型(Non-blittable Type)了。

第三,不能手動(dòng)調(diào)用這些函數(shù)。這顯然是廢話,因?yàn)檫@個(gè)方法專用于底層交互,自然就不能允許我們?cè)?C# 平臺(tái)里使用了。

Part 3 總結(jié)

歡迎你來到本文的最后一節(jié)。前文說的東西有些亂,我之后錄視頻可能還會(huì)講一遍。

實(shí)際上,總結(jié)我也不知道寫些什么,至少你得懂,函數(shù)指針是什么玩意兒。

參考資料

[1] https://devblogs.microsoft.com/dotnet/improvements-in-native-code-interop-in-net-5-0/

[2] https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-5.0

[3] https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types


探索 C# 9 的函數(shù)指針的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國家法律
永丰县| 湖北省| 天柱县| 黔南| 永宁县| 新乐市| 宿松县| 凌源市| 怀柔区| 遵义县| 东辽县| 永平县| 青神县| 重庆市| 沂源县| 吉林省| 株洲县| 平利县| 新建县| 鹤峰县| 临猗县| 南平市| 南雄市| 桐庐县| 门头沟区| 巨鹿县| 辽阳市| 碌曲县| 延寿县| 大安市| 嘉祥县| 浦江县| 遵化市| 来安县| 若羌县| 仙游县| 习水县| 河北区| 伊金霍洛旗| 镇远县| 永春县|