SingleOrDefault – Safe

On the other day I ran into a task to return the only one element of a list/enumeration if exists, and I had to do it in a performant way. The list or enumeration could have any number of not null items. Using FirstOrDefault is not an option as it does not restrict having multiple items in the list. SingleOrDefault could be a choice, but it throws an exception in the case of multiple items. Handling the exception was not an option.

One solution could be to call Count and only return the first item if the value is one. The problem with this solutions is enumerating through the items to get the count for enumerable types. A better solution is to write a new SingleOrDefault extension methods to handle list and enumeration types.

lifeguard-338128_960_720

Implementation

The following method is an extension of IEnumerable<T>


public static TSource SafeSingleOrDefault<TSource>(this IEnumerable<TSource> source)
{
  if(source == null)
  {
    throw new ArgumentNullException(nameof(source));
  }
  using(var enumerator = source.GetEnumerator())
  {
    if(!enumerator.MoveNext())
    {
      return default(TSource);
    }
    var first = enumerator.Current;
    if(!enumerator.MoveNext())
    {
      return first;
    }
    else
    {
      return default(TSource);
    }
  }
}

And one for IList<T>


public static TSource SafeSingleOrDefaul<TSource>(this IList<TSource> source)
{
  if(source == null)
  {
    throw new ArgumentNullException(nameof(source));
  }
  if(source.Count != 1)
  {
    return default(TSource);
  }
  else
  {
    return source[0];
  }
}

Both will run faster and slower depending to the use-case compared to the one implemented in the framework.

If you are using it only in well-known places, where you know that the compiler can bind the right method you will have a better performance, otherwise I suggest writing an extension method similar to SingleOrDefault, where the runtime type of the enumerable is tested against IList<T> first to decide to use Count or GetEnumerator (I also suggest the same when working with a larger team, so people can have the same expectation as the framework expectation)

In my case I only use the enumerable version of SafeSingleOrDefault<TSource>(this IEnumerable<TSource> source)


Performance testing

To add some context on the performance, I ran all these implementation against Lists and Enumerations and List as Enumeration. My test machine is a VM in the cloud, having 7GB Ram and Intel Xeon 2.4GHz processor. I ran the methods under test against


List<int> list = new List<int>() { 2 };
IEnumerable<int> trueEnumerable = list.Where(x => x > 1);
IEnumerable<int> listAsEnumerable = list.AsEnumerable<int>();

Tests ran in 5 rounds, each round 10,000,000 times, and output is the sum of elapsed time for each 5 test. The final result shows the average.


Algorithmically, I would consider all implementations as O(1), because even for the enumerable we need to move enumerator at most of a constant 2 times. While, in case of list, the count is given, and indexing an item is constant.

Single Item List Enumerable List as Enumerable
SafeSOD(IEnumerable) 930,6 681,2
SafeSOD(IList) 130,6
Framework SOD 288,0 1042,6 299,6
SafeSOD (test for IList) 294,8 1042,0 297,0

When Exception is thrown by the Framework implementation:

Multiple Items List Enumerable List as Enumerable
SafeSOD(IEnumerable) 888,4 648,2
SafeSOD(IList) 78,0
Framework SOD [too long] [too long] [too long]
SafeSOD (test wof IList) 732,2 1004,4 735,0

Explanation

  • SOD stands for SingleOrDefault
  • All numbers are represented in milliseconds
  • Framework SOD for multiple items took too long because of the exceptions thrown and caught
  • Cell without any value is because compiler matched the other method by type

Conclusion

Always very carefully consider the use-case to be solved as small modifications can result considerable performance improvement.

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s