今天,我想向您展示两种从 C# 方法中操作和返回列表的方法。
首先,我将通过创建一个空列表并用所需信息填充然,后返回它。演示执行此操作的“常规”方法。
第二种方法是使用 yield return,这是一种使用枚举的简洁方法,通过一次返回一个项目来获得更简洁的方法。
常规方法
返回值列表的常规方法是先初始化一个空列表,然后遍历所需的值并将它们添加到列表中。
一旦你完成了必要的操作并将所有你想要的值添加到列表后,你就可以返回它。
使用此方法,您将始终先构建整个列表,然后将其返回给调用它的人。
假设您有一个方法,获得一个数字列表并返回这些数字的双倍值。 这就是您按照常规方式执行此操作的方式:
public List<int> GetDoubledNumbers(List<int> list)
{
var result = new List<int>();
foreach(var i in list)
{
result.Add(i * 2);
}
return result;
}
现在,打印这些列表。 这只是一个普通函数,它遍历数字列表并打印出来。
void PrintNumbers(IEnumerable<int> list)
{
Console.WriteLine();
foreach(var i in list)
{
Console.Write(i + ", ");
}
}
现在,如果你想停止操作列表,你只需根据条件在循环中添加一个 break 语句。 就像下面的例子:
public List<int> GetDoubledNumbersIfMoreThanThreshhold(List<int> list, int threshhold)
{
var result = new List<int>();
foreach(var i in list)
{
if (i >= threshhold)
{
break;
}
result.Add(i * 2);
}
return result;
}
Yield 上下文关键字
返回列表的另一种方法是通过关键字 yield return。 在这里,它不会返回整个列表,而是始终返回一个枚举(值),并且只在迭代遍历列表成员后才执行逻辑。 所以,基本上,它不是返回完整的项目列表,而是一次返回一个项目。
这种方法的优点是,您只在迭代中轮到它时才处理迭代中的逻辑(例如,在 foreach 中)。
public IEnumerable<int> GetDoubledNumbers(List<int> list)
{
foreach (var i in list)
{
Console.Write("*"); // 用于证明此方法仅在调用此方法后的迭代(循环)中轮到它执行的代码
yield return i * 2;
}
}
现在像这样运行它:
var numbers = new List<int>
{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
};
var yieldList = GetDoubledNumbers(numbers);
PrintNumbers(yieldList);
// 输出将是
// *2, *4, *6, *8, *10, *12, *14, *16, *18, *20,
//而不是
// 2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
IEnumerable<int> GetDoubledNumbers(List<int> list)
{
foreach (var i in list)
{
Console.Write("*"); // 用于证明此方法仅在调用此方法后的迭代(循环)中轮到它执行的代码
yield return i * 2;
}
}
void PrintNumbers(IEnumerable<int> list)
{
Console.WriteLine();
foreach(var i in list)
{
Console.Write(i + ", ");
}
}
请注意,PrintNumbers 的结果不是
2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
而是
*2, *4, *6, *8, *10, *12, *14, *16, *18, *20,
* 在 GetDoubledNumbers 迭代运行时插入,而不是在您调用它时插入。 这是因为当我们调用 GetDoubleNumbers 时,代码并没有立即执行。 一旦 foreach 循环开始一次返回一个项目,它只会在每次迭代中执行。
因此,只有在您遍历它时才会抛出异常。 所以调试可能会有点混乱。
要记住的另一件事是,您不能将 yield return 语句包装在 try-catch 块中。 如果你想有异常处理,你必须在调用 yield return 之前,将你想要的逻辑放在 try-catch 块中。 像下面的代码:
var numbers = new List<int>
{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
};
var yieldList = GetDoubledNumbers(numbers);
PrintNumbers(yieldList);
IEnumerable<int> GetDoubledNumbers(List<int> list)
{
foreach (var i in list)
{
try
{
Console.Write("*");
}
catch (Exception e)
{
// 在这里做异常处理
throw;
}
yield return i * 2;
}
}
void PrintNumbers(IEnumerable<int> list)
{
Console.WriteLine();
foreach(var i in list)
{
Console.Write(i + ", ");
}
}
Yield break
现在,如果您有一个循环但想根据您定义的条件停止它,您通常会在正常循环中使用 break 关键字。
当然,您可以在使用 yield return 时使用它,但这会打破循环,运行它下面的代码。
var numbers = new List<int>
{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
};
var result = GetDoubledNumbersIfMoreThanThreshhold(numbers, 5);
PrintNumbers(result);
// 输出将会是
// 2, 4, 6, 8, *Break* *End Method*
IEnumerable<int> GetDoubledNumbersIfMoreThanThreshhold(List<int> list, int threshhold)
{
foreach (var i in list)
{
if (i >= threshhold)
{
Console.Write("*Break*");
break;
}
yield return i * 2;
}
Console.Write(" *End Method*");
}
void PrintNumbers(IEnumerable<int> list)
{
Console.WriteLine();
foreach(var i in list)
{
Console.Write(i + ", ");
}
}
请注意,*End Method* 也被输出了。 这是因为,仅使用 break 关键字,您就可以停止 foreach 循环,而不是立即停止整个枚举。
如果你只想停止枚举的执行,你必须使用 yield break 你可以从你的方法内部中断整个迭代。
var numbers = new List<int>
{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
};
var result = GetDoubledNumbersIfMoreThanThreshhold(numbers, 5);
PrintNumbers(result);
// 输出将会是
// 2, 4, 6, 8, *Break*
IEnumerable<int> GetDoubledNumbersIfMoreThanThreshhold(List<int> list, int threshhold)
{
foreach (var i in list)
{
if (i >= threshhold)
{
Console.Write("*Break*");
yield break;
}
yield return i * 2;
}
Console.Write(" *End Method*");
}
void PrintNumbers(IEnumerable<int> list)
{
Console.WriteLine();
foreach(var i in list)
{
Console.Write(i + ", ");
}
}
Async
最后,如果您想在枚举方法中处理异步代码,则不能只返回 Task<IEnumerable<T>>。 如果您尝试这样做,您将收到以下警告:
The body of 'GetDoubledNumbersIfMoreThanThreshhold(List<int>, int)' cannot be an iterator block because 'Task<IEnumerable<int>>' is not an iterator interface type
那是因为现在您正在创建和使用异步枚举流,这不适用于返回task。
为了能够正常工作,您应该返回一个 IAsyncEnumerable<T>,它是异步流的返回类型。 这基本上意味着每次迭代都会在请求时被调用。
为了遍历,不会使用标准的 foreach 循环,而是在它之前添加一个 await,使循环被称为 await foreach。
因此,要让异步代码正常运行,您可以像下面的代码那样做:
var numbers = new List<int>
{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
};
var result = GetDoubledNumbersAsync(numbers);
await PrintNumbersAsync(result);
async IAsyncEnumerable<int> GetDoubledNumbersAsync(List<int> list)
{
foreach (var i in list)
{
await Task.Delay(1000);
yield return i * 2;
}
}
async Task PrintNumbersAsync(IAsyncEnumerable<int> list)
{
Console.WriteLine();
await foreach(var i in list)
{
Console.Write(i + ", ");
}
}
这段代码的运行非常有趣:
一旦迭代时间到了,您可以看到数字是如何打印的。
总结
我们已经了解了通过 C# 方法构建和返回项目列表的两种方法之间的区别。
我们探索了 yield return 的强大功能以及它如何保持代码整洁。 但是您也看到了应该如何谨慎使用它,因为您的方法调用它不会返回整个项目列表,而是一旦开始迭代,结果枚举就会一次返回一个项目。
最后,如果您想通过返回 IAsyncEnumerable 并使用 await foreach 循环来使用 yield return,您可以学习如何使用异步代码。