C#訪問器、索引器和運算符淺析


  C#語言雖然誕生不久,但卻因其高度的封裝性和對其他早期語言的良好兼容使程序員的代碼編寫變得輕松加愉快。本文將對C#里的訪問器、索引器及運算符進行簡單的探討。

  其實說道這些大家應該都有印象,即使剛剛接觸編程的朋友應該也或多或少地使用過這些語法,具體的定義和概念我就不再贅述,我們通過下面的例子來認識什么是訪問器: 

 1 using System;
2
3 namespace AccessorEG
4 {
5 public class Student
6 {
7 // 私有字段 private field
8 private int _age;
9
10 // 公開的屬性 public property
11 public int Age
12 {
13 get { return _age; }
14 set { _age = value; }
15 }
16 }
17
18 class Program
19 {
20 static void Main(string[] args)
21 {
22 Student stu = new Student();
23 stu.Age = 10; // 使用了修改
24 Console.WriteLine(stu.Age.ToString()); // 使用了讀取
25 Console.ReadKey();
26 // 輸出 10
27 }
28 }
29 }

  很好理解,訪問器就是指對象類型成員對外界的借口,就是使對象類型成員與外界進行信息交互的橋梁,有了訪問器,外界就能對對象成員進行讀、寫的對應操作。

  那么,什么成員能夠擁有訪問器呢?非只讀的字段和事件是可以聲明訪問器的。當然,只讀域也能提供被外界獲取的借口,即get,但是只能在聲明或構造函數中初始化,而且它並不支持提供set方法,除了將其對象替換,我實在找不出更改它的辦法。

 1 using System;
2
3 namespace AccessorEG
4 {
5 public class Student
6 {
7 // 私有字段 private field
8 private readonly int _age = 10;
9
10 // 公開的屬性 public property
11 public int Age
12 {
13 get { return _age; }
14 }
15 }
16
17 class Program
18 {
19 static void Main(string[] args)
20 {
21 Student stu = new Student();
22 Console.WriteLine(stu.Age.ToString()); // 使用了讀取
23 Console.ReadKey();
24 // 輸出 10
25 }
26 }
27 }

  上述代碼中只讀域的值在聲明時就已經賦了,而它對應公開屬性的訪問器中也不能提供set方法,不然會無法通過編譯,但是它可以被外界取得。

  關於字段的訪問器我們還要說一些,常見的有以下寫法:

 1 using System;
2
3 namespace AccessorEG
4 {
5 public class Student
6 {
7 #region 全訪問權限
8 // 私有字段
9 private int _age;
10 // 與_age對應的公開屬性,包含了set和get方法
11 public int Age
12 {
13 get { return _age; }
14 set { _age = value; }
15 }
16
17 // 如果您安裝了.NET3.0,那么您可以使用自動屬性,屆時,上面的代碼即可以下面的代替
18      // 在VS.NET下輸入 prop 連擊兩下Tab鍵,編譯器會自動幫您生成自動屬性
19      // public int Age { get; set; }
20 #endregion // 全訪問權限
21
22 #region 只讀屬性
23 private string _name;
24
25 public string Name
26 {
27 get { return _name; }
28 }
29
30 // 等同於
31      // public string Name { private set; get; }
32 #endregion
33
34 #region 只寫屬性
35 private bool _sex;
36
37 public bool Sex
38 {
39 set { _sex = value; }
40 }
41 // 等同於
42      // public bool Sex { set; private get; }
43 #endregion
44
45 }
46
47 class Program
48 {
49 static void Main(string[] args)
50 {
51 Student stu = new Student();
52 stu.Age = 18;
53 // stu.Name = "Johness"; 異常,編譯錯誤,因為該屬性只讀
54        // Console.WriteLine(stu.Sex.ToString()); 異常,編譯錯誤,因為該屬性只寫
55 Console.WriteLine(stu.Age.ToString()); // 使用了讀取
56 Console.ReadKey();
57 // 輸出 18
58 }
59 }
60 }

  以上示例中的只讀、只寫僅對外界有效,如果您顯示得制定了該訪問器的所有者,即類的私有字段。那么在類的內部,您仍可以方便的使用您定義的私有字段進行讀寫操作,因此,我建議朋友們定義字段及其訪問器使用.NET2.0的語法而不用3.0的新語法(自動屬性)。當然,利用訪問器也能更好地對數據有效性進行驗證:

 1 using System;
