目录
程序操作使用语句进行表示。C#支持几种不同的语句,其中许多语句是从嵌入语句的角度来定义的。
- 代码块:由一系列在分隔符
{
和}
内编写的语句组成,可以包含一个或多个语句。当代码块只包含一个语句时,可以省略{}
。 - 声明语句:用于声明局部变量和常量。
- 表达式语句:用于计算表达式,包括方法调用、使用
new
运算符的对象分配、赋值、递增和递减运算等。 - 选择语句:用于根据一些表达式的值从多个可能的语句中选择一个以供执行,如
if
、if-else
和switch
。 - 循环语句:用于重复执行嵌入语句,如
while
、do-while
、for
和foreach
。 - 跳转语句:用于转移控制权,如
break
、continue
、return
、goto
、throw
和yield
。 - 异常处理语句:用于捕获并处理在代码块执行期间发生的异常,如
try
、catch
和finally
。 - 溢出检查语句用于控制整型类型算术运算和转换的溢出检查上下文,如
checked
和unchecked
。 unsafe
语句用于声明不安全的上下文,主要用于操作指针。fixed
语句用于防止垃圾收集器重新定位可移动变量。lock
语句用于获取给定对象的相互排斥锁定,执行语句,然后解除锁定。using
语句用于获取资源,执行语句,然后释放资源。yield
语句用于迭代器以提供下一个值或表示迭代结束。
选择语句
选择语句也叫分支语句,可以根据表达式的值从许多可能的路径中选择要执行的语句。if
语句根据布尔表达式的值来选择要执行的语句,switch
语句根据与表达式匹配的模式在语句列表中选择要执行的语句。
if
if
语句有两种形式:不包含else
部分的if
语句仅在布尔表达式计算结果为true
时执行其主体;包含else
部分的if
语句根据布尔表达式的值选择两个语句中的一个来执行。
if (condition) { consequent; }
if (condition) { consequent; }
else if (condition) { alternative; }
else { alternative; }
简单的if else
语句可以使用?:
运算符代替以简化代码。可嵌套if
语句来检查多个条件。
switch
switch
语句根据与表达式的模式在语句列表中选择要执行的语句。switch
语句按文本顺序从上到下对case
进行匹配,若匹配成功,并且匹配约束(case guard)为true
(如果存在的话),则进入相应case
。default
指定匹配表达式与其他任何case
都不匹配时要执行的语句。default
可以在switch
的任何位置,但不管在哪个位置,default
总是最后计算,并且仅在其他所有case
都不匹配时计算。如果匹配表达式与任何case
都不匹配,且没有default
,控制就会贯穿switch
语句。
匹配约束为与case
匹配的附加条件。匹配约束必须是布尔表达式,在case
后面的when
关键字之后指定。
switch ((int)score / 10)
{
case 10 when score == 100f:
case 9:
Console.WriteLine("Great!");
break;
case 8:
Console.WriteLine("Good!");
break;
case 7:
case 6:
Console.WriteLine("Qualified!");
break;
case 5:
case 4:
case 3:
case 2:
case 1:
case 0:
Console.WriteLine("Failed!");
break;
default:
Console.WriteLine("Wrong Input!");
break;
}
编译器在switch
语句包含无法访问的case
时会生成错误。可以为switch
语句的一部分指定多个case
(即控制可以从一个case
贯穿到下一个case
),但通常使用break
语句将控制从case
语句传递出去。还可使用return
、goto
和throw
语句将控制从case
语句传递出去。
switch
语句支持整型、浮点、字符、字符串、枚举,从C# 7.0开始支持模式匹配。
循环语句
循环语句用于重复执行一条语句或代码块,通常包括四个部分:声明并初始化循环控制变量、循环条件表达式、循环体、迭代操作。
- 通常需要声明并初始化循环控制变量。
- 循环条件表达式是一个布尔表达式,用于确定是否应执行循环中的下一个迭代。若该表达式计算结果为
true
或不存在,执行下一个迭代,否则退出循环。 - 循环体是循环执行的操作,可包含一条语句或一个代码块。
- 迭代操作是在每此循环体执行后将执行的操作,用于改变循环控制变量从而控制是否该退出循环。
在循环语句里循环体中的任何位置都可以使用break
语句中断循环,或者使用continue
语句继续执行循环中的下一次迭代。
while
在循环条件表达式的计算结果为true
时,while
语句会执行循环体。由于在每次计算此表达式之后才执行循环体,所以while
循环体会执行0次或多次。 应在while
循环体中改变循环控制变量。
int i = 0;
while (i < 5)
{
i++;
}
do-while
在循环条件表达式的计算结果为true
时,do-while
语句会执行循环体。由于在每次计算此表达式之前都会执行循环体,所以do-while
循环体至少会执行1次。 应在do-while
循环体中改变循环控制变量。
int i = 0;
do
{
i++;
}while (i < 5);
for
在循环条件表达式的计算结果为true
时,for
语句会执行循环体。
- 声明并初始化循环控制变量可以在
for
语句的初始化表达式中进行,该初始化仅在进入循环前执行一次。此时该循环控制变量无法从for
语句外部访问。 - 迭代操作不仅可以包括对循环控制变量的操作,还可包含用逗号分隔的零个或多个其他语句表达式。
for (int i = 0, b = 0; i < 2; i++, b++)
{
// 循环体
}
for
语句的所有部分都是可选的。
// 死循环
for { ; ; }
{
// 循环体
}
foreach
foreach
语句通常用于从头至尾循环读取数据集合中的所有子项而不需要循环控制变量、循环条件表达式和迭代操作, 如遍历数组等。
int[] array = new int[10];
foreach (int element in array)
{
// 循环体
}
以下类型可以使用foreach
语句遍历:
- 实现了
System.Collections.Generic.IEnumerable
或System.Collections.Generic.IEnumerable<T>
接口。 - 具有公共无参的
GetEnumerator()
方法(可以是扩展方法),并且该方法的返回值具有公共Current
属性和公共无参且返回值为bool
类型的MoveNext
方法。
如果foreach
语句读取到的引用为null
,则会引发NullReferenceException错误。如果foreach
语句的源集合为空,则foreach
语句的循环体不会被执行,而是被跳过。
foreach
语句只能读取,无法改变源集合中的数据。 读取的子项item
需要指定类型,显示指定类型时,源集合中数据类型必须可以隐式或显示的转换为指定类型,否则将引发InvalidCastException错误;可以使用var
关键字推断类型。
原理:foreach
首先会获得源集合中的迭代器(因此源集合需要具有迭代器才能使用foreach
遍历),在循环中通过迭代器的MoveNext()
移动到下一个元素,然后获取元素Current
,执行操作。
// 获取迭代器
IEnumerator iterator = 源集合.GetEnumerator();
// 移动到下一个元素
while (iterator.MoveNext())
{
// 获取元素
var item = iterator.Current;
// 其他操作
}
跳转语句
跳转语句可以无条件地转移程序控制。
break
break
终止最近的封闭循环语句(即for
、foreach
、while
或do-while
)或switch
语句,并将控制权转交给已终止语句后面的语句(若有)。
在嵌套循环中,break
仅终止包含它的最内部循环。在循环内使用switch
语句时,break
仅从switch
语句中转移控制权,包含switch
的循环不受影响。
continue
continue
中止当前轮的循环,开始最近的封闭循环语句(即for
、foreach
、while
或do-while
)新一轮的循环。
return
return
终止其所在的方法,并将控制权和方法结果(若有)返回给调用方。如果方法无返回值,则使用不带表达式的return
语句,无return
语句时在执行完最后一条语句后终止。
如果return
语句具有表达式,该表达式必须可隐式转换为函数成员的返回类型,除非它是异步的。对于async
函数,表达式必须可隐式转换为Task<TResult>
或ValueTask<TResult>
类型,以函数的返回类型为准。如果async
函数的返回类型为Task
或ValueTask
,则使用不带表达式的return
语句。
默认情况下,return
语句返回表达式的值。从C# 7.0开始,可以使用带ref
关键字的return
语句返回对变量的引用。
ref int FindFirst(int[] numbers, Func<int, bool> predicate)
{
for (int i = 0; i < numbers.Length; i++)
{
if (predicate(numbers[i]))
{
return ref numbers[i];
}
}
throw new InvalidOperationException("No element satisfies the given condition.");
}
goto
goto
将控制权转交给带有指定标签的语句。
int i = 0;
if (i > 5)
{
goto Condition;
}
else
{
// 操作
}
Condition:
// 操作
可以使用goto
语句退出循环或在switch
语句中将控制权移交到具有常量标签的case
,还可使用语句goto default;
将控制权转交给default
。
int choice = 1;
int result = 0;
switch (choice)
{
case 0:
result += 0;
break;
case 1:
result += 1;
goto case 0;
case 2:
result += 2;
goto default;
default:
result += 10;
break;
}
如果当前函数成员中不存在具有给定名称的标签,或者goto
语句不在标签范围内,则会出现编译时错误,即不能使用goto
语句将控制权从当前函数成员转移到任何嵌套范围之外的地方。
异常处理语句
使用try
语句在可能出现异常的地方捕获异常,catch
语句处理异常,finally
语句释放资源,throw
关键字引发异常。
throw
引发程序执行期间出现异常的信号。然后方法调用方使用try-catch
或try-catch-finally
块来处理引发的异常。
// 语法
throw [e]; // e是一个派生自System.Exception类的实例
public class NumberGenerator
{
int[] numbers = { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
public int GetNumber(int index)
{
if (index < 0 || index >= numbers.Length)
{
throw new IndexOutOfRangeException();
}
return numbers[index];
}
}
throw
也可以用于catch
块,以重新引发在catch
块中处理的异常。此时,throw
不使用异常操作数。
public char GetFirstCharacter()
{
try
{
return Value[0];
}
catch (NullReferenceException e)
{
throw;
}
}
从C# 7.0开始,throw
可以用作表达式和语句,这允许在上下文中引发异常。
// 在C# 7.0之前,此逻辑需在if/else中实现
string arg = args.Length >=
1 ? args[0] : throw new ArgumentException("You must supply an argument");
try-catch
try-catch语句为后接一或多个catch
子句的try
块,这些子句指定不同异常的处理程序。try
块包含可能导致异常的受保护的代码。
引发异常时,公共语言运行时(CLR)查找处理此异常的catch
语句。如果当前正在执行的方法不包含此类catch
块,则CLR查看调用了当前方法的方法,并以此类推遍历调用堆栈。如果未找到任何catch
,则CLR向用户显示一条未处理的异常消息,并停止执行程序。
不推荐使用不带参数catch
子句来捕获任何类型的异常。通常只应捕获知道如何从其恢复的异常。 因此,应始终指定派生自System.Exception
的对象参数。异常类型应尽可能具体,以避免不正确地接受异常处理程序实际上无法解决的异常。因此,最好是在Exception
基类型上使用具体的异常。
// 处理异常
int[] gen = new int[] { 1 };
try
{
int value = gen.GetNumber(index);
Console.WriteLine($"Retrieved {value}");
}
catch (IndexOutOfRangeException e)
{
Console.WriteLine($"{e.GetType().Name}: {index} is outside the bounds of the array");
}
// Output: IndexOutOfRangeException: 10 is outside the bounds of the array
可以使用同一try-catch语句中的多个特定catch
子句。在这种情况下,catch
子句的顺序很重要,因为catch
子句是按顺序检查的。在使用更笼统的子句之前获取更细节的异常。如果catch
块的排序使得永不会达到后面的catch
块,则编译器将产生错误。
try
{
string s = null;
ProcessString(s);
}
// Most specific:
catch (ArgumentNullException e)
{
Console.WriteLine("{0} First exception caught.", e);
}
// Least specific:
catch (Exception e)
{
Console.WriteLine("{0} Second exception caught.", e);
}
筛选想要处理的异常的一种方式是使用catch
参数。也可以使用异常约束进一步检查该异常以决定是否要对其进行处理。如果异常约束返回false
,则继续搜索处理程序。
catch (InvalidCastException e)
{
if (e.Data == null)
{
throw;
}
else
{
// Take some action.
}
}
// 等价于
catch (InvalidCastException e) when (e.Data != null)
{
// Take some action.
}
异常约束要优于捕获和重新引发(如下所述),因为约束将保留堆栈不受损坏。如果之后的处理程序转储堆栈,可以查看到异常的原始来源,而不只是重新引发它的最后一个位置。异常约束表达式的一个常见用途是日志记录。可以创建一个始终返回false
并输出到日志的异常约束,能在异常通过时进行记录,且无需处理并重新引发它们。
可在catch
块中使用throw
语句以重新引发已由catch
语句捕获的异常。此时,throw
不使用异常操作数。可以捕获一个异常而引发一个不同的异常。执行此操作时,请指定作为内部异常捕获的异常。
catch (InvalidCastException e)
{
// Perform some action here, and then throw a new exception.
throw new YourCustomException("Put your error message here.", e);
}
在try
块内,仅能初始化在其内部声明的变量;否则,在完成执行块之前,可能会出现异常。
try-finally
通过使用finally
块,可以清除try
块中分配的任何资源,即使在try
块中发生异常,也可以运行代码。通常情况下,finally
块的语句会在控制离开try
语句时运行。
已处理的异常中会保证运行相关联的finally
块。但是,如果异常未经处理,则finally
块的执行将取决于异常解除操作的触发方式。反过来,这又取决于计算机的设置方式。只有在finally
子句不运行的情况下,才会涉及程序被立即停止的情况。
通常情况下,当未经处理的异常终止应用程序时,finally
块是否运行已不重要。但是,如果finally
块中的语句必须在这种情况下运行,则可以将catch
块添加到try-finally
语句。另一种解决方法是,可以捕获可能在调用堆栈上方的try-finally
语句的try
块中引发的异常。可以通过以下几种方法来捕获异常:调用包含try-finally
语句的方法、调用该方法或调用堆栈中的任何方法。如果未捕获异常,则finally
块的执行取决于操作系统是否选择触发异常解除操作。
try-catch-finally
使用try-catch-finally
语句来处理在try
块执行期间可能发生的异常,并指定当控制离开try
语句时必须执行的代码。当异常由catch
块处理时,finally
块在该catch
块执行之后执行(即使在执行catch
块期间发生另一个异常)。
溢出检查语句
checked
和unchecked
语句用于控制整型类型算术运算和转换的溢出检查上下文。在unchecked
的代码块中,会自动丢弃不适合目标类型的任何高阶位来截断操作结果。在checked
代码块中则会引发编译时异常(compile-time)。
uint a = uint.MaxValue;
unchecked
{
Console.WriteLine(a + 3); // output: 2
}
double b = double.MaxValue;
try
{
a = checked((int)b);
}
catch (OverflowException e)
{
Console.WriteLine(e.Message); // output: Arithmetic operation resulted in an overflow.
}
checked
和unchecked
语句仅对当前语句块或强转括号内的操作执行溢出检查。
int Multiply(int a, int b) => a * b;
int factor = 2;
try
{
checked
{
// 不检查其他地方的溢出
Console.WriteLine(Multiply(factor, int.MaxValue)); // output: -2
}
}
catch (OverflowException e)
{
Console.WriteLine(e.Message);
}
try
{
checked
{
// 仅对当前语句块或强转括号内的操作执行溢出检查
Console.WriteLine(Multiply(factor, factor * int.MaxValue));
}
}
catch (OverflowException e)
{
Console.WriteLine(e.Message); // output: Arithmetic operation resulted in an overflow.
}
其他语句
unsafe
语句表示涉及指针操作的不安全的上下文。可以在类型或成员的声明中使用不安全修饰符,此时类型或成员的整个作用域都会被认为是不安全的上下文;也可以使用unsafe
语句在该块中使用不安全代码。
unsafe static void FastCopy(byte[] src, byte[] dst, int count)
{
// 不安全的上下文:从参数列表到代码块结束
}
unsafe
{
// 不安全的上下文
}
fixed
语句防止垃圾收集器重新定位可移动变量,并声明指向该变量的指针。固定或固定变量的地址在语句执行期间不会改变。只能在相应的固定语句中使用声明的指针。声明的指针是只读的,不能修改。使用stackalloc
表达式在栈上分配的内存不受垃圾收集器的控制因此不需要fixed
。
unsafe
{
byte[] bytes = [1, 2, 3];
fixed (byte* pointerToFirst = bytes)
{
// 操作
}
}
lock
语句能够获取给定对象(引用类型)的互斥锁并执行语句块,然后释放锁。当锁被持有时,持有该锁的线程可以再次获取并释放该锁,而任何其他线程都被阻止获取锁并等待直到锁被释放。lock
语句确保在任何时刻最多只有一个线程执行它的线程体。
lock (x)
{
// 操作
}
// 等价于
object _lockObj = x;
bool _lockWasTaken = false;
try
{
System.Threading.Monitor.Enter(_lockObj, ref _lockWasTaken);
// 操作
}
finally
{
if (_lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}
using
语句确保正确使用IDisposable
释放实例。当程序控制离开using
语句时,即使在该语句内发生异常,也可以确保指定实例在其作用域结束时能够被释放。由using
语声明的变量(可以同时声明多个,释放顺序与声明顺序相反)是只读的,不能对其重新赋值或将其作为ref
/out
参数传递。
string filePath = "";
using (StreamReader reader = File.OpenText(filePath))
{
// 使用资源
}
迭代器中使用yield
语句来提供下一个值(yield return
)或表示迭代结束(yield break
)。当迭代器开始迭代时,执行到第一个yield return
语句,然后挂起迭代器并将第一个迭代值返回调用者处理;在随后的每次迭代中,迭代器从之前挂起的yield return
语句之后继续执行,并一直到下一个yield return
语句;当控制到达迭代器末尾或yield break
语句时,迭代终止。
var numbers = ProduceEvenNumbers(5);
foreach (int i in numbers)
{
// 操作
}
IEnumerable<int> ProduceEvenNumbers(int upto)
{
for (int i = 0; i <= upto; i += 2)
{
yield return i;
}
}