详解C#中yield关键字的用法

 更新时间:2023年07月25日 10:31:49   作者:橙子家  
yield 关键字的用途是把指令推迟到程序实际需要的时候再执行,这个特性允许我们更细致地控制集合每个元素产生的时机,那么下面就一起来看下怎么用 yield 关键字吧

〇、前言

yield 关键字的用途是把指令推迟到程序实际需要的时候再执行,这个特性允许我们更细致地控制集合每个元素产生的时机。

对于一些大型集合,加载起来比较耗时,此时最好是先返回一个来让系统持续展示目标内容。类似于在餐馆吃饭,肯定是做好一个菜就上桌了,而不会全部的菜都做好一起上。

另外还有一个好处是,可以提高内存使用效率。当我们有一个方法要返回一个集合时,而作为方法的实现者我们并不清楚方法调用者具体在什么时候要使用该集合数据。如果我们不使用 yield 关键字,则意味着需要把集合数据装载到内存中等待被使用,这可能导致数据在内存中占用较长的时间。

下面就一起来看下怎么用 yield 关键字吧。

一、yield 关键字的使用

1.1 yield return:在迭代中一个一个返回待处理的值

如下示例,循环输出小于 9 的偶数,并记录执行任务的线程 ID:

class Program
{
    static async Task Main(string[] args)
    {
        foreach (int i in ProduceEvenNumbers(9))
        {
            ConsoleExt.Write($"{i}-Main");
        }
        ConsoleExt.Write($"--Main-循环结束");
        Console.ReadLine();
    }
    static IEnumerable<int> ProduceEvenNumbers(int upto)
    {
        for (int i = 0; i <= upto; i += 2)
        {
            ConsoleExt.Write($"{i}-ProduceEvenNumbers");
            yield return i;
            ConsoleExt.Write($"{i}-ProduceEvenNumbers-yielded");
        }
        ConsoleExt.Write($"--ProduceEvenNumbers-循环结束");
    }
}
public static class ConsoleExt
{
    public static void Write(object message)
    {
        Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
    }
    public static void WriteLine(object message)
    {
        Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
    }
    public static async void WriteLineAsync(object message)
    {
        await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} "));
    }
}

输出结果如下,可见整个循环是单线程运行,ProduceEvenNumbers()生产一个,然后Main()就操作一个,Main() 执行一次操作后,线程返回生产线,继续沿着 return 往后执行;生产线循环结束后,Main() 也接着结束:

1.2 yield break:标识迭代中断

如下示例代码,通过条件中断循环:

class Program
{
    static void Main()
    {
        ConsoleExt.Write(string.Join(" ", TakeWhilePositive(new[] { 2, 3, 4, 5, -1, 3, 4 })));
        ConsoleExt.Write(string.Join(" ", TakeWhilePositive(new[] { 9, 8, 7 })));
        Console.ReadLine();
    }
    static IEnumerable<int> TakeWhilePositive(IEnumerable<int> numbers)
    {
        foreach (int n in numbers)
        {
            if (n > 0) // 遇到负数就中断循环
            {
                yield return n;
            }
            else
            {
                yield break;
            }
        }
    }
}
public static class ConsoleExt
{
    public static void Write(object message)
    {
        Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
    }
    public static void WriteLine(object message)
    {
        Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
    }
    public static async void WriteLineAsync(object message)
    {
        await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} "));
    }
}

输出结果,第一个数组中第五个数为负数,因此至此就中断循环,包括它自己之后的数字不再返回:

1.3 返回类型为 IAsyncEnumerable<T> 的异步迭代器

 实际上,不仅可以像前边示例中那样返回类型为 IEnumerable<T>,还可以使用 IAsyncEnumerable<T> 作为迭代器的返回类型,使得迭代器支持异步。

 如下示例代码,使用 await foreach 语句对迭代器的结果进行异步迭代:(关于 await foreach 还有另外一个示例可参考 3.2 await foreach() 示例

class Program
{
    public static async Task Main()
    {
        await foreach (int n in GenerateNumbersAsync(5))
        {
            ConsoleExt.Write(n);
        }
        Console.ReadLine();
    }
    static async IAsyncEnumerable<int> GenerateNumbersAsync(int count)
    {
        for (int i = 0; i < count; i++)
        {
            yield return await ProduceNumberAsync(i);
        }
    }
    static async Task<int> ProduceNumberAsync(int seed)
    {
        await Task.Delay(1000);
        return 2 * seed;
    }
}
public static class ConsoleExt
{
    public static void Write(object message)
    {
        Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
    }
    public static void WriteLine(object message)
    {
        Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
    }
    public static async void WriteLineAsync(object message)
    {
        await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")},  Thread {Thread.CurrentThread.ManagedThreadId}): {message} "));
    }
}

