Closed17

Effective C# 6.0/7.0 のメモ(第4章)

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目29 コレクションを返すメソッドではなくイテレータを返すメソッドとすること

  • yield returnを使用して必要なときに値を返す(シーケンスの要素を生成する)メソッドをイテレータメソッドと呼ぶ。
イテレータ、シーケンス、コレクションの言葉の違いについてChatGPTに聞く

  • パフォーマンスの観点からイテレータを推奨する理由
    • イテレータを返すメソッドとすることで、返された側は受け取った値を順次処理すればよい。(一度処理した値をコレクションとして保存したりしなければ、その値は忘れられてよい)
    • コレクションを返されると、値の全てを覚えている必要がある。
  • イテレータメソッドから返される値の全体を取得したい場合は、ToListToArrayメソッドを利用することもできる。
実験コード(イテレータメソッドの遅延実行の挙動を理解する)
  • イテレータメソッドの中で引数の値チェックも記述する場合
  • このメソッドから返された値が使用されるときに初めて、引数の値チェックが実行される(遅延実行)
    • エラーが発生するタイミングが遅くなるため、問題の発見が難しくなる
public class Program
{
    public static IEnumerable<int> GetEvenNumbers(int max)
    {
        Console.WriteLine("GetEvenNumbers 呼び出し");
        
        if (max < 0)
        {
            throw new ArgumentException("maxは0以上でなければならない");
        }
        Console.WriteLine("maxの0以上チェック通過");


        for (int i = 0; i <= max; i++)
        {
            Console.WriteLine($"GetEvenNumbers forループ内: {i}");
            if (i % 2 == 0)
            {
                Console.WriteLine($"GetEvenNumbers yield 直前");
                yield return i;
                Console.WriteLine($"GetEvenNumbers yield 直後");
            }
        }
    }
    
    public static void Main()
    {
        Console.WriteLine("Main 呼び出し");
        var evenNumbers = GetEvenNumbers(-1);
        Console.WriteLine("evenNumbers変数設定後");
        foreach (var number in evenNumbers)
        {
            Console.WriteLine($"Mainのforeach内: {number}");
        }
    }
}

出力

- 引数に2を渡した場合
Main 呼び出し
evenNumbers変数設定後
GetEvenNumbers 呼び出し  <= 遅延実行されていることが分かる
maxの0以上チェック通過
GetEvenNumbers forループ内: 0
GetEvenNumbers yield 直前
Mainのforeach内: 0
GetEvenNumbers yield 直後
GetEvenNumbers forループ内: 1
GetEvenNumbers forループ内: 2
GetEvenNumbers yield 直前
Mainのforeach内: 2
GetEvenNumbers yield 直後

- 引数に-1を渡した場合
Main 呼び出し
evenNumbers変数設定後
GetEvenNumbers 呼び出し
Unhandled exception. System.ArgumentException: maxは0以上でなければならない
  • イテレータ部分を別メソッドとして切り分けた実装。
  • 上記とは異なり、実際に値が利用される前に引数チェックが実行される。
// クラス名などは↑と同じなので省略

    public static IEnumerable<int> GetEvenNumbers(int max)
    {
        Console.WriteLine("GetEvenNumbers 呼び出し");
        
        if (max < 0)
        {
            throw new ArgumentException("maxは0以上でなければならない");
        }
        Console.WriteLine("maxの0以上チェック通過");
        
        // イテレータ部分を別のメソッドとして切り分ける
        return GetEvenNumbersImpl(max);
    }

    private static IEnumerable<int> GetEvenNumbersImpl(int max)
    {
        for (int i = 0; i <= max; i++)
        {
            Console.WriteLine($"GetEvenNumbers forループ内: {i}");
            if (i % 2 == 0)
            {
                Console.WriteLine($"GetEvenNumbers yield 直前");
                yield return i;
                Console.WriteLine($"GetEvenNumbers yield 直後");
            }
        }
    }

// Mainメソッドは同じなので省略

出力

- 引数に2を渡した場合
Main 呼び出し
GetEvenNumbers 呼び出し <= 引数のチェック部分は遅延実行されないので、今回は"evenNumbers設定後"より先に出力される。
maxの0以上チェック通過
evenNumbers変数設定後
GetEvenNumbers forループ内: 0
...あとは同じなので省略

- 引数に-1を渡した場合
Main 呼び出し
GetEvenNumbers 呼び出し
Unhandled exception. System.ArgumentException: maxは0以上でなければならない
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目30 ループよりもクエリ構文を使用すること

  • クエリ形式を用いることで、何をしたいかの意図が明確になる(宣言的になる)
  • また、クエリ形式を利用すると、組み合わせ可能なAPIを作成しやすい(フィルタwhere、ソートorderby、射影selectを組み合わせることができる)。これらの処理は1回の走査中に行われる。
