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

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

第 58 講:反射(二):特性

2021-09-14 09:05 作者:SunnieShine  | 我要投稿

C# 在早期就擁有一個(gè)特別有趣的知識(shí)點(diǎn),而這個(gè)在 Java 早期是沒(méi)有的。正是因?yàn)槿绱耍珻# 的優(yōu)勢(shì)在此時(shí)得以體現(xiàn)。這個(gè)東西叫做特性(Attribute)。

這個(gè) attribute 單詞的意思其實(shí)是“屬性”、“特征”。不過(guò) C# 已經(jīng)擁有了一個(gè)翻譯成屬性的概念 property,它是面向?qū)ο罄锏钠渲幸环N成員;而這里的 attribute 和那個(gè)屬性完全不同,但在你看了我后面的內(nèi)容后就會(huì)發(fā)現(xiàn)它其實(shí)是另外一種維度上的屬性信息,所以叫它“屬性”也沒(méi)問(wèn)題。但再次翻譯成“屬性”會(huì)導(dǎo)致無(wú)法區(qū)別開(kāi),所以我們特此把這個(gè)概念翻譯成特性,而原來(lái)的那個(gè)屬性就叫它屬性就行。

Part 1 特性的基本用法

下面內(nèi)容我使用倒敘來(lái)介紹,先說(shuō)一下基本用法讓你曉得基本用法,然后我們?cè)賮?lái)針對(duì)于用法細(xì)節(jié)給大家介紹它的使用和處理機(jī)制。

考慮一種情況。如果我寫(xiě)了一個(gè)屬性已經(jīng)不想再使用了,但是你現(xiàn)在在維護(hù) API,不敢輕易刪除,畢竟有人在使用你的這套 API。這個(gè)時(shí)候,我想規(guī)范化使用讓別人使用這套 API 的人轉(zhuǎn)去使用別的成員。

假設(shè)我現(xiàn)在實(shí)現(xiàn)了這樣的代碼。其中的 ScoreAverageScore 是兩個(gè)實(shí)現(xiàn)效果一樣的屬性。假設(shè)這個(gè) Score 是你之前寫(xiě)的,而這個(gè)屬性因?yàn)槊譀](méi)取好所以我想讓別的用戶(hù)別再使用這個(gè)屬性了。于是,我可以這么加一行代碼:

也就是說(shuō)我們直接在原來(lái)第 5 行代碼的上方加一對(duì)中括號(hào),里面寫(xiě)上 Obsolete(..., false) 一樣的東西。乍一看這不是跟構(gòu)造器寫(xiě)法差不多嗎?是的,稍后我們會(huì)介紹它的詳細(xì)語(yǔ)法的細(xì)節(jié)。

接著,添加這一行的用途是什么呢?實(shí)際上它暫時(shí)對(duì)你的代碼本身沒(méi)有任何影響,它實(shí)際上在書(shū)寫(xiě)代碼的別處上有一些你暫時(shí)無(wú)法察覺(jué)的影響。

假設(shè)我現(xiàn)在真的有一個(gè)實(shí)例是 Person 類(lèi)型的,然后我嘗試去調(diào)用這兩個(gè)屬性,然后你就會(huì)發(fā)現(xiàn),編譯器居然會(huì)對(duì)剛才我們標(biāo)記了 Obsolete 的這個(gè)屬性 Score 報(bào)警告信息。這就很神奇了朋友們。

而且,奇怪的現(xiàn)象不只是這一點(diǎn)點(diǎn)。你把鼠標(biāo)懸停在 p.Score 上,可以看到錯(cuò)誤信息:

報(bào)錯(cuò)信息翻譯過(guò)來(lái)大概是說(shuō)“Person.Score 屬性已經(jīng)過(guò)時(shí)。具體的錯(cuò)誤信息是‘請(qǐng)使用屬性 AverageScore 來(lái)代替’?!?。這個(gè)英語(yǔ)句子的后半截可以明顯發(fā)現(xiàn)到,它其實(shí)就是我們?cè)?Obsolete 里寫(xiě)的那一句話。完全是一樣的、照搬過(guò)來(lái)的。而此時(shí)我們?cè)俅伟咽髽?biāo)切換移動(dòng)到下面的 ScoreAverageScore 上的時(shí)候,你會(huì)發(fā)現(xiàn)如下的信息:

AverageScore 沒(méi)有 Obsolete 這句話,所以

可以看到,標(biāo)記 Obsolete 內(nèi)容的屬性,在彈窗提示的小貼士文字里多了一個(gè)中括號(hào),寫(xiě)了一個(gè)單詞叫 deprecated;而沒(méi)有 Obsolete 的,則沒(méi)有這一坨東西。

這便是這段”標(biāo)記“在 Score 上方的、Obsolete 的東西的作用:影響編譯器分析代碼的行為。

