C# Tips | コレクション・LINQ:Empty除外

C# C#
スポンサーリンク

はじめに:「Empty除外」は“中身がないものを先にどかす”テクニック

null を除外する話をしましたが、実務ではもう一つよく出てくる「やっかいな存在」がいます。
それが「空文字」や「空コレクション」、つまり「Empty」です。

文字列なら ""(空文字)
配列やリストなら「要素数 0」

これらは「存在はしているけど、中身がない」状態です。
業務ロジック的には「扱いたくない」「集計に入れたくない」ことが多いのに、
そのまま流してしまうと、あとで条件分岐が増えてコードがごちゃつきます。

そこで出てくるのが「Empty除外」という発想です。
「LINQ の入り口で、“中身が空のもの”を全部はじいてしまう」ことで、その後の処理をスッキリさせます。

ここでは、初心者向けに

文字列の Empty除外
コレクションの Empty除外
Empty除外用のユーティリティ拡張メソッド
業務での具体的な使いどころ

を、例題付きでかみ砕いて説明していきます。


文字列の Empty除外:IsNullOrEmpty / IsNullOrWhiteSpace を使いこなす

単純な空文字を除外する

まずは一番シンプルなパターンから。

using System;
using System.Collections.Generic;
using System.Linq;

var names = new List<string?>
{
    "Alice",
    "",
    "Bob",
    "",
    "Charlie"
};

var nonEmptyNames = names
    .Where(x => x != "");   // 空文字を除外

foreach (var name in nonEmptyNames)
{
    Console.WriteLine(name);
}
C#

出力はこうなります。

Alice
Bob
Charlie

""(空文字)がきれいに取り除かれています。

ここでの重要ポイントは、「null"" は別物」ということです。
null は「値そのものが存在しない」、"" は「値はあるけど中身が空」という違いがあります。
Empty除外では、この "" をどう扱うかを決めます。

実務では IsNullOrEmpty / IsNullOrWhiteSpace を使う

現実のデータは、"" だけでなく null" "(空白だけ)も混ざります。
それをまとめて扱うために、string.IsNullOrEmptystring.IsNullOrWhiteSpace を使うのが定番です。

var names = new List<string?>
{
    "Alice",
    "",
    "  ",
    null,
    "Bob"
};

var validNames = names
    .Where(x => !string.IsNullOrWhiteSpace(x));

foreach (var name in validNames)
{
    Console.WriteLine(name);
}
C#

出力はこうなります。

Alice
Bob

ここでの重要ポイントは、「Empty除外と Null除外を一緒にやるなら、IsNullOrWhiteSpace が一番ラク」ということです。
null""、空白だけの文字列を、まとめて「中身なし」とみなせます。


コレクションの Empty除外:Count / Any を使う

要素数 0 のリストを除外する

次は「空のコレクション」を除外するパターンです。

using System;
using System.Collections.Generic;
using System.Linq;

var lists = new List<List<int>>
{
    new List<int> { 1, 2, 3 },
    new List<int>(),          // 空
    new List<int> { 4 },
    new List<int>()           // 空
};

var nonEmptyLists = lists
    .Where(xs => xs.Any());   // 要素が 1 件以上あるものだけ

foreach (var xs in nonEmptyLists)
{
    Console.WriteLine($"[{string.Join(", ", xs)}]");
}
C#

出力はこうなります。

[1, 2, 3]
[4]

空のリストが除外されています。

ここでの重要ポイントは、「コレクションの Empty除外には Count == 0 ではなく Any() を使う」ということです。
Any() は「1 件でもあれば true」で、パフォーマンス的にも意図的にも素直です。

子コレクションが Empty のものを除外する

もう少し実務寄りの例を見てみます。

public class Order
{
    public string Id { get; set; } = "";
    public List<OrderLine> Lines { get; set; } = new();
}

public class OrderLine
{
    public string Item { get; set; } = "";
    public int Amount { get; set; }
}
C#
var orders = new List<Order>
{
    new Order
    {
        Id = "O001",
        Lines = new List<OrderLine>
        {
            new OrderLine { Item = "A", Amount = 100 }
        }
    },
    new Order
    {
        Id = "O002",
        Lines = new List<OrderLine>() // 空
    }
};
C#

「明細が 1 件もない注文は、集計対象から外したい」という場合、こう書けます。

var validOrders = orders
    .Where(o => o.Lines.Any());

foreach (var o in validOrders)
{
    Console.WriteLine(o.Id);
}
C#

出力はこうなります。

O001

ここでの重要ポイントは、「Empty除外は“業務的に意味のないデータを早めに落とす”ために使う」ということです。
「明細ゼロの注文は集計しない」といったルールを、そのまま LINQ に落とし込めます。


Empty除外用のユーティリティ拡張メソッドを作る

文字列用:NotEmpty / NotNullOrWhiteSpace

毎回 Where(x => !string.IsNullOrWhiteSpace(x)) と書くのは長いので、
ユーティリティとして 1 本にまとめておくと読みやすくなります。

using System.Collections.Generic;
using System.Linq;

public static class StringFilterExtensions
{
    public static IEnumerable<string> NotNullOrWhiteSpace(
        this IEnumerable<string?> source)
    {
        return source.Where(x => !string.IsNullOrWhiteSpace(x))!;
    }
}
C#

使い方はこうです。

var names = new List<string?>
{
    "Alice",
    "",
    "  ",
    null,
    "Bob"
};

var validNames = names.NotNullOrWhiteSpace();

foreach (var name in validNames)
{
    Console.WriteLine(name);
}
C#

ここでの重要ポイントは、「メソッド名に“意図”を乗せる」ことです。
NotNullOrWhiteSpace() と書いてあれば、「ここで null と空文字と空白だけを落としているんだな」と一目で分かります。

