Galileo Figaro

常に初陣!

プロパティでやることの例

プロパティの役割

下記の記事でも述べたように、 プロパティは、インスタンス内部に持っているデータを、 外部から取得・設定する Getter/Setter の役割を持っています。

gero-blog.hatenablog.com

上記の記事では、インスタンス内部のデータを、 単に取得したり設定したりするような Getter/Setter を 書きました。 ここでは、初学者向けに、もうちょっと実用的な Getter/Setter の例を紹介します。

必ず対応したフィールドがあるわけではない

冒頭のリンク先の記事では、 インスタンス内部の age というフィールドの値を、 Age というプロパティを通して取得・設定していました。 フィールドとプロパティが一対一に対応付いているような 作りになっていましたが、一般的なプロパティは、 必ずしも対応付いているわけではないです。 次の例を見てみましょう。

public string FirstName { get; }

public string FamilyName { get; }

public string FullName
{
    get
    {
        return FirstName + " " + FamilyName;
    }
}

この例では 3 つのプロパティがありますが、 FullName プロパティは、他のプロパティから作り出されています。 一方、このクラスの利用者からみると、あたかも FirstNameFamilyNameFullName という 3 つのデータを持っているように見えます。

また、次の例を見てみましょう。

public int MaxMemberCount
{
    get
    {
        return 999;
    }
}

この例では、フィールドも何も存在せず、 ただ「999」という固定値を返しているだけです。

このように、インスタンスが持っている複数のデータから 一つのプロパティ値を構成したり、 逆に、インスタンス内のデータを使わず固定値を返したりする 場合があります。

Setter での値の検証

Setter では、セットされてきた値が受け入れ可能かどうか、 チェックするということがよく行われます。 例えば次のコードでは、名前が空白の場合は 受け入れないようにしています。

private string _name;
public string Name
{
    get => _name;
    set
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentException("名前がnullまたは空文字のみで構成されています。");
        }
        _name = value;
    }
}

おそらくこのクラスには、この Name を使用する処理があり、 きっと名前が空文字だけだとマズイ処理があるのでしょう。 空白をトリムして使うとしたら、文字列長は 0 になってしまうので、 そうなると何らかのバグを引き起こしちゃうことにもなりかねません。

このように、変な値を受け入れると、クラス内部のロジックが破綻してしまう ことがよくあるので、変な値は受け入れず、Setter で弾くようにしましょう。

C# プロパティ 基礎

プロパティが誕生した背景

オブジェクト指向の中で重要な考え方として「カプセル化」というものがあります。 カプセル化とは、データとそれを操作するメソッドを一つのモジュールとしてまとめることであり、 データはそのメソッドを通して操作するようにしなければいけません。

そのため、オブジェクト指向プログラミングでは、 フィールドとその値を取得・設定するための Getter および Setter を作成し、 フィールドは private にして外部から見えないようにすべきです。 これを単純に実装すると、例えば次のようなコードになるでしょう。

class Person
{
    private int age; // データを保持する変数 (フィールド)

    public void SetAge(int age)
    {
        this.age = age;
    }

    public int GetAge()
    {
        return age;
    }
}

基本にとても忠実です。素晴らしい。 でも、下記のようにちょっとイケてないです。

見た目がイケてない

1つのフィールドに対して、Get/Setという2つのメソッドを書くことになるし、 冗長で打鍵量も多いです。 前述の例だと、「Age」という文字列を 2 回も打たなければならないし、 メソッドなので引数リストも書かなくちゃいけないです。

外部から使うときもイケてない

クラス外からフィールドを変更するには、 これらの Getter/Setter を通さないといけないです。 例えば次のように取得・設定することになります。

person.SetAge(42);
int age = person.GetAge();

クラスの利用者にしてみれば、 インスタンス内のデータを取得したり設定したりしたいだけなので、 ぶっちゃけフィールドに直接アクセスできた方が楽です。

// age への直接アクセスはダメだけど、こう書けた方がスッキリするよね
person.age = 42;
int age = person.age;

プロパティとは

「プロパティ」は、前述のような背景があり生まれた C# の構文です。

アクセス修飾子 型 名前
{
    get
    {
        // Getter の処理
    }
    set
    {
        // Setter の処理
    }
}