眾多特性都具有這樣的行為,所以特性比較重要的其中一個(gè)用法就是用來(lái)標(biāo)記一個(gè)東西,然后使得編譯器在分析代碼的時(shí)候能夠幫助和引導(dǎo)我們使用正確的書(shū)寫(xiě)方式和格式,另一方面是順帶也影響編譯器本身執(zhí)行一些代碼的分析行為。

特性的基本語(yǔ)法格式是這樣的:

乍一看其實(shí)跟我們實(shí)例化的寫(xiě)法也差不多,只是把 new 換成了特性名,并且在整個(gè)這一坨的兩側(cè)用了一對(duì)中括號(hào)包裹起來(lái)了。是的,它就是這么寫(xiě)的。

不過(guò),因?yàn)橄袷?Obsolete 這樣的單詞憑空你是想不出來(lái)的,所以它其實(shí)是和之前學(xué)過(guò)的異常類(lèi)型基本是一個(gè)套路,用到一個(gè)就記住一個(gè)。

Part 2 定義一個(gè)特性

顯然,這種特性是無(wú)法學(xué)完的,所以我們用一個(gè)說(shuō)一個(gè)。下面我們來(lái)介紹如何創(chuàng)建自己定義的特性,以及它都什么地方用。假設(shè),我要遍歷一個(gè)類(lèi)型里的所有屬性成員,顯然我們可以使用前文介紹的 typeof 表達(dá)式,以及 GetProperties 來(lái)得到。不過(guò),如果我不想全都取出來(lái),而是按照我們自己定義的規(guī)則篩選掉我們不需要的屬性的話,那么我們就得借助特性來(lái)給屬性”打標(biāo)記“了。

假設(shè)我們現(xiàn)在定義一個(gè)特性,叫做 OnlyContainsGetter

然后,我們對(duì)前面給出的”只帶 get 方法的屬性都標(biāo)上這個(gè)特性“。

稍后我們會(huì)對(duì)特性的具體內(nèi)容(AttributeUsage 特性、標(biāo)記特性上去后為啥小括號(hào)沒(méi)了)作說(shuō)明。

接著,因?yàn)檫@三個(gè)屬性都標(biāo)記了這個(gè)特性,于是我們可以通過(guò)反射就可以把這三個(gè)標(biāo)記了 OnlyContainsGetter 特性的屬性給提出來(lái)。假設(shè)我們要找的就是它們的話:

注意這里我們用到了一個(gè)叫 Attribute 類(lèi)型里的 IsDefined 這個(gè)靜態(tài)方法。第一個(gè)參數(shù)傳入的是我們剛才通過(guò) GetProperties 方法得到的每一個(gè)元素,第二個(gè)參數(shù)則傳入的是你想要看是否標(biāo)記上去的那個(gè)特性的類(lèi)型數(shù)據(jù)本身。這里我們要用到的是 typeof 表達(dá)式。因?yàn)橐@得比較的數(shù)據(jù)信息,我們不得不使用 typeof 表達(dá)式才能得到一個(gè)類(lèi)型的基本信息,這樣才可以參與比較和數(shù)據(jù)處理。因?yàn)槲覀兌贾溃恢故?C 語(yǔ)言、C++ 還有 Java,大家都是不能直接把類(lèi)型自己當(dāng)成參數(shù)傳入的。

這樣的話,我們看到這個(gè)簡(jiǎn)略而又再正常不過(guò)的寫(xiě)法(一個(gè) foreach 里帶一個(gè) if 判斷一下即可),得到的就是我們要的、標(biāo)記了 OnlyContainsGetter 特性的所有屬性了。接著我們輸出它的屬性名本身。這樣你可以得到如下的結(jié)果:

這就是自定義特性的用法。

Part 3 再探特性的語(yǔ)法格式以及內(nèi)部處理機(jī)制

既然 OnlyContainsGetter 是一個(gè)特性,那么它為什么可以被定義為是一個(gè)特性呢?它的書(shū)寫(xiě)代碼不是跟普通的類(lèi)的聲明是一樣的嗎?為什么我試著把普通的類(lèi)用中括號(hào)給標(biāo)記在成員上不行而這個(gè) OnlyContainsGetter 可以呢?下面我們來(lái)說(shuō)一下特性的基本處理和使用機(jī)制。

一個(gè)特性需要滿足一個(gè)固有條件:它得是一個(gè)從 Attribute 這個(gè)抽象類(lèi)派生出來(lái)的非抽象類(lèi)類(lèi)型。雖然就只有這一個(gè)條件,但是細(xì)節(jié)很多,首先它得是一個(gè)類(lèi),然后還得是非抽象的,還必須從 Attribute 類(lèi)型派生。實(shí)際上,在 C# 的基本使用過(guò)程上來(lái)說(shuō),特性的底層 100% 都被微軟大大給實(shí)現(xiàn)和搞定了,所以如果你需要讓一個(gè)普通類(lèi)型”改造“成一個(gè)特性類(lèi)型的話,只需要把基類(lèi)型改成 Attribute 即可。

