4
\$\begingroup\$

Here is a static class with some helper methods for some common LINQ operations on enumerables, built against .NET Framework 2.0. This class is part of the xofz.Core98 library.

Edit: a full review is not necessary. Any limitations or irritations discovered would be much appreciated. The code here is provided free for your use either via the library or by copy and pasting it into your own project.

namespace xofz
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Reflection;

    public static class EnumerableHelpers
    {
        public static IEnumerable<TResult> Select<T, TResult>(
            IEnumerable<T> source,
            Func<T, TResult> selector)
        {
            if (source == default(IEnumerable<T>))
            {
                yield break;
            }

            if (selector == default(Func<T, TResult>))
            {
                yield break;
            }

            foreach (var item in source)
            {
                yield return selector(item);
            }
        }

        public static IEnumerable<TResult> SelectMany<T, TResult>(
            IEnumerable<T> source,
            Func<T, IEnumerable<TResult>> selector)
        {
            if (source == default(IEnumerable<T>))
            {
                yield break;
            }

            if (selector == default(Func<T, IEnumerable<TResult>>))
            {
                yield break;
            }

            foreach (var item in source)
            {
                foreach (var selectee in selector(item))
                {
                    yield return selectee;
                }
            }
        }

        public static IEnumerable<T> Where<T>(
            IEnumerable<T> source,
            Func<T, bool> predicate)
        {
            if (source == default(IEnumerable<T>))
            {
                yield break;
            }

            if (predicate == default(Func<T, bool>))
            {
                yield break;
            }

            foreach (var item in source)
            {
                if (predicate(item))
                {
                    yield return item;
                }
            }
        }

        public static IEnumerable<T> Skip<T>(
            IEnumerable<T> source,
            int numberToSkip)
        {
            if (source == default(IEnumerable<T>))
            {
                yield break;
            }

            var currentIndex = 0;
            foreach (var item in source)
            {
                ++currentIndex;
                if (currentIndex > numberToSkip)
                {
                    yield return item;
                }
            }
        }

        public static T First<T>(
            IEnumerable<T> source)
        {
            if (source == default(IEnumerable<T>))
            {
                throw new InvalidOperationException(
                    "The enumerable is null and therefore "
                    + "does not have a first item.  If this can happen, "
                    + "consider using FirstOrDefault<T>()");
            }

            foreach (var item in source)
            {
                return item;
            }

            throw new InvalidOperationException(
                "The enumerable is empty and therefore "
                + "does not have a first item.  If this can happen, "
                + "consider using FirstOrDefault<T>()");
        }

        public static T First<T>(
            IEnumerable<T> source,
            Func<T, bool> predicate)
        {
            if (source == default(IEnumerable<T>))
            {
                throw new InvalidOperationException(
                    "The enumerable is null and therefore "
                    + "does not have a first item.  If this can happen, "
                    + "consider using FirstOrDefault<T>()");
            }

            var empty = true;
            foreach (var item in source)
            {
                empty = false;
                if (predicate(item))
                {
                    return item;
                }
            }

            if (empty)
            {
                throw new InvalidOperationException(
                    "The enumerable is empty and therefore "
                    + "does not have a first item.  If this can happen, "
                    + "consider using FirstOrDefault<T>()");
            }

            throw new InvalidOperationException(
                "The non-empty enumerable did not have any elements "
                + "which matched the predicate.");
        }

        public static T FirstOrDefault<T>(
            IEnumerable<T> source)
        {
            if (source == default(IEnumerable<T>))
            {
                return default(T);
            }

            foreach (var item in source)
            {
                return item;
            }

            return default(T);
        }

        public static T FirstOrDefault<T>(
            IEnumerable<T> source,
            Func<T, bool> predicate)
        {
            if (source == default(IEnumerable<T>))
            {
                return default(T);
            }

            foreach (var item in source)
            {
                if (predicate(item))
                {
                    return item;
                }
            }

            return default(T);
        }

        public static bool Any<T>(
            IEnumerable<T> source)
        {
            if (source == null)
            {
                return false;
            }

            foreach (var item in source)
            {
                return true;
            }

            return false;
        }

        public static bool Any<T>(
            IEnumerable<T> source,
            Func<T, bool> predicate)
        {
            if (source == null)
            {
                return false;
            }

            foreach (var item in source)
            {
                if (predicate(item))
                {
                    return true;
                }
            }

            return false;
        }

        public static bool All<T>(
            IEnumerable<T> source,
            Func<T, bool> predicate)
        {
            if (source == null)
            {
                return true;
            }

            foreach (var item in source)
            {
                if (!predicate(item))
                {
                    return false;
                }
            }

            return true;
        }

        public static bool Contains<T>(
            IEnumerable<T> source,
            T item)
        {
            if (source == default(IEnumerable<T>))
            {
                return false;
            }

            var itemIsNull = item == null;
            foreach (var itemInSource in source)
            {
                if (itemInSource == null && itemIsNull)
                {
                    return true;
                }

                if (item?.Equals(itemInSource) ?? false)
                {
                    return true;
                }
            }

            return false;
        }

        public static IEnumerable<T> Cast<T>(
            IEnumerable source)
        {
            if (source == null)
            {
                yield break;
            }

            foreach (var item in source)
            {
                yield return (T)item;
            }
        }

        public static int Count<T>(
            IEnumerable<T> source)
        {
            if (source == default(IEnumerable<T>))
            {
                return default(int);
            }

            var totalCount = 0;
            foreach (var item in source)
            {
                ++totalCount;
            }

            return totalCount;
        }

        public static int Count<T>(
            IEnumerable<T> source,
            Func<T, bool> predicate)
        {
            if (source == default(IEnumerable<T>))
            {
                return default(int);
            }

            var totalCount = 0;
            foreach (var item in source)
            {
                if (predicate(item))
                {
                    ++totalCount;
                }
            }

            return totalCount;
        }

        public static long LongCount<T>(
            IEnumerable<T> source)
        {
            if (source == default(IEnumerable<T>))
            {
                return default(int);
            }

            long totalCount = 0;
            foreach (var item in source)
            {
                ++totalCount;
            }

            return totalCount;
        }

        public static long LongCount<T>(
            IEnumerable<T> source,
            Func<T, bool> predicate)
        {
            if (source == default(IEnumerable<T>))
            {
                return default(int);
            }

            long totalCount = 0;
            foreach (var item in source)
            {
                if (predicate(item))
                {
                    ++totalCount;
                }
            }

            return totalCount;
        }

        public static T[] ToArray<T>(IEnumerable<T> source)
        {
            if (source == default(IEnumerable<T>))
            {
                return new T[0];
            }

            var ll = new LinkedList<T>();
            foreach (var item in source)
            {
                ll.AddLast(item);
            }

            var array = new T[ll.Count];
            ll.CopyTo(array, 0);

            return array;
        }

        public static List<T> ToList<T>(
            IEnumerable<T> source)
        {
            return new List<T>(source);
        }

        public static ICollection<T> OrderBy<T, TKey>(
            IEnumerable<T> source,
            Func<T, TKey> keySelector)
        {
            return orderBy(
                source,
                keySelector,
                Comparer<TKey>.Default,
                false);
        }

        public static ICollection<T> OrderBy<T, TKey>(
            IEnumerable<T> source,
            Func<T, TKey> keySelector,
            IComparer<TKey> comparer)
        {
            return orderBy(
                source,
                keySelector,
                comparer,
                false);
        }

        public static ICollection<T> OrderByDescending<T, TKey>(
            IEnumerable<T> source,
            Func<T, TKey> keySelector)
        {
            return orderBy(
                source,
                keySelector,
                Comparer<TKey>.Default,
                true);
        }

        public static ICollection<T> OrderByDescending<T, TKey>(
            IEnumerable<T> source,
            Func<T, TKey> keySelector,
            IComparer<TKey> comparer)
        {
            return orderBy(
                source,
                keySelector,
                comparer,
                true);
        }

        private static ICollection<T> orderBy<T, TKey>(
            IEnumerable<T> source,
            Func<T, TKey> keySelector,
            IComparer<TKey> comparer,
            bool descending)
        {
            if (source == default(IEnumerable<T>))
            {
                return new List<T>();
            }

            if (keySelector == default(Func<T, TKey>))
            {
                return new List<T>();
            }

            var d = new Dictionary<TKey, IList<T>>();
            var itemsWithNullKeys = new LinkedList<T>();

            foreach (var item in source)
            {
                var key = keySelector(item);
                if (key == null)
                {
                    itemsWithNullKeys.AddLast(item);
                    continue;
                }

                if (!d.ContainsKey(key))
                {
                    d.Add(key, new List<T>());
                }

                d[key].Add(item);
            }

            var keyList = new List<TKey>(d.Keys);
            keyList.Sort(comparer);

            if (descending)
            {
                keyList.Reverse();
            }

            var finalList = new List<T>();
            foreach (var key in keyList)
            {
                finalList.AddRange(d[key]);
            }
            finalList.AddRange(itemsWithNullKeys);

            return finalList;
        }

        public static TEnd Aggregate<T, TEnd>(
            IEnumerable<T> source,
            TEnd seed,
            Func<TEnd, T, TEnd> accumulator)
        {
            if (source == default(IEnumerable<T>))
            {
                return default(TEnd);
            }

            if (accumulator == default(Func<TEnd, T, TEnd>))
            {
                return default(TEnd);
            }

            var end = seed;
            foreach (var item in source)
            {
                end = accumulator(end, item);
            }

            return end;
        }

        public static IEnumerable<T> OfType<T>(IEnumerable source)
        {
            if (source == null)
            {
                yield break;
            }

            foreach (var item in source)
            {
                if (item is T t)
                {
                    yield return t;
                }
            }
        }

        public static IEnumerable<T> SafeForEach<T>(IEnumerable<T> source)
        {
            if (source == default(IEnumerable<T>))
            {
                yield break;
            }

            foreach (var item in source)
            {
                yield return item;
            }
        }

        public static IEnumerable<T> Iterate<T>(params T[] items)
        {
            foreach (var item in items)
            {
                yield return item;
            }
        }

        public static IEnumerable<T> PrivateFieldsOfType<T>(
            object o)
        {
            if (o == null)
            {
                yield break;
            }

            foreach (var fieldInfo in o.GetType().GetFields(
                BindingFlags.Instance | BindingFlags.NonPublic))
            {
                var value = fieldInfo.GetValue(o);
                if (value is T t)
                {
                    yield return t;
                }
            }
        }

        public static int Sum<T>(
            IEnumerable<T> source,
            Func<T, int> valueComputer)
        {
            if (source == null)
            {
                return 0;
            }

            var sum = 0;
            foreach (var item in source)
            {
                checked
                {
                    sum += valueComputer(item);
                }
            }

            return sum;
        }

        public static long Sum<T>(
            IEnumerable<T> source,
            Func<T, long> valueComputer)
        {
            if (source == null)
            {
                return 0;
            }

            long sum = 0;
            foreach (var item in source)
            {
                checked
                {
                    sum += valueComputer(item);
                }
            }

            return sum;
        }

        public static int Min(
            IEnumerable<int> source)
        {
            if (source == null)
            {
                return 0;
            }

            var min = int.MaxValue;
            var minChanged = false;
            foreach (var item in source)
            {
                minChanged = true;
                if (item < min)
                {
                    min = item;
                }
            }

            if (!minChanged)
            {
                return 0;
            }

            return min;
        }

        public static long Min(
            IEnumerable<long> source)
        {
            if (source == null)
            {
                return 0;
            }

            var min = long.MaxValue;
            var minChanged = false;
            foreach (var item in source)
            {
                minChanged = true;
                if (item < min)
                {
                    min = item;
                }
            }

            if (!minChanged)
            {
                return 0;
            }

            return min;
        }

        public static int Max(
            IEnumerable<int> source)
        {
            if (source == null)
            {
                return 0;
            }

            var max = 0;
            foreach (var item in source)
            {
                if (item > max)
                {
                    max = item;
                }
            }

            return max;
        }

        public static long Max(
            IEnumerable<long> source)
        {
            if (source == null)
            {
                return 0;
            }

            long max = 0;
            foreach (var item in source)
            {
                if (item > max)
                {
                    max = item;
                }
            }

            return max;
        }

        public static ICollection<T> Reverse<T>(
            IEnumerable<T> source)
        {
            var ll = new LinkedList<T>();
            foreach (var item in source)
            {
                ll.AddFirst(item);
            }

            return ll;
        }
    }
}
\$\endgroup\$
  • 4
    \$\begingroup\$ This is a lot of code and none description at all. Could you write something more about it? What is so special about your code? What is your code doing and why? What is it solving? Give us something more please ;-) You don't what a review that is similar to your question, do you? It could go like that: there are some improvements possible in some places. :-( \$\endgroup\$ – t3chb0t Aug 10 '18 at 17:55
  • \$\begingroup\$ I just wanted to provide some free LINQ-like code for those that are still developing for .NET Framework 2.0. A full review not necessary; just any pointers or irritations people notice would be nice. If this question continues to receive downvotes, I will respectfully delete it. \$\endgroup\$ – xofz Aug 10 '18 at 20:17
  • 1
    \$\begingroup\$ "for those that are still developing for .NET Framework 2.0" I'd hope that in 2018 people aren't stuck developing on a 13 year old framework. \$\endgroup\$ – BCdotWEB Aug 14 '18 at 13:56
  • 1
    \$\begingroup\$ This can only end in madness: softwareengineering.stackexchange.com/a/211932/987 \$\endgroup\$ – Jesse C. Slicer Aug 14 '18 at 18:50
