1.1 - C#语言习惯 - 使用属性而不是可访问的数据成员
?
屬性一直是C#語言中的一等公民。自1.0版本以來,C#對屬性進(jìn)行了一系列的增強(qiáng),讓其表達(dá)能力不管提高。你甚至可以為setter和getter指定不同的訪問權(quán)限。
隱式屬性也極大降低了聲明屬性時(shí)的工作量,不會(huì)比聲明數(shù)據(jù)成員麻煩多少。
?
若你仍然在類型中聲明公有成員,或是仍在手工編寫set或get之類的方法,那么快停下來吧。
屬性允許將數(shù)據(jù)成員作為公共接口的一部分暴露出去,同時(shí)仍舊提供面向?qū)ο蟓h(huán)境下所需要的封裝。
屬性這個(gè)語言元素可以讓你像訪問數(shù)據(jù)成員一樣使用,但其底層依舊使用方法實(shí)現(xiàn)。
?
類型的某些成員確實(shí)非常適合作為數(shù)據(jù),例如某個(gè)客戶的名稱,某個(gè)站點(diǎn)的x、y坐標(biāo)或上一年度的收入等。
而屬性則讓你可以創(chuàng)建出類似于數(shù)據(jù)訪問,但實(shí)際上卻是方法調(diào)用的借口,自然也可以享受到方法調(diào)用的所有好處。
客戶代碼訪問屬性時(shí),就像是在訪問公有的字段,不過其底層使用方法實(shí)現(xiàn),其中可以自由定義屬性訪問器的行為。
?
.NET Framework假設(shè)你會(huì)對公有數(shù)據(jù)成員使用屬性。
實(shí)際上,.NET Framework中的數(shù)據(jù)綁定類僅支持屬性,而不支持公有數(shù)據(jù)成員。
對于類所有的數(shù)據(jù)綁定類庫均是如此,包括WPF、WinForms和Silverlight。數(shù)據(jù)綁定會(huì)將某個(gè)對象的一個(gè)屬性和某個(gè)用戶界面控件相互關(guān)聯(lián)起來。
數(shù)據(jù)綁定機(jī)制將使用反射來找到類型中的特定屬性:
1 textBoxCity.DataBindings.Add("Text", address, "City");這段代碼將textBoxCity控件的Text屬性綁定到了address對象的City屬性上。
公有的數(shù)據(jù)成員并不推薦使用,因此Framework Class Library設(shè)計(jì)其也不支持其實(shí)現(xiàn)綁定。這樣的設(shè)計(jì)也保證了你必須選擇合適的面向?qū)ο蠹夹g(shù)。
?
確實(shí),數(shù)據(jù)綁定只是用在用戶界面邏輯中會(huì)使用到的類中。但這并不意味著屬性僅應(yīng)該用在UI邏輯中,其他類和結(jié)構(gòu)中也應(yīng)使用屬性。
在日后產(chǎn)生新的需求或行為時(shí),屬性更易于修改。
例如,你會(huì)很快有這樣的想法,客戶對象不應(yīng)該有空白的名稱。若你使用了公有屬性來封裝Name,那么只要修改一處即可:
1 public class Customer 2 { 3 private string name; 4 public string Name 5 { 6 get { return name; } 7 set 8 { 9 if (string.IsNullOrEmpty(value)) 10 { 11 throw new ArgumentException("Name cannot be blank!", "Name"); 12 } 13 name = value; 14 } 15 } 16 }?
若是使用了公有的數(shù)據(jù)成員,那么就需要查找每一處設(shè)置客戶名稱的代碼并逐一修復(fù)。這將花費(fèi)大量的時(shí)間。
?
因?yàn)閷傩允鞘褂梅椒▉韺?shí)現(xiàn)的,所以添加多線程支持也非常簡單。
很容易即可在屬性的get和set訪問器中作如下的修改,從而支持對數(shù)據(jù)的同步訪問:
1 public class Customer 2 { 3 private object syncHandle = new object(); 4 5 private string name; 6 public string Name 7 { 8 get 9 { 10 lock (syncHandle) 11 { 12 return name; 13 } 14 } 15 set 16 { 17 if (string.IsNullOrEmpty(value)) 18 { 19 throw new ArgumentException("Name cannot be blank!", "Name"); 20 } 21 lock (syncHandle) 22 { 23 name = value; 24 } 25 } 26 } 27 }?
屬性可以擁有方法的所有語言特性。例如,屬性可以為虛的(virtual):
1 public class Customer 2 { 3 public virtual string Name 4 { 5 get; 6 set; 7 } 8 }注意,上述例子中使用了C# 3.0中的隱式屬性語法。使用屬性來封裝私有字段是一個(gè)常用的模式。
通常而言,我們并不需要驗(yàn)證屬性的getter或setter邏輯。
因?yàn)檎Z言本身提供了簡化的隱式屬性語法,力求盡量降低開發(fā)人員的輸入工作,即將一個(gè)簡單的字段暴露或?qū)傩浴?/p>
編譯器將為你創(chuàng)建一個(gè)私有的成員字段,并自動(dòng)生成最簡單的get和set訪問器的邏輯。
?
你還可以將屬性聲明為抽象的(abstract),以類似隱式屬性語法的形式將其定義在接口中。
下面的例子就將屬性定義在了一個(gè)泛型接口中。
需要注意的是,雖然其語法和隱式屬性完全相同,但是編譯器卻不會(huì)自動(dòng)生成任何實(shí)現(xiàn)。
接口只是定義了一個(gè)契約,強(qiáng)制所有實(shí)現(xiàn)了該接口的類型都必須滿足。
1 public interface INameValuePair<T> 2 { 3 string Name 4 { 5 get; 6 } 7 8 T value 9 { 10 get; 11 set; 12 } 13 }?
屬性是一種全功能的、第一等的語言元素,能夠一方法調(diào)用的形式訪問或修改內(nèi)部數(shù)據(jù)。成員函數(shù)中可以實(shí)現(xiàn)的功能均可在屬性中實(shí)現(xiàn)。
?
屬性的訪問器將作為兩個(gè)獨(dú)立的方法變異到你的類型中。在C#中,你可以為get和set訪問器制定不同的訪問權(quán)限。
這樣即可更精妙的控制作為屬性暴露出來的數(shù)據(jù)成員的可見性。
?
1 public class Customer { 2 public virtual string Name 3 { 4 get; 5 protected set; 6 } 7 }上述屬性語法的表達(dá)含義圓圓超出了簡單數(shù)據(jù)字段的范疇。
若類型需要包含并暴露出可索引的項(xiàng)目,那么可以使用索引器(即支持參數(shù)的屬性);若想反悔序列中的項(xiàng),創(chuàng)建一個(gè)屬性會(huì)是個(gè)不錯(cuò)的做法。
1 public class User 2 { 3 private string name; 4 5 public string Name 6 { 7 get { return name; } 8 set { name = value; } 9 } 10 11 // 獲取Name中的指定字符 12 public char this[int index] 13 { 14 get 15 { 16 if (index < 0) 17 { 18 throw new ArgumentException("Index must be more than 0!", "index"); 19 } 20 return name[index]; 21 } 22 } 23 } 24 class Program 25 { 26 static void Main(string[] args) 27 { 28 User mrZhang = new User() { Name = "張董" }; 29 30 Console.WriteLine(mrZhang[0]); 31 } 32 } 索引器(支持參數(shù)的屬性)運(yùn)行結(jié)果:
?
索引器和單一條目屬性有著同樣的語言支持:他們都是作為方法實(shí)現(xiàn)的,因此可以在索引器內(nèi)部實(shí)現(xiàn)任意的驗(yàn)證或計(jì)算邏輯。
索引器也可以為虛的或抽象的,可聲明在接口中,可以為只讀或讀寫。
?
一維且使用數(shù)字作為參數(shù)的索引器也可以參與數(shù)據(jù)綁定。使用非整數(shù)的索引器可用來定義Map和Dictionary:
1 public Address this[string name]2 { 3 get { return addressValues[name]; } 4 set { addressValues[name] = value; } 5 }
?
?
C#中支持多維數(shù)組,類似的,我們也可以創(chuàng)建多維索引器,每一個(gè)維度上可以使用同樣或不同的類型:
1 public int this[int x, int y] 2 { 3 get { return ComputeValue(x, y); } 4 } 5 6 public int this[int x, string name] 7 { 8 get { return ComputeValue(x, name); } 9 }?
需要注意的是,所有的索引器都是用this關(guān)鍵字聲明,C#不支持為索引器命名。
因此,類型中每個(gè)不同的索引器都必須有不同的參數(shù)列表,一面混淆。
幾乎屬性上的所有特性都能應(yīng)用到索引器上。索引器也可為虛的或抽象的,可以對setter和getter給出不同的訪問限制,不過卻不能像屬性那樣創(chuàng)建隱式索引器。
?
屬性的功能很強(qiáng)大,是個(gè)不錯(cuò)的改進(jìn)。
但你是不是還在想能不能先用數(shù)據(jù)成員來實(shí)現(xiàn),而在稍后需要其他各種功能的時(shí)候再改成屬性呢?
這看似是個(gè)不錯(cuò)的策略,不過實(shí)際上卻行不通。
考慮如下這個(gè)類的定義:
1 // using public data members, bad practice 2 public class Customer 3 { 4 public string Name; 5 // remaining implementation omitted 6 }?
這個(gè)類描述一個(gè)客戶(Customer),包含了一個(gè)名稱(Name)。你可以使用熟悉的成員表示方法獲取或設(shè)置該名稱:
1 Customer zhangDong = new Customer(); 2 zhangDong.Name = "張董"; 3 string name = zhangDong.Name;?
看似簡單直觀,你也會(huì)認(rèn)為若是日后將Name改成屬性,那么代碼也可以無需修改保持正常。
但這個(gè)答案并不是完全正確的。
屬性僅僅是訪問時(shí)類似于數(shù)據(jù)成員,這是語法所實(shí)現(xiàn)的目的。
不過屬性并不是數(shù)據(jù),屬性的訪問和數(shù)據(jù)的訪問將會(huì)生成不同的MSIL(Microsoft Intermediate Language,微軟中間語言)指令。
?
雖然屬性和數(shù)據(jù)成員在源代碼層次上是兼容的,不過在二進(jìn)制層面上卻大相徑庭。
這也就意味著,若將某個(gè)公有的數(shù)據(jù)成員改成了與之等同的共有屬性,那么久必須重新編譯所有用到該共有數(shù)據(jù)成員的代碼。
C#把二進(jìn)制程序及作為一等公民看待。該語言本身的一個(gè)目標(biāo)就是支持發(fā)布某個(gè)單一程序集時(shí),不需要更新整個(gè)的應(yīng)用程序。
而這個(gè)將數(shù)據(jù)成員改為屬性的簡單操作卻破壞掉了二進(jìn)制兼容性,也就會(huì)讓更新單一程序集變得非常困難。
?
若是查看屬性生成的IL,那么你或許會(huì)想比較一下屬性和數(shù)據(jù)成員的性能。
屬性當(dāng)然不會(huì)比數(shù)據(jù)成員訪問快,不過也不會(huì)比其慢多少。
JIT編譯器將內(nèi)聯(lián)一些方法調(diào)用,包括屬性訪問器。當(dāng)JIT編譯器內(nèi)聯(lián)了屬性訪問器時(shí),數(shù)據(jù)成員和屬性的訪問效率即可持平。
即使某個(gè)屬性訪問器沒有被內(nèi)聯(lián),其性能差距也實(shí)在是微乎其微,僅僅一次函數(shù)調(diào)用之別而已。只有在某些極端情況下,二者的差距才會(huì)有所影響。
在調(diào)用方法看,屬性雖然是方法,但它和數(shù)據(jù)卻有著類似的概念。這會(huì)使你的調(diào)用者對屬性有著一些潛意識(shí)的認(rèn)識(shí)。
例如,調(diào)用者會(huì)把屬性訪問當(dāng)成是數(shù)據(jù)的訪問。
不管怎樣,二者看上去很像描述性訪問器應(yīng)該滿足這些潛意識(shí)的預(yù)期。
get訪問器不應(yīng)該有可被觀察到的副作用。set訪問器會(huì)修改狀態(tài),用戶應(yīng)該可以看到調(diào)用后帶來的改變。
?
調(diào)用者也會(huì)對屬性訪問器的性能有著一定的預(yù)期。
屬性的訪問就像是訪問一個(gè)數(shù)據(jù)字段,因此不會(huì)與訪問數(shù)據(jù)由太過明顯的性能差別。
屬性訪問器不應(yīng)該執(zhí)行長時(shí)間的計(jì)算,或進(jìn)行跨應(yīng)用的調(diào)用(例如執(zhí)行數(shù)據(jù)庫查詢等),或是其他任何與調(diào)用者期待不符的耗時(shí)操作。
?
無論何時(shí)需要在類型的共有或保護(hù)接口中暴露數(shù)據(jù),都應(yīng)該使用屬性。你也應(yīng)該使用索引器來暴露序列或字典。
所有的數(shù)據(jù)成員都應(yīng)該是私有的,沒有任何例外。
這樣你就立即得到了數(shù)據(jù)綁定的支持,也便于日后瑞方法實(shí)現(xiàn)的各種修改。
對于將任何變量封裝到一個(gè)屬性所需的額外輸入工作其實(shí)不會(huì)占用太多時(shí)間,而日后若是需要使用屬性來更正設(shè)計(jì),則會(huì)花去大量的時(shí)間。
現(xiàn)在多投入一點(diǎn)點(diǎn),換來的是今后維護(hù)時(shí)的更加游刃有余。
?
總結(jié)
以上是生活随笔為你收集整理的1.1 - C#语言习惯 - 使用属性而不是可访问的数据成员的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: web的三种监听器
- 下一篇: OpenGL ES 2兼容函数列表