控制流程语句
控制流程语句在类C风格语言中都比较相似,这里我们简单介绍下C#中各种控制流程语句的写法,此外关于循环,我们这里额外介绍可迭代对象的接口以及生成器函数相关的知识。
判断
if else 语句
if else语句根据括号中的表达式返回true或false,执行对应的逻辑分支,判断的顺序是从上到下,且只会进入一个条件分支,也就是说如果有多个条件分支满足判断,也只会进入第一个。
if (TestExpr)
Statement1
else if (TestExpr)
Statement2
else
Statement3
这里要注意的是和C/C++/JavaScript不同,C#中判断分支语句必须严格符合bool类型,而不能使用int等。
switch case 语句
switch case语句将表达式的值依次和各分支标签比较,匹配成功则进入对应分支。和C/C++、Java不同,C#禁止分支“穿透”(fall through),除分支为空标签等少数情况外,每个分支都必须以break、return或goto等跳转语句结束,否则无法通过编译,因此C#中并不存在漏写break导致意外执行下一个分支的问题。
switch (TestExpr)
{
case Constant1:
Statement
break;
case Constant2:
Statement
break;
default:
Statement
break;
}
传统switch语句中,case后面的Constant1、Constant2必须是编译期常量。C# 7.0引入模式匹配后,case标签还可以编写类型模式、属性模式等更丰富的模式。
switch 表达式(C# 8.0)
C# 8.0引入了switch表达式,它把switch从语句升级为了表达式,可以直接基于匹配结果返回一个值,写法比起switch语句更加紧凑。
int day = 3;
string name = day switch
{
1 => "星期一",
2 => "星期二",
3 => "星期三",
_ => "未知"
};
switch表达式中使用=>分隔模式和返回值,_是弃元模式,即用于匹配所有剩余情况,作用类似switch语句中的default。
循环
while 语句
while语句会一直执行循环体的代码,直到判断条件为false。
while (TestExpr)
Statement
do while 语句
do while语句和while语句类似,只不过是先执行循环体再判断。
do
Statement
while (TestExpr)
for 语句
for循环语句通常会引入一个计数变量,用于自增或自减,直到满足条件。
for (Initializer; TestExpr; IterationExpr)
Statement
对于这种简单的计数变量、临时变量之类,我们可以使用类型推导来简化代码。
for (var i = 0; i < 3; i++)
{
Console.WriteLine(i);
}
foreach 语句
C#中foreach语句可以不使用计数变量直接循环一个可迭代对象,例子如下。
int[] arr = { 1, 2, 3 };
foreach (int i in arr)
{
Console.WriteLine(i);
}
IEnumerable和IEnumerator
foreach语句中所谓的可迭代对象,严格来说是指具有公开的GetEnumerator()方法、且该方法返回的枚举器包含MoveNext()方法和Current属性的类型,这被称为“可枚举模式”(enumerable pattern)。实现了IEnumerable<T>(或IEnumerable)接口的类自然满足这一模式,因此最常见。实际开发中,我们可以很方便地使用生成器函数创建这样的可迭代对象,下面是一段例子代码。
namespace Gacfox.Demo.DemoNetCore;
public class Program
{
IEnumerable<int> Foo()
{
yield return 1;
yield return 2;
yield return 3;
yield return 4;
}
static void Main()
{
Program program = new();
IEnumerator<int> enumerator = program.Foo().GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}
}
代码中,Foo()是我们自定义的生成器函数,其返回值为IEnumerable<int>类型。这里要注意,System.Collections.Generic命名空间中包含的IEnumerable<>是必须带泛型的,而System.Collections命名空间下还有一个不带泛型的IEnumerable,这是历史原因造成的,通常来说我们没必要使用后者。
有关生成器函数,其实使用方法很简单,生成器函数内部使用yield return语句返回若干值,其返回类型可以是可迭代对象IEnumerable<>(或IEnumerable),也可以是枚举器IEnumerator<>(或IEnumerator),C# 8.0之后还可以返回支持异步流的IAsyncEnumerable<>。在对其返回的可迭代对象进行迭代时,对应yield return的值就会被顺序取出。
在Main()函数中的代码演示了这一过程的基本原理,代码中我们使用了GetEnumerator()方法获取了IEnumerator<int>类型的对象,并用MoveNext()方法判断迭代是否完成,同时使用Current属性取出当前迭代的值。
实际开发中,我们不会像上面Main()方法中的代码来使用可迭代对象,一般都使用foreach语句。
namespace Gacfox.Demo.DemoNetCore;
public class Program
{
IEnumerable<int> Foo()
{
yield return 1;
yield return 2;
yield return 3;
yield return 4;
}
static void Main()
{
Program program = new();
foreach (int i in program.Foo())
{
Console.WriteLine(i);
}
}
}
生成器函数
前面已经介绍过,生成器函数内部使用yield return语句返回若干值,其返回类型可以是IEnumerable<>、IEnumerator<>(以及对应的非泛型版本)等,在对其返回的可迭代对象进行foreach迭代时,对应yield return的值就会被顺序取出。
IEnumerable<int> Foo()
{
yield return 1;
yield return 2;
yield return 3;
yield return 4;
}
此外,我们还可以使用yield break终止生成器函数的迭代,例子如下。
IEnumerable<int> Foo()
{
yield return 1;
yield return 2;
yield break;
yield return 4;
}
上面代码我们使用foreach迭代时,只能得到1和2。
我们已经学习了如何使用生成器函数编写可迭代对象,这里我们再详细分析一下生成器函数的执行流程。
namespace Gacfox.Demo.DemoNetCore;
public class Program
{
IEnumerable<int> Foo()
{
Console.WriteLine("before yield return");
yield return 1;
Console.WriteLine("after yield return");
}
static void Main()
{
Program program = new();
IEnumerator<int> enumerator = program.Foo().GetEnumerator();
Console.WriteLine("before first call Foo");
enumerator.MoveNext();
Console.WriteLine("after first call Foo");
Console.WriteLine("before second call Foo");
enumerator.MoveNext();
Console.WriteLine("after second call Foo");
}
}
程序最终输出如下。
before first call Foo
before yield return
after first call Foo
before second call Foo
after yield return
after second call Foo
我们可以发现生成器函数的特点,我们第1次调用MoveNext()时,生成器函数执行到yield return就停止了,而实际上其后面还有一段代码没有执行;而第2次调用MoveNext()时会恢复执行yield之后的剩余代码,直到方法结束。
跳转
前面介绍了判断和循环语句,实际上跳转语句也属于控制流程语句,它们一般和判断、循环语句结合使用。跳转语句的用法和类C语言基本一致,这里就不多介绍了。
break:用于跳出循环,或结束switch语句中的一个分支。
continue:用于结束当前循环体,直接开启下一次循环。
return:用于直接从函数返回。
goto:用于无条件跳转到一个标签,通常在跳出多层循环时使用。在switch语句中,还可以使用goto case和goto default跳转到指定分支,这也是C#中在switch内部实现“穿透”的手段。
using语句块
using关键字除了之前介绍过的引入命名空间和为类型、命名空间创建别名,还有一个与资源释放相关的重要用法,各种用法梳理如下。
using System;:引入命名空间。using Foo = System.Console;:为类型或命名空间创建别名,别名可以指向类型。using static System.Math;:静态导入,可直接使用指定类型的静态成员。using (var res = ...) { }或using var res = ...;:确保IDisposable对象在离开作用域时被释放。
像文件句柄、字体、网络连接这类对象往往包含非托管资源,它们在使用完成后需要调用Dispose()方法释放,否则可能造成资源泄漏。如果手动编写,还需要配合try...finally才能保证Dispose()一定被调用,代码比较繁琐,使用using是更推荐的方式。
using (var font = new Font("Arial", 10.0f))
{
byte charset = font.GdiCharSet;
}
上面using语句块会在块结束后自动调用font.Dispose()。C# 8.0还引入了更简洁的using声明写法,它不需要额外的大括号,变量会在其所在作用域结束时被释放。
using var font = new Font("Arial", 10.0f);
byte charset = font.GdiCharSet;
此外,对于实现了IAsyncDisposable接口的对象,可以使用await using,在释放时进行异步调用。
await using var stream = new FileStream("data.txt", FileMode.Open);