8
\$\begingroup\$

These are (to me) unexpected:

if (source == default(IEnumerable<T>))
{
    yield break;
}

if (selector == default(Func<T, IEnumerable<TResult>>))
{
    yield break;
}

LINQ itself crashes helpfully if you pass null to Select and other such methods (including FirstOrDefault). Your API would make me feel uneasy (because this sort of misplaced null is never fun to debug) and would go against expectations if you are trying to mimic LINQ directly. I'd much prefer an ArgumentException here. At the very least, this behaviour should be clearly documented with inline documentation (///).

ToList behaves differently, as new List<T>(null) throws unlike the rest of your methods. Aggregate is also weird in this regard. An empty source would give you seed, but null gives you default(TEnd). Min is weird also: it should defintiely crash, since the Min of an empty list is ill-defined, let alone the min of a non-existance list: you should cover these cases explicitly, and throw (or rather, that's what LINQ does, and that's what I'd expect).

It's also a bit odd that you compare to default(IEnumerable<T>) in some places, and null in others. null is sufficient, since it's an interface.


Regarding Contains, LINQ doesn't do this, but I'd personally appreciate a signature like

public static bool Contains<T>(IEnumerable<T> source, T item) where T : IEquatable<T>

LINQ no-doubt uses the default EquailtyComparer which will 'pick' an efficient comparison where possible, and something you might consider, since passing structs to Equals(object) incurs boxing.


In LongCount, this is a little odd:

return default(int);

Just return 0. It would be so much clearer what you are doing. However, returning 0 for a null source again makes no sense.


In ToArray: I wouldn't use LinkedList<T> here, because it's usually slower than a List<T>, and just has a less pleasant interface. In fact, you'd probably be better of just returning ToList(source).ToArray(). You might consider caching an empty array or using Array.Empty<T>() to handle the special case of an empty source, but again I'd suggest throwing if source is null.

As a general rule, I wouldn't use LinkedList<T> unless I was depending upon efficient insertion or removal at the start or in the middle with order retention.

The same in Reverse<T>. I'd use a Stack<T>: it's literally perfect for this job. (see Henrik Hansen's answer for a perfectly good example, which again will throw if source is null)


PrivateFieldsOfType<T> seems a little out-of-place here! It's also not clear that it's only meant to yield instance fields, and various other things that the specific BindingFlags imply. I'd argue that this method is useless without documentation or access to the code, since as a consumer I have no real idea what it is doing.


You have no Max<T> or Min<T> taking some sort of comparable: this limits their utility rather. Having such a method would save you needing separate implementations for int and long.

Also, integers can be negative, so initialising max = 0 is a problem (should be int.MinValue).


Saved until last, because it would be effort, but I'd be inclined to use a Heap to implement OrderBy, because a well-implemented Binary Heap can be built in linear time, so you only pay the log-linear cost for elements you pull (i.e. if you only need the first 10 elements, you don't have to 'completely' sort the other (n-10)).

OrderBy also puts the null key elements at the end for both ascending and descending sort. I don't know if this is intentional; I'd expect the comparer to handle nulls for me, and if it can't, then crashing would be nice.

\$\endgroup\$
5
\$\begingroup\$

I'll just add to what VisualMelon already has pointed out:

1)

