CodeGym /コース /C# SELF /位置での結合 Zip を使ってみよう

位置での結合 Zip を使ってみよう

C# SELF
レベル 33 , レッスン 4
使用可能

1. はじめに

2つの手紙の山を想像してみて:1つ目は名前が書かれた封筒、2つ目はその宛先用のハガキ。君のタスクは、同じ位置の封筒とハガキをペアにすること。1番目の封筒は1番目のハガキ、2番目は2番目…って感じで。両方の山を同時に進みながら、それぞれ1つずつ取って「ジップ」するイメージだね。

プログラミングでは、こういう操作を zip って呼ぶよ。ジッパーみたいに2つの側を「カチッ」と位置ごとに合わせる感じ。

LINQでは、これは2つ(時にはもっと)のシーケンスからデータを結合する便利で強力なツール。順番が大事なときに使う:1番目は1番目、2番目は2番目…ってね。
大事なポイント: Zip は両方のコレクションに要素がある間だけ動くよ。どっちかが短いと、結果もその短い方に合わせて切られる。

シンタックスと動作の原則

Zip は2つ(またはそれ以上)のリストを取って、新しいリストにする。要素はインデックスで結合される、つまり0番目と0番目、1番目と1番目…って感じ。どっちかのリストが先に終わったら、それ以上は「ジップ」されない。結果は一番短いリストの長さになるよ。

シグネチャ(基本形):


IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(
    this IEnumerable<TFirst> first,
    IEnumerable<TSecond> second,
    Func<TFirst, TSecond, TResult> resultSelector
)

注目ポイント:Zip は単なるペアじゃなくて、resultSelector で指定したものを返すよ。タプルでも文字列でもオブジェクトでも、君のロジック次第で何でもOK。

  • firstsecond — 結合するコレクション。
  • resultSelector — それぞれのコレクションの要素を受け取って、新しい結果を返す関数。

簡単に言うと:両方のコレクションを同時に進みながら、毎回ペアから好きなものを作るってこと。

2. Zip の使い方例 — シンプルから応用まで

一番シンプルな例:2つの数値配列の合計

例えば、2つのリストがあるとする:

  • 数学の点数
  • 文学の点数
同じ位置の各生徒の合計点を出したい。


// 生徒の点数が入った2つの配列
int[] mathScores = { 5, 4, 3, 5, 2 };
int[] literatureScores = { 4, 5, 3, 4, 3 };

// 位置ごとに結合して合計
var totalScores = mathScores.Zip(literatureScores, (math, literature) => math + literature);

foreach (var score in totalScores)
    Console.WriteLine($"生徒の合計点: {score}");

生徒の合計点: 9
生徒の合計点: 9
生徒の合計点: 6
生徒の合計点: 9
生徒の合計点: 5

見ての通り、超シンプル:1人目は最初の点数の合計、2人目は2つ目…って感じ。

文字列と数値を組み合わせる:商品に値段をつける

例えば、商品リストと値段リストがあるとする。出力は「ProductName — Price」みたいにしたい。


string[] productNames = { "りんご", "なし", "バナナ" };
decimal[] productPrices = { 50.5m, 60.0m, 35.2m };

var info = productNames.Zip(productPrices, (name, price) => $"{name} — {price} ユーロ");

foreach (var s in info)
    Console.WriteLine(s);

りんご — 50.5 ユーロ
なし — 60.0 ユーロ
バナナ — 35.2 ユーロ

オブジェクトコレクションで使う

さっきの例で Student クラスと生徒のコレクションがあったとしよう。今度は、順番が対応した別の配列でレーティングを持ってる場合:


public class Student
{
    public string Name { get; set; }
}

List<Student> students = new List<Student>
{
    new Student { Name = "バシャ" },
    new Student { Name = "ペチャ" },
    new Student { Name = "マーシャ" }
};

int[] ratings = { 7, 9, 8 };

var studentsWithRatings = students.Zip(ratings, (student, rating) =>
    $"{student.Name}: レーティング {rating}");

foreach (var s in studentsWithRatings)
    Console.WriteLine(s);

バシャ: レーティング 7
ペチャ: レーティング 9
マーシャ: レーティング 8

もっと複雑なオブジェクトを結合する

現実だと、例えば2つのシステムから来た注文リストを結合して、それぞれの注文に処理結果や状態を対応させたいことがよくある。


string[] orders = { "A-1", "B-2", "C-3" };
string[] statuses = { "完了", "処理中", "却下" };

var orderStatusList = orders.Zip(statuses, (order, status) => new { Order = order, Status = status });

foreach (var os in orderStatusList)
    Console.WriteLine($"注文 {os.Order}: {os.Status}");

注文 A-1: 完了
注文 B-2: 処理中
注文 C-3: 却下

4. 重要な特徴とよくあるミス

Zip を使うときは、結果のコレクションの長さは一番短い入力コレクションと同じになるって覚えておこう。
例えば、10人の生徒がいても評価が7つしかなければ、結果は7つだけになる。これがデータが「消えた」ってなるよくある理由。