実装
public class Program
{
    // 命令的なプログラムの実行。何を実行したいかよりも、どう実行するかに焦点が当てられている。
    public static IEnumerable<Tuple<int, int>> GetPair()
    {
        for (int x = 0; x < 5; x++)
            for (int y = 0; y < 3; y++)
                if (x + y < 5)
                    yield return Tuple.Create(x, y);
    }

    // クエリ形式 - クエリ構文
    public static IEnumerable<Tuple<int, int>> GetPair2()
    {
        return from x in Enumerable.Range(0, 5)
            from y in Enumerable.Range(0, 3)
            where x + y < 5
            select Tuple.Create(x, y);
    }
    
    // クエリ形式 - メソッド呼び出し形式
    public static IEnumerable<Tuple<int, int>> GetPair3()
    {
        return Enumerable.Range(0, 5)
            .SelectMany(x => Enumerable.Range(0, 3),
                (x, y) => Tuple.Create(x, y))
            .Where(pair => pair.Item1 + pair.Item2 < 5);
    }

    public static void Main()
    {
        var pairs = GetPair();
        // var pairs = GetPair2();
        // var pairs = GetPair3();
        foreach (var pair in pairs)
        {
            Console.WriteLine(pair);
        }
    }
}

出力(結果はGetPair, 2, 3全て同じ)

(0, 0)
(0, 1)
(0, 2)
(1, 0)
(1, 1)
(1, 2)
(2, 0)
(2, 1)
(2, 2)
(3, 0)
(3, 1)
(4, 0)
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目31 シーケンス用の組み合わせ可能なAPIを作成する

  • コレクション全体を処理するのは効率が悪い。複数の処理にまたがる場合は、中間結果を保存する別のコレクションを作成することになったり。変換の度にコレクション全体を走査することになる。
  • イテレータを利用することで、シーケンスを1回走査するだけで、複数の処理を行うことができるようになる。
  • yield returnは「継続可能なメソッド」である。yield returnによるイテレータメソッドによって、内部的に入力シーケンスの現在位置が記録されながら、出力シーケンスに含まれることになる次の要素が繰り返し返される。
  • 入力および出力を、IEnumerable<T>またはその派生型とするメソッドとする。
  • 複数のメソッドを組み合わせられることが価値
実装
  • この項目でも、再び遅延実行のタイミングについておさらい
public class Program
{
    public static IEnumerable<int> Unique(IEnumerable<int> nums)
    {
        var uniqueVals = new HashSet<int>();
        Console.WriteLine("\tUniqueを実行中");
        foreach (int num in nums)
        {
            Console.WriteLine($"\t{num}を評価中");
            if (!uniqueVals.Contains(num))
            {
                Console.WriteLine($"\t{num}をHashSetに追加中");
                uniqueVals.Add(num);
                yield return num;
                Console.WriteLine($"\tyield return後に再実行中");
            }
        }
        Console.WriteLine($"\tUniqueを終了");
    }
    
    public static void Main()
    {
        var nums = new int[] { 1, 2, 2, 3 };
        
        Console.WriteLine("Uniqueの呼び出し前");
        var uniqueNums = Unique(nums);
        Console.WriteLine("Uniqueの呼び出し後");
        
        foreach (int num in uniqueNums)
        {
            Console.WriteLine("foreach内部でnumを出力前");
            Console.WriteLine(num);
            Console.WriteLine("foreach内部でnumを出力後");
        }
    }
}

出力

Uniqueの呼び出し前 <= 「Uniqueの呼び出し前」と後の間に出力が無いので遅延実行
Uniqueの呼び出し後
        Uniqueを実行中
        1を評価中
        1をHashSetに追加中
foreach内部でnumを出力前
1
foreach内部でnumを出力後
        yield return後に再実行中
        2を評価中
        2をHashSetに追加中
foreach内部でnumを出力前
2
foreach内部でnumを出力後
        yield return後に再実行中
        2を評価中
        3を評価中
        3をHashSetに追加中
foreach内部でnumを出力前
3
foreach内部でnumを出力後
        yield return後に再実行中 <= 次の要素が無くてもyeild returnの後の行は実行される模様
        Uniqueを終了
イテレータメソッドを組み合わせている実装例
// 他は上と同じなので省略

    public static IEnumerable<int> Square(IEnumerable<int> nums)
    {
        foreach (int num in nums)
        {
            yield return num * num;
        }
    }
    
    public static void Main()
    {
        var nums = new int[] { 1, 2, 2, 3 };
        // 組み合わせて呼び出すことができる
        foreach (int num in Square(Unique(nums)))
            Console.WriteLine(num);
        }
    }

出力

1
4
9
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目32 反復処理をAction、Predicate、Funcと分離する

  • yield returnを使用するイテレータメソッドには、大きく2つに分類される。
    • A. シーケンスの走査(特定の条件に一致するかどうか確認)
    • B. 走査されたものに対する処理の実行
  • Aは関数機能のあるデリゲートで実装される。
    • TResult Func<T, TResult>(T arg)と、戻り値がboolである特殊事例のbool Predicate<T>(T obj)
  • BはActionデリゲートで実装される
    • void Action<T>(T obj)
  • 以下のコードのように、自分でWhereのようなメソッドを作ることもできる