Stack<T> is build for the reverse method:

public static IEnumerable<T> Reverse<T>(IEnumerable<T> source)
{
  return new Stack<T>(source);
}

or if you want a more LINQ-like lazyness you can do it this way:

public static IEnumerable<T> Reverse<T>(IEnumerable<T> source)
{
  Stack<T> rev = new Stack<T>(source);

  while (rev.Count > 0)
  {
    yield return rev.Pop();
  }
} 

2)

Max<T> and Min<T> can be implemented in a general manner like this (Max as example):

public static T Max<T>(IEnumerable<T> source, IComparer<T> comparer)
{
  if (comparer == null)
    throw new ArgumentNullException(nameof(comparer));

  return Max(source, comparer.Compare);
}

public static T Max<T>(IEnumerable<T> source, Compare<T> comparer)
{
  if (source == null)
    throw new ArgumentNullException(nameof(source));
  if (comparer == null)
    throw new ArgumentNullException(nameof(comparer));

  using (IEnumerator<T> enumerator = source.GetEnumerator())
  {
    if (enumerator.MoveNext())
    {
      T max = enumerator.Current;

      while (enumerator.MoveNext())
      {
        T current = enumerator.Current;
        if (comparer(max, current) < 0)
        {
          max = current;
        }
      }

      return max;
    }
  }

  throw new ArgumentException("The source vector has no entries", nameof(source));
}

