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

通常,当我接受此更改时, ReSharper向前迈了一步,建议我以闪亮的LINQ形式再次进行更改。

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

评论

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

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

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

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

#1 楼


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


两者返回的城市都相同,但差异很大。


使用foreach时,ListCities()被调用一次并产生47个项目。
使用for时,ListCities()被调用了94次,总共产生了28153个项目。

发生了什么?

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

foreach的情况下,仅请求一次结果。在上面错误编写的代码中实现的for的情况下,请求结果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>的结果转换为他们熟悉的东西,例如ElementAt

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


仍然,此代码非常不同从ListCities()替代品。同样,它给出的结果相同,这一次List<T>方法仅被调用一次,但产生575个项目,而foreach方法仅产生47个项目。

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

foreach是什么,真的吗?

for更接近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;
        }
    }
}


两者都产生相同的代码白介素两者具有相同的结果。两者都有相同的副作用。当然,可以使用类似的无限foreach重写此foreach,但是它甚至更长,并且容易出错。您可以自由选择一个更具可读性的语言。

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

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奥尔巴尼·亚历山大·阿尔罕布拉[...]波恩·波尔多波士顿

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

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

该数据被称为1次并生产575件。

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

该数据被调用了1次并产生了47个项目。

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

该数据被称为1次并产生了47个项目。


LINQ与传统方式

至于LINQ,您可能想学习函数式编程(FP)-不是C#FP的东西,而是真正的FP语言,例如Haskell。功能语言具有表达和呈现代码的特定方式。在某些情况下,它优于非功能性范例。众所周知,FP在处理列表(列表为通用术语,与while不相关)方面要优越得多。考虑到这一事实,在列表中以更实用的方式表达C#代码的能力是一件好事。

如果您不相信,请比较用两种功能编写的代码的可读性和我以前关于这个问题的答案中的非功能性方式。

评论


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

–但丁
2012年12月4日上午11:07

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

– HappyCat
2012年12月4日,11:12

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

–史蒂文·埃弗斯(Steven Evers)
2012年12月4日在16:49

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

–迈克尔·布朗(Michael Brown)
2012年12月4日17:51

就像我喜欢这个答案一样,我认为这些示例有些人为的。最后的总结表明,foreach比之有效,而实际上差异是故意破坏代码的结果。答案的彻底性本身就可以赎回,但是很容易看到一个随意的观察者可能得出错误的结论。

–罗伯特·哈维(Robert Harvey)
2012年12月6日23:39



#2 楼

关于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将返回序列of46。如果您要计算的全部是30,

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


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

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

#3 楼


,但是我看到代码变得越来越不可读了。


可读性在旁观者的眼中。有人会说

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


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

评论


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

– jk。
2012年12月4日,9:51

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

–科纳米曼
2012年12月4日在12:08

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

– Shauna
2012年12月4日在16:27

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

– R. Martinho Fernandes
2012年12月4日17:39

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

– Misko
2012年12月4日18:11

#4 楼

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


命令式的:您以这种风格告诉计算机“现在执行此操作...现在执行此操作...现在执行此操作现在执行此操作”。您一次将其喂入一个程序。
声明性的:以这种风格,您可以告诉计算机您想要的结果是什么,然后让它弄清楚如何到达那里。

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

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

var foo = bar.Distinct();


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

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

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

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

评论


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

–贝琳·洛里奇(Berin Loritsch)
18年2月13日在21:18

#5 楼

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

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


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

每个循环的A都非常适合遍历列表,但是如果那不是您要做什么,那就不要不使用一个。

评论


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

– jk。
2012年12月4日上午10:14

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

–埃里克·金(Eric King)
2012年12月4日14:50

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

–怀亚特·巴内特(Wyatt Barnett)
2012年12月4日15:35

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

–提莫西·鲍德里奇(Timothy Baldridge)
2012年12月4日16:13

这已经具有足够的否决权,所以我不会补充,但是支持无知/无能的同事的论点从来都不是有效的。

–史蒂文·埃弗斯(Steven Evers)
2012年12月4日17:09