Sorting in .NET: stability and comparers
Choose between Array.Sort, List.Sort and LINQ OrderBy, sort by several keys, write correct comparers, and avoid the classic sorting bugs.
| Method | Algorithm | Changes the original? | Stable? |
|---|---|---|---|
Array.Sort(a), list.Sort(), span.Sort() | |||
items.OrderBy(...), ThenBy(...) (LINQ) | |||
items.Order() (.NET 7+) |
Stability matters
Sorting by several keys
var beams = new List<Beam>{ new("B1", "IPE300", 6.0), new("B2", "HEA200", 4.5), new("B3", "IPE300", 4.5), new("B4", "HEA200", 6.0)}; // LINQ: readable, stable, returns a new listvar byProfileThenLongest = beams .OrderBy(b => b.Profile) .ThenByDescending(b => b.Length) .ToList(); foreach (var b in byProfileThenLongest) Console.WriteLine($"{b.Name} {b.Profile} {b.Length:0.0}"); // In place with a Comparison<T> lambda (no new list)beams.Sort((x, y) =>{ int c = string.Compare(x.Profile, y.Profile, StringComparison.Ordinal); return c != 0 ? c : y.Length.CompareTo(x.Length); // y before x = descending}); record Beam(string Name, string Profile, double Length);B4 HEA200 6.0 B2 HEA200 4.5 B1 IPE300 6.0 B3 IPE300 4.5
Three ways to define an order
`IComparable<T>` on the type itself: its natural order. int,string,DateTimealready have one. Thenlist.Sort()needs no arguments.`IComparer<T>`: a separate object describing an order. Useful when there are several orders, or you cannot change the type. Comparer<T>.Create(lambda)makes one from a lambda.`Comparison<T>`: a plain lambda (x, y) => int, accepted byList<T>.SortandArray.Sort.
var levels = new List<Level> { new("Roof", 12.0), new("Ground", 0.0), new("First", 4.0) };levels.Sort(); // uses CompareToConsole.WriteLine(string.Join(", ", levels.Select(l => l.Name))); class Level : IComparable<Level>{ public string Name { get; } public double Elevation { get; } public Level(string name, double elevation) => (Name, Elevation) = (name, elevation); // negative: this comes first, 0: equal, positive: other comes first public int CompareTo(Level? other) => other is null ? 1 : Elevation.CompareTo(other.Elevation);}Ground, First, Roof
Classic sorting bugs
(x, y) => x - y looks clever but overflows: for int.MinValue and 1 the result wraps around to a large positive number, so the order is wrong. Always use x.CompareTo(y).
"B10" comes before "B2" (because 1 < 2). The default comparer is also culture-sensitive. For IDs and file names, decide explicitly:
var names = new List<string> { "apple", "Banana", "cherry" }; names.Sort(StringComparer.Ordinal); // by character code: capitals firstConsole.WriteLine(string.Join(" ", names)); names.Sort(StringComparer.OrdinalIgnoreCase); // ignore case, culture-independentConsole.WriteLine(string.Join(" ", names)); var marks = new List<string> { "B10", "B2", "B1" };marks.Sort(StringComparer.Ordinal);Console.WriteLine(string.Join(" ", marks)); // text order, not number orderBanana apple cherry apple Banana cherry B1 B10 B2
Sort throw InvalidOperationException (“IComparer.Compare() method returns inconsistent results”) or produce garbage.
PriorityQueue of size 10 does it in O(n log 10), see the heaps lesson.
"A-10", "A-2", "B-1", "A-1". Sort them by the letter, then by the number (so A-2 comes before A-10). Expected: A-1, A-2, A-10, B-1.
Show a hint
-. Order by the first part, then by int.Parse of the second part.
Show the solution
var drawings = new List<string> { "A-10", "A-2", "B-1", "A-1" }; var sorted = drawings .OrderBy(d => d.Split('-')[0], StringComparer.Ordinal) .ThenBy(d => int.Parse(d.Split('-')[1])) .ToList(); Console.WriteLine(string.Join(", ", sorted));A-1, A-2, A-10, B-1
Remember
Array.Sort/List.Sortuse introsort: fast, in place, but not stable.LINQ OrderBy/ThenByis stable and returns a new sequence.Define order with IComparable<T>,IComparer<T>or aComparison<T>lambda, usingCompareTo, never subtraction.Be explicit about string order ( StringComparer.Ordinal,OrdinalIgnoreCase) and sort numbers inside IDs as numbers.