IEnumerable <t>是否存储以后要调用的函数?

时间:2018-10-15 21:38:30

标签: 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);

当我在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()中的代码从后面的行中调用?

3 个答案:

答案 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()的类型

注意: 请注意不要混淆名称。 IEnumeratorIEnumerable不同。

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。您可以看到SelectEnumerableIteratorSelectListIteratorSelectArrayIterator都有一个Func<TSource, TResult>作为私有字段。在此存储您指定供以后使用的功能。如果您知道要遍历有限集合,则数组和列表迭代器仅提供一些快捷方式。