例えば、前述の「年齢の取得・設定」の例は次のようになります。

class Person
{
    private int age;

    public int Age
    {
        get
        {
            return age;
        }
        set
        {
            age = value;
        }
    }
}

利用側は次のようになります。 Age という変数に直接アクセスしているように見えますが、 実際には Getter や Setter が呼ばれます。

person.Age = 42;
int age = person.Age;

このように、「クラスの実装者にとってはメソッドだけど、 クラスの利用者にとってはデータに直接アクセスしているように見える」 というのがプロパティです。

プロパティの導入によって、

  • Getter/Setter がひとまとまりになり、冗長な文字列も書かなくてよくなります。
  • 利用側からは直接フィールドの値を取得・設定しているように見え、スッキリします。

C#では、インスタンス内のデータを取得したり設定したりするときに、 通常のメソッドの形で Getter/Setter を用意することはまず無いです。 プロパティを作るようにしましょう。

なお、この例の age のようなフィールドのことを、 バッキングフィールド (backing field) と言います。 クラス外に見せるプロパティに対して、 「そのプロパティの背後にあるフィールド」 という意味合いからそう呼ばれているのだと思います。

自動実装プロパティ

前述のように、単に値の取得だけをする Getter や、 値の設定だけをする Setter を、プロパティとして用意したとしても、 結構な行数を必要としてしまいます。

private int age;
public int Age
{
    get
    {
        return age;
    }
    set
    {
        age = value;
    }
}

C# のインデント方式では、ブロックの始まりのカッコ「{」だけでも改行をするのが一般的ですので、 単に取得・設定するだけでも、12行も使うことになってしまいます。

単に取得・設定するだけのコードであれば、 コンパイラに自動で生成させちゃいましょう、 ってことで上記のコードは下記のように省略することができます。 これは自動実装プロパティと呼ばれます。

public int Age { get; set; }

この 1 行のコードと、前述の 12 行のコードは、同じ意味です。 自動実装プロパティを使うと、コンパイラによって、 自動でバッキングフィールドが作られ、その値を取得・設定するようなコードも自動生成されます。

ドキュメンテーションコメントスキル向上 - 外から見えないもので説明しない

ドキュメンテーションコメントとは

ドキュメンテーションコメントは、 よく型定義やメンバなどの直前に書かれるコメントで、 その型やメンバの仕様を説明するものである。 ここで重要なのが、 「実装」ではなく「仕様」の説明であるということだ。

基本的には、その型やメンバの「利用者」に向けた仕様説明だと思った方がよい。 「実装者」に向けた説明ではないのだ。 もちろん実装者がその実装の過程で、 ドキュメンテーションコメントを参考にすることもあるだろう。 しかしそれは「このメソッドってどういう仕様だっけ」というような、 仕様を確認するために見るぐらいだろう。

外から見えないもので説明しない

前述のように、ドキュメンテーションコメントは 利用者に向けた仕様説明である。 そのため、実装者しか見えないプライベートな要素を使って 説明するのはよろしくない。

例えば、private なメンバの名前や、 ローカル変数の名前が、説明文中に登場するのはよろしくない。

例1

private bool hasValue = false;

/// <summary>
/// hasValue が true の場合に名前を返す。
/// false の場合は空文字列を返す。
/// </summary>
public string Name => hasValue ? name : string.Empty;

hasValue というフラグ名が説明文中に登場する。 どうやら、hasValue というフラグによって振る舞いが変わるようだが、 そのフラグは外部からは見えず、 利用者にとっては謎のフラグだ。

利用者に見せている要素のみで説明すると、 例えば下記のようになる。

/// <summary>
/// コンストラクタで名前が与えられていればその名前を返す。
/// そうでなければ空文字列を返す。
/// </summary>
public string Name => hasValue ? name : string.Empty;

あるいは、そのフラグを外部に見せてしまうという手もある。 その場合、フラグにもドキュメンテーションコメントを記載し、 true や false になる条件を書こう。

/// <summary>
/// コンストラクタで名前が与えられていれば true、
/// そうでなければ false。
/// </summary>
public bool HasValue { get; } = false;

/// <summary>
/// <see cref="HasValue"/> が true の場合に名前を返す。
/// false の場合は空文字列を返す。
/// </summary>
public string Name => hasValue ? name : string.Empty;

