OMG, A lambda expression like this in C# has such a Bug

Zack Yang
2 min readDec 6, 2022

Today, a friend told me that he had encountered a strange bug in a very simple piece of C# code, which was to find a random item from a List as follows:

var random = new Random();
var users = Enumerable.Range(0, 10).Select(p => new User(p, "A" + p)).ToList();
var user = users.Find(p => p.Id == random.Next(0, 10));
Debug.Assert(user != null);

record User(int Id,string Name);

The second line generates a list of 10 Users with ids ranging from 0 to 9. In line 3, we call the Find method of the List to find a piece of data based on the lambda expression. In this case, we use random.Next () to get a random number between [0,10), and then compare that random number to the Id. It logically follows that Find will find a piece of data, so the assertion that the user is not null in line 4 will sometimes work and sometimes fail, causing the program to throw an exception, which is confusing.

Of course, his code is overly complex, users[random.Next(0, 10)] would be simple and efficient. But to reveal the nature of the problem, let me go ahead and analyze why using Find+lambda is causing the problems.

Let’s check the source code for the Find method, as follows:

public T? Find(Predicate<T> match)
{
for (int i = 0; i < _size; i++)
{
if (match(_items[i]))//attention
{
return _items[i];
}
}
return default;
}

The logic of the Find method is simple; it iterates through the List, calls the delegate ‘match’ for each item, and checks whether the current item matches the condition, and if it finds one, it returns it. If it doesn’t find any, it returns a default value (for example, null). This logic is so simple that you don’t see any problems.

The key is the “if (match(_items[i]))” statement. It calls the match delegate each time through the loop to see if the current data match. The body of the delegate pointed to by delegate “match” is “p => p Id == random.next (0, 10)”, which means that each match gets a new random number to compare against. Suppose the 10 random numbers generated during the loop are: 9,8,8,7,9,1,1,2,3,4, then the match(_items[i]) test will always be false, resulting in null being returned, which means no data will be found.

With this in mind, the idea is to avoid generating random numbers to be compared in the lambda, and instead generate them ahead of time, as follows:

int randId = random.Next(0, 10);
var user = users.Find(p => p.Id == randId);

The same principle applies to LINQ operations such as Single() and Where(). It’s also important to avoid complex computations in lambda expressions, which not only avoids bugs like the ones mentioned in this post, but also makes your program more efficient.

--

--