LINQ

【C#】LINQとDataTableでスマートにフィルタ・ソート・集計する方法

LINQを使えばDataTableをスマートに絞り込み、ソート、集計できます。

AsEnumerableで行列を列挙型へ変換し、whereOrderByで条件指定や並び替え、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

このコードでは、IDNameAgeの3列を持つDataTableを作成し、3件のデータを追加しています。

Rowsコレクションを使って各行のデータを取得し、コンソールに表示しています。

LINQとは

LINQ(Language Integrated Query)は、C#やVB.NETに組み込まれたクエリ言語で、コレクションやデータベース、XMLなどのデータソースに対して統一的な方法で問い合わせや操作を行うことができます。

LINQを使うことで、複雑なデータ操作も簡潔で読みやすいコードで記述できるようになります。

LINQの主な特徴は以下の通りです。

  • 統一されたクエリ構文

配列やリスト、DataTable、データベース、XMLなど、さまざまなデータソースに対して同じ構文でクエリを記述できます。

  • 型安全でコンパイル時チェックが可能

クエリはコンパイル時に型チェックされるため、実行時エラーを減らせます。

  • 遅延実行

クエリの実行は必要になるまで遅延されるため、効率的な処理が可能です。

  • 拡張メソッドによる柔軟な操作

WhereSelectOrderByGroupByなど、多彩な拡張メソッドを使ってデータをフィルタリング、変換、並べ替え、集計できます。

LINQには主に2つの記法があります。

  • クエリ式(Query Syntax)

SQLに似た構文で、fromwhereselectなどのキーワードを使います。

  • メソッドチェーン(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

このように、AsEnumerableDataTableをLINQで扱える形に変換し、Field<T>で型安全に列の値を取得しながら、Whereで条件を指定し、OrderByで並べ替えています。

これにより、従来のループと条件分岐を使ったコードよりも簡潔で直感的な記述が可能です。

以上の理由から、DataTableとLINQを組み合わせることは、C#でのデータ操作をスマートに行うための有効な手段となっています。

準備作業

AsEnumerableで列挙型へ変換

DataTableはそのままではLINQのクエリ構文や拡張メソッドで直接操作できません。

LINQで扱うためには、DataTableの行を列挙可能な形式に変換する必要があります。

これを実現するのがAsEnumerableメソッドです。

AsEnumerableDataTableの拡張メソッドで、DataRowのシーケンスIEnumerable<DataRow>を返します。

これにより、LINQのWhereSelectOrderByなどのメソッドを使って柔軟にデータを操作できます。

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での参照追加手順は以下の通りです。

  1. ソリューションエクスプローラーでプロジェクトを右クリックし、「参照の追加」を選択。
  2. 「アセンブリ」→「フレームワーク」タブを開きます。
  3. 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の行を条件に基づいて抽出する際に使います。

AsEnumerableDataTableを列挙可能に変換した後、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のAnyAllは、条件に合致する行が存在するかどうかを判定するのに便利です。

  • 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

このように、AnyAllを使うことで、条件に合致する行の存在チェックを簡単に行えます。

Like検索をContainsやStartsWithで代替

DataTable.SelectメソッドのようにSQLのLIKE検索はLINQにはありませんが、文字列の部分一致や前方一致はContainsStartsWithメソッドで代替できます。

たとえば、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("佐"));

このように、ContainsStartsWithを使うことで、SQLのLIKEに近い柔軟な文字列検索が可能です。

大文字・小文字の区別を無視したい場合は、ToLower()ToUpper()を組み合わせて比較する方法もあります。

ソートの実践

OrderByとOrderByDescending

LINQのOrderByOrderByDescendingは、DataTableの行を指定した列の値で昇順または降順に並べ替えるために使います。

AsEnumerableDataTableを列挙可能に変換した後、これらのメソッドを適用してソートを行います。

数値列の昇降順

数値列を昇順に並べ替えるには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は大きい順に並べ替えます。

文字列列の昇降順

文字列列も同様にOrderByOrderByDescendingで並べ替えられます。

たとえば、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を使ったカスタム比較も可能ですが、基本的な使い方ではOrderByOrderByDescendingで十分です。

ThenByで二次ソート

OrderByOrderByDescendingで一次ソートを行った後、さらに別の列で二次ソートを行うにはThenByThenByDescendingを使います。

これにより、複数の列を基準にした安定したソートが可能です。

たとえば、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

ThenByThenByDescendingは、一次ソートの結果を保持しつつ、同じキーの要素をさらに細かく並べ替えるために使います。