例2

private const int userNumMax = 999;

/// <summary>
/// 新しくユーザーを追加する。
/// </summary>
/// <exception cref="InvalidOperationException">
/// すでに userNumMax 人登録されている場合。
/// </exception>
public void AddUser(User newUser)
{
    // ...
}

どうやらこのクラスには、登録できるユーザ数に上限があって、 それを超えると例外を発生させるようだ。 ただ、userNumMax が外部から見えないので、 利用者は何人なら登録できるのか分からない。

上限人数が分かるように、

/// <exception cref="InvalidOperationException">
/// すでに 999 人登録されている場合。
/// </exception>

のように書いてもいいが、それだと「999」という数字を コード中の 2 カ所に書くことになり、DRY の原則に反する。 さっきよりマシではあるが。

なので、上限人数を利用者が見えるようにしよう。

/// <summary>登録できる上限人数</summary>
public int UserNumMax => 999;

/// <summary>
/// 新しくユーザーを追加する。
/// </summary>
/// <exception cref="InvalidOperationException">
/// すでに <see cref="UserNumMax"/> 人登録されている場合。
/// </exception>
public void AddUser(User newUser)
{
    // ...
}

余談だが、上限人数を外部に見せることはなにかと便利である。 例えば、ユーザーが見る画面に「最大○人まで登録できます。」というような 文言を表示するのにも使える。

例3

private readonly System.Timers.Timer timer = new System.Timers.Timer(1000);

/// <summary>
/// バックグラウンドで定期処理を開始する
/// </summary>
/// <exception cref="InvalidOperationException">タイマーが既に実行中の場合</exception>
public void StartPolling()
{
    // ...
}

このメソッドは、定期処理をタイマー (System.Timers.Timer) を使って実現している。 具体的なメンバ名 (timer) こそ出ていないものの、 「タイマー」という語は利用者にとっては初出だし、何のことか分からないかもしれない。

また、タイマーを使っているのは内部の事情であり、利用者にとっては関係のないことである。 定期処理を、タイマーで実現しようが無限ループで実現しようが、 利用者にとってはブラックボックスだ。

内部の要素を使わずに説明するとなると、 例えば、下記のようにするとよいだろう。

/// <exception cref="InvalidOperationException">定期処理が既に開始されている場合</exception>

WPF ブートキャンプ: Chapter 2: Command

力試し

簡単なプログラムを作成してみます。 まずは自力でチャレンジしてみましょう。

起動すると、下記のような画面が現れ、

ボタンを押すと、下記のようなダイアログを 表示するプログラムを作成しましょう。

コマンドの概要

コマンドは、System.Windows.Input.ICommand インターフェイスを 実装したクラスです。 ICommand インターフェイスには、下記のメンバが存在します。役割は下記のとおりです。

  • Execute メソッド

    コマンドの処理です。 ボタンなどが押されたときに実行されます。

  • CanExecute メソッド

    Execute メソッドを実行できるかどうかを表します。 false を返すと、ボタンなどが非活性になり、押せなくなります。

  • CanExecuteChanged イベント

    CanExecute から返す値が変わるかもしれない場合に発生させます。 発生させると、CanExecute が再度呼ばれ、 ボタンなどの活性・非活性状態が再評価されます。

この ICommand インターフェイスを実装させたコマンドは、 ボタンとバインディングさせて使います。 そのためには、ビューモデルでコマンドインスタンスを得る プロパティを用意しておき、それをボタンなどの Command という プロパティにバインディングすることで、処理を実行します。

解答例

冒頭の力試しの解答例と解説を示します。

https://github.com/Geroshabu/WpfBootCamp/tree/main/Chapter2/Code/Sample/Sample

コマンドの用意

ICommand インターフェイスを実装した、 ShowMessageCommand クラスを用意しました。 Execute メソッドではメッセージボックスを出す処理をさせています。 特に活性・非活性制御はしないので、CanExecute メソッドは 常に true を返すようにしています。

internal class MessageCommand : ICommand
{
    public event EventHandler? CanExecuteChanged;

    public bool CanExecute(object? parameter) => true;

    public void Execute(object? parameter)
    {
        MessageBox.Show("ボタンが押されました。");
    }
}

コマンドの紐づけ