里面呢?什么都不必寫(xiě)。一個(gè)超級(jí)簡(jiǎn)單的類(lèi)的聲明:有派生關(guān)系,有 class 關(guān)鍵字,有一對(duì)大括號(hào)。是的,就這樣就完事了。那么這個(gè) A 因?yàn)闆](méi)有 abstract 關(guān)鍵字修飾,所以它就可以用來(lái)標(biāo)記了。此時(shí) A 是一個(gè)沒(méi)有任何限制的特性。所謂的”沒(méi)有限制“,是它基本上可以標(biāo)記在任何成員上。屬性、字段、方法、事件、運(yùn)算符,甚至是類(lèi)型本身上面。

這些都可以。

稍微說(shuō)明一下。如果特性里不帶構(gòu)造器的話,因?yàn)槭菬o(wú)參構(gòu)造器,因此這樣的特性在標(biāo)記的時(shí)候是不需要寫(xiě)出這對(duì)小括號(hào)的(當(dāng)然你寫(xiě)出來(lái) A() 這樣的語(yǔ)法也可以)。

嗯,你可能會(huì)問(wèn),第 14 行里,A 屬性和 A 特性都是用的字母 A,這樣不會(huì)沖突嗎?實(shí)際上不會(huì)。大家都知道屬性肯定是跟特性是兩回事。編譯器實(shí)際上也能完全區(qū)分開(kāi),所以你這么取名也不會(huì)引起編譯器誤判。

不過(guò),特性只能作用到成員或者類(lèi)型上,所以臨時(shí)變量是不可以的:

這樣是不可以的。按照微軟的 C# 語(yǔ)言設(shè)計(jì)的團(tuán)隊(duì)的話來(lái)說(shuō),之所以不能,是因?yàn)榕R時(shí)變量能夠使用和作用的范圍太小了:它只能在方法里面使用。出了方法,這個(gè)臨時(shí)變量就什么都不是了。因?yàn)橥獠课覀儫o(wú)法訪問(wèn)它。臨時(shí)變量的使用頻次高,但生存時(shí)間短(只在方法里出現(xiàn),用完就丟,所以”生存“的時(shí)間并不長(zhǎng)),因此對(duì)這種東西標(biāo)記特性是沒(méi)有里程碑式的意義的。確實(shí),至少現(xiàn)在我們都沒(méi)有理由說(shuō)服 C# 團(tuán)隊(duì)。

因?yàn)樗仨殢?Attribute 類(lèi)型派生,所以我們可以繼續(xù)拓展 C# 數(shù)據(jù)類(lèi)型體系的拓?fù)鋱D:

至此,所有的數(shù)據(jù)類(lèi)型的繼承關(guān)系都給大家展示到了。Attribute 被加在了最右邊。

順帶我加上了指針類(lèi)型。

3-1 AttributeTargets 枚舉

不過(guò),一個(gè)特性完全實(shí)現(xiàn)了之后,我們還得有一個(gè)防御措施。萬(wàn)一別人誤用了咋辦。確實(shí) A 這個(gè)特性奏效了,可以用了,但是特性可以標(biāo)注在所有成員上面,這是不是有點(diǎn)太過(guò)于奇怪了?所以,我們需要給特性加上一些限制措施。

做法是在 A 類(lèi)型的定義上追加一個(gè)叫做 AttributeUsage 的特性。我們把前文 OnlyContainsGetter 特性上面標(biāo)記的特性這部分單獨(dú)提出來(lái)給大家說(shuō)明一下細(xì)節(jié)。

首先,特性名叫 AttributeUsage。然后傳入了三個(gè)參數(shù)(因?yàn)橛袃蓚€(gè)逗號(hào),所以是三個(gè)數(shù)據(jù))。第一個(gè)參數(shù)傳入的是一個(gè)枚舉類(lèi)型,它指定我們這個(gè) OnlyContainsGetter 特性只能標(biāo)記到屬性上。一般來(lái)說(shuō),默認(rèn)情況下,這里第一個(gè)參數(shù)相當(dāng)于傳入的是一個(gè)叫做 AttributeTargets.All 的枚舉字段。它表示所有成員(或類(lèi)型)都可以使用此特性。

這里有你想要設(shè)置的全部成員。不止如此,它還有別的一些設(shè)置項(xiàng),比如 AssemblyModule 等等,一會(huì)兒我們?cè)賮?lái)介紹;就算是類(lèi)型,這個(gè)枚舉也是分開(kāi)的,它包含 Class、Struct、Interface、Delegate 四種不同的情況。而且它還能用在方法的參數(shù)和返回值上。你說(shuō)神奇不神奇。所以,你不限制這個(gè)參數(shù)的話,特性啥地方都可以用,顯然很多地方就先得毫無(wú)意義。因此,限制這種東西是有意義的。

另外,單獨(dú)一個(gè)明顯是不夠的。假設(shè)我想要讓這個(gè)特性可標(biāo)記在所有的類(lèi)型上的話,我們可使用 | 來(lái)疊加枚舉字段。不知道你還記不記得我之前講枚舉的時(shí)候說(shuō)過(guò)一個(gè) flag 的概念。這個(gè)枚舉就是遵守了這個(gè)概念,因而允許你這么使用。