ミスその1: コレクションの長さや順番が合ってない。
リストの順番を間違えたり、要素が対応してないまま結合すると、ペアがズレる。
例えば、生徒リストと評価リストの順番が違うと、バシャがペチャの評価をもらっちゃう。左右違う靴下を履いたみたいな違和感だね。

ミスその2: どちらかのコレクションが空。
どちらかのリストが空だと、結果も空になる。
Zip両方のシーケンスに要素がある間だけ動く。どちらかがデータなしだと「何もない」になるよ。

5. 複数コレクションでのZip

もともとLINQは2つのコレクションだけ結合できた。でも.NET 6からは、なんと3つ同時にジップできるオーバーロードが追加された!

3つのコレクションの例:


string[] names = { "レイ", "ルーク", "レイア" };
string[] planets = { "タトゥイーン", "タトゥイーン", "アルデラーン" };
int[] ages = { 19, 23, 19 };

// .NET 6+ でのオーバーロード:
var characters = names.Zip(planets, ages, (name, planet, age) =>
    $"{name}({planet})、{age}歳");

foreach (var c in characters)
    Console.WriteLine(c);

レイ(タトゥイーン)、19歳
ルーク(タトゥイーン)、23歳
レイア(アルデラーン)、19歳

結果の要素数は一番短いリストに合わせる!もし ages が1つ少なかったら、最後のルーカスは「年齢なし」になるよ。

6. ZipとLINQクエリ構文:あるの?

気づいたかもだけど、ずっと「メソッド」シンタックスを使ってるよね。LINQクエリ構文には Zip 専用のキーワードはないし、from ... in ... でやるのは無理。理由は簡単:「ジップ」は位置ごとのペア結合だけど、LINQクエリは joinselect みたいにキーで「紐付け」るのが基本だから。

まとめ: Zip はドット(つまりメソッド)シンタックスで使おう:


var zipped = collection1.Zip(collection2, (a, b) => ...);

どうしてもクエリ構文でやりたいなら…やめといた方がいいかも。どうしてもやりたいなら、自作の拡張メソッドを書くしかないね。🙂

7. 実アプリでのZipの使い道

実践例:古い値段と新しい値段の比較

例えば、学習アプリで商品リストがあって、たまに新しい値段が届くとする。各商品の値段がどう変わったかを出したい:


string[] productNames = { "りんご", "バナナ", "パイナップル" };
decimal[] oldPrices = { 80.0m, 30.0m, 110.0m };
decimal[] newPrices = { 75.0m, 33.0m, 120.0m };

var priceChanges = productNames
    .Zip(oldPrices, newPrices, (name, oldPrice, newPrice) => 
        $"{name}: 前は {oldPrice}、今は {newPrice}、変化: {newPrice - oldPrice:+#;-#;0}");

foreach (var s in priceChanges)
    Console.WriteLine(s);

りんご: 前は 80、今は 75、変化: -5
バナナ: 前は 30、今は 33、変化: +3
パイナップル: 前は 110、今は 120、変化: +10

実践:異なるソースの結果を合体

「本番」プロジェクトだと、Zip はよく異なるAPIやテーブルから来た結果を合体するのに使う。データの順番が外部で保証されてる場合(例えば天気予報の配列と実測値の配列)とかね。

8. LINQチェーンでのZip:他の演算子と組み合わせる

Zip は単体じゃなくて、LINQの長いチェーンの中でよく使う。例えば、まずリストをフィルタしてからジップして、最後に統計を出すとか。


var passedMath = mathScores.Where(x => x >= 3);
var passedLit = literatureScores.Where(x => x >= 3);

var passedPairs = passedMath.Zip(passedLit, (m, l) => m + l);
var avgScore = passedPairs.Average();

Console.WriteLine($"合格者の平均合計点: {avgScore}");

ポイント:Where の後でどっちかのリストが短くなったら、Zip は短い方に合わせて結果を切るよ。

Zipと他の結合メソッドの比較

メソッド 概要 何で結合? 結果の長さ
Zip
インデックスで要素を結合 インデックス(位置) 短いコレクション
Join
キーで結合 共通キー 一致したペア全部
GroupJoin
各キーごとにコレクション キー 最初のコレクション
Concat
コレクションを後ろにくっつける 単純に末尾 合計の長さ

Zipは位置対応のためのもの。 Join は何か共通の値(例えばコードやid)で対応させるためのもの。

9. 実践アドバイスとよくあるミス

自分のアプリで Zip を使うときは、特にこれをチェックしよう:

  • 両方のコレクションの順番が合ってるか:じゃないとバシャとペチャの評価が混ざるよ。
  • コレクションがちゃんとソートされてるか、または同期して走査できるように準備されてるか。
  • 結果は必ず一番短いコレクションの長さになる。
  • 何かキーでデータを対応させたいなら Zip じゃなくて Join を使おう!
  • もう1つよくあるミス:片方に重複があってもう片方にはない場合や、フィルタリングで順番が変わって対応が崩れた場合。
1
アンケート/クイズ
コレクションの結合、レベル 33、レッスン 4
使用不可
コレクションの結合
アドバンスドLINQ:結合とプロジェクション
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION