C#の開発において、データの並べ替え (ソート) は最も頻繁に行われる操作の一つです。
単純な数値の昇順・降順から、オブジェクトのプロパティに基づいた複雑な条件での並べ替えまで、C#には用途に応じた強力なメソッドが用意されています。
本記事では、標準的なArray.Sortと、柔軟性の高いLINQの使い分け、さらにはカスタムソートの実装方法やパフォーマンス最適化まで、現場で役立つテクニックを網羅的に解説します。
C#における配列ソートの基本
C#で配列をソートする場合、大きく分けて2つのアプローチがあります。
1つは System.Array クラスが提供する静的メソッドを使用する方法、もう1つは System.Linq 名前空間の拡張メソッドを使用する方法です。
これらには明確な違いがあります。
Array.Sort は元の配列を直接書き換える (破壊的変更)のに対し、 LINQ は元の配列を保持したままソート済みの新しいシーケンスを生成します。
どちらが適しているかは、メモリ使用量やデータの不変性 (イミュータビリティ) の必要性によって決まります。
Array.Sortを使用した基本的なソート
Array.Sort は、C#で最も標準的かつ高速なソート手法です。
内部的には「イントロソート (Introspective Sort)」というアルゴリズムが採用されており、データの量や状態に応じて「クイックソート」「ヒープソート」「挿入ソート」を自動的に切り替えて効率的に処理を行います。
昇順ソートと降順ソート
まずは、最もシンプルな数値配列のソートを見てみましょう。
using System;
class Program
{
static void Main()
{
// ソート対象の配列
int[] numbers = { 5, 2, 8, 1, 9 };
// 昇順にソート (元の配列が書き換わる)
Array.Sort(numbers);
Console.WriteLine("昇順: " + string.Join(", ", numbers));
// 降順にするには、ソートした後に反転させる
Array.Reverse(numbers);
Console.WriteLine("降順: " + string.Join(", ", numbers));
}
}
昇順: 1, 2, 5, 8, 9
降順: 9, 8, 5, 2, 1
Array.Sort 自体には標準で「降順」を指定する引数がありません。
そのため、降順にしたい場合は、一度昇順でソートしてから Array.Reverse を使用するのが一般的な手順です。
配列の一部だけをソートする
特定のインデックス範囲だけをソートしたい場合、オーバーロードされた Array.Sort を使用できます。
これにより、不必要な要素の移動を抑制し、処理コストを抑えることが可能です。
using System;
class Program
{
static void Main()
{
int[] data = { 9, 8, 7, 1, 2, 3, 6, 5, 4 };
// インデックス3から3つの要素だけをソートする
// (1, 2, 3 の部分をソート)
Array.Sort(data, 3, 3);
Console.WriteLine("部分ソート: " + string.Join(", ", data));
}
}
部分ソート: 9, 8, 7, 1, 2, 3, 6, 5, 4
(※この例では元々並んでいるため変化が見えにくいですが、指定範囲内のみアルゴリズムが適用されます。)
LINQを使用した宣言的なソート
現代的なC#開発では、コードの可読性を重視して LINQ (Language Integrated Query) が多用されます。
LINQを使用すると、SQLのような直感的な記述でソートを実装できます。
OrderByとOrderByDescending
LINQを使用するには、ファイルの先頭に using System.Linq; を追加する必要があります。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 5, 2, 8, 1, 9 };
// LINQによる昇順ソート (新しい配列を生成)
int[] sortedAsc = numbers.OrderBy(n => n).ToArray();
// LINQによる降順ソート
int[] sortedDesc = numbers.OrderByDescending(n => n).ToArray();
Console.WriteLine("昇順 (LINQ): " + string.Join(", ", sortedAsc));
Console.WriteLine("降順 (LINQ): " + string.Join(", ", sortedDesc));
// 元の配列は変更されていない
Console.WriteLine("元の配列: " + string.Join(", ", numbers));
}
}
昇順 (LINQ): 1, 2, 5, 8, 9
降順 (LINQ): 9, 8, 5, 2, 1
元の配列: 5, 2, 8, 1, 9
LINQのメリットは、元のデータを破壊しない点にあります。
副作用を最小限に抑える関数型プログラミングの考え方に適しており、デバッグも容易になります。
複数の条件でソートする (ThenBy)
「第1キーでソートし、同じ値の場合は第2キーでソートする」といった複合条件も、LINQなら非常に簡潔に記述できます。
using System;
using System.Linq;
class Program
{
static void Main()
{
var people = new[]
{
new { Name = "田中", Age = 30 },
new { Name = "佐藤", Age = 25 },
new { Name = "鈴木", Age = 30 },
new { Name = "伊藤", Age = 20 }
};
// 年齢で昇順、同じ年齢なら名前で昇順
var sortedPeople = people
.OrderBy(p => p.Age)
.ThenBy(p => p.Name);
foreach (var p in sortedPeople)
{
Console.WriteLine($"{p.Age}歳: {p.Name}");
}
}
}
20歳: 伊藤
25歳: 佐藤
30歳: 佐藤
30歳: 鈴木
Array.Sort でこれを行うには、カスタムの比較ロジックを実装する必要がありますが、LINQであればメソッドチェーンで繋ぐだけで実現可能です。
Array.Sort vs LINQ:どちらを使うべきか?
どちらの手法を選択するかは、開発しているアプリケーションの要件に依存します。
以下の比較表を参考にしてください。
| 特徴 | Array.Sort | LINQ (OrderBy) |
|---|---|---|
| 変更の有無 | 破壊的 (元の配列を書き換える) | 非破壊的 (新しい配列を生成する) |
| メモリ効率 | 非常に高い (追加の配列生成なし) | 低い (新しいシーケンスのメモリを消費) |
| パフォーマンス | 高速 | 比較的人並み (オーバーヘッドあり) |
| 可読性 | 標準的 | 非常に高い (宣言的に記述可能) |
| 主な用途 | 大規模データの高速処理 | ビジネスロジック、データ抽出 |
大量のデータを扱うゲーム開発やリアルタイム処理では、GC (ガベージコレクション) を抑制するために Array.Sort を選択するのが定石です。
一方、保守性が重視される業務アプリケーションでは、コードが読みやすくなる LINQ が推奨されます。
カスタムオブジェクトのソート
独自のクラスや構造体を要素とする配列をソートする場合、C#に「何をもって大小を比較するか」を教える必要があります。
Comparison<T> デリゲートを使用する方法
Array.Sort にラムダ式を渡すことで、簡潔にカスタム比較を定義できます。
using System;
public class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
class Program
{
static void Main()
{
Product[] products = {
new Product { Name = "Apple", Price = 150 },
new Product { Name = "Banana", Price = 100 },
new Product { Name = "Orange", Price = 120 }
};
// 価格の昇順でソート
Array.Sort(products, (x, y) => x.Price.CompareTo(y.Price));
foreach (var p in products)
{
Console.WriteLine($"{p.Name}: {p.Price}円");
}
}
}
IComparable<T> インターフェースの実装
クラス自体にデフォルトのソート順を持たせたい場合は、 IComparable<T> インターフェースを実装します。
using System;
public class Student : IComparable<Student>
{
public string Name { get; set; }
public int Score { get; set; }
public int CompareTo(Student other)
{
if (other == null) return 1;
// スコアが高い順 (降順) にソートされるように設定
return other.Score.CompareTo(this.Score);
}
}
class Program
{
static void Main()
{
Student[] students = {
new Student { Name = "Aさん", Score = 80 },
new Student { Name = "Bさん", Score = 95 },
new Student { Name = "Cさん", Score = 70 }
};
// IComparableの実装に基づきソート
Array.Sort(students);
foreach (var s in students)
{
Console.WriteLine($"{s.Name}: {s.Score}点");
}
}
}
Bさん: 95点
Aさん: 80点
Cさん: 70点
このように、エンティティ自体に順序の定義を持たせることで、プロジェクト全体で一貫したソート順を適用できるようになります。
高度なソート:Span<T>の活用
近年のC# (特に .NET Core 以降) では、パフォーマンスを極限まで引き出すために Span<T> が導入されました。
配列の一部をスライスとして切り出し、その範囲に対して直接ソートを行うことができます。
using System;
class Program
{
static void Main()
{
int[] numbers = { 10, 5, 8, 1, 3, 9, 2 };
// 配列の一部をSpanとして参照 (コピーが発生しない)
Span<int> span = numbers.AsSpan(1, 4); // インデックス1から4要素
// Spanに対してソートを実行
span.Sort();
Console.WriteLine("Spanによる部分ソート後: " + string.Join(", ", numbers));
}
}
Spanによる部分ソート後: 10, 1, 3, 5, 8, 9, 2
Span<T>.Sort は、スタック上で動作するため非常に高速であり、ヒープメモリへの割り当てを極力避けたいハイパフォーマンスなライブラリ開発などで重宝されます。
多次元配列とジャグ配列のソート
C#の多次元配列 (int[,]) は、そのままでは Array.Sort に渡すことができません。
一方、ジャグ配列 (配列の配列 int[][]) は、各要素が独立した配列であるためソートが可能です。
ジャグ配列のソート例
using System;
using System.Linq;
class Program
{
static void Main()
{
int[][] jaggedArray = new int[][]
{
new int[] { 3, 1, 2 },
new int[] { 1, 5 },
new int[] { 2, 4, 3 }
};
// 子配列の「先頭要素」を基準に親配列をソート
Array.Sort(jaggedArray, (x, y) => x[0].CompareTo(y[0]));
foreach (var subArray in jaggedArray)
{
Console.WriteLine(string.Join(", ", subArray));
}
}
}
1, 5
2, 4, 3
3, 1, 2
多次元配列をソートする必要がある場合は、一度1次元配列にフラット化するか、ジャグ配列への書き換えを検討してください。
ソート時の注意点とベストプラクティス
不安定なソート (Unstable Sort) に注意
Array.Sort は「不安定なソート」です。
これは、同じ値を持つ要素の相対的な順序が保持されない可能性があることを意味します。
もし、元の順序を維持したままソートしたい (安定ソート) 場合は、LINQの OrderBy を使用してください。
LINQは安定ソートであることが保証されています。
文字列ソートとカルチャ
文字列をソートする場合、言語設定 (カルチャ) によって結果が変わることがあります。
StringComparer.OrdinalIgnoreCase などを使用して、大文字小文字を区別するか、どの言語規則に従うかを明示的に指定するのが安全です。
string[] fruits = { "apple", "Banana", "cherry" };
Array.Sort(fruits, StringComparer.OrdinalIgnoreCase);
空の配列とnullチェック
配列が null の状態でソートメソッドを呼び出すと ArgumentNullException が発生します。
実務では必ずnullチェック、または空配列のチェックを事前に行うようにしましょう。
まとめ
C#で配列をソートする方法は多岐にわたりますが、基本となるのは以下の使い分けです。
- パフォーマンス重視・インプレース更新なら
Array.Sort - 可読性重視・非破壊的処理なら
LINQ (OrderBy) - 極限の最適化なら
Span<T>.Sort
また、カスタムオブジェクトを扱う際は、 IComparable<T> やラムダ式を適切に活用することで、柔軟なデータ操作が可能になります。
それぞれの特性を理解し、プロジェクトの要件に合わせた最適なソート手法を選択してください。