有點(diǎn)長(zhǎng)……將就看吧。意思是這個(gè)意思。因?yàn)閷?xiě)不下了,估計(jì)你看文檔也得往后翻,所以我換行寫(xiě)了。C# 編譯器就是好,允許我們換行書(shū)寫(xiě)。

3-2 Inherited 命名參數(shù)

這個(gè)語(yǔ)法挺新鮮的。之前的方法的參數(shù)好像沒(méi)有這種用 屬性 = 數(shù)值 的格式書(shū)寫(xiě)的。這是特性的獨(dú)特的賦值語(yǔ)法,叫做特性命名參數(shù)(Named Parameter in Attributes)。特性命名參數(shù)這個(gè)詞有點(diǎn)長(zhǎng),它其實(shí)指的就是在特性里特有的命名參數(shù)的機(jī)制。命名參數(shù)的”命名“一詞表示可直接給特性類(lèi)型里固定的參數(shù)直接賦值的過(guò)程時(shí),必須要指定清楚到底賦值給誰(shuí)的意思,比如 Inherited 這個(gè)參數(shù)名其實(shí)在 AttributeUsage 這個(gè)特性里有一個(gè)如此的屬性。是的,這確實(shí)是一個(gè)屬性:

這便是 C# 特性的另外一個(gè)機(jī)制。如果這個(gè)特性類(lèi)型里包含 getset 方法都有的屬性,那么這個(gè)屬性可以充當(dāng)特性命名參數(shù)使用。

那么,Inherited 屬性管什么呢?這個(gè)屬性是表示,是否我這個(gè)特性可以提供給成員的派生成員(重寫(xiě)過(guò)的),或者是類(lèi)型的派生類(lèi)型復(fù)制一份。

假設(shè)我現(xiàn)在有兩個(gè)特性,一個(gè)叫 Inherited,有一個(gè)叫 NotInherited

然后,我這么寫(xiě)了代碼使用這些特性:

其中,BaseADeivedA 是一組,BaseBDerivedB 是一組。請(qǐng)注意,在基類(lèi)型 BaseABaseB 上我們分別標(biāo)記了 Inherited = trueInherited 特性和 Inherited = falseNotInherited 特性。

標(biāo)記了 Inherited = true 屬性的 Inherited 特性的類(lèi)型 BaseA 會(huì)有一個(gè)奇特的現(xiàn)象:就是派生類(lèi)型即使不標(biāo)記這個(gè)特性,也會(huì)自動(dòng)包含這個(gè)特性的實(shí)例作副本。也就是說(shuō),你不標(biāo)記到 DerivedA 上面,也會(huì)自動(dòng)帶有 [Inherited] 標(biāo)記在 DerivedA 上;但是,NotInherited 這個(gè)特性不會(huì)。這就是這個(gè) Inherited 屬性的用法。

注意一定分清楚我說(shuō)的是 Inherited 屬性,還是 Inherited 特性。

3-3 AllowMultiple 命名參數(shù)

這個(gè)命名參數(shù)解釋起來(lái)就相當(dāng)簡(jiǎn)單了。如果這個(gè)數(shù)值為 true,那么這個(gè)特性就可以在同一個(gè)位置上多次使用;如果為 false 就不行。

比如前文我們的 AllowMultiplefalse,那么就不能這么寫(xiě)代碼:

甚至更多 [A] 的疊加。但是,如果 AllowMultiple 屬性為 true,那么疊加就是可以的。

和前文的 Inherited 命名參數(shù)的機(jī)制完全一致,AllowMultiple 也是 AttributeUsage 特性里的一個(gè)固有屬性,它也同時(shí)包含 getset 方法,因而滿足特性命名參數(shù)的基本規(guī)則,因此可以 AllowMultiple = 數(shù)值 地這么書(shū)寫(xiě)代碼。

Part 4 普通參數(shù)的傳參

前面我們介紹到了命名參數(shù)的特有傳參方式,下面我們來(lái)說(shuō)一下如何直接使用普通參數(shù)傳參的方式。這個(gè)其實(shí)和普通類(lèi)型的 new 是完全一樣的。

假設(shè)我現(xiàn)在有這么一個(gè)特性。那么,標(biāo)記的時(shí)候,我們直接傳參即可:

比如這樣就可以。

唯一需要注意一個(gè)規(guī)范是,特性命名參數(shù)必須出現(xiàn)在普通參數(shù)傳參完畢之后。也就是說(shuō),普通參數(shù)必須先寫(xiě),而且寫(xiě)完了才能寫(xiě)命名參數(shù)(如果需要的話)。命名參數(shù)在語(yǔ)法設(shè)計(jì)上,是可有可無(wú)的一種機(jī)制,如果它沒(méi)有也可以。但普通參數(shù)是必須有的。所以,命名參數(shù)必須最后寫(xiě),防止編譯器識(shí)別分析不出到底哪個(gè)對(duì)應(yīng)哪個(gè)參數(shù)。