2
3 namespace AccessorEG
4 {
5 public class Student
6 {
7 // 私有字段
8 private int _age;
9 // 與_age對應的公開屬性,包含了set和get方法
10 public int Age
11 {
12 get { return _age; }
13 // 利用訪問器對輸入的年齡進行驗證
14        // 如果輸入值小於0或者大於100
15       // 可以賦為默認值18或者不進行操作
16 set
17 {
18 if (value >= 0 && value <= 100)
19 _age = value;
20 // 如果數據無效不進行操作可以注釋以下內容
21 else
22 _age = 18;
23 }
24 }
25
26 }
27
28 class Program
29 {
30 static void Main(string[] args)
31 {
32 Student stu = new Student();
33 stu.Age = -2; // 賦無效值
34 Console.WriteLine(stu.Age.ToString());
35 Console.ReadKey();
36 // 輸出 18
37 }
38 }
39 }

  字段的訪問器我們就介紹到這里,接下來看看事件的訪問器。事件的訪問器和字段的訪問器類似,基本上只是將set,get換成了add,remove。

  我們平時使用事件的時候只管一通+= 、-=,到了事件的訪問器了卻進行了如下操作:

 1 using System;
2
3 namespace AccessorEG
4 {
5 // 定義委托
6 public delegate void TestEventHandler();
7
8 public class Program
9 {
10 // 委托類型成員
11 private TestEventHandler _testEvent;
12 // 封裝過后的事件
13 public event TestEventHandler TestEvent
14 {
15 add
16 {
17 _testEvent += value;
18 }
19 remove
20 {
21 _testEvent -= value;
22 }
23 }
24 }
25 }

  使用過事件的朋友可能都知道,事件可以理解為是委托的封裝。那么這個add和remove就十分容易理解了,和set、get沒什么區別。

  但是事件的add和remove不能缺,和自動屬性有一點類似,而且add和remove必須有主體。

  但是您可以利用自定義事件訪問器實現如下操作:

 1 using System;
2
3 namespace AccessorEG
4 {
5 // 定義委托
6 public delegate void TestEventHandler();
7
8 public class Program
9 {
10 // 委托類型成員
11 private TestEventHandler _testEvent;
12 // 封裝過后的事件
13 public event TestEventHandler TestEvent
14 {
15 add
16 {
17 _testEvent += value;
18 }
19 remove { } // 1.您可以在add或remove主體內不進行任何操作
20
21        // add
22      // { // 2.您可以在add方法主體內更新委托綁定的方法而不是增加,即可以使委托(或事件)只綁定最后一個指定方法
23       // _testEvent = value;
24       // }
25       // remove { // …… }
26 }
27 }
28 }

  事件的訪問器沒什么需要贅述的,當然,更多的我也不知道……

  那么,下面我們看看C#的索引器語法有什么神奇的地方。

 1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4
5 namespace IndexerEG
6 {
7 public class Program
8 {
9 public static void Main(string[] args)
10 {
11 // 先聲明數組,多種方式可供選擇
12 int[] array = new int[3] { 1, 2, 3 };
13 // 由於數組不容易觀察,我們選用泛型集合List來查看索引器
14 IList<int> list = new List<int>(array);
15 Console.WriteLine(list[2].ToString());
16 // 輸出 3
17 }
18 }
19 }

  我們看一下系統定義的索引器方法簽名:

 1 // 摘要:
2 // 獲取或設置指定索引處的元素。
3 //
4 // 參數:
5 // index:
6 // 要獲得或設置的元素從零開始的索引。
7 //
8 // 返回結果:
9 // 指定索引處的元素。
10 //
11 // 異常:
12 // System.ArgumentOutOfRangeException:
13 // index 不是 System.Collections.Generic.IList<T> 中的有效索引。
14 //
15 // System.NotSupportedException:
16 // 設置該屬性,而且 System.Collections.Generic.IList<T> 為只讀。
17 T this[int index] { get; set; }

  從System.Collections.Generic命名空間下的IList<T>中可以看到,看起來,說它是方法是極不准確的了。它更像一個擁有訪問器的字段,只是擁有更多神奇的地方,我們可以大膽地分析一下(T在此處是泛型,泛指任意類型):

  在一個可排序或者可制定查找規則的對象容器(或稱之為集合、數組) 中,通過一個參數,按照某種特定排序或查找方式,將特定的位置的對象反回或修改其值。……說的自己都暈。好,廢話少說,我們來看看具體應用。

  其實大家不用太過在意語法,只需要知道方法簽名的含義,即傳入什么參數,返回什么值就可以了。

    下面我們來試一試自定義索引器: 

 1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4
