Thinking Linq

I’ve been comfortable using LINQ for what seems like a long time. I’m noticing that I’ve been implementing some of the lesser used functions a bit more as of late, but I’m not sure exactly when they’re appropriate. Take the following method that is a prime candidate for leveraging LINQ.

public DirectoryInfo GetFirstDirectoryFrom(params string[] list)
{
    foreach (var item in list)
    {
        var directory = new DirectoryInfo(item);
        if (directory.Exists)
            return directory;
    }
    return null;
}

Any time you see that if inside a foreach in C#, you’ll probably want to write this differently. A year ago, I probably would have written this:

public DirectoryInfo GetFirstDirectoryFrom(params string[] list)
{
    return list.Select(l => new DirectoryInfo(l)).Where(d => d.Exists).FirstOrDefault();
}

More recently, my thought process makes me want to write that same code like so:

public DirectoryInfo GetFirstDirectoryFrom(params string[] list)
{
    return list.Select(l => new DirectoryInfo(l)).SkipWhile(d => !d.Exists).FirstOrDefault();
}

For some reason, I like the second LINQ implementation better. It just seems to read nicer and reflect the method’s intent a bit more accurately.

I don’t have a strong preference either way, so I decided to see how different they actually are in IL:

GetFirstDirectoryFrom2:
IL_0000:  ldarg.1     
IL_0001:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate2
IL_0006:  brtrue.s    IL_0019
IL_0008:  ldnull      
IL_0009:  ldftn       UserQuery.<GetFirstDirectoryFrom2>b__0
IL_000F:  newobj      System.Func<System.String,System.IO.DirectoryInfo>..ctor
IL_0014:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate2
IL_0019:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate2
IL_001E:  call        System.Linq.Enumerable.Select
IL_0023:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate3
IL_0028:  brtrue.s    IL_003B
IL_002A:  ldnull      
IL_002B:  ldftn       UserQuery.<GetFirstDirectoryFrom2>b__1
IL_0031:  newobj      System.Func<System.IO.DirectoryInfo,System.Boolean>..ctor
IL_0036:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate3
IL_003B:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate3
IL_0040:  call        System.Linq.Enumerable.Where
IL_0045:  call        System.Linq.Enumerable.FirstOrDefault
IL_004A:  ret         

GetFirstDirectoryFrom3:
IL_0000:  ldarg.1     
IL_0001:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate6
IL_0006:  brtrue.s    IL_0019
IL_0008:  ldnull      
IL_0009:  ldftn       UserQuery.<GetFirstDirectoryFrom3>b__4
IL_000F:  newobj      System.Func<System.String,System.IO.DirectoryInfo>..ctor
IL_0014:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate6
IL_0019:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate6
IL_001E:  call        System.Linq.Enumerable.Select
IL_0023:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate7
IL_0028:  brtrue.s    IL_003B
IL_002A:  ldnull      
IL_002B:  ldftn       UserQuery.<GetFirstDirectoryFrom3>b__5
IL_0031:  newobj      System.Func<System.IO.DirectoryInfo,System.Boolean>..ctor
IL_0036:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate7
IL_003B:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate7
IL_0040:  call        System.Linq.Enumerable.SkipWhile
IL_0045:  call        System.Linq.Enumerable.FirstOrDefault
IL_004A:  ret         

As far as I’m concerned, these are exactly the same. I’m going to go with the second implementation, the one that uses .SkipWhile().

Related:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Chris Missal

