索引器
索引器(Indexer)是C#引入的一个新型的类成员它使得对象可以像数组那样被方便直观的引用索引器非常类似于我们前面讲到的属性但索引器可以有参数列表且只能作用在实例对象上而不能在类上直接作用下面是典型的索引器的设计我们在这里忽略了具体的实现
class MyClass{ public object this [int index] { get { // 取数据 } set { // 存数据 } }}
索引器没有像属性和方法那样的名字关键字this清楚地表达了索引器引用对象的特征和属性一样value关键字在set后的语句块里有参数传递意义实际上从编译后的IL中间语言代码来看上面这个索引器被实现为
class MyClass{ public object get_Item(int index) { // 取数据 } public void set_Item(int index object value) {//存数据 }}
由于我们的索引器在背后被编译成get_Item(int index)和set_Item(int index object value)两个方法我们甚至不能再在声明实现索引器的类里面声明实现这两个方法编译器会对这样的行为报错这样隐含实现的方法同样可以被我们进行调用继承等操作和我们自己实现的方法别无二致通晓C#语言底层的编译实现为我们下面理解C#索引器的行为提供了一个很好的基础
和方法一样索引器有种存取保护级别和种继承行为修饰以及外部索引器这些行为同方法没有任何差别这里不再赘述唯一不同的是索引器不能为静态(static)这在对象引用的语义下很容易理解值得注意的是在覆盖(override)实现索引器时应该用base[E]来存取父类的索引器
和属性的实现一样索引器的数据类型同时为get语句块的返回类型和set语句块中value关键字的类型
索引器的参数列表也是值得注意的地方索引的特征使得索引器必须具备至少一个参数该参数位于this关键字之后的中括号内索引器的参数也只能是传值类型不可以有ref(引用)和out(输出)修饰参数的数据类型可以是C#中的任何数据类型C#根据不同的参数签名来进行索引器的多态辨析中括号内的所有参数在get和set下都可以引用而value关键字只能在set下作为传递参数
下面是一个索引器的具体的应用例子它对我们理解索引器的设计和应用很有帮助
using System;class BitArray{int[] bits;int length;public BitArray(int length) {if (length < ) throw new ArgumentException();bits = new int[((length ) >> ) + ];thislength = length;}public int Length {get { return length; }}public bool this[int index] {get {if (index < || index >= length) throw new IndexOutOfRangeException();elsereturn (bits[index >> ] & << index) != ;}set{if (index < || index >= length)throw new IndexOutOfRangeException();else if(value) bits[index >> ] |= << index;elsebits[index >> ] &= ~( << index);}}}class Test{static void Main() {BitArray Bits=new BitArray();for(int i=;i<;i++)Bits[i]=(i%)==; ConsoleWrite(Bits[i]+ );}}
编译并运行程序可以得到下面的输出
True False True False True False True False True False
上面的程序通过索引器的使用为用户提供了一个界面友好的bool数组同时又大大降低了程序的存储空间代价索引器通常用于对象容器中为其内的对象提供友好的存取界面这也是为什么C#将方法包装成索引器的原因所在实际上我们可以看到索引器在NET Framework类库中有大量的应用
操作符重载
操作符是C#中用于定义类的实例对象间表达式操作的一种成员和索引器类似操作符仍然是对方法实现的一种逻辑界面抽象也就是说在编译成的IL中间语言代码中操作符仍然是以方法的形式调用的在类内定义操作符成员又叫操作符重载C#中的重载操作符共有三种一元操作符二元操作符和转换操作符并不是所有的操作符都可以重载三种操作符都有相应的可重载操作符集列于下表
一元操作符+ ! ~ ++ true false
二元操作符+ * / % & | ^ << >> == != > < >= <=
转换操作符隐式转换()和显式转换()
重载操作符必须是public和static 修饰的否则会引起编译错误这在操作符的逻辑语义下是不言而喻的父类的重载操作符会被子类继承但这种继承没有覆盖隐藏抽象等行为不能对重载操作符进行virtual sealed override abstract修饰操作符的参数必须为传值参数我们下面来看一个具体的例子
using System;class Complex{double r v; //r+ v ipublic Complex(double r double v){thisr=r;thisv=v;}public static Complex operator +(Complex a Complex b) {return new Complex(ar+br av+bv);}public static Complex operator (Complex a){return new Complex(arav);}public static Complex operator ++(Complex a) { double r=ar+; double v=av+;return new Complex(r v);}public void Print(){ConsoleWrite(r+ + +v+i);}}class Test{public static void Main(){Complex a=new Complex();Complex b=new Complex();Complex c=a;cPrint();Complex d=a+b;dPrint();aPrint();Complex e=a++;aPrint();ePrint();Complex f=++a;aPrint();fPrint();}}
编译程序并运行可得到下面的输出
+ i + i + i + i + i + i + i
我们这里实现了一个+号二元操作符一个号一元操作符(取负值)和一个++一元操作符注意这里我们都没有对传进来的参数作任何改变这在参数是引用类型的变量是尤其重要虽然重载操作符的参数只能是传值方式而我们在返回值时往往需要new一个新的变量除了true和false操作符这在重载++和 操作符时尤其显得重要也就是说我们做在a++时我们将丢弃原来的a值而取代的是新的new出来的值给a! 值得注意的是e=a++或f=++a中e的值或f的值根本与我们重载的操作符返回值没有一点联系!它们的值仅仅是在前置和后置的情况下获得a的旧值或新值而已!前置和后置的行为不难理解
操作符重载对返回值和参数类型有着相当严格的要求一元操作符中只有一个参数操作符++和返回值类型和参数类型必须和声明该操作符的类型一样操作符+ ! ~的参数类型必须和声明该操作符的类型一样返回值类型可以任意true和false操作符的参数类型必须和声明该操作符的类型一样而返回值类型必须为bool而且必须配对出现也就是说只声明其中一个是不对的会引起编译错误参数类型的不同会导致同名的操作符的重载实际上这是方法重载的表现
二元操作符参数必须为两个而且两个必须至少有一个的参数类型为声明该操作符的类型返回值类型可以任意有三对操作符也需要必须配对声明出现它们是==和!=>和<>=和<=需要注意的是两个参数的类型不同虽然类型相同但顺序不同都会导致同名的操作符的重载
转换操作符为不同类型之间提供隐式转换和显式转换主要用于方法调用转型表达和赋值操作转换操作符对其参数类型(被转换类型)和返回值类型(转换类型)也有严格的要求参数类型和返回值类型不能相同且两者之间必须至少有一个和定义操作符的类型相同转换操作符必须定义在被转换类型或转换类型任何其中一个里面不能对系统定义过的转换操作进行重新定义两个类型也都不能是object或接口类型两者之间不能有直接或间接的继承关系这三种情况系统已经默认转换我们来看一个例子
using System;public struct Digit{byte value;public Digit(byte value) {if (value < || value > ) throw new ArgumentException();thisvalue = value;}public static implicit operator byte(Digit d) {return dvalue;}public static explicit operator Digit(byte b) {return new Digit(b);}}
上面的例子提供了Digit类型和byte类型之间的隐式转换和显式转换从Digit到byte的转换为隐式转换转换过程不会因为丢失任何信息而抛出异常从byte到Digit的转换为显式转换转换过程有可能因丢失信息而抛出异常实际上这也为我们揭示了什么时候声明隐式转换什么时候声明显示转换的设计原则不能对同一参数类型同时声明隐式转换和显式转换隐式转换和显式转换无需配对使用虽然C#推荐这样做
实际上可以看到对于属性索引器和操作符这些C#提供给我们的界面操作都是方法的某种形式的逻辑抽象包装它旨在为我们定义的类型的用户提供一个友好易用的界面我们完全可以通过方法来实现它们实现的功能理解了这样的设计初衷我们才会恰当正确地用好这些操作而不致导致滥用和错用