C# Tips | 文字列処理:行分割

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

はじめに 「行分割」は“テキストを“1行ずつの粒”にする基本技”

長いテキストを扱うとき、
「1行ずつ処理したい」「行番号を付けたい」「空行をスキップしたい」
といったニーズは、業務システムでも頻出です。

その入口になるのが 行分割(行ごとに文字列を分ける処理) です。

ただ、「とりあえず Split('\n')」と書くと、

改行コードの違い(\r\n, \n, \r
行末に \r が残る
最後の空行の扱い

などで、地味にハマりがちです。

ここでは、初心者向けに、

改行コードと行分割の関係
素朴な Split の落とし穴
実務で使いやすい「行分割ユーティリティ」の作り方
空行・トリム・行番号など、よくあるニーズへの対応

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


行分割の前提:改行コードをどう扱うか

改行コードが混在していると、きれいに分割できない

前の「改行コード統一」の話ともつながりますが、
行分割を安定させるには、改行コードをどう扱うかを最初に決める必要があります。

代表的な改行コードは次の3つです。

\r\n(Windows形式)
\n(Unix / Linux / 現行macOS)
\r(古いMacなど)

例えば、次のような文字列を Split('\n') するとどうなるかを見てみます。

string text = "A\r\nB\r\nC";

string[] lines = text.Split('\n');

Console.WriteLine(lines[0]); // "A\r"
Console.WriteLine(lines[1]); // "B\r"
Console.WriteLine(lines[2]); // "C"
C#

行末に \r が残ってしまっています。
このまま処理すると、「行末に謎の文字がいる」状態になり、比較やトリムでハマります。

先に「改行コード統一」をしておくのが鉄板

なので、行分割の前に、

\r\n\r をすべて \n に統一する

という一手間を挟むと、後がとても楽になります。

public static class NewLineNormalizer
{
    public static string ToLf(string? text)
    {
        if (string.IsNullOrEmpty(text))
        {
            return string.Empty;
        }

        string normalized = text.Replace("\r\n", "\n");
        normalized = normalized.Replace("\r", "\n");
        return normalized;
    }
}
C#

これを通してから Split('\n') すれば、
「行末に \r が残る」問題は消えます。


素朴な Split と、そのままだと困るところ

Split('\n') の基本的な挙動

改行コードをLFに統一した前提で、
まずは素朴に Split('\n') を使ってみます。

string text = "A\nB\nC";
string[] lines = text.Split('\n');

Console.WriteLine(lines.Length); // 3
Console.WriteLine(lines[0]);    // "A"
Console.WriteLine(lines[1]);    // "B"
Console.WriteLine(lines[2]);    // "C"
C#

これは期待通りです。

しかし、末尾に改行がある場合はどうなるでしょうか。

string text = "A\nB\nC\n";
string[] lines = text.Split('\n');

Console.WriteLine(lines.Length); // 4
Console.WriteLine(lines[0]);    // "A"
Console.WriteLine(lines[1]);    // "B"
Console.WriteLine(lines[2]);    // "C"
Console.WriteLine(lines[3]);    // ""(空文字)
C#

最後に「空行」が1つ増えます。
これを「空行として扱いたい」のか、「無視したい」のかは、要件次第です。

空行をどう扱うかを決める

業務でよくあるパターンは、次の2つです。

空行も1行として扱いたい(レイアウトや行番号が重要な場合)
空行は無視したい(データとして意味がない場合)

Split には StringSplitOptions を指定できるオーバーロードがあり、
RemoveEmptyEntries を使うと「空文字の行を除外」できます。

string text = "A\n\nB\n";
string[] lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries);

// 結果: ["A", "B"]
C#

ただし、「本当に意味のある空行」まで消えてしまうので、
どちらの挙動が正しいかは、ユーティリティ側で方針を決める必要があります。


実務で使いやすい「行分割ユーティリティ」を作る

基本形:LFに統一してから、空行も含めて分割

まずは、「改行コードをLFに統一し、空行も含めてそのまま返す」基本形です。

using System;
using System.Collections.Generic;

public static class LineSplitter
{
    public static IReadOnlyList<string> SplitLines(string? text)
    {
        if (string.IsNullOrEmpty(text))
        {
            return Array.Empty<string>();
        }

        string normalized = NewLineNormalizer.ToLf(text);

        string[] lines = normalized.Split('\n');

        return lines;
    }
}
C#

動作イメージはこうです。

string text = "A\r\nB\n\rC\r\n";

var lines = LineSplitter.SplitLines(text);

