きなこもち.net

.NET Framework × UiPath,Orchestrator × Azure × AWS × Angularなどの忘備録

Effective C# 6.0/7.0 × 3章:ジェネリックによる処理 × 自分なりの要点まとめ

目的

流し読みにならないように、1項目毎要約をする。
今回は、3章:ジェネリックによる処理の項目に対してのまとめ。 今回も特に心に残った項目をピックアップした。

項目20:IConparableIComparerにより順序関係を実装する

  • IConparableインターフェースは後方互換の為に実装する必要があるが、実装する際、インターフェースを明示的に指定する形で実装することで、意図せず呼び出されることを防ぐことができる。
  • IConparable.CompareToメソッドは、Object型の引数を受け付ける。そのため、引数に値型の変数が指定された場合Boxing、Unboxingの処理が発生するため、IComparable<T>のCompareToメソッドの処理に比べてパフォーマンスの低下が懸念される。
  • IComparerの実装と呼び出し方
public struct Customer : IComparable<Customer>, IComparable
{
    public string Name { get; }
    public double Revenue { get; }
    public Customer(string name, double revenue)
    {
        Name = name;
        Revenue = revenue;
    }
    public int CompareTo([AllowNull] Customer other)
    {
        return this.Name.CompareTo(other.Name);
    }
    int IComparable.CompareTo(object obj)
    {
        if ((obj is Customer) == false)
        {
            throw new ArgumentException();
        }
        return this.CompareTo((Customer)obj);
    }
    public static Comparison<Customer> CompareByRevenue => (left, right) => left.Revenue.CompareTo(right.Revenue);
    private class RevenueComparer : IComparer<Customer>
    { 
        int IComparer<Customer>.Compare(Customer left, Customer right) => left.Revenue.CompareTo(right.Revenue);
    }
}
static void Main(string[] args)
{
    var a = new Customer("BBB",10);
    var b = new Customer("AAA",9);
    var c = new Customer("DDD",1);
    var d = new Customer("CCC",7);
    List<Customer> customers = new List<Customer>() { a, b, c,d};

    //IComparable<T>
    customers.Sort();
    foreach (var item in customers)
    {
        Console.WriteLine(item.Name);
    }

    //IComparer<T>
    customers.Sort(Customer.CompareByRevenue);
    foreach (var item in customers)
    {
        Console.WriteLine(item.Name);
    }
}    

IComparer を 使用 する と、 一般的 な 順序 とは 異なる 順序 を 定義 し たり、 外部 から 提供 さ れ た 型 に対する 順序 を 定義 し たり する こと が でき ます。BillWagner. Effective C# 6.0/7.0 (p.89). 株式会社翔泳社. Kindle 版.

項目22:ジェネリックの共変性と反変性をサポートする

  • 基底クラスを戻り値とするデリゲートに対して、 派生クラスを戻り値とするメソッドを代入できることを covariance といいます。
  • 派生クラスを引数とするデリゲートに対して、 基底クラスを引数とするデリゲートを代入できることを contravariance といいます
  • Memo:誤植?out修飾子がついているので、共変性では?C#リファレンスが間違っているのか・・・?

    Func < out T > は 反 変性 を 持つ もの として 宣言 さ れ て い ます が、 用途 から 見れ ば IContravariantDelegate インター フェイス を 実装 する オブジェクト を 入力 に 取る という こと を 表し て い ます。 しかし IContravariantDelegate インター フェイス の 観点 では 反 変性 で 正しい の です。 BillWagner. Effective C# 6.0/7.0 (p.97). 株式会社翔泳社. Kindle 版.

項目23:型パラメータにおけるメソッドの制約をデリゲートとして定義する

ジェネリック型では、型引数に具体的な型が指定されているかどうかで型情報が変わります。 たとえばListクラスを例にとった場合、型引数Tの型が具体的に定まっていないListを特にオープンジェネリック型(あるいはジェネリック型定義)、対してListやListなど型引数Tの型が具体的な型が定まっているものをクローズジェネリック型(あるいは構築ジェネリック型、構築された型)と呼びます。 - ジェネリッククラス<T>で、TクラスにAddメソッドが必要な処理を定義する場合、ジェネリッククラスを提供する側は、IAddインタフェースを制約としたジェネリッククラスを作成する。一方、利用する側は、IAddインターフェースを実装したクラスを用意する必要がある。そして、提供されたじぇねりくクラスをクローズジェネリック型として利用することが求められる。 - 提供側、利用側で↑の実装をより柔軟にんするため、ジェネリッククラスで必要だったAddメソッドをデリゲートとして定義し、利用側が呼び出すときに指定できるようにする。

//提供側(IAddインターフェースを定義しなくてOK、制約をかけなくてOK)
public static T Add<T>(T left, T right, Func<T, T, T> AddFunc)
{
    return AddFunc(left, right);
}
//同じこと
//public static T Add<T>(T left, T right, Func<T, T, T> AddFunc) =>
//    AddFunc(left, right);

//呼び出し側(IAddの実装をしなくてOK、クローズジェネリッククラスを用意しなくてOK)
Example.Add(1, 1, (x, y) => x + y);

ほとんど の 場合、 ジェネリッククラス で 呼び出す 必要 の ある メソッド は 特定 の デリゲート として 置き換える こと が でき ます。 BillWagner. Effective C# 6.0/7.0 (p.100). 株式会社翔泳社. Kindle 版.

定義してあっても、使わなければいいかと無視していたが、制約やデリゲートを利用することで、使わせないようにすることや、その制約にかかる実装コストを減らすことができる・・・ということなのか・・・

項目25:型引数がインスタンスのフィールドでない場合には、ジェネリックメソッドとして定義すること

  • 項目23の例を使うと、以下のようになる。
//ジェネリッククラスとして定義(型引数がインスタンスのフィールドではない)
public static class Example<T>
{
    public static T Add(T left, T right, Func<T, T, T> AddFunc)
    {
        return AddFunc(left, right);
    }
}

Example<int>.Add(1, 1, (x, y) => x + y);
Example<double>.Add(1, 1, (x, y) => x + y);
//非ジェネリッククラスのジェネリックメソッドとして定義
public static class Example
{
    public static T Add<T>(T left, T right, Func<T, T, T> AddFunc)
    {
        return AddFunc(left, right);
    }
    //こうして最適化もできる
    public static int Add(int left, int right, Func<int,int,int> AddFunc)
    {
        return AddFunc(left, right);
    }
}

//呼び出し側が、Exampleクラスの型引数を意識しなくてよくなった。振り分けは、Exampleクラスが責任をもって行う。
Example.Add(1, 1, (x, y) => x + y);  //Integer
Example.Add(1.1, 1.1, (x, y) => x + y); //Double

所感

機能 は する かも しれ ませ ん が、 実行 時 の 型 が IComparer < T > を 実装 し て いる かを 実行 時 に 確認 し、 適切 な メソッド を 呼び出す という 余計 な 手間 が かかり ます。BillWagner. Effective C# 6.0/7.0 (p.107). 株式会社翔泳社. Kindle 版.

紹介されていたtipsも勉強になったが、これまでの開発において、いかに↑の意識がうすかったかという点がよくわかったし、勉強になった。動かないとそもそもダメだが、動けばいいということでもない。