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 }
以上代码是无措的(至少是编译时)。
当然,在绚丽的技术如果我们不能运用或者不能掌握,也是枉然。那么,运算符的自定义重载对我们编程有什么帮助吗?或者,我们可以在什么时候运用呢?
首先应该说明的是:自定义运算符重载是不被推荐的。
而具体的应用我现在还不能举出比较好的例子,不过听说过可能的应用:在数据表中的姓名字段上加上++运算符直接拿到下一条数据……朋友们可以自由发挥啊!