第 32 講:面向?qū)ο缶幊蹋ㄋ模核饕?/h1>
上回我們說完了屬性的基本用法,它把字段包裝起來,避免外部調(diào)用的時(shí)候出現(xiàn)賦值的錯(cuò)誤。今天我們來講一下第二類屬性:索引器(Indexer)。
索引器也稱為有參屬性(Parameterful Property),因?yàn)樗撕蛯傩缘幕居梅ú畈欢嘁酝?,還可以帶有一些自定義的額外參數(shù)信息。索引這個(gè)詞語在 C 語言里就已經(jīng)存在了,不過它不能自定義。C 語言里,我們?nèi)∫粋€(gè)數(shù)組或者指針對(duì)應(yīng)位置的數(shù)據(jù)的時(shí)候,會(huì)使用索引運(yùn)算符,就是那個(gè)中括號(hào)語法。
在 C# 里,為了靈活使用語法,C# 貼心地為我們提供了自定義索引器的機(jī)制,并把索引器當(dāng)成了面向?qū)ο蟮囊环N成員,可見地位還是很高的。
Part 1 引例
假設(shè)我們?cè)O(shè)計(jì)了兩個(gè)數(shù)據(jù)結(jié)構(gòu),一個(gè)是鏈表節(jié)點(diǎn)(Node),一個(gè)則是叫鏈表(List)。假設(shè)我們暫時(shí)不考慮那些增刪改查的處理代碼,而只關(guān)心基本的過程的話:
這里啰嗦一下前面好像沒有說過的語法點(diǎn)。因?yàn)樗鼈兘?jīng)常用到,但因?yàn)檫@些東西比起別的知識(shí)點(diǎn)來說不是那么重要,所以我們不必單獨(dú)拿一節(jié)內(nèi)容給大家講,干脆就在這里說了。
首先是這里的
List
類下的Start
屬性和Node
類下的Next
屬性。這兩個(gè)屬性是沒有set
方法的。在 C# 里,這個(gè)語法是允許的,這種屬性被稱為只讀屬性(Read-only Property)。這里的“只讀”和字段的“只讀”略有不同。字段里,“只讀”指的是字段的數(shù)據(jù)無法修改而標(biāo)記的readonly
get
和set
方法)的整合形式的成員,因此我們是無法對(duì)方法標(biāo)記readonly
修飾符的;而另一方面,因?yàn)閷傩灾挥?get
方法,因此它僅用來取值,所以用戶就無法使用屬性 = 數(shù)值
的賦值語法對(duì)后臺(tái)的這個(gè)字段作賦值了。因此從這個(gè)角度來說,它是“只讀”的:只用來讀取數(shù)據(jù)的。從另外一個(gè)角度來說,我們無法使用屬性 = 數(shù)值
的賦值語法,也就無從通過別的方式對(duì)這個(gè)后臺(tái)字段賦值了。后臺(tái)字段此時(shí)就是只讀的了,因此這個(gè)字段可以標(biāo)記readonly
修飾符。第二。我們可以看到
Node
類型里,有一個(gè)Node
自己這個(gè)數(shù)據(jù)類型的字段。這個(gè)語法稱為遞歸類型成員(Recursed Member)。按道理講,如果這個(gè)A
類型下有A
類型的字段的話,那么數(shù)據(jù)存儲(chǔ)就可能無休止地使得存儲(chǔ)空間膨脹起來直到撐爆內(nèi)存。但實(shí)際上是這樣的嗎?C# 其實(shí)并不會(huì)導(dǎo)致這種情況的發(fā)生。它采用了指針的概念。Node
類型是用類寫成的,每一個(gè) C# 的類里,所有用到的類類型的成員,都會(huì)被認(rèn)為一個(gè)特殊的指針。換句話說,這個(gè)成員實(shí)際上是一個(gè)指針變量,它指向的就是另外一塊內(nèi)存區(qū)域,存儲(chǔ)的就是這個(gè)成員的數(shù)據(jù)內(nèi)容了。而就原本的這個(gè)對(duì)象來說,假設(shè)它只有一個(gè)int
類型的數(shù)據(jù),和一個(gè)A
這個(gè)類類型的數(shù)據(jù)的話,那么整體這個(gè)對(duì)象的存儲(chǔ)空間只占據(jù)int
的大小,和一個(gè)指針類型的存儲(chǔ)大小的總和。因此,數(shù)據(jù)本身并不會(huì)像我們想的那樣:放在一起導(dǎo)致內(nèi)存被撐爆。最后,因?yàn)轭愵愋偷臇|西在存儲(chǔ)到別的對(duì)象里作為一個(gè)成員而存在的時(shí)候,它是一個(gè)指針。因此,類類型的所有數(shù)據(jù),它們的默認(rèn)數(shù)值是
null
。這個(gè)null
在字符串里講過,它表示“沒有內(nèi)存分配”。是的,引用類型一般都較大,而且大小不固定,所以用null
專門表達(dá)這些類型的對(duì)象“還沒有內(nèi)存分配”。那么,如果我們沒有針對(duì)字段賦初始數(shù)值的話,所有字段會(huì)被賦值為“它這個(gè)類型的默認(rèn)數(shù)值”。比如說int
類型的默認(rèn)數(shù)值是 0,而Node
類型的默認(rèn)數(shù)值就是null
了。如果不寫,就等價(jià)于在字段最后追加= null;
。
大概就這個(gè)樣子。下面我們針對(duì)于這里的 List
類型來思考取元素的問題。
我們認(rèn)為 List
是一個(gè)鏈表,因?yàn)樗鼛в幸粋€(gè)起頭節(jié)點(diǎn)(Start
屬性)。那么,我們通過移動(dòng)“指針”,來達(dá)到遍歷鏈表的過程。
比如這段代碼。我們假設(shè)初始化的時(shí)候,給 temp
這個(gè)臨時(shí)變量賦值 Start
。然后,讓 temp
不斷執(zhí)行 temp = temp.Next
這樣的過程。temp
是 Node
類型的,而 Node
類型里包含 Next
屬性,這個(gè) Next
屬性的意思是“當(dāng)前鏈表節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn);如果鏈表沒有下一個(gè)節(jié)點(diǎn)的話,這個(gè)屬性的數(shù)值就是 null
”。那么不斷 temp = temp.Next
就是在不斷讓 temp
的“指針”移動(dòng)到下一個(gè)節(jié)點(diǎn)上去的過程。直到中間的條件 temp != null
不成立的時(shí)候,循環(huán)退出。
在循環(huán)體里,我們不斷執(zhí)行 count++
操作,這表示每移動(dòng)一次 temp
的“指針”,count
就會(huì)自動(dòng)增加一個(gè)單位。對(duì),沒有錯(cuò),這個(gè) count
就是在記錄整根鏈表的總節(jié)點(diǎn)個(gè)數(shù)。
== null
和!= null
的語法判別對(duì)象是不是null
這個(gè)數(shù)值的。但是,這個(gè)等號(hào)和不等號(hào)后面是不能寫別的東西的,比如== 3
。因?yàn)?Node
類型怎么可能可以和一個(gè)int
作比較呢?要比較也只能比較兩個(gè)Node
類型才對(duì)啊。C# 的
==
和!=
在值類型(就是前面說的那些個(gè)系統(tǒng)自帶類型,string
除外)里,是判別數(shù)值是不是相同;但在類里,==
和!=
默認(rèn)是判斷指針是否一致,即兩個(gè)對(duì)象是否指向的是同一塊內(nèi)存空間。顯然,我們可以構(gòu)造出兩個(gè)數(shù)值上完全一樣的Node
類型的變量,但我們使用==
和!=
是無法判別里面存儲(chǔ)的數(shù)值是否一致的,因此一定要注意,==
和!=
在類里的比較行為。
所以,我們可以使用屬性 Length
,并把這段代碼抄進(jìn)去,來計(jì)算鏈表的總長度。
是的,代碼就這么寫就完事了。也不是很難,對(duì)吧。
Part 2 索引器的語法
索引器之所以還有個(gè)名字叫做有參屬性,是因?yàn)樗母袷胶蛯傩缘恼Z法基本一樣,也是使用 get
和 set
來表達(dá)傳入的信息。不過因?yàn)樗€會(huì)帶有額外參數(shù),因此還是有不一樣的語法。
可以從代碼里看到,get
是我們要實(shí)現(xiàn)的,set
方法實(shí)現(xiàn)起來太復(fù)雜了(牽扯到鏈表里上下兩個(gè)節(jié)點(diǎn)的指針的變動(dòng)),因此我們暫時(shí)不考慮,也沒有必要這里講這么難的東西,畢竟這不是數(shù)據(jù)結(jié)構(gòu)的課程。
我們只是改良了一下 屬性的 ?get
方法的代碼。我們使用 p
表示和記錄當(dāng)前走到第幾個(gè)節(jié)點(diǎn)了。如果 p
一直往下移動(dòng),走到和 index
的數(shù)值一樣的時(shí)候,我們就認(rèn)為,我們走到了這個(gè)節(jié)點(diǎn)上,于是就把 temp
的數(shù)據(jù)(之前說過 temp
是類類型的變量,因此它表達(dá)的是一個(gè)指針,相當(dāng)于把指針作為數(shù)值)返回出去。另外一方面,如果我們傳入的 index
不正常的話(比如鏈表還沒有那么長,但你傳入的 index
超過了這個(gè)總長度,顯然就沒有意義),于是我們返回 null
默認(rèn)表達(dá)一個(gè)隱式信息:index
不正常。當(dāng)然,這里你拋異常也行。
這里我們要說的是兩個(gè)點(diǎn)。一個(gè)是語法上 public Node this[in index]
里的 Node
,另一個(gè)是 this[int index]
。
第一,Node
表達(dá)的意思和屬性里表達(dá)的意思是一樣的。寫在索引器聲明之前,如果是 get
方法,就表示這個(gè)屬性的返回值類型;如果是 set
方法,就表示賦值的這個(gè)隱式變量 value
是什么類型的。索引器也是一樣,get
方法里,表示這個(gè)返回值的類型是 Node
;在 set
方法里則表示 value
這個(gè)隱式變量是 Node
類型的(從外部傳進(jìn)來的數(shù)值)。雖然我們這里 set
方法沒有實(shí)現(xiàn),但你肯定知道它的用法,對(duì)吧。
第二,this[int index]
。這個(gè)語法看起來有點(diǎn)怪,我們說一下意思。this
是一個(gè) C# 的關(guān)鍵字,它大概有四個(gè)用法:
索引器聲明里(
this[int index]
里這個(gè)this
);構(gòu)造器串聯(lián)調(diào)用(
: this(參數(shù))
里這個(gè)this
,這個(gè)還沒講);this
引用(this.成員
里這個(gè)this
,這個(gè)也沒講);擴(kuò)展方法的參數(shù)(
this 類型 參數(shù)名
,這個(gè)是 C# 3 才有的特性,所以還沒有講)。
當(dāng)然了,這些語法我們之后慢慢都會(huì)接觸到,不過我們這里需要了解的是其中的第一個(gè)用法。這里的 this
你可以理解成“一個(gè)萬能替換變量”。當(dāng) Node
類型的變量出來之后,這個(gè)變量名就可以直接配合索引器運(yùn)算符 []
來表示取值。數(shù)組里,我們使用 arr[3]
來取 arr
這個(gè)數(shù)組的第 4 個(gè)元素的數(shù)值,而這里的 arr
就是一個(gè)數(shù)值類型的變量,它的寫法格式就和這里的 this[參數(shù)]
是一樣的,而具體碰到什么變量了,就把這個(gè)變量替換掉這里的 this
來表達(dá)索引器,比如說 list[3]
來取鏈表的第 4 個(gè)節(jié)點(diǎn)。這就是為什么索引器的聲明語法這么奇怪的原因。
另外,在索引器參數(shù)里,我們是寫成類似方法的參數(shù)一樣的形式,先寫類型,然后再寫參數(shù)名。這個(gè)參數(shù)名是可以在 get
和 set
方法里用的。另外因?yàn)榉椒ǖ奶厥庑裕?/span>set
方法里你還可以用 value
這個(gè)隱式變量。
那么,語法我們就說到這里。下面我們來說一下索引器的使用。
Part 3 索引器的用法
下面我們說一下索引器的用法。其實(shí)也沒啥好說的,和屬性還有字段的方式完全一樣,放在等號(hào)左側(cè)就表示賦值(自動(dòng)調(diào)用 set
方法);放在別的地方作為表達(dá)式計(jì)算的時(shí)候,就用的是 get
方法)。比如說:
這表示什么呢?這顯然就表示我們要取出 list[2]
這個(gè)節(jié)點(diǎn),然后取 Value
屬性作為結(jié)果輸出。顯然 list[2]
就是 a
了,因此我們可以看到輸出的結(jié)果是 1。
Part 4 其它的問題
最后說兩個(gè)無關(guān)緊要的問題,可能你會(huì)有這樣的疑惑。
4-1 參數(shù)個(gè)數(shù)是否無限制
索引器的參數(shù)(比如前面這個(gè) int index
)可以無限制往后追加嗎?
實(shí)際上,是可以的。索引器參數(shù)并沒有規(guī)定非得只能有一個(gè)。你看,C# 里,矩形數(shù)組不就是可以后面用逗號(hào)分隔每一個(gè)數(shù)值嗎?索引器在聲明出來的時(shí)候,你完全可以寫很多參數(shù)進(jìn)去,比如這樣:
這樣是可以的。只是你在調(diào)用索引器的時(shí)候,你也得寫這么多參數(shù)進(jìn)去:variable[0, 1, 2, 3.0, 4F, "hello"]
。
4-2 索引器是否可以重載
是的,索引器也能重載,畢竟它也是有參的存在。方法、構(gòu)造器之所以可以重載,是因?yàn)樗鼈兌际怯袇?shù)的成員。索引器是可以重載的,因?yàn)樗部梢宰远x參數(shù)和參數(shù)類型。只是我們從 C 語言里學(xué)習(xí)過來,可能有一種經(jīng)驗(yàn)主義思維,以為 C# 索引器好像只能用 int
一樣。實(shí)際上,并不是。因此,只要滿足前面那些個(gè)重載規(guī)則,索引器也是可以重載的。