C#を用いたアプリケーション開発において、文字列の操作は最も頻繁に行われる処理の一つです。

ユーザー入力の整形、ファイルパスの解析、ログデータの抽出など、特定のルールに従って文字列の一部を切り出す場面は多岐にわたります。

その中心的な役割を担うのがSubstringメソッドですが、近年のC#(.NET 5以降やC# 8.0以降)では、より直感的かつ高効率な手法が登場しています。

本記事では、C#における文字列切り出しの基本であるSubstringの使い方はもちろん、範囲演算子(Range)やインデックス演算子(Index)、そしてパフォーマンスを極限まで追求するためのReadOnlySpan<char>を用いた手法まで、プロフェッショナルな視点で詳しく解説します。

Substringメソッドの基本的な使い方

C#のstringクラスに用意されているSubstringメソッドは、文字列から指定した範囲の文字列を新しく生成して返すメソッドです。

最も歴史が古く、現在でも多くのプロジェクトで使用されている標準的な手法です。

Substringには、主に2つのオーバーロードが存在します。

指定した位置から最後までを切り出す

引数に開始位置(インデックス)のみを指定すると、その位置から文字列の末尾までをすべて切り出します。

C#
using System;

class Program
{
    static void Main()
    {
        string text = "Hello, C# World!";
        
        // インデックス7(C#の'C')から末尾までを取得
        string result = text.Substring(7);
        
        Console.WriteLine($"元の文字列: {text}");
        Console.WriteLine($"切り出し後: {result}");
    }
}
実行結果
元の文字列: Hello, C# World!
切り出し後: C# World!

C#のインデックスは0から始まることに注意してください。

上記の例では、7番目の文字である「C」から後ろがすべて抽出されています。

指定した位置から指定した文字数を切り出す

第2引数に「文字数」を指定することで、必要な長さだけをピンポイントで取得できます。

C#
using System;

class Program
{
    static void Main()
    {
        string text = "2025/12/31 23:59:59";
        
        // 0文字目から4文字分(西暦部分)を取得
        string year = text.Substring(0, 4);
        
        // 5文字目から2文字分(月部分)を取得
        string month = text.Substring(5, 2);
        
        Console.WriteLine($"年: {year}");
        Console.WriteLine($"月: {month}");
    }
}
実行結果
年: 2025
月: 12

ここでよくある間違いは、第2引数を「終了インデックス」だと思い込んでしまうことです。

第2引数はあくまで「長さ(Length)」であることを忘れないようにしましょう。

Substring使用時の注意点と例外回避

Substringは便利ですが、引数の指定を誤ると実行時に例外が発生し、プログラムが強制終了する原因となります。

ArgumentOutOfRangeExceptionの発生条件

以下の条件に当てはまる場合、ArgumentOutOfRangeExceptionがスローされます。

  1. 開始インデックスが負の値である場合
  2. 開始インデックスが文字列の長さを超えている場合
  3. 「開始インデックス + 切り出し文字数」が元の文字列の長さを超える場合

特に3番目のケースは、動的に文字列を処理している際に発生しやすいため注意が必要です。

安全に切り出すための実装例

例外を防ぐためには、事前に文字列の長さをチェックするか、計算結果をクランプ(範囲内に収める)処理を入れるのが定石です。

C#
using System;

class Program
{
    static void Main()
    {
        string input = "Short";
        int start = 0;
        int length = 10; // 文字列より長い指定
        
        // 安全な切り出しのロジック
        string result = SafeSubstring(input, start, length);
        
        Console.WriteLine($"結果: {result}");
    }

    static string SafeSubstring(string text, int start, int length)
    {
        if (string.IsNullOrEmpty(text)) return string.Empty;
        if (start < 0) start = 0;
        if (start > text.Length) return string.Empty;
        
        // 残り文字数を超えないように調整
        int actualLength = Math.Min(length, text.Length - start);
        return text.Substring(start, actualLength);
    }
}
実行結果
結果: Short

このように、「残りの文字数(text.Length – start)」と「要求された長さ」の小さい方を選択することで、境界エラーを確実に回避できます。

C# 8.0以降の最新手法:範囲演算子(..)とインデックス演算子(^)

モダンなC#開発では、Substringの代わりにより簡潔な記述が可能な「範囲演算子(Range)」と「インデックス演算子(Index)」が頻繁に使用されます。

インデックス演算子(^)による末尾からの指定

^演算子を使うと、「末尾から数えて何番目か」という指定が直感的に行えます。

  • ^1:最後の文字
  • ^2:最後から2番目の文字
  • ^text.Length:先頭の文字

範囲演算子(..)によるスライシング

x..yという記法で、x番目からy番目の「手前」までを切り出すことができます。

C#
using System;

class Program
{
    static void Main()
    {
        string filename = "data_backup_2025.csv";
        
        // 先頭から4文字目まで(0123番目)
        string prefix = filename[..4]; // 0..4 と同じ
        
        // 最後から3文字分(拡張子)
        string extension = filename[^3..];
        
        // 5文字目から最後から5文字目の手前まで
        string middle = filename[5..^5];
        
        Console.WriteLine($"接頭辞: {prefix}");
        Console.WriteLine($"拡張子: {extension}");
        Console.WriteLine($"中間部: {middle}");
    }
}
実行結果
接頭辞: data
拡張子: csv
中間部: backup_2025

この記法のメリットは、「終了位置を直接指定できる」点にあります。

従来のSubstringでは「終了位置 – 開始位置」を計算して長さを求める必要がありましたが、範囲演算子ならその手間が省け、コードの可読性が飛躍的に向上します。

