语句、表达式和运算符
语句
语句是构造所有C#程序的过程构造块。语句可以声明局部变量或常数,调用方法,创建对象或将值赋予变量、属性或字段。控制语句可以创建循环,如for循环,也可以进行判断并分支到新的代码块,如if或switch语句。语句通常以分号终止。
由大括号括起来的一系列语句构成代码块。方法体是代码块的一个示例。代码块通常出现在控制语句之后。在代码块中声明的变量或常数可用于同一代码块中的语句。例如,下面的代码演示在控制语句之后出现的一个方法块和一个代码块:
bool IsPositive(int number)
{
if (number > 0)
{
return true;
}
else
{
return false;
}
}
C#中的语句通常包含表达式。C#中的表达式是一个包含文本值、简单名称或运算符及其操作数的代码段。大多数常用表达式在计算时都将产生文本值、变量、对象属性或对象索引器访问。只要从表达式中识别变量、对象属性或对象索引器访问,该项的值都将用作表达式的值。在C#中,表达式可以置于需要值或对象的任意位置,条件是表达式最终的计算结果必须为所需的类型。
有些表达式的计算结果为命名空间、类型、方法组或事件访问。这些具有特殊用途的表达式只在某些情况下(通常是作为较大的表达式的以部分时)有效,如果使用不正确,则将产生编译器错误。
表达式
表达式是可以计算且结果为单个值、对象、方法或命名空间的代码片段。表达式可以包含文本值、方法调用、运算符及其操作数,或简单名称。简单名称可以是变量、类型成员、方法参数、命名空间或类型的名称。
表达式可以使用运算符,而运算符又可以将其表达式用作参数,或者使用方法调用,而方法调用的参数又可以是其他方法调用,因此表达式既可以非常简单,也可以非常复杂。
文本和简单名称
最简单的两种表达式类型是文本和简单名称。文本是没有名称的常数值。例如,在下面的代码示例中,5和“Hello world”都是文本值:
int i = 5;
string s = "Hello World";
在上面的示例中,i和s都是用于标识局部变量的简单名称。在表达式中使用这些变量时,变量值检索后将用于表达式中。例如,在下面的代码示例中,当调用DoWork时,该方法默认情况下接收值“5”,且不能访问变量var:
int var = 5;
DoWork(var);
调用表达式
在下面的代码示例中,对DoWork的调用是另一种表达式,称为调用表达式。
DoWork(var);
具体来说,调用方法即是一个方法调用表达式。方法调用要求使用方法的名称(如前面的示例中那样作为名称,或作为其他表达式的结果),后跟括号和任意方法参数。委托调用使用委托的名称和括号内的方法参数。如果方法返回值,则方法调用和委托调用的计算结果为该方法的返回值。返回void的方法不能替代表达式中的值。
只要从表达式中识别到变量、对象属性或对象索引器访问,该项的值都会用作表达式的值。在C#中,只要表达式的最终计算结果是所需的类型,表达式就可以放置在任何需要值或对象的位置。
运算符
在C#中,运算符是术语或符号,它接受一个或多个称为操作数的表达式作为输入并返回值。接受一个操作数的运算符被称作一元运算符,例如增量运算符(++)或new。接受两个操作数的运算符被称作二元运算符,例如算术运算符+、-、*、/。条件运算符?:接受三个操作数,是C#中唯一的三元运算符。
下面的C#语句包含一个一元运算符和一个操作数。增量运算符++修改操作数y的值:
y++;
下面的C#语句包含两个二元运算符,它们分别有两个操作数。赋值运算符=将一个整数y和一个表达式2+3作为操作数。表达式2+3本身包含加运算符,并使用整数值2和3作为操作数:
y = 2 + 3;
操作数可以是任何大小、由任何数量的其他操作组成的有效表达式。
表达式中的运算符按照称为运算符优先级的特定顺序计算。下表根据运算符执行的操作类型将它们划分到不同的类别中。类别按优先级顺序列出。
基本 |
x.y、f(x)、a[x]、x++、x--、new typeof、checked、unchecked |
一元 |
+、-、!、~、++x、--x、(T)x |
算术 乘法 |
*、/、% |
算术 加法 |
+、- |
移位 |
<<、>> |
关系和类型检测 |
<、>、<=、>=、is、as |
相等 |
==、!= |
逻辑(按优先级顺序) |
&、^、| |
条件(按优先级顺序) |
&&、||、?: |
赋值 |
=、+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>= |
当表达式中出现两个具有相同优先级的运算符时,它们根据结合性进行计算。左结合运算符按从左到右的顺序计算。例如,x*y/z计算为(x*y)/z。右结合运算符按从右到左的顺序计算。赋值运算符和三元运算符(?:)是右结合运算符。其他所有二元运算符都是左结合运算符。然而,C#标准没有指定何时执行表达式中的增量指令的“设置”部分。例如,下面的代码示例的输出为6:
int num1 = 5;
num1++;
System.Console.WriteLine(num1);
而下面的代码示例的输出确实未定义的:
int num2 = 5;
num2 = num2++; //not recommended
System.Console.WriteLine(num2);
因此,建议不要使用后一个示例。可以在表达式两侧使用括号强制在计算其他任何表达式之前计算该表达式。例如,2+3*2正常情况下计算为8.这是因为乘法运算符的优先级高于加法运算符。将该表达式写为(2+3)*2的形式,结果将是10,因为它指示C#编译器必须在计算乘法运算符*之前计算加法运算符+。
对于自定义的类和结构,可以更改运算符的行为,此过程成为运算符重载。
可重载运算符
C#允许用户定义的类型通过使用operator关键字定义静态成员函数来重载运算符。但不是所有的运算符都可被重载,下表列出了不能被重载的运算符:
运算符 |
可重载性 |
+、-、!、~、++、--、true和false |
可以重载这些一元运算符 |
+、-、*、/、%、&、|、^、<<、>> |
可以重载这些二进制运算符 |
==、!=、<、>、<=、>= |
比较运算符可以重载 |
&&、|| |
条件逻辑运算符不能重载,但可以使用能够重载的&和|进行计算 |
[] |
不能重载数组索引运算符,但可定义索引器 |
+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>= |
赋值运算符不能重载,但+=可使用+计算 |
=、.、?:、->、new、is、sizeof和typeof |
不能重载这些运算符 |
注意
比较运算符(如果重载)必须成对重载;也就是说,如果重载==,也必须重载!=。反之亦然,<和>以及<=和>=同样如此。
public static Complex operator +(Complex c1, Complex c2)
转换运算符
C#允许程序员在类或结构上声明转换,以便类或结构与其他类或结构或者基本类型进行相互转换。转换的定义方法类似于运算符,并根据它们所转换到的类型命名。要转换的类型参数或转换结果的类型必须是(不能两者同时都是)包含类型。
class SampleClass
{
public static explicit operator SampleClass(int i)
{
SampleClass temp = new SampleClass();
// code to convert from int to SampleClass...
return temp;
}
}
转换运算符概述
转换运算符具有以下特点:
l 声明为implicit的转换在需要时自动进行
l 声明为explicit的转换需要要调用强制转换
l 所有转换都必须是static转换
使用转换运算符
转换运算符可以是explicit,也可以是implicit。隐式转换运算符更容易使用,但是如果您希望运算符的用户能够意识到正在进行转换,则显示运算符很有用。此主题演示了这两种类型。
示例1
说明:这个显式转换运算符的一个示例。此运算符将类型Byte转换为称为Digit的值类型。由于不是所有字节都可以转换为数字,因此转换是显式的,这意味着必须使用强制转换,如Main方法所示。


































