第 30 講:面向?qū)ο缶幊蹋ǘ簩?shí)例、構(gòu)造器和字段
前一節(jié)我們講到了基本的面向?qū)ο蟮氖褂梅绞?。我估?jì)你也看得不是很懂,是因?yàn)槟銢]有轉(zhuǎn)換思維方式,還是用的 ?C 語言那套面向過程的編程模式??傊煜ひ幌旅嫦?qū)ο蟮膶懛ǜ袷?,用類來把代碼規(guī)范規(guī)劃出來,構(gòu)成一整個項(xiàng)目,這是 C# 最基本的編程范式。
今天我們要說的是另外一個面向?qū)ο罄镏匾幕靖拍睿?/span>實(shí)例(Instance)。
Part 1 什么是實(shí)例
在之前我們簡單說到過,實(shí)例和靜態(tài)的區(qū)別是,實(shí)例是將一個事物用代碼呈現(xiàn)出來的時候,一個單獨(dú)的個體。要想寫成代碼,我們就得考慮寫成代碼的時候,操作的行為是以個體為單位的形式,還是沒有單獨(dú)的個體操作的形式。實(shí)例和靜態(tài)方法的區(qū)別就在于,只需要去掉 static
修飾符,就從靜態(tài)方法改成實(shí)例方法了。
Part 2 字段
2-1 字段的概念
只說方法,可能不一定能體現(xiàn)出實(shí)例的真正作用。我們來寫一個真正的例子。我們現(xiàn)在用 Person
類來表達(dá)一個人,存儲這個人的基本信息(姓名、性別等等信息)。
下面我們來看一下這個例子。這個例子里我們給 Person
里寫了三個直接以分號結(jié)尾,也沒有括號的語句。這個格式的東西我們稱之為字段(Field)。字段的作用就是存儲這個類里基本的數(shù)據(jù)信息;而如果用面向?qū)ο蟮慕嵌葋碚f,Person
類專門用來表達(dá)一個人的個體的基本數(shù)據(jù)信息,而 Name
字段可以理解成這個人的姓名這個基本信息、Age
則是年齡、而 IsBoy
則是表達(dá)這個人是不是男生。
可以看到,這三個字段都沒有任何一個標(biāo)記了 static
修飾符。這意味著這三個字段都是實(shí)例字段。之前說過靜態(tài)方法,因?yàn)樗恼麄€行為和操作過程都和個體無關(guān);但是這里給出的三個字段都跟個體聯(lián)系上:因?yàn)檫@些字段信息存儲的都應(yīng)該是一個個體本身的信息。
字段可以有很多,也可以一個都沒有。字段體現(xiàn)整個“個體”本身的基本數(shù)據(jù)信息。有些時候,要想精確描述和表達(dá)一個個體的具體內(nèi)容,就必須得很多字段;但是有時候,一個都不需要也是可以的。
2-2 字段的用法
字段的信息怎么和我們交互呢?這就需要我們通過變量或者 Console.ReadLine
來輸入了。
現(xiàn)在,我們可以考慮在 Main
方法里,給定一些基本的變量,而且變量的類型對于字段的類型要一一匹配。
我們注意第 2 行代碼。這一行的代碼我們用到了和 throw
-new
一致的 new 類名()
person
的變量(似乎,確實(shí)是在定義和賦值變量,因?yàn)樽兞康亩x就是這么寫的)。
接著,我們給變量錄入了一些數(shù)值,并使用的是 .Name
、.Age
這樣的書寫。是不是很眼熟?是的,之前學(xué)習(xí)字符串操作的時候,我們用過一個叫做 .Length
的寫法來獲取字符串的長度;而在數(shù)組的后面直接跟上 .Length
得到的則是數(shù)組的元素?cái)?shù)。是的,這里我們依然用的是這個小數(shù)點(diǎn)。小數(shù)點(diǎn)相信已經(jīng)深入人心了:它就是我們之前前一節(jié)的成員訪問運(yùn)算符,讀作“的”。不論在獲取數(shù)組長度和字符串長度的時候,都是跟一個單獨(dú)的個體關(guān)聯(lián)起來的,因而它們(Length
這樣的東西)都是實(shí)例成員。只是說,這些是系統(tǒng)提供的,你無法查看內(nèi)部的代碼,只能看到它有這么一個東西的存在。但是用法和我說的它的用法是沒有出入的。
它就和數(shù)組的用法是一樣的,數(shù)組的索引器(arr[i]
)有兩種用法:
arr[i] = value;
(賦值)Console.WriteLine(arr[i]);
(取值)
這里的成員訪問運(yùn)算符所構(gòu)成的表達(dá)式,也是如此:既可以賦值,也可以取值。取值就是類似第 10 到第 13 行里這么使用;而賦值就是最開始的第 3 到第 5 行的代碼。
Part 3 構(gòu)造器
3-1 構(gòu)造器的概念
在第 2 行,我們用到了這個 new
表達(dá)式,這個表達(dá)式得到結(jié)果賦值給左側(cè)的過程稱為實(shí)例化(Instantiation);而你可以認(rèn)為,new Person()
里的 Person()
是一個沒有返回值(即 void
類型返回值)、不需要參數(shù)的一個特殊的方法,而這個特殊的方法,則可以認(rèn)為是一個就叫做 Person
的方法。
這種和類名重名的方法,稱為構(gòu)造器(Constructor)。構(gòu)造器和字段一樣,也都是這個類的成員類型。構(gòu)造器是一類特殊的方法,它專門使用 new
關(guān)鍵字,來調(diào)用它們;而我們自己是無法調(diào)用的。
3-2 構(gòu)造器到底是實(shí)例還是靜態(tài)成員
這個說法估計(jì)你也沒明白,我們這么解釋一下。按照道理來說,方法的調(diào)用方式是 類名.成員
(靜態(tài)成員的調(diào)用方式)或者是 變量.成員
(實(shí)例成員的調(diào)用方式)。但是,構(gòu)造器到底應(yīng)該是實(shí)例成員,還是靜態(tài)成員呢?按道理來說,它最終產(chǎn)生一個個體出來,然后賦值給左邊的所謂“變量”(只是這里的變量的類型用是 Person
而不是 int
這樣的數(shù)據(jù)類型了)。按道理來說,因?yàn)樗鼪]有綁定任何一個個體,只是產(chǎn)生個體,因此應(yīng)該屬于靜態(tài)成員;而使用靜態(tài)成員的調(diào)用方式,那就得是 Person.Person()
了:前面這個 Person
是類名,而后面這個 Person
在前面說過,它是一個和類名同名的特殊方法的方法名稱。但是你這么寫,C# 編譯器會告訴你,這么寫不對:

