Click here to Skip to main content
15,881,852 members
Articles / Programming Languages / C#
Tip/Trick

Deferred Sorting of Collections

Rate me:
Please Sign up or sign in to vote.
3.93/5 (7 votes)
7 Sep 2017CPOL1 min read 11.8K   3   8
Recently I faced a problem of storing sorting information for later use. Sorting is easy if you already have a collection to sort. Just use LINQ extension methods. But what if you don't have the collection yet? What if you'll get the collection later, but you need to store sorting rules now?

Background

Let's say, we need to sort a collection of simple 'Person' class:

C#
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

If we have a collection, it is easy to apply any sorting:

C#
IEnumerable<Person> people = new List<Person>();

var sortedPeople = people.OrderBy(p => p.Name).ThenByDescending(p => p.Age);

But what if we don't have a collection yet? I want to have the following class Sorter:

C#
public enum SortingDirection
{
    Ascending,
    Descending
}

public class Sorter
{
    public void AddSortingExpression<T>(Func<Person, T> keySelector, SortingDirection direction)
    {
        ...
    }

    public IEnumerable<Person> Sort(IEnumerable<Person> people)
    {
        ...
    }
}

And I want to use it like this:

C#
var sorter = new Sorter();
sorter.AddSortingExpression(p => p.Name, SortingDirection.Ascending);
sorter.AddSortingExpression(p => p.Age, SortingDirection.Descending);

var sortedPeople = sorter.Sort(people);

How should the Sorter class be implemented?

Solution 1

One possible solution is in dynamic creation of function, which will actually implement sorting:

C#
public class Sorter
{
    private Func<IEnumerable<Person>, IOrderedEnumerable<Person>> _sortingFunction;

    public void AddSortingExpression<T>(Func<Person, T> keySelector, SortingDirection direction)
    {
        if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));

        switch (direction)
        {
            case SortingDirection.Ascending:
                if (_sortingFunction == null)
                {
                    _sortingFunction = people => people.OrderBy(keySelector);
                }
                else
                {
                    var oldSorter = _sortingFunction;
                    _sortingFunction = people => oldSorter(people).ThenBy(keySelector);
                }
                break;
            case SortingDirection.Descending:
                if (_sortingFunction == null)
                {
                    _sortingFunction = people => people.OrderByDescending(keySelector);
                }
                else
                {
                    var oldSorter = _sortingFunction;
                    _sortingFunction = people => oldSorter(people).ThenByDescending(keySelector);
                }
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(direction), 
                                 direction, "Unknown sorting direction");
        }
    }

    public IEnumerable<Person> Sort(IEnumerable<Person> people)
    {
        if (_sortingFunction == null)
            return people;

        return _sortingFunction(people).ToArray();
    }
}

Here we construct sorting function (field _sortingFunction). On each invocation of AddSortingExpression method, we change this function using its previous value. I would like to pay your attention to the usage of 'oldSorter' variable. We can't use the following expression:

C#
_sortingFunction = people => _sortingFunction(people).ThenByDescending(keySelector);

because it will lead to the endless recurrent calls of the same function. This is why we store the current value of sorting function into a temporary variable.

Solution 2

Another possible solution uses the fact, that LINQ offers lazy evaluation:

C#
public class Sorter
{
    private class InternalEnumerable : IEnumerable<Person>
    {
        public IEnumerable<Person> People { get; set; } = Enumerable.Empty<Person>();

        public IEnumerator<Person> GetEnumerator()
        {
            return People.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

    private readonly InternalEnumerable _initialEnumerable = new InternalEnumerable();

    private IOrderedEnumerable<Person> _sortedPeople;

    public void AddSortingExpression<T>(Func<Person, T> keySelector, SortingDirection direction)
    {
        if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));

        switch (direction)
        {
            case SortingDirection.Ascending:
                if (_sortedPeople == null)
                {
                    _sortedPeople = _initialEnumerable.OrderBy(keySelector);
                }
                else
                {
                    _sortedPeople = _sortedPeople.ThenBy(keySelector);
                }
                break;
            case SortingDirection.Descending:
                if (_sortedPeople == null)
                {
                    _sortedPeople = _initialEnumerable.OrderByDescending(keySelector);
                }
                else
                {
                    _sortedPeople = _sortedPeople.ThenByDescending(keySelector);
                }
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(direction), 
                                    direction, "Unknown sorting direction");
        }
    }

    public IEnumerable<Person> Sort(IEnumerable<Person> people)
    {
        if (_sortedPeople == null)
            return people;

        _initialEnumerable.People = people;
        return _sortedPeople.ToArray();
    }
}

Here, we always start from the instance of InternalEnumerable class, implementing IEnumerable<Person>. So it looks like we have our collection to sort from the beginning. But in the Sort method, we change the actual collection, which the instance of this class returns.

I hope this small tip will be useful for you in your programs.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Finstek
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Question[My vote of 2] Isn't it simpler to store the delegates ? Pin
Member 112713588-Sep-17 2:07
Member 112713588-Sep-17 2:07 
AnswerRe: [My vote of 2] Isn't it simpler to store the delegates ? Pin
Ivan Yakimov10-Sep-17 23:29
professionalIvan Yakimov10-Sep-17 23:29 
GeneralMy vote of 3 Pin
Graeme_Grant7-Sep-17 13:11
mvaGraeme_Grant7-Sep-17 13:11 
QuestionLinq compile expressions Pin
Denis Ibragimov7-Sep-17 5:53
professionalDenis Ibragimov7-Sep-17 5:53 
AnswerRe: Linq compile expressions Pin
Ivan Yakimov10-Sep-17 23:00
professionalIvan Yakimov10-Sep-17 23:00 
QuestionQuestion Pin
FatCatProgrammer7-Sep-17 4:26
FatCatProgrammer7-Sep-17 4:26 
AnswerRe: Question Pin
Ivan Yakimov7-Sep-17 5:09
professionalIvan Yakimov7-Sep-17 5:09 
GeneralRe: Question Pin
FatCatProgrammer7-Sep-17 5:58
FatCatProgrammer7-Sep-17 5:58 
that's a strange use case
Relativity

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.