tags:

views:

206

answers:

7

Dear SO,
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?

+11  A: 

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 Lycken
and what does the poor soul running .net 2.0 have to do?
Galilyou
@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
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
@Winston Smith: It is a hack. It is not supported officially. It violates the user licence.
Jaroslav Jandek
+2  A: 

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
A: 

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
Exists then Find? That's searching twice, right?
Galilyou
@Galilyou: Yes.
Jaroslav Jandek
+3  A: 

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

Markus
I missed the FindIndex in the original post ... nevermind. :)
Markus
+1  A: 

The same answer I gave on Reddit.

Enumerable.FirstOrDefault( predicate )
leppie
It is exactly what the OP is trying to avoid.
Jaroslav Jandek
A: 

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
A: 

Just for fun

evens.Where(c => c > 10)
    .Select(c => (int?)c)
    .DefaultIfEmpty(null)
    .First();
VirtualBlackFox