它會告訴你,這么寫不對:因?yàn)?Person
類里沒有叫 Person
那難道就是實(shí)例成員嗎?看起來也不是。剛已經(jīng)解釋過了,構(gòu)造器是創(chuàng)建和產(chǎn)生一個個體,而不是綁定和使用個體。但是,很遺憾地通知你,構(gòu)造器是實(shí)例成員。原因很簡單:因?yàn)樗皇庆o態(tài)成員,所以它就是實(shí)例成員了?!疚婺?jpg】
實(shí)際上是這樣的。因?yàn)樗膭?chuàng)建過程是在對個體創(chuàng)建內(nèi)存空間,然后將結(jié)果賦值給左側(cè)的這個“變量”。從細(xì)節(jié)上講,它確實(shí)在改動這個個體。不過這個改動是在初始化,就好比給變量賦初始值一樣,這個初始數(shù)值不也得憑空產(chǎn)生嗎?既然要產(chǎn)生出來那就必然會有一定的內(nèi)存的復(fù)雜操作,所以它是在改動這個個體的。
那么至此,我們需要注意和掌握的兩個內(nèi)容就是:
構(gòu)造器是實(shí)例成員,而不是靜態(tài)成員。另外構(gòu)造器是實(shí)例成員,也只能是實(shí)例成員;
構(gòu)造器必然只能使用
new
關(guān)鍵字,帶上構(gòu)造器寫法來使用。
正是因?yàn)闃?gòu)造器只能是實(shí)例成員,因此我們無法對構(gòu)造器添加和追加 static
修飾符。
3-3 自定義構(gòu)造器
構(gòu)造器既然是特殊的方法,那么我們就得知道參數(shù)傳入的問題。構(gòu)造器只能是無參(Parameterless)的嗎?實(shí)際上不是。
構(gòu)造器可由我們自行定義,而 Person()
這個無參構(gòu)造器是系統(tǒng)自動生成的:只要我們不自己定義構(gòu)造器的話,無參構(gòu)造器就會自動生成;而要定義構(gòu)造器的話,這個無參構(gòu)造器系統(tǒng)就不會給你自動產(chǎn)生。
這么說也不明白,我們還是拿例子來解釋。
private
是不合適的。而之前就說過,構(gòu)造器也是成員。成員的默認(rèn)的訪問修飾符是 private
,因此我們不能省略這個訪問修飾符。因此,整個構(gòu)造器的簽名就長成這樣。
然后,我們在構(gòu)造器的里面寫的是賦值。我們把參數(shù)寫出來,正好對應(yīng)上每一個字段,這樣保證每一個字段都能夠賦值完成。
在調(diào)用方,我們需要修改構(gòu)造器的那行代碼。因?yàn)槲覀儎偛耪f過,一旦我們自定義了構(gòu)造器后,無參構(gòu)造器就不會默認(rèn)產(chǎn)生,因此我們這么寫此時會出錯。
改成這樣:
我們可以看到,代碼改成這樣了。我們按照順序?qū)?shù)據(jù)傳入到里面去,最終就會得到一個這樣的個體,賦值給左側(cè)。
3-4 其它的構(gòu)造器的問題
下面我們來針對于構(gòu)造器來解答一些你可能有的困惑。
3-4-1 如果我們不對一些字段賦值,這樣寫可以嗎?
實(shí)際上,是可以的。C# 允許你不給變量賦值。那么這個字段如果不賦值的話,就會保持這個類型的默認(rèn)數(shù)值作為初始結(jié)果。
舉個例子,假設(shè)我們還是用無參構(gòu)造器初始化的話,那么產(chǎn)生的個體,最終得到的 Name
的數(shù)值是 null
、Age
的數(shù)值是 0,而 IsBoy
的數(shù)值則是 false
。這個 null
是什么呢?你可以這么想這個問題。類產(chǎn)生的個體一般都很大,因?yàn)樗嵌鄠€字段構(gòu)造搭起來的。而且是不定長的。這個 null
就相當(dāng)于沒有內(nèi)存空間存儲這個個體。換句話說,給 person
這個“變量”賦值 new
出來的個體,和賦值 null
的區(qū)別是,一個會產(chǎn)生一個個體出來,而另外一個則完全不會產(chǎn)生個體。你按照集合的空集來理解就行:它不占任何存儲空間,只是一個概念上的不存儲數(shù)據(jù)的一種存在。因此,對于字符串來說,null
、空字符串的區(qū)別就是,一個是有內(nèi)存空間占據(jù)的,一個則是完全不影響程序的無內(nèi)存空間占據(jù)的。
而 0 和 false
作為默認(rèn)數(shù)值就比較好理解了,因此我就不用多說了。這一點(diǎn)在數(shù)據(jù)類型里就說過一次。
3-4-2 構(gòu)造器是沒返回值的,那怎么 new
的時候可以賦值給變量?
這個問題問得好。答案也比較好說:就是特殊處理過。構(gòu)造器本身并不會用 return
帶出數(shù)值結(jié)果,但是本身是在對一個個體修改內(nèi)部的數(shù)據(jù)。這個正在創(chuàng)建和改動的個體,就是整個 new
表達(dá)式的結(jié)果。但是可以從這個說法里看出,這個帶出的個體我們是無法通過代碼書寫出來的,因此我們就沒有寫了。
3-4-3 構(gòu)造器可以重載嗎?可以重載的話,我能自己定義無參構(gòu)造器嗎?
答案是可以的。構(gòu)造器允許重載,它和方法是差不多的,因此重載規(guī)則是一樣的。而因?yàn)闊o參構(gòu)造器會在自己定義構(gòu)造器后自動消失,因此我們可以自己手動把無參構(gòu)造器寫出來:
是的,就是一個單純的大括號,里面啥都不寫。因?yàn)闊o參構(gòu)造器,難道還想跟這些字段給初始數(shù)值嗎?反正系統(tǒng)會自己賦值給字段初始化默認(rèn)數(shù)據(jù)(null
、0、false
那個,剛才說過了)。
當(dāng)然了,你如果不喜歡 null
的話,你可以手動在里面添加一行代碼,來提供字符串的初始化行為,比如這樣:
其它的兩個就不必賦值了。
Part 4 readonly
修飾符
是的,字段在前面我們已經(jīng)介紹了賦值和取值過程。但是你有沒有發(fā)現(xiàn)一個問題,這個字段就算寫了構(gòu)造器,后續(xù)也是依然可以修改和變動的:
注意最后一行代碼,我們確實(shí)改動了 IsBoy
字段。但是按照道理來講,person
個體應(yīng)該只在實(shí)例化后就不能再變動了。這個時候,我們可能會需要一個全新的關(guān)鍵字:readonly
readonly
,這個成員就不再可以修改和變動了。
那么很明顯可以看出,readonly
是只能放在字段上的。因?yàn)樗轻槍τ谛薷暮妥儎觼碜鳛橄拗坪图s束,那唯一適用的對象就只有字段了,因此,readonly
只能用在類的字段上。
我們將 Person
類的字段替換成這樣的寫法,即在訪問修飾符 public
和類型名稱中間插入 readonly
修飾符,這就表示字段只讀了。只讀的字段就無法在初始化(實(shí)例化)后再次變動和修改。
我們返回到調(diào)用方:

