C# IEnumerable<;T>;是否存储稍后调用的函数?

C# IEnumerable<;T>;是否存储稍后调用的函数?,c#,linq,select,ienumerable,func,C#,Linq,Select,Ienumerable,Func,我最近遇到了一些代码,它们的行为与我预期的不一样 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); 我觉得我现在了解了代

我最近遇到了一些代码,它们的行为与我预期的不一样

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);

我觉得我现在了解了代码的行为(我在网上发现了其他问题,这些问题都是这种行为的结果)。但是,究竟是什么原因导致从后面的行调用
Select()
中的代码?

您有一个对LINQ查询的引用,该查询的计算次数与您对它们的迭代次数相同

(您可以看到这被称为延迟执行):

如前所述,查询变量本身仅存储查询命令。查询的实际执行被延迟,直到您在foreach语句中迭代查询变量。这个概念被称为延迟执行

因为查询变量本身从不保存查询结果,所以您可以随时执行它。例如,您可能有一个数据库正在由单独的应用程序不断更新。在应用程序中,可以创建一个检索最新数据的查询,并且可以每隔一定时间重复执行该查询,每次检索不同的结果

所以,当你

IEnumerable<int> result = numbers.Select(DoSomething);
迭代
结果
直到第一个元素。对于
ElementAt(4)
,也会发生同样的情况,但这次它会迭代到第五个元素。请注意,您只看到打印的
正在执行某些操作。。。5
因为当前的只计算一次。 如果此时查询无法生成5项,则调用将失败。
调用
.Count
再次迭代
结果
查询,并返回此时的元素数量

如果您没有保留对查询的引用,而是保留了对结果的引用,即:

IEnumerable<int> result = numbers.Select(DoSomething).ToArray();
// or
IEnumerable<int> result = numbers.Select(DoSomething).ToList();

让我们一块一块地把它分解,直到你理解为止。相信我;花点时间阅读这篇文章,它将对您理解可枚举类型和回答您的问题有所启示。

查看
IEnumerable
界面,它是
IEnumerable
的基础。它包含一种方法<代码>IEnumerator GetEnumerator()

枚举数是一个棘手的问题,因为它们可以做任何想做的事情。真正重要的是在
foreach
循环中自动调用
GetEnumerator()
;或者你可以手工做

GetEnumerator()
做什么?它返回另一个接口,
IEnumerator

这就是魔法。
IEnumerator
有1个属性和2个方法

object Current { get; }
bool MoveNext();
void Reset();
让我们来打破魔法

首先让我解释一下它们的典型特征,我之所以说典型特征,是因为正如我所提到的,它可能是一种狡猾的野兽。无论您选择什么方式,您都可以实现此功能。。。有些类型不符合标准

对象当前{get;}
很明显。它获取
IEnumerator
中的当前对象;默认情况下,这可能为空

bool MoveNext()
如果
IEnumerator
中有另一个对象,则返回
true
,并应将
当前值设置为该新对象

void Reset()告诉类型从头开始

现在让我们实现这个。请花时间查看此
IEnumerator
类型,以便您理解它。要意识到,当您引用一个
IEnumerable
类型时,您甚至没有引用
IEnumerator
(这一点);但是,您正在引用一个类型,该类型通过
GetEnumerator()返回此
IEnumerator

注意:小心不要混淆名称
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)
的电话时,你没有迭代任何项目。。。您返回的类型与上面的类型非常相似<代码>选择(…)
返回
IEnumerable
。好吧,看看上面
IEnumerable
只是一个调用
GetEnumerator()
的接口。每当您进入循环情况或可以手动完成时,就会发生这种情况。因此,考虑到这一点,您已经可以看到迭代在调用
GetEnumerator()
之前从未开始,甚至在调用
GetEnumerator()
的结果的
MoveNext()
方法之前也从未开始,该方法是
IEnumerator
类型

所以

换句话说,您只需要在调用中引用一个
IEnumerable
,就可以了。没有发生迭代。这就是代码在您的代码中跳回的原因,因为它最终在
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>();
    }
}
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
}

现在,让我们做同样的事情,但使其通用。我不会写那么多,但密切监视代码的变化,你会得到它

我将把它全部复制粘贴到一个文件夹中

整个控制台应用程序

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>();
    }
}
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或LINQtoSQL包含与典型Linq不同的扩展方法。主要区别在于Linq中的查询表达式(在引用数据库时)将返回
IQueryable
,它实现了
IQueryable
接口,该接口创建运行和迭代的SQL表达式。换句话说。。。类似于
.W的东西