Every one of us has used the sorting in our applications. Rarely the sorting is implemented by ourselves. In most of the cases the IComparer<T> interface and TComparison<T> functions are used.
To sort a collection the following syntax is used:
FCollection.Sort(TComparer<TMyType>.Construct( function (const L, R: TMyType): Integer begin Result := L.Value – R.Value; end));
If you are using the Spring4d framework you can omit the comparer construction and just throw the comparison function directly. Nothing to improve, right?
When it comes to multi-field sorting (ordering) it is also simple and usually done like following:
FCollection.Sort(TComparer<TMyType>.Construct( function (const L, R: TMyType): Integer begin Result := L.Value1 – R.Value1; if Result = 0 then Result := L.Value2 – R.Value2; end));
What I do not like from readability point of view is the following things:
- You write code to get value for the left/right operand twice (once for left, once for right);
- You need to care about “comparer – specific” business with integer comparison result;
- For nested sorting, you need to add the nested IF statements.
Ordering like a pro
Lets sort the list of record by 3 fields now (by ParentID field descending, then by Name case insensitive and then by ID).
type TRec = record ID, ParentID: Integer; Name: String; end; var LList: IList<TRec>;
So instead of the following code:
LList.Sort( function (const L, R: TRec): Integer begin Result := R.ParentID - L.ParentID; if Result = 0 then begin Result := CompareText(L.Name, R.Name); if Result = 0 then Result := L.ID - R.ID; end; end);
we will have the following code:
LList.Sort(TComparerEx<TRec>.Create .OrderByDesc( function (const A: TRec): Integer begin Result := A.ParentID; end) .OrderBy( function (const A: TRec): String begin Result := A.Name; end) .OrderBy( function (const A: TRec): Integer begin Result := A.ID; end));
And the TComparerEx<T> class implements the IComparerEx<T> interface which descends from the standard IComparer<T>. Each call to the OrderBy constructs a copy of the comparer so that the source instance is not affected in case if you are passing the comparer to the outer world and want to append comparisons there. You can also use Ascending and Descending methods to get a new instance of the comparer with changed direction of the last comparison.
When you use the extended comparer, you get following advantages:
- Better readable code as there is no comparer-specific code (in most cases);
- More transparent for the complex types;
- You write the value retrieve/lookup only once and not twice;
- Unit tests are much easier for the standard data types as there is no branching and you do not need to test the TComparerEx<T> itself every time.
What you lose:
- Some kind of compactness in a line count (subjectively negative);
- Performance drop – not noticeable unless you sort multiple hundreds of thousands of records by 3 fields (for 600’000 records of TRec it is 1050ms vs 440ms when using standard comparison with nested IFs).
The source code for the TComparerEx is available on GitHub. You can use it on your own risk, modify it as you want – I do not give you any guarantees on that.
Delphi still lacks of support for lambda expressions. If there will be lambda support then we will be able to make sorting similar to C# or Java languages in the very nice way like:
FList.OrderByDesc(x => x.ParentID).ThenBy(x => x.Name).ThenBy(x => x.ID);