【问题标题】:IEnumerable performs differently on Array vs ListIEnumerable 在 Array 和 List 上的表现不同
【发布时间】:2018-08-22 17:54:29
【问题描述】:

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

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。但是,基础类型是 WhereSelectArrayIteratorList

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

使用this handy website,我能够(我认为)追踪导致差异的代码位。

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

查看 第 169 行 上的内容将我指向 Iterator,因为它似乎在此处调用了 GetEnumerator()

从第 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() 方法中所写)。

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

更新

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

更新2

这是我认为我的问题的一个例子。感谢大家提供的信息。

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

【问题讨论】:

  • 不同的是transformed是IEnumerable,而transformedList是一个列表,因为你在后者上调用ToList()。虽然您可以修改 List 中的值,但您只能枚举 IEnumerable。
  • 因为您没有“枚举”第一个 IEnumerable,所以当您再次遍历它以将它们打印出来时,它将再次“重新枚举”(再次运行 select 语句)IEnumerable 等等不会有您的更改。
  • 如果要将数组与列表进行比较,您需要在第一个数组的末尾添加 ToArray

标签: c# linq ienumerable ienumerator


【解决方案1】:

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

真正令人大开眼界的是在 where 或 select 中放置调试语句或断点以实际查看延迟的管道执行

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

【讨论】:

  • 也许需要补充的是,每次执行 foreach (var i in transformed) 时,都会创建新的 Example 对象。在第一次迭代中,为这些实例更新了MyString 属性,但在第二次迭代中,创建了新的Examples(并且没有明显应用这些更改)。我认为您已经说过了,但它使它更加明确。
  • 这基本上就是我所追求的。我知道延迟执行,并且通过调用 ToList() 它基本上会强制它执行。我感兴趣的部分是,当它被实际调用时,无论是否延迟,我期望它有一个对“基础”对象的引用。因此,它第一次执行时不会有一个,它会评估 Select。然后我第二次枚举它,它会引用同一个对象。 IE。 List 如何运作。
  • 看多了,我开始看到了。我专注于 IEnumerable 方面。当这可能不是不同功能的来源时。那个克隆方法可能把我送进了错误的兔子洞。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-01-25
  • 1970-01-01
  • 1970-01-01
  • 2018-05-06
  • 2019-08-25
  • 2011-05-31
相关资源
最近更新 更多