DataSet/DataTableから列名を取り出し、アルファベット順(昇順)でソートした配列を得る最短手順と、実務でつまずきやすい「大文字小文字の扱い」「カルチャ差」「数字を含む列名の自然順」「Caption(表示名)で並べたい」などの課題を一気に解消するための実装レシピをまとめます。コピペで即使えるコードと判断基準を揃えました。
DataTable の列名を LINQ で昇順ソートする方法
質問概要
DataSet 内の DataTable から取得した列名 (ColumnName) の配列を、アルファベット順(昇順)に並べ替えたい。
回答・解決策(最短コード)
Select で列名を取り出し、OrderBy で昇順、最後に ToArray() で配列化します。
string[] columnNames = ds.Tables[0]
.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderBy(name => name) // 昇順ソート(カルチャ依存)
.ToArray();
降順にしたい場合
string[] columnNamesDesc = ds.Tables[0]
.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderByDescending(name => name) // 降順
.ToArray();
補足(よく使う Variation)
- 大文字/小文字を無視:
OrderBy(name => name, StringComparer.CurrentCultureIgnoreCase) - すでに
string[]を持っている:names = names.OrderBy(x => x).ToArray(); ColumnsはDataColumnCollectionなので、LINQ するためにCast<DataColumn>()またはOfType<DataColumn>()を挟みます。
なぜその書き方で正しいのか(要点を最短理解)
- 抽出 → 並べ替え → 配列化 の順で、不要なオブジェクトを増やさずに済む。
OrderByの既定比較は 現在カルチャ(通常はOSやプロセスのカルチャ)に依存。英字と日本語が混在する場合の順序もカルチャにより変わる。- 列名は
DataTable.Columns内で一意(重複不可)。ただし、複数テーブルから結合した列名リストでは重複が起こり得るため、その場合はDistinctを挟む。
実行例:サンプル DataTable で結果を確認
まずは動く最小例で、昇順/降順/大文字小文字の影響を確認します。
using System;
using System.Data;
using System.Linq;
using System.Collections.Generic;
class Program
{
static void Main()
{
var dt = new DataTable();
dt.Columns.Add("Name");
dt.Columns.Add("id");
dt.Columns.Add("ID");
dt.Columns.Add("col10");
dt.Columns.Add("col2");
dt.Columns.Add("_meta");
dt.Columns.Add("番号");
// 昇順(既定カルチャ、大文字小文字区別あり)
var asc = dt.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderBy(n => n)
.ToArray();
// 昇順(大文字小文字無視:CurrentCultureIgnoreCase)
var ascIgnoreCase = dt.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderBy(n => n, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
// 降順
var desc = dt.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderByDescending(n => n)
.ToArray();
Console.WriteLine("昇順: " + string.Join(", ", asc));
Console.WriteLine("昇順(大/小無視): " + string.Join(", ", ascIgnoreCase));
Console.WriteLine("降順: " + string.Join(", ", desc));
}
}
典型的な結果のイメージは次のとおりです(カルチャ設定により細部は変わる可能性があります)。
| 並べ替え条件 | 想定される並び | ポイント |
|---|---|---|
| 昇順(区別あり) | ID, Name, _meta, col10, col2, id, 番号 | 大文字の ID と小文字の id が別扱い |
| 昇順(大/小無視) | ID, id, Name, _meta, col10, col2, 番号 | 大小無視なので ID と id が近接 |
| 降順 | 番号, id, col2, col10, _meta, Name, ID | 降順は OrderByDescending |
カルチャと比較方法の選び方(確実に押さえる)
OrderBy は比較器(IComparer<T>)を渡せます。用途に応じて使い分けを明確にしましょう。
| 比較器 | 大文字小文字 | 特性 | 主な用途 | 注意点 |
|---|---|---|---|---|
StringComparer.CurrentCulture | 区別する | OS/プロセスの現在カルチャ依存 | UI表示順をローカル文化に合わせたい | 実行環境により順序が変わる可能性 |
StringComparer.CurrentCultureIgnoreCase | 区別しない | ユーザ向けの自然な並び | 人間が読むリストの基本 | やはり環境差が出る |
StringComparer.InvariantCulture | 区別する | カルチャ固定(中立) | 環境差を抑えたいテスト・サーバーサイド | 人間的自然さはやや弱い |
StringComparer.InvariantCultureIgnoreCase | 区別しない | 中立&大小無視 | 再現性重視のサービス/バッチ | UIの期待と異なる場面あり |
StringComparer.Ordinal | 区別する | バイナリ的(コードポイント順) | IDやキーの厳密・高速比較 | 人にとって不自然な並びになり得る |
StringComparer.OrdinalIgnoreCase | 区別しない | 大小を無視しつつ高速・確定的 | 英字中心の列名で安定性重視 | 多言語の自然さは期待しない |
まとめると、UI表示なら CurrentCultureIgnoreCase、再現性重視なら Ordinal(IgnoreCase) か InvariantCulture(IgnoreCase) が実務的です。
// UIフレンドリ
var namesForUi = dt.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderBy(n => n, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
// 再現性・速度重視(英数字中心)
var namesStable = dt.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
.ToArray();
数字を含む列名を「人間的に」並べたい(自然順ソート)
col1, col2, col10 のように数字を含む列名は、単純な辞書式だと col1, col10, col2 になりがちです。人間的には col1, col2, col10 と並んでほしい——これが「自然順(Natural Sort)」です。
手早いキー変換方式(数字をゼロ埋め)
桁数がある程度予測できるなら、数字をゼロ埋めしたキーでソートする簡易策が使えます。
using System.Text.RegularExpressions;
var natural = dt.Columns.Cast()
.Select(c => c.ColumnName)
.OrderBy(n =>
Regex.Replace(n, "\d+", m => m.Value.PadLeft(10, '0')), // 10桁ゼロ埋め
StringComparer.OrdinalIgnoreCase) // 比較器は用途に合わせて
.ToArray();
この方法は軽量ですが、連続する数値が極端に大きいと想定外になります。実務では「最大桁数」を十分に取る(例:20桁)か、次のフル実装へ。
汎用的な自然順コンパレータ
数値と文字をトークン分割して比較する、安定の王道実装です。コピペで使えます。
using System;
using System.Collections.Generic;
public sealed class NaturalStringComparer : IComparer
{
private readonly StringComparer _textComparer;
public NaturalStringComparer(StringComparer textComparer = null)
{
_textComparer = textComparer ?? StringComparer.OrdinalIgnoreCase;
}
public int Compare(string x, string y)
{
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;
int ix = 0, iy = 0;
while (ix < x.Length || iy < y.Length)
{
if (ix >= x.Length) return -1;
if (iy >= y.Length) return 1;
char cx = x[ix];
char cy = y[iy];
// 両方が数字であれば数値として比較
if (char.IsDigit(cx) && char.IsDigit(cy))
{
long vx = 0, vy = 0;
int sx = ix, sy = iy;
while (ix < x.Length && char.IsDigit(x[ix])) { vx = vx * 10 + (x[ix] - '0'); ix++; }
while (iy < y.Length && char.IsDigit(y[iy])) { vy = vy * 10 + (y[iy] - '0'); iy++; }
int num = vx.CompareTo(vy);
if (num != 0) return num;
// 数値が等しい場合は、数字列の長さ(先頭の0の数)で安定化
int lenX = ix - sx;
int lenY = iy - sy;
if (lenX != lenY) return lenX.CompareTo(lenY);
}
else
{
// 文字トークンを取り出して比較
int sx = ix, sy = iy;
while (ix < x.Length && !char.IsDigit(x[ix])) ix++;
while (iy < y.Length && !char.IsDigit(y[iy])) iy++;
string tx = x.Substring(sx, ix - sx);
string ty = y.Substring(sy, iy - sy);
int text = _textComparer.Compare(tx, ty);
if (text != 0) return text;
}
}
return 0;
}
}
使用例:
var naturalComparer = new NaturalStringComparer(StringComparer.CurrentCultureIgnoreCase);
var namesNatural = dt.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderBy(n => n, naturalComparer)
.ToArray();
表示名(Caption)で並べたい/空なら ColumnName にフォールバック
列の「見せる名前」である DataColumn.Caption を優先して並べたい場合の定番パターンです。Caption が空なら ColumnName を使います。
var namesByCaption = dt.Columns.Cast<DataColumn>()
.OrderBy(c => string.IsNullOrEmpty(c.Caption) ? c.ColumnName : c.Caption,
StringComparer.CurrentCultureIgnoreCase)
.Select(c => c.ColumnName) // 返り値は ColumnName の配列にする例
.ToArray();
表示と内部IDを分けたいUI(コンボボックス・ヘッダ選択ダイアログ等)で便利です。
拡張メソッド化で呼び出しを簡潔に
何度も使うなら、DataTable へ拡張メソッドを生やしておくと、呼び出し側がスッキリします。
public static class DataTableExtensions
{
public static string[] SortedColumnNames(
this DataTable table,
bool descending = false,
IComparer<string> comparer = null,
bool useCaption = false,
bool natural = false)
{
if (table is null) throw new ArgumentNullException(nameof(table));
comparer ??= StringComparer.CurrentCultureIgnoreCase;
IEnumerable<string> keys = table.Columns.Cast<DataColumn>()
.Select(c => {
var key = useCaption && !string.IsNullOrEmpty(c.Caption) ? c.Caption : c.ColumnName;
return natural
? NormalizeForNatural(key)
: key;
});
// natural=true のときは NaturalStringComparer を使うのが堅い
var effComparer = natural ? new NaturalStringComparer(comparer as StringComparer ?? StringComparer.CurrentCultureIgnoreCase)
: comparer;
var sorted = descending ? keys.OrderByDescending(k => k, effComparer)
: keys.OrderBy(k => k, effComparer);
return sorted.ToArray();
static string NormalizeForNatural(string s) => s; // キー変換が必要ならここで
}
}
利用例:
var asc = dt.SortedColumnNames(); // 既定: 昇順 & CurrentCultureIgnoreCase
var descNat = dt.SortedColumnNames(descending: true, natural: true);
var inv = dt.SortedColumnNames(comparer: StringComparer.InvariantCulture);
var cap = dt.SortedColumnNames(useCaption: true);
「複数テーブルをまとめた列名」をソートしたい場合
結合レポートなどで複数テーブルの列名を一括したいとき、重複を排除してからソートします。
var allNames = ds.Tables.Cast<DataTable>()
.SelectMany(t => t.Columns.Cast<DataColumn>().Select(c => c.ColumnName))
.Distinct(StringComparer.OrdinalIgnoreCase) // 重複排除(大小無視)
.OrderBy(n => n, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
DataView.Sortとの混同に注意
DataView.Sort は行の並べ替えに使います。ここで扱っているのは列(DataColumn)の並び。用途が別なので混同しないようにしましょう。
Unicode・正規化の観点(多言語環境の落とし穴)
- 全角/半角や濁点結合の違いが混入すると、想定外の順序になることがあります。
- 入力元が外部なら、
string.Normalize(NormalizationForm.FormKC)で整形してから比較すると安定します。
var normalizedAsc = dt.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName.Normalize(NormalizationForm.FormKC))
.OrderBy(n => n, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
パフォーマンスとメモリ(現実解)
- 列数は多くても数百が相場。ソート計算量は
O(n log n)で、通常は誤差の範囲です。 OrderByは遅延評価。最終的にToArray()で一度だけ具象化するのが無駄が少ない。string[]をすでに持っているなら、Array.Sort(names, comparer)が最小オーバーヘッドです。
string[] names = dt.Columns.Cast<DataColumn>().Select(c => c.ColumnName).ToArray();
Array.Sort(names, StringComparer.OrdinalIgnoreCase); // in-place ソート
| 方式 | 割り当て | 書きやすさ | コメント |
|---|---|---|---|
| LINQ + ToArray | 中 | 高 | 表現力と読みやすさのバランスが良い |
| Array.Sort | 低 | 中 | 既存配列があるなら最速・省メモリ |
| List.Sort | 中 | 中 | 可変コレクションが必要な場合に有効 |
安全性チェック(例外・Null・命名)
DataColumn.ColumnNameは空やnullにできません(DataTable内では一意)。通常の運用で null 対策は不要です。- UIで編集した表示名(Caption)を列名として流用する場合は、使用禁止文字(ピリオド等)に留意。
- 他システムと連携する場合、カルチャによる順序差が障害化しやすいので、サーバーサイドは
OrdinalIgnoreCase等で確定順にするのが無難です。
目的別コード・スニペット集(コピペ可)
1) もっとも簡単な昇順(既定カルチャ)
string[] cols = ds.Tables[0]
.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderBy(n => n)
.ToArray();
2) 大文字小文字を無視してUIに優しい昇順
string[] colsUi = ds.Tables[0]
.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderBy(n => n, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
3) 英数字中心で再現性重視(OrdinalIgnoreCase)
string[] colsStable = ds.Tables[0]
.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
.ToArray();
4) 降順
string[] colsDesc = ds.Tables[0]
.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderByDescending(n => n, StringComparer.CurrentCulture)
.ToArray();
5) 自然順(数字を人間的に扱う)
var colsNatural = ds.Tables[0]
.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderBy(n => n, new NaturalStringComparer(StringComparer.CurrentCultureIgnoreCase))
.ToArray();
6) Caption優先、FallbackはColumnName
string[] colsByCaption = ds.Tables[0]
.Columns.Cast<DataColumn>()
.OrderBy(c => string.IsNullOrEmpty(c.Caption) ? c.ColumnName : c.Caption,
StringComparer.CurrentCultureIgnoreCase)
.Select(c => c.ColumnName)
.ToArray();
7) 既にある string[] を並べ替え(メモリ最小)
Array.Sort(columnNames, StringComparer.InvariantCultureIgnoreCase);
8) クエリ式で書きたい派
var queryStyle =
(from DataColumn c in ds.Tables[0].Columns
orderby c.ColumnName
select c.ColumnName).ToArray();
チェックリスト(導入・レビュー時に使える)
- どの比較器を使うか決めた?(UI=CurrentCultureIgnoreCase、サーバー=OrdinalIgnoreCase など)
- 数字を含む列名の自然順は必要?(必要なら
NaturalStringComparer) - CaptionとColumnNameのどちらで並べる?(Caption優先ならフォールバックを)
- 複数テーブルを束ねる場合は
Distinct済み? - 配列をその場で並べ替えるなら
Array.Sortを検討した? - 多言語データでは正規化を検討した?(
NormalizationForm.FormKC)
よくある落とし穴と回避策
- 落とし穴:「手元では並びが違う」
回避:比較器を明示し、環境差を抑える(本番・テストで同一設定)。 - 落とし穴:
DataView.Sortを使って列順を変えられると思い込む
回避:列順はDataColumnリストのソートで扱う。行と列を混同しない。 - 落とし穴:「col1, col10, col2」が辞書式で崩れる
回避:自然順コンパレータを導入。 - 落とし穴:Captionで表示しているのに列名でソートしている
回避:UIのキー(表示名)と内部ID(列名)を分け、必要に応じて二段構え。
まとめ
- 最短解は
.Select(c => c.ColumnName).OrderBy(n => n).ToArray()。 - 人が使うUIなら
StringComparer.CurrentCultureIgnoreCaseを明示。 - 英数字主体・再現性重視なら
OrdinalIgnoreCase。 - 数字を含む列名は自然順コンパレータで解決。
- Captionで並べたい場合はフォールバック設計を。
- 既存配列の並べ替えは
Array.Sortが最小オーバーヘッド。
コピー用:問合せに対する最小回答(再掲)
基本形
string[] columnNames = ds.Tables[0]
.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderBy(name => name) // 昇順ソート
.ToArray();
降順にしたい場合
string[] columnNamesDesc = ds.Tables[0]
.Columns.Cast<DataColumn>()
.Select(c => c.ColumnName)
.OrderByDescending(name => name) // 降順
.ToArray();
補足
OrderByは規定カルチャに従って比較。大文字/小文字を無視したい場合はOrderBy(name => name, StringComparer.CurrentCultureIgnoreCase)のようにIComparerを渡す。string[]が既にある場合でも、columnNames = columnNames.OrderBy(x => x).ToArray();のように再並べ替えできる。
ここまで押さえれば、「DataTable の列名を LINQ で昇順ソート」はもちろん、UI品質・再現性・多言語・自然順までカバーできます。現場の基準として、本記事のスニペットをそのままプロジェクトのユーティリティに取り込んでおくと、列名ソートに悩む時間をゼロにできます。

コメント