実装
public class Program
{
    public static void Main()
    {
        var list = new List<int> { 1, 2, 3, 3, 4, 4 };
        var newSec = MyWhere(list, num => num % 2 == 0);
        foreach (var item in newSec)
        {
            Console.WriteLine(item);
        }
    }

    public static IEnumerable<T> MyWhere<T>
        (IEnumerable<T> sequence, Predicate<T> filterFunc)
    {
        // sequenceとfilterFuncのnullチェックは省略
        foreach (T item in sequence)
        {
            if (filterFunc(item))
            {
                yield return item;
            }
        }
    }
}

出力

2
4
4
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目33 要求に応じてシーケンスの要素を生成する

  • シーケンスを生成するメソッドの戻り値が、イテレータメソッド(yield return)ではなく、例えばList<int>などの場合、以下のような問題が発生する。
    • 呼び出し元で、要素の作成の個数を制御できない(常にすべての個数を作成して返却してくる)
  • イテレータメソッドとすることで、TakeWhileなどを利用して制御することができる。
  • また、イテレータメソッドに対するToList()は、リストのシーケンスの要素のコピーが作成される。そのため複数参照によるバグが発生しにくい。
実装
public class Program
{
    public static void Main()
    {
        var seq = CreateSequence(5, 1, 5);
        // TakeWhileを用いることで生成する要素を制限できる。
        // var seq = CreateSequence(5, 1, 5)
        //     .TakeWhile(num => num < 15);
        
        foreach (var item in seq)
        {
            Console.WriteLine(item);
        }
    }

    public static IEnumerable<int> CreateSequence(int number, int startAt, int stepBy)
    {
        for (int i = 0; i < number; i++)
        {
            Console.WriteLine($"\t{startAt + i * stepBy}を追加中");
            yield return startAt + i * stepBy;
        }
    }
}

出力

- TakeWhile無しの場合
        1を追加中
1
        6を追加中
6
        11を追加中
11
        16を追加中
16
        21を追加中
21

- .TakeWhile(num => num < 15)有りの場合
        1を追加中
1
        6を追加中
6
        11を追加中
11
        16を追加中 <= yield returnされた時点で条件に落ちて終了する
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目34 関数引数を使用して役割を分離する

  • インターフェースではなく、関数引数(Funt<T, TResult>等)を引数に渡すことで、簡単に & 疎結合に機能を実走しよう。(下の具体例が分かりやすい)
実装
  • 自作したMyListクラスのRemoveAllの引数(Removeする方法)を、インターフェースで受け取ることも、関数引数として受け取ることも両方可能
  • 関数引数として受け取った方が
    • 実装が簡単(Interfaceとclassを作成する必要がない)
    • RemoveAllメソッド自体と、その使用者の意図(どのようにRemoveしたいか)を分離できる(疎結合にできる)
      • 引数にインターフェースを使用する場合には、「そのインターフェースを使う」というところまでは結合している。
  • IPredicate<T>インターフェースにはそれだけで完結した意味を持たない。
    • IComparable<T>やIEquatable<T>は、「比較できる」、「同等か確認できる」という明確な意味がある
    • IPredicate<T>には、今回の場合は「比較ロジックに使える」というところまでしか意味を限定できない。
public interface IPredicate<T>
{
    bool Match(T obj);
}

public class MyList<T>
{
    // インターフェースを引数として使用する冗長な実装
    public void RemoveAll(IPredicate<T> match)
    {
        // 省略(match.Match()を使って要素を削除する)
    }

    // 関数を引数として使用する簡潔な実装
    public void RemoveAll(Predicate<T> match)
    {
        // 省略(match()を使って要素を削除する)
    }
}

public class MyPredicate : IPredicate<int>
{
    public bool Match(int target) => target < 100;
}

public class Program
{
    public static void Main(string[] args)
    {
        var list = new MyList<int>();
        // RemoveAllの引数をインターフェース経由で渡す場合
        list.RemoveAll(new MyPredicate());
        // RemoveAllの引数を関数経由で渡す場合
        list.RemoveAll(target => target < 100);
    }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目35 拡張メソッドをオーバーロードしないこと