Where Compare<T> is a delegate on the form:

public delegate int Compare<T>(T x, T y);

3)

I don't like that OrderBy(...) returns a different kind of type than the input. It should be an IEnumerable<T>.

Beside that, the normal LINQ-implementation puts the null-values at the start in the ascending ordered result.

With a special implementation of IComparer<..> you can take full advantage of List<T>.Sort() if you implement private IEnumerable<> OrderBy(...) like this:

class OrderByComparer<T, TKey> : IComparer<T>
{
  Func<T, TKey> _keySelector;
  IComparer<TKey> _keyComparer;

  public OrderByComparer(Func<T, TKey> keySelector, IComparer<TKey> keyComparer)
  {
    _keySelector = keySelector;
    _keyComparer = keyComparer;
  }

  public int Compare(T x, T y)
  {
    return _keyComparer.Compare(_keySelector(x), _keySelector(y));
  }
}

private static IEnumerable<T> OrderBy<T, TKey>(IEnumerable<T> source, Func<T, TKey> keySelector, IComparer<TKey> comparer, bool descending)
{
  if (source == default(IEnumerable<T>))
    throw new ArgumentNullException(nameof(source));
  if (keySelector == default(Func<T, TKey>))
    throw new InvalidOperationException(nameof(keySelector));
  if (comparer == null)
    throw new InvalidOperationException(nameof(comparer));

  OrderByComparer<T, TKey> keyComparer = new OrderByComparer<T, TKey>(keySelector, comparer);
  List<T> data = new List<T>(source);
  data.Sort(keyComparer);


  foreach (T item in data)
  {
    yield return item;
  }
}

