我最近遇到了一些代码,这些代码与我的预期行为不符。
1: int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8 };
2: IEnumerable<int> result = numbers.Select(n => n % 2 == 0 ? n : 0);
3:
4: int a = result.ElementAt(0);
5: numbers[0] = 10;
6: int b = result.ElementAt(0);
当我在Visual Studio中逐步执行此代码时,我惊讶地看到黄色突出显示从第4行跳回到第2行的lambda表达式,然后又从第6行跳至第2行的lambda。
此外,运行此代码后,a
的值为0,而b
的值为10。
使我意识到这可能/将要发生的原始代码涉及Select()
中的方法调用,并且访问IEnumerable的任何属性或特定元素都会导致Select()
中的方法被调用一遍又一遍。
// The following code prints out:
// Doing something... 1
// Doing something... 5
// Doing something... 1
// Doing something... 2
// Doing something... 3
// Doing something... 4
// Doing something... 5
using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
int[] numbers = { 1, 2, 3, 4, 5 };
IEnumerable<int> result = numbers.Select(DoSomething);
int a = result.ElementAt(0);
int b = result.ElementAt(4);
int c = result.Count();
}
static int DoSomething(int x)
{
Console.WriteLine("Doing something... " + x);
return x;
}
}
我觉得我现在了解了代码的行为方式(并且我在网上发现了其他行为所导致的问题)。但是,究竟是什么完全导致Select()
中的代码从后面的行中调用?
答案 0 :(得分:3)
您有一个LINQ查询的引用,该查询在您对其进行迭代时将被评估多次。
From the docs(您可以看到这称为延迟执行):
如前所述,查询变量本身仅存储查询命令。查询的实际执行将延迟,直到您在foreach语句中遍历查询变量为止。这个概念称为延迟执行
...
由于查询变量本身永远不保存查询结果,因此您可以根据需要多次执行。例如,您可能拥有一个由单独的应用程序连续更新的数据库。在您的应用程序中,您可以创建一个查询以检索最新数据,并且可以以一定的间隔重复执行该查询以每次检索不同的结果。
那么,当你拥有
IEnumerable<int> result = numbers.Select(DoSomething);
您具有对查询的引用,该查询会将numbers
中的每个元素转换为DoSomething
的结果。
因此,您可以这样说:
int a = result.ElementAt(0);
迭代result
直到第一个元素。 ElementAt(4)
也会发生同样的情况,但是这次迭代直到第五个元素。请注意,您仅看到打印的Doing something... 5
,因为.Current
被评估一次。
如果查询当时无法产生5个项目,则调用将失败。
.Count
调用再次迭代result
查询并返回此时的元素数量。
如果您保留对结果的引用,而不是保留对查询的引用,即:
IEnumerable<int> result = numbers.Select(DoSomething).ToArray();
// or
IEnumerable<int> result = numbers.Select(DoSomething).ToList();
您只会看到以下输出:
// Doing something... 1
// Doing something... 2
// Doing something... 3
// Doing something... 4
// Doing something... 5
答案 1 :(得分:1)
让我们一步一步地分解,直到您理解为止。相信我;花点时间阅读本文,这对您了解Enumerable
类型并回答您的问题将是一个启示。
看一下IEnumerable
的基础的IEnumerable<T>
接口。它包含一种方法; IEnumerator GetEnumerator();
。
可枚举是一种棘手的野兽,因为它们可以做任何想做的事。真正重要的是在GetEnumerator()
循环中自动发生的对foreach
的调用;或者您可以手动完成。
GetEnumerator()
的作用是什么?它返回另一个接口IEnumerator
。
这是魔术。 IEnumerator
具有1个属性和2个方法。
object Current { get; }
bool MoveNext();
void Reset();
让我们分解魔法。
首先让我解释一下它们的典型含义,我之所以这样说是因为像我提到的那样,它可能是棘手的野兽。您可以选择实现它,但是您可以选择...有些类型不符合标准。
object Current { get; }
很明显。它在IEnumerator
中获取当前对象;默认情况下,它可能为空。
bool MoveNext();
如果true
中还有另一个对象,则返回IEnumerator
,并且应将Current
的值设置为该新对象。
void Reset();
告诉类型从头开始。
现在让我们实现它。请花点时间查看此IEnumerator
类型,以便您理解。意识到当您引用IEnumerable
类型时,您甚至都没有引用IEnumerator
(此);但是,您引用的类型是通过IEnumerator
GetEnumerator()
的类型
注意: 请注意不要混淆名称。 IEnumerator
与IEnumerable
不同。
IEnumerator
public class MyEnumerator : IEnumerator
{
private string First => nameof(First);
private string Second => nameof(Second);
private string Third => nameof(Third);
private int counter = 0;
public object Current { get; private set; }
public bool MoveNext()
{
if (counter > 2) return false;
counter++;
switch (counter)
{
case 1:
Current = First;
break;
case 2:
Current = Second;
break;
case 3:
Current = Third;
break;
}
return true;
}
public void Reset()
{
counter = 0;
}
}
现在,让我们创建一个IEnumerable
类型并使用此IEnumerator
。
IEnumerable
public class MyEnumerable : IEnumerable
{
public IEnumerator GetEnumerator() => new MyEnumerator();
}
这是一种可以吸收的东西...当您拨打numbers.Select(n => n % 2 == 0 ? n : 0)
之类的电话时,您没有迭代任何项...您返回的类型与上述类似。 .Select(…)
返回IEnumerable<int>
。看起来不错……IEnumerable
只是调用GetEnumerator()
的接口。每当您进入循环情况或可以手动完成时,都会发生这种情况。因此,考虑到这一点,您已经可以看到迭代永远不会开始,直到您调用GetEnumerator()
为止,甚至直到您调用MoveNext()
结果的GetEnumerator()
方法(即IEnumerator
类型。
所以...
换句话说,您在呼叫中仅引用了IEnumerable<T>
,仅此而已。没有迭代发生。这就是为什么代码跳回到您的代码中的原因,因为它最终在ElementAt
方法中进行了迭代,然后查看了lamba表达式。和我呆在一起,稍后我将更新一个示例,使本课整整圈开,但现在让我们继续我们的简单示例:
现在让我们制作一个简单的控制台应用程序来测试我们的新类型。
控制台应用程序
class Program
{
static void Main(string[] args)
{
var myEnumerable = new MyEnumerable();
foreach (var item in myEnumerable)
Console.WriteLine(item);
Console.ReadKey();
}
// OUTPUT
// First
// Second
// Third
}
现在让我们做同样的事情,但是要使其通用。我不会写太多,但会密切监视代码中的更改,您会明白的。
我将全部复制并粘贴。
整个控制台应用程序
using System;
using System.Collections;
using System.Collections.Generic;
namespace Question_Answer_Console_App
{
class Program
{
static void Main(string[] args)
{
var myEnumerable = new MyEnumerable<Person>();
foreach (var person in myEnumerable)
Console.WriteLine(person.Name);
Console.ReadKey();
}
// OUTPUT
// Test 0
// Test 1
// Test 2
}
public class Person
{
static int personCounter = 0;
public string Name { get; } = "Test " + personCounter++;
}
public class MyEnumerator<T> : IEnumerator<T>
{
private T First { get; set; }
private T Second { get; set; }
private T Third { get; set; }
private int counter = 0;
object IEnumerator.Current => (IEnumerator<T>)Current;
public T Current { get; private set; }
public bool MoveNext()
{
if (counter > 2) return false;
counter++;
switch (counter)
{
case 1:
First = Activator.CreateInstance<T>();
Current = First;
break;
case 2:
Second = Activator.CreateInstance<T>();
Current = Second;
break;
case 3:
Third = Activator.CreateInstance<T>();
Current = Third;
break;
}
return true;
}
public void Reset()
{
counter = 0;
First = default;
Second = default;
Third = default;
}
public void Dispose() => Reset();
}
public class MyEnumerable<T> : IEnumerable<T>
{
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<T> GetEnumerator() => new MyEnumerator<T>();
}
}
因此,让我们来回顾一下... IEnumerable<T>
是一种类型,该方法具有返回IEnumerator<T>
类型的方法。 IEnumerator<T>
类型具有T Current { get; }
属性以及IEnumerator
方法。
让我们在代码中再分解一次,然后手动调用各个部分,以便您可以更清晰地看到它。这将只是应用程序的控制台部分,因为其他所有内容都保持不变。
控制台应用程序
class Program
{
static void Main(string[] args)
{
IEnumerable<Person> enumerable = new MyEnumerable<Person>();
IEnumerator<Person> enumerator = enumerable.GetEnumerator();
while (enumerator.MoveNext())
Console.WriteLine(enumerator.Current.Name);
Console.ReadKey();
}
// OUTPUT
// Test 0
// Test 1
// Test 2
}
仅供参考: 要指出的一件事是上面的答案中有Linq的两个版本。 EF或Linq-to-SQL中的Linq包含与典型linq不同的扩展方法。主要区别在于,Linq中的查询表达式(当引用数据库时)将返回IQueryable<T>
,该{* 1}实现了IQueryable
接口,该接口创建了要运行并对其进行迭代的SQL表达式。换句话说,类似.Where(…)
子句的查询不会查询整个数据库,然后对其进行遍历。它将表达式转换为SQL表达式。这就是为什么.Equals()
之类的东西在那些特定的Lambda表达式中不起作用的原因。
答案 2 :(得分:0)
IEnumerable<T>
是否存储一个以后要调用的函数?
是的。 IEnumerable就是它所说的。这是将来可以列举的 。您可以将其视为建立操作流程。
直到真正被枚举(即调用foreach
,.ElementAt()
,ToList()
等)时,任何这些操作才被实际调用。这称为deferred execution。
究竟是什么导致后来的代码调用Select()中的代码?
调用SomeEnumerable.Select(SomeOperation)
时,结果是IEnumerable,它是表示您已设置的“管道”的对象。 IEnumerable的实现确实存储您传递给它的函数。 (针对.net核心)的实际来源是here。您可以看到SelectEnumerableIterator
,SelectListIterator
和SelectArrayIterator
都有一个Func<TSource, TResult>
作为私有字段。在此存储您指定供以后使用的功能。如果您知道要遍历有限集合,则数组和列表迭代器仅提供一些快捷方式。