for vs.foreach与LINQ


86

当我在Visual Studio中编写代码时,ReSharper(上帝保佑它!)经常建议我以更紧凑的foreach形式更改旧式的for循环。

通常,当我接受此更改时,ReSharper会往前走一步,建议我以闪亮的LINQ形式再次更改它。

因此,我想知道:这些改进是否有真正的优势?在非常简单的代码执行中,我看不到任何速度提升(显然),但是我可以看到代码变得越来越不可读...所以我想知道:值得吗?


2
请注意-如果您熟悉SQL语法,则LINQ语法实际上可读性强。LINQ还有两种格式(类似于SQL的lambda表达式和链接方法),这可能使学习起来更容易。仅仅是ReSharper的建议,使它看起来难以理解。
Shauna 2012年

3
根据经验,除非处理已知长度数组或类似迭代次数相关的类似情况,否则我通常使用foreach。至于LINQ修饰,我通常会看到ReSharper对foreach的理解,并且如果生成的LINQ语句整洁/琐碎/可读,我会使用它,否则我将其还原。如果在需求发生变化的情况下重新编写原始的非LINQ逻辑很麻烦,或者如果有必要通过LINQ语句所抽象的逻辑进行细粒度的调试,那么我不使用LINQ并将其保留很长时间形成。
Ed Hastings

一个常见的错误foreach是在枚举的同时从集合中删除项目,通常for需要从最后一个元素开始循环。
斯莱

您可能会从Øredev2013-Jessica Kerr-面向对象开发人员的功能原理中获得价值。在33分钟之后,Linq很快就进入了演示文稿,标题为“声明式样式”。
Theraot

Answers:


139

forforeach

常见的困惑是,这两种构造非常相似,并且两者可以互换,如下所示:

foreach (var c in collection)
{
    DoSomething(c);
}

和:

for (var i = 0; i < collection.Count; i++)
{
    DoSomething(collection[i]);
}

两个关键字都以相同的三个字母开头的事实并不意味着它们在语义上是相似的。这种混乱非常容易出错,特别是对于初学者。遍历一个集合并且对元素做一些事情foreach; for除非您真的知道自己在做什么,否则不必也不应将其用于此目的

让我们看一个例子的问题。最后,您将找到用于收集结果的演示应用程序的完整代码。

在该示例中,我们在遇到“波士顿”之前从数据库中加载了一些数据,更确切地说是从Adventure Works中按名称排序的城市。使用以下SQL查询:

select distinct [City] from [Person].[Address] order by [City]

数据通过ListCities()返回的方法加载IEnumerable<string>。这里是什么foreach样子:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

让我们用重写它for,假设两者都是可互换的:

var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
    var city = cities.ElementAt(i);

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

两者返回相同的城市,但有很大的不同。

  • 使用时foreachListCities()被调用一次并产生47个项目。
  • 使用时forListCities()被调用94次,总共产生28153个项目。

发生了什么?

IEnumerable懒惰的。这意味着它将仅在需要结果时进行工作。惰性评估是一个非常有用的概念,但有一些警告,包括容易错过需要结果的时刻的事实,特别是在多次使用结果的情况下。

在a的情况下,foreach仅请求一次结果。for 上述错误编写的代码中实现的 a的情况下,请求结果94次,即47×2:

  • 每次cities.Count()被叫(47次),

  • 每次cities.ElementAt(i)被调用(47次)。

查询数据库94次而不是查询一次是很糟糕的,但不是最糟糕的事情。例如,想象一下,如果在select查询之前执行查询,并且该查询还在表中插入一行,将会发生什么情况。是的,我们希望for它将调用数据库2,147,483,647次,除非希望它之前崩溃了。

当然,我的代码是有偏见的。我故意使用的惰性,IEnumerable并以反复调用的方式编写它ListCities()。可以注意到,初学者永远不会那样做,因为:

  • IEnumerable<T>不具有这样的性质Count,但只有方法Count()。调用方法很可怕,并且可以期望它的结果不会被缓存,也不适合在一个for (; ...; )块中使用。

  • 无法建立索引IEnumerable<T>,找到ElementAtLINQ扩展方法并不明显。

可能大多数初学者只会将结果转换为ListCities()他们熟悉的内容,例如List<T>

