1、运算符
我们来看看一些常见的运算符:
<1>条件运算符
其语法如下:
Condition ? true_Expression : false_Expression
当条件Condition为真时,其将执行true_Expression,否则执行false_Expression。
<2> checked 和 unchecked运算符
使用语法如下:
checked
{
//代码块
}
使用checked运算符可以检查被标记的代码块中是否有溢出。如果发生溢出,则抛出OverflowException异常。如果我们要禁止溢出检查,则使用unchecked运算符。
注意:unchecked是默认行为。如果没有被显式的标记为checked,则都默认是unchecked。
<3> is 运算符
is 运算符可以检查对象是否与特定类型兼容。例如,我们有两个对象A和B。如果A和B兼容,那么表达式A is B将为true,否则就为false。兼容表示的是,A和B是同一类型或者它们之间具有派生关系。
<4> as 运算符
as 运算符用于执行引用类型的显式转换。如果与要转换的类型兼容,转换就会成功进行,否则as运算符就会返回null值。
<5> sizeof 运算符
sizeof运算符可以确定栈中值类型的长度(单位:字节)。如果对非基本类型使用sizeof运算符,就需要把代码放入unsafe块中。
<6>typeof运算符
typeof运算符可以返回一个特定类型的System.Type对象。例如,typeof(string)返回表示System.String类型的Type对象。
<7>可空类型和运算符
通常可空类型使用一元或二元运算符,只要有一个操作数为null,那么结果就为null。在比较可空类型时,只要有一个操作数为null那么比较结果就是false。所以不能因为一个条件是false其对立面就是true。
<8>空合并运算符(??)
空合并运算符(??)为可空类型转换为非空类型提供了便利。我们以int的可空类型为例:
int? a = null;
int b = a ?? 10; //这时b就等于10
a = 3;
b = a ?? 10; //这时b就等于3
2、比较对象的相等性
根据比较机制的不同,我们可以分为引用类型的比较和值类型的比较:
<1>比较引用类型的相等性
System.Object提供了4种方式来比较对象的相等性。
①ReferenceEquals()方法
ReferenceEquals()方法是一个静态方法,用于测试两个引用是否为同一个实例。作为静态方法,所以它不能被重写。
②虚拟的Equals()方法
虚拟的Equals()方法,这它默认的比较引用。不过因为它是虚拟的方法,所以我们可以重写它,使它按值来比较对象。
③静态的Equals()方法
Equals()的静态版本与虚拟版本的功能一致,区别:静态版本带有两个参数,可以处理两个对象中有一个是null的方法。但是如果比较两个引用,它就相当于调用Equals()的虚拟版本,即相当于重写了静态的Equals()方法
④比较运算符(==)
通常引用类型使用比较运算符(没有重载前)都是比较引用(System.String除外,因为.Net对String重写了比较运算符)。
<2>比较值类型的相等性
在比较值类型的相等性时,与引用类型相同:ReferenceEquals()用于比较引用,Equals用于比较值,对于基本类型==则是比较值,然而结构需要重载==运算符才可以进行比较。
注意:ReferenceEquals()方法引用于值类型时,它总是false。因为调用这个方法,值类型需要被装箱到对象中,故不会得到相同的引用。
3、运算符重载
在很多时候,我们使用运算符来表示一个表达式,会使我们的程序变得更加简洁易懂,另一方面运算符的重载还可以提升我们的开发效率。假设我们定义了一个矩阵类Matrix的对象a, b, c。假设我们来表达 c = a + b,用方法来表达的话可能是这样 c = Matrix.Add(a, b); 相信通过这个例子大家应该都了解了运算符重载的好处了吧。
现在我们来看看运算符重载的语法吧:
//参数列表中的参数个数,取决于重载的运算符是几元运算符
public static [返回的类型] operator [重载的运算符] (参数列表)
{
//processing
}
知道了重载的语法我们现在来实战一下:
//我们以向量vector类为例,假设它有三个整型字段x, y, z, 定义向量加法
public static vector operator + (vector lhs, vector rhs)
{
vector ans = new vector();
ans.x = lhs.x + rhs.x;
ans.y = lhs.y + rhs.y;
ans.z = lhs.z + rhs.z;
return ans;
}
这时我们就完成了vector类的+运算符的重载,现在我们就可以直接使用+进行加法运算。同时编译器还自动完成了+=的重载。
还有两个运算符我们不能直接重载,分别是索引运算符和强制转换运算符。
我们先看看索引运算符的重载吧,语法如下:
[访问属性] [返回类型] this[int index]
{
get { //添加返回第index个元素的代码 }
set{ //设置第index个元素的值 }
}
我们现在以链表类(LinkedList)为例:假设LinkedList基本元素类型为LinkedListNode,且有Query(int k)方法用于返回第k个元素的引用和Mod(int k, LinkedListNode node)方法用于修改第k个元素的值。所以我们重载索引运算符,如下:
public LinkedListNode this[int index]
{
get
{
return Query(index);
}
set
{
Mod(index, value);
}
}
我们已经完成了索引运算符的重载,现在我们可以直接使用下标进行访问集合的元素了。
注意:在C# 中比较运算符必须成对的重载。对于自增运算符(++)或者自减运算符(--)运算符,在C# 中只要重载后置++,那么编译器就会自动重载好前置++,同理自减运算符(--)也一样。
4、类型强制转换
在很多时候我们不能保证所有操作数的类型相同,这是我们就需要强制转换。然而强制转换又分为显式强制转换和隐式强制转换。不同数据类型之间的转换有所不同,强制转换可以分为五类:
<1>预定义类型强制转换
在预定义的数据类型执行强制转换,我们只需谨记一个准则就好:
大数据转换为小数据需要显式强制转换(因为这是不安全的,可能会丢失数据),小数据转换为大数据可以显式强制转换也可以隐式的(因为它总是安全的)。
<2>装箱和拆箱
在强制转换的过程中往往会遇到值类型和引用类型之间的相互转换。然而因为值类型在栈上,引用类型在托管堆上。这是他们要互相转换就需要装箱和拆箱的操作了。
装箱(boxing):在堆上创建一个临时的引用类型”箱子”,把值类型装到”箱子”里。
拆箱(unboxing):与装箱相反的一个过程。把之前装进”箱子”里的值类型变回原样。
注意:装箱(值类型转换为引用类型)可以显式转换也可以隐式转换。然而拆箱必须为显式强制转换。且拆箱时,必须保证现有的值类型变量必须有足够的空间储存拆箱的值的所有字节。否则将会抛出InvalidCastException异常。
<3>基类与派生类之间的强制转换
编译器提供了基类和派生类之间的强制转换,但是事实上这种转换并没有对对象进行任何数据转换。只是改变了对象的引用。因为一个基类引用可以引用一个派生类的实例。所以就可以完成派生类对基类的转换。如果基类引用的对象不是派生类的对象,那么基类转换为派生类将会失败,并且抛出一个异常。
我们用一段代码来说明(MyBase为基类,MyDerived为MyBase的派生类):
MyBase B1 = new MyDerived(); //隐式的从派生类转换为基类
MyBase B2 = new MyBase();
MyDerived D1 = (MyDerived)B1; //成功
MyDerived D2 = (MyDerived)B2 //抛出异常
<4>自定义的类型强制转换
自定义的类型强制转换,类似于重载运算符。假设我们要从类型A转换为类型B类型。那么我们定义以强制转换的语法如下:
public static [强制类型转换方式] B (A value)
{
//processing
}
强制转换方式分为显示转换(explicit)和隐式转换(implicit)。
这里我们用一个例子来了解自定义的类型强制转换。我们以之前的vector类为例,假设我们vector显式强制转换为double(计算向量的模长的平方)。
public static explicit double(vector v)
{
return (v.x*v.x + v.y*v.y + v.z*v.z);
}
假设我们有vector的一个对象Test和一个double型的 Len。
Len = (double)Test; //现在我们就完成了自定义类型强制转换的调用
注意:在不断的转换的过程中,有时因为数据类型的精度不够会造成精度的损失。只是我们可以使用Convert.ToUInt16()方法来避免精度的损失。但是会有性能损失。还可以用精度高的数据类型来避免这个问题。
当然我们在实际过程中不可能总是和基本数据类型进行转换,类与类之间的转换才是我们日常中运用比较多的。类与类之间的转换和类与基本数据类型的转换类似。唯一的区别就是,类与类之间的强制转换有两个限制:
①类与类之间具有继承派生关系,那么就不能定义类型之间的强制转换(因为它们之间已经存在强制转换)
②类型强制转换必须在源数据类型或目标数据类型的内部定义
(这是为了防止第三方把类型强制转换引入类中)
<5>多重类型强制转换
在转换的过程中没有直接的强制转换方式C#编译器会需找一种转换方式把几种强制转换合并起来。例如:我们有一个Foo类,定义了Foo –> int 的强制转换,现在我们让Foo的对象转换为double类型,那么编译器就会这样转换:Foo -> int -> double。但是如果依赖于多重强制转换,那么程序的性能将会不足。也就是说如果我们需要Foo -> double的转换,我们直接定义这种转换,性能比多重转换将会更具有优势。
附:
1、C#支持的运算符
组 |
运算符 |
初级运算符 |
() . [] x++ x-- new typeof sizeof checked unchecked |
一元运算符 |
+ - ! ~ ++x --x 数据类型强制转换 |
乘/除运算符 |
* / % |
加/减运算符 |
+ - |
移位运算符 |
<< >> |
关系运算符 |
< > <= >= is as |
比较运算符 |
== != |
按位AND运算符 |
& |
按位XOR运算符 |
^ |
按位OR运算符 |
| |
布尔AND运算符 |
&& |
布尔OR运算符 |
|| |
条件运算符 |
?: |
赋值运算符 |
= += -= *= /= %= &= |= ^= <<= >>= >>>= |
2、支持重载的运算符
类别 |
运算符 |
限制 |
算术二元运算符 |
+ * / - % |
无 |
算术一元运算符 |
+ - ++ -- |
无 |
按位二元运算符 |
& | ^ << >> |
无 |
按位一元运算符 |
! ~ true false |
true和false必须成对重载 |
比较运算符 |
== != >= <= < > |
比较运算符必须成对重载 |
赋值运算符 |
+= -= *= /= %= &= |= ^= <<= >>= |
不要显式的重载这些运算符,编译器会隐式的重载 |
索引运算符 |
[] |
不能够直接重载索引运算符 |
数据类型强制转换运算符 |
() |
不能够直接重载强制转换运算符 |