15

I used to think that List<T> is considered dangerous. My point is that, I think default(T) is not a safe return value! Many other people think so too Consider the following:

List<int> evens = new List<int> { 0, 2, 4, 6, , 8};
var evenGreaterThan10 = evens.Find(c=> c > 10);
// evenGreaterThan10 = 0 #WTF

default(T) for value types is 0, hence 0 is goona be returned is the above code segment!
I didn't like this, so I added an extension method called TryFind that returns a boolean and accepts an output parameter besides the Predicate, something similar to the famous TryParse approach.
Edit:
Here's my TryFind extension method:

public static bool TryFind<T>(this List<T> list, Predicate<T> predicate, out T output)  
{  
  int index = list.FindIndex(predicate);  
  if (index != -1)  
  {  
    output = list[index];  
    return true;  
  }  
  output = default(T);  
  return false;  
}  

What's a your way to do Find on generic Lists?

Ahmed
  • 11,063
  • 16
  • 55
  • 67
  • Why not just use .Where(c=> c>10)? I'm no expert but that should work.. – Jouke van der Maas Jul 08 '10 at 11:02
  • @Jouke - *Where()* returns an IEnumerable list, not just the first item found that *Find()* does. – slugster Jul 08 '10 at 11:31
  • Thank you very much for pointing this out -- IMHO it is a serious flaw, making Find a no-go solution (luckily there is Where and First). On the other hand Find() should be 100% equivalent to Where().First(). – greenoldman Jul 08 '10 at 11:53
  • @macias urw! I actually got bitten with this, so I thought I should share. Right now in my extension method I take advantage of FindIndex! see my edit! – Ahmed Jul 08 '10 at 11:59
  • Nice edit. It is pretty much what I answered execept your return value and params are reversed and my code does one less comparison :) – Jaroslav Jandek Jul 08 '10 at 21:47

8 Answers8

20

I don't. I do .Where()

evens.Where(n => n > 10); // returns empty collection

evens.Where(n => n > 10).First(); // throws exception

evens.Where(n => n > 10).FirstOrDefault(); // returns 0

The first case just returns a collection, so I can simply check if the count is greater than 0 to know if there are any matches.

When using the second, I wrap in a try/catch block that handles InvalidOperationException specfically to handle the case of an empty collection, and just rethrow (bubble) all other exceptions. This is the one I use the least, simply because I don't like writing try/catch statements if I can avoid it.

In the third case I'm OK with the code returning the default value (0) when no match exists - after all, I did explicitly say "get me the first or default" value. Thus, I can read the code and understand why it happens if I ever have a problem with it.

Update:

For .NET 2.0 users, I would not recommend the hack that has been suggested in comments. It violates the license agreement for .NET, and it will in no way be supported by anyone.

Instead, I see two ways to go:

  1. Upgrade. Most of the stuff that runs on 2.0 will run (more or less) unchanged on 3.5. And there is so much in 3.5 (not just LINQ) that is really worth the effort of upgrading to have it available. Since there is a new CLR runtime version for 4.0, there are more breaking changes between 2.0 and 4.0 than between 2.0 and 3.5, but if possible I'd recommend upgrading all the way to 4.0. There's really no good reason to be sitting around writing new code in a version of a framework that has had 3 major releases (yes, I count both 3.0, 3.5 and 4.0 as major...) since the one you're using.

  2. Find a work-around to the Find problem. I'd recommend either just using FindIndex as you do, since the -1 that is returned when nothing is found is never ambiguous, or implementing something with FindIndex as you did. I don't like the out syntax, but before I write an implementation that doesn't use it, I need some input on what you want returned when nothing was found.
    Until then, the TryFind can be considered OK, since it aligns with previous functionality in .NET, for example Integer.TryParse. And you do get a decent way to handle nothing found doing

    List<Something> stuff = GetListOfStuff();
    Something thing;
    
    if (stuff.TryFind(t => t.IsCool, thing)) { 
        // do stuff that's good. thing is the stuff you're looking for.
    }
    else 
    {
        // let the user know that the world sucks.
    }
    
