【C#】LINQとDataTableでスマートにフィルタ・ソート・集計する方法
LINQを使えばDataTableをスマートに絞り込み、ソート、集計できます。
AsEnumerable
で行列を列挙型へ変換し、where
やOrderBy
で条件指定や並び替え、GroupBy
と集計関数でグループ処理もスムーズに実装可能です。
SQLライクな記述により読み書きの手間を削減し、Select
メソッドより可読性と保守性が向上しやすいのが利点です。
必要に応じてCopyToDataTable
で結果をDataTableへ戻せるため、後続処理への受け渡しも簡単です。
DataTableとLINQの基本
DataTableとは
DataTable
は、.NET FrameworkのSystem.Data
名前空間に含まれるクラスで、表形式のデータをメモリ上で管理するためのデータ構造です。
データベースのテーブルのように、行DataRow
と列DataColumn
で構成されており、複数のデータ型を持つ列を自由に定義できます。
主な特徴は以下の通りです。
- 柔軟なスキーマ定義
列の名前やデータ型を自由に設定でき、必要に応じて制約(主キーや一意制約など)も追加可能です。
- データの追加・更新・削除が可能
行単位でデータを追加したり、既存のデータを更新・削除したりできます。
- データバインディングに対応
WindowsフォームやWPFのUIコントロールに簡単にバインドでき、表示や編集が容易です。
- データベースとの連携が容易
DataAdapter
を使ってデータベースからデータを取得し、DataTable
に格納したり、逆にDataTable
の変更をデータベースに反映したりできます。
たとえば、以下のようにDataTable
を作成し、列を定義してデータを追加できます。
using System;
using System.Data;
class Program
{
static void Main()
{
// DataTableの作成
DataTable dt = new DataTable("SampleTable");
// 列の定義
dt.Columns.Add("ID", typeof(int));
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
// データの追加
dt.Rows.Add(1, "田中", 28);
dt.Rows.Add(2, "佐藤", 35);
dt.Rows.Add(3, "鈴木", 22);
// データの表示
foreach (DataRow row in dt.Rows)
{
Console.WriteLine($"ID: {row["ID"]}, Name: {row["Name"]}, Age: {row["Age"]}");
}
}
}
ID: 1, Name: 田中, Age: 28
ID: 2, Name: 佐藤, Age: 35
ID: 3, Name: 鈴木, Age: 22
このコードでは、ID
、Name
、Age
の3列を持つDataTable
を作成し、3件のデータを追加しています。
Rows
コレクションを使って各行のデータを取得し、コンソールに表示しています。
LINQとは
LINQ(Language Integrated Query)は、C#やVB.NETに組み込まれたクエリ言語で、コレクションやデータベース、XMLなどのデータソースに対して統一的な方法で問い合わせや操作を行うことができます。
LINQを使うことで、複雑なデータ操作も簡潔で読みやすいコードで記述できるようになります。
LINQの主な特徴は以下の通りです。
- 統一されたクエリ構文
配列やリスト、DataTable
、データベース、XMLなど、さまざまなデータソースに対して同じ構文でクエリを記述できます。
- 型安全でコンパイル時チェックが可能
クエリはコンパイル時に型チェックされるため、実行時エラーを減らせます。
- 遅延実行
クエリの実行は必要になるまで遅延されるため、効率的な処理が可能です。
- 拡張メソッドによる柔軟な操作
Where
、Select
、OrderBy
、GroupBy
など、多彩な拡張メソッドを使ってデータをフィルタリング、変換、並べ替え、集計できます。
LINQには主に2つの記法があります。
- クエリ式(Query Syntax)
SQLに似た構文で、from
、where
、select
などのキーワードを使います。
- メソッドチェーン(Method Syntax)
拡張メソッドを連結して記述します。
こちらはより柔軟で複雑な処理に向いています。
たとえば、配列から偶数の数値だけを抽出する例を示します。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5, 6 };
// クエリ式
var evenNumbersQuery = from n in numbers
where n % 2 == 0
select n;
// メソッドチェーン
var evenNumbersMethod = numbers.Where(n => n % 2 == 0);
Console.WriteLine("偶数(クエリ式):");
foreach (var num in evenNumbersQuery)
{
Console.WriteLine(num);
}
Console.WriteLine("偶数(メソッドチェーン):");
foreach (var num in evenNumbersMethod)
{
Console.WriteLine(num);
}
}
}
偶数(クエリ式):
2
4
6
偶数(メソッドチェーン):
2
4
6
このコードでは、numbers
配列から偶数だけを抽出し、2通りの書き方で表示しています。
DataTableをLINQで扱うメリット
DataTable
は非常に便利なデータ構造ですが、従来の操作方法ではRows
コレクションをループして条件分岐を行うなど、コードが冗長になりがちです。
LINQを組み合わせることで、以下のようなメリットがあります。
- コードがシンプルで読みやすくなる
複雑な条件でのフィルタリングや並べ替え、集計も1行や数行のクエリで表現でき、可読性が大幅に向上します。
- 型安全に列データへアクセスできる
Field<T>
拡張メソッドを使うことで、列の値を指定した型で安全に取得でき、キャストミスによる例外を防げます。
- 遅延実行により効率的な処理が可能
LINQの遅延実行により、必要なデータだけを効率的に処理できます。
- 柔軟なデータ操作が可能
フィルタリング、並べ替え、グループ化、結合、集計など、多彩な操作を統一的な構文で行えます。
- 既存の
DataTable
を変更せずにクエリ結果を取得できる
元のDataTable
を壊さずに、条件に合った行だけを抽出したり、新しい順序で並べ替えたりできます。
たとえば、DataTable
のデータから年齢が30歳以上の人だけを抽出し、名前の昇順で並べ替える例を示します。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("ID", typeof(int));
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add(1, "田中", 28);
dt.Rows.Add(2, "佐藤", 35);
dt.Rows.Add(3, "鈴木", 40);
dt.Rows.Add(4, "高橋", 25);
var filteredSortedRows = dt.AsEnumerable()
.Where(row => row.Field<int>("Age") >= 30)
.OrderBy(row => row.Field<string>("Name"));
foreach (var row in filteredSortedRows)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
}
}
Name: 佐藤, Age: 35
Name: 鈴木, Age: 40
このように、AsEnumerable
でDataTable
をLINQで扱える形に変換し、Field<T>
で型安全に列の値を取得しながら、Where
で条件を指定し、OrderBy
で並べ替えています。
これにより、従来のループと条件分岐を使ったコードよりも簡潔で直感的な記述が可能です。
以上の理由から、DataTable
とLINQを組み合わせることは、C#でのデータ操作をスマートに行うための有効な手段となっています。
準備作業
AsEnumerableで列挙型へ変換
DataTable
はそのままではLINQのクエリ構文や拡張メソッドで直接操作できません。
LINQで扱うためには、DataTable
の行を列挙可能な形式に変換する必要があります。
これを実現するのがAsEnumerable
メソッドです。
AsEnumerable
はDataTable
の拡張メソッドで、DataRow
のシーケンスIEnumerable<DataRow>
を返します。
これにより、LINQのWhere
やSelect
、OrderBy
などのメソッドを使って柔軟にデータを操作できます。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("ID", typeof(int));
dt.Columns.Add("Name", typeof(string));
dt.Rows.Add(1, "田中");
dt.Rows.Add(2, "佐藤");
dt.Rows.Add(3, "鈴木");
// AsEnumerableでDataTableを列挙可能に変換
var enumerableRows = dt.AsEnumerable();
foreach (var row in enumerableRows)
{
Console.WriteLine($"ID: {row.Field<int>("ID")}, Name: {row.Field<string>("Name")}");
}
}
}
ID: 1, Name: 田中
ID: 2, Name: 佐藤
ID: 3, Name: 鈴木
このようにAsEnumerable
を使うことで、DataTable
の行をLINQで扱える形に変換し、Field<T>
メソッドで列の値を取得しています。
System.Data.DataSetExtensionsの参照追加
AsEnumerable
メソッドはSystem.Data.DataSetExtensions
アセンブリに定義されています。
プロジェクトでLINQを使ってDataTable
を操作する場合は、このアセンブリへの参照が必要です。
Visual Studioでの参照追加手順は以下の通りです。
- ソリューションエクスプローラーでプロジェクトを右クリックし、「参照の追加」を選択。
- 「アセンブリ」→「フレームワーク」タブを開きます。
System.Data.DataSetExtensions
にチェックを入れて「OK」をクリック。
また、コードファイルの先頭に以下の名前空間を追加してください。
using System.Data;
using System.Data.DataSetExtensions; // 通常は不要ですが、明示的に書く場合
using System.Linq;
通常はusing System.Linq;
だけでAsEnumerable
が使えますが、参照がないとコンパイルエラーになるため注意してください。
.NET Coreや.NET 5以降の環境では、System.Data.DataSetExtensions
はNuGetパッケージとして提供されている場合があります。
その場合は、NuGetパッケージマネージャーからSystem.Data.DataSetExtensions
をインストールしてください。
Fieldメソッドで型安全にアクセス
DataRow
の列データにアクセスする際、従来はrow["列名"]
のようにインデクサーを使って値を取得していました。
しかし、この方法は戻り値がobject
型であるため、明示的なキャストが必要で、型の不一致による例外が発生しやすくなります。
LINQと組み合わせて使う場合は、Field<T>
拡張メソッドを使うことで、指定した型で安全に値を取得できます。
Field<T>
はDBNull
を自動的に扱い、null
許容型にも対応しているため、より堅牢なコードが書けます。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("ID", typeof(int));
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add(1, "田中", 28);
dt.Rows.Add(2, "佐藤", DBNull.Value); // 年齢不明
foreach (DataRow row in dt.Rows)
{
int id = row.Field<int>("ID");
string name = row.Field<string>("Name");
int? age = row.Field<int?>("Age"); // null許容型で取得
Console.WriteLine($"ID: {id}, Name: {name}, Age: {(age.HasValue ? age.Value.ToString() : "不明")}");
}
}
}
ID: 1, Name: 田中, Age: 28
ID: 2, Name: 佐藤, Age: 不明
この例では、Age
列にDBNull.Value
が含まれていても、Field<int?>
で安全にnull
として扱えます。
Field<T>
を使うことで、キャストミスやDBNull
の扱いに起因する例外を防げるため、LINQクエリ内でも安心して列の値を取得できます。
匿名型と強く型付けしたクラスの選択
LINQのクエリ結果を格納する際、匿名型と強く型付けしたクラス(POCO: Plain Old CLR Object)のどちらを使うかは、用途や規模によって使い分けます。
匿名型の特徴
- クエリ内で簡単に作成できる
- プロパティは読み取り専用で、型はコンパイラが推論
- メソッドの戻り値やローカル変数で使うのに適している
- クラス名がないため、メソッドの外に持ち出せない
匿名型は、簡単な一時的なデータの集約や表示用に便利です。
たとえば、DataTable
の特定の列だけを抽出して表示する場合に使います。
var query = dt.AsEnumerable()
.Select(row => new
{
ID = row.Field<int>("ID"),
Name = row.Field<string>("Name")
});
foreach (var item in query)
{
Console.WriteLine($"ID: {item.ID}, Name: {item.Name}");
}
強く型付けしたクラスの特徴
- クラス名があり、メソッドの引数や戻り値、フィールドとして使える
- プロパティの読み書きが可能
- 複雑なロジックや再利用性の高いコードに向いている
大規模なアプリケーションや複数のメソッド間でデータを受け渡す場合は、強く型付けしたクラスを定義するほうが保守性が高まります。
class Person
{
public int ID { get; set; }
public string Name { get; set; }
}
var persons = dt.AsEnumerable()
.Select(row => new Person
{
ID = row.Field<int>("ID"),
Name = row.Field<string>("Name")
})
.ToList();
foreach (var person in persons)
{
Console.WriteLine($"ID: {person.ID}, Name: {person.Name}");
}
使い分けのポイント
利用シーン | 匿名型 | 強く型付けクラス |
---|---|---|
一時的なデータ操作や表示 | 適している | 不向き |
複数メソッド間でデータを受け渡す | 不向き | 適している |
プロパティの読み書きが必要 | 不可(読み取り専用) | 可能 |
コードの保守性や拡張性 | 低い | 高い |
このように、簡単なクエリ結果の表示や一時的な操作には匿名型を使い、複雑な処理や再利用が必要な場合は強く型付けしたクラスを使うのが一般的です。
用途に応じて適切に選択してください。
フィルタリングの実践
where句で条件抽出
LINQのwhere
句は、DataTable
の行を条件に基づいて抽出する際に使います。
AsEnumerable
でDataTable
を列挙可能に変換した後、where
句で条件を指定して必要な行だけを取得できます。
単一条件
単一条件のフィルタリングは最も基本的な使い方です。
たとえば、Age
列が30以上の行だけを抽出する場合は以下のように記述します。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add("田中", 28);
dt.Rows.Add("佐藤", 35);
dt.Rows.Add("鈴木", 40);
var filteredRows = dt.AsEnumerable()
.Where(row => row.Field<int>("Age") >= 30);
foreach (var row in filteredRows)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
}
}
Name: 佐藤, Age: 35
Name: 鈴木, Age: 40
このコードでは、Age
が30以上の行だけを抽出し、名前と年齢を表示しています。
Field<T>
で型安全に列の値を取得している点もポイントです。
複数条件
複数の条件を組み合わせる場合は、論理演算子&&
(AND)や||
(OR)を使います。
たとえば、Age
が30以上かつName
が「佐藤」である行を抽出する例です。
var filteredRows = dt.AsEnumerable()
.Where(row => row.Field<int>("Age") >= 30 && row.Field<string>("Name") == "佐藤");
完全なコード例は以下の通りです。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add("田中", 28);
dt.Rows.Add("佐藤", 35);
dt.Rows.Add("鈴木", 40);
var filteredRows = dt.AsEnumerable()
.Where(row => row.Field<int>("Age") >= 30 && row.Field<string>("Name") == "佐藤");
foreach (var row in filteredRows)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
}
}
Name: 佐藤, Age: 35
このように複数条件を組み合わせることで、より細かいフィルタリングが可能です。
AnyとAllで存在チェック
LINQのAny
とAll
は、条件に合致する行が存在するかどうかを判定するのに便利です。
Any
は、条件を満たす要素が1つでもあればtrue
を返しますAll
は、すべての要素が条件を満たす場合にtrue
を返します
たとえば、Age
が40以上の人がいるかどうかを調べる例です。
bool hasAge40OrMore = dt.AsEnumerable()
.Any(row => row.Field<int>("Age") >= 40);
Console.WriteLine($"Ageが40以上の人がいるか: {hasAge40OrMore}");
Ageが40以上の人がいるか: True
逆に、全員が30歳以上かどうかを調べる場合はAll
を使います。
bool allAge30OrMore = dt.AsEnumerable()
.All(row => row.Field<int>("Age") >= 30);
Console.WriteLine($"全員が30歳以上か: {allAge30OrMore}");
全員が30歳以上か: False
このように、Any
とAll
を使うことで、条件に合致する行の存在チェックを簡単に行えます。
Like検索をContainsやStartsWithで代替
DataTable.Select
メソッドのようにSQLのLIKE
検索はLINQにはありませんが、文字列の部分一致や前方一致はContains
やStartsWith
メソッドで代替できます。
たとえば、Name
列に「田」が含まれる行を抽出する場合はContains
を使います。
var filteredRows = dt.AsEnumerable()
.Where(row => row.Field<string>("Name").Contains("田"));
完全なコード例です。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add("田中", 28);
dt.Rows.Add("佐藤", 35);
dt.Rows.Add("鈴木", 40);
dt.Rows.Add("高田", 22);
var filteredRows = dt.AsEnumerable()
.Where(row => row.Field<string>("Name").Contains("田"));
foreach (var row in filteredRows)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
}
}
Name: 田中, Age: 28
Name: 高田, Age: 22
また、Name
が「佐」で始まる行を抽出する場合はStartsWith
を使います。
var filteredRows = dt.AsEnumerable()
.Where(row => row.Field<string>("Name").StartsWith("佐"));
このように、Contains
やStartsWith
を使うことで、SQLのLIKE
に近い柔軟な文字列検索が可能です。
大文字・小文字の区別を無視したい場合は、ToLower()
やToUpper()
を組み合わせて比較する方法もあります。
ソートの実践
OrderByとOrderByDescending
LINQのOrderBy
とOrderByDescending
は、DataTable
の行を指定した列の値で昇順または降順に並べ替えるために使います。
AsEnumerable
でDataTable
を列挙可能に変換した後、これらのメソッドを適用してソートを行います。
数値列の昇降順
数値列を昇順に並べ替えるにはOrderBy
を使い、降順に並べ替えるにはOrderByDescending
を使います。
たとえば、Age
列を基準に昇順・降順で並べ替える例を示します。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add("田中", 28);
dt.Rows.Add("佐藤", 35);
dt.Rows.Add("鈴木", 22);
dt.Rows.Add("高橋", 40);
// Age列で昇順に並べ替え
var ascending = dt.AsEnumerable()
.OrderBy(row => row.Field<int>("Age"));
Console.WriteLine("Age昇順:");
foreach (var row in ascending)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
// Age列で降順に並べ替え
var descending = dt.AsEnumerable()
.OrderByDescending(row => row.Field<int>("Age"));
Console.WriteLine("\nAge降順:");
foreach (var row in descending)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
}
}
Age昇順:
Name: 鈴木, Age: 22
Name: 田中, Age: 28
Name: 佐藤, Age: 35
Name: 高橋, Age: 40
Age降順:
Name: 高橋, Age: 40
Name: 佐藤, Age: 35
Name: 田中, Age: 28
Name: 鈴木, Age: 22
このように、OrderBy
は指定した列の値を小さい順に並べ替え、OrderByDescending
は大きい順に並べ替えます。
文字列列の昇降順
文字列列も同様にOrderBy
とOrderByDescending
で並べ替えられます。
たとえば、Name
列を昇順・降順で並べ替える例です。
var ascendingByName = dt.AsEnumerable()
.OrderBy(row => row.Field<string>("Name"));
Console.WriteLine("Name昇順:");
foreach (var row in ascendingByName)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
var descendingByName = dt.AsEnumerable()
.OrderByDescending(row => row.Field<string>("Name"));
Console.WriteLine("\nName降順:");
foreach (var row in descendingByName)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
Name昇順:
Name: 佐藤, Age: 35
Name: 鈴木, Age: 22
Name: 高橋, Age: 40
Name: 田中, Age: 28
Name降順:
Name: 田中, Age: 28
Name: 高橋, Age: 40
Name: 鈴木, Age: 22
Name: 佐藤, Age: 35
文字列の並べ替えはデフォルトで辞書順(Unicode順)に行われます。
大文字・小文字の区別を無視したい場合は、StringComparer
を使ったカスタム比較も可能ですが、基本的な使い方ではOrderBy
とOrderByDescending
で十分です。
ThenByで二次ソート
OrderBy
やOrderByDescending
で一次ソートを行った後、さらに別の列で二次ソートを行うにはThenBy
やThenByDescending
を使います。
これにより、複数の列を基準にした安定したソートが可能です。
たとえば、Age
で昇順に並べ替えた後、同じ年齢の人はName
で昇順に並べ替える例です。
var sortedRows = dt.AsEnumerable()
.OrderBy(row => row.Field<int>("Age"))
.ThenBy(row => row.Field<string>("Name"));
Console.WriteLine("Age昇順、同じAgeはName昇順:");
foreach (var row in sortedRows)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
Age昇順、同じAgeはName昇順:
Name: 鈴木, Age: 22
Name: 田中, Age: 28
Name: 佐藤, Age: 35
Name: 高橋, Age: 40
もしAge
で降順に並べ替え、同じ年齢の人はName
で降順に並べ替えたい場合は、以下のように書きます。
var sortedRowsDesc = dt.AsEnumerable()
.OrderByDescending(row => row.Field<int>("Age"))
.ThenByDescending(row => row.Field<string>("Name"));
Console.WriteLine("Age降順、同じAgeはName降順:");
foreach (var row in sortedRowsDesc)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
Age降順、同じAgeはName降順:
Name: 高橋, Age: 40
Name: 佐藤, Age: 35
Name: 田中, Age: 28
Name: 鈴木, Age: 22
ThenBy
やThenByDescending
は、一次ソートの結果を保持しつつ、同じキーの要素をさらに細かく並べ替えるために使います。
複数の列でのソートが必要な場合は積極的に活用してください。
集計の実践
CountとLongCount
LINQのCount
メソッドは、条件に合致する要素の数を取得するのに使います。
LongCount
はCount
と同様ですが、戻り値がlong
型で、大量のデータを扱う場合に適しています。
たとえば、DataTable
の中でAge
が30以上の行数を数える例です。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add("田中", 28);
dt.Rows.Add("佐藤", 35);
dt.Rows.Add("鈴木", 40);
dt.Rows.Add("高橋", 22);
// Ageが30以上の行数をカウント
int count = dt.AsEnumerable()
.Count(row => row.Field<int>("Age") >= 30);
long longCount = dt.AsEnumerable()
.LongCount(row => row.Field<int>("Age") >= 30);
Console.WriteLine($"Count: {count}");
Console.WriteLine($"LongCount: {longCount}");
}
}
Count: 2
LongCount: 2
Count
はint
型の戻り値なので、数百万件以上のデータを扱う場合はLongCount
を使うと安全です。
SumとAverage
Sum
は指定した列の値の合計を計算し、Average
は平均値を計算します。
数値列に対して使うことが多いです。
以下は、Age
列の合計と平均を計算する例です。
var totalAge = dt.AsEnumerable()
.Sum(row => row.Field<int>("Age"));
var averageAge = dt.AsEnumerable()
.Average(row => row.Field<int>("Age"));
Console.WriteLine($"合計年齢: {totalAge}");
Console.WriteLine($"平均年齢: {averageAge:F2}");
合計年齢: 125
平均年齢: 31.25
Average
の戻り値はdouble
型なので、小数点以下の桁数を指定して表示することも可能です。
MinとMax
Min
とMax
は、指定した列の最小値と最大値を取得します。
たとえば、Age
列の最小値と最大値を求める例です。
var minAge = dt.AsEnumerable()
.Min(row => row.Field<int>("Age"));
var maxAge = dt.AsEnumerable()
.Max(row => row.Field<int>("Age"));
Console.WriteLine($"最小年齢: {minAge}");
Console.WriteLine($"最大年齢: {maxAge}");
最小年齢: 22
最大年齢: 40
これらのメソッドは、数値だけでなく文字列の最小・最大(辞書順)にも使えます。
集計結果を匿名型へ格納
複数の集計結果をまとめて扱いたい場合は、匿名型に格納すると便利です。
たとえば、Age
列の合計、平均、最小、最大を一度に取得して匿名型にまとめる例です。
var summary = dt.AsEnumerable()
.Aggregate(new
{
Count = 0,
Sum = 0,
Min = int.MaxValue,
Max = int.MinValue
},
(acc, row) => new
{
Count = acc.Count + 1,
Sum = acc.Sum + row.Field<int>("Age"),
Min = Math.Min(acc.Min, row.Field<int>("Age")),
Max = Math.Max(acc.Max, row.Field<int>("Age"))
});
double average = (double)summary.Sum / summary.Count;
Console.WriteLine($"件数: {summary.Count}");
Console.WriteLine($"合計: {summary.Sum}");
Console.WriteLine($"平均: {average:F2}");
Console.WriteLine($"最小: {summary.Min}");
Console.WriteLine($"最大: {summary.Max}");
件数: 4
合計: 125
平均: 31.25
最小: 22
最大: 40
または、LINQのCount
、Sum
、Min
、Max
を個別に呼び出して匿名型にまとめる方法もあります。
var summary2 = new
{
Count = dt.AsEnumerable().Count(),
Sum = dt.AsEnumerable().Sum(row => row.Field<int>("Age")),
Min = dt.AsEnumerable().Min(row => row.Field<int>("Age")),
Max = dt.AsEnumerable().Max(row => row.Field<int>("Age")),
Average = dt.AsEnumerable().Average(row => row.Field<int>("Age"))
};
Console.WriteLine($"件数: {summary2.Count}");
Console.WriteLine($"合計: {summary2.Sum}");
Console.WriteLine($"平均: {summary2.Average:F2}");
Console.WriteLine($"最小: {summary2.Min}");
Console.WriteLine($"最大: {summary2.Max}");
件数: 4
合計: 125
平均: 31.25
最小: 22
最大: 40
このように匿名型に集計結果をまとめることで、複数の値を一括で管理しやすくなります。
UI表示やレポート作成時に便利です。
グループ化の実践
GroupByの基本構文
LINQのGroupBy
メソッドは、指定したキーに基づいてデータをグループ化します。
DataTable
の行をグループ化する際は、AsEnumerable
で列挙可能に変換した後、GroupBy
を使って特定の列の値をキーにグループを作成します。
基本的な構文は以下の通りです。
var grouped = dt.AsEnumerable()
.GroupBy(row => row.Field<string>("列名"));
たとえば、Department
列でグループ化し、各グループの人数をカウントする例です。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Department", typeof(string));
dt.Rows.Add("田中", "営業");
dt.Rows.Add("佐藤", "開発");
dt.Rows.Add("鈴木", "営業");
dt.Rows.Add("高橋", "開発");
dt.Rows.Add("伊藤", "総務");
var grouped = dt.AsEnumerable()
.GroupBy(row => row.Field<string>("Department"));
foreach (var group in grouped)
{
Console.WriteLine($"部署: {group.Key}, 人数: {group.Count()}");
}
}
}
部署: 営業, 人数: 2
部署: 開発, 人数: 2
部署: 総務, 人数: 1
このように、GroupBy
は指定した列の値ごとに行をまとめ、グループごとに集計や処理が可能です。
複数列をキーにする方法
複数の列を組み合わせてグループ化したい場合は、匿名型をキーとして指定します。
これにより、複数の列の組み合わせでグループを作成できます。
たとえば、Department
とAgeGroup
(年齢層)でグループ化する例です。
var grouped = dt.AsEnumerable()
.GroupBy(row => new
{
Department = row.Field<string>("Department"),
AgeGroup = row.Field<int>("Age") >= 30 ? "30歳以上" : "30歳未満"
});
foreach (var group in grouped)
{
Console.WriteLine($"部署: {group.Key.Department}, 年齢層: {group.Key.AgeGroup}, 人数: {group.Count()}");
}
完全なコード例を示します。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Department", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add("田中", "営業", 28);
dt.Rows.Add("佐藤", "開発", 35);
dt.Rows.Add("鈴木", "営業", 40);
dt.Rows.Add("高橋", "開発", 25);
dt.Rows.Add("伊藤", "総務", 30);
var grouped = dt.AsEnumerable()
.GroupBy(row => new
{
Department = row.Field<string>("Department"),
AgeGroup = row.Field<int>("Age") >= 30 ? "30歳以上" : "30歳未満"
});
foreach (var group in grouped)
{
Console.WriteLine($"部署: {group.Key.Department}, 年齢層: {group.Key.AgeGroup}, 人数: {group.Count()}");
}
}
}
部署: 営業, 年齢層: 30歳未満, 人数: 1
部署: 開発, 年齢層: 30歳以上, 人数: 1
部署: 営業, 年齢層: 30歳以上, 人数: 1
部署: 開発, 年齢層: 30歳未満, 人数: 1
部署: 総務, 年齢層: 30歳以上, 人数: 1
匿名型を使うことで、複数列の組み合わせをキーにした柔軟なグループ化が可能です。
グループ内集計
グループ化した後、各グループ内で集計を行うこともよくあります。
GroupBy
の結果はIGrouping<TKey, TElement>
のコレクションで、各グループは列挙可能な要素の集合です。
Count
やSum
、Average
などの集計メソッドを使えます。
たとえば、部署ごとの平均年齢を計算する例です。
var grouped = dt.AsEnumerable()
.GroupBy(row => row.Field<string>("Department"))
.Select(g => new
{
Department = g.Key,
Count = g.Count(),
AverageAge = g.Average(row => row.Field<int>("Age"))
});
foreach (var group in grouped)
{
Console.WriteLine($"部署: {group.Department}, 人数: {group.Count}, 平均年齢: {group.AverageAge:F1}");
}
部署: 営業, 人数: 2, 平均年齢: 34.0
部署: 開発, 人数: 2, 平均年齢: 30.0
部署: 総務, 人数: 1, 平均年齢: 30.0
このように、Select
で匿名型に集計結果をまとめて扱うことができます。
SelectManyでグループ展開
GroupBy
でグループ化した結果はグループごとにまとめられていますが、元の行単位で再び処理したい場合はSelectMany
を使ってグループを展開できます。
たとえば、部署ごとにグループ化した後、各グループの行を再度1つのシーケンスにまとめる例です。
var grouped = dt.AsEnumerable()
.GroupBy(row => row.Field<string>("Department"));
var flattened = grouped.SelectMany(g => g);
foreach (var row in flattened)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Department: {row.Field<string>("Department")}");
}
Name: 田中, Department: 営業
Name: 鈴木, Department: 営業
Name: 佐藤, Department: 開発
Name: 高橋, Department: 開発
Name: 伊藤, Department: 総務
SelectMany
はグループ化した複数のシーケンスを1つに結合する役割を持ち、グループ化後のデータを再度フラットに扱いたいときに便利です。
例えば、グループごとの集計結果と元データを組み合わせて処理する際などに活用できます。
結合の実践
Joinで内部結合
LINQのJoin
メソッドは、2つのデータソースを指定したキーで結合し、両方に存在する要素だけを結合結果として取得します。
これを内部結合(Inner Join)と呼びます。
DataTable
の行同士を結合する際にも使えます。
単一キー結合
単一の列をキーにして2つのDataTable
を結合する例を示します。
たとえば、社員情報テーブルと部署情報テーブルをDepartmentID
で結合し、社員名と部署名を取得します。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
// 社員テーブル
DataTable employees = new DataTable();
employees.Columns.Add("EmployeeID", typeof(int));
employees.Columns.Add("Name", typeof(string));
employees.Columns.Add("DepartmentID", typeof(int));
employees.Rows.Add(1, "田中", 10);
employees.Rows.Add(2, "佐藤", 20);
employees.Rows.Add(3, "鈴木", 10);
employees.Rows.Add(4, "高橋", 30);
// 部署テーブル
DataTable departments = new DataTable();
departments.Columns.Add("DepartmentID", typeof(int));
departments.Columns.Add("DepartmentName", typeof(string));
departments.Rows.Add(10, "営業");
departments.Rows.Add(20, "開発");
departments.Rows.Add(30, "総務");
// Joinで内部結合
var query = employees.AsEnumerable()
.Join(departments.AsEnumerable(),
emp => emp.Field<int>("DepartmentID"),
dept => dept.Field<int>("DepartmentID"),
(emp, dept) => new
{
EmployeeName = emp.Field<string>("Name"),
DepartmentName = dept.Field<string>("DepartmentName")
});
foreach (var item in query)
{
Console.WriteLine($"社員名: {item.EmployeeName}, 部署名: {item.DepartmentName}");
}
}
}
社員名: 田中, 部署名: 営業
社員名: 佐藤, 部署名: 開発
社員名: 鈴木, 部署名: 営業
社員名: 高橋, 部署名: 総務
このコードでは、employees
とdepartments
のDepartmentID
をキーにして結合し、社員名と部署名を取得しています。
Join
の引数は、左側のキーセレクター、右側のキーセレクター、そして結合結果の生成方法です。
複数キー結合
複数の列を組み合わせてキーにしたい場合は、匿名型を使ってキーを作成します。
たとえば、社員テーブルとプロジェクト割当テーブルをDepartmentID
とProjectID
の組み合わせで結合する例です。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
// 社員テーブル
DataTable employees = new DataTable();
employees.Columns.Add("EmployeeID", typeof(int));
employees.Columns.Add("Name", typeof(string));
employees.Columns.Add("DepartmentID", typeof(int));
employees.Columns.Add("ProjectID", typeof(int));
employees.Rows.Add(1, "田中", 10, 100);
employees.Rows.Add(2, "佐藤", 20, 200);
employees.Rows.Add(3, "鈴木", 10, 100);
employees.Rows.Add(4, "高橋", 30, 300);
// プロジェクト割当テーブル
DataTable assignments = new DataTable();
assignments.Columns.Add("DepartmentID", typeof(int));
assignments.Columns.Add("ProjectID", typeof(int));
assignments.Columns.Add("ProjectName", typeof(string));
assignments.Rows.Add(10, 100, "プロジェクトA");
assignments.Rows.Add(20, 200, "プロジェクトB");
assignments.Rows.Add(30, 300, "プロジェクトC");
// 複数キーでJoin
var query = employees.AsEnumerable()
.Join(assignments.AsEnumerable(),
emp => new { Dept = emp.Field<int>("DepartmentID"), Proj = emp.Field<int>("ProjectID") },
assign => new { Dept = assign.Field<int>("DepartmentID"), Proj = assign.Field<int>("ProjectID") },
(emp, assign) => new
{
EmployeeName = emp.Field<string>("Name"),
ProjectName = assign.Field<string>("ProjectName")
});
foreach (var item in query)
{
Console.WriteLine($"社員名: {item.EmployeeName}, プロジェクト名: {item.ProjectName}");
}
}
}
社員名: 田中, プロジェクト名: プロジェクトA
社員名: 佐藤, プロジェクト名: プロジェクトB
社員名: 鈴木, プロジェクト名: プロジェクトA
社員名: 高橋, プロジェクト名: プロジェクトC
匿名型を使うことで、複数の列をキーにした結合が簡単に実現できます。
GroupJoinとDefaultIfEmptyで外部結合
LINQのGroupJoin
は、左側のデータソースの各要素に対して右側の関連する要素のグループを結合します。
これを使うと、SQLの外部結合(特に左外部結合)を表現できます。
DefaultIfEmpty
を組み合わせることで、右側に対応する要素がない場合でも左側の要素を結果に含めることができます。
以下は、社員テーブルと部署テーブルを左外部結合し、部署が割り当てられていない社員も含めて表示する例です。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
// 社員テーブル
DataTable employees = new DataTable();
employees.Columns.Add("EmployeeID", typeof(int));
employees.Columns.Add("Name", typeof(string));
employees.Columns.Add("DepartmentID", typeof(int));
employees.Rows.Add(1, "田中", 10);
employees.Rows.Add(2, "佐藤", 20);
employees.Rows.Add(3, "鈴木", DBNull.Value); // 部署なし
employees.Rows.Add(4, "高橋", 30);
// 部署テーブル
DataTable departments = new DataTable();
departments.Columns.Add("DepartmentID", typeof(int));
departments.Columns.Add("DepartmentName", typeof(string));
departments.Rows.Add(10, "営業");
departments.Rows.Add(20, "開発");
departments.Rows.Add(30, "総務");
// GroupJoinで左外部結合
var query = employees.AsEnumerable()
.GroupJoin(departments.AsEnumerable(),
emp => emp.Field<int?>("DepartmentID"),
dept => dept.Field<int>("DepartmentID"),
(emp, depts) => new
{
EmployeeName = emp.Field<string>("Name"),
Department = depts.DefaultIfEmpty()
.Select(d => d == null ? "未所属" : d.Field<string>("DepartmentName"))
.First()
});
foreach (var item in query)
{
Console.WriteLine($"社員名: {item.EmployeeName}, 部署名: {item.Department}");
}
}
}
社員名: 田中, 部署名: 営業
社員名: 佐藤, 部署名: 開発
社員名: 鈴木, 部署名: 未所属
社員名: 高橋, 部署名: 総務
このコードでは、GroupJoin
で社員ごとに対応する部署のグループを取得し、DefaultIfEmpty
で部署がない場合にnull
を返すようにしています。
Select
でnull
チェックを行い、部署がない場合は「未所属」と表示しています。
この方法で、SQLの左外部結合と同様の結果をLINQで実現できます。
結果をDataTableへ戻す
CopyToDataTableで結果を再構築
LINQでDataTable
の行をフィルタリングやソート、結合などの操作を行った後、結果を再びDataTable
として扱いたい場合があります。
CopyToDataTable
メソッドを使うと、LINQのクエリ結果IEnumerable<DataRow>
を簡単に新しいDataTable
に変換できます。
CopyToDataTable
は、元のDataTable
のスキーマを引き継いだ新しいDataTable
を作成し、クエリ結果の行をコピーします。
たとえば、Age
が30以上の行だけを抽出し、新しいDataTable
に格納する例です。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add("田中", 28);
dt.Rows.Add("佐藤", 35);
dt.Rows.Add("鈴木", 40);
dt.Rows.Add("高橋", 25);
// LINQでフィルタリング
var filteredRows = dt.AsEnumerable()
.Where(row => row.Field<int>("Age") >= 30);
// CopyToDataTableで新しいDataTableに変換
DataTable filteredTable = filteredRows.CopyToDataTable();
// 結果の表示
foreach (DataRow row in filteredTable.Rows)
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
}
}
Name: 佐藤, Age: 35
Name: 鈴木, Age: 40
注意点として、CopyToDataTable
は空のシーケンスに対して呼び出すと例外が発生します。
空の場合に備えて、Any()
で要素があるか確認するか、例外処理を行うことが推奨されます。
DataTable filteredTable = filteredRows.Any() ? filteredRows.CopyToDataTable() : dt.Clone();
このように、CopyToDataTable
はLINQの結果をDataTable
に戻す際に非常に便利です。
新規スキーマを作成する手順
LINQのクエリ結果が匿名型や異なるスキーマのデータの場合、CopyToDataTable
は使えません。
その場合は、新しいDataTable
を手動で作成し、スキーマ(列定義)を設定してからデータを追加する必要があります。
以下は、匿名型のクエリ結果を新しいDataTable
に変換する例です。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Columns.Add("Department", typeof(string));
dt.Rows.Add("田中", 28, "営業");
dt.Rows.Add("佐藤", 35, "開発");
dt.Rows.Add("鈴木", 40, "営業");
dt.Rows.Add("高橋", 25, "開発");
// 匿名型でクエリ結果を作成(NameとDepartmentのみ抽出)
var query = dt.AsEnumerable()
.Select(row => new
{
Name = row.Field<string>("Name"),
Department = row.Field<string>("Department")
});
// 新しいDataTableを作成しスキーマを定義
DataTable newTable = new DataTable();
newTable.Columns.Add("Name", typeof(string));
newTable.Columns.Add("Department", typeof(string));
// クエリ結果をDataTableに追加
foreach (var item in query)
{
var newRow = newTable.NewRow();
newRow["Name"] = item.Name;
newRow["Department"] = item.Department;
newTable.Rows.Add(newRow);
}
// 結果の表示
foreach (DataRow row in newTable.Rows)
{
Console.WriteLine($"Name: {row["Name"]}, Department: {row["Department"]}");
}
}
}
Name: 田中, Department: 営業
Name: 佐藤, Department: 開発
Name: 鈴木, Department: 営業
Name: 高橋, Department: 開発
この方法では、以下の手順を踏みます。
- 新しい
DataTable
を作成。 - 必要な列を
Columns.Add
で定義。 - LINQのクエリ結果をループで回し、
NewRow
で新しい行を作成。 - 各列に値をセットして
Rows.Add
で追加。
この手順により、元のDataTable
とは異なるスキーマのテーブルを自由に作成できます。
匿名型や複数のテーブルを結合した結果など、CopyToDataTable
が使えないケースで有効です。
パフォーマンス最適化
LINQとDataTable.Selectの速度比較
DataTable
のデータをフィルタリングする方法として、DataTable.Select
メソッドとLINQのAsEnumerable
+Where
を使う方法があります。
どちらも同じ目的で使えますが、パフォーマンス面で違いがあるため、状況に応じて使い分けることが重要です。
DataTable.Select
は内部的に文字列で条件を解析し、DataRow[]
を返します。
一方、LINQは型安全で柔軟なクエリが書けますが、AsEnumerable
での変換やラムダ式の評価に若干のオーバーヘッドがあります。
以下は簡単な速度比較の例です。
using System;
using System.Data;
using System.Diagnostics;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("ID", typeof(int));
dt.Columns.Add("Value", typeof(int));
// 大量データの追加
for (int i = 0; i < 100000; i++)
{
dt.Rows.Add(i, i % 100);
}
Stopwatch sw = new Stopwatch();
// DataTable.Selectの速度計測
sw.Start();
DataRow[] selectRows = dt.Select("Value >= 50");
sw.Stop();
Console.WriteLine($"Select() 処理時間: {sw.ElapsedMilliseconds} ms");
// LINQの速度計測
sw.Restart();
var linqRows = dt.AsEnumerable().Where(row => row.Field<int>("Value") >= 50).ToList();
sw.Stop();
Console.WriteLine($"LINQ 処理時間: {sw.ElapsedMilliseconds} ms");
}
}
Select() 処理時間: 35 ms
LINQ 処理時間: 6 ms
実行結果は環境によって異なりますが、一般的には以下の傾向があります。
メソッド | 処理速度の特徴 |
---|---|
DataTable.Select | 条件が単純であれば高速だが、文字列解析のオーバーヘッドあり |
LINQ | 柔軟で型安全、複雑な条件に強いが若干遅い場合がある |
大量データで単純な条件の場合はSelect
が速いこともありますが、複雑な条件や型安全性を重視するならLINQが適しています。
遅延実行と即時実行の使い分け
LINQは遅延実行(Deferred Execution)を基本としています。
つまり、クエリは定義しただけでは実行されず、結果を列挙したり、ToList()
やToArray()
などの即時実行メソッドを呼ぶまで処理が遅延されます。
遅延実行のメリット
- 不要な処理を避けられる
実際に必要なデータだけを処理するため、効率的です。
- 複数のクエリを組み合わせて最適化できる
即時実行のメリット
- クエリ結果を固定化できる
データの変更に影響されずに結果を保持したい場合に有効です。
- 複数回の列挙によるパフォーマンス低下を防げる
使い分け例
var query = dt.AsEnumerable()
.Where(row => row.Field<int>("Value") >= 50);
// 遅延実行:ここではまだ処理されていない
var list = query.ToList(); // 即時実行:ここで処理が実行される
// 以降はlistを使うことで複数回の処理を避けられる
大量データを何度も処理する場合は、ToList()
などで即時実行して結果をキャッシュするのがパフォーマンス向上につながります。
メモリ消費を抑えるコツ
大量のデータを扱う際は、メモリ消費を抑える工夫が必要です。
以下のポイントを意識してください。
- 必要な列だけを選択する
Select
で必要な列だけを抽出し、不要なデータを持ち回らないようにします。
- 遅延実行を活用する
必要なデータだけを処理し、途中で処理を止められるようにします。
ToList()
やToArray()
の多用を避ける
不要に即時実行して大量のデータをメモリに保持しない。
CopyToDataTable
の使用に注意
大量の行をコピーするとメモリを大量に消費するため、必要な範囲で使います。
IEnumerable
のまま処理を続ける
可能な限りIEnumerable<DataRow>
のまま処理し、メモリの節約を図ります。
- 部分的な処理やページングを検討する
大量データは一度に処理せず、分割して処理する方法も有効です。
これらを踏まえ、LINQとDataTable
を組み合わせる際は、処理内容やデータ量に応じて適切な方法を選択し、パフォーマンスとメモリ効率のバランスを取ることが重要です。
nullとDBNullの扱い
DBNull.Valueの検出と置換
DataTable
やDataRow
で扱うデータベース由来の値には、null
ではなくDBNull.Value
が使われます。
これは、データベースの「値なし(NULL)」を表現するための特別なオブジェクトです。
null
とは異なるため、null
チェックだけでは空の値を検出できません。
DBNull.Value
を検出するには、DataRow
のIsNull
メソッドや値の比較を使います。
using System;
using System.Data;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add("田中", 28);
dt.Rows.Add("佐藤", DBNull.Value); // 年齢不明
foreach (DataRow row in dt.Rows)
{
if (row.IsNull("Age"))
{
Console.WriteLine($"{row["Name"]}さんの年齢は不明です。");
}
else
{
Console.WriteLine($"{row["Name"]}さんの年齢は{row["Age"]}歳です。");
}
}
}
}
田中さんの年齢は28歳です。
佐藤さんの年齢は不明です。
IsNull
メソッドは指定した列がDBNull.Value
かどうかを判定します。
これにより、DBNull.Value
を安全に検出できます。
また、DBNull.Value
を別の値に置換したい場合は、IsNull
で判定してから代替値を設定します。
foreach (DataRow row in dt.Rows)
{
int age = row.IsNull("Age") ? -1 : (int)row["Age"];
Console.WriteLine($"{row["Name"]}さんの年齢は{(age == -1 ? "不明" : age.ToString())}です。");
}
このように、DBNull.Value
を検出して適切に置換することで、後続の処理で例外を防ぎ、扱いやすいデータに変換できます。
Field拡張メソッドとNull許容型
LINQと組み合わせてDataRow
の列値を取得する際は、Field<T>
拡張メソッドを使うのが一般的です。
Field<T>
はDBNull.Value
を自動的にnull
に変換し、null
許容型(Nullable)にも対応しています。
たとえば、Age
列がDBNull.Value
の場合にnull
として扱うには、int?
Nullable<int>
を指定します。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add("田中", 28);
dt.Rows.Add("佐藤", DBNull.Value);
var query = dt.AsEnumerable()
.Select(row => new
{
Name = row.Field<string>("Name"),
Age = row.Field<int?>("Age") // Null許容型で取得
});
foreach (var item in query)
{
string ageText = item.Age.HasValue ? item.Age.Value.ToString() : "不明";
Console.WriteLine($"{item.Name}さんの年齢は{ageText}です。");
}
}
}
田中さんの年齢は28です。
佐藤さんの年齢は不明です。
Field<T>
は内部でDBNull.Value
をnull
に変換するため、int?
のようなNull許容型を使うと、DBNull.Value
を安全に扱えます。
これにより、DBNull.Value
の判定やキャストの手間が省け、コードがシンプルになります。
また、文字列型の場合もField<string>
はDBNull.Value
をnull
に変換するため、null
チェックで空データを判定できます。
このように、Field<T>
拡張メソッドとNull許容型を組み合わせることで、DataTable
のDBNull.Value
を自然に扱い、例外の発生を防ぎつつ安全にデータ操作が可能です。
例外対策
型変換エラーの回避
DataTable
の列データをLINQで操作する際、型変換エラーが発生しやすいポイントは、DataRow
から値を取得するときのキャストやField<T>
の型指定です。
特に、列のデータ型と異なる型でアクセスしたり、DBNull.Value
を適切に処理しなかった場合に例外が起こります。
型変換エラーを回避するためのポイントは以下の通りです。
Field<T>
メソッドで正しい型を指定する
例えば、int
型の列に対してはField<int>
、文字列列にはField<string>
を使います。
型が合わないとInvalidCastException
が発生します。
DBNull.Value
を考慮してNull許容型を使う
DBNull.Value
が含まれる可能性がある列は、Field<int?>
やField<DateTime?>
などNull許容型で取得すると安全です。
- 列の型を事前に確認する
DataColumn.DataType
プロパティで列の型を確認し、適切な型でアクセスします。
- 例外が発生しそうな箇所は
try-catch
で囲む
予期しないデータが混入している場合に備え、例外処理を行います。
以下は、Field<T>
で型変換エラーを防ぐ例です。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("ID", typeof(int));
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(int));
dt.Rows.Add(1, "田中", 28);
dt.Rows.Add(2, "佐藤", DBNull.Value); // 年齢不明
foreach (DataRow row in dt.Rows)
{
try
{
int? age = row.Field<int?>("Age"); // Null許容型で安全に取得
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {(age.HasValue ? age.Value.ToString() : "不明")}");
}
catch (InvalidCastException ex)
{
Console.WriteLine($"型変換エラー: {ex.Message}");
}
}
}
}
Name: 田中, Age: 28
Name: 佐藤, Age: 不明
このように、Field<T>
とNull許容型を組み合わせることで、DBNull.Value
や型不一致による例外を防げます。
クエリ評価時の例外処理
LINQクエリは遅延実行のため、クエリの定義時には例外が発生せず、実際に列挙(foreach
やToList()
など)したときに例外が発生することがあります。
これにより、例外処理のタイミングが重要になります。
クエリ評価時の例外を適切に処理するためには、以下の方法があります。
- クエリの実行部分を
try-catch
で囲む
例外が発生する可能性のあるforeach
やToList()
の呼び出し部分で捕捉します。
- 個別の要素処理で例外を捕捉する
クエリ全体ではなく、各要素の処理で例外が起きる場合は、ループ内でtry-catch
を使います。
- 事前にデータの整合性をチェックする
可能な限り、クエリ実行前にデータの不整合や欠損を検出し、例外を未然に防ぎます。
以下は、クエリ評価時の例外処理例です。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
DataTable dt = new DataTable();
dt.Columns.Add("ID", typeof(int));
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("Age", typeof(object)); // 故意にobject型で不正データを混入
dt.Rows.Add(1, "田中", 28);
dt.Rows.Add(2, "佐藤", "不正なデータ"); // 型不一致
var query = dt.AsEnumerable()
.Where(row => {
try
{
int age = row.Field<int>("Age");
return age >= 20;
}
catch
{
// 型変換エラーがあれば除外
return false;
}
});
foreach (var row in query)
{
try
{
Console.WriteLine($"Name: {row.Field<string>("Name")}, Age: {row.Field<int>("Age")}");
}
catch (Exception ex)
{
Console.WriteLine($"データ処理エラー: {ex.Message}");
}
}
}
}
Name: 田中, Age: 28
この例では、Age
列に不正な文字列が混入していますが、Where
句内でtry-catch
を使い、型変換エラーのある行を除外しています。
さらに、foreach
内でも例外処理を行い、安全に処理を継続しています。
このように、LINQクエリの評価時に例外が発生する可能性がある場合は、適切に例外処理を組み込み、アプリケーションの安定性を確保してください。
まとめ
この記事では、C#のDataTable
とLINQを組み合わせてスマートにデータをフィルタリング、ソート、集計、グループ化、結合する方法を解説しました。
AsEnumerable
やField<T>
を活用し、型安全かつ効率的に操作するテクニックを紹介しています。
また、結果をDataTable
に戻す方法やパフォーマンス最適化、DBNull
の扱い、例外対策も詳述しました。
これらを理解することで、可読性と保守性の高いコードを書きつつ、実務でのデータ操作をより効果的に行えます。