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.

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(this IEnumerable 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.