C# Sorting
Sorting means arranging data into a useful order, such as numbers from smallest to largest or names alphabetically. It matters because sorted data is easier to display, search, compare, group, and process. In C#, you usually sort with built-in library methods instead of writing a sorting algorithm from scratch.
Overview: How C# Sorting Works
C# offers several sorting tools, and the right one depends on the collection and whether you want to modify it. Array.Sort sorts an array in place. List<T>.Sort sorts a list in place. LINQ methods such as OrderBy, OrderByDescending, ThenBy, and ThenByDescending return a new ordered sequence and leave the original collection unchanged.
Sorting is built around comparison. For simple types like int, double, DateTime, and string, .NET already knows the default order because those types implement comparison interfaces such as IComparable<T>. For your own classes and records, you must tell C# how to compare values. You can do that with a comparison delegate, an IComparer<T>, or a LINQ key selector.
Most general-purpose comparison sorting takes about O(n log n) time for n items. That is much faster than repeatedly scanning for the smallest item in large collections, but it still becomes noticeable as data grows. Sorting usually needs many comparisons, so expensive comparison logic can dominate runtime. If a key is expensive to compute, LINQ’s key-based ordering can be clearer because the key selector expresses what is being sorted rather than hiding work inside a comparer.
Mutation is a major difference. Array.Sort(scores) changes the existing array. names.Sort() changes the existing list. names.OrderBy(name => name) does not sort names itself; it creates an ordered enumerable that you normally materialize with ToList or enumerate directly. This distinction prevents many bugs.
Stability is another important concept. A stable sort keeps equal items in their original relative order. LINQ ordering is stable, which is useful when you sort by multiple keys or when ties should preserve input order. In-place array and list sorting should not be treated as stable, so if equal items must keep their original order, use LINQ ordering or include an explicit tie-breaker.
Syntax
Array.Sort(array);
Array.Sort(array, comparer);
list.Sort();
list.Sort((left, right) => left.Property.CompareTo(right.Property));
IEnumerable<T> ordered = items.OrderBy(item => item.PrimaryKey)
.ThenBy(item => item.SecondaryKey);
List<T> sorted = ordered.ToList();
| Form | Meaning |
|---|---|
Array.Sort(array) |
Sorts an array in place using the default comparer. |
list.Sort() |
Sorts a List<T> in place using the default comparer. |
list.Sort(comparison) |
Sorts a list in place using a lambda that returns negative, zero, or positive. |
OrderBy |
Returns an ordered sequence by one key, without changing the source collection. |
ThenBy |
Adds a secondary key while preserving earlier ordering decisions. |
IComparer<T> |
A reusable object that defines custom ordering for a type. |
Examples
Sorting an Array and a List
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
int[] scores = { 42, 7, 19, 7, 100 };
Array.Sort(scores);
Console.WriteLine("Sorted array:");
Console.WriteLine(string.Join(", ", scores));
List<string> names = new List<string> { "Maya", "Ada", "Grace", "Linus" };
names.Sort();
Console.WriteLine("Sorted list:");
Console.WriteLine(string.Join(", ", names));
}
}
Output:
Sorted array:
7, 7, 19, 42, 100
Sorted list:
Ada, Grace, Linus, Maya
This example uses default sorting. The integer array is sorted numerically, and the string list is sorted by the default string comparer. Both operations modify the original collection, so any other reference to that same array or list sees the new order.
Sorting Objects with LINQ
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<Student> students = new List<Student>
{
new Student("Ada", 95, 2),
new Student("Ben", 95, 1),
new Student("Cora", 88, 3),
new Student("Dev", 88, 2)
};
List<Student> sorted = students
.OrderByDescending(student => student.Score)
.ThenBy(student => student.Attempts)
.ThenBy(student => student.Name)
.ToList();
foreach (Student student in sorted)
{
Console.WriteLine($"{student.Name}: {student.Score}, attempts {student.Attempts}");
}
}
}
record Student(string Name, int Score, int Attempts);
Output:
Ben: 95, attempts 1
Ada: 95, attempts 2
Dev: 88, attempts 2
Cora: 88, attempts 3
LINQ sorting is usually the clearest way to sort objects for display or reporting. This code sorts by score from high to low, then by attempts from low to high, then by name. The original students list remains in its original order because OrderByDescending returns a separate ordered sequence.
Using a Custom Comparer
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<FileItem> files = new List<FileItem>
{
new FileItem("report.pdf", 2500),
new FileItem("notes.txt", 800),
new FileItem("archive.zip", 2500),
new FileItem("photo.jpg", 1200)
};
files.Sort(new FileSizeThenNameComparer());
foreach (FileItem file in files)
{
Console.WriteLine($"{file.Name}: {file.Bytes}");
}
}
}
record FileItem(string Name, int Bytes);
class FileSizeThenNameComparer : IComparer<FileItem>
{
public int Compare(FileItem? x, FileItem? y)
{
if (ReferenceEquals(x, y))
{
return 0;
}
if (x is null)
{
return -1;
}
if (y is null)
{
return 1;
}
int sizeComparison = x.Bytes.CompareTo(y.Bytes);
if (sizeComparison != 0)
{
return sizeComparison;
}
return string.Compare(x.Name, y.Name, StringComparison.Ordinal);
}
}
Output:
notes.txt: 800
photo.jpg: 1200
archive.zip: 2500
report.pdf: 2500
A comparer is useful when the same ordering will be reused in several places. The Compare method returns a negative number when x should come before y, zero when they are equal for sorting purposes, and a positive number when x should come after y. This comparer sorts by file size first and file name second.
How Sorting Works Step by Step
- You call a sorting API such as
SortorOrderBy. - The sorting code chooses pairs of elements to compare. The exact internal algorithm is an implementation detail, so your code should depend on the comparison contract, not on a specific sequence of comparisons.
- The comparer decides whether one item belongs before, at the same position as, or after another item.
- For in-place sorting, the array or list rearranges its existing elements. Value types such as
intare moved as values; reference-type elements are moved as references, not cloned objects. - For LINQ ordering, the source is read when the query is enumerated. The ordered result can then be turned into an array or list with
ToArrayorToList. - If multiple sort keys are used,
ThenBycompares only items that tied under the previous key.
The CLR does not magically know the meaning of your business data. It only executes the comparison logic supplied by the type, delegate, comparer, or key selector. Good sorting code therefore starts with a precise question: what does before mean for this data?
Common Mistakes
Expecting Sort to Return a New List
List<int> numbers = new List<int> { 3, 1, 2 };
List<int> sorted = numbers.Sort();
List<T>.Sort returns void because it changes the existing list. Assigning its result is a compile-time error. If you want a new sorted list, use LINQ and call ToList.
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 3, 1, 2 };
List<int> sorted = numbers.OrderBy(number => number).ToList();
Console.WriteLine("Original: " + string.Join(", ", numbers));
Console.WriteLine("Sorted: " + string.Join(", ", sorted));
}
}
Output:
Original: 3, 1, 2
Sorted: 1, 2, 3
Writing an Overflow-Prone Comparison
items.Sort((a, b) => a.Id - b.Id);
Subtracting integers looks compact, but it can overflow for very large values and produce the wrong sign. Use CompareTo or Comparer<T>.Default.Compare instead.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<Job> jobs = new List<Job>
{
new Job(int.MaxValue, "Last"),
new Job(0, "Middle"),
new Job(int.MinValue, "First")
};
jobs.Sort((a, b) => a.Id.CompareTo(b.Id));
foreach (Job job in jobs)
{
Console.WriteLine($"{job.Id}: {job.Name}");
}
}
}
record Job(int Id, string Name);
Output:
-2147483648: First
0: Middle
2147483647: Last
Forgetting That OrderBy Is Deferred
OrderBy builds a query. It does not perform the full sort until you enumerate the result. If the source collection changes before enumeration, the query sees the changed data. Use ToList when you need a snapshot.
Best Practices
- Use
Array.SortorList<T>.Sortwhen you intentionally want to reorder the existing collection. - Use LINQ ordering when you want a sorted result without changing the source, especially for display and reports.
- Use
ThenByfor secondary keys; do not callOrderByagain unless you mean to replace the previous primary ordering. - Prefer
CompareTo,Comparer<T>.Default.Compare, orstring.Compareover subtraction-based comparisons. - Choose string comparison rules deliberately. Use
StringComparer.Ordinalfor technical identifiers and culture-aware comparison for user-facing language when appropriate. - Include explicit tie-breakers when deterministic output matters.
- Do not write your own quicksort or mergesort for normal application code; use the library and focus on the comparison rule.
Practice Exercises
- Create a list of product names and prices. Sort by price ascending, then by name ascending for products with the same price.
- Write a comparer that sorts strings by length first and alphabetically second. Test it with at least five words.
- Start with an array of integers. Print the original order, sort a copied array, then print both arrays to prove which one changed.
Summary
- Sorting arranges items according to a comparison rule.
Array.SortandList<T>.Sortsort in place.- LINQ ordering returns a separate ordered sequence and is stable.
- Custom comparers let you define reusable ordering for your own types.
- Most general-purpose sorting is about
O(n log n), but comparison cost still matters. - Clear tie-breakers and deliberate string comparison rules make sorted output predictable.