// lines[0] == "A"
// lines[1] == "B"
// lines[2] == ""   (\n\r の間の空行)
// lines[3] == "C"
// lines.Count == 4
C#

「空行も含めて、元の構造をそのまま行ごとに分ける」イメージです。

応用形:空行を除外するバージョン

空行を無視したい場合は、
RemoveEmptyEntries を使うか、自前でフィルタします。

using System.Linq;

public static class LineSplitter
{
    public static IReadOnlyList<string> SplitLinesIgnoreEmpty(string? text)
    {
        if (string.IsNullOrEmpty(text))
        {
            return Array.Empty<string>();
        }

        string normalized = NewLineNormalizer.ToLf(text);

        string[] lines = normalized
            .Split('\n', StringSplitOptions.RemoveEmptyEntries);

        return lines;
    }
}
C#

あるいは、「空白だけの行も無視したい」なら、
Trim() してから判定します。

public static IReadOnlyList<string> SplitLinesIgnoreBlank(string? text)
{
    if (string.IsNullOrEmpty(text))
    {
        return Array.Empty<string>();
    }

    string normalized = NewLineNormalizer.ToLf(text);

    var result = new List<string>();

    foreach (var line in normalized.Split('\n'))
    {
        if (string.IsNullOrWhiteSpace(line))
        {
            continue;
        }

        result.Add(line);
    }

    return result;
}
C#

ここでのポイントは、

「空行」「空白だけの行」をどう扱うかを、ユーティリティ側で明示的に決める

ということです。


StringReader を使った「ストリーム的な行読み」

ReadLine ベースで1行ずつ読む方法

Split は一気に全部の行を配列にしますが、
「巨大なテキストを少しずつ処理したい」場合は、
StringReaderReadLine を使う方法もあります。

using System;
using System.Collections.Generic;
using System.IO;

public static class LineReader
{
    public static IEnumerable<string> ReadLines(string? text)
    {
        if (string.IsNullOrEmpty(text))
        {
            yield break;
        }

        using var reader = new StringReader(text);

        string? line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}
C#

ReadLine は、CRLF / LF / CR をいい感じに認識してくれるので、
事前に改行コードを統一しなくても、行単位で読み取れます。

ただし、「末尾の空行をどう扱うか」などの挙動は ReadLine に依存します。
「内部表現をLFに統一する」という方針と合わせるなら、
ToLf を通してから Split するほうが一貫性は高いです。


行番号やメタ情報を付けたい場合

「行番号付き」の結果を返す

ログ解析やエラー表示などで、
「何行目のデータか」を一緒に持っておきたいことがあります。

その場合は、行分割の結果を「行番号付きの構造体」にしてしまうのも手です。

public readonly record struct LineInfo(int LineNumber, string Text);

public static class LineSplitterWithNumber
{
    public static IReadOnlyList<LineInfo> SplitWithLineNumber(string? text)
    {
        if (string.IsNullOrEmpty(text))
        {
            return Array.Empty<LineInfo>();
        }

        string normalized = NewLineNormalizer.ToLf(text);
        string[] lines = normalized.Split('\n');

        var result = new List<LineInfo>(lines.Length);

        for (int i = 0; i < lines.Length; i++)
        {
            result.Add(new LineInfo(i + 1, lines[i]));
        }

        return result;
    }
}
C#

使い方のイメージはこうです。

var lines = LineSplitterWithNumber.SplitWithLineNumber(text);

foreach (var line in lines)
{
    Console.WriteLine($"{line.LineNumber}: {line.Text}");
}
C#

これで、「何行目に何が書いてあったか」を簡単に扱えるようになります。


まとめ 「行分割ユーティリティ」は“テキストを安全に“行の粒”にするための基礎工事”

行分割は、「ただ Split('\n') すればいい」ように見えて、
改行コードの違い・空行の扱い・行末の \r など、
細かいところでバグの温床になりやすい処理です。

押さえておきたいポイントは、

改行コードが混在している前提で考える
まず \r\n / \r\n に統一してから分割すると安定する
空行を「残すのか」「消すのか」「空白だけも消すのか」をユーティリティ側で決める
巨大なテキストには StringReader.ReadLine という選択肢もある
行番号などのメタ情報が欲しいなら、構造体(レコード)で返す設計もあり

ここまで理解できれば、「なんとなく行ごとに分けている」段階から一歩進んで、
“改行コードの違いにも強く、要件に合わせて行を扱えるC#ユーティリティ”を、
自分の手で設計・実装できるようになっていきます。

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