【问题标题】:IEnumerable Where() and ToList() - What do they really do?IEnumerable Where() 和 ToList() - 它们的真正作用是什么?
【发布时间】:2014-04-15 17:23:46
【问题描述】:

我想知道 Where()ToList() 方法到底在做什么。具体来说,我想知道Where() 是否会在内存中创建一个新对象或返回一个新对象。

好的,看下面的代码,说我有一个骨架日志类。

public class Log()
{
    public string Log {get;set;}
    public string CreatedByUserId {get;set;}
    public string ModifiedUserId {get;set;}
}

在我的业务逻辑中,假设我只希望某个用户创建或修改日志。这将通过一个方法来完成:FilterLogsAccordingToUserId()

public IEnumerable<Log> FilterLogsAccordingToUserId(IEnumerable<Log> logs, string userId)
{
    int user = int.Parse(userId);
    return logs.Where(x => x.CreatedByUserId.Equals(user) ||
                           x.ModifiedByUserId.Equals(user)).ToList();
}

在这种情况下,Where() 是通过删除所有不符合条件的对象来修改IEnumerable&lt;Log&gt;,还是抓取所有对象,将该对象强制转换为内存中的列表,然后返回该新对象?

如果是第二种可能性,如果将足够大的日志列表传递给函数,我担心性能是否正确?

【问题讨论】:

  • "会在内存中创建一个新对象还是返回一个新对象",有什么区别?
  • 您是否阅读过相关方法的文档?见Enumerable.WhereEnumerable.ToList
  • 您应该使用 Object.ReferenceEquals 对您的两个列表中的项目进行测试。
  • @LasseV.Karlsen 我可能通过在此处添加“或”误解了提问者的意图。我的错。

标签: c# .net linq


【解决方案1】:

让我们分别采取这两种方法。

在哪里

这将返回一个新对象,枚举时将通过谓词过滤原始集合对象。

它绝不会改变原始集合,但它会链接到它

它也是一个延迟执行的集合,这意味着在你真正枚举它之前,每次你枚举它,它都会使用原始集合并对其进行过滤。

这意味着如果你改变了原来的集合,它的过滤结果也会随之改变。

这是一个简单的LINQPad 程序,演示:

void Main()
{
    var original = new List<int>(new[] { 1, 2, 3, 4 });
    var filtered = original.Where(i => i > 2);
    original.Add(5);
    filtered.Dump();
    original.Add(6);
    filtered.Dump();
}

输出:

如您所见,在原始集合中添加更多满足第二个集合的过滤条件的元素会使这些元素也出现在过滤后的集合中。

到列表

这将创建一个新的列表对象,用集合填充它,然后返回该集合。

这是一种即时方法,这意味着一旦您拥有该列表,它现在就是一个与原始集合完全分开的列表。

请注意,该列表中的对象 in 可能仍与原始集合共享,ToList 方法不会创建所有这些的新副本,但 集合 是一个新的。

这是一个简单的LINQPad 程序,演示:

void Main()
{
    var original = new List<int>(new[] { 1, 2, 3, 4 });
    var filtered = original.Where(i => i > 2).ToList();
    original.Add(5);

    original.Dump();
    filtered.Dump();
}

输出:

在这里您可以看到,一旦我们创建了该列表,如果原始集合发生变化,它也不会改变。

您可以将Where 方法视为链接到原始集合,而ToList 将简单地返回一个包含元素的新列表,而不是链接到原始集合。

现在,让我们看看您的最后一个问题。你应该担心性能吗?嗯,这是一个相当大的话题,但是是的,你应该担心性能,但不要担心到你一直这样做的程度。

如果你给Where调用一个集合,每次你枚举Where调用的结果,你将枚举原始的大集合并过滤它。如果过滤器只允许其中少数元素通过它,那么每次您枚举它时,它仍然会枚举原始的大型集合。

另一方面,对大型项目执行ToList 也会创建一个大型列表。

这会是性能问题吗?

谁能说得清,但对于所有事情的表现,这是我的第一答案:

  1. 先知道自己有问题
  2. 第二次使用适当的(内存、cpu 时间等)工具测量您的代码,找出哪里性能问题
  3. 修复它
  4. 回到1号

您经常会看到程序员为一段代码而烦恼,认为它会导致性能问题,结果却被缓慢的用户看着屏幕想知道下一步该做什么,或者数据的下载时间相形见绌,或者将数据写入磁盘所需的时间,或者其他什么。

首先你知道,然后你修复。

【讨论】:

  • 很好解释。谢谢。
【解决方案2】:

Where() 返回一个 IEnumerable。它是原始序列的过滤版本(投影),原始序列保持不变。 ToList() 使用投影返回一个新列表。

同样重要的是要注意调用.Where() 不会评估投影,这是在枚举可枚举时完成的。例如,在 foreach 循环中使用时,或者在这种情况下,在调用 ToList() 时。

【讨论】:

  • @AlexeiLevenkov:是的,我应该补充一下。
  • +1。 Where 的惰性(或延迟)评估是一个很好的注意事项,因为 OP 担心如果有性能成本会在哪里支付。
【解决方案3】:

Where 过滤 IEnumerable&lt;T&gt; 以仅保留那些满足谓词的元素,保持顺序。这强制枚举IEnumerable&lt;T&gt;源,因此它本质上是声明性的。

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    foreach (var item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

ToListIEnumerable&lt;T&gt; 转换为List&lt;T&gt;,保持顺序。这会强制枚举整个 IEnumerable&lt;T&gt; 源。

public static List<TSource> ToList<TSource>(IEnumerable<TSource> source)
{
    var list = new List<TSource>();
    foreach (var item in source)
    {
        list.Add(item);
    }
    return list;
}

在这种情况下,.WHERE 是通过删除所有不符合条件的对象来修改 IEnumerable 日志,还是从 ienumerable 日志中获取所有对象,将该对象转换为内存中的列表,然后返回新对象?

您对logs.Where(...).ToList() 形式的查询将通过Where 部分流式传输您的日志项,然后仅将满足谓词的那些放入最终的List&lt;Log&gt;

【讨论】:

    【解决方案4】:

    Where 将创建一个迭代器,该迭代器将枚举您的集合并仅返回与您的谓词匹配的项目。这里的关键是在您实际尝试访问它之前不会执行此迭代(例如,在foreach)循环中。

    ToList 但是会将对可枚举集合中每个项目的引用复制到一个新列表(不是您复制 reference 而不是对象本身)。如果您在 Where 的末尾添加 ToList,那么您将导致 Where 必须遍历集合。

    简而言之,如果您使用Where,您将不会创建任何新对象(迭代器本身除外),也不会更改原始集合中的任何内容。如果您使用ToList,那么您会将与您的Where 子句匹配的对象的references 复制到一个新的List(当然,原始列表保持不变-除非您分配它回到同一个变量)。

    因此,如果您实际上不需要创建新列表,请不要使用ToList。如果您需要做的只是遍历您的集合,请跳过ToList 部分。但是,如果您这样做,这里有一个微妙的点:

    var filtered = logs.Where(x => x.CreatedByUserId.Equals(user) ||
                           x.ModifiedByUserId.Equals(user));
    

    然后更改您的收藏,然后执行以下操作:

    foreach (var f in filtered) 
    {
       //....
    }
    

    您将遍历您的原始集合 (logs),它是现在,而不是您声明 filtered 时的样子。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-07
      • 1970-01-01
      • 2021-12-26
      • 2016-10-24
      相关资源
      最近更新 更多