Can all 'for' loops be replaced with a LINQ statement?

前端 未结 7 543
逝去的感伤
逝去的感伤 2021-02-05 08:48

Is it possible to write the following \'foreach\' as a LINQ statement, and I guess the more general question can any for loop be replaced by a LINQ statement.

I\'m not i

相关标签:
7条回答
  • 2021-02-05 08:52

    Technically, yes.

    Any foreach loop can be converted to LINQ by using a ForEach extension method,such as the one in MoreLinq.

    If you only want to use "pure" LINQ (only the built-in extension methods), you can abuse the Aggregate extension method, like this:

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

    This will correctly handle any foreach loop, even JaredPar's answer. EDIT: Unless it uses ref / out parameters, unsafe code, or yield return.
    Don't you dare use this trick in real code.


    In your specific case, you should use a string Join extension method, such as this one:

    ///<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(); }
    
    0 讨论(0)
  • 2021-02-05 08:56

    Sure. Heck, you can replace arithmetic with LINQ queries:

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

    But you shouldn't.

    The purpose of a query expression is to represent a query operation. The purpose of a "for" loop is to iterate over a particular statement so as to have its side-effects executed multiple times. Those are frequently very different. I encourage replacing loops whose purpose is merely to query data with higher-level constructs that more clearly query the data. I strongly discourage replacing side-effect-generating code with query comprehensions, though doing so is possible.

    0 讨论(0)
  • 2021-02-05 08:56

    In general, you can write a lambda expression using a delegate which represents the body of a foreach cycle, in your case something like :

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

    and then simply use within a ForEach extension method. Whether this is a good idea depends on the complexity of the body, in case it's too big and complex you probably don't gain anything from it except for possible confusion ;)

    0 讨论(0)
  • 2021-02-05 08:59

    The specific loop in your question can be done declaratively like this:

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

    As to performance, you can expect a performance drop but this is acceptable for most applications.

    0 讨论(0)
  • 2021-02-05 09:02

    I think what's most important here is that to avoid semantic confusion, your code should only be superficially functional when it is actually functional. In other words, please don't use side effects in LINQ expressions.

    0 讨论(0)
  • 2021-02-05 09:06

    In fact, your code does something which is fundamentally very functional, namely it reduces a list of strings to a single string by concatenating the list items. The only imperative thing about the code is the use of a StringBuilder.

    The functional code makes this much easier, actually, because it doesn’t require a special case like your code does. Better still, .NET already has this particular operation implemented, and probably more efficient than your code1):

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

    (Yes, the call to ToArray() is annoying but Join is a very old method and predates LINQ.)

    Of course, a “better” version of Join could be used like this:

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

    The implementation is rather straightforward – but once again, using the StringBuilder (for performance) makes it imperative.

    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) Without having looked at the implementation in the reflector, I’d guess that String.Join makes a first pass over the strings to determine the overall length. This can be used to initialize the StringBuilder accordingly, thus saving expensive copy operations later on.

    EDIT by SLaks: Here is the reference source for the relevant part of String.Join from .Net 3.5:

    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!");
    } 
    
    0 讨论(0)
提交回复
热议问题