Part 5 特性的實(shí)例

特性不只是從語(yǔ)法上帶一對(duì)中括號(hào)那么簡(jiǎn)單。在底層上也是有實(shí)質(zhì)性的含義和存在的意義的。不然,為什么我們前面用 IsDefined 還能查找這些寫(xiě)法呢?是吧。

C# 的特性一旦標(biāo)記上去,就等于是產(chǎn)生了一個(gè)同樣類(lèi)型的實(shí)例。比如前面的 AttributeUsage 的標(biāo)記,它在底層是真的有一個(gè) AttributeUsage 的實(shí)例在里面。

如何獲取這個(gè)標(biāo)記呢?我們拿 OnlyContainsGetter 特性舉例。

假設(shè)這個(gè)是 Person 類(lèi)型里的一個(gè)屬性。我們獲取這個(gè)屬性的方式是這樣的:

是吧。獲取上面標(biāo)記的特性,我們是這么做的:

比如上面這樣就可以。我們調(diào)用一個(gè)叫 Attribute.GetCustomAttribute 的靜態(tài)方法來(lái)獲取 pi 這個(gè)屬性信息實(shí)體類(lèi)指向的 AverageScore 屬性上是不是標(biāo)記了 OnlyContainsGetter 特性的實(shí)例。如果有,這個(gè)方法必然就會(huì)返回這個(gè)實(shí)例類(lèi)型。不過(guò)因?yàn)閷?shí)例的類(lèi)型是方法 API 本身無(wú)法直接確定的,所以它返回的是 Attribute 類(lèi)型而不是 OnlyContainsGetter 類(lèi)型。因此,即使你知道它一定不是 null,也要記得強(qiáng)制轉(zhuǎn)換或使用 as 運(yùn)算符。

接著,我們后面跟著一個(gè) if 判斷 attr 變量是不是 null。只要它不是 null,那么就說(shuō)明獲取成功了。

再來(lái)看一個(gè)帶參數(shù)的例子。

我們假設(shè)給 AverageScore 指定 A 特性,并給 Prop 屬性賦值 42。接著,我們獲取這個(gè)數(shù)值:

因?yàn)檗D(zhuǎn)換是成功的,所以 attr 必然是 A 類(lèi)型的實(shí)例,因此 if 條件結(jié)果為 true,則會(huì)遇到 Console.WriteLine 打印出 Prop 的結(jié)果 42。

Part 6 特性目標(biāo)

6-1 適用于返回值和參數(shù)的特性的語(yǔ)法

在 C# 里你甚至可以把特性用于參數(shù)和返回值上。那么怎么書(shū)寫(xiě)代碼呢?

倘若我們適用于參數(shù)上,我們的書(shū)寫(xiě)方式是這樣的:

直接在 object o 這個(gè)聲明的前面帶上特性標(biāo)記就可以了。

不過(guò)返回值稍微麻煩一點(diǎn)。返回值并不是標(biāo)記在返回值類(lèi)型名的左側(cè)的,而是寫(xiě)在方法上:

注意語(yǔ)法 [return: NotNull]。這里我們就會(huì)給大家介紹一個(gè)新的概念:特性目標(biāo)(Attribute Target)。

6-2 特性目標(biāo)的概念

在有些時(shí)候,因?yàn)闀?shū)寫(xiě)代碼的方便,以及消除歧義,C# 使用特性目標(biāo)的語(yǔ)法來(lái)約定特性作用到什么東西上。它的語(yǔ)法其實(shí)就是在中括號(hào)的開(kāi)頭帶上作用的對(duì)象。

一般情況下,我們都是可以不用寫(xiě)出來(lái)的;只是像是 [return: A] 這樣的語(yǔ)法,如果不寫(xiě) return 的話,就不知道到底特性作用于返回值還是方法本身了,因?yàn)樗鼈兌紝?xiě)在了一樣的位置。

比如這樣寫(xiě)的話,[A] 作用在方法上,而 [return: A] 作用于返回值上。

稍微注意一下。這些單詞都不是關(guān)鍵字,它僅在特性目標(biāo)的語(yǔ)法里才會(huì)起作用,其它的任何地方都可以用它作為標(biāo)識(shí)符的,它并不會(huì)占用關(guān)鍵字的位置,比如

即使你寫(xiě)了一個(gè) int property = 42; 也不會(huì)影響。

Part 7 其它問(wèn)題

7-1 特性支持的數(shù)據(jù)類(lèi)型

有沒(méi)有考慮這種問(wèn)題:

假設(shè)有這么一個(gè)特性類(lèi)型 A,不過(guò)我在構(gòu)造器里要求傳入一個(gè) B 類(lèi)型的實(shí)例過(guò)去。

然后,難不成特性標(biāo)記要這么寫(xiě):

