C#高编 - 运算符和类型强制转换
摘要
- C#中的运算符
- 处理引用类型和值类型时相等的含义
- 基本数据类型之间的数据转换
- 使用装箱技术把值类型转换为引用类型
- 通过类型强制转换在引用类型之间转换
- 重载标准的运算符以支持自定义类型
- 给自定义类型添加类型强制转换运算符
1.运算符
算数运算符:+ - * / %
逻辑运算符:& | ^ ~ && || !
字符串连接运算符:+
增量和减量运算符:++ --
移位运算符:<< >>
比较运算符:== != <> <= >=
赋值运算符:= += -= *= /= %= &= |= ^= <<= >>=
成员访问运算符:.
索引运算符:[]
类型转换运算符:()
条件运算符(三元运算符):?:
委托连接和删除运算符:+ -
对象创建运算符:new
类型信息运算符:sizeof is typeof as
溢出异常控制运算符:checked unchecked
间接寻址运算符:[]
名称空间别名限定符:::
空合并运算符:??
其中:sizeof、*、->、&:只能用于不安全的代码
2.checked和unchecked运算符
把一个代码块标记为checked,CLR就会执行溢出检查,如果发生溢出,就抛出OverflowException异常。如,
byte b = 255; checked { b++; } Console.WriteLine(b.ToString());
注意:用/checked编译器选项进行编译,就可以检查程序中所有未标记代码中的溢出。
相反,要禁止溢出检查,则用unchecked,不会抛出异常,但会丢失数据,溢出的位会别丢弃。
unchecked是默认行为,当需要在标记为checked的代码块中增加几行未检查的代码时才会使用。
3.is运算符
可以检查对象是否与特定的类型兼容,“兼容”表示对象或者该类型,或者派生自该类型。如,
int i = 10; if(i is object) { Console.WriteLine("i is an object"); }
4.as运算符
用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容,转换就会成功;如果类型不兼容,as运算符就会返回null值。
5.sizeof运算符
确定栈中值类型需要的长度(单位是字节)
对于复杂类型(和非基元类型)使用sizeof运算符,就需要把safe放在unsafe块中,如,
unsafe { Console.WriteLine(sizeof(Customer)); }
6.可空类型和运算符
通常可控类型与一元或二元运算符一起使用时,如果其中一个操作数或两个操作数都是null,其结果就是null。如
int? a = null; int? b = a+4;//b = null
在比较可空类型时,只要有一个操作数是null,比较结果就是false。即不能因为一个条件是false,就认为条件的对立面是true,这在使用非可空类型的程序中很常见。
7.空合并运算符
提供了一种快捷方式,可以在处理可空类型和引用类型时表示null可能的值。
第一个数必须是一个可空类型或引用类型;第二个数必须与第一个的类型相同,或者可以隐含地转换,
- 如果第一个操作不是null,整个表达式就等于第一个操作的值。
- 如果第一个操作数是null,整个表达式就等于第二个操作数的值。
如
int? a = null; int b; b = a ?? 10;//b has the value 10 a = 3; b = a ?? 10;//b has the value 3
8.类型转换
隐式转换:可以从较小的整数类型隐式转换成较大的整数类型,反之不可以。也可以在整数和浮点数之间转换,但会丢失4个字节的数据。
无符号的变量值可以转换为有符号的变量,只要大小在有符号的变量的范围之内即可。
可空类型转换为其它可空类型,如int?转换为long?,或者非可空类型转换为可空类型,遵循非可空类型的转换规则。
byte value1 = 10; byte value2 = 20; long value; total = value1 + value2; Console.WriteLine(total);
显式转换:
以下是部分不能隐式转换类型的场合,需要进行强制转换,如int i = (int)val;
int>>short、int>>uint、uint>>int、float>>int、任何数字>>char、decimal>>任何数字、int?>>int
使用checked运算符可以检查类型强制转换是否安全,不安全会迫使运行库抛出一个溢出异常 int i = checked((int)val);
所有的显式转换都可能不安全。如果从可空类型强制转换为非可空类型,且变量的值是null,就会抛出InvalidOperationException异常。
显式类型转换的限制:值类型只能在数字、char类型和enum类型之间转换,不能直接把布尔类型强制转换为其他类型,也不能把其他类型转换为布尔类型。
当需要分析字符串,以检索一个数字或布尔值,可以使用所有预定义值类型都支持的Parse()方法。
拆箱与装箱:值类型和引用类型之间的相互转换
装箱可以隐式地进行也可以显式地转换,拆箱是显式进行的。
当拆箱后得到的值变量无足够空间存储时,会导致一个InvalidCastException的异常。
9.比较对象的相等性
比较引用类型的相等性:
System.Object定义了3个方法来比较对象的相对性,ReferenceEquals()和两个版本的Equals()。再加上比较运算符==。
- 静态的ReferenceEquals()方法:测试两个引用是否引用类的同一个实例,是否包含内存中的相同地址,是则返回true,但它认为null等于null。
- 虚拟的Equals()方法:可以比较引用,可以在类中重写从而实现按值比较。如,如果希望类的实例用作字典的键就需要重写此方法。
- 静态的Equals()方法:与虚拟的版本作用相同,区别是带有两个参数,并对它们进行相等性比较。这个方法可以处理两个对象中有一个是null的情况。当有一个为null,可以抛出异常。
- 比较运算符(==):看做严格的值比较和严格的引用比较之间的中间选项。可以重写比较运算符(运算符重载),以执行值的比较,如System.String(微软重写了).
比较值类型的相等性:
在比较值类型的相等性时,采用与引用类型相同的规则:
- ReferenceEquals()用于比较引用,在比较值类型时,总是返回false,需要将值类型装箱才能比较引用。如bool b = ReferenceEquals(v,v);但是v会被单独装箱,所以结果仍为false,所以用它来比较值类型不好。
- Equals()用于比较值:微软在System.ValueType类中重载了实例方法Equals(),以便对值类型进行合适的相等性测试。如sA.Equals(sB);sA和sB是某个结构的实例,根据sA和sB是否在其所有的字段中包含相同的值返回true或false。也可重写,以提高性能。如果值类型包含作为字段的引用类型就需要重写,因为默认只比较地址。
- 比较运算符:值类型需要装箱,才能转换为引用,进而才能对它们执行方法。
10.运算符重载
重载不仅仅限于算术运算符,还需要考虑比较运算符==、<、>、!=、>=、<=
==运算符:默认比较内存地址、对于string,则重写为比较值、对于值类型默认不工作并产生编译错误,除非显式重载
运算符实现过程:查找最佳匹配的运算符重载版本>>查找可以将某个运算符重载版本参数隐式转换成当前参数的版本>>找不到则产生编译错误
运算符用于结构或类时,工作方式是一样的,如
//x表示到原点x方向距离,y表示到原点y方向距离,z表示高度 struct Vector { public double x,y,z; public Vector(double x,double y,double z)//用坐标初始化对象 { this.x = x; this.y = y; this.z = z; } public Vector(Vector rhs)//用三维矢量对象初始化 { x = rhs.x; y = rhs.y; z = rhs.z; } public override string ToString() { return "(" + "," + y + "," + z +")"; } //运算符重载方法 public static Vector operator + (Vector lhs,Vector rhs) { Vector result = new Vector(lhs); result.x += rhs.x; result.y += rhs.y; result.z += rhs.z; return result; } }
使用operator关键字:自定义的运算符重载。一般将运算符左边的参数命名为lhs,右边的的命名为rhs。
C#要求所有的运算符重载都声明为public和static,因此运算符重载的代码体不能访问非静态类成员,也不能访问this标识符。
与C++不同,C#不允许重载“=”运算符,但如果重载“+”运算符,编译器就会自动使用“+”运算符的重载来执行“+=”运算符的操作。-=、&=、*=、/=等所有赋值运算符也遵循此规则。
比较运算符的重载:
C#中有6个比较运算符,分为3对:
- ==和!=
- >和<
- >=和<=
C#语言要求成对重载比较运算符。即,如果重载了"==",也就必须重载"!=",否则会产生编译错误。
比较运算符必须返回布尔类型的值,否则没有意义。这是与算术运算符的根本区别。
public static bool operator ==(Vector lhs,Vector rhs) { if(lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z) return true; else return false; }
还需要重载运算符"!=",
public static bool operator !=(Vector lhs,Vector rhs) { return ! (lhs == rhs); }
浅度比较式比较对象是否执行内存中的同一个位置,而深度比较是比较对象的值和属性是否相等。
运算符 |
可重载性 |
---|---|
可以重载这些一元运算符。 |
|
可以重载这些二进制运算符。 |
|
比较运算符可以重载(但请参见本表后面的说明)。 |
|
条件逻辑运算符不能重载,但可使用能够重载的 & 和 | 进行计算。 |
|
不能重载数组索引运算符,但可定义索引器。 |
|
赋值运算符不能重载,但 += 可使用 + 计算,等等。 |
|
=, .,?:,??, - AMP_GT, =AMP_GT, f f(x),, checked, unchecked, default, delegate, is, new,sizeof, typeof |
不能重载这些运算符。 |
说明 |
---|
比较运算符(如果重载)必须成对重载;也就是说,如果重载 ==,也必须重载 !=。 反之亦然,< 和 > 以及 <= 和 >= 同样如此。 |
11.用户定义的类型强制转换
C#允许支持在自定义的数据类型之间进行类型强制转换。方法是把类型强制转换运算符定义为相关类的一个成员运算符,类型强制转换运算符必须标记为隐式或显式。
- 如果知道无论在源变量中存储什么值,类型强制转换总是安全的,就可以把它定义为隐式强制转换。//implicit
- 如果某些数值可能会出错,如丢失数据或抛出异常,就可以把它定义为显式强制转换。//explicit
定义类型强制转换的语法如下,必须同时声明为public和static,下面代码表示可以将Currenty对象隐式转换为float
public static implicit operator float(Currency value) { //processing }
示例
struct Currency { public uint Dollars; public ushort Cents; public Currency(uint dollars,ushort cents) { this.Dollars = dollars; this.Cents = cents; } public override string ToString() { return string.Format("${0}.{1,-2:00}",Dollars,Cents); } //隐式转换 public static implicit operator float(Currency value) { return value.Dollars + (value.Cents/100.0f); } //显式转换,但这样写会产生小数舍入错误,转换成ushort将截断(详见188页案例说明) //public static explicit operator Currentcy(float value) //{ //uint dollars = (uint)value; //ushort cents = (ushort)((value - dollars) * 100); //return new Currency(dollars,cents); //} //显式转换,将四舍五入转换为uInt16,使用checked抛出溢出异常 public static explicit operator Currency(float value) { checked { uint dollars = (uint)value; ushort cents = Convert.ToUInt16((value - dollars) * 100); return new Currency(dollars, cents); } }
}
使用checked检测溢出异常的代码不应该写在调用强制类型转换时,应给像上例一样放在强制转换方法体中,因为这是在强制转换运算符的代码中发生的。
类之间的类型强制转换:
定义不同的结构或类的实例之间的类型强制转换有两个限制:
- 如果某个类派生自另一个类,就不能定义这两个类之间的类型强制转换(这些类型的类型转换已经存在)
- 类型强制转换必须在源数据类型或目标数据类型的内部定义
如以下层次结构,从上到下依次继承,则唯一合法的自定义类型强制转换就是类C和D之间的转换,因为它们没有互相派生。
System.Object
|
A
|
B
/ \
C D
如:定义可以放在C的类定义内部,或者在D的类定义内部,但不能放在其他地方定义。一旦在一个类中定义,就不能在另一个类中定义。
public static explicit operator D(C value){...}
基类和派生类之间的类型强制转换:
MyDerived derivedObject = new MyDerived(); MyBase baseCopy = derivedObject;
从派生类隐式地强制转换为基类是可以的,因为对基类的任何引用都可以引用基类或派生类的对象。
将基类显式强制转换为派生类是不可以的,比较合适的方法是将基类作为参数在初始化派生类时传入,让构造函数完成初始化。
装箱和拆箱数据类型强制转换:
值类型与引用类型一致,都是支持从派生类到基类的隐式转换。本质上进行了装箱。
在使用装箱和拆箱时,这两个过程都把数据复制到新装箱和拆箱的对象上。
12.多重类型强制转换
如果在进行要求的数据类型转换时,C#编译器没有可用的直接强制转换方式,编译器会寻找一种转换方式,把几种强制转换合并起来。如,
Currency balance = new Currency(10,50); long amount = (long)balance; double amoutD = balance;
因为编译器知道存在Currency到float的隐式转换,而且它知道怎么从float显式地转换为long。
转换当存在多条路径时,C#有一些严格的规则(详见MSDN),告诉编译器该如何确定哪条是最佳路径,但最好自己设计类型强制转换,让所有转换都得到相同的结果(但没精度损失)。
13.当将类型强制转换和方法重载合并起来时,会出现不希望的错误源
如果方法调用带有多个重载方法,并要给该方法传送参数,而该参数的数据类型不匹配任何重载方法,就可以迫使编译器确定使用哪些强制转换方式进行数据转换,从而决定使用哪个重载方法(并进行相应的数据转换)。当然,编译器总是按逻辑和严格的规则来工作,但结果可能不是我们所期望的。如果存在疑问,最好指定显式地使用哪种强制转换。如,
Currency balance = new Currency(50,35); Console.WriteLine(balance);//50 Console.WriteLine(balance.ToString());//50.35 //因为Console.WriteLine找不到参数为Currency结构的重载版本,最后使用了参数是unit,显式unit的版本。