var cities = Program.ListCities();
var flushedCities = cities.ToList();
for (var i = 0; i < flushedCities.Count; i++)
{
    var city = flushedCities[i];

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

但是,此代码与foreach替代代码有很大不同。同样,它给出的结果相同,这一次该ListCities()方法只调用了一次,但是产生了575个项目,而使用时foreach,它只产生了47个项目。

差异来自ToList()导致从数据库加载所有数据的事实。尽管foreach仅要求“波士顿”之前的城市,但新的for要求检索所有城市并将其存储在内存中。使用575个短字符串,这可能并没有多大区别,但是如果我们只从包含数十亿条记录的表中检索几行呢?

那到底是foreach什么?

foreach更接近while循环。我以前使用的代码:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

可以简单地替换为:

using (var enumerator = Program.ListCities().GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        var city = enumerator.Current;
        Console.Write(city + " ");

        if (city == "Boston")
        {
            break;
        }
    }
}

两者产生相同的IL。两者具有相同的结果。两者都有相同的副作用。当然,这while可以用类似的infinite重写for,但是它甚至更长,并且容易出错。您可以自由选择更易读的内容。

要自己测试吗?这是完整的代码:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;

public class Program
{
    private static int countCalls;

    private static int countYieldReturns;

    public static void Main()
    {
        Program.DisplayStatistics("for", Program.UseFor);
        Program.DisplayStatistics("for with list", Program.UseForWithList);
        Program.DisplayStatistics("while", Program.UseWhile);
        Program.DisplayStatistics("foreach", Program.UseForEach);

        Console.WriteLine("Press any key to continue...");
        Console.ReadKey(true);
    }

    private static void DisplayStatistics(string name, Action action)
    {
        Console.WriteLine("--- " + name + " ---");

        Program.countCalls = 0;
        Program.countYieldReturns = 0;

        var measureTime = Stopwatch.StartNew();
        action();
        measureTime.Stop();

        Console.WriteLine();
        Console.WriteLine();
        Console.WriteLine("The data was called {0} time(s) and yielded {1} item(s) in {2} ms.", Program.countCalls, Program.countYieldReturns, measureTime.ElapsedMilliseconds);
        Console.WriteLine();
    }

