IEnumerable在数组与列表上的执行情况不同

时间:2018-08-22 17:54:29

标签: c# linq ienumerable ienumerator

这个问题更多的是“我的理解是否正确”,如果不正确,请帮助我解决这个问题。我有这段代码来解释我的问题:

class Example
{
    public string MyString { get; set; }
}

var wtf = new[] { "string1", "string2"};
IEnumerable<Example> transformed = wtf.Select(s => new Example { MyString = s });
IEnumerable<Example> transformedList = wtf.Select(s => new Example { MyString = s }).ToList();

foreach (var i in transformed)
    i.MyString = "somethingDifferent";

foreach (var i in transformedList)
    i.MyString = "somethingDifferent";

foreach(var i in transformed)
    Console.WriteLine(i.MyString);

foreach (var i in transformedList)
    Console.WriteLine(i.MyString);

它输出:

string1
string2
somethingDifferent
somethingDifferent

这两个 Select()方法乍一看都返回 IEnumerable <示例> 。但是,基础类型为 WhereSelectArrayIterator <字符串,示例> 列表<示例>

这是我的理智开始受到质疑的地方。根据我的理解,上面输出的差异是因为两种基础类型都实现了 GetEnumerator()方法。

使用this handy website,我能够(我认为)找到引起差异的代码。

class WhereSelectArrayIterator<TSource, TResult> : Iterator<TResult>
{ }

查看第169行上的内容,将我指向 Iterator ,因为这就是 GetEnumerator()被调用的地方。 / p>

从第90行开始,我看到:

public IEnumerator<TSource> GetEnumerator() {
    if (threadId == Thread.CurrentThread.ManagedThreadId && state == 0) {
        state = 1;
        return this;
    }
    Iterator<TSource> duplicate = Clone();
    duplicate.state = 1;
    return duplicate;
}

我从中收集的信息是,当您枚举它时,实际上是在枚举一个克隆的源(如WhereSelectArrayIterator类的Clone()方法中所写)。

这将满足我目前的理解需求,但是作为奖励,如果有人可以帮助我弄清楚为什么我第一次枚举时未返回 在数据上。据我所知, 状态 应该在第一遍= 0。除非,也许在幕后发生了从不同线程调用相同方法的魔术。

更新

在这一点上,我认为我的“发现”有点误导(该死的Clone方法使我误入了错误的兔子洞),这确实是由于延迟执行所致。我错误地认为,即使我推迟执行,但第一次枚举它会将这些值存储在变量中。我应该知道的更好;毕竟,我在 Select 中使用了 new 关键字。就是说,它仍然让我感到惊讶,即特定类的 GetEnumerator()实现仍然可以返回一个克隆,这将带来一个非常相似的问题。碰巧我的问题有所不同。

Update2

这是我 想法 的问题的一个示例。感谢大家提供的信息。

IEnumerable<Example> friendly = new FriendlyExamples();
IEnumerable<Example> notFriendly = new MeanExamples();

foreach (var example in friendly)
    example.MyString = "somethingDifferent";
foreach (var example in notFriendly)
    example.MyString = "somethingDifferent";

foreach (var example in friendly)
    Console.WriteLine(example.MyString);
foreach (var example in notFriendly)
    Console.WriteLine(example.MyString);

// somethingDifferent
// somethingDifferent
// string1
// string2

支持类:

class Example
{
    public string MyString { get; set; }
    public Example(Example example)
    {
        MyString = example.MyString;
    }
    public Example(string s)
    {
        MyString = s;
    }
}
class FriendlyExamples : IEnumerable<Example>
{
    Example[] wtf = new[] { new Example("string1"), new Example("string2") };

    public IEnumerator<Example> GetEnumerator()
    {
        return wtf.Cast<Example>().GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return wtf.GetEnumerator();
    }
}
class MeanExamples : IEnumerable<Example>
{
    Example[] wtf = new[] { new Example("string1"), new Example("string2") };

    public IEnumerator<Example> GetEnumerator()
    {
        return wtf.Select(e => new Example(e)).Cast<Example>().GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return wtf.Select(e => new Example(e)).GetEnumerator();
    }
}

1 个答案:

答案 0 :(得分:2)

Linq通过使每个函数返回另一个IEnumerable来工作,该IEnumerable通常是延迟的处理器。直到最后返回的Ienumerable的枚举发生时,才发生实际执行。这样可以创建有效的管道。

这样做的时候

var transformed = wtf.Select(s => new Example { MyString = s });

选择代码尚未实际执行。只有当您最终枚举转换时,才会进行选择。即在这里

foreach (var i in transformed)
    i.MyString = "somethingDifferent";

请注意,如果这样做

foreach (var i in transformed)
    i.MyString = "somethingDifferent";

管道将再次执行。在这里,这没什么大不了的,但是如果涉及到IO,则可能会很大。

此行

 var transformedList = wtf.Select(s => new Example { MyString = s }).ToList();

相同
var transformedList = transformed.ToList();

真正令人大开眼界的是将调试语句或断点放在一个位置或选择实际查看延迟的管道执行情况

阅读linq的实现很有用。这是选择https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,5c652c53e80df013,references