  • 拡張メソッドに型の機能を拡張する方法として過度に頼ってしまうと、メソッドが競合するなどの問題が発生する
    • usingを変更することで、同じ名前の別のメソッドを呼び出すなどの対応が必要になる。しかし、usingを変更したらメソッドの挙動が変わることは開発者にとって想定外であり悪い実装。
  • 拡張メソッドは、型の一機能とみなせるような自然な形での場合に限定するべき
    • 具体例は 項目27
    • IComparableの拡張としてbool LessThanなど
  • それを守るために、
    • 拡張メソッドは名前空間を超えてオーバーロードしてはいけない。
悪い実装例
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

namespace ConsoleExtensions
{
    public static class ConsoleReport
    {
        public static string Format(this Person target)
        {
            return $"Console: {target.FirstName} {target.LastName}";
        }
    }
}

namespace XmlExtensions
{
    public static class XmlReport
    {
        public static string Format(this Person target)
        {
            return $"XML: {target.FirstName} {target.LastName}";
        }
    }
}
// using ConsoleExtensions;
using XmlExtensions;

public class Program
{
    public static void Main()
    {
        var people = new List<Person>
        {
            new Person { FirstName = "Taro", LastName = "Yamada" },
            new Person { FirstName = "Hanako", LastName = "Sato" }
        };

        foreach (var person in people)
        {
            // usingを指定しない場合、Formatはmultiple choicesとなりコンパイルエラー)
            Console.WriteLine(person.Format());
        }
    }
}

出力

- using ConsoleExtensions;が有効なとき
Console: Taro Yamada
Console: Hanako Sato

- using XmlExtensions;が有効なとき
XML: Taro Yamada
XML: Hanako Sato
良い実装例
public static class PersonReports
{
    public static string FormatAsText(Person target)
    {
        return $"Console: {target.FirstName} {target.LastName}";
    }

    public static string FormatAsXml(Person target)
    {
        // 省略
    }
}

// 呼び出し箇所
PersonReports.FormatAsText(person)
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目36 クエリ式とメソッド呼び出しの対応を把握する

  • クエリ式はメソッドに変換できる
  • この章はSelectManyJoinなどの使い方に慣れる感じのベーシックな内容
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目37 クエリを即時評価ではなく遅延評価すること

  • クエリでは、定義されたときに「値が決まるのではなく」、「そのクエリを操作した時に実行される一連の手順が決まる」
遅延実行していることがわかる実験
public class Program
{
    public static void Main()
    {
        LazyEvaluation();
        
    }

    private static IEnumerable<TResult> Generate<TResult>(int number, Func<TResult> generator)
    {
        for (var i = 0; i < number; i++)
        {
            yield return generator();
        }
    }

    private static void LazyEvaluation()
    {
        Console.WriteLine($"テスト開始時間: {DateTime.Now:T}");
        // ポイント: このsequence変数の値はこの定義場所で固定されているのではなく、変動する
        var sequence = Generate(2, () => DateTime.Now);
        
        Console.WriteLine("待機中①、エンターキーを押してください");
        Console.ReadLine();

        Console.WriteLine($"走査中①: {DateTime.Now:T}");
        foreach (var value in sequence)
        {
            Console.WriteLine($"{value:T}");
        }
        
        Console.WriteLine("待機中②、エンターキーを押してください");
        Console.ReadLine();
        Console.WriteLine($"走査中②: {DateTime.Now:T}");
        foreach (var value in sequence)
        {
            Console.WriteLine($"{value:T}");
        }
    }
}

出力

テスト開始時間: 14:22:36
待機中①、エンターキーを押してください

走査中①: 14:22:39
14:22:39
14:22:39
待機中②、エンターキーを押してください

走査中②: 14:22:42
14:22:42
14:22:42
  • メソッドには、シーケンス全体を必要とするものとそうではないものが有る
    • 全体を必要とするもの: Where, OrderBy(一つ一つの要素に対して処理を適用するひつようがあるため)
    • 全体を必要としないもの: Take
  • 全体を必要とするメソッドを呼ぶ前に、フィルターをかけることができたら、全体を必要とするメソッドが適用される要素の数が少なくなるのでパフォーマンスがよい
    • 先にTake(10)してからWhereとか、先にWhereで絞ってからOrderByとか。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目38 メソッドよりもラムダ式を使用すること

  • クエリは、デリゲートや、SQLに変換される。この変換が適切に行われるようにするためには、クエリの中にメソッドを含む場合、そのメソッドはシーケンスを入力として、シーケンスを返すようなものである必要がある。
  • 本の中の例に様に、「boolを返すメソッドを作成してそのメソッドをwhereの中で使う」のではなく、「フィルターとして機能するクエリを記述した、シークエンスを返すメソッドを作成し、そのメソッドをクエリの中に組み込む」
  • (要はシーケンスの状態を保ったまま組み合わせていきましょう、って感じかな)
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目39 FuncやAction内では例外をスローしないこと

  • シーケンスの個別の要素に対して、1つ1つに「変更を加える」ような処理が途中でエラーとなって停止した場合、問題が発生する(どの要素が問題なく変更されたのか、どの要素が駄目だったのか。元の状態に戻すことも難しい)
  • 対処法1: 変更を加える前にフィルターを加えて、エラーが発生しないようにする(nullチェック的なイメージ。要素がこの状態のもののみ変更を加える、ということを実現するためにFindAllのようなメソッドをかけた後に変更する)
  • 対処法2: 個別要素を「変更」するのではなく、個別要素が変更された新しい全体を作成し、シーケンスの参照自体を置き換える
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目40 即時実行と遅延実行を区別すること

