第 58 講:反射(二):特性
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)去使用別的成員。
Score
和 AverageScore
是兩個(gè)實(shí)現(xiàn)效果一樣的屬性。假設(shè)這個(gè) Score
也就是說(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)到下面的 Score
和 AverageScore
上的時(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
即可。
class
關(guān)鍵字,有一對(duì)大括號(hào)。是的,就這樣就完事了。那么這個(gè) A
因?yàn)闆](méi)有 abstract
關(guān)鍵字修飾,所以它就可以用來(lái)標(biāo)記了。此時(shí) A
這些都可以。
稍微說(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),比如 Assembly
、Module
等等,一會(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)型里包含 get
和 set
方法都有的屬性,那么這個(gè)屬性可以充當(dāng)特性命名參數(shù)使用。
那么,Inherited
屬性管什么呢?這個(gè)屬性是表示,是否我這個(gè)特性可以提供給成員的派生成員(重寫(xiě)過(guò)的),或者是類(lèi)型的派生類(lèi)型復(fù)制一份。
假設(shè)我現(xiàn)在有兩個(gè)特性,一個(gè)叫 Inherited
,有一個(gè)叫 NotInherited
:
然后,我這么寫(xiě)了代碼使用這些特性:
其中,BaseA
和 DeivedA
是一組,BaseB
和 DerivedB
是一組。請(qǐng)注意,在基類(lèi)型 BaseA
和 BaseB
上我們分別標(biāo)記了 Inherited = true
的 Inherited
特性和 Inherited = false
的 NotInherited
特性。
標(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
就不行。
比如前文我們的 AllowMultiple
是 false
,那么就不能這么寫(xiě)代碼:
甚至更多 [A]
的疊加。但是,如果 AllowMultiple
屬性為 true
,那么疊加就是可以的。
和前文的 Inherited
命名參數(shù)的機(jī)制完全一致,AllowMultiple
也是 AttributeUsage
特性里的一個(gè)固有屬性,它也同時(shí)包含 get
和 set
方法,因而滿足特性命名參數(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
特性舉例。
Person
是吧。獲取上面標(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ù)的例子。
AverageScore
指定 A
特性,并給 Prop
因?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ì)象。

[return: A]
這樣的語(yǔ)法,如果不寫(xiě) return
比如這樣寫(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ù):
byte
、sbyte
、ushort
、short
、uint
、int
、ulong
、long
;浮點(diǎn)數(shù):
float
、double
、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í)有 Hello
和 HelloAttribute
特性,所以 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í)含有Hello
和HelloAttribute
的話,就會(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ù)信息。