ビューモデルではコマンドを返すプロパティを用意します。

internal class MainWindowViewModel
{
    public ICommand MessageCommand { get; } = new MessageCommand();
}

最後に、ボタンの Command プロパティにバインディングします。

<Button Content="押せ"
        Command="{Binding MessageCommand}"
        Width="100" Height="50"/>

Exercise

次のプログラムを作ってみましょう。

次のような、財布を表す Wallet クラスがあるとします。

/// <summary>財布</summary>
internal class Wallet
{
    /// <summary>一万円札の枚数</summary>
    public int CountOf10000Yen { get; } = 1;
    /// <summary>五千円札の枚数</summary>
    public int CountOf5000Yes { get; } = 3;
    /// <summary>千円札の枚数</summary>
    public int CountOf1000Yen { get; } = 3;

    /// <summary>合計金額</summary>
    public int Sum
    {
        get
        {
            return 10000 * CountOf10000Yen
                + 5000 * CountOf5000Yes
                + 1000 * CountOf1000Yen;
        }
    }
}

この財布の中身を表示するプログラムを作ってみましょう。

起動すると、一万円札、五千円札、千円札の枚数が表示されます。 二千円札は...なかったことにしましょう。

「合計」ボタンを押すと、合計が表示されます。

Chapter 1: MVVM

力試し

簡単なプログラムを作成します。 まずは自力でチャレンジしてみましょう。

次のようなクラスがあるとします。

internal class User
{
    public string Name { get; } = "Freddie";
    public int Age { get; } = 42;
}

このクラスのインスタンスを一つ作り、起動すると画面に名前と年齢を表示する、次のようなプログラムを作成してください。

MVVM の概要

MVVM はソフトウェアアーキテクチャの一つで、ソフトを Model、View、ViewModel の 3 要素で表すものです。 Microsoft による MVVM の説明では、利点がいくつか挙げられていますが、簡単に言うと次のようなことを言っているのだと思います。

  • UI とビジネスロジック疎結合になるので、 お互いに、一方の変更がもう一方に影響しにくい。
  • 一般的に UI が絡んでくると単体テストがやりにくいが、 View を分離できるため ViewModel と Model の単体テストがやりやすい。
  • デザイナーは View の開発、プログラマは ViewModel と Model の 開発をすることができ、分業しやすい。 (ただ、私は xaml を書けるデザイナーに出会ったことはないです。)

ソフトを Model、View、ViewModel の 3 つに別れるということですが、 それぞれの役割は大まかに次のような役割だと思います。

  • View

    • UI の見た目担当
    • コントロールの配置やサイズや色を決定する
  • ViewModel

    • Model が持っている値を、View に伝える
    • View で入力された値を、Model に伝える
    • ユーザーが入力中の、まだ確定していない値の保持
    • ユーザーが入力した値の、ビジネスロジックが絡まない検証
    • Model の状態の変化を検知し、View に通知する
  • Model

こう書くと ViewModel のやることが多いように見えますが、 実際には Model の分量が多くなると思います。

解答例

冒頭の力試しの解答例を示します。 ぜひ、自分の考えたコードと比較してください。

https://github.com/Geroshabu/WpfBootCamp/tree/main/Chapter1/Code/Sample

以降、要点を説明していきます。

ビューモデルの作成

今回の例では、User クラスがモデルです。 このモデルが持つ名前と年齢の情報を、ビューに表示するために、 ビューモデルが橋渡し役になっています。

ビューモデルの例を下記に示します。 ビューモデルの名前は、「ビューの名前+ViewModel」という名前にすることが多いです。

internal class MainWindowViewModel
{
    private readonly User model = new();

    public string Name
    {
        get
        {
            return model.Name;
        }
    }

    // ...(略)...
}

ご覧の通り、ビューモデルは User インスタンスを持っています。 これがモデルです。 モデルが持つ値を、プロパティ Name を通じて、ビューに返しています。

ビューとビューモデルの紐づけ

ビューモデルを作っただけでは、画面には表示されません。 ビューとビューモデルを対応付けることが必要です。 まずは、ビューが持つ DataContext というプロパティに、 ビューモデルを設定する必要があります。 DataContext へのビューモデルの セットにはいくつか方法がありますが、 今回は、MainWindow のコードビハインド (.caml.csファイル) で、 自身の DataContext プロパティにビューモデルを設定してみます。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        DataContext = new MainWindowViewModel();
    }
}

