收益率回报解释!
Yield 关键字一直是一个被严重误解的关键字C#
,因此我将尝试用一个简单的例子来解释它,希望您能够理解它,因为它将帮助您获得更好的应用程序性能。
➽创建控制台应用程序
☞首先假设我们有一个简单的.Net Core 3.1
控制台应用程序来处理学生名单。
Student Class
将具有以下结构:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
看起来很简单吧?别担心,它会一直这样下去的😉
main
☞下一步,在控制台应用程序的方法中创建学生列表:
static void Main(string[] args)
{
CreateStudents();
}
☞因此,我们有一个简单的函数,CreateStudents
它接受 1 个参数(要创建的学生人数),该函数将创建学生,如果student ID
少于1000,则将其输入到屏幕上:
public static void CreateStudents()
{
var students = GetStudents(1000000);
foreach (var student in students)
{
if (student.Id < 1000)
Console.WriteLine($"Id: {student.Id}, Name: {student.Name}");
else
break;
}
}
static IEnumerable<Student> GetStudents(int count)
{
var students = new List<Student>();
for (int i = 0; i < count; i++)
{
students.Add(new Student() { Id = i, Name = $"Student Number {i}" });
}
return students;
}
☞GetStudents
函数只是创建一个新的学生列表,然后用新学生填充它并返回它。
☞让我们运行该应用程序并查看其行为,并再次记住我们只想输入 ID 小于1000 的学生...
☞通过在调用函数后设置断点🔴 GetStudents
,注意到创建了一百万名学生,然后foreach
进入循环仅打印前 1000 名,那么我们真的需要创建一百万名学生来仅打印 1000 名😖 吗?
当然不是..
➥我们想要的是避免创建所有百万条记录并将它们添加到列表中并返回包含百万条记录的完整列表,相反,如果我们只返回我们想要的内容,那就太好了。
➽申请收益率
因此让我们对CreateStudents
功能进行一些修改...
static IEnumerable<Student> GetStudents(int count)
{
for (int i = 0; i < count; i++)
{
yield return new Student() { Id = i, Name = $"Student Number {i}" };
}
}
☞现在让我们使用相同的断点设置运行应用程序,看看会发生什么。
☞我们现在进入foreach
创建零名学生的循环!!
☞让我们在函数yield return
内部的行中设置另一个断点GetStudents
来查看到底发生了什么,然后在 Visual Studio 中点击Step Over (F10)来继续调试过程...
☞调试器将遍历循环中的(学生)foreach
,然后遍历(in)关键字,然后跳转到函数return yield
中的行GetStudents
,而不会进入函数foreach
中CreateStudents
……
☞之后我们进入foreach
循环,然后输入 if 语句,第一个学生的数据打印在控制台中...
**in**
☞继续使用 F10 跳过...您将在中输入关键字,foreach
然后再次输入return yield
...
➽那么我们在这里到底做什么呢?
☞实际上,我们正在对学生集合进行懒惰的迭代,我们一次只请求一名学生。
☞两种方式我们都得到了相同的结果,即在屏幕上打印 1000 名学生
☞它们之间的区别在于性能和内存使用率yield return
有了巨大的提高,我们稍后会看到。
☞实际上,我们基本上是在创建一个迭代器来遍历我们的集合。
我们的foreach
循环只是像这样的语句的语法糖
var studentesEnumerator = students.GetEnumerator();
while (studentesEnumerator.MoveNext())
{
var student = studentesEnumerator.Current;
}
这就是它大致的汇编内容。
➥因此,由于不必遍历整个集合,我们实际上不必在列表中创建一百万个学生,我们实际上循环并只创建前 1000 个学生,
因为yield
当我们进入这个循环时,这个循环会一次创建一个学生。
➽使用 BenchmarkDotNet 测试差异:
● 现在,让我们测试并诊断应用程序性能,看看是否真的有所不同。
● 本次测试我们将使用BenchmarkDotNet Nuget包来进行,您可以从这里下载BenchmarkDotNet Nuget
● 我们只需对代码进行简单的编辑,即可应用基准统计数据...
● 我们将创建一个类,我们称之为,这个类将包含我们函数的旧版本和使用 yield return 的新版本,该类将用[MemoryDiagnoser]YieldBenchmarks
属性修饰,旧版本和新版本的函数将用[Benchmark]属性修饰,代码如下CreateStudent
[MemoryDiagnoser]
public class YieldBenchmarks
{
[Benchmark]
public void CreateStudents()
{
var students = GetStudents(1000000);
foreach (var student in students)
{
if (student.Id < 1000)
Console.WriteLine($"Id: {student.Id}, Name: {student.Name}");
else
break;
}
}
static IEnumerable<Student> GetStudents(int count)
{
var students = new List<Student>();
for (int i = 0; i < count; i++)
{
students.Add(new Student() { Id = i, Name = $"Student Number {i}" });
}
return students;
}
[Benchmark]
public void CreateStudentsYield()
{
var students = GetStudentsYield(1000000);
foreach (var student in students)
{
if (student.Id < 1000)
Console.WriteLine($"Id: {student.Id}, Name: {student.Name}");
else
break;
}
}
static IEnumerable<Student> GetStudentsYield(int count)
{
for (int i = 0; i < count; i++)
{
yield return new Student() { Id = i, Name = $"Student Number {i}" };
}
}
}
该main
功能如下:
static void Main(string[] args)
{
var result = BenchmarkRunner.Run<YieldBenchmarks>();
}
● 现在让我们在发布模式下运行应用程序...
● 请注意,BenchmarkRunner
将多次运行每个功能以便能够执行其诊断,结果将如下所示:
● 现在请注意时间流逝的巨大差异,还请注意内存使用量的巨大差异,它是133.5 MB对220 KB!!
这只是对yield
关键字的简单说明,接下来,我们将使用Async Yield。等待它:)