那么這樣是可行的嗎?實(shí)際上,這是不允許的。C# 規(guī)定,特性里不論是命名參數(shù)還是構(gòu)造器的普通參數(shù),都只能出現(xiàn)如下的這些數(shù)據(jù)類(lèi)型:

  • 基本內(nèi)置數(shù)據(jù)類(lèi)型:

    • 整數(shù):bytesbyte、ushort、short、uint、int、ulonglong;

    • 浮點(diǎn)數(shù):floatdouble、decimal

    • 字符和字符串:char、string;

    • 布爾:bool

    • object 類(lèi)型。

  • 自定義的枚舉類(lèi)型;

  • Type 類(lèi)型的實(shí)例(即直接用 typeof 表達(dá)式獲取到的數(shù)據(jù)的數(shù)據(jù)類(lèi)型);

  • 上述兩種可能情況的對(duì)應(yīng)一維數(shù)組類(lèi)型。

只有這些數(shù)據(jù)類(lèi)型才可以充當(dāng)命名參數(shù)和構(gòu)造器普通參數(shù)的數(shù)據(jù)類(lèi)型。只要不屬于這里面的,都不允許。而且,在基于這些數(shù)據(jù)類(lèi)型的情況下,你在特性傳參的過(guò)程里,只能使用常量(包括靜態(tài)只讀量都不行,只能是常量),比如 int 類(lèi)型的就得接收 int 類(lèi)型的常量,比如 43 這種,或者是某個(gè)類(lèi)型里的常量的引用(AClassType.Constant 這樣的方式);如果是 double 類(lèi)型就用比如 30.5 這樣的常量來(lái)書(shū)寫(xiě);而 string 類(lèi)型就只能是字符串常量或者是 null 了;如果是一維數(shù)組,就只能是 new 類(lèi)型[] { 成員 } 了。

條件相當(dāng)嚴(yán)苛,對(duì)吧。但是你想沒(méi)想過(guò)這種限定的原因是為什么。因?yàn)?,這些數(shù)據(jù)是寫(xiě)死到程序里的。一旦它們實(shí)例化成功,那么它們自動(dòng)被存儲(chǔ)到一個(gè)不是堆內(nèi)存和棧內(nèi)存的一個(gè)極為特殊的存儲(chǔ)區(qū)域下。這種特殊的存儲(chǔ)區(qū)域是只容納特性實(shí)例的數(shù)據(jù)的,它們被我們稱(chēng)為元數(shù)據(jù)(Metadata)。元數(shù)據(jù)我們稍后作詳細(xì)解釋?zhuān)@里你先知道有這么一個(gè)玩意兒就可以了。

先思考一下,為什么那些引用類(lèi)型不允許被當(dāng)成特性的參數(shù)類(lèi)型呢?是不是因?yàn)樗鼈兓蚨嗷蛏俣紩?huì)和棧內(nèi)存和堆內(nèi)存扯上關(guān)系啊。棧內(nèi)存受到方法進(jìn)出而自動(dòng)分配和釋放內(nèi)存,而堆內(nèi)存則是受到 GC 釋放。如果丟進(jìn)這里面的任何一個(gè)地方去,是不是都可能被釋放掉,導(dǎo)致程序不安全甚至出現(xiàn)嚴(yán)重的 bug。而且它們對(duì)于我們系統(tǒng)來(lái)說(shuō)算是非常重要的一些數(shù)據(jù),因此它們一旦被初始化后就不容修改,干脆就丟進(jìn)了一個(gè)單獨(dú)的內(nèi)存區(qū)域里管轄,這就要求數(shù)據(jù)只能是非常嚴(yán)苛的條件下滿足的那些數(shù)據(jù)類(lèi)型才行。首先,那些系統(tǒng)自帶的類(lèi)型自己就是一個(gè)特殊的存在,它們本身不容別的數(shù)據(jù)類(lèi)型表達(dá)出來(lái):它們自己就可以表示自己,而別的所有數(shù)據(jù)類(lèi)型都是用的這些基本數(shù)據(jù)類(lèi)型搭建起來(lái)的,所以它們可受系統(tǒng)底層直接處理。而搭建起來(lái)的復(fù)雜數(shù)據(jù)類(lèi)型就沒(méi)必要處理了,反正這些基本數(shù)據(jù)類(lèi)型都可以搞定。

所以,總的來(lái)說(shuō),一來(lái)是內(nèi)存限制,二來(lái)是沒(méi)必要實(shí)現(xiàn),因此特性只支持上面給的那些數(shù)據(jù)類(lèi)型作為參數(shù)。

7-2 特性和普通注釋的區(qū)別

特性實(shí)際上只對(duì)后面分析、使用反射的時(shí)候有意義,那么特性不就跟普通的注釋沒(méi)啥區(qū)別了嗎?

實(shí)際上還是不同的。注釋甚至不參與編輯運(yùn)行,而特性的書(shū)寫(xiě)會(huì)影響編譯器執(zhí)行和分析代碼,所以還是不同的。

7-3 Attribute 后綴以及帶原義符號(hào) @ 的特性

下面我們來(lái)說(shuō)一下 C# 里對(duì)特性命名規(guī)則的額外規(guī)則。

