【问题标题】:Converting foreach to Linq将 foreach 转换为 Linq
【发布时间】:2015-07-02 18:01:33
【问题描述】:

当前代码:

对于MapEntryTable 中的每个元素,检查属性IsDisplayedColumnIsReturnColumn,如果它们为真则将该元素添加到另一组列表中,其运行时间为O(n),会有很多两个属性都为 false 的元素,因此不会被添加到循环中的任何列表中。

 foreach (var mapEntry in MapEntryTable)
 {
   if (mapEntry.IsDisplayedColumn)
      Type1.DisplayColumnId.Add(mapEntry.OutputColumnId);

   if (mapEntry.IsReturnColumn)
      Type1.ReturnColumnId.Add(mapEntry.OutputColumnId);
 }

以下是执行相同操作的 Linq 版本:

MapEntryTable.Where(x => x.IsDisplayedColumn == true).ToList().ForEach(mapEntry => Type1.DisplayColumnId.Add(mapEntry.OutputColumnId));       

MapEntryTable.Where(x => x.IsReturnColumn == true).ToList().ForEach(mapEntry => Type1.ReturnColumnId.Add(mapEntry.OutputColumnId));

我正在将所有这些 foreach 代码转换为 linq,因为我正在学习它,但我的问题是:

  • 在这种情况下,我能从 Linq 转换中获得任何优势还是劣势?

  • 有没有更好的方法使用 Linq 来做同样的事情

更新:

考虑在列表中的 1000 个元素中 80% 的两个属性都为 false 的情况,然后 where 为我提供了快速查找具有给定条件的元素的好处。

Type1 是一个自定义类型,具有一组 List<int> 结构、DisplayColumnIdReturnColumnId

【问题讨论】:

  • 如果你想缩短你的代码有点摆脱“== true”比较。如果 IsDisplayedColumn 为真,x => x.IsDisplayedColumn 将返回真,无需将其与实际值进行比较。您可能还不必要地调用 ToList()。
  • @Andrew B 第一部分是正确的,但第二部分是强制性的,Foreach 是一个列表扩展方法而不是 IEnumerable
  • list1和list2是什么类型?
  • 我从未见过具有 DisplayColumnId 属性的 List...我一定在这里遗漏了一些东西。无论如何,没有足够的信息来回答:当您可以使用sequence.ToList()list.AddRange(sequence) 将序列分配或附加到列表时,您可能不需要ToList().ForEach()
  • @MrinalKamboj 那么你的名字都非常具有误导性。当它们不是列表时,您不应该有名为 list1list2 的变量,并且名称 DisplayColumnId 听起来根本不像应该是一个集合,它的名称暗示它是一个单数标识符。

标签: c# linq


【解决方案1】:

ForEach 不是 LINQ 方法。这是List的方法。它不仅不是 LINQ 的一部分,而且非常违反了 LINQ 的价值观和模式。 Eric Lippet explains this in a blog post 是他在 C# 编译器团队担任主要开发人员时编写的。

你的“LINQ”方法也是:

  • 完全不必要地将所有要添加的项目复制到列表中,这既浪费时间和内存,也与 LINQ 在执行查询时延迟执行的目标相冲突。
  • 实际上不是查询Where 运算符除外。您正在对查询中的项目进行操作,而不是执行查询。 LINQ 是一个查询工具,而不是用于操作数据集的工具。
  • 您将源序列迭代两次。这可能是也可能不是问题,具体取决于源序列实际是什么以及迭代它的成本是多少。

尽可能多地使用 LINQ 的解决方案是这样使用它:

foreach (var mapEntry in MapEntryTable.Where(entry => mapEntry.IsDisplayedColumn))
    list1.DisplayColumnId.Add(mapEntry.OutputColumnId);
foreach (var mapEntry in MapEntryTable.Where(entry => mapEntry.IsReturnColumn))
    list2.ReturnColumnId.Add(mapEntry.OutputColumnId);

【讨论】:

  • 为什么需要foreach?你可以像安德鲁建议的那样做:list1.DisplayColumnId = MapEntryTable.Where(x => x.IsDisplayedColumn).Select(mapEntry => mapEntry.OutputColumnId);list2.ReturnColumnId = MapEntryTable.Where(x => x.IsReturnColumn).Select(mapEntry => mapEntry.OutputColumnId);
  • @Carlos 假设list2.ReturnColumnId 的类型是IEnumerable,并且那里的所有内容都应该替换为此内容。如果这不是变量的类型,和/或该集合中的某些项目也应该保留在那里,那么这将不起作用。
  • 嗯,有道理。干杯! :)
  • @Servy 感谢您的详细说明,我将坚持使用 foreach,但 linq 中的 Where 扩展方法是否会快速搜索元素或枚举完整列表,因此不会产生任何重大好处过滤,所以我的原始代码最好
  • 我现在修改了我的代码以使用AddRange,它采用IEnumerable,并且不会与现有内容混淆。
