はじめに 「行分割」は“テキストを“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 は一気に全部の行を配列にしますが、
「巨大なテキストを少しずつ処理したい」場合は、StringReader の ReadLine を使う方法もあります。
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#ユーティリティ”を、
自分の手で設計・実装できるようになっていきます。
