C# 屬性模式
1、語(yǔ)法
屬性模式是用于專門(mén)體現(xiàn)對(duì)象的屬性信息的匹配模式。我們使用一對(duì)大括號(hào)來(lái)表達(dá)參數(shù)是否必須滿足這個(gè)數(shù)值信息。
假如,我們現(xiàn)在的 Point
類型的 X
和 Y
不再使用字段表達(dá),而是用屬性來(lái)表達(dá):
那么,我們即使不給出解構(gòu)函數(shù),也可以使用屬性的方式來(lái)對(duì)每一個(gè)成員信息進(jìn)行判斷:
屬性模式專門(mén)給屬性提供數(shù)據(jù)判斷的服務(wù),因此這種模式叫屬性模式。
2、屬性模式的棄元
一般來(lái)說(shuō),屬性模式下,由于不需要依賴于解構(gòu)函數(shù),因此屬性是可以寫(xiě)出來(lái)判斷的;反過(guò)來(lái)說(shuō),如果屬性不判斷的話,那么寫(xiě)出來(lái)就沒(méi)意義了。不過(guò) C# 的語(yǔ)法允許我們使用棄元來(lái)默認(rèn)通過(guò)某個(gè)屬性的判定:
這樣的話,Y
屬性是永真式,即不用判斷了。說(shuō)白了,這里的 Y: _
是可以不寫(xiě)的。只是 C# 允許這種語(yǔ)法存在,體現(xiàn)出了語(yǔ)法的靈活性。
3、空屬性模式及變量聲明內(nèi)聯(lián)
如果屬性模式里的成員為空,那么它表示什么呢?
是的,對(duì)于可空類型(不管是值類型也好,還是引用類型也好),都表示“不為 null
”。比如 nullable
是一個(gè)可空的 Point
類型,那么 is { }
就表示 nullable.HasValue
。當(dāng)滿足條件后,我們用 point
表示這個(gè) Point
類型的數(shù)據(jù)。
從這個(gè)例子里,我們可以得到的若干信息是這些:
is { }
表示“不為null
”,適用于任何可為空的類型;大括號(hào)后可繼續(xù)內(nèi)聯(lián)一個(gè)變量,和
is T variable
寫(xiě)法格式(聲明模式)一致,但是,注意內(nèi)聯(lián)的這個(gè)變了和原始變量的類型和可空語(yǔ)義的不同:被匹配的變量(原始變量)是可空的,但是內(nèi)聯(lián)的后者這個(gè)變量是一定不空的。
C# 是允許變量聲明的內(nèi)聯(lián)作為模式匹配的一部分的。這里僅用空屬性模式介紹了內(nèi)聯(lián)變量的寫(xiě)法,但你要知道的是,內(nèi)聯(lián)變量可用在任何情況下的屬性模式。
4、盡量不要讓本來(lái)就不為 null
的表達(dá)式使用屬性模式
可以發(fā)現(xiàn),is
的左邊其實(shí)可以為一個(gè)表達(dá)式。因此下面的代碼是合法的:
不過(guò),這種寫(xiě)法具有副作用。is
的左邊一定是一個(gè)不為 null
的表達(dá)式,那么我們就沒(méi)有理由使用 is { }
來(lái)進(jìn)行模式匹配。因?yàn)檫@樣會(huì)導(dǎo)致編譯器生成不必要的判空代碼。
因此,為了避免這樣的寫(xiě)法出現(xiàn),我們可以改成 var
模式,或者是直接定義一個(gè)新的變量來(lái)進(jìn)行賦值。
var
這種寫(xiě)法看似是在直接使用大括號(hào)語(yǔ)法來(lái)同時(shí)獲取兩個(gè)屬性的數(shù)值,但是如果 Student
是引用類型的話,屬性模式的大括號(hào)本身會(huì)讓編譯器自動(dòng)生成判空代碼,于是這樣的代碼等價(jià)于 !ReferenceEquals(student, null) && student.Name is var name && student.Age is var age
。是的,它會(huì)做一次判斷 null
的冗余操作。
如果你需要對(duì)多個(gè)這樣的屬性一齊取值的話,我建議你使用值元組來(lái)進(jìn)行賦值:
用這樣的語(yǔ)法來(lái)代替原來(lái)的寫(xiě)法。這樣的賦值和原始的賦值的期望結(jié)果是一致的,但代碼里也不會(huì)多出冗余的判空。
5、可空值類型模式匹配是匹配的內(nèi)部數(shù)值
判別對(duì)象是否為空,我們可以使用 is null
來(lái)完成,因此不空就使用 !(obj is null)
就可以了;與此同時(shí),由于空屬性模式也可以完成相同的行為,因此這樣的代碼也可以寫(xiě)成 obj is { }
;對(duì)于可空值類型來(lái)說(shuō),我們還可以使用 HasValue
屬性來(lái)完成:obj.HasValue
。
但是,可空值類型在模式匹配里是當(dāng)成值類型來(lái)假設(shè)的——它可能含有數(shù)值,那么數(shù)值直接拿出來(lái)即可;如果不含有數(shù)值,返回 null
就是判斷模式的結(jié)果。而這里的 HasValue
是對(duì)所有可空值類型都具備的一個(gè)獨(dú)特特性。但是在模式匹配里,你無(wú)法這么寫(xiě)代碼:
HasValue
屬性來(lái)完成屬性模式匹配,這樣的語(yǔ)法是錯(cuò)誤的。因?yàn)榫幾g器會(huì)假設(shè) nullableValueObject
在模式匹配里是按數(shù)值進(jìn)行判斷的,即使它本身是可空值類型,但在模式匹配里它是被視為一個(gè)包含 null
的普通數(shù)值類型。比如說(shuō) a
是 int?
類型,那么 a is { HasValue: _ }
就是錯(cuò)誤寫(xiě)法:因?yàn)?a
會(huì)被視為包含 null
的普通 int
類型,而不會(huì)被當(dāng)成 int?
類型(即 Nullable<int>
類型)。這個(gè)意義在于,由于它進(jìn)行模式匹配并不會(huì)被視為可空值類型,因此你無(wú)法使用 { HasValue: _ }
類似的模式來(lái)獲取其結(jié)果。
如果確實(shí)要獲取可空值類型的內(nèi)部數(shù)據(jù),你應(yīng)該寫(xiě) a is { } v
或 a.HasValue && a.Value is var v
,而不是 a is { HasValue: _, Value: var v }
。
6、用屬性模式解構(gòu)值類型對(duì)象
是的,C# 編譯器確保了我們的操作完全只包含解構(gòu)行為的時(shí)候,是可以不做判斷即可使用這些變量的。舉個(gè)例子。
get
訪問(wèn)器可以用于取值操作,這個(gè)屬性就可以用來(lái)作為屬性模式解構(gòu)操作的一部分。這種解構(gòu)形式和之前學(xué)到的解構(gòu)函數(shù)的解構(gòu)模式不同,這里用的是屬性模式的方式獲取,因此稱為屬性模式解構(gòu)(Property-pattern-styled Deconstruction)。
另外,上面用到了棄元符號(hào)。因?yàn)?is
表達(dá)式不可單獨(dú)使用,它必須返回?cái)?shù)值給變量調(diào)用。如果你確實(shí)不使用結(jié)果變量(實(shí)際上這個(gè)解構(gòu)行為根本就不可能失敗,所以上面這樣的 is
表達(dá)式永遠(yuǎn)返回 true
)賦值給等號(hào)左側(cè)的話,只需要寫(xiě)棄元符號(hào)即可,它等價(jià)于這樣:
又或者是
等等寫(xiě)法。
另外,這樣的解構(gòu)風(fēng)格允許你包含棄元模式嵌套在屬性模式之中。但凡右側(cè) 100% 是成功的解構(gòu)操作的話,你怎么寫(xiě)模式匹配都可以:
if
這樣的話,由于 A
屬性判斷了數(shù)值,所以可能解構(gòu)操作不成功,這種場(chǎng)合你只能使用 if
,而且不能簡(jiǎn)化成上面屬性模式風(fēng)格的解構(gòu)的樣式。順帶一說(shuō),_ = a is pattern
表達(dá)式的 _
不是模式匹配,它只是表示變量我們不使用了。
7、遞歸模式Ⅰ:屬性模式遞歸
C# 強(qiáng)大的地方在于,語(yǔ)法很靈活,這樣我們寫(xiě)代碼可以不用唯一的一條道路去實(shí)現(xiàn)。比如前面的解構(gòu)模式。(x: var x, y: var y)
里又是一個(gè) var
模式的變量聲明。所以,正是因?yàn)檫@樣,我們學(xué) C# 就不必學(xué)得那么痛苦。
C# 的屬性模式是 C# 一大秀兒語(yǔ)法。它允許遞歸使用屬性模式進(jìn)行判斷。假設(shè)我有這么一個(gè)對(duì)象:
這個(gè)對(duì)象是表示一個(gè)人的基本數(shù)據(jù)信息,比如名字啊、年齡啥的,當(dāng)然也存儲(chǔ)了 ta 的父母的實(shí)例的引用。
其中,我們假設(shè)
Gender
類型是個(gè)暫時(shí)只包含Male
和Female
倆字段的枚舉類型。
Person?
語(yǔ)法表示Person
這個(gè)引用類型具有和值類型類似的語(yǔ)法:這個(gè)屬性信息可為null
。反之,如果沒(méi)有?
標(biāo)記的類型,這個(gè)成員的數(shù)值就不能為null
。這個(gè)語(yǔ)法是 C# 8 里的,這里為了體現(xiàn)出判斷用法,故意寫(xiě)上了?
來(lái)表達(dá)為null
、更顯眼一點(diǎn);另外,這里故意取可為null
的寫(xiě)法,還有一個(gè)目的,是為了體現(xiàn)一會(huì)兒模式匹配的語(yǔ)義,所以請(qǐng)不要和現(xiàn)實(shí)世界進(jìn)行對(duì)比或者對(duì)號(hào)入座。
假如,我們要判斷是否某個(gè)人的姓名是“張三”、年齡 24,他爸叫“張二”、而他的媽媽則叫“李四”。如果要判斷這個(gè)對(duì)象的具體信息,我們可以這么寫(xiě)代碼:
注意這里的模式匹配寫(xiě)法。前面模式匹配就用的是大括號(hào),因此我們可以對(duì)對(duì)象的內(nèi)部信息繼續(xù)作判斷。比如 Father
和 Mother
屬性又是一個(gè) Person
類型的對(duì)象,因此我們還可以接續(xù)一個(gè)大括號(hào)對(duì) Father
和 Mother
的值的具體內(nèi)容繼續(xù)進(jìn)行判斷。
一定要注意。Father
和 Mother
屬性是可能為 null
的。當(dāng) Father
屬性的數(shù)值本身就是 null
的時(shí)候,那么顯然就不存在 Name: "Zhang 'er"
的判斷行為了:因?yàn)?null
值本身就無(wú)法繼續(xù)判斷內(nèi)部數(shù)據(jù)了。因此,在 Father
為 null
的時(shí)候,模式匹配結(jié)果一定是 false
。當(dāng)且僅當(dāng)整個(gè)判斷的邏輯全都匹配,if
條件才成立。
順帶給大家看下,C# 的模式匹配到底多有魅力:給大家展示一個(gè)我之前寫(xiě)過(guò)的一段代碼,用到了這里的模式匹配。
這里,這么一大坨都是遞歸的模式匹配。正好這體現(xiàn)出了模式匹配的魅力。
8、遞歸模式Ⅱ:對(duì)位模式和屬性模式是可以放在一起的
C# 的屬性模式具有和對(duì)位模式完全一致的判斷行為,因此 C# 就把對(duì)位模式和屬性模式在語(yǔ)義分析上放在了一起。假設(shè)我有一個(gè) Point
類型,包含 X
和 Y
屬性(它們通過(guò)解構(gòu)函數(shù)解構(gòu)為 x
和 y
兩個(gè)參數(shù)),并且包含 Area
屬性表示當(dāng)前點(diǎn)到坐標(biāo)原點(diǎn)構(gòu)成的矩形的面積。
這里不是講數(shù)學(xué),我只是告訴你如何并用兩個(gè)模式。
可以看到,我們直接在 (x: 10, y: 30)
這個(gè)對(duì)位模式后加上了 { Area: _ }
屬性模式。在 C# 里,對(duì)位模式和屬性模式均可以用于遞歸使用(比如假設(shè)一個(gè)對(duì)位模式的成員是可以繼續(xù)通過(guò)別的模式進(jìn)行匹配的,那么這個(gè)成員就可以繼續(xù)遞歸地進(jìn)行模式的判斷),同時(shí)屬性模式也是如此,前文已經(jīng)說(shuō)過(guò)了。因此,C# 把對(duì)位模式和屬性模式統(tǒng)稱遞歸模式(Recursive Pattern)。換句話說(shuō),在概念上來(lái)講,你可以同時(shí)使用對(duì)位模式和屬性模式的兩種不同模式的判別,并放在一起,這個(gè)整體叫做遞歸模式。
但請(qǐng)注意,必須是先對(duì)位模式,后屬性模式的順序。寫(xiě)反了是不行的。