在 C# 里,特性一般都會(huì)帶有 Attribute 這個(gè)單詞作為類(lèi)型名的后綴。當(dāng)然也可以不要。因?yàn)橐?guī)范里一般都帶有,所以我們將按照這個(gè)規(guī)范給大家講解。

C# 里如果一個(gè)特性帶有 Attribute 后綴的話,那么這個(gè)特性在書(shū)寫(xiě)和標(biāo)記的時(shí)候,這個(gè) Attribute 后綴可以自動(dòng)省略掉。比如 HelloAttribute 特性標(biāo)記的時(shí)候可以只寫(xiě) Hello 這部分(當(dāng)然也可以全寫(xiě))。

不過(guò),這種 C# 的簡(jiǎn)記規(guī)則會(huì)造成歧義。假設(shè)我同時(shí)包含不帶 Attribute 后綴和帶 Attribute 后綴的兩個(gè)不同特性的話,因?yàn)榍懊嬲f(shuō)過(guò),類(lèi)型名稱(chēng)只看類(lèi)型名稱(chēng)的每一個(gè)字母是不是都一樣,大小寫(xiě)都一樣,因此后綴什么的……并不在判定范圍里。

此時(shí),如果我標(biāo)記 Hello 的話,到底它使用的是 Hello 特性還是 HelloAttribute 特性呢?C# 語(yǔ)言約定,如果你刻意說(shuō)明不帶 Attribute 的這個(gè)特性的話,你需要在標(biāo)記之前加上原義字符 @;此時(shí)較長(zhǎng)的帶有 Attribute 后綴的這個(gè)特性在引用的時(shí)候就不能使用簡(jiǎn)記來(lái)去掉 Attribute 后綴了。

舉個(gè)例子。

這樣的話,@Hello 指向 Hello?特性,而 HelloAttribute 則指向 HelloAttribute 特性。因?yàn)檫@個(gè)時(shí)候同時(shí)有 HelloHelloAttribute 特性,所以 Q 類(lèi)上面的標(biāo)記,就不能寫(xiě) Hello 簡(jiǎn)單寫(xiě)法了。

可能你會(huì)覺(jué)得,既然 @ 表示不帶 Attribute 后綴的這個(gè)特性,那如果不寫(xiě) @ 不就直接表示引用的是較長(zhǎng)的這個(gè)了嗎?為什么編譯器仍然禁止簡(jiǎn)記呢?編譯器確實(shí)可以區(qū)分開(kāi),但這么做的目的其實(shí)是為了避免用戶(hù)誤用。因?yàn)槿绻环乐惯@么用的話,單純引用書(shū)寫(xiě) [Hello] 的時(shí)候,就不一定知道是 Hello 還是 HelloAttribute 了,畢竟你自己可能根本就不知道到底是不是只有 Hello 特性。如果只有 Hello 特性的話,那么 [Hello] 是引用的它;但如果同時(shí)含有 HelloHelloAttribute 的話,就會(huì)去引用 HelloAttribute 導(dǎo)致語(yǔ)義的變化,因此編譯器禁止了這一條以防止用戶(hù)誤用。

7-4 模塊的概念,以及特性用于程序集和模塊上的書(shū)寫(xiě)方法

一個(gè)程序要想跑起來(lái),離不開(kāi)各個(gè)組件、部件的協(xié)同工作。我們寫(xiě)的整個(gè)程序,被裝進(jìn)了一個(gè)容器里面,這個(gè)容器可以添加我們很多很多的代碼,這個(gè)容器整體稱(chēng)為一個(gè)程序集(Assembly)。一個(gè)程序集可以包含多個(gè)模塊(Module),但是在微軟設(shè)計(jì)的 Visual Studio 里,一般一個(gè)程序集默認(rèn)只創(chuàng)建出一個(gè)模塊,是一一對(duì)應(yīng)的關(guān)系,因此我們一般認(rèn)為模塊和程序集的范圍好像是一樣大的,實(shí)際上不是。

不過(guò)呢,因?yàn)槟J(rèn)是只能一個(gè)程序集包含一個(gè)模塊,因此我們無(wú)法自己在一個(gè)程序集里通過(guò)鼠標(biāo)操作添加別的模塊。不過(guò)你可以使用代碼創(chuàng)建新的模塊;另外,在 C# 里是沒(méi)有模塊的概念的,不過(guò) C# 運(yùn)行的 .NET 環(huán)境,另外一個(gè)語(yǔ)言 VB.NET(這我就簡(jiǎn)稱(chēng) VB 了)是有代碼語(yǔ)法層面的模塊的概念的。

不過(guò),這種概念 C# 語(yǔ)法上是不存在的,所以就不存在這個(gè)等價(jià)語(yǔ)法的說(shuō)法了。

接著我們說(shuō)一下程序集的概念。程序集是程序里最大的代碼控制單位了。程序集可以包含模塊,模塊里又可以包含若干的代碼文件,是這么一個(gè)關(guān)系;而直接作用于程序集的特性就意味著會(huì)出現(xiàn)和體現(xiàn)出程序集級(jí)別的效果。