さらに .xaml ファイルでは、ビューモデルの Name プロパティを、 Label.Content プロパティにバインディング (紐づけ) しています。 これで、ビューモデルから返された値を、ラベルに表示することができます。

<Label Content="{Binding Name}"/>

.xaml ファイルにこのように書くと、 実行時に Name というプロパティがあるかを探し、 見つかれば Name プロパティの値を、Label.Content の値とします。 どこから探し出すのかというと、DataContext プロパティに設定した ビューモデルから探し出します。 ですので、DataContext プロパティに何も設定していないと、 紐づけがうまくいきません。

LINQ の遅延評価

LINQ 初学者の認識はおそらくこう...

C#LINQ には、コレクションを処理する とても便利なメソッドがそろっています。 次のコードを見てみましょう。

var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result = input
    .Select(x => x * x)
    .Where(x => x % 2 == 1)
    .Take(3);

foreach (var num in result)
{
    Console.WriteLine(num);
}

LINQ を触ったことのある人にとっては、なんてことはないですね。

  1. int 型配列を用意し
  2. 各要素を 2 乗し、
  3. そのうち、奇数だけを抜き出し
  4. 最初の 3 個を取り出し

しているだけです。 したがって、画面には「1」と「9」と「25」が表示されます。

ここで、LINQ の各メソッドを、メソッドチェインを使わずに書いてみます。 また、型を明示的に書いてみます。

var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<int> result1 = input.Select(x => x * x);
IEnumerable<int> result2 = result1.Where(x => x % 2 == 1);
IEnumerable<int> result3 = result2.Take(3);

foreach (var num in result3)
{
    Console.WriteLine(num);
}

当然、表示される結果は変わりません。

ここで、LINQ の各メソッドから返ってくる、 IEnumerable<int> 型の戻り値は一体何なのでしょうか。 LINQ のメソッドによって作られたコレクションが返ってくる、 と答えたアナタ、おそらく下記のようなイメージで 考えているのではないでしょうか。

IEnumerable<int> result1 = input.Select(x => x * x);
// ↑ 返ってきた result1 には { 1, 4, 9, 16, 25, 36, 49, 64, 81, 100 } が入っている。

IEnumerable<int> result2 = result1.Where(x => x % 2 == 1);
// ↑ 返ってきた result2 には { 1, 9, 25, 49, 81 } が入っている。

IEnumerable<int> result3 = result2.Take(3);
// ↑ 返ってきた result3 には { 1, 9, 25 } が入っている。

そして、このイメージは残念ながら違います。

LINQ のメソッドに、コレクションを入力すると、 何らかの処理が施されたコレクションが返ってくる、 という考え方は確かに直感的であるため、 前述のようなイメージを持ってしまいがちです。 C# を学び始めるときは、その方がとっつきやすいですが、 慣れてきたらその考えを改めましょう。

遅延評価とは

遅延評価は、「必要になる時まで評価しない」という考え方です。 LINQ 関係のメソッドは、すべてこの考え方で処理を行います。

また IEnumerable<T> は、直訳すると「列挙可能」であることを 表すインターフェイスですが、まぁよく分かりませんよね。

この IEnumerable<T> は、配列やリストや辞書のような、 複数の値を格納しておくものではありません。 そういう意味で、「コレクションのようでコレクションにあらず」です。 そのため、コレクションと区別して「シーケンス」と言ったりします。

実際には「コレクションをどのように操作するかを表したもの」 が近いと思います。

冒頭サンプルの正しい動き

前章で IEnumerable<T> はコレクションでないと言ったばかりですが、 ここでは、IEnumerable<T> の中身を { 1, 2, 3 } のように 書いて説明するとします。 その方が概念分かりやすいので...

さて、冒頭サンプルを実行すると、上から順に処理が進み、 SelectWhereTake が呼ばれ、 最後に foreach でループが回されます。

foreach の直前までたどり着いた時点の、 各メソッドの戻り値を示してみます。

var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

IEnumerable<int> result1 = input.Select(x => x * x);
// result1 は {  }
IEnumerable<int> result2 = result1.Where(x => x % 2 == 1);
// result2 は {  }
IEnumerable<int> result3 = result2.Take(3);
// result3 は {  }