  • 挙動の違いを理解して、遅延実行なので結果が変わってバグるみたいなことが無いように都度判断しましょう、的な
実験コード
  • 通常のメソッドを渡す。即時実行されている。
    public static void Main()
    {
        UseInt(ReturnInt());
    }

    private static void UseInt(int val)
    {
        Console.WriteLine("UseIntの呼び出し直後");
        Console.WriteLine($"UseInt: {val}");
    }

    private static int ReturnInt()
    {
        Console.WriteLine("ReturnIntの呼び出し直後");
        return 1;
    }

出力

ReturnIntの呼び出し直後
UseIntの呼び出し直後
UseInt: 1
  • Actionをメソッド渡す。Actionとして渡されたメソッドを即時実行されて、Actionの中身は遅延実行されている
    public static void Main()
    {
        UseAction(ReturnAction());
    }

    private static void UseAction(Action action)
    {
        Console.WriteLine("UseActionの呼び出し直後");
        action();
    }

    private static Action ReturnAction()
    {
        Console.WriteLine("ReturnActionの呼び出し直後");
        return () => Console.WriteLine("ReturnActionのAction本体");
    }

出力

ReturnActionの呼び出し直後
UseActionの呼び出し直後
ReturnActionのAction本体
  • Actionをラムダ式で渡す。遅延実行されている
    public static void Main()
    {
        UseAction(() => Console.WriteLine("ラムダ式の中身"));
    }

    private static void UseAction(Action action)
    {
        Console.WriteLine("UseActionの呼び出し直後");
        action();
    }

出力

UseActionの呼び出し直後
ラムダ式の中身
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目41 コストのかかるリソースを維持し続けないこと

  • クロージャによって束縛された(メソッド内の)ローカル変数は、メソッドを抜けてもメモリに残り続ける。
    • クロージャへの参照が消えるまでは。
まず、クロージャ、キャプチャ、束縛変数について理解する
public class Program
{
    private static Func<int> CreateCounter()
    {
        // count変数が、「キャプチャされる変数」=「束縛変数」
        int count = 0;  // この変数は外部からアクセスできない
        
        // 以下の無名関数(ラムダ式)がクロージャ
        return () => count++;
    }

    public static void Main()
    {
        var counter = CreateCounter();
        Console.WriteLine(counter());
        Console.WriteLine(counter());
        Console.WriteLine(counter());
    }
}

出力

0
1
2
  • ChatGPTによる、上記の例の説明

上記のC#の例において、count変数は「キャプチャされる変数」や「束縛変数」(captured variable)としてクロージャ内で利用されています。
キャプチャとは、無名関数(あるいはラムダ式)が外部スコープの変数を利用することを指します。このキャプチャされた変数は、無名関数がその変数を使う限りメモリ上に保持されます。そして、無名関数がその変数にアクセス・操作することができるようになります。
上記の例でのcount変数は、CreateCounterメソッドのローカル変数です。通常、ローカル変数はその変数が定義されたメソッドが終了すると、その寿命も終わります。しかし、この例ではラムダ式によってcount変数がキャプチャされているため、CreateCounterメソッドを抜けてもその変数は継続して存在します。
そして、このキャプチャされたcount変数を「束縛変数」とも言います。ラムダ式や無名関数に「束縛」され、それが存在する限り寿命が継続するのです。
要するに、上記の例でcountはキャプチャされ、クロージャ(無名関数)によって束縛されています。そのため、外部からはアクセスできないものの、クロージャ内からは継続してcountにアクセス・操作することができます。

  • 上記のプログラムがILによって変換された後のC#コード
public class Program
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
        public int count;

        internal int <CreateCounter>b__0()
        {
            return count++;
        }
    }

    [System.Runtime.CompilerServices.NullableContext(1)]
    private static Func<int> CreateCounter()
    {
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        <>c__DisplayClass0_.count = 0;
        return new Func<int>(<>c__DisplayClass0_.<CreateCounter>b__0);
    }

    public static void Main()
    {
        Func<int> func = CreateCounter();
        Console.WriteLine(func());
        Console.WriteLine(func());
        Console.WriteLine(func());
    }
}

StreamReaderでの実験

準備: StreamReaderクラスのDisposeでログを出力するようにした継承クラス
  • StreamReaderクラスは標準ライブラリで変更ができない。usingでDisposeされたかどうかを今後出力したいので、ログを出すだけの子クラスをつくる
public class LoggingStreamReader : StreamReader
{
    public LoggingStreamReader(Stream stream) : base(stream) { }

    protected override void Dispose(bool disposing)
    {
        Console.WriteLine("Disposing LoggingStreamReader.");
        base.Dispose(disposing);
    }
}
用意したcsv
  • 1行目のaは、数値変換ができずにデフォルト値が返却される
