【问题标题】:Can all 'for' loops be replaced with a LINQ statement?可以用 LINQ 语句替换所有“for”循环吗?
【发布时间】:2011-01-14 12:07:49
【问题描述】:

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

【问题讨论】:

    标签: c# linq for-loop


    【解决方案1】:

    从技术上讲,是的。

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

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

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

    这将正确处理 any 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(); }
    

    【讨论】:

    • 您的代码中没有一个 LINQ 查询,所以它不能真正回答问题,是吗? Language INtegrated Query 并不意味着扩展方法 :) 它是围绕扩展方法构建的,但这并不会使每个扩展方法都成为 Language INtegrated Query 语法的一部分
    • @Rune:我很清楚这一点。但是,常见用法(包括此处的所有其他答案)是指遵循 LINQ 样式的任何扩展方法。 (例如,MoreLinq)
    【解决方案2】:

    事实上,您的代码做了一些基本非常功能的事情,即它通过连接列表项将字符串列表减少为单个字符串。关于代码唯一必要的事情是使用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!");
    } 
    

    【讨论】:

    • +1。以清晰、简洁的方式处理这种特定情况的唯一明智的方法。
    • 在 .NET4 中,String.Join 也采用 IEnumerable :)
    • String.Join 实际上使用 char[] 和一些内部不安全的复制技巧来获得更好的性能。
    • @SLaks:是的,但我想保持简单并仍然获得合理的性能。
    • @Konrad:由于您的代码不在 mscorlib 中,因此无论如何您都不能使用这些技巧。我认为没有任何方法可以在不使用内部成员的情况下让您的代码更快。
    【解决方案3】:

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

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

    但你不应该。

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

    【讨论】:

    • @Eric:您是否发现使用 ForEach 扩展方法执行非副作用生成代码有什么问题?对查询的每个结果执行 WriteLn?
    • @Tom:我在这里没有跟随你的思路。写出一行肯定是副作用,不是吗?
    • @Eric:我见过的唯一两个支持我将for 循环转换为类似查询的扩展的论点是:1)在消耗时对副作用的延迟评估站点,以及 2) 将循环转换为用于并行评估的功能结构(如在 PLINQ 中)。使用普通循环都很难实现。
    • @Eric:是的,我想这在最严格的定义中是正确的,但它不会改变结果集中的任何值——只是将它们转储到输出。几乎您看到的每个 LINQ 示例都以一个单独的 foreach 循环结束,该循环转储值,这一直困扰着我——使用漂亮的 LINQ 语法后它看起来很笨拙。您能否详细说明为什么您“强烈反对”这种方法?您的观点似乎是“LINQ 仅用于查询”,但至少在简单的情况下是否有任何潜在危害?不想争论,只是为了理解。
    • @Tom:没有副作用的操作应该能够(1)惰性求值,(2)相对于其他惰性操作的无序求值,(3)求值一次并缓存,( 4) 如果缓存策略抛出缓存结果,则评估两次。如果你想向控制台写一些东西,你希望它被写一次,相对于所有其他写来说,顺序是正确的。
    【解决方案4】:

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

    【讨论】:

    • 你是在暗示这样一个事实,即对于循环中的每个项目,任何等效的 LINQ 语句都应该对所有项目执行完全相同的操作...
    • 不,我只是说不应将 LINQ 表达式视为“做”任何事情,而应将其视为“存在”某事,即具有价值。如果您找不到一种自然的方式来思考它,最好不要将其表述为 LINQ 表达式。碰巧的是,您给出的具体示例 does 作为函数表达式是有意义的,正如 Konrad 上面所展示的。编辑:呃,等等,有点。它应该“完全相同”,因为它应该是无状态的(不应该依赖于更新变量等的语句),如果这就是你的意思的话。
    【解决方案5】:

    您问题中的特定循环可以像这样声明性地完成:

    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(); 
    

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

    【讨论】:

      【解决方案6】:

      一般情况下是的,但有些特殊情况非常困难。例如,在一般情况下,如果没有大量黑客攻击,以下代码不会移植到 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&lt;int&gt; 对象列表,它们都返回 3。LINQ 版本生成一个 Func&lt;int&gt; 列表,它们分别返回 1,2 和 3。这就是使这种捕获方式难以移植的原因。

      【讨论】:

      • @Sean,你的反例产生了不同的结果。我的示例将创建一个 Func&lt;int&gt; 列表,它们都返回 3。你的将返回 1,2 和 3。
      【解决方案7】:

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

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

      然后简单地在 ForEach 扩展方法中使用。这是否是一个好主意取决于身体的复杂性,如果它太大太复杂,除了可能的混乱之外,你可能不会从中获得任何好处;)

      【讨论】:

        猜你喜欢
        • 2014-03-30
        • 1970-01-01
        • 1970-01-01
        • 2014-04-29
        • 2020-11-24
        • 1970-01-01
        • 2020-03-26
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多