// ★
foreach (var num in result3)
{
    Console.WriteLine(num);
}

「★」までたどり着いた時点では、 IEnumerable<T> の中身は空っぽです。 なぜなら、「まだ必要とされてない」からです。

その後 foreach 文に入り、result3 から最初の 1 つ目を取り出そうとします。 ここで初めて、シーケンスの 1 つ目の要素が要求されます。

すると、

  1. input の最初の「1」が取り出され
  2. 1 * 1 が評価され 1 となり
  3. 1 % 2 == 1 の条件もクリアし
  4. Take(3) の条件もクリアし

ループ内変数 num1 が入ってきます。 ここまでの各メソッドの戻り値の内容は、 次のようになっています。

var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

IEnumerable<int> result1 = input.Select(x => x * x);
// result1 は { 1 }
IEnumerable<int> result2 = result1.Where(x => x % 2 == 1);
// result2 は { 1 }
IEnumerable<int> result3 = result2.Take(3);
// result3 は { 1 }

foreach (var num in result3)
{
    Console.WriteLine(num); // →「1」と表示
}

次のループでは、result3 から、 2 つ目の要素を取り出そうとします。 すると、

  1. input から「2」が取り出され、
  2. 2 * 2 が評価され 4 になります。

しかしその次の 4 % 2 == 1 を満たさないので、 4 についてはここで評価が打ち切られます。 続いて、

  1. input から次の「3」が取り出され、
  2. 3 * 3 が評価され 9 となり、
  3. 9 % 2 == 1 の条件もクリアし、
  4. Take(3) の条件もクリアし、

ループ内変数 num9 が入ってきます。 ここまでの各メソッドの戻り値の内容は 次のようになっています。

var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

IEnumerable<int> result1 = input.Select(x => x * x);
// result1 は { 1, 4, 9 }
IEnumerable<int> result2 = result1.Where(x => x % 2 == 1);
// result2 は { 1, 9 }  ←「4」はこの時点で評価打ち切り
IEnumerable<int> result3 = result2.Take(3);
// result3 は { 1, 9 }

foreach (var num in result3)
{
    Console.WriteLine(num); // →「9」と表示
}

このように、foreach で次の値を要求するたびに、 最初の input から値が取り出されて、 x * xx % 2 == 1 が順に評価されていきます。

foreach で 3 つ目の値を取り出すときも同様です。 次のようになるでしょう。

var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

IEnumerable<int> result1 = input.Select(x => x * x);
// result1 は { 1, 4, 9, 16, 25 }
IEnumerable<int> result2 = result1.Where(x => x % 2 == 1);
// result2 は { 1, 9, 25 }  ←「16」はこの時点で評価打ち切り
IEnumerable<int> result3 = result2.Take(3);
// result3 は { 1, 9, 25 }

foreach (var num in result3)
{
    Console.WriteLine(num); // →「25」と表示
}

さて、ここまでの繰り返しで、 result3 からは 3 つの値を取り出しました。 これ以上のシーケンスの評価は無意味であり、 この時点で foreach 文を抜けます。 なぜなら、Take(3) と指定されているので、 せいぜい 3 つまでの値しか必要ないからです。

というわけで、input 配列の「6」以降の数字は、 取り出されることすらなく、プログラムを終わります。

遅延評価のメリット

無駄な評価を省ける

前述の例では、Take(3) と指定していたので、 最初の 3 つの値が求まったら、処理を打ち切っていました。

このように、不必要な計算をするのを回避することができます。

メモリ容量の節約

File.ReadLines メソッドは、 ファイルの全行を読み出すメソッドです。 しかし、戻り値は IEnumerable<string> であり、 「必要になった時点で、次の行を読み出す」 という動きになります。

例えば、下記の例では、 foreach で次の行が要求されるたびに、 ファイルから 1 行読み込まれ、 "aaa" と連結され、表示されます。

var result1 = File.ReadLines(@"d:\foo\bar.txt");
var result2 = result1.Select(line => "aaa" + line);
foreach (string line in result2)
{
    Console.WriteLine(line);
}

