C# Tips | 文字列処理:指定行取得

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

はじめに 「指定行取得」は“長いテキストから、ピンポイントで1行だけ抜き出す”技

ログファイル、設定ファイル、外部システムからのテキスト——
業務では「長いテキストの中から、n行目だけ欲しい」という場面がよくあります。

例えば、

  • エラーが起きた「123行目」の内容を表示したい
  • ユーザーに「5行目を修正してください」と言うために、その行を抜き出したい
  • 固定フォーマットのファイルで「2行目はヘッダ、3行目からデータ」と決まっている

こういうときに使うのが 指定行取得(GetLine)ユーティリティ です。

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

改行コードと行番号の前提
シンプルな「n行目取得」メソッド
範囲外のときの扱い(例外にするか、nullにするか)
実務で使いやすい Try パターンや、行番号付き構造との連携

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


前提整理:行番号は「1始まり」、改行コードは「統一」して考える

行番号は「1行目=1」として扱う

人間が「3行目」と言うとき、
それは「0,1,2…」ではなく「1,2,3…」の世界の話です。

なので、指定行取得ユーティリティも、

  • 1行目 → lineNumber = 1
  • 2行目 → lineNumber = 2

という 1始まり(1-based) で設計するのが自然です。

内部的には配列のインデックスが 0 から始まるので、
lineNumber - 1 という変換をしてあげればOKです。

改行コードは先に統一しておく

前の「改行コード統一」「行分割」の話ともつながりますが、
指定行取得も、結局は「行分割した結果の中から1つ選ぶ」処理です。

なので、まずは改行コードをLF(\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#

このユーティリティを前提にして、指定行取得を考えていきます。


基本形:指定行を取得する GetLine メソッド

シンプルな実装(範囲外なら null を返す)

まずは、「n行目を文字列として返す。範囲外なら null」という、
扱いやすい基本形を作ってみます。

using System;

public static class LineAccessor
{
    public static string? GetLine(string? text, int lineNumber)
    {
        if (string.IsNullOrEmpty(text))
        {
            return null;
        }

        if (lineNumber <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(lineNumber), "行番号は1以上を指定してください。");
        }

        string normalized = NewLineNormalizer.ToLf(text);

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

        int index = lineNumber - 1;

        if (index < 0 || index >= lines.Length)
        {
            return null;
        }

        return lines[index];
    }
}
C#

ここでやっていることを、順番に言葉で追うとこうなります。

  1. テキストが null または空なら、行は存在しないので null を返す。
  2. 行番号が 0 以下なら、そもそもおかしいので ArgumentOutOfRangeException を投げる。
  3. 改行コードをLFに統一する。
  4. Split('\n') で行ごとに分割する。
  5. 「行番号 − 1」を配列インデックスとして計算する。
  6. 範囲外なら null、範囲内ならその行の文字列を返す。

動作例

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

Console.WriteLine(LineAccessor.GetLine(text, 1)); // "A"
Console.WriteLine(LineAccessor.GetLine(text, 2)); // "B"
Console.WriteLine(LineAccessor.GetLine(text, 3)); // "C"
Console.WriteLine(LineAccessor.GetLine(text, 4)); // null
C#

ここでのポイントは、

  • 行番号は1始まり
  • 存在しない行を指定したときに「例外ではなく null」を返す

という挙動にしていることです。


Tryパターン:成功・失敗を bool で返す

例外を投げたくないときの書き方

業務コードでは、「存在しない行を指定することもあり得る」ケースが多いです。
そのたびに例外を投げると、パフォーマンスや可読性に影響が出ます。

そこでよく使われるのが、Tryパターンです。

public static class LineAccessor
{
    public static bool TryGetLine(string? text, int lineNumber, out string? line)
    {
        line = null;

        if (string.IsNullOrEmpty(text))
        {
            return false;
        }

        if (lineNumber <= 0)
        {
            return false;
        }

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

        int index = lineNumber - 1;

        if (index < 0 || index >= lines.Length)
        {
            return false;
        }

        line = lines[index];
        return true;
    }
}
C#

使い方はこうなります。

if (LineAccessor.TryGetLine(text, 3, out var line))
{
    Console.WriteLine($"3行目: {line}");
}
else
{
    Console.WriteLine("3行目は存在しません。");
}
C#

このスタイルだと、

  • 例外を使わずに「取れた/取れなかった」を判定できる
  • out パラメータで結果を受け取れる

ので、業務ロジックに組み込みやすくなります。


行番号付き構造との連携(ログやエラー表示に便利)

行番号+テキストをまとめて扱う

「指定行取得」とセットでよく出てくるのが、
「何行目かも一緒に扱いたい」というニーズです。

例えば、エラー表示で、

エラー:5行目に不正な値があります
5行目の内容:XXX,YYY,ZZZ

のように出したい場合、
行番号と行テキストをセットで持っておくと便利です。

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

public static class LineAccessorWithInfo
{
    public static LineInfo? GetLineInfo(string? text, int lineNumber)
    {
        string? line = LineAccessor.GetLine(text, lineNumber);

        if (line is null)
        {
            return null;
        }

        return new LineInfo(lineNumber, line);
    }
}
C#

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

var info = LineAccessorWithInfo.GetLineInfo(text, 5);

if (info is { } lineInfo)
{
    Console.WriteLine($"行番号: {lineInfo.LineNumber}");
    Console.WriteLine($"内容   : {lineInfo.Text}");
}
else
{
    Console.WriteLine("指定された行は存在しません。");
}
C#

こうしておくと、「行番号」と「行内容」をセットで扱えるので、
ログ出力やエラー表示が書きやすくなります。


実務での使いどころと注意点

よくある使いどころ

指定行取得は、業務でこんな場面によく登場します。

  • 外部から受け取ったファイルの「ヘッダ行」「データ開始行」を取り出す
  • ログファイルの特定行だけを抜き出して調査するツール
  • ユーザーに「n行目のエラー」を知らせるときに、その行の内容を一緒に表示する

例えば、「1行目はヘッダ、2行目はタイトル、3行目からデータ」という仕様なら、

string header = LineAccessor.GetLine(text, 1) ?? "";
string title  = LineAccessor.GetLine(text, 2) ?? "";
C#

のように書けます。

パフォーマンスを意識するなら「何度も Split しない」

巨大なテキストに対して、
GetLine(text, 1), GetLine(text, 2), GetLine(text, 3)…と何度も呼ぶと、
そのたびに Split が走ってしまい、無駄が増えます。

そういう場合は、

  • 一度だけ行分割してリストにしておく
  • そのリストに対してインデックスアクセスする

という設計にしたほうが効率的です。

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

// あとは lines[0], lines[1], ... を直接使う
C#

指定行取得ユーティリティは「簡単に使える窓口」として用意しつつ、
重い処理では「事前に行配列を作る」などの工夫をするとバランスが良くなります。


まとめ 「指定行取得ユーティリティ」は“長いテキストから、欲しい1行を安全に抜き出すピンセット”

指定行取得は、一見シンプルですが、

  • 行番号は1始まりで扱う
  • 改行コードの違いを吸収してから行分割する
  • 範囲外のときの挙動(例外/null/false)を決める
  • パフォーマンスが気になる場合は、事前に行配列を作る

といったポイントを押さえておくと、
業務コードの中で「安心して使える小さな道具」になります。

ここまで理解できれば、「なんとなく Split してインデックスを触っている」段階から一歩進んで、
“仕様に沿った行番号と改行コードを意識した、指定行取得ユーティリティ”を、
自分のC#コードの中にきちんと組み込めるようになっていきます。

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