Oh hey, I'm a Senior Consultant for Headspring in Austin, TX. I've been working in software professionally since 2006 and I really, really love it. I'm mostly in the Microsoft world, but enjoy building computer things of all sorts (to be vague). When I'm not slinging code, I'm probably out and about slinging discs, bowling balls, or good beer with great friends.
This entry was posted in .NET, C#, LINQ and tagged , , . Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Jon Norton

    How about list.Select(l => new DirectoryInfo(l)).FirstOrDefault(d => !d.Exists);

    • Will Rogers

      Remove the ! and you’ve got it.

  • http://profiles.google.com/bartoncasey Casey Barton

    FirstOrDefault, like many LINQ methods lets you use a test lambda directly:

    list.Select(l => new DirectoryInfo(l)).FirstOrDefault(d => d.Exists)

    I haven’t looked, but I suspect the IL would again be similar.

    • http://chrismissal.lostechies.com Chris Missal

      Ahh yes, I wrote that without Visual Studio or ReSharper (and R# gives you a suggestion to “Replace with single call to FirstOrDefault(..)”. The green squiggly would have reminded me of this.
      That definitely helps shift my preference in the other direction. Less code!

      • Chris Bristol

        I’m not sure (and I’m too lazy to check), but I think the compiler will actually optimize:

        items.Where(item => item.X).FirstOrDefault();

        into

        items.FirstOrDefault(item => item.X);

        as long as the result isn’t being assigned to an expression tree.

        • Mihasic

          AFAIK, it’s not the compiler to solve that problem and those are different implementation.

          First: create Iterator wrapper depending on type of enumerable, than browse for first item.
          Second: just foreach loop with condition check (actual original method).

      • Pierre-Alain Vigeant

        There is a implementation problem with FirstOrDefault(lambda). Using a reflector tool, you can see that the Where(lambda) does a chekc on the type of collection and change its iteration method accordingly, whereas FirstOrDefault(lambda) use a simple foreach.

        I don’t know the rational behind that, because I think they should be implemented the same way.

        So technically, while the result is the same, they are not doing the same process, and Where(lamba).FirstOrDefault() could be faster than FirstOrDefault(lambda) depending on the type of collection.

  • John
  • Diego Frata

    This doesn’t mean much as you’re looking at your own IL, not SkipWhile or Where IL. Of course it looks the same, it’s just a method call. 

    SkipWhile and Where have different implementations and different purposes. It only works the same because you discard the rest of the enumeration taking only the first item. But again, you could accomplish the same passing the predicate directly into FirstOrDefault call, as stated by Casey and Jon.

  • Will Rogers

    I prefer the first LINQ version. The SkipWhile version addresses the items you *don’t* want to Select, therefore requiring a second negative (d => !d.Exists) in order to resolve things.

    Or, if you prefer: Of the LINQ examples, I don’t prefer the one that does not not require a double negative. :-)

  • http://www.daedtech.com/blog Erik Dietrich

    I agree with Will, but I recognize that this is purely a subjective distinction.  The task is, “given this list of potential directories, find the first one that exists on disk”.  It seems a little awkward to rephrase that as “given this list of potential directories, find the first one that doesn’t not exist on disk.”

    But, I suppose that’s the beauty of Linq – we get to argue over which pretty readable piece of semantics is slightly better.  :)

    • http://chrismissal.lostechies.com Chris Missal

      Lol, yes we do! :)

  • Daniel González García

    I have to agree with some the folks that shy away from negative boolean logic.
    The less code to parse and evaluate while reading it, the best.

    And the second case does not click in my mind automatically, but needs second thoughts.

    In my coding I favor the predicate-receiving functions of .First(), .Last(), .Single() (and their …OrDefault() counterparts) over the .Where() filtering upfront.

    But, at the end of the day, what matters is that LINQ rocks and has turned around my way of thinking about loops and sequence processing.

    Cheers

  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1135

  • John

    FirstOrDefault can take an expression:

    .FirstOrDefault(d => d.Exists)

  • Oliver Hallam

    Well yeah, function calls to different functions with the same arguments will look similar in IL.  What does this prove?

    Surely you want to be looking at the difference in the implementations of Enumerable.Where and Enumerable.SkipWhile!  In fact though they are very similar, the TakeWhile enumerator just has an extra flag to ensure that it returns everything after the first element that didn’t match.

  • http://blog.andrei.rinea.ro/ http://blog.andrei.rinea.ro

    Cute :)

  • Guest

    2nd version is harder to read.