5 namespace IndexerEG
6 {
7 public class Program
8 {
9 public static void Main(string[] args)
10 {
11 Test test = new Test();
12 Console.WriteLine(test[1]); // 通過索引器得到一個值並輸出
13 test[1] = "John"; // 通過索引器改變一個值
14 Console.WriteLine(test[1]); // 再次輸出
15 Console.ReadKey();
16 // 輸出 Johness2
17        // John
18 }
19 }
20
21 public class Test
22 {
23 // 定義用於實驗的字符串數組
24 string[] testStrings = { "Johness1", "Johness2", "Johness3", "Johness4", "Johness5" };
25 // 定義索引器
26 public string this[int index]
27 {
28 get
29 {
30 return testStrings[index];
31 }
32 set
33 {
34 testStrings[index] = value;
35 }
36 }
37 }
38 }

  在索引器的方法體內(姑且這樣說吧),set和get不是必須都有的,但聲明了之后就必須有方法體,當然,也就是要有大括號……

  一個類中只能聲明一個索引器(這僅僅是相對於入參而言),只是相對於父類繼承的索引器需要有一點注意:

 1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4
5 namespace IndexerEG
6 {
7 public class Program
8 {
9 public static void Main(string[] args)
10 {
11 // 調用子類的索引器
12 Test_Child test = new Test_Child();
13 Console.WriteLine(test[1]); // 通過索引器得到一個值並輸出
14 test[1] = "John"; // 通過索引器改變一個值
15 Console.WriteLine(test["Johness2"]);
16 Console.WriteLine(test[1]); // 再次輸出
17 Console.WriteLine("---------------------------------------");
18 // 輸出 Johness2
19        // -1
20        // John
21
22        // 調用父類的索引器,完成與子類的比較
23 Test_Base tb = new Test_Base(); // 父類引用保存父類變量
24 Test_Base tc = test; // 父類引用保存子類變量
25 Console.WriteLine(tb[2]); // 父類索引器被調用
26 Console.WriteLine(tc[2]); // 還是父類索引器被調用(因為子類相同入參的索引器向父類隱藏了!由於是實例成員,可能二義性的成員都會被隱藏)
27 Console.WriteLine((tc as Test_Child)[2]); // 顯式地調用子類索引器
28 Console.ReadKey();
29 // 輸出 3
30        // 3
31      // Johness3
32 }
33 }
34
35 public class Test_Child:Test_Base
36 {
37 // 定義用於實驗的字符串數組
38 string[] testStrings = { "Johness1", "Johness2", "Johness3", "Johness4", "Johness5" };
39
40 // 一個類中可以定義多個索引器
41      // 可以理解為索引器的重載
42      // 只要入參類型不同即可(所有索引器的入參集合中)
43      // 這意味着您若添加以下代碼也能通過編譯並運行正常
44      // public int this[double index] { get { return 0; } }
45      // public string this[char index] { get { return null; } }
46 public int this[string index]
47 {
48 get
49 {
50 for (int i = 0; i < testStrings.Length; i++)
51 {
52 if (testStrings[i] == index)
53 {
54 return i;
55 }
56 }
57 return -1;
58 }
59 }
60
61 // 定義索引器
62      // 由於與父類的索引器入參一樣,編譯器會報警告
63     // 可以添加new修飾符,當然,如果不添加也不會有問題
64 new public string this[int index]
65 {
66 get { return testStrings[index]; }
67 set { testStrings[index] = value; }
68 }
69
70 }
71
72 // 父類
73 public class Test_Base
74 {
75 int[] testInts = {1,2,3,4,5,6 };
76 // 只讀的索引器
77 public int this[int index]
78 {
79 get { return testInts[index]; }
80 }
81 }
82 }

  關於索引器的就是這些,索引器是類的特殊成員,也擁有類似訪問器的set和get,它可以重載,可以從父類繼承。它無法訪問靜態成員。

  嚴格來說,一個類只有一個索引器,所謂重載只是改變入參和出參的類型,切記:出參類型可以重復,但入參不行(在整個類中)。除非是該類實例成員與其父類所繼承所擁有的。

  下面,我們將簡要地分析C#里的運算符,並探討自定義運算符的可行性與應用。

  我們先從系統API中觀察運算符的些許特點: 

 1 // 摘要:
