可以用LINQ语句替换所有’for’循环吗?

是否可以将以下’foreach’编写为LINQ语句,我想更常见的问题是,任何for循环都可以被LINQ语句替换。

我对任何潜在的性能成本都不感兴趣,只是在传统的命令式代码中使用声明性方法的潜力。

private static string SomeMethod() { if (ListOfResources .Count == 0) return string.Empty; var sb = new StringBuilder(); foreach (var resource in ListOfResources ) { if (sb.Length != 0) sb.Append(", "); sb.Append(resource.Id); } return sb.ToString(); } 

干杯

AWC

当然。 哎呀,你可以用LINQ查询替换算术

http://blogs.msdn.com/ericlippert/archive/2009/12/07/query-transformations-are-syntactic.aspx

但你不应该。

查询表达式的目的是表示查询操作 。 “for”循环的目的是迭代特定语句,以便多次执行其副作用。 这些往往是非常不同的。 我鼓励更换循环,其目的仅仅是使用更高级别的结构查询数据,以便更清楚地查询数据。 我强烈反对用查询理解替换产生副作用的代码,尽管这样做是可能的。

总的来说是的,但有些特殊情况非常困难。 例如,一般情况下的以下代码在没有大量黑客攻击的情况下不会移植到LINQ表达式。

 var list = new List>(); foreach ( var cur in (new int[] {1,2,3})) { list.Add(() => cur); } 

原因在于,使用for循环,可以看到在闭包中如何捕获迭代变量的副作用。 LINQ表达式隐藏迭代变量的生命周期语义,并防止您看到捕获它的值的副作用。

注意。 上面的代码等同于以下LINQ表达式。

 var list = Enumerable.Range(1,3).Select(x => () => x).ToList(); 

foreach示例生成一个Func对象列表,它们都返回3. LINQ版本生成一个Func列表,它们分别返回1,2和3。 这就是使这种捕获方式难以移植的原因。

实际上,您的代码执行的function基本上非常实用,即通过连接列表项将字符串列表减少为单个字符串。 关于代码的唯一必要条件是使用StringBuilder

实际上,function代码使这更容易,因为它不需要像代码那样的特殊情况。 更好的是,.NET已经实现了这个特定的操作,并且可能比你的代码更有效1)

 return String.Join(", ", ListOfResources.Select(s => s.Id.ToString()).ToArray()); 

(是的,对ToArray()的调用很烦人但是Join是一个非常古老的方法,早于LINQ。)

当然,可以像这样使用“更好”的Join版本:

 return ListOfResources.Select(s => s.Id).Join(", "); 

实现相当简单 – 但再一次,使用StringBuilder (性能)使其成为当务之急。

 public static String Join(this IEnumerable items, String delimiter) { if (items == null) throw new ArgumentNullException("items"); if (delimiter == null) throw new ArgumentNullException("delimiter"); var strings = items.Select(item => item.ToString()).ToList(); if (strings.Count == 0) return string.Empty; int length = strings.Sum(str => str.Length) + delimiter.Length * (strings.Count - 1); var result = new StringBuilder(length); bool first = true; foreach (string str in strings) { if (first) first = false; else result.Append(delimiter); result.Append(str); } return result.ToString(); } 

1)如果没有查看reflection器中的实现,我猜猜String.Join首先通过字符串来确定总长度。 这可以用于相应地初始化StringBuilder ,从而节省了昂贵的复制操作。

由SLaks编辑 :这是来自.Net 3.5的String.Join的相关部分的参考源:

 string jointString = FastAllocateString( jointLength ); fixed (char * pointerToJointString = &jointString.m_firstChar) { UnSafeCharBuffer charBuffer = new UnSafeCharBuffer( pointerToJointString, jointLength); // Append the first string first and then append each following string prefixed by the separator. charBuffer.AppendString( value[startIndex] ); for (int stringToJoinIndex = startIndex + 1; stringToJoinIndex <= endIndex; stringToJoinIndex++) { charBuffer.AppendString( separator ); charBuffer.AppendString( value[stringToJoinIndex] ); } BCLDebug.Assert(*(pointerToJointString + charBuffer.Length) == '\0', "String must be null-terminated!"); } 

我认为这里最重要的是,为了避免语义混淆,你的代码只有在实际function时才能表面上起作用。 换句话说,请不要在LINQ表达式中使用副作用。

从技术上讲,是的。

可以使用ForEach扩展方法将任何foreach循环转换为LINQ,例如MoreLinq中的方法 。

如果您只想使用“纯”LINQ(仅内置扩展方法),则可以滥用Aggregate扩展方法,如下所示:

 foreach(type item in collection { statements } type item; collection.Aggregate(true, (j, itemTemp) => { item = itemTemp; statements return true; ); 

这将正确处理任何 foreach循环,甚至是JaredPar的答案。 编辑 :除非它使用ref / out参数,不安全的代码或yield return
难道你不敢在实际代码中使用这个技巧。


在您的特定情况下,您应该使用字符串Join extension方法,例如:

 ///Appends a list of strings to a StringBuilder, separated by a separator string. ///The StringBuilder to append to. ///The strings to append. ///A string to append between the strings. public static StringBuilder AppendJoin(this StringBuilder builder, IEnumerable strings, string separator) { if (builder == null) throw new ArgumentNullException("builder"); if (strings == null) throw new ArgumentNullException("strings"); if (separator == null) throw new ArgumentNullException("separator"); bool first = true; foreach (var str in strings) { if (first) first = false; else builder.Append(separator); builder.Append(str); } return builder; } ///Combines a collection of strings into a single string. public static string Join(this IEnumerable strings, string separator, Func selector) { return strings.Select(selector).Join(separator); } ///Combines a collection of strings into a single string. public static string Join(this IEnumerable strings, string separator) { return new StringBuilder().AppendJoin(strings, separator).ToString(); } 

通常,您可以使用委托来编写lambda表达式,该委托表示foreach循环的主体,在您的情况下,类似于:

 resource => { if (sb.Length != 0) sb.Append(", "); sb.Append(resource.Id); } 

然后只需在ForEach扩展方法中使用。 这是否是一个好主意取决于身体的复杂性,如果它太大而复杂,你可能不会从中获得任何东西,除非可能产生混淆;)

您的问题中的特定循环可以以声明方式完成,如下所示:

 var result = ListOfResources .Select(r => r.Id.ToString()) .Aggregate(new StringBuilder(), (sb, s) => sb.Append(sb.Length > 0 ? ", " : String.Empty).Append(s)) .ToString(); 

至于性能,您可以预期性能下降,但这对于大多数应用程序来说是可以接受的。