【C#】LINQでサクッと文字列抽出・変形・連結を実現するテクニック大全
C#のLINQを使えば、文字列をコレクションとして扱えます。
Whereで条件抽出、Selectで部分取り出し、Aggregateで連結というように、日常的な処理を1行レベルで記述でき、正規表現や複雑なループを減らせます。
パフォーマンスは標準関数と同等で、可読性が高まり保守も楽になります。
LINQとは何か
LINQ(Language Integrated Query)は、C#に組み込まれた強力なクエリ機能で、データの検索や操作を簡潔に記述できる仕組みです。
もともとはデータベースのクエリ言語として発展しましたが、現在では配列やリスト、XML、さらには文字列など、さまざまなデータソースに対して利用できます。
LINQを使うことで、複雑なループや条件分岐を減らし、読みやすく保守しやすいコードを書くことが可能です。
LINQによる文字列処理のメリット
文字列は単なるテキストの塊のように見えますが、実は文字の並び(シーケンス)として扱えます。
LINQを使うと、この文字のシーケンスに対して直感的にクエリを実行できるため、文字列の抽出や変形、連結などの操作が非常に効率的になります。
具体的なメリットは以下の通りです。
- コードの簡潔化
文字列の特定の条件に合う文字だけを抽出したり、変換したりする処理を、複雑なループや条件分岐なしで書けます。
例えば、数字だけを取り出す処理が1行で書けることもあります。
- 読みやすさの向上
LINQのクエリ構文やメソッドチェーンは、処理の意図が明確に表現されるため、他の開発者がコードを理解しやすくなります。
- 遅延実行によるパフォーマンス最適化
LINQは遅延実行を基本としているため、必要なデータだけを効率的に処理できます。
大きな文字列や大量のデータを扱う際に無駄な処理を減らせます。
- 豊富な組み込みメソッドの活用
WhereやSelect、Aggregateなどの標準的なメソッドを使うことで、文字列のフィルタリングや変換、集約が簡単に行えます。
- 他のデータソースとの親和性
LINQは文字列だけでなく、配列やリスト、XML、データベースなど多様なデータソースに対して同じような操作が可能です。
文字列操作の知識が他の領域でも活かせます。
このように、LINQを使うことで文字列操作のコードがシンプルかつパワフルになり、開発効率が大幅にアップします。
文字列をシーケンスとして扱う考え方
C#の文字列はSystem.String型で表されますが、実はIEnumerable<char>インターフェースを実装しているため、文字のシーケンスとして扱えます。
つまり、文字列は「文字の並び」としてLINQの対象になるのです。
例えば、文字列"Hello"は、'H', 'e', 'l', 'l', 'o'という5つの文字のシーケンスとみなせます。
LINQのWhereやSelectなどのメソッドは、このシーケンスの各要素(文字)に対して処理を行います。
この考え方を理解すると、文字列の操作がより柔軟になります。
たとえば、
- 文字列から特定の条件に合う文字だけを抽出する
- 文字ごとに大文字・小文字変換を行う
- 文字のUnicodeコードポイントを取得する
- 文字列を文字単位で分割・変換して新しい文字列を作る
といった処理が、LINQのメソッドチェーンで簡単に書けます。
また、文字列を単なる文字の集合として扱うことで、文字列の一部を取り出したり、条件に合う文字だけを選んだり、文字列の並びを変えたりする操作が直感的に行えます。
以下のようなイメージです。
- 文字列 → 文字のシーケンス
IEnumerable<char> - LINQの
Whereで条件に合う文字だけ抽出 - LINQの
Selectで文字を変換 - LINQの
Aggregateで文字を連結して新しい文字列を生成
このように、文字列をシーケンスとして扱うことで、LINQの強力な機能を活用した文字列操作が可能になります。
これがLINQを使った文字列処理の基本的な考え方です。
前提知識:基本構文のおさらい
クエリ構文とメソッド構文の違い
LINQには主に2つの書き方があります。
ひとつはSQLに似た「クエリ構文」、もうひとつはメソッドチェーンで記述する「メソッド構文」です。
どちらも同じ処理を実現できますが、用途や好みによって使い分けられます。
クエリ構文
クエリ構文は、from、where、selectなどのキーワードを使い、SQLのような見た目で記述します。
読みやすく直感的なため、複雑な条件を扱う場合に便利です。
例:文字列から数字だけを抽出するクエリ構文
string input = "A1B2C3";
var digits = from ch in input
where char.IsDigit(ch)
select ch;
foreach (var digit in digits)
{
Console.Write(digit + " ");
}
// 出力: 1 2 3メソッド構文
メソッド構文は、WhereやSelectなどの拡張メソッドを連結して書きます。
ラムダ式を使うため柔軟で、メソッドチェーンの形で処理の流れがわかりやすいです。
同じ処理をメソッド構文で書くと以下のようになります。
string input = "A1B2C3";
var digits = input.Where(ch => char.IsDigit(ch));
foreach (var digit in digits)
{
Console.Write(digit + " ");
}
// 出力: 1 2 3選び方のポイント
- 簡単なフィルタリングや変換ならメソッド構文が短くて便利です
- 複雑な結合やグループ化を行う場合はクエリ構文のほうが読みやすいことがあります
- 実際には両者を混在させることも可能です
デリゲートとラムダ式の基礎
LINQのメソッド構文では、条件や変換のロジックを引数として渡す必要があります。
これを実現するのが「デリゲート」と「ラムダ式」です。
デリゲートとは
デリゲートは、メソッドの参照を保持できる型で、関数ポインタのような役割を果たします。
LINQのWhereやSelectは、条件や変換を表すデリゲートを受け取ります。
例えば、Func<char, bool>は「charを受け取ってboolを返す関数」を表します。
ラムダ式とは
ラムダ式は匿名関数の一種で、簡潔に関数を定義できます。
LINQではラムダ式を使って条件や変換を記述するのが一般的です。
例:文字が数字かどうかを判定するラムダ式
ch => char.IsDigit(ch)これは「引数chを受け取り、char.IsDigit(ch)の結果を返す関数」を意味します。
実際の使い方
string input = "A1B2C3";
var digits = input.Where(ch => char.IsDigit(ch));ここでch => char.IsDigit(ch)がWhereメソッドに渡されるデリゲートです。
IEnumerable<char>とIEnumerable<string>
LINQはIEnumerable<T>インターフェースを実装したシーケンスに対して操作を行います。
文字列はIEnumerable<char>として扱えますが、単語の集合などはIEnumerable<string>となります。
IEnumerable<char>
文字列は文字のシーケンスなので、IEnumerable<char>として扱えます。
これにより、文字単位でのフィルタリングや変換が可能です。
例:文字列の中から母音だけを抽出する
string text = "Hello World";
var vowels = text.Where(ch => "aeiouAEIOU".Contains(ch));
foreach (var v in vowels)
{
Console.Write(v + " ");
}
// 出力: e o oIEnumerable<string>
単語の集合や文字列の配列はIEnumerable<string>です。
単語単位での操作が可能で、例えば長さでフィルタしたり、先頭文字だけを取り出したりできます。
例:長さが3の単語を抽出する
string[] words = { "the", "quick", "fox", "jumps" };
var shortWords = words.Where(word => word.Length == 3);
foreach (var word in shortWords)
{
Console.WriteLine(word);
}
// 出力:
// the
// fox- 文字列は
IEnumerable<char>として扱い、文字単位の操作ができます - 単語や文字列の集合は
IEnumerable<string>として扱い、単語単位の操作ができます - LINQのメソッドはどちらのシーケンスにも適用可能で、用途に応じて使い分けます
抽出: 条件に合う文字や単語を取り出す
Charメソッドと組み合わせたフィルタリング
C#のCharクラスには文字の種類を判定する便利なメソッドが多数用意されています。
LINQのWhereメソッドと組み合わせることで、文字列から特定の条件に合う文字だけを簡単に抽出できます。
数字だけを拾う
文字列から数字だけを抽出するには、Char.IsDigitメソッドを使います。
Whereで文字ごとに判定し、数字だけを取り出せます。
using System;
using System.Linq;
class Program
{
static void Main()
{
string input = "ABCDE99F-J74-12-89A";
var digits = input.Where(ch => Char.IsDigit(ch));
Console.WriteLine("数字だけを抽出:");
foreach (var digit in digits)
{
Console.Write(digit + " ");
}
}
}数字だけを抽出:
9 9 7 4 1 2 8 9このコードでは、Char.IsDigitが文字が数字かどうかを判定し、数字だけが抽出されています。
アルファベットのみを抽出
アルファベットだけを取り出したい場合は、Char.IsLetterを使います。
大文字・小文字を問わずアルファベットを抽出できます。
using System;
using System.Linq;
class Program
{
static void Main()
{
string input = "Hello123World!";
var letters = input.Where(ch => Char.IsLetter(ch));
Console.WriteLine("アルファベットのみを抽出:");
foreach (var letter in letters)
{
Console.Write(letter + " ");
}
}
}アルファベットのみを抽出:
H e l l o W o r l d特定文字集合でフィルタ
特定の文字だけを抽出したい場合は、文字集合を用意してContainsメソッドで判定します。
例えば、母音だけを抽出する例です。
using System;
using System.Linq;
class Program
{
static void Main()
{
string input = "This is an example sentence.";
char[] vowels = { 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U' };
var extracted = input.Where(ch => vowels.Contains(ch));
Console.WriteLine("母音だけを抽出:");
foreach (var ch in extracted)
{
Console.Write(ch + " ");
}
}
}母音だけを抽出:
i i a e a e e eこのように、任意の文字集合を用意してフィルタリングできます。
正規表現との併用
LINQのWhereメソッドは任意の条件を指定できるため、正規表現を使った複雑なパターンマッチングも可能です。
System.Text.RegularExpressions.Regexクラスと組み合わせて使います。
Regex.IsMatchをWhereで使う
文字列の配列や単語リストから、正規表現にマッチするものだけを抽出する例です。
using System;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
string[] words = { "apple", "banana", "cherry", "date", "fig", "grape" };
var pattern = @"^[a-d]"; // a〜dで始まる単語
var filtered = words.Where(word => Regex.IsMatch(word, pattern));
Console.WriteLine("a〜dで始まる単語:");
foreach (var word in filtered)
{
Console.WriteLine(word);
}
}
}a〜dで始まる単語:
apple
banana
cherry
date括弧で囲まれた部分を取得
文字列中の括弧で囲まれた部分だけを抽出したい場合は、Regex.Matchesを使い、LINQで結果を列挙します。
using System;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
string input = "This is a test (sample) string with (multiple) parentheses.";
var matches = Regex.Matches(input, @"\(([^)]*)\)")
.Cast<Match>()
.Select(m => m.Groups[1].Value);
Console.WriteLine("括弧内の文字列:");
foreach (var match in matches)
{
Console.WriteLine(match);
}
}
}括弧内の文字列:
sample
multipleこの例では、正規表現で括弧内の文字列をキャプチャし、LINQのSelectで抽出しています。
インデックスベースの抽出
LINQのSkipやTakeメソッドを使うと、文字列や配列の特定の範囲を簡単に切り出せます。
インデックスを意識した抽出も可能です。
SkipとTakeで範囲を切り取る
文字列の先頭や途中から一定数の文字を取り出す例です。
using System;
using System.Linq;
class Program
{
static void Main()
{
string input = "HelloWorld";
var middle = input.Skip(2).Take(5);
Console.WriteLine("3文字目から5文字を抽出:");
foreach (var ch in middle)
{
Console.Write(ch);
}
}
}3文字目から5文字を抽出:
lloWoSkip(2)で先頭2文字を飛ばし、Take(5)で次の5文字を取得しています。
インターバル抽出のアイデア
例えば、文字列の偶数番目の文字だけを抽出したい場合は、Selectでインデックスを取得し、Whereで偶数かどうかを判定します。
using System;
using System.Linq;
class Program
{
static void Main()
{
string input = "ABCDEFGHIJ";
var evenIndexChars = input.Select((ch, index) => new { ch, index })
.Where(x => x.index % 2 == 0)
.Select(x => x.ch);
Console.WriteLine("偶数番目の文字:");
foreach (var ch in evenIndexChars)
{
Console.Write(ch + " ");
}
}
}偶数番目の文字:
A C E G Iこのように、インデックスを活用した抽出もLINQで簡単に実現できます。
変形: 文字列を別形式に加工する
Selectで個々の要素を加工
LINQのSelectメソッドは、シーケンスの各要素に対して変換処理を行い、新しいシーケンスを生成します。
文字列をIEnumerable<char>として扱い、文字単位で大文字・小文字変換やUnicodeコードポイントの取得など、さまざまな加工が可能です。
大文字小文字の変換
文字列の各文字を大文字や小文字に変換するには、Selectでchar.ToUpperやchar.ToLowerを使います。
変換後はstring.Concatで再び文字列に結合します。
using System;
using System.Linq;
class Program
{
static void Main()
{
string input = "Hello World!";
var upperChars = input.Select(ch => char.ToUpper(ch));
var upperString = string.Concat(upperChars);
Console.WriteLine("大文字変換結果:");
Console.WriteLine(upperString);
}
}大文字変換結果:
HELLO WORLD!同様に小文字変換も簡単です。
var lowerString = string.Concat(input.Select(ch => char.ToLower(ch)));Unicodeコードポイント取得
文字列の各文字のUnicodeコードポイント(整数値)を取得することもできます。
Selectで(int)chとキャストすればOKです。
using System;
using System.Linq;
class Program
{
static void Main()
{
string input = "ABC";
var codePoints = input.Select(ch => (int)ch);
Console.WriteLine("Unicodeコードポイント:");
foreach (var code in codePoints)
{
Console.WriteLine(code);
}
}
}Unicodeコードポイント:
65
66
67このように、文字列の各文字を数値に変換して処理できます。
SelectManyでネストを平坦化
SelectManyは、各要素から複数の要素を生成し、それらを一つのシーケンスに平坦化します。
文字列のリストから文字のリストを作るときに便利です。
単語リストを文字リストへ
単語のリストを文字単位に分解し、すべての文字を一つのシーケンスにまとめる例です。
using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> words = new List<string> { "apple", "banana", "cherry" };
var allChars = words.SelectMany(word => word);
Console.WriteLine("単語リストを文字リストに変換:");
foreach (var ch in allChars)
{
Console.Write(ch + " ");
}
}
}単語リストを文字リストに変換:
a p p l e b a n a n a c h e r r ySelectManyを使うことで、ネストしたシーケンスを平坦化し、文字単位での処理がしやすくなります。
Projection with anonymous types
LINQのSelectでは、匿名型を使って複数の情報をまとめて返すことができます。
文字列とその長さのペアを作る例を紹介します。
元の文字列と長さのペアを作る
文字列のリストから、各文字列とその長さを持つ匿名型のシーケンスを生成します。
using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> words = new List<string> { "apple", "banana", "cherry" };
var wordInfo = words.Select(word => new { Text = word, Length = word.Length });
Console.WriteLine("文字列と長さのペア:");
foreach (var info in wordInfo)
{
Console.WriteLine($"単語: {info.Text}, 長さ: {info.Length}");
}
}
}文字列と長さのペア:
単語: apple, 長さ: 5
単語: banana, 長さ: 6
単語: cherry, 長さ: 6匿名型を使うことで、複数の関連情報をまとめて扱いやすくなります。
Zipで2つの文字列を合成
Zipメソッドは2つのシーケンスを要素ごとに結合し、新しいシーケンスを作ります。
文字列同士を組み合わせる際に便利です。
マスク付き合成パターン
例えば、マスク文字列を使って元の文字列の一部を隠す処理を考えます。
マスク文字列の'*'の位置は'*'に置き換え、それ以外は元の文字を使います。
using System;
using System.Linq;
class Program
{
static void Main()
{
string original = "SensitiveData";
string mask = "***aaa****a**";
var masked = new string(original.Zip(mask, (o, m) => m == '*' ? '*' : o).ToArray());
Console.WriteLine("マスク付き合成結果:");
Console.WriteLine(masked);
}
}マスク付き合成結果:
***sit****a**この例では、Zipで2つの文字列の対応する文字を比較し、マスク文字が'*'なら'*'を、そうでなければ元の文字を採用しています。
ToArrayで文字配列に変換し、new stringで文字列に戻しています。
このようにZipを使うと、2つの文字列を要素単位で組み合わせて新しい文字列を作ることができます。
整列・ソート: 文字列を順序付ける
OrderByでアルファベット順
LINQのOrderByメソッドは、シーケンスの要素を指定したキーに基づいて昇順に並べ替えます。
文字列のリストや文字のシーケンスをアルファベット順にソートしたい場合に使います。
以下は文字列の配列をアルファベット順に並べ替える例です。
using System;
using System.Linq;
class Program
{
static void Main()
{
string[] words = { "banana", "apple", "cherry", "date" };
var sortedWords = words.OrderBy(word => word);
Console.WriteLine("アルファベット順にソート:");
foreach (var word in sortedWords)
{
Console.WriteLine(word);
}
}
}アルファベット順にソート:
apple
banana
cherry
dateこの例では、OrderByに文字列自体をキーとして渡しているため、辞書順(アルファベット順)に並び替えられています。
文字列の各文字をアルファベット順に並べ替えることも可能です。
string input = "LINQ";
var sortedChars = input.OrderBy(ch => ch);
string result = new string(sortedChars.ToArray());
Console.WriteLine("文字列の文字をアルファベット順にソート:");
Console.WriteLine(result);文字列の文字をアルファベット順にソート:
ILNQThenByで複合キーソート
OrderByで一次ソートを行った後、ThenByを使うと二次ソート、三次ソートと複数のキーで順序付けができます。
複雑な条件でのソートに便利です。
例えば、単語の長さで昇順に並べ替え、同じ長さの単語はアルファベット順に並べる例です。
using System;
using System.Linq;
class Program
{
static void Main()
{
string[] words = { "apple", "bat", "banana", "cat", "dog", "apricot" };
var sortedWords = words.OrderBy(word => word.Length)
.ThenBy(word => word);
Console.WriteLine("長さで昇順、同じ長さはアルファベット順にソート:");
foreach (var word in sortedWords)
{
Console.WriteLine(word);
}
}
}長さで昇順、同じ長さはアルファベット順にソート:
bat
cat
dog
apple
banana
apricotこのように、OrderByでまず長さ順に並べ替え、ThenByで同じ長さの単語をアルファベット順に整列しています。
Reverseで反転
Reverseメソッドはシーケンスの要素の順序を逆にします。
ソート結果を逆順にしたい場合や、元の順序を反転させたい場合に使います。
例えば、先ほどのアルファベット順にソートした単語リストを逆順に表示する例です。
using System;
using System.Linq;
class Program
{
static void Main()
{
string[] words = { "banana", "apple", "cherry", "date" };
var reversedWords = words.OrderBy(word => word).Reverse();
Console.WriteLine("アルファベット順の逆順:");
foreach (var word in reversedWords)
{
Console.WriteLine(word);
}
}
}アルファベット順の逆順:
date
cherry
banana
appleまた、文字列の文字を逆順に並べ替えることも簡単です。
string input = "LINQ";
var reversedChars = input.Reverse();
string result = new string(reversedChars.ToArray());
Console.WriteLine("文字列の文字を逆順に:");
Console.WriteLine(result);文字列の文字を逆順に:
QNILReverseは元のシーケンスを変更せず、新しい逆順のシーケンスを返すため、元のデータはそのまま保持されます。
集約: 連結と統計
Aggregateで自由に連結
LINQのAggregateメソッドは、シーケンスの要素を1つにまとめるための強力な集約関数です。
文字列の連結や複雑な結合処理を自由にカスタマイズして実装できます。
区切り文字付き連結
文字列のリストをカンマやスペースなどの区切り文字で連結する場合、Aggregateを使うと簡潔に書けます。
以下はカンマ区切りで連結する例です。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<string> fruits = new List<string> { "りんご", "ばなな", "みかん" };
string result = fruits.Aggregate((current, next) => current + ", " + next);
Console.WriteLine("カンマ区切りの連結結果:");
Console.WriteLine(result);
}
}カンマ区切りの連結結果:
りんご, ばなな, みかんAggregateは最初の要素をcurrentに取り、次の要素をnextに渡して結合を繰り返します。
区切り文字を自由に指定できるため、カスタムな連結処理に適しています。
インデントと改行を含む連結
複数行の文字列をインデント付きで連結したい場合もAggregateで実現可能です。
例えば、各行の前にスペースを付けて改行を含む連結を行います。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<string> lines = new List<string> { "行1", "行2", "行3" };
string indented = lines.Aggregate("", (acc, line) => acc + " " + line + Environment.NewLine);
Console.WriteLine("インデントと改行を含む連結:");
Console.WriteLine(indented);
}
}インデントと改行を含む連結:
行1
行2
行3この例では、空文字から開始し、各行の前に2つのスペースを付けて改行を加えながら連結しています。
CountとGroupByで文字頻度
文字列や文字のシーケンスに対して、各文字の出現回数を集計するにはGroupByとCountを組み合わせます。
これにより文字頻度の辞書を簡単に作成できます。
出現回数を辞書化
以下は文字列中の各文字の出現回数を辞書にまとめる例です。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
string text = "abracadabra";
var frequency = text.GroupBy(ch => ch)
.ToDictionary(g => g.Key, g => g.Count());
Console.WriteLine("文字の出現回数:");
foreach (var kvp in frequency)
{
Console.WriteLine($"'{kvp.Key}': {kvp.Value}回");
}
}
}文字の出現回数:
'a': 5回
'b': 2回
'r': 2回
'c': 1回
'd': 1回GroupByで同じ文字ごとにグループ化し、Countで各グループの要素数を数えています。
ToDictionaryでキー(文字)と値(出現回数)の辞書に変換しています。
MaxとMinで辞書順先頭末尾を取得
文字列のシーケンスや文字の集合から、辞書順で最も先頭や末尾に位置する要素を取得するにはMaxとMinを使います。
using System;
using System.Linq;
class Program
{
static void Main()
{
string[] words = { "apple", "banana", "cherry", "date" };
string minWord = words.Min();
string maxWord = words.Max();
Console.WriteLine($"辞書順で最も先頭の単語: {minWord}");
Console.WriteLine($"辞書順で最も末尾の単語: {maxWord}");
}
}辞書順で最も先頭の単語: apple
辞書順で最も末尾の単語: date文字列のMinは辞書順で最も小さい(先頭の)文字列を返し、Maxは最も大きい(末尾の)文字列を返します。
文字単位でも同様に使えます。
string input = "hello";
char minChar = input.Min();
char maxChar = input.Max();
Console.WriteLine($"最小文字: {minChar}");
Console.WriteLine($"最大文字: {maxChar}");最小文字: e
最大文字: oこのように、MaxとMinは文字列や文字のシーケンスの範囲を調べるのに便利です。
分割: チャンクとトークン化
Chunk拡張メソッドで一定長分割
.NET 6以降で利用可能なChunk拡張メソッドは、シーケンスを指定したサイズのチャンク(塊)に分割します。
文字列を文字単位で分割し、一定長の部分列に分けたい場合に便利です。
using System;
using System.Linq;
class Program
{
static void Main()
{
string text = "123456789ABCDEF";
int chunkSize = 4;
var chunks = text.Chunk(chunkSize)
.Select(chars => new string(chars));
Console.WriteLine($"文字列を{chunkSize}文字ごとに分割:");
foreach (var chunk in chunks)
{
Console.WriteLine(chunk);
}
}
}文字列を4文字ごとに分割:
1234
5678
9ABC
DEFChunkは文字列をIEnumerable<char>として扱い、指定したサイズごとに分割した配列のシーケンスを返します。
Selectで文字配列を文字列に変換しています。
GroupBy(index / n)パターン
Chunkが使えない環境や、独自に分割したい場合は、Selectでインデックスを取得し、GroupByでグループ化する方法があります。
インデックスをnで割った商をキーにしてグループ化することで、n個ずつのチャンクに分割できます。
using System;
using System.Linq;
class Program
{
static void Main()
{
string text = "ABCDEFGHIJKL";
int chunkSize = 3;
var chunks = text.Select((ch, index) => new { ch, index })
.GroupBy(x => x.index / chunkSize)
.Select(g => new string(g.Select(x => x.ch).ToArray()));
Console.WriteLine($"GroupByで{chunkSize}文字ごとに分割:");
foreach (var chunk in chunks)
{
Console.WriteLine(chunk);
}
}
}GroupByで3文字ごとに分割:
ABC
DEF
GHI
JKLこの方法はChunkが使えない.NETのバージョンでも利用でき、柔軟に分割処理を実装できます。
SplitとLINQの組み合わせ
文字列の分割にはstring.Splitメソッドがよく使われますが、分割後の配列に対してLINQを組み合わせることで、さらに柔軟な処理が可能です。
空文字除去とTrim
Splitで区切った結果に空文字や不要な空白が含まれることがあります。
Whereで空文字を除去し、Selectで各要素の前後の空白をTrimで削除する例です。
using System;
using System.Linq;
class Program
{
static void Main()
{
string csv = " apple, banana, , orange , , grape ";
var fruits = csv.Split(',')
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s));
Console.WriteLine("空文字除去とTrim後の要素:");
foreach (var fruit in fruits)
{
Console.WriteLine($"'{fruit}'");
}
}
}空文字除去とTrim後の要素:
'apple'
'banana'
'orange'
'grape'このように、Splitで分割した後にLINQで空文字を除去し、Trimで余分な空白を取り除くことで、きれいなトークンのリストを得られます。
結合: Joinで関連付け
単語と翻訳表をJoin
LINQのJoinメソッドは、2つのシーケンスを共通のキーで結合し、新しいシーケンスを作成します。
例えば、英単語のリストとその翻訳表を結びつけて、単語と対応する翻訳をペアで取得するケースに使えます。
using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
static void Main()
{
var words = new List<string> { "apple", "banana", "cherry", "date" };
var translations = new List<(string English, string Japanese)>
{
("apple", "りんご"),
("banana", "ばなな"),
("cherry", "さくらんぼ"),
("fig", "いちじく")
};
var joined = words.Join(
translations,
word => word,
trans => trans.English,
(word, trans) => new { Word = word, Translation = trans.Japanese }
);
Console.WriteLine("単語と翻訳の結合結果:");
foreach (var item in joined)
{
Console.WriteLine($"{item.Word} => {item.Translation}");
}
}
}単語と翻訳の結合結果:
apple => りんご
banana => ばなな
cherry => さくらんぼこの例では、wordsの各単語とtranslationsの英単語をキーにして結合し、対応する日本語訳を取得しています。
Joinは内部結合なので、両方に存在するキーだけが結果に含まれます。
GroupJoinでカテゴリ別連結
GroupJoinは、左側のシーケンスの各要素に対して、右側のシーケンスの関連する複数の要素をグループ化して結合します。
カテゴリごとに複数のアイテムをまとめて関連付けたい場合に便利です。
以下は、商品カテゴリと商品リストをGroupJoinで結合し、カテゴリごとに商品をまとめて表示する例です。
using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
class Product
{
public string Name { get; set; }
public int CategoryId { get; set; }
}
static void Main()
{
var categories = new List<Category>
{
new Category { Id = 1, Name = "果物" },
new Category { Id = 2, Name = "野菜" }
};
var products = new List<Product>
{
new Product { Name = "りんご", CategoryId = 1 },
new Product { Name = "ばなな", CategoryId = 1 },
new Product { Name = "にんじん", CategoryId = 2 },
new Product { Name = "キャベツ", CategoryId = 2 }
};
var grouped = categories.GroupJoin(
products,
cat => cat.Id,
prod => prod.CategoryId,
(cat, prods) => new { Category = cat.Name, Products = prods.Select(p => p.Name) }
);
Console.WriteLine("カテゴリ別の商品一覧:");
foreach (var group in grouped)
{
Console.WriteLine($"{group.Category}:");
foreach (var product in group.Products)
{
Console.WriteLine($" - {product}");
}
}
}
}カテゴリ別の商品一覧:
果物:
- りんご
- ばなな
野菜:
- にんじん
- キャベツこの例では、categoriesの各カテゴリに対して、productsの該当する商品をグループ化して関連付けています。
GroupJoinは左外部結合のような動作をし、カテゴリに商品がない場合でもカテゴリは結果に含まれます。
パフォーマンス考慮
ToListを避けた遅延実行
LINQは基本的に遅延実行(Deferred Execution)を採用しており、クエリの実行は結果が実際に必要になったタイミングまで遅延されます。
これにより無駄な処理を避けられますが、ToListやToArrayを呼び出すと即時実行(Immediate Execution)となり、すべての要素がメモリに展開されます。
例えば、文字列のフィルタリングを行い、その結果をすぐにリスト化するとメモリ消費が増え、パフォーマンスに影響を与えることがあります。
using System;
using System.Linq;
class Program
{
static void Main()
{
string input = "A1B2C3D4E5";
// 遅延実行のまま処理
var digitsQuery = input.Where(ch => char.IsDigit(ch));
Console.WriteLine("遅延実行の結果:");
foreach (var d in digitsQuery)
{
Console.Write(d + " ");
}
Console.WriteLine();
// 即時実行でリスト化
var digitsList = input.Where(ch => char.IsDigit(ch)).ToList();
Console.WriteLine("即時実行でリスト化した結果:");
foreach (var d in digitsList)
{
Console.Write(d + " ");
}
}
}遅延実行の結果:
1 2 3 4 5
即時実行でリスト化した結果:
1 2 3 4 5遅延実行のまま処理を続けることで、必要な分だけ処理し、メモリ使用量を抑えられます。
ToListは便利ですが、不要な場合は避けるのがパフォーマンス向上のポイントです。
StringBuilder vs Aggregate
文字列の連結をLINQのAggregateで行うことが多いですが、Aggregateは内部的に文字列の連結を繰り返すため、文字列の長さが大きくなるとパフォーマンスが低下します。
これは文字列が不変(immutable)であるため、連結のたびに新しい文字列が生成されるからです。
一方、StringBuilderは可変のバッファを使って効率的に文字列を連結できるため、大量の連結処理には適しています。
Aggregateでの連結例
using System;
using System.Linq;
class Program
{
static void Main()
{
var words = new[] { "Hello", "World", "from", "LINQ" };
string result = words.Aggregate((acc, next) => acc + " " + next);
Console.WriteLine("Aggregateで連結:");
Console.WriteLine(result);
}
}Aggregateで連結:
Hello World from LINQStringBuilderでの連結例
using System;
using System.Text;
class Program
{
static void Main()
{
var words = new[] { "Hello", "World", "from", "LINQ" };
var sb = new StringBuilder();
foreach (var word in words)
{
if (sb.Length > 0) sb.Append(' ');
sb.Append(word);
}
Console.WriteLine("StringBuilderで連結:");
Console.WriteLine(sb.ToString());
}
}StringBuilderで連結:
Hello World from LINQ大量の文字列を連結する場合はStringBuilderを使うほうが効率的です。
Aggregateは簡潔ですが、パフォーマンスを重視する場面では注意が必要です。
Span<char>拡張とのハイブリッド
Span<char>はC#のメモリ効率の良い文字列操作を可能にする構造体で、配列や文字列の部分範囲を安全かつ高速に扱えます。
LINQの遅延実行や柔軟性と組み合わせることで、パフォーマンスと可読性のバランスを取ることができます。
例えば、文字列の一部をSpan<char>で切り出しつつ、LINQで条件抽出を行う例です。
using System;
using System.Linq; // Although not strictly needed for the corrected loop, it's fine to keep it if other LINQ operations are planned.
class Program
{
static void Main()
{
string input = "Hello123World456";
ReadOnlySpan<char> span = input.AsSpan();
Console.WriteLine("Span<char>とLINQの組み合わせで数字抽出 (修正版):");
// 数字だけを抽出(Spanを直接反復処理)
foreach (char ch in span)
{
if (char.IsDigit(ch))
{
Console.Write(ch + " ");
}
}
Console.WriteLine(); // 改行
}
}Span<char>とLINQの組み合わせで数字抽出:
1 2 3 4 5 6Span<char>は配列や文字列のコピーを避けて部分的に操作できるため、メモリ割り当てを減らし高速化に寄与します。
LINQの柔軟なクエリ機能と組み合わせることで、効率的かつ表現力豊かな文字列処理が可能です。
ただし、Span<char>はスタック上の構造体であり、非同期処理やクロージャー内での使用に制限があるため、使いどころを見極める必要があります。
エラー処理と安全性
Null文字列の扱い
LINQで文字列を扱う際、対象の文字列がnullである場合は注意が必要です。
nullの文字列に対してLINQの拡張メソッドを呼び出すとNullReferenceExceptionが発生します。
安全に処理を行うためには、事前にnullチェックを行うか、nullを空文字列に置き換える方法が一般的です。
using System;
using System.Linq;
class Program
{
static void Main()
{
string? input = null;
// nullチェックを行う例
if (!string.IsNullOrEmpty(input))
{
var letters = input.Where(ch => char.IsLetter(ch));
Console.WriteLine("文字数: " + letters.Count());
}
else
{
Console.WriteLine("入力文字列がnullまたは空です。");
}
// nullを空文字列に置き換えて処理する例
var safeInput = input ?? string.Empty;
var digits = safeInput.Where(ch => char.IsDigit(ch));
Console.WriteLine("数字の数: " + digits.Count());
}
}入力文字列がnullまたは空です。
数字の数: 0このように、nullの可能性がある文字列は必ずチェックするか、??演算子で空文字列に置き換えてからLINQ処理を行うと安全です。
DefaultIfEmptyで空シーケンス回避
LINQのクエリ結果が空のシーケンスになる場合、後続の処理で例外や意図しない動作が起こることがあります。
DefaultIfEmptyメソッドを使うと、空シーケンスの場合にデフォルト値を返すため、処理の安全性が向上します。
例えば、文字列から数字を抽出し、数字がなければ'0'を返す例です。
using System;
using System.Linq;
class Program
{
static void Main()
{
string input = "abcde";
var digits = input.Where(ch => char.IsDigit(ch))
.DefaultIfEmpty('0');
Console.WriteLine("数字またはデフォルト値:");
foreach (var ch in digits)
{
Console.Write(ch + " ");
}
}
}数字またはデフォルト値:
0この例では、数字が1つも見つからなかったため、DefaultIfEmptyによって'0'が返されています。
これにより、空シーケンスによる問題を回避できます。
TryParse系とSelectで数値変換
文字列の数値変換では、int.Parseなどの直接変換は例外を投げる可能性があるため、int.TryParseを使うのが安全です。
LINQのSelectと組み合わせて、変換に成功した値だけを抽出する方法を紹介します。
using System;
using System.Linq;
class Program
{
static void Main()
{
string[] inputs = { "10", "abc", "20", "30x", "40" };
var numbers = inputs.Select(s =>
{
bool success = int.TryParse(s, out int value);
return new { success, value };
})
.Where(x => x.success)
.Select(x => x.value);
Console.WriteLine("変換成功した数値:");
foreach (var num in numbers)
{
Console.WriteLine(num);
}
}
}変換成功した数値:
10
20
40このコードでは、SelectでTryParseの結果を匿名型で保持し、Whereで成功したものだけをフィルタリングしています。
これにより、無効な文字列による例外を防ぎつつ安全に数値変換ができます。
実用サンプル集
CSVパースと再生成
CSV(カンマ区切り値)形式の文字列をLINQでパースし、必要に応じて加工して再生成する例です。
ここでは、CSVの各行を分割し、特定の列だけを抽出して新しいCSVを作成します。
using System;
using System.Linq;
class Program
{
static void Main()
{
string csvData =
@"名前,年齢,職業
山田太郎,30,エンジニア
鈴木花子,25,デザイナー
田中一郎,40,マネージャー";
// CSVを行ごとに分割し、ヘッダーとデータに分ける
var lines = csvData.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var header = lines.First().Split(',');
var dataLines = lines.Skip(1);
// 年齢が30以上の人の名前と職業だけを抽出
var filtered = dataLines.Select(line => line.Split(','))
.Where(fields => int.TryParse(fields[1], out int age) && age >= 30)
.Select(fields => new { Name = fields[0], Job = fields[2] });
// 新しいCSVを生成
var newCsv = "名前,職業\n" +
string.Join("\n", filtered.Select(item => $"{item.Name},{item.Job}"));
Console.WriteLine("30歳以上の人の名前と職業:");
Console.WriteLine(newCsv);
}
}30歳以上の人の名前と職業:
名前,職業
山田太郎,エンジニア
田中一郎,マネージャーこの例では、Splitで行と列に分割し、Whereで条件を指定、Selectで必要な列を抽出しています。
最後にstring.Joinで新しいCSV形式の文字列を作成しています。
ログファイルのエラーメッセージ抽出
ログファイルのテキストからエラーメッセージだけを抽出する例です。
LINQと正規表現を組み合わせて、エラーレベルのログ行をフィルタリングします。
using System;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
string log =
@"INFO 2024-06-01 10:00:00 処理開始
ERROR 2024-06-01 10:01:00 ファイルが見つかりません
WARN 2024-06-01 10:02:00 メモリ使用率が高い
ERROR 2024-06-01 10:03:00 ネットワーク接続失敗";
var errorLines = log.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Where(line => Regex.IsMatch(line, @"^ERROR"));
Console.WriteLine("エラーメッセージ抽出:");
foreach (var line in errorLines)
{
Console.WriteLine(line);
}
}
}エラーメッセージ抽出:
ERROR 2024-06-01 10:01:00 ファイルが見つかりません
ERROR 2024-06-01 10:03:00 ネットワーク接続失敗Splitでログを行単位に分割し、Regex.IsMatchでERRORで始まる行だけを抽出しています。
これにより、エラーメッセージだけを効率的に取り出せます。
ユーザ入力サニタイズ
ユーザからの入力文字列をLINQでサニタイズ(不要な文字の除去や変換)する例です。
ここでは、英数字と一部の記号だけを許可し、それ以外の文字を除去します。
using System;
using System.Linq;
class Program
{
static void Main()
{
string userInput = "Hello! こんにちは123 @#¥%";
// 許可する文字集合(英数字と一部記号)
char[] allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#".ToCharArray();
var sanitized = new string(userInput.Where(ch => allowedChars.Contains(ch)).ToArray());
Console.WriteLine("サニタイズ後の入力:");
Console.WriteLine(sanitized);
}
}サニタイズ後の入力:
Hello123@#この例では、Whereで許可文字だけを抽出し、new stringで再構築しています。
LINQを使うことで簡潔にサニタイズ処理が実装できます。
よくある落とし穴
反復評価による多重実行
LINQのクエリは遅延実行が基本であり、クエリの結果は列挙されるたびに再評価されます。
これにより、同じクエリを複数回列挙すると、意図せず同じ処理が何度も実行されてしまうことがあります。
特に副作用のある処理や重い計算を含む場合はパフォーマンス低下や予期しない動作の原因になります。
using System;
using System.Linq;
class Program
{
static int counter = 0;
static void Main()
{
var numbers = Enumerable.Range(1, 3).Select(n =>
{
counter++;
Console.WriteLine($"処理中: {n}");
return n * 2;
});
Console.WriteLine("1回目の列挙:");
foreach (var num in numbers)
{
Console.WriteLine(num);
}
Console.WriteLine("2回目の列挙:");
foreach (var num in numbers)
{
Console.WriteLine(num);
}
Console.WriteLine($"処理回数: {counter}");
}
}1回目の列挙:
処理中: 1
2
処理中: 2
4
処理中: 3
6
2回目の列挙:
処理中: 1
2
処理中: 2
4
処理中: 3
6
処理回数: 6この例では、numbersのクエリが2回列挙されるため、Select内の処理が2回ずつ実行されています。
これを避けるには、ToList()やToArray()で結果をキャッシュしておく方法があります。
Mutableオブジェクトとの相互作用
LINQはシーケンスの要素を参照で扱うため、ミュータブル(変更可能)なオブジェクトを操作すると、クエリの結果が予期せぬ形で変化することがあります。
特に、クエリの実行後にオブジェクトの状態を変更すると、結果に影響を与えます。
using System;
using System.Collections.Generic;
using System.Linq;
class Item
{
public int Value { get; set; }
}
class Program
{
static void Main()
{
var items = new List<Item>
{
new Item { Value = 1 },
new Item { Value = 2 },
new Item { Value = 3 }
};
var query = items.Where(item => item.Value > 1);
// クエリ実行前に値を変更
items[0].Value = 5;
Console.WriteLine("クエリ結果:");
foreach (var item in query)
{
Console.WriteLine(item.Value);
}
}
}クエリ結果:
5
2
3この例では、items[0]のValueを変更したため、Whereの条件に合致する要素が変わっています。
ミュータブルなオブジェクトを扱う場合は、クエリの実行タイミングやオブジェクトの状態管理に注意が必要です。
拡張メソッドを自作する
フルワード検索ContainsAll
複数のキーワードすべてを含むかどうかを判定する拡張メソッドContainsAllを作成します。
文字列が指定したすべての単語を含んでいるかをチェックする際に便利です。
using System;
using System.Linq;
public static class StringExtensions
{
// 文字列がすべてのキーワードを含むか判定する拡張メソッド
public static bool ContainsAll(this string source, params string[] keywords)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (keywords == null) throw new ArgumentNullException(nameof(keywords));
return keywords.All(keyword => !string.IsNullOrEmpty(keyword) && source.Contains(keyword));
}
}
class Program
{
static void Main()
{
string text = "LINQはC#の強力なクエリ機能です";
bool result1 = text.ContainsAll("LINQ", "C#");
bool result2 = text.ContainsAll("LINQ", "Java");
Console.WriteLine($"'LINQ'と'C#'を含むか: {result1}");
Console.WriteLine($"'LINQ'と'Java'を含むか: {result2}");
}
}'LINQ'と'C#'を含むか: True
'LINQ'と'Java'を含むか: Falseこの拡張メソッドは、paramsで複数のキーワードを受け取り、すべてのキーワードがsourceに含まれているかをAllで判定しています。
空文字やnullは無視せず例外を投げる設計です。
CamelCaseスプリッタ
CamelCaseやPascalCaseの文字列を単語ごとに分割する拡張メソッドを作成します。
大文字で始まる単語の境界を検出し、単語のリストを返します。
using System;
using System.Collections.Generic;
using System.Text;
public static class StringExtensions
{
public static IEnumerable<string> SplitCamelCase(this string source)
{
if (string.IsNullOrEmpty(source))
yield break;
var sb = new StringBuilder();
foreach (char ch in source)
{
if (char.IsUpper(ch) && sb.Length > 0)
{
yield return sb.ToString();
sb.Clear();
}
sb.Append(ch);
}
yield return sb.ToString();
}
}
class Program
{
static void Main()
{
string camelCase = "ThisIsCamelCaseExample";
var words = camelCase.SplitCamelCase();
Console.WriteLine("CamelCase分割結果:");
foreach (var word in words)
{
Console.WriteLine(word);
}
}
}CamelCase分割結果:
This
Is
Camel
Case
Exampleこのメソッドは文字列を1文字ずつ走査し、大文字が現れたら現在の単語を区切ってyield returnします。
最後の単語も忘れずに返します。
左パディング改良版PadLeftLinq
標準のPadLeftは文字列の左側に指定した文字を追加して指定長にしますが、LINQを使って同様の機能を自作し、柔軟に拡張できる例です。
using System;
using System.Linq;
public static class StringExtensions
{
public static string PadLeftLinq(this string source, int totalWidth, char paddingChar = ' ')
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (totalWidth <= source.Length) return source;
int padCount = totalWidth - source.Length;
var padding = Enumerable.Repeat(paddingChar, padCount);
return string.Concat(padding) + source;
}
}
class Program
{
static void Main()
{
string text = "123";
string padded = text.PadLeftLinq(6, '0');
Console.WriteLine($"元の文字列: '{text}'");
Console.WriteLine($"パディング後: '{padded}'");
}
}元の文字列: '123'
パディング後: '000123'この拡張メソッドは、Enumerable.Repeatで指定した数だけパディング文字を生成し、string.Concatで連結してから元の文字列に付加しています。
標準のPadLeftと同様の動作をLINQで実装していますが、必要に応じてさらにカスタマイズ可能です。
C#バージョン別機能差異
LINQ to Objects限定機能
LINQは大きく分けて「LINQ to Objects」「LINQ to SQL」「LINQ to XML」など複数の実装がありますが、ここで紹介する機能は主に「LINQ to Objects」、つまりメモリ上のコレクションに対して動作するLINQの拡張メソッドに限定されます。
LINQ to ObjectsはIEnumerable<T>を対象にしており、配列やリスト、文字列などのシーケンスに対して柔軟なクエリを実行できます。
例えば、WhereやSelect、GroupBy、OrderByなどの標準的なメソッドはLINQ to Objectsで利用可能です。
一方、LINQ to SQLやLINQ to Entitiesでは、SQLに変換可能なメソッドしか使えず、ToListやToArrayなどの即時実行メソッドや、Chunkのような新しい拡張は使えない場合があります。
また、LINQ to Objects限定の機能としては、以下のようなものがあります。
Chunkメソッド
.NET 6で導入された、シーケンスを指定したサイズのチャンクに分割するメソッド。
メモリ上のシーケンスにのみ適用可能です。
Zipの拡張
複数のシーケンスを要素ごとに結合する機能で、LINQ to Objectsで特に活用されます。
Aggregateの多様なオーバーロード
複雑な集約処理を柔軟に記述可能です。
これらはLINQ to Objectsの強みであり、C#のバージョンや.NETのバージョンによって利用可能な機能が異なるため、開発環境に応じて使い分けが必要です。
.NET 6のChunk拡張
.NET 6で新たに追加されたChunk拡張メソッドは、IEnumerable<T>のシーケンスを指定したサイズのチャンク(小分割)に分割する機能です。
これにより、大きなシーケンスを扱う際に一定サイズごとに処理を分割でき、コードがシンプルかつ効率的になります。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 10);
int chunkSize = 3;
var chunks = numbers.Chunk(chunkSize);
Console.WriteLine($"{chunkSize}個ずつのチャンクに分割:");
foreach (var chunk in chunks)
{
Console.WriteLine(string.Join(", ", chunk));
}
}
}3個ずつのチャンクに分割:
1, 2, 3
4, 5, 6
7, 8, 9
10Chunkは内部で効率的にバッファリングし、最後のチャンクは要素数が不足していても問題なく返します。
これまではGroupByとインデックス計算で代用していましたが、Chunkの導入でコードがより直感的になりました。
.NET 8のメモリ効率改善
.NET 8では、LINQのパフォーマンスとメモリ効率がさらに改善されています。
特に文字列や配列の操作において、Span<T>やMemory<T>を活用した内部実装の最適化が進み、不要なメモリ割り当てを削減しています。
具体的な改善点の例は以下の通りです。
WhereやSelectの内部処理の最適化
遅延実行時のクロージャー生成やデリゲート呼び出しのオーバーヘッドが減少し、より高速に処理が行われます。
ChunkやTake、Skipのメモリ割り当て削減
これらのメソッドがSpan<T>を活用し、配列のコピーを最小限に抑えています。
string.Concatの高速化
文字列連結の内部処理が改善され、大量の文字列連結でも効率的に動作します。
これらの改善により、LINQを使った文字列操作やコレクション処理がより高速かつ低メモリで実行可能になりました。
最新の.NETランタイムを利用することで、パフォーマンス面での恩恵を受けられます。
これらのバージョン別の機能差異を理解し、開発環境に合わせて適切なLINQ機能を選択することが、効率的で保守性の高いコードを書くポイントです。
まとめ
この記事では、C#のLINQを活用した文字列操作の基本から応用まで幅広く解説しました。
文字列をシーケンスとして扱い、抽出・変形・連結・分割・結合など多彩な操作を簡潔に実装する方法がわかります。
パフォーマンスやエラー処理の注意点、拡張メソッドの自作例、バージョンごとの機能差異も押さえられるため、実務での効率的な文字列処理に役立ちます。