C#高级编程笔记2016年10月12日 运算符重载
1、运算符重载:运算符重重载的关键是在对象上不能总是只调用方法或属性,有时还需要做一些其他工作,例如,对数值进行相加、相乘或逻辑操作等。例如,语句if(a==b)。对于类,这个语句在默认状态下会比较引用 a 和 b 。检测这两个引用是否指向内存中的同一个地址,而不是检测两个实例是否包含相同的数据。然而对于 string 类,这种操作就会重写,于是比较字符串实际上就是比较每个字符串的内容。可以对自己的类进行这样的操作。 对于结构,“==” 运算符在默认状态下是不做任何工作。试图比较两个结构,看看它们是否相等,就会产生一个编译错误。除非显示地重载了“==”,告诉编译器如何进行比较。
运算符的工作方式
-
int myInteger=3; uint myUnsignedInt=2; double myDouble=4.0; long myLong=myInteger+myUnsignedInt; double myOtherDouble=myDouble+myInteger;
当编译器遇到下面这行代码时会发生什么情况:
-
long myLong=myInteger+myUnsignedInt;
编译器知道它需要把两个整数加起来,并把结果赋予一个long 型变量。调用一个方法把数字加在一起时,表达式myInteger +myUnsignedInt 是一种非常直观和方便的语法。该方法接受两个参数 myInteger 和myUnsignedInt, 并返回他们的和。所以编译器完成的任务与任何方法调用一样——它会根据参数的类型查找最匹配的“+”运算符重载,这里是带两个整数参数的“+”运算符重载。与一般的重载方法一样,预定义的返回类型不会因为编译器所调用方法的哪个版本而影响编译器的选择。
-
double myOtherDouble=myDouble+myInteger;
在这个例子中,参数是一个double 类型的数据和一个 int 类型的数据,但“+”运算符没有带这种复合参数的重载形式,所以编译器认为,最匹配的“+”运算符重载是把两个 double 作为其参数的版本,并隐式地把int强制转换为 double。 把两个 double 加在一起与把两个整数加在一起完全不同,浮点数存储为一个尾数和一个指数。把她们加在一起要按位移动一个double的尾数,从而使两个指数有相同的值,然后把尾数加起来,移动所得尾数的数,调整其指数,保证答案有尽可能高的精度。
- 下面是运算符重载的例子 Vector结构。(在运算符重载时,结构和类的工作方式是一样的。)
- Vector 是一个三维矢量比如 a(1.0,2.0,3.0) b(-1.0,3.0,-4.0) 相加得到的结果 c=a+b 为(0,5,-1)
-
1 namespace Com.Test.Yinxi 2 { 3 struct Vector 4 { 5 public double x,y,z; 6 7 public Vector(double x,double y,double z) 8 { 9 this.x=x; 10 this.y=y; 11 this.z=z; 12 } 13 14 public Vector(Vector rhs) 15 { 16 x=rhs.x; 17 y=rhs.y; 18 z=rhs.z; 19 } 20 21 public override string ToString() 22 { 23 return "("+x+", "+y+", "+z+")"; 24 }
下面是Vector的运算符提供运算符重载支持
-
public static Vector operator +(Vector lhs,Vector rhs) { Vector result=new Vector(lhs); result.x+=rhs.x; result.y+=rhs.y; reault.z+=rhs.z; return result; }
c#要求所有的运算符重载都声明为public 和 static ,这表示他们与它们的类或结构相关联,而不是与某个特定实例相关联,所以运算符重载的代码体不能访问非静态成员,也不能访问this标识符;这是可以的,因为参数提供了运算符执行其任务所需要知道的所有输入数据。
完整代码如下:
-
1 using System; 2 3 namespace CSharpTest 4 { 5 class OperatorTest 6 { 7 public double x, y, z; 8 9 public OperatorTest(double x, double y, double z) 10 { 11 this.x = x; 12 this.y = y; 13 this.z = z; 14 } 15 16 public OperatorTest(OperatorTest oper) 17 { 18 x = oper.x; 19 y = oper.y; 20 z = oper.z; 21 } 22 23 public override string ToString() 24 { 25 return "(" + x + ", " + y + ", " + z+")"; 26 } 27 public static OperatorTest operator + (OperatorTest lhs, OperatorTest rhs) 28 { 29 OperatorTest oper = new OperatorTest(lhs); 30 oper.x += rhs.x; 31 oper.y += rhs.y; 32 oper.z += rhs.z; 33 return oper; 34 } 35 36 public static void Main(string[] args) 37 { 38 OperatorTest oper1 = new OperatorTest(1.0, 2.0, 3.0); 39 OperatorTest oper2 = new OperatorTest(-1.0, -3.5, -5.0); 40 //OperatorTest oper3 = new OperatorTest(2.0, 3.0, 1.0); 41 OperatorTest oper3; 42 oper3 = oper1 + oper2; 43 Console.WriteLine(oper1.ToString()); 44 Console.WriteLine(oper2.ToString()); 45 Console.WriteLine(oper3.ToString()); 46 47 } 48 } 49 50 }
算术运算符重载的 声明方式
-
//1、 public static double operator *(operatorTest lhs,operatroeTest rhs) { //...... } 可以根据自己所需要的,所要得到的重载结果 相应的改变 double 函数返回值
比较运算符的重载
C#中共有6个比较运算符 它们分为3对
- == 和!=
- > 和<
- >=和<=
c#要求成对重载比较运算符,即,如果重载了“==”,也就必须重载“!=”;否则会产生编译错误。另外,比较运算符必须返回布尔类型的值。这是它们与算术运算符的根本区别,
在重载 “==” 和“!=” 时,还必须重载从 System.Object 中继承的 Equals() 和GetHashCode() 方法,否则会产生一个编译警告。原因是Equals()方法应实现与“==”运算符相同类型的相等逻辑。
除了这些区别外,重载比较运算符所遵循的规则与重载算术运算符相同。但比较两个数并不像想象的那么简单,例如,如果只比较两个对象引用,就是比较存储对象的内存地址。比较运算符很少进行这样的比较,所以必须编写代码重载运算符,比较对象的值,并返回相应的布尔结果。
-
1 public static bool operator ==(OperatorTest lhs,OperatorTest rhs) 2 { 3 if(lhs.x==rhs.x&&lhs.y==rhs.y&&lhs.z==rhs.z) 4 { 5 return true; 6 } 7 else 8 { 9 return false; 10 } 11 }
这种方式仅根据矢量元素的值,来对它们进行相等性的比较。对于大多数结构,这就是我们希望的,但在某些情况下,可能需要仔细考虑相等的含义。例如,如果有嵌入的类,那么是应比较引用是否指向同一个对象(浅度比较),还是应比较对象的值是否相等(深度比较)?
- 浅度比较是比较对象是否指向内存中的同一个位置,而深度比较是比较对象的值和属性是否相等。应根据具体情况进行相等检查,从而有助于确定要验证什么。
-
不要通过调用从 System.Object 中继承的 Equals()方法的实例版本,来重载比较运算符。如果这么做,在 objA 是 null 时判断 (objA==objB),就会产生一个异常,因为.NET
运行库会试图判断null.Equals(objB)。采用其他方法(重写 Equals()方法以调用比较运算符)比较安全public static bool operator !=(OperatorTest lhs,OperatorTest rhs) { return !(lhs==rhs); }
可以重载的运算符:
类别 | 运算符 | 限制 |
算术二元运算符 | + 、*、 /、 -、 % | 无 |
算术一元运算符 | +、 -、 ++、 -- | 无 |
按位二元运算符 | &、|、^、<<、>> | 无 |
按位一元运算符 | !、~、true、false | true 和 false 必须成对重载 |
比较运算符 | ==、!=、>=、<=、<、> | 必须成对重载 |
赋值运算符 | += 、-=、 *=、 /=、 >>=、 <<=、 %=、 |=、 ^= | 不能显式地重载这些运算符,在重写单个运算符(如+,-,%等)时,它们会被隐式地重写。 |
索引运算符 | [] | 不能直接重载索引运算符。索引器成员类型允许在类和结构上支持索引运算符 |
数据类型强制转换运算符 | () | 不能直接重载类型强制转换运算符。用户定义的类型强制转换允许定义定制的类型强制转换行为 |
2、用户定义的类型强制转换
定义类型强制转换的语法类似于重载运算符,类型强制转换在某种情况下可以看做是一种运算符默,其作用是从源类型转换为目标类型。
public static implicit operator float(Currency value)//implicit 隐式 explicit 显式 { //.... }
运算符的返回类型定义了类型强制转换操作的目标类型,它有一个参数,即要转换的源对象。这里定义的类型强制转换可以隐式地把Currency 型的值转换为float 型。 注意,如果数据类型转换声明为隐式,编译器就可以隐式或显式地使用这个转换。如果数据类型转换声明为显式,编译器就只能显式地使用它。与其他运算符重载一样,类型强制转换必须同时声明为public 和static。
3、装箱和拆箱数据类型强制转换
- 装箱
-
Currency balance =new Currency(40,0); object baseCopy=balance;
在执行上述隐式地强制转换时,balance的内容被复制到堆上,放在一个装箱的对象上,baseCopy 对象引用被设置为该对象。实际上在后台发生的情况是:在最初定义Currency结构时,.NET Framework 隐式地提供另一个(隐藏的)类,即装箱的Currency 类,它包含于Currency 结构相同的所有字段,但它是一个引用类型,存储在堆上。无论定义的这个值类型是一个结构还是一个枚举,定义它时都存在类似的装箱引用类型,对应于所有的基元值类型,如 int double 和 uint 等。不能也不必再源代码中直接通过编程访问某些装箱类,但在把一个值类型强制转换为object 时,它们实在后台工作的对象。在隐式地把currency转换为object时,会实例化一个装箱的Currency实例,并用Currency结构中的所有数据进行初始化。在上面的代码中,baseCopy对象引用的就是这个已装箱的currency实例。通过这种方式,就可以实现从派生类型到基类型的强制转换,并且,值类型的语法与引用类型的语法一样。
-
-
1 object derivedObject=new Currency(40,0); 2 object baseObject=new Object(); 3 Currency derivedCopy1=(Currency)derivedObject;//可以转换 4 Currency derivedCopy2=(Currency)baseObject;//抛出异常
上述代码的工作方式与前面的引用类型中的代码一样。把derivedObject 强制转换为 Currency 会成功进行,因为derivedObject 实际上引用的是装箱 Currency实例——强制转换的过程是把已装箱的Currency对象的字段复制到一个新的currency 结构中。第二种强制转换会失败,因为 baseObject没有引用已装箱的Currency 对象。
在使用装箱和拆箱时,这两个过程都把数据复制到新装箱或拆箱的对象上。对装箱对象的操作就不会影响原始值类型的内容。