【解决方案2】:

我会说坚持使用 foreach 循环的原始方式,因为您只遍历列表 1 次。

你的 linq 应该看起来更像这样:

list1.DisplayColumnId.AddRange(MapEntryTable.Where(x => x.IsDisplayedColumn).Select(mapEntry => mapEntry.OutputColumnId));       

list2.ReturnColumnId.AddRange(MapEntryTable.Where(x => x.IsReturnColumn).Select(mapEntry => mapEntry.OutputColumnId));

【讨论】:

  • 我怀疑这会起作用,因为Select 会返回一个IEnumerable<T>,而list1.DisplayColumnId 很可能是一个List<T>,但即使你在最后打了一个ToList(),结果也会与添加到现有集合不同(基本上您会丢失集合中已经存在的所有数据)。
  • 同意@juharr 这对我不起作用,我会丢失现有的列表数据,它们不是空集合
【解决方案3】:

foreach 与 Linq ForEach 的性能几乎完全相同,相差不到几纳秒。假设您在测试时在两个版本的循环中具有相同的内部逻辑。

但是,for 循环的性能大大优于两者。 for(int i; i

虽然使用 Linq ForEach 循环确实有一个很大的缺点。你不能跳出循环。如果你需要这样做,你必须维护一个像“breakLoop = false”这样的布尔值,将其设置为true,如果breakLoop为true,则让每个递归退出......在那里表现不佳。其次,您不能使用 continue,而是使用“return”。

我从不使用 Linq 的 foreach 循环。

如果您正在处理 linq,例如

List<Thing> things = .....;
var oldThings = things.Where(p.DateTime.Year < DateTime.Now.Year);

这将在内部使用 linq 进行 foreach 并只返回比当前年份少一年的项目。酷..

但如果我这样做:

List<Thing> things = new List<Thing>();

foreach(XElement node in Results) {
    things.Add(new Thing(node));
}

我不需要为每个循环使用 linq。即使我这样做了......

foreach(var node in thingNodes.Where(p => p.NodeType == "Thing") {
    if (node.Ignore) {
        continue;
    }
    thing.Add(node);
}

即使我可以写出像这样的清洁器

foreach(var node in thingNodes.Where(p => p.NodeType == "Thing" && !node.Ignore) {
    thing.Add(node);
}

我想不出这样做的真正原因..>

   things.ForEach(thing => {
       //do something
       //can't break
       //can't continue
       return; //<- continue
   });

如果我想要尽可能快的循环,

for (int i = 0; i < things.Count; ++i) {
    var thing = things[i];
    //do something
}

会更快。

【讨论】:

    【解决方案4】:

    您的 LINQ 不太正确,因为您将 Where 的结果转换为列表,然后使用 ForEach 对这些结果进行伪迭代以添加到另一个列表。使用ToListAddRange 将序列转换或添加到列表中。

    例如,覆盖list1(如果它实际上是List&lt;T&gt;):

    list1 = MapEntryTable.Where(x => x.IsDisplayedColumn == true)
    .Select(mapEntry => mapEntry.OutputColumnId).ToList();
    

    或附加:

    list1.AddRange(MapEntryTable.Where(x => x.IsDisplayedColumn == true)
    .Select(mapEntry => mapEntry.OutputColumnId));
    

    【讨论】:

    • 感谢第二部分会很有帮助,因为我需要添加到现有集合中
    【解决方案5】:

    在 C# 中,要在一次调用中完成您想要的功能,您必须编写自己的分区方法。如果你愿意使用 F#,你可以使用List.Partition&lt;'T&gt;

    https://msdn.microsoft.com/en-us/library/ee353782.aspx

    【讨论】:

    • 在这种情况下,您必须创建一个接收源列表和 lambda 表达式的函数,然后在将 lambda 表达式应用于源后返回 Tuple&lt;List&lt;MapEntry&gt;, List&lt;MapEntry&gt;&gt;
    • 顺便说一句,使用上面的 foreach 代码最容易实现,或者需要调用 GroupBy。
    猜你喜欢
    • 2021-06-05
    • 2016-09-30
    • 2019-07-22
    • 1970-01-01
    • 1970-01-01
    • 2022-01-01
    • 1970-01-01
    • 2014-03-16
    • 1970-01-01
    相关资源
    最近更新 更多