2 // 將指定的日期和時間與另一個指定的日期和時間相減,返回一個時間間隔。
3 //
4 // 參數:
5 // d1:
6 // System.DateTime(被減數)。
7 //
8 // d2:
9 // System.DateTime(減數)。
10 //
11 // 返回結果:
12 // System.TimeSpan,它是 d1 和 d2 之間的時間間隔,即 d1 減去 d2。
13 [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
14 public static TimeSpan operator -(DateTime d1, DateTime d2);
15 public static DateTime operator -(DateTime d, TimeSpan t);
16 public static bool operator !=(DateTime d1, DateTime d2);
17 public static DateTime operator +(DateTime d, TimeSpan t);
18 public static bool operator <(DateTime t1, DateTime t2);
19 public static bool operator ==(DateTime d1, DateTime d2);
20 public static bool operator >(DateTime t1, DateTime t2);
21 public static bool operator >=(DateTime t1, DateTime t2);

  以上是系統API中自帶運算符的方法簽名(部分注釋省略),出處為System.DateTime;

  看得出,以上代碼就是方法原型。運算符相當於方法名,前面的是關鍵字作為限定和聲明,它也有入參和出參。入參是括號中的兩個值,使用時在該運算符的一前一后,出參當然就是operator關鍵字前面的了,在使用時通過整體的返回值獲得出參。

  下面我們通過自定義運算符來詳細了解,並提出和解決問題:

 1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5
6 namespace OperatorEG
7 {
8 class Program
9 {
10 static void Main(string[] args)
11 {
12 Test1 test1 = new Test1();
13 Test1 test2 = new Test1();
14 Console.WriteLine(test1 == test2); // 使用運算符操作
15 Console.WriteLine(test1 != test2); // 使用運算符操作
16 Console.ReadKey();
17 // 輸出 True
18        // False
19 Console.WriteLine(test1.MyProperty);
20 }
21 }
22
23 class Test1
24 {
25 public int MyProperty { get; set; }
26 // 在編譯之后會有以下警告,通常,他們不會造成不良影響
27      // 警告 “OperatorEG.test”定義運算符 == 或運算符 !=,但不重寫 Object.Equals(object o)
28      // 警告 “OperatorEG.test”定義運算符 == 或運算符 !=,但不重寫 Object.GetHashCode()
29 public static bool operator !=(Test1 current, Test1 other) { return false; }
30 public static bool operator ==(Test1 current, Test1 other) { return true; }
31 // 如果==和!=沒有成對出現,會報出異常
32      // 比如將!=運算符去除
33      // 則會出現以下錯誤
34      // 錯誤 運算符“OperatorEG.test.operator ==(OperatorEG.test, OperatorEG.test)”要求也要定義匹配的運算符“!=”
35      // >和<運算符也必須成對出現
36      // >=和<=也是如此
37      // -和+不需要
38 }
39 }

  在一系列自定義運算符的操作中,編譯器告訴我們:運算符必須是public以及static的。有一些運算符是不能重載的,如+=……

  由於筆者技術有限請大家參見http://msdn.microsoft.com/zh-cn/library/8edha89s.aspx(可重載運算符(C# 編程指南))

  http://msdn.microsoft.com/zh-cn/library/6fbs5e2h.aspx(如何:使用運算符重載創建復數類(C# 編程指南))

  http://msdn.microsoft.com/zh-cn/library/s53ehcz3.aspx(運算符(C# 參考))

  http://msdn.microsoft.com/zh-cn/library/6a71f45d.aspx(C# 運算符)

  不過我的資料基本來自自己的經驗,我能告訴朋友們的還有:一元運算符采用一個參數,二元運算符需采用兩個參數,一般情況下,其中之一須是包含類型。 

  先貼圖一張:

  

  

  以上圖片信息來自網絡。

   我們再看一個有意思的東西:

 1 using System;
2
3 namespace OperatorEG
4 {
5 class Test2
6 {
7 public static bool operator ==(Test2 str1,string str2) { return false; }
8 public static bool operator !=(Test2 str1, string str2) { return true; }
9 public static bool operator !=(Test2 str1, Test2 str2) { return true; }
10 public static bool operator ==(Test2 str1, Test2 str2) { return true; }
11 }
12 }

  以上代碼是無措的(至少是編譯時)。

  當然,在絢麗的技術如果我們不能運用或者不能掌握,也是枉然。那么,運算符的自定義重載對我們編程有什么幫助嗎?或者,我們可以在什么時候運用呢?  

  首先應該說明的是:自定義運算符重載是不被推薦的。

  而具體的應用我現在還不能舉出比較好的例子,不過聽說過可能的應用:在數據表中的姓名字段上加上++運算符直接拿到下一條數據……朋友們可以自由發揮啊! 


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM