当我在Visual Studio中编写代码时,ReSharper(上帝保佑它!)经常建议我以更紧凑的foreach形式更改旧式的for循环。
通常,当我接受此更改时,ReSharper会往前走一步,建议我以闪亮的LINQ形式再次更改它。
因此,我想知道:这些改进是否有真正的优势?在非常简单的代码执行中,我看不到任何速度提升(显然),但是我可以看到代码变得越来越不可读...所以我想知道:值得吗?
foreach
是在枚举的同时从集合中删除项目,通常for
需要从最后一个元素开始循环。
当我在Visual Studio中编写代码时,ReSharper(上帝保佑它!)经常建议我以更紧凑的foreach形式更改旧式的for循环。
通常,当我接受此更改时,ReSharper会往前走一步,建议我以闪亮的LINQ形式再次更改它。
因此,我想知道:这些改进是否有真正的优势?在非常简单的代码执行中,我看不到任何速度提升(显然),但是我可以看到代码变得越来越不可读...所以我想知道:值得吗?
foreach
是在枚举的同时从集合中删除项目,通常for
需要从最后一个元素开始循环。
Answers:
for
与 foreach
常见的困惑是,这两种构造非常相似,并且两者可以互换,如下所示:
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
是懒惰的。这意味着它将仅在需要结果时进行工作。惰性评估是一个非常有用的概念,但有一些警告,包括容易错过需要结果的时刻的事实,特别是在多次使用结果的情况下。
在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>
,找到ElementAt
LINQ扩展方法并不明显。
可能大多数初学者只会将结果转换为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,您可能想学习函数式编程(FP)-不是C#FP的东西,而是真正的FP语言,例如Haskell。功能语言具有表达和呈现代码的特定方式。在某些情况下,它优于非功能性范例。
众所周知,FP在处理列表(列表作为通用术语,与不相关List<T>
)方面要优越得多。考虑到这一事实,当涉及到列表时,以更实用的方式表达C#代码的能力相当不错。
foreach
方法比更为有效for
,而实际上差异是故意破坏代码的结果。答案的彻底性本身就可以赎回,但是很容易看出临时观察者可能会得出错误的结论。
关于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的功能将复杂查询的执行推迟到需要时再执行。
但我看到代码变得越来越难读
易读性在情人眼中。有人可能会说
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和LINQ之间的区别foreach
实际上可以归结为两种不同的编程风格:命令式和声明式。
势在必行:您以这种方式告诉计算机“现在执行此操作...现在执行此操作...现在执行此现在操作”。您一次只将其喂入一个程序。
声明性的:以这种风格,您可以告诉计算机您想要的结果是什么,并让它弄清楚如何到达那里。
这两种样式的经典示例是将汇编代码(或C)与SQL进行比较。在汇编中,您一次给出(字面意义)说明。在SQL中,您表示如何将数据连接在一起以及从数据中获得什么结果。
声明式编程的一个很好的副作用是它往往会更高一些。这使平台可以在您下面发展,而无需更改代码。例如:
var foo = bar.Distinct();
这是怎么回事 Distinct是否使用一个核心?二?五十?我们不知道,我们不在乎。.NET开发人员可以随时重写它,只要它继续执行相同的目的,我们的代码就可以在代码更新后更快地变魔术。
这就是功能编程的力量。而且,您会发现使用Clojure,F#和C#等语言(以函数式编程思维方式编写)的代码的原因通常要比命令式代码小3至10倍。
最后,我喜欢声明式样式,因为在大多数情况下,使用C#,这使我可以编写不会更改数据的代码。在上面的示例中,Distinct()
不更改栏,它返回数据的新副本。这意味着无论酒吧是什么,无论它来自哪里,它都不会突然改变。
因此,就像其他张贴者所说的那样,学习函数式编程。它会改变你的生活。如果可以的话,请使用真正的函数式编程语言来完成。我更喜欢Clojure,但是F#和Haskell也是不错的选择。
var foo = bar.Distinct()
基本上是IEnumerator<T>
直到您致电.ToList()
或.ToArray()
。这是一个重要的区别,因为如果您不了解这一点,可能会导致难以理解的错误。
团队中的其他开发人员可以阅读LINQ吗?
如果不这样做,请不要使用它,否则将发生以下两种情况之一:
for each循环非常适合遍历列表,但是如果这不是您要执行的操作,请不要使用它。