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

时间:2010-02-08 18:48:06

标签: c# linq for-loop

是否可以将以下'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

7 个答案:

答案 0 :(得分:25)

不确定。哎呀,你可以用LINQ查询替换算术

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

但你不应该。

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

答案 1 :(得分:15)

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

var list = new List<Func<int>>();
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<int>个对象列表,它们都返回3. LINQ版本生成一个Func<int>列表,分别返回1,2和3。这使得这种捕获方式难以移植。

答案 2 :(得分:3)

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

实际上,功能代码使这更容易,因为它不需要像代码那样的特殊情况。更好的是,.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<T>(this IEnumerable<T> 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)如果没有查看反射器中的实现,我猜测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!");
} 

答案 3 :(得分:2)

技术上,是的。

任何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扩展方法,例如:

///<summary>Appends a list of strings to a StringBuilder, separated by a separator string.</summary>
///<param name="builder">The StringBuilder to append to.</param>
///<param name="strings">The strings to append.</param>
///<param name="separator">A string to append between the strings.</param>
public static StringBuilder AppendJoin(this StringBuilder builder, IEnumerable<string> 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;
}

///<summary>Combines a collection of strings into a single string.</summary>
public static string Join<T>(this IEnumerable<T> strings, string separator, Func<T, string> selector) { return strings.Select(selector).Join(separator); }
///<summary>Combines a collection of strings into a single string.</summary>
public static string Join(this IEnumerable<string> strings, string separator) { return new StringBuilder().AppendJoin(strings, separator).ToString(); }

答案 4 :(得分:2)

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

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

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

答案 5 :(得分:2)

我认为最重要的是,为避免语义混淆,当实际功能时,您的代码应该只是表面功能。换句话说,请不要在LINQ表达式中使用副作用。

答案 6 :(得分:1)

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

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

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