複数の列でのソートが必要な場合は積極的に活用してください。

集計の実践

CountとLongCount

LINQのCountメソッドは、条件に合致する要素の数を取得するのに使います。

LongCountCountと同様ですが、戻り値が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

Countint型の戻り値なので、数百万件以上のデータを扱う場合は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

MinMaxは、指定した列の最小値と最大値を取得します。

たとえば、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のCountSumMinMaxを個別に呼び出して匿名型にまとめる方法もあります。

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は指定した列の値ごとに行をまとめ、グループごとに集計や処理が可能です。

複数列をキーにする方法

複数の列を組み合わせてグループ化したい場合は、匿名型をキーとして指定します。

これにより、複数の列の組み合わせでグループを作成できます。

たとえば、DepartmentAgeGroup(年齢層)でグループ化する例です。

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>のコレクションで、各グループは列挙可能な要素の集合です。

CountSumAverageなどの集計メソッドを使えます。

たとえば、部署ごとの平均年齢を計算する例です。

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}");
        }
    }
}
社員名: 田中, 部署名: 営業
社員名: 佐藤, 部署名: 開発
社員名: 鈴木, 部署名: 営業
社員名: 高橋, 部署名: 総務

このコードでは、employeesdepartmentsDepartmentIDをキーにして結合し、社員名と部署名を取得しています。

Joinの引数は、左側のキーセレクター、右側のキーセレクター、そして結合結果の生成方法です。

複数キー結合

複数の列を組み合わせてキーにしたい場合は、匿名型を使ってキーを作成します。

たとえば、社員テーブルとプロジェクト割当テーブルをDepartmentIDProjectIDの組み合わせで結合する例です。

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を返すようにしています。

Selectnullチェックを行い、部署がない場合は「未所属」と表示しています。

この方法で、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: 開発

この方法では、以下の手順を踏みます。

  1. 新しいDataTableを作成。
  2. 必要な列をColumns.Addで定義。
  3. LINQのクエリ結果をループで回し、NewRowで新しい行を作成。
  4. 各列に値をセットしてRows.Addで追加。

この手順により、元のDataTableとは異なるスキーマのテーブルを自由に作成できます。

匿名型や複数のテーブルを結合した結果など、CopyToDataTableが使えないケースで有効です。

パフォーマンス最適化

LINQとDataTable.Selectの速度比較

DataTableのデータをフィルタリングする方法として、DataTable.SelectメソッドとLINQのAsEnumerableWhereを使う方法があります。

どちらも同じ目的で使えますが、パフォーマンス面で違いがあるため、状況に応じて使い分けることが重要です。

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の検出と置換

DataTableDataRowで扱うデータベース由来の値には、nullではなくDBNull.Valueが使われます。

これは、データベースの「値なし(NULL)」を表現するための特別なオブジェクトです。

nullとは異なるため、nullチェックだけでは空の値を検出できません。

DBNull.Valueを検出するには、DataRowIsNullメソッドや値の比較を使います。

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.Valuenullに変換するため、int?のようなNull許容型を使うと、DBNull.Valueを安全に扱えます。

これにより、DBNull.Valueの判定やキャストの手間が省け、コードがシンプルになります。

また、文字列型の場合もField<string>DBNull.Valuenullに変換するため、nullチェックで空データを判定できます。

このように、Field<T>拡張メソッドとNull許容型を組み合わせることで、DataTableDBNull.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クエリは遅延実行のため、クエリの定義時には例外が発生せず、実際に列挙(foreachToList()など)したときに例外が発生することがあります。

これにより、例外処理のタイミングが重要になります。

クエリ評価時の例外を適切に処理するためには、以下の方法があります。

  • クエリの実行部分をtry-catchで囲む

例外が発生する可能性のあるforeachToList()の呼び出し部分で捕捉します。

  • 個別の要素処理で例外を捕捉する

クエリ全体ではなく、各要素の処理で例外が起きる場合は、ループ内で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を組み合わせてスマートにデータをフィルタリング、ソート、集計、グループ化、結合する方法を解説しました。

AsEnumerableField<T>を活用し、型安全かつ効率的に操作するテクニックを紹介しています。

また、結果をDataTableに戻す方法やパフォーマンス最適化、DBNullの扱い、例外対策も詳述しました。

これらを理解することで、可読性と保守性の高いコードを書きつつ、実務でのデータ操作をより効果的に行えます。

関連記事

Back to top button
目次へ