    private static void UseFor()
    {
        var cities = Program.ListCities();
        for (var i = 0; i < cities.Count(); i++)
        {
            var city = cities.ElementAt(i);

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForWithList()
    {
        var cities = Program.ListCities();
        var flushedCities = cities.ToList();
        for (var i = 0; i < flushedCities.Count; i++)
        {
            var city = flushedCities[i];

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForEach()
    {
        foreach (var city in Program.ListCities())
        {
            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseWhile()
    {
        using (var enumerator = Program.ListCities().GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                var city = enumerator.Current;
                Console.Write(city + " ");

                if (city == "Boston")
                {
                    break;
                }
            }
        }
    }

    private static IEnumerable<string> ListCities()
    {
        Program.countCalls++;
        using (var connection = new SqlConnection("Data Source=mframe;Initial Catalog=AdventureWorks;Integrated Security=True"))
        {
            connection.Open();

            using (var command = new SqlCommand("select distinct [City] from [Person].[Address] order by [City]", connection))
            {
                using (var reader = command.ExecuteReader(CommandBehavior.SingleResult))
                {
                    while (reader.Read())
                    {
                        Program.countYieldReturns++;
                        yield return reader["City"].ToString();
                    }
                }
            }
        }
    }
}

结果:

---代表----
Abingdon Albany Alexandria Alhambra [...]波恩·波尔多波士顿

该数据称为94次,共产生28153条。

---带有列表---
Abingdon Albany Alexandria Alhambra [...]波恩波尔多波士顿

该数据称为1次,产生了575项。

---同时---
阿宾登·奥尔巴尼·亚历山大·阿尔罕布拉[波恩·波尔多]波士顿

该数据称为1次,产生了47项。

--- foreach ---
阿宾登·奥尔巴尼·亚历山大·阿尔罕布拉[...]波恩·波尔多波士顿

该数据称为1次,产生了47项。

LINQ与传统方式

至于LINQ,您可能想学习函数式编程(FP)-不是C#FP的东西,而是真正的FP语言,例如Haskell。功能语言具有表达和呈现代码的特定方式。在某些情况下,它优于非功能性范例。

众所周知,FP在处理列表(列表作为通用术语,与不相关List<T>)方面要优越得多。考虑到这一事实,当涉及到列表时,以更实用的方式表达C#代码的能力相当不错。

如果您不确定,请在我之前对这个问题的回答中比较以功能和非功能方式编写的代码的可读性。


1
有关ListCities()示例的问题。为什么只运行一次?在过去,我在解决收益率上没有任何问题。
但丁

1
他并不是说您只会从IEnumerable中得到一个结果-他是说SQL查询(这是方法的昂贵部分)只会执行一次-这是一件好事。然后它将读取并从查询中产生所有结果。
HappyCat 2012年

9
@乔治:虽然这个问题是可以理解的,但是让一种语言的语义适应初学者可能会感到困惑的地方并不能使我们拥有一种非常有效的语言。
史蒂文·埃弗斯

4
LINQ不仅仅是语义糖。它提供了延迟执行。对于IQueryables(例如Entity Framework),允许在查询被迭代之前一直传递和组合查询(这意味着向返回的IQueryable添加where子句将导致SQL在迭代时传递给服务器,以包括where子句将过滤器卸载到服务器上)。
迈克尔·布朗

8
我很喜欢这个答案,但我认为这些示例有些人为。最后的总结表明,这种foreach方法比更为有效for,而实际上差异是故意破坏代码的结果。答案的彻底性本身就可以赎回,但是很容易看出临时观察者可能会得出错误的结论。
罗伯特·哈维

19

关于for和foreach之间的区别已经有一些很好的论述。LINQ的角色有些误解。

LINQ语法不仅是语法糖,它的功能类似于C#。LINQ向C#提供了功能构造,包括其所有优点。结合返回IEnumerable而不是IList,LINQ提供了迭代的延迟执行。人们现在通常要做的是像这样从其函数构造并返回一个IList

public IList<Foo> GetListOfFoo()
{
   var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         retVal.Add(foo);
      }
   }
   return retVal;
}

而是使用yield return语法创建延迟的枚举。

public IEnumerable<Foo> GetEnumerationOfFoo()
{
   //no need to create an extra list
   //var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         //yield the match compiler handles the complexity
         yield return foo;
      }
   }
   //no need for returning a list
   //return retVal;
}

现在,枚举将不会发生,除非您ToList或对其进行迭代。它仅在需要时发生(这是Fibbonaci的枚举,没有堆栈溢出问题)

/**
Returns an IEnumerable of fibonacci sequence
**/
public IEnumerable<int> Fibonacci()
{
  int first, second = 1;
  yield return first;
  yield return second;
  //the 46th fibonacci number is the largest that
  //can be represented in 32 bits. 
  for (int i = 3; i < 47; i++)
  {
    int retVal = first+second;
    first=second;
    second=retVal;
    yield return retVal;
  }
}

在Fibonacci函数上执行foreach将返回46的序列。如果您要计算30,则将全部计算得出

var thirtiethFib=Fibonacci().Skip(29).Take(1);

我们可以从中获得很多乐趣的地方是lambda表达式语言的支持(与IQueryable和IQueryProvider构造结合使用,这允许对各种数据集进行查询的功能组合,IQueryProvider负责解释传入的数据)表达式以及使用源的本机结构创建和执行查询)。我不会在这里详细介绍细节,但是有一系列博客文章显示了如何在此处创建SQL查询提供程序

总而言之,当函数的使用者执行简单的迭代时,您应该更喜欢返回IEnumerable而不是IList。并使用LINQ的功能将复杂查询的执行推迟到需要时再执行。


13

但我看到代码变得越来越难读

易读性在情人眼中。有人可能会说

var common = list1.Intersect(list2);

完全可读;其他人可能会说这是不透明的,因此宁愿

List<int> common = new List<int>();
for(int i1 = 0; i1 < list1.Count; i1++)
{
    for(int i2 = 0; i2 < list2.Count; i2++)
    {
        if (list1[i1] == list2[i2])
        {
            common.Add(i1);
            break;
        }
    }
}

清楚说明正在做什么。我们无法告诉您您发现更具可读性的内容。但是在我这里构建的示例中,您也许可以发现我自己的一些偏见...


28
老实说,我说Linq客观地使意图更具可读性,而for循环使该机制客观地更具可读性。
jk。