在一些 C# 的程序里我們經(jīng)??梢钥吹竭@樣的代碼:

使用 [assembly: 特性] 的方式表達(dá)的內(nèi)容,而且它們往往被放在一個(gè)單獨(dú)的文件里,這個(gè)文件里沒(méi)有任何的類(lèi)、結(jié)構(gòu)這些數(shù)據(jù)類(lèi)型的聲明,而只是一些這樣的特性標(biāo)記。這些就是作用于程序集上的特性。這些數(shù)據(jù)會(huì)被寫(xiě)入到元數(shù)據(jù)里,因?yàn)樗鼈兪枪潭ú蛔兊目陀^數(shù)據(jù),比如上面這些專(zhuān)門(mén)表示程序作者信息啊、程序版本信息、版權(quán)信息什么的,用特性給寫(xiě)進(jìn)元數(shù)據(jù)里就是一個(gè)再好不過(guò)的選擇。

而模塊作用范圍比程序集要?。m然我們有時(shí)候感覺(jué)不到),但有時(shí)候也不乏有這樣的用法。

比如這樣的書(shū)寫(xiě)方式格式就是作用于模塊的特性。

7-5 元數(shù)據(jù)的概念,以及特性?xún)?nèi)數(shù)值的不可修改性

最后我們來(lái)說(shuō)一個(gè)神奇的東西。

假設(shè)我們?cè)囍鴺?biāo)記一個(gè)特性到類(lèi)型上面:

A 特性是這樣的:

我們可以看到,Value 屬性是可讀可寫(xiě)的,所以可使用命名參數(shù)傳值;但是因?yàn)橛肿詭?gòu)造器,所以也可以直接通過(guò)構(gòu)造器傳值。

我們上面的方式傳入了一個(gè) 10 進(jìn)去,而我們可以通過(guò)反射機(jī)制獲取到這個(gè)數(shù)據(jù)。

請(qǐng)看第 4 到第 8 行的代碼。如果 inst 變量不是 null,那么就說(shuō)明我們成功獲取到了特性的數(shù)據(jù)。那么顯然,我們輸出 inst.Value 的結(jié)果那肯定是 10,是吧。

我們此時(shí)通過(guò)第 6 行代碼變更 inst 的數(shù)據(jù)后,然后輸出后面的數(shù)值,顯然是可以改變結(jié)果的,因此第 8 行會(huì)輸出 30,對(duì)吧。

是的,現(xiàn)在的工作都是按部就班在進(jìn)行。但是如果重新獲取一次特性呢?因?yàn)槲覀冎?inst 是引用類(lèi)型,也就是說(shuō)我們更改它自己就意味著這個(gè)內(nèi)存塊上的數(shù)據(jù)跟著會(huì)變化,那么按道理特性上的 10 也會(huì)變成 30,于是我們?cè)囍匦芦@取一次特性。

我們?cè)囍某蛇@樣。我們?cè)僖淮潍@取 P 類(lèi)型上標(biāo)記的特性。然后獲取到了之后輸出這個(gè) another 變量的 Value 數(shù)值。因?yàn)樘匦允强陀^存在的,所以隨時(shí)獲取應(yīng)該都是有數(shù)值的,對(duì)吧。

似乎行得通。不過(guò)我們?cè)囍纯?,原本特性上?10 是否改成了 30。

不過(guò)很遺憾。你打開(kāi)程序運(yùn)行起來(lái)就可以發(fā)現(xiàn),這個(gè)程序一共輸出兩次數(shù)據(jù),但都是 10,而并不是一次 10 一次 30。

一頭霧水。為什么我們引用類(lèi)型的實(shí)體都改了數(shù)值了,而特性原本的 10 卻沒(méi)有改變呢?這是因?yàn)椋?/span>特性里傳入的那些數(shù)據(jù)都是不可修改的。因?yàn)閿?shù)據(jù)在上面?zhèn)魅氲臅r(shí)候就已經(jīng)給定了,而它們被儲(chǔ)存在元數(shù)據(jù)里,所以永遠(yuǎn)不會(huì)變動(dòng)。因此你無(wú)法通過(guò)任何的 C# 已知的機(jī)制來(lái)修改變更元數(shù)據(jù)里的數(shù)據(jù)信息。

第 58 講:反射(二):特性的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
六安市| 道孚县| 日喀则市| 济南市| 黄平县| 资溪县| 织金县| 昌邑市| 通河县| 梨树县| 元江| 临清市| 沅江市| 南丹县| 诸暨市| 襄汾县| 青河县| 房山区| 重庆市| 南昌县| 崇礼县| 云阳县| 大荔县| 安徽省| 区。| 迭部县| 县级市| 吉安市| 新和县| 内江市| 和林格尔县| 页游| 衡阳县| 通渭县| 珠海市| 清涧县| 二连浩特市| 陆河县| 广南县| 湾仔区| 红安县|