所有的“for”循环都可以用LINQ语句替换吗?

xzabzqsa  于 12个月前  发布在  其他
关注(0)|答案(7)|浏览(131)

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

0tdrvxhp

0tdrvxhp1#

当然。见鬼,但你不应该。
查询表达式的目的是 * 表示一个查询操作 *。“for”循环的目的是遍历一个特定的语句,以便多次执行其副作用。这些经常是非常不同的。我鼓励用更高级别的结构来替换循环,这些结构更清楚地查询数据。我强烈反对替换副作用-生成带有查询解析的代码,尽管这样做是可能的。

9vw9lbht

9vw9lbht2#

一般来说是的,但也有一些特殊的情况是非常困难的。例如,下面的代码在一般情况下不移植到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。这就是这种捕获风格难以移植的原因。

5jdjgkvh

5jdjgkvh3#

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

l7mqbcuq

l7mqbcuq4#

事实上,你的代码做了一些基本上非常实用的事情,即通过连接列表项将字符串列表减少到一个字符串。关于代码的唯一必要的事情是使用StringBuilder
实际上,函数式代码使这变得更容易,因为它不需要像你的代码那样特殊的情况。更好的是,.NET已经实现了这个特定的操作,并且可能比你的代码更有效1):

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

字符串
(Yes,调用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,从而节省稍后昂贵的复制操作。

EDIT by 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!");
}

xytpbqjk

xytpbqjk5#

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

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

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

9o685dep

9o685dep6#

严格来说是的
任何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的答案。EDIT:除非它使用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(); }

w51jfk4q

w51jfk4q7#

一般来说,你可以使用一个代表foreach循环主体的委托来编写一个lambda表达式,在你的例子中,类似于:

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

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

相关问题