コレクション用:NotEmpty

コレクションに対しても同じ発想で拡張メソッドを作れます。

using System.Collections.Generic;
using System.Linq;

public static class CollectionFilterExtensions
{
    public static IEnumerable<IEnumerable<T>> NotEmpty<T>(
        this IEnumerable<IEnumerable<T>> source)
    {
        return source.Where(xs => xs.Any());
    }
}
C#

使い方はこうです。

var lists = new List<List<int>>
{
    new List<int> { 1, 2 },
    new List<int>(),
    new List<int> { 3 }
};

var nonEmptyLists = lists.NotEmpty();

foreach (var xs in nonEmptyLists)
{
    Console.WriteLine($"[{string.Join(", ", xs)}]");
}
C#

ここでの重要ポイントは、「NotEmpty() という名前だけで、“空のコレクションを除外している”ことが伝わる」ということです。
Where(xs => xs.Any()) よりも、業務コードとして読みやすくなります。


Null除外と Empty除外を組み合わせる

文字列の「null または空」をまとめて落とす

実務では、「null も空も空白だけも全部 NG」ということが多いです。
その場合は、Null除外と Empty除外を一緒にやります。

var names = new List<string?>
{
    "Alice",
    "",
    "  ",
    null,
    "Bob"
};

var validNames = names
    .Where(x => !string.IsNullOrWhiteSpace(x));

foreach (var name in validNames)
{
    Console.WriteLine(name);
}
C#

あるいは、ユーティリティを使うならこうです。

var validNames = names.NotNullOrWhiteSpace();
C#

ここでの重要ポイントは、「“null を許さないのか”“空文字を許さないのか”を最初に決めて、入り口でまとめて落とす」ということです。
後ろの処理で毎回 if を書くより、パイプラインの最初でフィルターしてしまったほうが、コードがきれいになります。

子コレクションが null または Empty のものを除外する

さきほどの Order の例で、Linesnull のこともある場合を考えます。

public class Order
{
    public string Id { get; set; } = "";
    public List<OrderLine>? Lines { get; set; }
}
C#
var orders = new List<Order>
{
    new Order
    {
        Id = "O001",
        Lines = new List<OrderLine>
        {
            new OrderLine { Item = "A", Amount = 100 }
        }
    },
    new Order
    {
        Id = "O002",
        Lines = null
    },
    new Order
    {
        Id = "O003",
        Lines = new List<OrderLine>()
    }
};
C#

「明細が 1 件もない注文(null も Empty も)は除外したい」なら、こう書けます。

var validOrders = orders
    .Where(o => o.Lines != null && o.Lines.Any());

foreach (var o in validOrders)
{
    Console.WriteLine(o.Id);
}
C#

出力はこうなります。

O001

ここでの重要ポイントは、「Null除外と Empty除外をセットで考える」ということです。
null だけ見ていると Empty を見落とし、Empty だけ見ていると null で落ちる、という事故が起きがちです。


業務での Empty除外の典型パターン

例1:検索条件の「空文字」を除外する

検索画面から複数条件が飛んでくるとき、
「ユーザーが入力していない項目(空文字)は条件に含めない」というのはよくある要件です。

public class SearchCondition
{
    public string? Name { get; set; }
    public string? Email { get; set; }
}
C#
IQueryable<User> ApplyCondition(IQueryable<User> query, SearchCondition cond)
{
    if (!string.IsNullOrWhiteSpace(cond.Name))
    {
        query = query.Where(u => u.Name.Contains(cond.Name));
    }

    if (!string.IsNullOrWhiteSpace(cond.Email))
    {
        query = query.Where(u => u.Email.Contains(cond.Email));
    }

    return query;
}
C#

ここでの重要ポイントは、「Empty除外をしないと、“空文字で検索する”という意味不明な条件が付いてしまう」ということです。
IsNullOrWhiteSpace で「実質的に入力されていない条件」を落としてから、クエリを組み立てます。

例2:ログやレポートから「中身のない行」を除外する

ログファイルやレポートを LINQ で加工するとき、
「空行」や「項目が全部空の行」は無視したいことが多いです。

var lines = File.ReadAllLines("log.txt");

var validLines = lines
    .Where(x => !string.IsNullOrWhiteSpace(x));

foreach (var line in validLines)
{
    Console.WriteLine(line);
}
C#

ここでの重要ポイントは、「Empty除外は“ノイズを先に落とす”ためのフィルター」として使える、ということです。
ノイズを落としてから本題の処理をすると、ロジックがシンプルになります。


まとめ:「Empty除外ユーティリティ」は“中身のないものを早めに追い出すフィルター”

Empty除外の本質は、

「後ろの処理で“中身がないケース”を毎回気にするくらいなら、
 最初に中身のないものを全部はじいてしまおう」

という発想です。

押さえておきたいポイントをもう一度整理すると、

文字列の Empty除外には !string.IsNullOrWhiteSpace(x) が実務的に便利
Null除外と Empty除外を一緒にやるなら、IsNullOrWhiteSpace 一択でいい場面が多い
コレクションの Empty除外には xs.Any() を使う(Count > 0 より素直)
NotNullOrWhiteSpace()NotEmpty() のような拡張メソッドを用意すると、意図が読みやすくなる
Empty除外は「ノイズを先に落とす」ための設計として、パイプラインの入り口に置く

ここまで腹落ちしていれば、
「とりあえず全部流して、途中で if でがんばる」段階から卒業して、
“null と empty を早めに追い出した、きれいな LINQ パイプライン”を、自分で組めるようになります。

タイトルとURLをコピーしました