控制流程语句

控制流程语句在类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);
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。