如何:在结构之间实现用户定义的转换
本示例定义RomanNumeral和BinaryNumeral两个结构,并演示二者之间的转换。
示例












































































可靠编程
在上面的示例中,语句:
binary = (BinaryNumeral)(int)roman;
执行从RomanNumeral到BinaryNumeral的转换。由于没有从RomanNumeral到BinaryNumeral的直接转换,所以使用一个转换将RomanNumeral转换为int,并使用另一个转换将int转换为BinaryNumeral。
另外,语句:
roman = binary;
执行从BinaryNumeral到RomanNumeral的转换。由于RomanNumeral定义了从BinaryNumeral的隐式转换,所以不需要转换。
如何:使用运算符重载创建复数类
本示例展示如何使用运算符重载创建定义复数加法的复数类Complex。本程序使用ToString方法的重载显示数字的虚部和实部以及加法结果。
示例











































Equals()和运算符==的重载准则
C#中有两种不同的相等:引用相等和值相等。值相等是大家普遍理解的意义上的相等:它意味着两个对象包含相同的值。例如,两个值为2的整数具有值相等性。引用相等意味着要比较的不是两个对象,而是两个对象引用,这两个对象引用引用的是同一个对象。这可以通过简单的赋值来实现,如下面的示例所示:
System.Object a = new System.Object();
System.Object b = a;
System.Object.ReferenceEquals(a, b); //returns true
从上面的代码中,只存在一个对象,但存在对该对象的多个引用:a和b。由于它们引用的是同一个对象,因此具有引用相等性。如果两个对象具有引用相等性,则它们也具有值相等性,但是值相等性不能保证引用相等性。
若要检查引用相等性,应使用ReferenceEquals。若要检查值相等性,应使用Equals。
重写Equals
Equals是一个虚方法,允许任何类重写其实现。表示某个值(本质上可以是任何值类型)或一组值(如复数类)的任何类都应该重写Equals。如果类型要实现IComparable,则它应该重写Equals。
Equals的新实现应该遵循Equals的所有保证:
l x.Equals(x)返回true
l x.Equals(y)与y.Equals(x)返回相同的值
l 如果(x.Equals(y)&&y.Equals(z))返回true,则x.Equals(z)返回true
l 只要不修改x和y所引用的对象,x.Equals(y)的后续调用就返回相同的值。
l x.Equals(null)返回false
Equals的新实现不应该引发异常。建议重写Equals的任何类同时也重写System.Object.GetHashCode。除了实现Equals(对象)外,还建议所有的类为自己的类型实现Equals(类型)以增强性能。例如:















































可调用基类的Equals的任何派生类在完成其比较之前都应该这样做。在下面的示例中,Equals调用基类Equals,后者将检查空参数并将参数的类型与派生类的类型做比较。这样就把检查派生类中声明的新数据字段的任务留给了派生类中的Equals实现:



































Overriding Operator ==
默认情况下,运算符==通过判断两个引用是否指示同一对象来测试引用是否相等,因此引用类型不需要实现运算符==就能获得此功能。当类型不可变时,意味着实例中包含的数据不可更改,此时通过重载运算符==来比较值是否相等而不是比较引用是否相等可能会很有用,因为作为不可变的对象,只要它们具有相同的值,就可以将它们看作是相同的。建议不要在非不可变类型中重写运算符==。
重载的运算符==实现不应引发异常。重载运算符==的任何类型还应重载运算符!=。例如:

























注意
运算符==的重载只能够的常见错误是使用(a==b)、(a==null)或(b==null)来检查引用相等性。这会导致调用重载的运算符==,从而导致无限循环。应使用ReferenceEquals或将类型强制转换为Object来避免无限循环。