Tomas Aschan
  • 58,548
  • 56
  • 243
  • 402
  • and what does the poor soul running .net 2.0 have to do? – Ahmed Jul 08 '10 at 11:12
  • 1
    @Galilyou - upgrade? ;) You can use LINQ in .net 2.0 environments, see http://stackoverflow.com/questions/2138/linq-on-the-net-2-0-runtime – Winston Smith Jul 08 '10 at 11:16
  • I'm a big user of *FirstOrDefault*, but when using it against value types you end up in the same predicament. Like you said though, it is possibly a better option that using *Find* due to it being more explicit. I wonder how many subtle bugs have been introduced to products due to the default(T) behaviour :) – slugster Jul 08 '10 at 11:28
  • @Winston Smith: It is a hack. It is not supported officially. It violates the user licence. – Jaroslav Jandek Jul 08 '10 at 21:39
  • 1
    Why `evens.Where(n => n > 10).First();` instead of just `evens.First(n => n > 10);`? Surely `Where().First()` would be much slower. – Igby Largeman Aug 23 '11 at 18:25
  • @Charles: Actually, because how LINQ to Objects is implemented, it won't make much of a difference. Nothing is actually done in the call to `Where` until something calls for an object, which will be when `First` is called. Then, *one and only one* element is processed in the `Where` call, and since `First` will never ask again, `Where` will never be called again and never even try to filter any more results. – Tomas Aschan Aug 23 '11 at 22:58
  • @Charles: Jon Skeet has a very educational (and quite lengthy) blog series about reimplementing LINQ to Objects here: http://msmvps.com/blogs/jon_skeet/archive/tags/Edulinq/default.aspx Definitely worth a look! – Tomas Aschan Aug 24 '11 at 13:03
  • Hey that's cool - thanks. Just to be clear, _is_ there actually a reason to use `Where(n => n > 10).First()` instead of `First(n => n > 10)`? – Igby Largeman Aug 24 '11 at 18:32
8

Instead of Find you could use FindIndex. It returns the index of the element found or -1 if no element was found.

http://msdn.microsoft.com/en-us/library/x1xzf2ca%28v=VS.80%29.aspx

sunside
  • 8,069
  • 9
  • 51
  • 74
3

It is ok if you know there are no default(T) values in your list or if the default(T) return value can't be the result.

You could implement your own easily.

public static T Find<T>(this List<T> list, Predicate<T> match, out bool found)
{
    found = false;

  for (int i = 0; i < list.Count; i++)
  {
    if (match(list[i]))
    {
            found = true;
      return list[i];
    }
  }
  return default(T);
}

and in code:

bool found;
a.Find(x => x > 5, out found);

Other options:

evens.First(predicate);//throws exception
evens.FindAll(predicate);//returns a list of results => use .Count.

It depends on what version of framework you can use.

Jaroslav Jandek
  • 9,463
  • 1
  • 28
  • 30
3

If there is a problem with the default value of T, use Nullable to get a more meaningful default value :

List<int?> evens = new List<int?> { 0, 2, 4, 6, 8 };
var greaterThan10 = evens.Find(c => c > 10);
if (greaterThan10 != null) 
{
    // ...
}

This also requires no additional call of Exists() first.

Ronald
  • 111
  • 2
2

Doing a call to Exists() first will help with the problem:

int? zz = null;
if (evens.Exists(c => c > 10))
    zz = evens.Find(c => c > 10);

if (zz.HasValue)
{
    // .... etc ....
}

slightly more long-winded, but does the job.

slugster
  • 49,403
  • 14
  • 95
  • 145
  • I had to do this option, none of the other options appeared in C# 4.6 / Visual Studio 2017... Yes, it searches twice, but fixed my problem until I find a better method, thanks! – Billy Willoughby Feb 26 '18 at 16:00
2

The same answer I gave on Reddit.

Enumerable.FirstOrDefault( predicate )
leppie
  • 115,091
  • 17
  • 196
  • 297
1

Inspired by Jaroslav Jandek above I would make an extension that returns bool true if match and passes the object in out if match:

static class Extension
{
    public static bool TryFind<T>(this List<T> list, Predicate<T> match, out T founditem)
    {


        for (int i = 0; i < list.Count; i++)
        {
            if (match(list[i]))
            {
                founditem = list[i];
                return true;
            }
        }

        founditem = default(T);
        return false;
    }
}

Then it can used on a List object 'a' and called like this using 'out var' syntax:

if (a.TryFind(x => x > 5, out var founditem)){
    //enter code here to use 'founditem'
};
flodis
  • 1,123
  • 13
  • 9
0

Just for fun

evens.Where(c => c > 10)
    .Select(c => (int?)c)
    .DefaultIfEmpty(null)
    .First();
Julien Roncaglia
  • 17,397
  • 4
  • 57
  • 75