八、表达式和运算符
表情
本章定义表达式并描述 C# 提供的运算符。它还解释了如何定义 C# 运算符来处理用户定义的类。
一个操作符是一个符号,代表一个返回单一结果的操作。操作数是操作者用作输入的数据元素。操作员执行以下操作:
- Take its operand as input.
- Perform an action
- According to the action
返回值
一个表达式是一串操作符和操作数。C# 运算符接受一个、两个或三个操作数。以下是一些可以作为操作数的构造:
- characters
- constant
- variable
- Method call
- Element accessors, such as array accessors and indexers
- Other expressions
可以使用运算符组合表达式,以创建更复杂的表达式,如以下包含三个运算符和四个操作数的表达式所示:
求值表达式是将每个运算符以正确的顺序应用于其操作数以产生一个值的过程。
- Returns the value to the position where the expression is evaluated. There, it may be used as an operand in a closed expression again.
- Besides the return value, some expressions also have side effects, such as setting a value in memory.
文字量
文字是键入源代码中的数字或字符串,表示特定类型的特定集合值。
例如,下面的代码显示了六种类型的文字。例如,请注意double
文字和float
文字之间的差异。
static void Main() Literals { ↓ Console.WriteLine("{0}", 1024); // int literal Console.WriteLine("{0}", 3.1416); // double literal Console.WriteLine("{0}", 3.1416F); // float literal Console.WriteLine("{0}", true); // boolean literal Console.WriteLine("{0}", 'x'); // character literal Console.WriteLine("{0}", "Hi there"); // string literal }
这段代码的输出如下:
1024 3.1416 3.1416 True x Hi there
因为文字被写入源代码,所以它们的值在编译时必须是已知的。几个预定义类型有自己的文字形式:
- Type
bool
has two characters:true
andfalse
.- For reference type variables, the word
null
indicates that the variable does not point to data in memory.
整数文字量
整数文字是最常用的文字。它们被写成十进制数字序列,具有以下特征:
- No decimal point
- An optional suffix to specify the type of integer.
例如,下面几行显示了整数 236 的四个文本。每个都被编译器解释为不同类型的整数,这取决于它的后缀。
236 // int 236L // long 236U // unsigned int 236UL // unsigned long
整数类型的文字也可以写成十六进制(hex)形式。数字必须是十六进制数字(0 到 F),字符串必须以0x
或0X
开头(数字 0 ,字母 x )。
图 8-1 显示了整数文字格式的形式。名称在方括号中的组件是可选的。
***图 8-1。*整数文字格式
表 8-1 列出了整数文字后缀。对于给定的后缀,编译器会将数字字符串解释为四种列出的整数类型中最小的一种,这四种整数类型可以在不丢失数据的情况下表示该值。
例如,以文字236
和5000000000
为例,它们都没有后缀。由于236
可以用 32 位来表示,所以它将被编译器解释为int
。然而,第二个数字不适合 32 位,所以编译器将它表示为一个long
。
实字面值
实数的文字由以下内容组成:
- decimal digits
- Optional decimal point
- Optional exponential part
- Optional suffix
例如,以下代码显示了实数类型的各种文本格式:
float f1 = 236F; double d1 = 236.714; double d2 = .35192; double d3 = 6.338e-26;
图 8-2 显示了真实文字的有效格式。名称在方括号中的组件是可选的。表 8-2 显示了真正的后缀及其含义。
***图 8-2。*真正的文字格式
注意不带后缀的真实文字是double
类型,不是float
!
字符字面量
字符文字由两个单引号之间的字符表示组成。字符表示可以是以下任何一种:单个字符、简单转义序列、十六进制转义序列或 Unicode 转义序列。
- The type of a character is
char
.- A simple escape sequence is a backslash followed by a single character.
- The hexadecimal escape sequence is a backslash followed by an uppercase or lowercase x , followed by up to four hexadecimal digits.
- Unicode escape sequence is a backslash followed by an uppercase or lowercase u , followed by up to four hexadecimal digits.
例如,以下代码显示了各种格式的字符文本:
char c1 = 'd'; // Single character char c2 = '\n'; // Simple escape sequence char c3 = '\x0061'; // Hex escape sequence char c4 = '\u005a'; // Unicode escape sequence
表 8-3 显示了一些重要的特殊字符及其编码。
字符串文字
字符串文字使用双引号,而不是字符文字中使用的单引号。有两种类型的字符串文字:
- General string literal
- Word for word character string
常规字符串由一组双引号之间的字符序列组成。常规字符串文字可以包括以下内容:
- character
- Simple escape sequence
- Hexadecimal and Unicode escape sequences
这里有一个例子:
string st1 = "Hi there!"; string st2 = "Val1\t5, Val2\t10"; string st3 = "Add\x000ASome\u0007Interest";
逐字字符串文字的书写方式类似于常规字符串文字,但以@字符开头。逐字字符串的重要特征如下:
- Word-for-word literals differ from regular string literals in that the escape sequence is not calculated. Everything between double quotation marks (including what is usually considered as an escape sequence) is printed out as listed in the string.
- The only exception to word for word is multiple sets of consecutive double quotes, which are interpreted as a double quote character.
例如,下面的代码比较了一些常规字符串和逐字字符串:
` string rst1 = “Hi there!”;
string vst1 = @“Hi there!”;
string rst2 = “It started, “Four score and seven…””;
string vst2 = @“It started, ““Four score and seven…”””;
string rst3 = “Value 1 \t 5, Val2 \t 10”; // Interprets tab esc sequence
string vst3 = @“Value 1 \t 5, Val2 \t 10”; // Does not interpret tab
string rst4 = “C:\Program Files\Microsoft\”;
string vst4 = @“C:\Program Files\Microsoft”;
string rst5 = " Print \x000A Multiple \u000A Lines";
string vst5 = @" Print
Multiple
Lines";`
打印这些字符串会产生以下输出:
`Hi there!
Hi there!
It started, “Four score and seven…”
It started, “Four score and seven…”
Value 1 5, Val2 10
Value 1 \t 5, Val2 \t 10
C:\Program Files\Microsoft
C:\Program Files\Microsoft\
Print
Multiple
Lines
Print
Multiple
Lines`
注意编译器通过让相同的字符串在堆中共享相同的内存位置来节省内存。
评估的顺序
一个表达式可以由许多嵌套的子表达式组成。子表达式的求值顺序会对表达式的最终值产生影响。
例如,给定表达式 3 * 5 + 2,根据子表达式的求值顺序,有两种可能的结果,如图 8-3 所示。
- If you multiply first, the result is 17.
- If you add 5 and 2 together first, the result is 21.
***图 8-3。*简单的评估顺序
优先顺序
您从小学时代就知道,在前面的例子中,乘法必须在加法之前执行,因为乘法的优先级高于加法。但是与小学时代不同,那时你有四个操作符和两个优先级,C# 的情况稍微复杂一些,它有超过 45 个操作符和 14 个优先级。
表 8-4 显示了操作符的完整列表以及每个操作符的优先级。该表在顶部列出了优先级最高的运算符,并在底部列出了优先级最低的运算符。
关联性
当编译器计算所有运算符都具有不同优先级的表达式时,将计算每个子表达式,从具有最高优先级的子表达式开始,向下计算优先级。
但是如果两个顺序操作符具有相同的优先级呢?例如,给定表达式 2 / 6 * 4,有两种可能的评估序列:
(2 / 6) * 4 = 4/3
或者
2 / (6 * 4) = 1/12
当顺序运算符具有相同的优先级时,求值顺序由*运算符结合性决定。*也就是说,给定两个优先级相同的操作符,根据操作符的结合律,一个或另一个将具有优先级。运算符结合性的一些重要特征如下,并在表 8-5 中进行了总结:
- The left union operator is evaluated from left to right.
- The right association operator evaluates from right to left.
- Binary operators, except assignment operators, are left associative.
- Assignment operators and conditional operators are right associative.
因此,根据这些规则,前面的示例表达式应该从左到右分组,得到(2 / 6) * 4,即 4/3。
通过使用括号,可以显式设置表达式子表达式的求值顺序。带括号的子表达式
- Override priority and binding rules
- From the innermost nested set to the outermost set
的顺序进行评估
简单的算术运算符
简单算术运算符执行四种基本算术运算,列于表 8-6 。这些运算符是二元的和左结合的。
算术运算符对所有预定义的简单算术类型执行标准算术运算。
以下是简单算术运算符的示例:
` int x1 = 5 + 6; double d1 = 5.0 + 6.0;
int x2 = 12 - 3; double d2 = 12.0 - 3.0;
int x3 = 3 * 4; double d3 = 3.0 * 4.0;
int x4 = 10 / 3; double d4 = 10.0 / 3.0;
byte b1 = 5 + 6;
sbyte sb1 = 6 * 5;`
余数运算符
余数运算符(%
)将第一个操作数除以第二个操作数,忽略商,并返回余数。表 8-7 给出了它的描述。
余数运算符是二元的和左结合的。
以下几行显示了整数余数运算符的示例:
- 0
%
3 = 0, because 0 divided by 3 is 0 with a remainder of 0.- 1
%
3 = 1, because 1 divided by 3 is 0 and the remainder is 1.- 2
%
3 = 2, because 2 divided by 3 is 0 and the remainder is 2.
%
- 4
%
3 = 1, because 4 divided by 3 equals 1 and the remainder is 1.
余数运算符也可用于实数,给出的实数余数。
Console.WriteLine("0.0f % 1.5f is {0}" , 0.0f % 1.5f); Console.WriteLine("0.5f % 1.5f is {0}" , 0.5f % 1.5f); Console.WriteLine("1.0f % 1.5f is {0}" , 1.0f % 1.5f); Console.WriteLine("1.5f % 1.5f is {0}" , 1.5f % 1.5f); Console.WriteLine("2.0f % 1.5f is {0}" , 2.0f % 1.5f); Console.WriteLine("2.5f % 1.5f is {0}" , 2.5f % 1.5f);
该代码产生以下输出:
0.0f % 1.5f is 0 // 0.0 / 1.5 = 0 remainder 0 0.5f % 1.5f is 0.5 // 0.5 / 1.5 = 0 remainder .5 1.0f % 1.5f is 1 // 1.0 / 1.5 = 0 remainder 1 1.5f % 1.5f is 0 // 1.5 / 1.5 = 1 remainder 0 2.0f % 1.5f is 0.5 // 2.0 / 1.5 = 1 remainder .5 2.5f % 1.5f is 1 // 2.5 / 1.5 = 1 remainder 1
关系和相等比较运算符
关系和相等比较操作符是二元操作符,它们比较它们的操作数并返回一个类型为bool
的值。表 8-8 列出了这些操作员。
关系运算符和等式运算符是二元的和左关联的。
带有关系或相等运算符的二元表达式返回一个类型为bool.
的值
注意与 C 和 C++不同,C# 中的数字没有布尔解释。
int x = 5; if( x ) // Wrong. x is of type int, not type boolean. ... if( x == 5 ) // Fine, since expression returns a value of type boolean ...
打印时,布尔值true
和false
由字符串输出值True
和False
表示。
int x = 5, y = 4; Console.WriteLine("x == x is {0}" , x == x); Console.WriteLine("x == y is {0}" , x == y);
这段代码产生以下输出:
x == x is True x == y is False
比较和相等操作
当比较大多数引用类型是否相等时,只比较引用。
- If the references are equal-that is, if they point to the same object in memory-the equal comparison is
true
; Otherwise, it isfalse
, even though two independent objects in memory are completely equivalent in other aspects.- This is called shallow ratio .
illustrates the comparison of reference types. On the left side of the figure- , the references held by
a
andb
are the same, so the comparison will returntrue
. On the right side of the picture- , the reference is different, so even if the contents of two
AClass
objects are exactly the same, the comparison will returnfalse
.
***图 8-4。*比较引用类型是否相等
类型string
的对象也是引用类型,但是比较方式不同。当比较字符串是否相等时,会比较字符串的长度和区分大小写的内容。
- If two strings have the same length and the same case-sensitive content, the equal comparison returns
true
, even if they occupy different memory areas.- This is called deep comparison .
在第十五章中提到的代表也是引用类型,也使用深度比较。当比较委托是否相等时,如果两个委托都是null
,或者如果两个委托的调用列表中有相同数量的成员并且调用列表匹配,则比较返回true
。
比较数值表达式时,会比较类型和值。当比较enum
类型时,比较是在操作数的基础值上进行的。枚举包含在第十三章中。
递增和递减运算符
递增运算符将操作数加 1。减量运算符从操作数中减去 1。表 8-9 列出了操作者及其描述。
这些操作符是一元的,有两种形式,即前置形式和后置形式,它们的作用不同。
- In the prefix, the operator is placed before the operand; Such as
++x
and--y
.- In postposition, the operator is placed after the operand; Such as
x++
andy--
.
在比较运算符的前置和后置形式时
- The only difference is the value returned by the operator to the expression.
*表 8-10 显示了一个总结行为的例子。
例如,下面简单演示了四种不同版本的运算符。为了显示同一输入的不同结果,在每个赋值语句之前,操作数x
的值被重置为5
。
` int x = 5, y;
y = x++; // result: y: 5, x: 6
Console.WriteLine(“y: {0}, x: {1}” , y, x);
x = 5;
y = ++x; // result: y: 6, x: 6
Console.WriteLine(“y: {0}, x: {1}” , y, x);
x = 5;
y = x–; // result: y: 5, x: 4
Console.WriteLine(“y: {0}, x: {1}” , y, x);
x = 5;
y = --x; // result: y: 4, x: 4
Console.WriteLine(“y: {0}, x: {1}” , y, x);`
该代码产生以下输出:
y: 5, x: 6 y: 6, x: 6 y: 5, x: 4 y: 4, x: 4
条件逻辑运算符
逻辑运算符用于比较或否定其操作数的逻辑值,并返回结果逻辑值。表 8-11 列出了操作员。
逻辑 AND 和逻辑 OR 运算符是二进制和左关联的。逻辑 NOT 是一元的。
这些运算符的语法如下,其中 Expr1
和 Expr2
计算为布尔值:
Expr1 && Expr2 *Expr1* || *Expr2* ! *Expr*
以下是一些例子:
` bool bVal;
bVal = (1 == 1) && (2 == 2); // True, both operand expressions are true
bVal = (1 == 1) && (1 == 2); // False, second operand expression is false
bVal = (1 == 1) || (2 == 2); // True, both operand expressions are true
bVal = (1 == 1) || (1 == 2); // True, first operand expression is true
bVal = (1 == 2) || (2 == 3); // False, both operand expressions are false
bVal = true; // Set bVal to true.
bVal = !bVal; // bVal is now false.`
条件逻辑运算符以“短路”模式运行,这意味着,如果在对 Expr1
求值之后,结果已经确定,那么它跳过对 Expr2
的求值。以下代码显示了在计算第一个操作数后可以确定其值的表达式示例:
` bool bVal;
bVal = (1 == 2) && (2 == 2); // False, after evaluating first expression
bVal = (1 == 1) || (1 == 2); // True, after evaluating first expression`
由于短路行为,不要将有副作用的表达式(如更改值)放在 Expr2
中,因为它们可能不会被求值。在下面的代码中,变量iVal
的后增量不会被执行,因为在第一个子表达式被执行后,可以确定整个表达式的值是false
。
` bool bVal; int iVal = 10;
bVal = (1 == 2) && (9 == iVal++); // result: bVal = False, iVal = 10
↑ ↑
False Never evaluated`
逻辑运算符
按位逻辑运算符通常用于设置方法参数的位模式。表 8-12 列出了按位逻辑运算符。
除了按位求反运算符之外,这些运算符都是二元和左关联的。按位求反运算符是一元的。
二元按位运算符比较两个操作数中每个位置的相应位,并根据逻辑运算设置返回值中的位。
图 8-5 展示了按位逻辑运算的四个例子。
***图 8-5。*按位逻辑运算符的例子
下面的代码实现了前面的示例:
` const byte x = 12, y = 10;
sbyte a;
a = x & y; // a = 8
a = x | y; // a = 14
a = x ^ y; // a = 6
a = ~x; // a = -13`
移位操作符
按位移位运算符将位模式向右或向左移动指定的位置数,空出的位用 0 或 1 填充。表 8-13 列出了移位操作符。
移位运算符是二元的和左结合的。这里显示了按位移位运算符的语法。移动的位置数量由 Count
给出。
*Operand* << *Count* // Left shift *Operand* >> *Count* // Right shift
对于绝大多数的 C# 编程来说,你不需要了解任何底层的硬件。但是,如果您正在对有符号的数字进行按位操作,了解数字表示会很有帮助。底层硬件以一种叫做二进制补码的形式表示带符号的二进制数。在二进制补码表示中,正数有它们正常的二进制形式。要对一个数求反,需要对该数进行逐位求反,然后加 1。这个过程将正数转化为负数,反之亦然。在二进制补码中,所有负数在最左边的位上都有一个 1。图 8-6 显示了对数字 12 的否定。
***图 8-6。*要得到一个二进制补码数的反数,取其按位反数并加 1。
当移位有符号数字时,底层表示很重要,因为将整数值左移一位的结果与将它乘以二的结果相同。向右移动和除以二是一样的。
然而,如果您将一个负数向右移位,并且最左边的位被填充为 0,则会产生错误的结果。最左边的 0 表示正数。但这是不正确的,因为负数除以 2 不会产生正数。
为了解决这种情况,当操作数是一个有符号整数时,如果操作数最左边的位是 1(表示一个负数),左边的位位置用 1 而不是 0 填充。这保持了正确的二进制补码表示。对于正数或无符号数,左边空出的位用 0 填充。
图 8-7 显示了表达式 14 <<
3 如何在byte
中求值。此操作会导致以下情况:
- Each bit in operand (14) is shifted to the left by three bits.
- The left three digits at the right end are filled with zeros.
- The resulting value is 112.
***图 8-7。*左移三位的例子
图 8-8 说明了按位移位操作。
***图 8-8。*按位移位
下面的代码实现了前面的示例:
` int a, b, x = 14;
a = x << 3; // Shift left
b = x >> 3; // Shift right
Console.WriteLine(“{0} << 3 = {1}” , x, a);
Console.WriteLine(“{0} >> 3 = {1}” , x, b);`
该代码产生以下输出:
14 << 3 = 112 14 >> 3 = 1
赋值运算符
赋值运算符计算运算符右侧的表达式,并使用该值设置运算符左侧的变量表达式的值。表 8-14 列出了赋值运算符。
赋值操作符是二元的和右关联的。
语法如下:
VariableExpression Operator Expression
对于简单赋值,计算运算符右边的表达式,将其值赋给左边的变量。
int x; x = 5; x = y * z;
记住赋值表达式是一个表达式,因此返回一个值到它在语句中的位置。执行赋值后,赋值表达式的值是左操作数的值。因此,在表达式x = 10
的情况下,值 10 被赋给变量x
。x 的值,现在是 10,变成了整个表达式的值。
由于赋值是一个表达式,它可以是一个更大表达式的一部分,如图 8-9 所示。表达式的评估如下:
- Since the assignment is right-related, the evaluation starts from the right, and the variable
x
is assigned a value of 10.- That expression is the right operand assigned by the variable
y
, so the value ofx
, now 10, is assigned toy
.- The assignment of
y
is the right operand of the assignment ofz
-all three variables have values of 10.
***图 8-9。*赋值表达式在完成赋值后返回其左操作数的值。
可以位于赋值运算符左侧的对象类型如下。它们将在本文后面讨论。
- Variables (local variables, fields, parameters)
- attribute
- Indexer
- event
复合赋值
通常,您会希望对表达式求值,并将结果添加到变量的当前值中,如下所示:
x = x + expr;
复合赋值操作符允许一种简化方法,以避免在某些常见情况下左侧变量在右侧重复出现。例如,下面两个语句在语义上是等价的,但是第二个语句更短,也同样容易理解。
x = x + (y – z); x += y – z;
其他复合赋值语句是类似的:
Notice the parentheses. ↓ ↓ x *= y – z; // Equivalent to x = x * (y – z) x /= y – z; // Equivalent to x = x / (y – z) ...
条件运算符
条件运算符是根据条件的结果返回两个值之一的强大而简洁的方法。表 8-15 显示了操作员。
条件运算符是三元的。
条件运算符的语法如下所示。它有一个测试表达式和两个结果表达式。
Condition
must return a value of typebool
.- If
Condition
evaluates totrue
,Expression1
evaluates and returns. Otherwise, evaluate and returnExpression2
.
Condition ? Expression1 : Expression2
条件运算符可以与if...else
结构进行比较。例如,下面的if...else
构造检查一个条件,如果条件为真,该构造将5
赋给变量intVar
。否则,它给它赋值 10。
if ( x < y ) // if...else intVar = 5; else intVar = 10;
条件运算符可以以不太冗长的形式执行相同的操作,如以下语句所示:
intVar = x < y ? 5 : 10; // Conditional operator
将条件和每个返回表达式放在单独的行上,如下面的代码所示,这使得意图非常容易理解。
intVar = x < y ? 5 : 10 ;
图 8-10 比较了示例中所示的两种形式。
***图 8-10。*条件运算符 vs. if…否则
例如,下面的代码使用了三次条件运算符——在每个WriteLine
语句中使用一次。首先,它返回x
的值或者y
的值。在后两个实例中,它返回空字符串或字符串“not”。
` int x = 10, y = 9;
int highVal = x > y // Condition
? x // Expression 1
: y; // Expression 2
Console.WriteLine(“highVal: {0}\n” , highVal);
Console.WriteLine(“x is{0} greater than y” ,
x > y // Condition
? “” // Expression 1
: " not" ); // Expression 2
y = 11;
Console.WriteLine(“x is{0} greater than y” ,
x > y // Condition
? “” // Expression 1
: " not" ); // Expression 2`
该代码产生以下输出:
`highVal: 10
x is greater than y
x is not greater than y`
注if...else
语句是一条控制流语句。它应该用于执行两个动作中的一个或另一个。条件运算符返回一个表达式。用于返回两个值中的一个或另一个。
一元算术运算符
一元运算符设置数值的符号。它们在表 8-16 中列出。
- The unary operator simply returns the value of the operand.
- The unary negative operator returns the value of the operand minus 0.
例如,以下代码显示了运算符的用法和结果:
int x = +10; // x = 10 int y = -x; // y = -10 int z = -y; // z = 10
用户自定义类型转换
用户定义的转换在第十六章中有更详细的讨论,但是我在这里也会提到它们,因为它们是操作符。
您可以为自己的类和结构定义隐式和显式转换。这允许您将用户定义类型的对象转换为其他类型,反之亦然。* C# provides implicit and explicit conversions.
* Using implicit conversion , when the compiler resolves what type to use in a specific context, it will automatically convert if necessary.
* With explicit conversion , the compiler will only convert when using the explicit conversion operator.
声明隐式转换的语法如下。所有用户定义的转换都需要public
和static
修饰符。
Required Target Source <ins> ↓ </ins> ↓ <ins> ↓ </ins> public static implicit operator *TargetType* ( *SourceType Identifier* ) { ... return *ObjectOfTargetType*; }
显式转换的语法是相同的,只是用explicit
代替了implicit
。
下面的代码展示了一个转换操作符的声明示例,它将把类型为LimitedInt
的对象转换为类型为int
的对象,反之亦然。
` class LimitedInt Target Source
{
↓ ↓
public static implicit operator int (LimitedInt li) // LimitedInt to int
{
return li.TheValue;
} Target Source
↓ ↓
public static implicit operator LimitedInt (int x) // int to LimitedInt
{
LimitedInt li = new LimitedInt();
li.TheValue = x;
return li;
}
private int _theValue = 0;
public int TheValue{ … }
}`
例如,下面的代码重复并使用了刚刚定义的两个类型转换操作符。在Main
中,一个int
文字被转换成一个LimitedInt
对象,在下一行中,一个LimitedInt
对象被转换成一个int
。
` class LimitedInt
{
const int MaxValue = 100;
const int MinValue = 0;
public static implicit operator int(LimitedInt li) // Convert type
{
return li.TheValue;
}
public static implicit operator LimitedInt(int x) // Convert type
{
LimitedInt li = new LimitedInt();
li.TheValue = x;
return li;
}
private int _theValue = 0;
public int TheValue // Property
{
get { return _theValue; }
set
{
if (value < MinValue)
_theValue = 0;
else
_theValue = value > MaxValue
? MaxValue
: value;
}
}
}
class Program
{
static void Main() // Main
{
LimitedInt li = 500; // Convert 500 to LimitedInt
int value = li; // Convert LimitedInt to int
Console.WriteLine(“li: {0}, value: {1}” , li.TheValue, value);
}
}`
该代码产生以下输出:
li: 100, value: 100
显式转换和强制转换运算符
前面的示例代码展示了从int
到LimitedInt
类型的隐式转换,以及从LimitedInt
到int
类型的隐式转换。但是,如果您将两个转换操作符声明为explicit
,那么在进行转换时,您将不得不显式地使用 cast 操作符。
一个转换操作符由你想要将表达式转换成的类型名组成,在一对括号内。例如,在下面的代码中,方法Main
将值 500 转换为一个LimitedInt
对象。
Cast operator <ins> ↓ </ins> LimitedInt li = (LimitedInt) 500;
例如,下面是代码的相关部分,其中的更改标记为:
在这两个版本的代码中,输出如下:
li: 100, value: 100
还有另外两个运算符,它们接受一种类型的值,并返回另一种指定类型的值。这些是is
操作符和as
操作符。这些都在第十六章的结尾有所涉及。
运算符重载
正如您所看到的,C# 操作符被定义为使用预定义的类型作为操作数。如果遇到用户定义的类型,操作者根本不知道如何处理它。运算符重载允许您定义 C# 运算符应该如何对用户定义类型的操作数进行运算。
- Operator overloading is only available for classes and structures.
- By declaring a method named
operator
x
to realize behavior (for example,operator +
,operator -
and so on), you can overload an operatorx
for your class or structure.
- The overloaded method of unary operator adopts a single parameter of type
class
orstruct
.- Overload method of binary operator takes two parameters, at least one of which must be of type
class
orstruct
.public static LimitedInt operator -(LimitedInt x) // Unary public static LimitedInt operator +(LimitedInt x, double y) // Binary
运算符重载方法的声明需要满足以下条件:
- Declarations must use both
static
andpublic
modifiers.- Operator must be a member of the class or structure to which the operator belongs.
例如,下面的代码显示了类LimitedInt
的两个重载操作符:加法操作符和求反操作符。您可以看出这是否定,而不是减法,因为运算符重载方法只有一个参数,因此是一元的,而减法运算符是二元的。
` class LimitedInt Return
{
Required Type Keyword Operator Operand
↓ ↓ ↓ ↓ ↓
public static LimitedInt operator + (LimitedInt x, double y)
{
LimitedInt li = new LimitedInt();
li.TheValue = x.TheValue + (int)y;
return li;
}
public static LimitedInt operator - (LimitedInt x)
{
// In this strange class, negating a value just sets it to 0.
LimitedInt li = new LimitedInt();
li.TheValue = 0;
return li;
}
…
}`
对运算符重载的限制
并非所有的运算符都可以重载,而且可以重载的类型也有限制。关于运算符重载的限制,您应该知道的一些重要的事情将在本节的后面描述。
只有下列运算符可以重载。列表中明显缺少的是赋值运算符。
可重载的一元运算符:、+
、-
、!
、~
、++
、--
、true
、false
可重载的二元运算符:+``-``*``/``%``&``|``^``<<``>>``==``!=``>``<``>=``<=
递增和递减运算符是可重载的。但是与预定义版本不同,重载操作符的使用前和使用后没有区别。
在操作符重载的情况下,您不能做以下事情:
- Create a new operator
- Change the syntax of operators.
- Redefine how operators act on predefined types.
- Change the priority or combination of operators.
注意你的重载操作符要符合操作符的直观含义。
运算符重载的例子
以下示例显示了类LimitedInt
的三个运算符的重载:求反、减法和加法。
`class LimitedInt {
const int MaxValue = 100;
const int MinValue = 0;
public static LimitedInt operator -(LimitedInt x)
{
// In this strange class, negating a value just sets its value to 0.
LimitedInt li = new LimitedInt();
li.TheValue = 0;
return li;
}
public static LimitedInt operator -(LimitedInt x, LimitedInt y)
{
LimitedInt li = new LimitedInt();
li.TheValue = x.TheValue - y.TheValue;
return li;
}
public static LimitedInt operator +(LimitedInt x, double y)
{
LimitedInt li = new LimitedInt();
li.TheValue = x.TheValue + (int)y;
return li;
}
private int _theValue = 0;
public int TheValue
{
get { return _theValue; }
set
{
if (value < MinValue)
_theValue = 0;
else
_theValue = value > MaxValue
? MaxValue
: value;
}
}
}
class Program {
static void Main() {
LimitedInt li1 = new LimitedInt();
LimitedInt li2 = new LimitedInt();
LimitedInt li3 = new LimitedInt();
li1.TheValue = 10; li2.TheValue = 26;
Console.WriteLine(" li1: {0}, li2: {1}" , li1.TheValue, li2.TheValue);
li3 = -li1;
Console.WriteLine(“-{0} = {1}” , li1.TheValue, li3.TheValue);
li3 = li2 - li1;
Console.WriteLine(" {0} - {1} = {2}" ,
li2.TheValue, li1.TheValue, li3.TheValue);
li3 = li1 - li2;
Console.WriteLine(" {0} - {1} = {2}" ,
li1.TheValue, li2.TheValue, li3.TheValue);
}
}`
该代码产生以下输出:
li1: 10, li2: 26 -10 = 0 26 - 10 = 16 10 - 26 = 0
操作员的类型
typeof
操作符返回作为其参数给出的任何类型的System.Type
对象。从这个对象,你可以了解类型的特征。(对于任何给定的类型,只有一个System.Type
对象。)不能重载typeof
操作符。表 8-17 列出了操作员的特征。
typeof
运算符是一元的。
以下是typeof
运算符的语法示例。Type
是System
名称空间中的一个类。
Type t = typeof ( SomeClass )
例如,下面的代码使用了typeof
操作符来获取关于名为SomeClass
的类的信息,并打印其公共字段和方法的名称。
` using System.Reflection; // Use the Reflection namespace to take full advantage
// of determining information about a type.
class SomeClass
{
public int Field1;
public int Field2;
public void Method1() { }
public int Method2() { return 1; }
}
class Program
{
static void Main()
{
Type t = typeof(SomeClass);
FieldInfo[] fi = t.GetFields();
MethodInfo[] mi = t.GetMethods();
foreach (FieldInfo f in fi)
Console.WriteLine(“Field : {0}” , f.Name);
foreach (MethodInfo m in mi)
Console.WriteLine(“Method: {0}” , m.Name);
}
}`
该代码产生以下输出:
Field : Field1 Field : Field2 Method: Method1 Method: Method2 Method: ToString Method: Equals Method: GetHashCode Method: GetType
操作符typeof
也被GetType
方法调用,该方法可用于每种类型的每一个对象。例如,下面的代码检索对象的类型名称:
` class SomeClass
{
}
class Program
{
static void Main()
{
SomeClass s = new SomeClass();
Console.WriteLine(“Type s: {0}” , s.GetType().Name);
}
}`
该代码产生以下输出:
Type s: SomeClass
其他操作员
本章介绍的运算符是内置类型的标准运算符。还有其他特殊用途的操作符,以及它们的操作数类型,将在本书后面讨论。例如,可空类型有一个特殊的操作符,称为空合并操作符,这在第二十五章中有详细描述。*
九、语句
什么是报表?
C# 中的语句与 C 和 C++中的语句非常相似。本章涵盖了 C# 语句的特征,以及该语言提供的控制流语句。
- Statement is a source code instruction that describes the type or tells the program to perform actions.
- There are three categories of statements:
- Declaration statement : A statement that declares a type or variable.
- Embedded statement : a statement that performs actions or manages control flow.
- tag statement : the statement that controls the jump to.
前面的章节已经介绍了许多不同的声明语句,包括局部变量、类和类成员的声明。本章介绍嵌入式语句,这些语句不声明类型、变量或实例。相反,它们使用表达式和控制流构造来处理由声明语句声明的对象和变量。
- Simple statement consists of an expression followed by a semicolon.
块
- Declarative statement
- Embedded statement
- Tag statement
- Nested block
以下代码给出了每种情况的示例:
` int x = 10; // Simple declaration
int z; // Simple declaration
{ // Start of a block
int y = 20; // Simple declaration
z = x + y; // Embedded statement
top: y = 30; // Labeled statement
…
{ // Start of a nested block
…
} // End of nested block
} // End of outer block`
注意一个块在语法上算作一个嵌入式语句。在语法上需要嵌入语句的任何地方,都可以使用块。
一个空语句只包含一个分号。您可以在语言语法需要嵌入语句但程序逻辑不需要任何操作的任何位置使用空语句。
例如,下面的代码显示了使用 empty 语句的示例。
- The second line in the code is an empty statement. This is necessary because there must be an embedded statement between the
if
part and theelse
part of the construction.- The fourth line is a simple statement, indicated by a terminating semicolon.
if( x < y ) ; // Empty statement else z = a + b; // Simple statement
表情语句
前一章看了表情。表达式返回值,但是它们也会有副作用。
- A side effect is an action that affects the state of the program.
- Many expressions are only evaluated for their side effects.
通过在表达式后放置语句终止符(分号),可以从表达式创建语句。表达式返回的任何值都将被丢弃。例如,下面的代码显示了一个表达式语句。它由赋值表达式(一个赋值运算符和两个操作数)后跟一个分号组成。这做了以下两件事:
x
这被认为是副作用。
- After setting the value of
x
, the expression returns the new value ofx
. But nothing can receive this return value, so it was ignored.x = 10;
评估表达式的全部原因是为了实现副作用。
控制流报表
C# 提供了现代编程语言常见的控制流结构。
- Conditional execution Execute or skip a piece of code according to conditions. The conditional execution statement is as follows:
if
if...else
switch
- Loop statement Repeats a piece of code. The loop statement reads as follows:
while
do
for
foreach
- Jump statement Changes the control flow from one code to a specific statement in another code. The jump statement reads as follows:
break
continue
return
goto
throw
条件执行和循环构造(除了foreach
)需要一个测试表达式,或者条件,来决定程序应该在哪里继续执行。
注意与 C 和 C++不同,在 C# 中测试表达式必须返回一个bool
类型的值。在 C# 中,数字没有布尔解释。
if 语句
if
语句实现条件执行。此处显示了if
语句的语法,并在图 9-1 中进行了说明。
TestExpr
A value of typebool
must be calculated.- If
TestExpr
evaluates totrue
,Statement
is executed.- If the evaluation is
false
, skipStatement
.
if( *TestExpr* ) *Statement*
***图 9-1。*if 语句
以下代码显示了if
语句的示例:
` // With a simple statement
if( x <= 10 )
z = x – 1; // Single statement–no curly braces needed
// With a block
if( x >= 20 )
{
x = x – 5; // Block–curly braces needed
y = x + z;
}
int x = 5;
if( x ) // Error: test expression must be a bool, not int
{
…
}`
如果…else 语句
if...else
语句实现了双向分支。此处显示了if...else
语句的语法,并在图 9-2 中进行了说明。
- If
TestExpr
evaluates totrue
,Statement1
is executed.- Otherwise, perform
Statement2
instead.
if( *TestExpr* ) *Statement1* else *Statement2*
***图 9-2。*如果…else 语句
下面是一个if...else
语句的例子:
if( x <= 10 ) z = x – 1; // Single statement else { // Multiple statements--block x = x – 5; y = x + z; }
当然, Statement1
、 Statement2
或者两者本身都可以是if
或者if...else
语句,可以进一步嵌套。如果您正在查看包含嵌套的if...else
语句的代码,并且需要确定哪个else
与哪个if
相配,有一个简单的规则。每个else
都属于没有关联else
子句的最近的前一个 if
。
当 Statement2
是一个if
或if...else
语句时,常见的格式如下所示,将第二个if
子句与else
子句放在同一行。这个例子展示了两个if...else
语句,但是您可以创建一个任意长的链。
if( *TestExpr1* ) *Statement1* else if ( *TestExpr2* ) *Statement2* else *Statement3*
while 循环
while
循环是一个简单的循环结构,其中测试表达式在循环的顶部执行。这里显示了while
循环的语法,并在图 9-3 中进行了说明。
- Evaluate
TestExpr
first- If
TestExpr
evaluates tofalse
, execution will continue after thewhile
loop ends.- Otherwise, when
TestExpr
is evaluated astrue
,Statement
is executed andTestExpr
is evaluated again. Every time the calculation result ofTestExpr
istrue
,Statement
will be executed once WhenTestExpr
evaluates tofalse
, the loop ends.
while( *TestExpr* ) *Statement*
***图 9-3。*while 循环
下面的代码展示了一个while
循环的例子,其中测试表达式变量从值 3 开始,并在每次迭代中递减。当变量值变为 0 时,循环退出。
int x = 3; while( x > 0 ) { Console.WriteLine("x: {0}", x); x--; } Console.WriteLine("Out of loop");
该代码产生以下输出:
x: 3 x: 2 x: 1 Out of loop
do 循环
do
循环是一个简单的循环结构,其中测试表达式在循环的底部执行。这里显示了do
循环的语法,并在图 9-4 中进行了说明。
- First,
Statement
is executed.- Then,
TestExpr
is evaluated.- If
TestExpr
returns totrue
,Statement
is executed again.- Every time
TestExpr
returns totrue
,Statement
is executed again.- When
TestExpr
returns tofalse
, control is transferred to the statement after the loop construction is completed.
do *Statement* while( *TestExpr* ); // End of do loop
***图 9-4。*do 循环
do
循环有几个特征使它区别于其他控制流结构。它们是:
- The main body of the loop,
Statement
, is always executed at least once, even ifTestExpr
is originallyfalse
. The reason is thatTestExpr
is not evaluated until the bottom of the loop.- A semicolon is required after the closing bracket of the test expression.
以下代码显示了一个do
循环的例子:
int x = 0; do Console.WriteLine("x is {0}", x++); while (x<3); ↑ Required
该代码产生以下输出:
x is 0 x is 1 x is 2
for 循环
当测试表达式在循环的顶部求值时,只要它返回true
,for
循环结构就执行循环体。这里显示了for
循环的语法,如图 9-5 中的所示。
- At the beginning of the
for
loop,Initializer
is executed once.TestExpr
is the later evaluation.- If
TestExpr
returnstrue
,Statement
is executed, followed byIterationExpr
.- Control then returns to the top of the loop and evaluates
TestExpr
again.- As long as
TestExpr
returnstrue
Statement
, followed byIterationExpr
, it will be executed.- Once
TestExpr
returns tofalse
, continue to execute the statements afterStatement
.
Separated by semicolons ↓ ↓ for( *Initializer*; *TestExpr*; *IterationExpr* ) *Statement*
语句的某些部分是可选的,其他部分是必需的。
Initializer
,TestExpr
,IterationExpr
are optional. Their positions can be left blank. If theTestExpr
position is empty, the test assumes thattrue
is returned. Therefore, if the program wants to avoid entering an infinite loop, there must be other ways to exit the statement.- These semicolons are always required for field separators, even if any optional items are omitted.
***图 9-5。*for 循环
图 9-5 说明了通过for
语句的控制流程。您还应该了解其组件的以下信息:
Initializer
is only executed once, before any other part offor
structure. It is usually used to declare and initialize local values to be used in loops.TestExpr
is evaluated to determine whetherStatement
should be executed or skipped. It must calculate a value of typebool
. As mentioned earlier, ifTestExpr
is empty, the assumption is always true.IterationExp
R is executed immediately afterStatement
and before returning to the top ofTestExpr
cycle.
例如,在下面的代码中
- First, the initializer (
int i=0
) defines a variable namedi
and initializes its value to0
.- Then evaluate the condition (
i<3
). If it istrue
, then loop body is executed.- At the bottom of the loop, after all the loop statements are executed, execute the
IterationExpr
statement-in this case, increment the value ofi
.
` // The body of this for loop is executed three times.
for( int i=0 ; i<3 ; i++ )
Console.WriteLine(“Inside loop. i: {0}”, i);
Console.WriteLine(“Out of Loop”);`
该代码产生以下输出:
Inside loop. i: 0 Inside loop. i: 1 Inside loop. i: 2 Out of Loop
for 语句中变量的范围
在初始化器中声明的变量,称为循环变量,仅在 for
语句中可见。**
- This is different from C and C++, where declarations introduce variables into closed blocks.
- The following code illustrates this point:
` Type is needed here for declaration.
↓
for(int i=0; i<10; i++ ) // Variable i is in scope here, and also
Statement; // here within the statement.
// Here, after the statement, i no longer exists.
Type is needed here again because the previous variable i has gone out of existence.
↓
for(int i=0; i<10; i++ ) // We need to define a new variable i here,
Statement; // the previous one has gone out of existence.`
在循环体内声明的局部变量只有在循环内才是已知的。
注意循环变量通常被赋予标识符i
、j
或k
。这是 FORTRAN 编程时代的传统。在 FORTRAN 中,以字母 I 、 J 、 K 、 L 、 M 和 N 开头的标识符默认为INTEGER
类型,您不必声明它们。由于循环变量通常是整数,程序员简单地使用了简单的惯例,即使用I
作为循环变量的名称。它简短易用,而且你不用申报。如果他们有一个嵌套循环,内部循环变量通常被命名为J
。如果还有另一个内部嵌套循环,该变量被命名为K
。
虽然有些人反对使用非描述性的名称作为标识符,但我喜欢历史联系,以及使用这些标识符作为循环变量时的清晰和简洁。
初始值设定项和迭代表达式中的多个表达式
初始化表达式和迭代表达式都可以包含多个表达式,只要用逗号分隔。
例如,以下代码在初始值设定项中有两个变量声明,在迭代表达式中有两个表达式:
` static void Main( )
{
const int MaxI = 5;
Two declarations Two expressions
↓ ↓ for (int i = 0, j = 10; i < MaxI; i++, j += 10)
{
Console.WriteLine(“{0}, {1}”, i, j);
}
}`
该代码产生以下输出:
0, 10 1, 20 2, 30 3, 40 4, 50
切换语句
switch
语句实现多路分支。图 9-6 显示了switch
语句的语法和结构。
- The
switch
statement contains zero or more switch segments .- Each switch segment starts with one or more switch labels.
- Each switch segment must end with a
break
statement or one of the other four jump statements.
- The jump statements are
break
,return
,continue
,goto
andthrow
. These will be introduced later in this chapter.- Among the five jump statements, the
break
statement is the most commonly used statement to end aswitch
paragraph.break
statement transfers execution to the end ofswitch
statement. I will discuss all jump statements later in this chapter.
开关标签按顺序进行评估。如果一个匹配测试表达式的值,它的 switch 部分被执行,然后控制跳转到switch
语句的底部。
***图 9-6。*switch 语句的结构
开关标签具有以下形式:
<ins>case</ins> ConstantExpression: ↑ ↑ Keyword Switch label terminator
图 9-6 中通过结构的控制流程如下:
- Test the expression
TestExpr
and evaluate it at the top of the construct.- If the value of
TestExpr
is equal to the value ofConstExpr1
, the constant expression in the first switch tag is executed, and then the statements in the statement list of after the switch tag are executed until one of the jump statements is encountered.default
paragraph is optional, but if it is included, it must end with one of the jump statements.
图 9-7 说明了通过switch
语句进行控制的一般流程。您可以通过一个带有goto
语句或return
语句的switch
语句来修改流程。
***图 9-7。*通过 switch 语句控制的流程
一个开关例子
下面的代码执行了五次switch
语句,其中x
的值从1
到5
。从输出中,可以看出在循环的每个周期中执行了哪个 case 部分。
` for( int x=1; x<6; x++ )
{
switch( x ) // Evaluate the value of variable x.
{
case 2: // If x equals 2
Console.WriteLine(“x is {0} – In Case 2”, x);
break; // Go to end of switch.
case 5: // If x equals 5
Console.WriteLine(“x is {0} – In Case 5”, x);
break; // Go to end of switch.
default: // If x is neither 2 nor 5
Console.WriteLine(“x is {0} – In Default case”, x);
break; // Go to end of switch.
}
}`
该代码产生以下输出:
x is 1 -- In Default case x is 2 -- In Case 2 x is 3 -- In Default case x is 4 -- In Default case x is 5 -- In Case 5
更多关于开关的语句
一个switch
语句可以有任意数量的开关部分,包括零个。不需要default
部分,如下例所示。但是,包含它通常被认为是一种好的做法,因为它可以捕捉潜在的错误。
例如,下面代码中的switch
语句没有default
部分。switch
语句在一个for
循环中,该循环执行该语句五次,x
的值从1
开始,到5
结束。
for( int x=1; x<6; x++ ) { switch( x ) { case 5: Console.WriteLine("x is {0} -- In Case 5", x); break; } }
该代码产生以下输出:
x is 5 -- In Case 5
以下代码只有默认部分:
for( int x=1; x<4; x++ ) { switch( x ) { default: Console.WriteLine("x is {0} -- In Default case", x); break; } }
该代码产生以下输出:
x is 1 -- In Default case x is 2 -- In Default case x is 3 -- In Default case
切换标签
开关标签中关键字case
后面的表达式必须是常量表达式,因此在编译时,编译器必须完全可以对其求值。它还必须与测试表达式的类型相同。
例如,图 9-8 显示了三个示例switch
报表。
***图 9-8。*带有不同类型开关标签的开关报表
注意与 C 和 C++不同,在 C# 中每个switch
段,包括可选的默认段,都必须以其中一个跳转语句结束。在 C# 中,你不能在一个开关部分执行代码,然后通过进入下一个。
尽管 C# 不允许从一个开关部分切换到另一个开关部分,但您可以执行以下操作:
- You can attach multiple switch labels to any switch section.
- After the list of statements associated with a case, there must be a jump statement before the next switch tag, unless there is no to insert the executable statement between the switch tags.
例如,在下面的代码中,由于前三个开关标签之间没有可执行语句,所以一个接一个就可以了。然而,案例 5 和案例 6 之间有一个可执行语句,所以在案例 6 之前必须有一个跳转语句。
switch( x ) { case 1: // Acceptable case 2: case 3: ... // Execute this code if x equals 1, 2, or 3. break; case 5: y = x + 1; case 6: // Not acceptable because there is no break ...
跳转报表
当控制流到达跳转语句时,程序执行被无条件地转移到程序的另一部分。跳转语句如下:
break
continue
return
goto
throw
本章涵盖了这些陈述中的前四条。第十一章的中解释了throw
声明。
break 语句
在本章的前面,你看到了在switch
语句中使用的break
语句。它也可用于以下语句类型:
for
foreach
while
do
在这些语句的主体中,break
导致执行退出最里面的封闭循环。
例如,如果下面的while
循环只依赖于它的测试表达式,那么它将是一个无限循环,测试表达式总是true
。但是,在循环的三次迭代之后,遇到了break
语句,循环被退出。
int x = 0; while( true ) { x++; if( x >= 3 ) break; }
继续语句
continue
语句使程序执行到以下类型的最内层封闭循环的顶层:
while
do
for
foreach
例如,下面的for
循环执行五次。在前三次迭代中,它遇到了continue
语句并直接返回到循环的顶部,错过了循环底部的WriteLine
语句。执行仅在最后两次迭代中到达WriteLine
语句。
` for( int x=0; x<5; x++ ) // Execute loop five times
{
if( x < 3 ) // The first three times
continue; // Go directly back to top of loop
// This line is only reached when x is 3 or greater.
Console.WriteLine(“Value of x is {0}”, x);
}`
该代码产生以下输出:
Value of x is 3 Value of x is 4
下面的代码展示了一个在while
循环中的continue
语句的例子。这段代码产生与前面的for
循环示例相同的输出。
` int x = 0;
while( x < 5 )
{
if( x < 3 )
{
x++;
continue; // Go back to top of loop
}
// This line is reached only when x is 3 or greater.
Console.WriteLine(“Value of x is {0}”, x);
x++;
}`
标注报表
带标签的语句由一个标识符、一个冒号和一个语句组成。它具有以下形式:
Identifier: Statement
执行带标签的语句时,就好像标签不存在,只包含了 Statement
部分。
- Tagging statements allows control to be transferred from another part of the code to statements.
- Tagged statements can only be used inside a block.
标签
标签有自己的声明空间,因此带标签语句中的标识符可以是任何有效的标识符,包括那些可能在重叠范围内声明的标识符,如局部变量或参数名。
例如,以下代码显示了与局部变量具有相同标识符的标签的有效用法:
{ int xyz = 0; // Variable xyz ... xyz: Console.WriteLine("No problem."); // Label xyz }
然而,还是有一些限制。标识符不能是
- A keyword
- Same as another label identification and overlapping range
标注报表的范围
带标签的语句不能从声明它们的块之外的处看到(或访问)。带标签的语句的范围如下:
- Declare as
- Block, nested in the block.
内的任何块
例如,图 9-9 左边的代码包含了几个嵌套的块,并标记了它们的作用域。在程序的作用域 B 中声明了两个带标签的语句:increment
和end
。
- The shaded part on the right side of the figure shows the area where the markup statement is located in the code.
- Code in scope B, and all nested blocks, can see and access the marked statements.
- Code from any internal scope can jump from to marked statement.
- Code from outside (scope A in this example) cannot jump to a block with a tag statement.
***图 9-9。*标签的范围包括嵌套块。
goto 语句
goto
语句无条件地将控制权转移给一个*标记的语句。*其一般形式如下,其中 Identifier
是被标注语句的标识符:
goto *Identifier* ;
例如,下面的代码显示了一个goto
语句的简单用法:
` bool thingsAreFine;
while (true)
{
thingsAreFine = GetNuclearReactorCondition();
if ( thingsAreFine )
Console.WriteLine(“Things are fine.”);
else
goto NotSoGood;
}
NotSoGood: Console.WriteLine(“We have a problem.”);`
goto
语句必须是在标记语句的范围内。
- A
goto
statement can jump to any marked statement in its own block, or can jump out of to any block it is nested in.- The
goto
statement cannot jump to any block nested within its own block.
警告强烈反对使用goto
语句,因为这会导致代码结构不良,难以调试和维护。Edsger Dijkstra 在 1968 年写给 ACM 的题为“转到被认为有害的语句”的信是对计算机科学的一个重要贡献;这是最先发表的关于使用goto
语句的缺陷的描述之一。
switch 语句中的 goto 语句
还有另外两种形式的goto
语句,在switch
语句中使用。这些goto
语句将控制转移到switch
语句中相应命名的开关标签。
goto case ConstantExpression; goto default;
使用语句
某些类型的非托管对象数量有限,或者占用大量系统资源。很重要的一点是,当你的代码完成后,要尽快发布它们。using
语句有助于简化过程,并确保这些资源得到适当的处置。
资源是实现System.IDisposable
接口的类或结构。接口在第十五章中有详细介绍——但简而言之,接口是类和结构可以选择实现的未实现函数成员的集合。IDisposable
接口包含一个名为Dispose
的方法。
使用资源的阶段如图 9-10 所示,包括以下内容:
- Allocate resources
- Use resources
- Dispose of resources
如果在使用资源的代码部分发生意外的运行时错误,则释放资源的代码可能无法执行。
***图 9-10。*使用资源的组件
注意using
语句不同于using
指令。第二十一章中详细介绍了using
指令。
包装一种资源的使用
using
语句通过巧妙地打包资源的使用,有助于减少意外运行时错误的潜在问题。
using
语句有两种形式。第一种形式如下,如图 9-11 所示。
- Code in brackets allocates resources.
Statement
is the code for using resources.- The
using
statement implicitly generates the code of processing resources.
using ( <ins>*ResourceType* *Identifier* = *Expression*</ins> ) <ins>*Statement*</ins> ↑ ↑ Allocates resource Uses resource
意外的运行时错误被称为异常,在第二十二章中有详细介绍。处理异常可能性的标准方法是将可能导致异常的代码放在try
块中,并将必须执行的任何代码放在finally
块中,无论是否有异常。
这种形式的using
语句正是这样做的。它执行以下操作:
- Allocate resources
- Put
Statement
intotry
block.- Create a call to the
Dispose
method of the resource and put it in thefinally
block.
***图 9-11。*using 语句的效果
using 语句的例子
下面的代码使用了两次using
语句——一次用于名为TextWriter
的类,一次用于名为TextReader
的类,两次都来自于System.IO
名称空间。按照using
语句的要求,这两个类都实现了IDisposable
接口。
TextWriter
The resource opens a text file for writing, and writes a line into the file.TextReader
The resource then opens the same text file and reads and displays the contents line by line.- In both cases, the
using
statement ensures that theDispose
method of the object is called.- Pay attention to the difference between
using
statement inMain
andusing
instruction in the first two lines.
` using System; // using DIRECTIVE; not using statement
using System.IO; // using DIRECTIVE; not using statement
namespace UsingStatement
{
class Program
{
static void Main( )
{
// using statement
using (TextWriter tw = File.CreateText(“Lincoln.txt”) )
{
tw.WriteLine(“Four score and seven years ago, …”);
}
// using statement
using (TextReader tr = File.OpenText(“Lincoln.txt”))
{
string InputString;
while (null != (InputString = tr.ReadLine()))
Console.WriteLine(InputString);
}
}
}
}`
该代码产生以下输出:
Four score and seven years ago, ...
多个资源和嵌套
using
语句也可以用于相同类型的多个资源,用逗号分隔资源声明。语法如下:
Only one type Resource Resource ↓ <ins> ↓ </ins> <ins> ↓ </ins> using ( *ResourceType* Id1 = Expr1, Id2 = Expr2, ... ) *EmbeddedStatement*
例如,在下面的代码中,每个using
语句分配并使用两个资源:
` static void Main()
{
using (TextWriter tw1 = File.CreateText(“Lincoln.txt”),
tw2 = File.CreateText(“Franklin.txt”))
{
tw1.WriteLine(“Four score and seven years ago, …”);
tw2.WriteLine(“Early to bed; Early to rise …”);
}
using (TextReader tr1 = File.OpenText(“Lincoln.txt”),
tr2 = File.OpenText(“Franklin.txt”))
{
string InputString;
while (null != (InputString = tr1.ReadLine()))
Console.WriteLine(InputString);
while (null != (InputString = tr2.ReadLine()))
Console.WriteLine(InputString);
}
}`
using
语句也可以嵌套。在下面的代码中,除了嵌套using
语句之外,还要注意没有必要对第二个using
语句使用一个块,因为它只包含一个简单的语句。
` using ( TextWriter tw1 = File.CreateText(“Lincoln.txt”) )
{
tw1.WriteLine(“Four score and seven years ago, …”);
using ( TextWriter tw2 = File.CreateText(“Franklin.txt”) ) // Nested
tw2.WriteLine(“Early to bed; Early to rise …”); // Single
}`
using 语句的另一种形式
using
语句的另一种形式如下:
Keyword Resource Uses resource ↓ ↓ ↓ using ( *Expression* ) *EmbeddedStatement*
在这种形式中,资源是在using
语句之前声明的。
` TextWriter tw = File.CreateText(“Lincoln.txt”); // Resource declared
using ( tw ) // using statement
tw.WriteLine(“Four score and seven years ago, …”);`
尽管这种形式仍然可以确保在您使用完资源后总是调用Dispose
方法,但是它并不能防止您在using
语句释放其非托管资源后尝试使用该资源,使其处于不一致的状态。因此,它提供的保护较少,不被鼓励。该表格如图图 9-12 所示。
***图 9-12。*using 语句前的资源声明
其他报表
还有一些与该语言的特定特征相关的陈述。这些陈述包含在处理这些特性的章节中。其他章节中涉及的声明如表 9-1 所示。
十、结构
什么是结构?
结构是程序员定义的数据类型,非常类似于类。它们有数据成员和函数成员。尽管结构类似于类,但还是有许多重要的区别。最重要的如下:
- Similar to reference type, structure is value type.
- Structures are implicitly sealed, which means that they cannot be derived from.
声明结构的语法类似于声明类的语法:
`Keyword
↓
struct StructName
{
MemberDeclarations
}`
例如,下面的代码声明了一个名为Point
的结构。它有两个公共字段,名为X
和Y
。在Main
中,声明了三个结构类型为Point
的变量,它们的值被赋值并打印出来。
` struct Point
{
public int X;
public int Y;
}
class Program
{
static void Main()
{
Point first, second, third;
first.X = 10; first.Y = 10;
second.X = 20; second.Y = 20;
third.X = first.X + second.X;
third.Y = first.Y + second.Y;
Console.WriteLine(“first: {0}, {1}”, first.X, first.Y);
Console.WriteLine(“second: {0}, {1}”, second.X, second.Y);
Console.WriteLine(“third: {0}, {1}”, third.X, third.Y);
}
}`
这段代码产生以下输出:
first: 10, 10 second: 20, 20 third: 30, 30
结构是值类型
与所有值类型一样,结构类型的变量包含自己的数据。因此
- A variable of type struct cannot be
null
.- Two structs variables cannot refer to the same object.
例如,下面的代码声明了一个名为CSimple
的类,一个名为Simple
的结构,以及它们各自的一个变量。图 10-1 显示了这两者在内存中是如何排列的。
` class CSimple
{
public int X;
public int Y;
}
struct Simple
{
public int X;
public int Y;
}
class Program
{
static void Main()
{
CSimple cs = new CSimple();
Simple ss = new Simple();
…`
图 10-1 。类与结构的内存排列
分配给一个结构
将一个结构赋给另一个结构会将值从一个结构复制到另一个结构。这与从类变量复制截然不同,后者只复制引用。
图 10-2 显示了类变量和结构变量赋值的区别。注意,在类赋值之后,cs2
指向堆中与cs1
相同的对象。但是在 struct 赋值之后,ss2
成员的值是那些在ss1
中的值的副本。
` class CSimple
{ public int X; public int Y; }
struct Simple
{ public int X; public int Y; }
class Program
{
static void Main()
{
CSimple cs1 = new CSimple(), cs2 = null; // Class instances
Simple ss1 = new Simple(), ss2 = new Simple(); // Struct instances
cs1.X = ss1.X = 5; // Assign 5 to ss1.X and cs1.X.
cs1.Y = ss1.Y = 10; // Assign 10 to ss1.Y and cs1.Y.
cs2 = cs1; // Assign class instance.
ss2 = ss1; // Assign struct instance.
}
}`
图 10-2 。分配一个类变量和一个结构变量
构造函数和析构函数
结构可以有实例和静态构造函数,但不允许析构函数。
实例构造函数
该语言隐式地为每个结构提供了一个无参数的构造函数。此构造函数将结构的每个成员设置为该类型的默认值。值成员被设置为其默认值。参考成员被设置为null
。
每个结构都有预定义的无参数构造函数,您不能删除或重定义它。但是,您可以创建额外的构造函数,只要它们有参数。请注意,这与类不同。对于类,只有当没有其他构造函数被声明时,编译器才会提供一个隐式的无参数构造函数*。*
要调用一个构造函数,包括隐式的无参数构造函数,使用new
操作符。注意,即使内存不是从堆中分配的,也使用了new
操作符。
例如,下面的代码声明了一个简单的 struct,其构造函数带有两个int
参数。Main
创建该结构的两个实例——一个使用隐式无参数构造函数,另一个使用声明的双参数构造函数。
` struct Simple
{
public int X;
public int Y;
public Simple(int a, int b) // Constructor with parameters
{
X = a;
Y = b;
}
}
class Program
{
static void Main()
{ Call implicit constructor
↓ Simple s1 = new Simple();
Simple s2 = new Simple(5, 10);
↑
Call constructor
Console.WriteLine(“{0},{1}”, s1.X, s1.Y);
Console.WriteLine(“{0},{1}”, s2.X, s2.Y);
}
}`
你也可以不使用new
操作符来创建一个结构的实例。但是,如果这样做,会有一些限制,如下所示:
- The values of data members cannot be used unless explicitly set.
- You can’t call any function members of this structure until all data members are assigned values.
例如,下面的代码显示了在没有使用new
操作符的情况下创建的 struct Simple
的两个实例。当试图在没有显式设置数据成员值的情况下访问s1
时,编译器会产生一条错误消息。在给其成员赋值后,从s2
中读取没有问题。
`struct Simple
{
public int X;
public int Y;
}
class Program
{
static void Main()
{
No constructor calls
↓ ↓
Simple s1, s2;
Console.WriteLine(“{0},{1}”, s1.X, s1.Y); // Compiler error
↑ ↑
s2.X = 5; Not yet assigned
s2.Y = 10;
Console.WriteLine(“{0},{1}”, s2.X, s2.Y); // OK
}
}`
静态构造函数
与类一样,结构的静态构造函数创建并初始化静态数据成员,不能引用实例成员。结构的静态构造函数遵循与类相同的规则。
在以下两个操作中的第一个操作之前调用静态构造函数:
- Call an explicitly declared constructor.
- A static member of a reference structure
构造函数和析构函数概要
不允许字段初始值设定项
结构声明中不允许字段初始值设定项,如下面的代码所示:
struct Simple { Not allowed <ins> ↓ </ins> public int x = 0; // Compile error public int y <ins>= 10</ins>; // Compile error } ↑ Not allowed
结构是密封的
结构总是隐式密封的,因此不能从它们派生其他结构。
由于结构不支持继承,对结构成员使用几个类成员修饰符没有意义;因此,不能在它们的声明中使用它们。不能与结构一起使用的修饰符如下:
protected
- inside
- abstract
virtual
结构本身是从System.ValueType
派生出来的,而System.ValueType
又是从object
派生出来的。
当创建一个与基类System.ValueType
成员同名的成员时,你可以对结构成员使用的两个继承相关的关键字是new
和override
修饰符,所有的结构都是从基类【】派生的。
装箱和拆箱
与其他值类型数据一样,如果要将 struct 实例用作引用类型对象,则必须进行装箱复制。装箱是制作值类型变量的引用类型副本的过程。装箱和拆箱在第十六章中有详细解释。
结构体作为返回值和参数
结构可以用作返回值和参数。
- Return value : When struct is the return value, create a copy and return it from the function member.
- Parameter: when a struct is used as Value parameter, a copy of the actual parameter struct is created. This copy is used for the execution of the method.
ref
andout
Parameters : If a structure is used as aref
orout
parameter, the reference to the structure will be passed to the method so that the data member can be changed.
关于结构的附加信息
分配结构比创建类的实例需要更少的开销,所以使用结构而不是类有时可以提高性能——但是要小心装箱和拆箱的高成本。
最后,关于结构,您应该知道的最后一些事情如下:
- Predefined simple types (
int
,short
,long
, etc.), although in. NET and C# are actually all in. NET as a structure.- You can declare the
partial
structure as you declare thepartial
class, as described in Chapter 6 of .
像类一样,结构也可以实现接口,这将在第十五章中讨论。