特に巨大なファイルを読み込む場合は、 すべての行を読み込んでから処理を行うと 多くのメモリを消費しますが、 1 行ずつ読んで処理するのであれば、 メモリ消費は少なくて済みます。

すべてのデータが揃わなくても処理を開始できる

例えばネットワークを介してデータを受け取る場合、 すべてのデータが揃うまでそれなりに時間がかかります。 すべてのデータが揃うのを待って、 データを加工しようとすると、 加工処理の着手が遅れてしまいます。

1個ずつ取り出して加工するようにすれば、 到着したデータから順次、加工処理をすることができます。

「必要になったら」とは?

前述の foreach では、値を取り出すたびに 値が一つ計算されていました。 foreach で「値が必要になった」からです。 そのほかに、評価が必要になるメソッドを考えてみましょう。

Any()、All()

Any は、要素が一つでも条件を満たしたら true、 All は、要素がすべて条件を満たしたら true となります。

これらのメソッドは bool 値を返さないといけないので、 呼んだ時点で値をひとつずつ評価していきます。

Any は条件を満たす要素が見つかったら、 All は条件を満たさない要素が見つかったら、 それ以降の評価は不要なので、そこで評価を打ち切ります。

First()、FirstOrDefault()

条件を満たす最初の要素を返すメソッドです。 条件を満たす最初の要素が見つかったら、 その時点で以降の評価を打ち切ります。

Count()

要素の個数を求めるメソッドです。 要素を個数を数えるには、 要素をすべて計算しなければなりませんので、 Count() を呼んだ時点でシーケンスの すべての要素の評価が行われます。

ToList()、ToArray()、ToDictionary()

これらはシーケンスからコレクションを生成するメソッドです。 コレクションを生成するには、 コレクションに含めるすべての要素を得る必要がありますので、 これらを呼んだ時点で、シーケンスの すべての要素の評価が行われます。

Max()、Min()、Average()、Sum()

要素の最大値、最小値、平均値、最大値を計算するメソッドですが、 それらを求めるためには要素がすべて必要ですので、 これらが呼ばれた時点で、シーケンスの すべての要素の評価が行われます。

前半おわり

以上が、LINQ の遅延評価の基本的なことです。 長くなったのでここでいったん切りましょう。

SelectWhere を呼んだ時点で 各要素が評価されるわけではなく、 「必要になった時点で評価される」 ということを意識しましょう。

プルリク中に次の開発を始めるときのTips

想定しているケース

例えば、下図ので featureY と featureZ がプルリクのレビュー中で、main へのマージ待ちとします。

mainfeatureXfeatureYfeatureZABCDE

ここで、main から新たにブランチを作成して開発を開始したいとします。 しかし、そのためには featureY と featureZ のコードが欲しい、ということがよくあります。 これらが main にマージされるまで、ローカルのみで開発を進めておく方法を説明します。

ローカルで先行開発用のブランチを作成する

ローカルで、main からブランチ (下図のnext) を作成し、そこに欲しいブランチ (featureY と featureZ) をマージします。

mainfeatureXfeatureYfeatureZnextABCDE

そこから開発を始めれば、featureY と featureZ が入った状態で、開発を始めることができます。 当たり前ですが。

mainfeatureXfeatureYfeatureZnextABCDEFGH

先行開発する next ブランチは、あくまでもローカルブランチのみです。 プッシュしてはいけません。

プルリクが終わったら

featureY と featureZ のプルリクが終わって、main へマージされたら、改めて main からブランチを作成し、コミットを移し替えます。

まず、featureY と featureZ がマージされて、次のような形になっているとします。

mainfeatureXfeatureYfeatureZnextABCDEFGH

ここで、main から新たにブランチ featureW を作成し、先行開発しておいたコミット F、G、H を、順にすべてチェリーピックします。 それぞれ、チェリーピックすると、F2、G2、H2というコミットが生成されたとします。

mainfeatureXfeatureYfeatureZnextfeatureWABCDEFGHF2G2H2

コミットをすべて新しいブランチに移し替えたら、古いブランチ (next) は削除してしまいましょう。

mainfeatureXfeatureYfeatureZnextfeatureWABCDEF2G2H2

これで、featureW をプルリクに出す準備が整いました。

このように、ローカルで先行開発しておいて、必要なブランチがマージされたら、すぐに次のプルリクを出すことができます。