Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c#

for-loop

linq

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 interested in any potential performance cost just the potential of using declarative approaches in what is traditionally imperative code.

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

Cheers

AWC

like image 624
AwkwardCoder Avatar asked Feb 08 '10 18:02

AwkwardCoder


4 Answers

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.

like image 140
Eric Lippert Avatar answered Oct 13 '22 00:10

Eric Lippert


In general yes, but there are specific cases that are extremely difficult. For instance, the following code in the general case does not port to a LINQ expression without a good deal of hacking.

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

The reason why is that with a for loop, it's possible to see the side effects of how the iteration variable is captured in a closure. LINQ expressions hide the lifetime semantics of the iteration variable and prevent you from seeing side effects of capturing it's value.

Note. The above code is not equivalent to the following LINQ expression.

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

The foreach sample produces a list of Func<int> objects which all return 3. The LINQ version produces a list of Func<int> which return 1,2 and 3 respectively. This is what makes this style of capture difficult to port.

like image 30
JaredPar Avatar answered Oct 12 '22 22:10

JaredPar


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!");
} 
like image 44
Konrad Rudolph Avatar answered Oct 13 '22 00:10

Konrad Rudolph


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.

like image 23
ChaosPandion Avatar answered Oct 12 '22 23:10

ChaosPandion