16
我会尽可能快地跑出来,告诉别人如果是for-for-if版本比相交版本更具可读性。
Konamiman

3
@Konamiman-这取决于一个人在想到“可读性”时所寻找的东西。jk。的评论很好地说明了这一点。从某种意义上讲,您可以轻松地看到循环如何获得最终结果,而LINQ 在最终结果应该是什么方面则更具可读性,因此它更具可读性。
Shauna 2012年

2
这就是循环进入实现的原因,然后您在所有地方都使用Intersect。
R. Martinho Fernandes

8
@Shauna:想象一下方法中的for循环版本可以完成其他几件事;一团糟。因此,自然地,您将其拆分为自己的方法。在可读性方面,它与IEnumerable <T> .Intersect相同,但是现在您已经复制了框架功能并引入了更多代码来维护。唯一的借口是如果出于行为原因需要自定义实现,但是我们在这里仅讨论可读性。
Misko

7

LINQ和LINQ之间的区别foreach实际上可以归结为两种不同的编程风格:命令式和声明式。

  • 势在必行:您以这种方式告诉计算机“现在执行此操作...现在执行此操作...现在执行此现在操作”。您一次只将其喂入一个程序。

  • 声明性的:以这种风格,您可以告诉计算机您想要的结果是什么,并让它弄清楚如何到达那里。

这两种样式的经典示例是将汇编代码(或C)与SQL进行比较。在汇编中,您一次给出(字面意义)说明。在SQL中,您表示如何将数据连接在一起以及从数据中获得什么结果。

声明式编程的一个很好的副作用是它往往会更高一些。这使平台可以在您下面发展,而无需更改代码。例如:

var foo = bar.Distinct();

这是怎么回事 Distinct是否使用一个核心?二?五十?我们不知道,我们不在乎。.NET开发人员可以随时重写它,只要它继续执行相同的目的,我们的代码就可以在代码更新后更快地变魔术。

这就是功能编程的力量。而且,您会发现使用Clojure,F#和C#等语言(以函数式编程思维方式编写)的代码的原因通常要比命令式代码小3至10倍。

最后,我喜欢声明式样式,因为在大多数情况下,使用C#,这使我可以编写不会更改数据的代码。在上面的示例中,Distinct()不更改栏,它返回数据的新副本。这意味着无论酒吧是什么,无论它来自哪里,它都不会突然改变。

因此,就像其他张贴者所说的那样,学习函数式编程。它会改变你的生活。如果可以的话,请使用真正的函数式编程语言来完成。我更喜欢Clojure,但是F#和Haskell也是不错的选择。


2
LINQ处理推迟到您实际对其进行迭代之前。 var foo = bar.Distinct()基本上是IEnumerator<T>直到您致电.ToList().ToArray()。这是一个重要的区别,因为如果您不了解这一点,可能会导致难以理解的错误。
Berin Loritsch

-5

团队中的其他开发人员可以阅读LINQ吗?

如果不这样做,请不要使用它,否则将发生以下两种情况之一:

  1. 您的代码将无法维护
  2. 您将不得不维护所有代码以及所有依赖于它的内容

for each循环非常适合遍历列表,但是如果这不是您要执行的操作,请不要使用它。


11
嗯,我很欣赏这对于单个项目可能是个答案,但是从中长期来看,您应该培训您的员工,否则您将陷入代码理解的底线,这听起来不是一个好主意。
jk。

21
实际上,可能会发生第三件事:其他开发人员可以付出少量的努力,并实际学习一些新的有用的知识。这不是闻所未闻的。
埃里克·金

6
@InvertedLlama如果我在一家公司中,开发人员需要接受正式培训以了解新的语言概念,那么我会考虑寻找一家新公司。
Wyatt Barnett 2012年

13
也许您可以通过使用库来摆脱这种态度,但是当涉及到核心语言功能时,这并不能解决问题。您可以选择框架。但是,优秀的.NET程序员需要了解该语言以及核心平台(System。*)的每个功能。考虑到不使用Linq甚至无法正确使用EF,我不得不说...在这个时代,如果您是.NET程序员,但您不了解Linq,那么您将无能为力。
蒂莫西·鲍德里奇

7
这已经具有足够的否决权,所以我将不去补充,但是支持无知/无能的同事的论点从来都不是有效的。
史蒂文·埃弗斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.