1,2,a
4,5,6,7
8,9
StreamReaderを破棄していないダメな実装
public static class Program
{
    // ファイルから1行ずつ読み込む。yield returnなので遅延評価(要求されたら調べにいく)
    private static IEnumerable<string> ReadLines(this TextReader reader)
    {
        var txt = reader.ReadLine();
        while (txt != null)
        {
            yield return txt;
            txt = reader.ReadLine();
        }
    }

    // 文字を数字に変換する(変換できなかったらデフォルト値にする)だけのメソッド
    private static int DefaultParse(this string input, int defaultValue)
    {
        int answer;
        return (int.TryParse(input, out answer)) ? answer : defaultValue;
    }

    private static IEnumerable<IEnumerable<int>> ReadNumbersFromStream(TextReader t)
    {
        var allLines = from line in t.ReadLines()
            select line.Split(",");
        var matrixOfValues = from line in allLines 
            select from item in line 
                select item.DefaultParse(0);
        return matrixOfValues;
    }

    // 以下のように書くと、ファイルを開いたままになるのでだめ。
    public static void Main()
    {
        // tが破棄されない
        var t = new LoggingStreamReader(File.OpenRead("data.txt"));
        var rowsOfNumbers = ReadNumbersFromStream(t);

        foreach (var row in rowsOfNumbers)
        {
            foreach (var item in row)
            {
                Console.Write("{0} ", item);
            }
            Console.WriteLine();
        }
    }
}

出力(Disposing LoggingStreamReader.が呼ばれていない)

1 2 0 
4 5 6 7 
8 9 
StreamReaderを破棄するOKな実装(ただし一般的な解法ではない)
    public static void Main()
    {
        IEnumerable<IEnumerable<int>> rowsOfNumbers;
        using (var t = new LoggingStreamReader(File.OpenRead("data.txt")))
        {
            rowsOfNumbers = ReadNumbersFromStream(t);
            
            foreach (var row in rowsOfNumbers)
            {
                foreach (var item in row)
                {
                    Console.Write("{0} ", item);
                }
                Console.WriteLine();
            }
        }
    }

出力

1 2 0 
4 5 6 7 
8 9 
Disposing LoggingStreamReader.
  • なお、usingスコープの外でアクセスするとエラーとなる
    • ファイルを閉じた後にファイルを読み取ろうとしている。これはファイルの読み取りが、rowsOfNumbers変数の定義の時点ではなく、走査の時点でなされるから(遅延実行だから)
    • そのため上のコードブロックのように、usingステートメントの中でその変数を利用するようにしている。すべてをusingステートメントに収めることができるような今回の単純な例ではこれでよい。
    public static void Main()
    {
        IEnumerable<IEnumerable<int>> rowsOfNumbers;
        using (var t = new LoggingStreamReader(File.OpenRead("data.txt")))
        {
            rowsOfNumbers = ReadNumbersFromStream(t);
        }
        
        // usingの外でrowsOfNumbersにアクセスするとエラーとなる
        foreach (var row in rowsOfNumbers)
        {
            foreach (var item in row)
            {
                Console.Write("{0} ", item);
            }
            Console.WriteLine();
        }
    }

出力

Disposing LoggingStreamReader.
Unhandled exception. System.ObjectDisposedException: Cannot read from a closed TextReader.
Object name: 'LoggingStreamReader'.
   at System.IO.StreamReader.<ThrowIfDisposed>g__ThrowObjectDisposedException|73_0()
   at System.IO.StreamReader.ReadLine()
ファイルの読み取りの部分でusingを用いる方法
  • 読み取り処理がどのタイミングで実行されたかに関わらず、すべての要素が読み取られた後においてのみ、StreamReaderオブジェクトが破棄される
public static class Program2
{
    // すべての要素が読み取られた後のみでStreamReaderオブジェクトが破棄されるようにするメソッド
    private static IEnumerable<string> ParseFile(string path)
    {
        using (var r = new LoggingStreamReader(File.OpenRead(path)))
        {
            var line = r.ReadLine();
            while (line != null)
            {
                yield return line;
                line = r.ReadLine();
            }
        }
    }
    
    // 文字を数字に変換する(変換できなかったらデフォルト値にする)だけのメソッド
    private static int DefaultParse(this string input, int defaultValue)
    {
        int answer;
        return (int.TryParse(input, out answer)) ? answer : defaultValue;
    }

    private static IEnumerable<IEnumerable<int>> ReadNumbersFromStream(string path)
    {
        var allLines = from line in ParseFile(path)
            select line.Split(",");
        var matrixOfValues = from line in allLines 
            select from item in line 
                select item.DefaultParse(0);
        return matrixOfValues;
    }

    public static void Main()
    {
        var rowsOfNumbers = ReadNumbersFromStream("data.txt");
        foreach (var row in rowsOfNumbers)
        {
            foreach (var item in row)
            {
                Console.Write("{0} ", item);
            }
            Console.WriteLine();
        }
    }
}

出力(ちゃんと読み終えた後にDisposeが呼ばれている)