如果你覺得,這個字段只能在實(shí)例化的時候修改變動數(shù)據(jù)的話,那么字段請使用 readonly
修飾符修飾它,來保證以后無法修改它。
另外,除了我們給字段本身標(biāo)記 readonly
以外,我們還可以為字段本身設(shè)置 static
,因此字段是具有 static readonly
雙重組合的修飾符表達(dá)的。
考慮一種情況。我們?nèi)绻胍獎?chuàng)建一個默認(rèn)的個體,這個個體我怕別人用的時候亂用,我就打算寫成一個只讀的靜態(tài)字段。在別人想要使用的時候,通過靜態(tài)成員的訪問方式來對個體進(jìn)行訪問,這樣就可以避免他們不會使用這個類了。
我們將無參構(gòu)造器用 private
修飾,而是給他們提供一個只讀的靜態(tài)字段 DefaultInstance
,并賦值 = new Person()
??纯催@有什么奇妙的理解方式:
注意第 3 行。我們書寫的這個寫法。字段允許直接在后面就賦值,和變量的寫法是一樣的,這是 C# 允許的。而我們將無參構(gòu)造器更改成 private
級別,這是防止別人使用的時候,在外部調(diào)用。如果想要使用默認(rèn)的數(shù)值的話,我們提供了 DefaultInstance
字段。
于是,用戶在使用的時候,必須這么寫:
用這個寫法,我們就可以通過作者給出的模式讓你來獲取默認(rèn)情況的個體。
哦對,順帶一說。這種故意將無參構(gòu)造器改成
private
修飾,然后提供一個靜態(tài)的只讀字段來表達(dá)默認(rèn)數(shù)據(jù)的行為,稱為單例模式(Singleton)。這種寫法是一種固定的設(shè)計(jì)模式。所謂的設(shè)計(jì)模式就是為了幫助和輔助我們使用一些固定的軟性規(guī)定,達(dá)到代碼的固定書寫格式,來達(dá)到一種模式化的意義。設(shè)計(jì)模式有非常多種,單例模式是其中的一種。