输出结果如下,可见输出的结果有不同线程执行:

1.4 迭代器的返回类型可以是 IEnumerator<T> 或 IEnumerator

以下示例代码,通过实现 IEnumerable<T> 接口、GetEnumerator 方法,返回类型为 IEnumerator<T>,来展现 yield 关键字的一个用法:

class Program
{
    public static void Main()
    {
        var ints = new int[] { 1, 2, 3 };
        var enumerable = new MyEnumerable<int>(ints);
        foreach (var item in enumerable)
        {
            Console.WriteLine(item);
        }
        Console.ReadLine();
    }
}
public class MyEnumerable<T> : IEnumerable<T>
{
    private T[] items;
    public MyEnumerable(T[] ts)
    {
        this.items = ts;
    }
    public void Add(T item)
    {
        int num = this.items.Length;
        this.items[num + 1] = item;
    }
    public IEnumerator<T> GetEnumerator()
    {
        foreach (var item in this.items)
        {
            yield return item;
        }
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

1.5 不能使用 yield 的情况

1.yield return 不能套在 try-catch 中;

2.yield break 不能放在 finally 中;

3.yield 不能用在带有 in、ref 或 out 参数的方法;

4.yield 不能用在 Lambda 表达式和匿名方法;

5.yield 不能用在包含不安全的块(unsafe)的方法。

https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/statements/yield 

二、使用 yield 关键字实现惰性枚举

在 C# 中,可以使用 yield 关键字来实现惰性枚举。惰性枚举是指在使用枚举值时,只有在真正需要时才会生成它们,这可以提高程序的性能,因为在不需要使用枚举值时,它们不会被生成或存储在内存中。

当然对于简单的枚举,实际上还没普通的 List<T> 有优势,因为取枚举值也会对性能有损耗,所以只针对处理大型集合或延迟加载数据才能看到效果。

下面是一个简单示例,展示了如何使用 yield 关键字来实现惰性枚举:

public static IEnumerable<int> enumerableFuc()
{
    yield return 1;
    yield return 2;
    yield return 3;
}
// 使用惰性枚举
foreach (var number in enumerableFuc())
{
    Console.WriteLine(number);
}

在上面的示例中,GetNumbers() 方法通过yield关键字返回一个 IEnumerable 对象。当我们使用 foreach 循环迭代这个对象时,每次循环都会调用 MoveNext() 方法,并执行到下一个 yield 语句处,返回一个元素。这样就实现了按需生成枚举的元素,而不需要一次性生成所有元素。

三、通过 IL 代码看 yield 的原理

类比上一章节的示例代码,用 while 循环代替 foreach 循环,发现我们虽然没有实现 GetEnumerator(),也没有实现对应的 IEnumerator 的 MoveNext() 和 Current 属性,但是我们仍然能正常使用这些函数。

static async Task Main(string[] args)
{
    // 用 while (enumerator.MoveNext()) 
    // 代替 foreach(int item in enumerableFuc())
    IEnumerator<int> enumerator = enumerableFuc().GetEnumerator();
    while (enumerator.MoveNext())
    {
        int current = enumerator.Current;
        Console.WriteLine(current);
    }
    Console.ReadLine();
}
// 一个返回类型为 IEnumerable<int>,其中包含三个 yield return
public static IEnumerable<int> enumerableFuc()
{
    Console.WriteLine("enumerableFuc-yield 1");
    yield return 1;
    Console.WriteLine("enumerableFuc-yield 2");
    yield return 2;
    Console.WriteLine("enumerableFuc-yield 3");
    yield return 3;
}

输出的结果:

下面试着简单看一下 Program 类的源码

源码如下,除了明显的 Main() 和 enumerableFuc() 两个函数外,反编译的时候自动生成了一个新的类 '<enumerableFuc>d__1'。

注:反编译时,语言选择:“IL with C#”,有助于理解。

然后看自动生成的类的实现,发现它继承了 IEnumerable、IEnumerable<T>、IEnumerator、IEnumerator<T>,也实现了MoveNext()、Reset()、GetEnumerator()、Current 属性,这时我们应该可以确认,这个新的类,就是我们虽然没有实现对应的 IEnumerator 的 MoveNext() 和 Current 属性,但是我们仍然能正常使用这些函数的原因了。

然后再具体看下 MoveNext() 函数,根据输出的备注字段,也能清晰的看到迭代过程,下图中紫色部分:

下边是是第三、四次迭代,可以看到行标识可以对得上:

每次调用 MoveNext() 函数都会将“ <>1__state”加 1,一共进行了 4 次迭代,前三次返回 true,最后一次返回 false,代表迭代结束。这四次迭代对应被 3 个 yield return 语句分成4部分的 enumberableFuc() 中的语句。

用 enumberableFuc() 来进行迭代的真实流程就是:

  • 运行 enumberableFuc() 函数,获取代码自动生成的类的实例;
  • 接着调用 GetEnumberator() 函数,将获取的类自己作为迭代器,准备开始迭代;
  • 每次运行 MoveNext() “ <>1__state”增加 1,通过 switch 语句可以让每次调用 MoveNext() 的时候执行不同部分的代码;
  • MoveNext() 返回 false,结束迭代。

这也就说明了,yield 关键字其实是一种语法糖,最终还是通过实现 IEnumberable<T>、IEnumberable、IEnumberator<T>、IEnumberator 接口实现的迭代功能

到此这篇关于详解C#中yield关键字的用法的文章就介绍到这了,更多相关C# yield内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C#实现Workstation相关功能过程

    C#实现Workstation相关功能过程

    本文提供了实现Workstation功能的C#代码示例,涵盖工作站管理、远程控制、性能监控和定时任务等核心模块,代码结构清晰,可根据需要扩展,但使用时需注意网络权限和异常处理
    2026-05-05
  • C#根据反射和特性实现ORM映射实例分析

    C#根据反射和特性实现ORM映射实例分析

    这篇文章主要介绍了C#根据反射和特性实现ORM映射的方法,实例分析了反射的原理、特性与ORM的实现技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-04-04
  • 使用C#实现插入各种表格到Word文档

    使用C#实现插入各种表格到Word文档

    在许多企业应用场景中,Word 文档依旧是最常用的信息呈现与内容输出格式,下面将介绍在 C# 中如何以编程方式创建 Word 文档、插入表格、设置样式,并扩展到动态行列与嵌套表格等高级操作,希望对大家有所帮助
    2025-11-11
  • C#中如何在Excel工作表创建混合型图表实例

    C#中如何在Excel工作表创建混合型图表实例

    本篇文章主要介绍了C#中如何在Excel工作表创建混合型图表实例,具有一定的参考价值,有需要的可以了解一下。
    2016-11-11
  • C#中实现向数组中动态添加元素

    C#中实现向数组中动态添加元素

    这篇文章主要介绍了C#中实现向数组中动态添加元素方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-06-06
  • c# unmanaged 约束的具体使用

    c# unmanaged 约束的具体使用

    本文主要介绍了c# unmanaged 约束的具体使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2026-03-03
  • C# TimeSpan计算时间差的操作完整指南

    C# TimeSpan计算时间差的操作完整指南

    TimeSpan 是 .NET 中用于表示时间间隔或持续时间的重要结构体,它提供了丰富的方法和属性来处理时间跨度,下面我们就来看看C# TimeSpan计算时间差的完整操作吧
    2025-12-12
  • c# checked和unchecked关键字的使用

    c# checked和unchecked关键字的使用

    C#中的checked关键字用于启用整数运算的溢出检查,可以捕获并抛出System.OverflowException异常,而unchecked关键字则禁用这种检查,允许结果溢出,下面就来具体介绍一下
    2025-01-01
  • Unity制作游戏自定义按键详解

    Unity制作游戏自定义按键详解

    这篇文章主要介绍了在Unity中如何实现游戏自定义按键,文中的示例代码讲解详细,对我们学习Unity有一定帮助,感兴趣的可以跟随小编学习一下
    2022-01-01
  • C#中类的异常处理详解

    C#中类的异常处理详解

    大家好,本篇文章主要讲的是C#中类的异常处理详解,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2022-02-02

最新评论