With a vector of 1 million ints it's about twice a fast as the original.

\$\endgroup\$
  • \$\begingroup\$ This is a nice trick with the stack... I think you could just return it without the helper variable. Althogh it has one drawback. It's eager whereas linq is lazy so this API isn't consitant with the others. \$\endgroup\$ – t3chb0t Aug 11 '18 at 14:48
  • \$\begingroup\$ @t3chb0t: You are as usual right... I'll edit \$\endgroup\$ – Henrik Hansen Aug 11 '18 at 14:49
  • \$\begingroup\$ Max implementation is a great example. @t3chb0t you can't lazily reverse a lazy IEnumerable ;) you'd have to fast-path IReadonlyList and such to achieve that (where it's possible) \$\endgroup\$ – VisualMelon Aug 11 '18 at 15:15
  • 1
    \$\begingroup\$ @VisualMelon no, you cannot but you can prevent enumerating the source that you are going to reverse until really necessary. The first implementation doesn't do it and the stack pulls all the items from the source right away. You can check it in linqpad with var reversed = source.Select(x => x.Dump()).Reverse(); - the first one will dump all the items and the second one not. \$\endgroup\$ – t3chb0t Aug 11 '18 at 15:25
  • \$\begingroup\$ @t3chb0t sorry, I see what you mean now. Yes, you're absolutely right. \$\endgroup\$ – VisualMelon Aug 11 '18 at 15:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.