1 2 0 
4 5 6 7 
8 9 
Disposing LoggingStreamReader.
  • なお、2回アクセスした場合、1回ごとに破棄される挙動となっている
    public static void Main()
    {
        var rowsOfNumbers = ReadNumbersFromStream("data.txt");
        foreach (var row in rowsOfNumbers)
        {
            foreach (var item in row)
            {
                Console.Write("{0} ", item);
            }
            Console.WriteLine();
        }
        
        Console.WriteLine("繰り返します");
        foreach (var row in rowsOfNumbers)
        {
            foreach (var item in row)
            {
                Console.Write("{0} ", item);
            }
            Console.WriteLine();
        }
    }

出力

1 2 0 
4 5 6 7 
8 9 
Disposing LoggingStreamReader.
繰り返します
1 2 0 
4 5 6 7 
8 9 
Disposing LoggingStreamReader.
ラムダ式を渡すことで、ファイルストリームの中で処理を完結させる例
public static class Program
{
    private static IEnumerable<string> ReadLines(this TextReader reader) { // 略 }

    // 文字を数字に変換する(変換できなかったらデフォルト値にする)だけのメソッド
    private static int DefaultParse(this string input, int defaultValue) { // 略}

    private static TResult ProcessFile<TResult>(string filePath, Func<IEnumerable<IEnumerable<int>>, TResult> action)
    {
        using (TextReader t = new LoggingStreamReader(File.OpenRead(filePath)))
        {
            var allLines = from line in t.ReadLines()
                select line.Split(",");
            var matrixOfValues = from line in allLines
                select from item in line
                    select item.DefaultParse(0);
            
            // FileStreamの中ですべての処理を実行している
            return action(matrixOfValues);
        }
    }

    public static void Main()
    {
        // ラムダ式でファイルに対してしてほしい処理を渡す。
        // これによって、処理がファイルの読み取りの内部に完全に格納されることになる。
        int max = ProcessFile("data.txt", (arrayOfNums) => (from line in arrayOfNums
            select line.Max()).Max());

        Console.WriteLine($"Max value is {max}");
    }
}
  • あるメソッドの中にコストのかかるリソースがある場合、ToList()Average()などのシーケンス全体を必要とするようなメソッドをかけて、即時評価されるようにすることで、早めに解放できる。
    • 同じメソッドの中にコストのかかるリソースを持ち続けずに、即時実行できる形で外部メソッドに切り分けることで、早くリソースを開放できる。
  • C#コンパイラは、単一のスコープ内にあるすべてのクロージャを処理するクラスを1つ作成する。
    • これゆえ、コストのかかるリソースを保つ場合は別のメソッドとしたほうが良いということが導かれる。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目42 IEnumerableとIQueryableを区別すること

  • IQueryableでは、C#コードで書かれたクエリは集計され、LINQ to SQLによってデータベースへアクセスされる。
  • IEnumerableでは、LINQ to Objectによって、クエリはローカルで処理される。
  • IQueryableが使用できる場合は、たいていの場合はIEnumerableより効率がよい。
  • ただし、独自のメソッドをwhere句で用いている場合には、IQueryableのままではエラーが発生することがある。このような場合には.AsEnumerable()を使用してエラーを回避できる。
IQueryable<T>の実装
  • 確かにIEnumerableを継承している
namespace System.Linq
{
  /// <summary>Provides functionality to evaluate queries against a specific data source wherein the type of the data is known.</summary>
  /// <typeparam name="T">The type of the data in the data source.</typeparam>
  public interface IQueryable<out T> : IEnumerable<T>, IEnumerable, IQueryable
  {
  }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目43 クエリに期待する意味をSingle()やFirst()を使用して表現すること

  • クエリの結果が単一のものを返す場合、Single()やFirst()を使用することでその意図が明確になる。
  • Single()は、検索結果の要素が1ではない場合(0や2以上)、エラーを発生させる。これは厳しいように思えるが、予想と異なることをエラーですぐ気付けるので良い。
    • 0でもよい場合はSingleOrDefault()を用いる
実験コード
public class Program
{
    private static List<Person> somePeople = new()
    {
        new() { FirstName = "Kota", LastName = "Yamada" },
        new() { FirstName = "Kota", LastName = "Suzuki" },
        new() { FirstName = "Kota", LastName = "Tanaka" }
    };

    public static void Main()
    {
        var answer = (from p in somePeople
                where p.LastName == "ss"
                    select p).SingleOrDefault();
        Console.WriteLine($"LastName: {answer?.FirstName}");
    }
}

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目44 束縛した変数を変更しないこと

  • クロージャにキャプチャされた束縛変数を変更すると思わぬエラーを引き起こすことがあるので、変更しないようにする。
束縛変数を途中で変更する実験
public class Program
{
    public static void Main()
    {
        var index = 0;
        Func<IEnumerable<int>> sequence =
            () => Utilities.Generate(5, () => index++);
        
        Console.WriteLine("1回目");
        index = 20;
        foreach (int n in sequence())
        {
            Console.WriteLine(n);
        }
        
        Console.WriteLine("2回目");
        index = 100;
        foreach (int n in sequence())
        {
            Console.WriteLine(n);
        }
    }
}

public class Utilities
{
    public static IEnumerable<int> Generate(int count, Func<int> func)
    {
        for (var i = 0; i < count; i++)
        {
            yield return func();
        }
    }
}
  • 出力
1回目
20
21
22
23
24
2回目
100
101
102
103
104
  • ILを通して変換されたC#コードを読み解く
前提
  • 以下いくつかのパターンのSampleClass.SampleMethod()を利用する。その利用元となるクラスはこちら
public class Program
{
    public static void Main()
    {
        var sample = new SampleClass();
        var answers = sample.SampleMethod();
        foreach (var answer in answers)
        {
            Console.WriteLine(answer);
        }
    }
}
単純なラムダ式の変換
  • ラムダ式が、インスタンス変数やメソッド内のローカル変数を使用しない場合。
    • ラムダ式の引数のみを使用している
public class SampleClass
{
    public IEnumerable<int> SampleMethod()
    {
        List<int> someNumbers = new List<int> { 2, 3, 4 };
        var answers = from n in someNumbers
            select n * n;
        return answers;
    }
}

// なお、利用元からの出力は
// 4
// 9
// 16

  • 変換後
public class SampleClass
{
    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();
        // staticデリゲートとして、Enumerable.Selectに渡される
        public static Func<int, int> <>9__0_0;

        internal int <SampleMethod>b__0_0(int n)
        {
            return n * n;
        }
    }

    [System.Runtime.CompilerServices.NullableContext(1)]
    public IEnumerable<int> SampleMethod()
    {
        List<int> list = new List<int>();
        list.Add(2);
        list.Add(3);
        list.Add(4);
        return Enumerable.Select(list, <>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Func<int, int>(<>c.<>9.<SampleMethod>b__0_0)));
    }
}
  • なお、Enumerable.Selectは第一引数に対して第二引数のFuncを実行する
        List<int> someNumbers = new List<int> { 2, 3, 4 };
        var answers = Enumerable.Select(someNumbers, (n) => n * n);
        foreach (var answer in answers)
            Console.WriteLine(answer);

// 出力
// 4
// 9
// 16
インスタンス変数を使う場合
public class SampleClass
{
    private readonly int _mod = 2;
    public IEnumerable<int> SampleMethod()
    {
        List<int> someNumbers = new List<int> { 2, 3, 4 };
        var answers = from n in someNumbers
            where n % _mod == 0
            select n * n;
        return answers;
    }
}

// なお、利用元からの出力は
// 4
// 16