高度なパフォーマンス:ReadOnlySpan<char>による切り出し

大量の文字列処理を行うサーバーサイドアプリケーションや、リアルタイム性が求められる処理において、Substringの多用はパフォーマンス低下を招く恐れがあります。

Substringのコストとは

Substringメソッドは、切り出した結果を「新しいstringオブジェクト」としてヒープメモリに割り当てます

これは、元の文字列がどれほど大きくても、一部分を切り出すたびに新しいメモリ確保(アロケーション)とコピーが発生することを意味します。

大量のループ内でこれを行うと、ガベージコレクション(GC)の負荷が高まり、システム全体のパフォーマンスが低下します。

Span<T>とReadOnlySpan<T>の利点

.NET Core 2.1以降(および.NET 5/6/7/8+)で導入されたReadOnlySpan<char>を使用すると、「メモリのコピーを発生させずに」文字列の一部を参照できます。

C#
using System;

class Program
{
    static void Main()
    {
        string heavyText = "ID:12345-TYPE:PREMIUM-DATE:20250101";
        
        // stringをReadOnlySpanとして扱う
        ReadOnlySpan<char> span = heavyText.AsSpan();
        
        // 3文字目から5文字分を「スライス」
        // ※この時点では新しい文字列は作成されない
        ReadOnlySpan<char> idSection = span.Slice(3, 5);
        
        Console.WriteLine($"ID部分の長さ: {idSection.Length}");
        
        // 最後に文字列が必要な場合だけToStringする
        // (またはSpanのまま処理を続けるのが最速)
        if (idSection.SequenceEqual("12345"))
        {
            Console.WriteLine("IDが一致しました。");
        }
    }
}
実行結果
ID部分の長さ: 5
IDが一致しました。

Sliceメソッドは、元の文字列の特定のメモリ範囲を指し示す「ビュー(窓)」を作るだけなので、メモリ消費量はほぼゼロです。

解析処理(パーサー)などを実装する場合は、可能な限りReadOnlySpan<char>で処理を完結させることが、モダンなC#プログラミングのベストプラクティスです。

各手法の比較まとめ

用途に応じて最適な手法を選択できるよう、各手法の特徴を以下の表にまとめました。

手法構文特徴主な用途
Substrings.Substring(i, len)標準的、新しい文字列を生成一般的な文字列操作、古い.NET環境
範囲演算子s[i..j]直感的、可読性が高い書きやすさ優先、最新のC#環境
ReadOnlySpans.AsSpan().Slice(i, len)超高速、メモリ割り当てなし大規模データの解析、高負荷な処理

基本的には「範囲演算子」を使用し、パフォーマンスがボトルネックになる場所では「Span」を採用するという方針が推奨されます。

実践的な活用シーン

文字列の切り出しを実際の開発でどのように活用するか、具体的なケースを見ていきましょう。

ファイルパスからの情報抽出

Pathクラスを使うのが正解ですが、文字列操作の練習としてLastIndexOfSubstringを組み合わせる例を挙げます。

C#
using System;

class Program
{
    static void Main()
    {
        string fullPath = @"C:\Users\Admin\Documents\report.pdf";
        
        int lastBackslash = fullPath.LastIndexOf('\\');
        int lastDot = fullPath.LastIndexOf('.');
        
        if (lastBackslash != -1 && lastDot > lastBackslash)
        {
            // フォルダパス
            string folder = fullPath[..lastBackslash];
            // ファイル名(拡張子なし)
            string fileName = fullPath[(lastBackslash + 1)..lastDot];
            // 拡張子
            string ext = fullPath[lastDot..];
            
            Console.WriteLine($"フォルダ: {folder}");
            Console.WriteLine($"ファイル: {fileName}");
            Console.WriteLine($"拡張子: {ext}");
        }
    }
}
実行結果
フォルダ: C:\Users\Admin\Documents
ファイル: report
拡張子: .pdf

固定長データのパース

古いシステムとの連携などで、特定の文字数ごとに意味がある「固定長データ」を扱う際にも役立ちます。

C#
using System;

class Program
{
    static void Main()
    {
        // 社員コード(4) + 氏名(10) + 部署(4)
        string line = "1001山田 太郎  営業";
        
        string code = line[0..4].Trim();
        string name = line[4..14].Trim();
        string dept = line[14..18].Trim();
        
        Console.WriteLine($"コード: [{code}]");
        Console.WriteLine($"氏名:   [{name}]");
        Console.WriteLine($"部署:   [{dept}]");
    }
}
実行結果
コード: [1001]
氏名:   [山田 太郎]
部署:   [営業]

全角文字が含まれる場合、C#のstringはUnicodeベースなので、1文字を「長さ1」としてカウントします。

バイト数で処理が必要な場合は、Encoding.GetByteCount等を用いる別の処理が必要になる点に注意してください。

まとめ

C#で文字列を切り出す方法は、単なるSubstringから、表現力の高い範囲演算子、そして究極の効率を求めるSpanへと進化してきました。

  • Substring:基本。第2引数が「長さ」であることを忘れない。
  • 範囲演算子(..):モダン。開始と終了の位置を直感的に指定できる。
  • ReadOnlySpan<char>:最強。メモリ消費を抑えたいプロフェッショナルな実装に必須。

日常的なコーディングでは範囲演算子を用いた読みやすいコードを心がけ、パフォーマンスが要求される場面ではReadOnlySpanを導入するという使い分けができるようになると、C#エンジニアとしてのレベルが一段階上がります。

それぞれの特徴を理解し、現場の状況に最適な手法を選択してください。