  • 変換後
public class SampleClass
{
    [Serializable]
    [CompilerGenerated]
    // このクラスは先程の単純な例と同じ
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

        public static Func<int, int> <>9__1_1;

        internal int <SampleMethod>b__1_1(int n)
        {
            return n * n;
        }
    }

    private readonly int _mod = 2;

    [System.Runtime.CompilerServices.NullableContext(1)]
    public IEnumerable<int> SampleMethod()
    {
        List<int> list = new List<int>();
        list.Add(2);
        list.Add(3);
        list.Add(4);
        return Enumerable.Select(Enumerable.Where(list, new Func<int, bool>(<SampleMethod>b__1_0)),
            <>c.<>9__1_1 ?? (<>c.<>9__1_1 = new Func<int, int>(<>c.<>9.<SampleMethod>b__1_1)));
    }

    // インスタンスメソッドとしてWhereの条件判定をするメソッドを作成する。
    // このメソッドが、Enumerable.Selectに渡されるlistにかかるWhereに利用される
    [CompilerGenerated]
    private bool <SampleMethod>b__1_0(int n)
    {
        return n % _mod == 0;
    }
}
ローカル変数を使う場合
public class SampleClass
{
    public IEnumerable<int> SampleMethod()
    {
        int localVal = 0;
        List<int> someNumbers = new List<int> { 2, 3, 4 };
        var answers = from n in someNumbers
            select n * n + (++localVal);
        return answers;
    }
}

// なお利用元からの出力は、
// 5
// 11
// 19
public class SampleClass
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
        // ローカル変数はネストされたクラスのフィールドとして変換される
        public int localVal;

        internal int <SampleMethod>b__0(int n)
        {
            return n * n + ++localVal;
        }
    }

    [System.Runtime.CompilerServices.NullableContext(1)]
    public IEnumerable<int> SampleMethod()
    {
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        <>c__DisplayClass0_.localVal = 0;
        List<int> list = new List<int>();
        list.Add(2);
        list.Add(3);
        list.Add(4);
        return Enumerable.Select(list, new Func<int, int>(<>c__DisplayClass0_.<SampleMethod>b__0));
    }
}
このスクラップは2023/12/02にクローズされました