Closed12

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

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

項目18 最低限必須となる制約を常に定義すること

  • 制約を指定することで安全性が高まる(実行時エラーではなくコンパイルエラーとなる)
  • 一方で、制約が厳しすぎると柔軟性が失われ使われなくなる(ジェネリックなのに!)
制約をかけることで幅が広がる例
public class MyGeneric<T> where T : SampleClass
{
    public T Value { get; set; }
    
    public MyGeneric(T value)
    {
        Value = value;
    }
    
    public void DoSomething()
    {
        // もし where T : SampleClass が無ければ、以下の1行はコンパイルエラーとなる
        Value.DoSample();
    }
}

public class SampleClass
{
    public void DoSample() { }
}
  • 制約がない場合、TにはSystem.Objectに定義されたメソッドしか使えない
  • 制約を課すと、呼び出されるメソッドの型が変わることがある
IEquatable<T>.EqualsとSystem.Object.Equalsの切り替え


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

項目19 実行時の型チェックを使用してジェネリックアルゴリズムを特化する

  • 型引数(T)が、さらなる機能をサポートする場合(リッチな機能を持った型の場合)、その機能を優先的に生かせるような実装をしていきたい。
  • コンストラクタを複数用意したり、実行時にオブジェクトの型をチェックして処理を分岐させたりなどの工夫をする。
非効率なコード
  • 前提
    • 逆順で出力するためのIEnumerable<T>を作る。
    • ReverseEnumeratorの中でランダムアクセス([i]によるアクセス)を利用するため、どこかのタイミングでIEnumerable<T>型のsequenceをIList<T>に変換する必要がある
    • ReverseEnumerable
using System.Collections;

public class ReverseEnumerable<T> : IEnumerable<T>
{
    private IEnumerable<T> _sourceSequence;
    private IList<T> _listSequence;

    // コンストラクタの引数が、IEnumerable<T>までしか保証していない(IList<T>の実装を保証していない)。そのため変換の必要性がある。
    public ReverseEnumerable(IEnumerable<T> sequence)
    {
        _sourceSequence = sequence;
    }

    public IEnumerator<T> GetEnumerator()
    {
        // ★課題感
        // IList<T>で渡したいが、IEnumerable<T>までしか保証されていないため、コピーを作って型を変換する必要がある。
        // しかし、_sourceSequenceがもしIList<T>型なのであれば、このように値をコピーするのは無駄な作業となる。(既にIList<T>型にする目的を達成しているので)
        if (_listSequence == null)
        {
            _listSequence = new List<T>();
            foreach (T item in _sourceSequence)
            {
                _listSequence.Add(item);
            }
        }
        return new ReverseEnumerator<T>(_listSequence);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

public class ReverseEnumerator<T> : IEnumerator<T>
{
    private int _currentIndex;
    private IList<T> collection;

    public ReverseEnumerator(IList<T> srcCollection)
    {
        collection = srcCollection;
        _currentIndex = collection.Count;
    }
    
    // CurrentがIList<T>ではなく、IEnumerable<T>であると、[]によるアクセス(ランダムアクセス)を使えない
    // 参考①: IList<T> : ICollection<T>
    // 参考②: ICollection<T> : IEnumerable<T>
    public T Current => collection[_currentIndex];

    object IEnumerator.Current => this.Current;

    public bool MoveNext()
    {
        _currentIndex--;
        return _currentIndex >= 0;
    }

    public void Reset() => _currentIndex = collection.Count;

    // 何か処理
    public void Dispose() { }
}

public class Program
{
    public static void Main()
    {
        var sampleList = new List<int>() { 1, 2, 3, 4, 5 };
        var reverseEnumerable = new ReverseEnumerable<int>(sampleList);
        foreach (var item in reverseEnumerable)
        {
            Console.Write(item);
        }
    }
}

出力

54321
効率的になるように書き換えたコード
  • 差分があるのはReverseEnumerable<T>だけなのでそのクラスのみを書く。
using System.Collections;
using System.Collections.ObjectModel;

public class ReverseEnumerable<T> : IEnumerable<T>
{
    private IEnumerable<T> _sourceSequence;
    private IList<T> _listSequence;

    public ReverseEnumerable(IEnumerable<T> sequence)
    {
        _sourceSequence = sequence;
        // もし、_sourceSequenceがIList<T>型でない場合、_listSequenceはnullのままとなるが、GetEnumerator()で回収されるのでOK。
        // asを使えばキャスト時に例外が出ないのが嬉しい点。
        _listSequence = sequence as IList<T>;
    }
    
    public ReverseEnumerable(IList<T> sequence)
    {
        _sourceSequence = sequence;
        // コンストラクタの時点でIList<T>型なので、キャストをする必要がない
        _listSequence = sequence;
    }
    
    public IEnumerator<T> GetEnumerator()
    {
        if (_sourceSequence is string)
        {
            // stringの場合には特殊ケースとして扱うとのこと。
        }
        
        if (_listSequence == null)
        {
            // さらに、IList<T>を実装しないICollection<T>である場合、new List<T>(初期数)とすることで効率的にすることができる
            if (_sourceSequence is ICollection<T>)
            {
                ICollection<T> source = _sourceSequence as ICollection<T>;
                _listSequence = new List<T>(source.Count);
            }
            else
            {
                _listSequence = new List<T>();   
            }
            
            foreach (T item in _sourceSequence)
            {
                _listSequence.Add(item);
            }
        }
        return new ReverseEnumerator<T>(_listSequence);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目20 IComparable<T>とICompare<T>により順序関係を実装する

  • 型に順序比較の機能を作る場合は、基本的にIComparable<T>を利用する
  • IComparableを実装する場合、IComparableの順序と一致するような比較演算子(<, >, <=, >=)も同時に実装するべき
実験コード
public struct Customer : IComparable<Customer>, IComparable
{
    private readonly string _name;
    
    public Customer(string name)
    {
        _name = name;
    }
    
    public int CompareTo(Customer other)
    {
        return _name.CompareTo(other._name);
    }

    int IComparable.CompareTo(object obj)
    {
        if (!(obj is Customer))
            throw new ArgumentException("引数はCustomer型ではない", "obj");
        Customer otherCustomer = (Customer)obj;
        return this.CompareTo(otherCustomer);
    }
    
    // 演算子オーバーロード(このオーバーロードの中のCompareToはタイプセーフ
    public static bool operator <(Customer left, Customer right) => left.CompareTo(right) < 0;
    public static bool operator >(Customer left, Customer right) => left.CompareTo(right) > 0;
    // (<=, >=も同様)
}

public struct Employee {}

public class Program
{
    public static void Main()
    {
        var c1 = new Customer("axx");
        var c2 = new Customer("bxx");

        Console.WriteLine($"c1.CompareTo(c2): {c1.CompareTo(c2)}"); // axx < bxxなので、"-1"が出力
        Console.WriteLine($"c1 < c2: {c1 < c2}"); // axx < bxxなので、"True"が出力
        
        // タイプセーフに関する実験
        var e1 = new Employee();
        
        // CompareToの引数がCustomer以外の型であるとコンパイルエラーとなる(タイプセーフ)
        // Console.WriteLine($"c1 vs e1: {c1.CompareTo(e1)}"); // Argument type 'Employee' is not assignable to parameter type 'Customer'
        
        // 明示的にIComparable.CompareToを呼び出すことで、コンパイルは通過する。しかし実行時エラーとなる。
        Console.WriteLine($"c1 vs e1: {((IComparable)c1).CompareTo(e1)}");
    }
}

出力

c1.CompareTo(c2): -1
c1 < c2: True
Unhandled exception. System.ArgumentException: 引数はCustomer型ではない (Parameter 'obj')
   at Customer.System.IComparable.CompareTo(Object obj) in F:\Project\CSharpLearning\Program.cs:line 17
   at Program.Main() in F:\Project\CSharpLearning\Program.cs:line 47
  • 古いライブラリでは、IComparerインターフェースを利用することで、「その型で第一に使われる変数」以外の変数での比較を作ることができる。この方法は.NET Framework内で定義されたクラスを並び替える場合などのようにソースコードにアクセスできないクラスに対してのみ採用するべき
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目21 破棄可能な型引数をサポートするようにジェネリック型を作成すること

  • TのDispose処理を呼ぶように意識をする必要がある
  • ジェネリッククラスのメソッド内でIDisposableが実装されたT型インスタンスを生成する場合は(ローカル変数の場合は)、usingを使用すればOK
実験コード
public interface IEngine
{
    void DoWork();
}

public class DisposableEngine : IEngine, IDisposable
{
    public void DoWork()
    {
        Console.WriteLine("DisposableEngine doing work");
    }
    
    public void Dispose()
    {
        Console.WriteLine("Engine disposing");
    }
}

public class UnDisposableEngine : IEngine
{
    public void DoWork()
    {
        Console.WriteLine("UnDisposableEngine doing work");
    }
}

public class EngineDriver<T> where T : IEngine, new()
{
    public void GetThingsDone()
    {
        T engine = new T();
        
        // ここのキャストを(IDisposable)engineとすると、IDisposableを実装していない場合実行時エラーになる。
        // asならnullが返却されるだけで、挙動には影響を与えない。
        // これは、コンパイラによって、Dispose()の呼び出し前にnullチェックが実行されるため
        using (engine as IDisposable)
        {
            engine.DoWork();   
        }
    }
}

public class Program
{
    public static void Main()
    {
        Console.WriteLine("=== DisposableEngine ===");
        var driverWithDisposable = new EngineDriver<DisposableEngine>();
        driverWithDisposable.GetThingsDone();
        
        Console.WriteLine("=== UnDisposableEngine ===");
        var driverWithUnDisposable = new EngineDriver<UnDisposableEngine>();
        driverWithUnDisposable.GetThingsDone();
    }
}

出力

=== DisposableEngine ===
DisposableEngine doing work
Engine disposing
=== UnDisposableEngine ===
UnDisposableEngine doing work
  • 補足、using (null)の中は実行される
using (null)
{
    // これは実行される
    Console.WriteLine("using (null)の中で実行");
}
  • ジェネリッククラスのメンバ変数にDisposableが実装されたT型インスタンスが入る場合は、クラス自体がDisposableを実装してインスタンスを破棄する
実験コード
// 派生クラスでDispose()が呼ばれることを考えないでよいようにするためにsealed
public sealed class EngineDriver<T> : IDisposable where T : IEngine, new()
{
    private T _engine = new();

    public void GetThingsDone()
    {
        _engine.DoWork();
    }

    public void Dispose()
    {
        if (_engine is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

public class Program
{
    public static void Main()
    {
        Console.WriteLine("== DisposableEngine ==");
        var driverWithDisposable = new EngineDriver<DisposableEngine>();
        driverWithDisposable.GetThingsDone();
        driverWithDisposable.Dispose();
        
        Console.WriteLine("== UnDisposableEngine ==");
        var driverWithUnDisposable = new EngineDriver<UnDisposableEngine>();
        driverWithUnDisposable.GetThingsDone();
        driverWithUnDisposable.Dispose();
    }
}

出力

== DisposableEngine ==
DisposableEngine doing work
Engine disposing
== UnDisposableEngine ==
UnDisposableEngine doing work
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

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

  • 独自にインターフェースやデリゲートを作成する場合、できる限りinやout修飾子を設定するべき
    (といいつつ現時点での自分の理解では.NETが用意しているインターフェースを理解することにとどまりそう)

共変

  • 基底型に対して派生型を代入できるということ
  • <out T>...T型、もしくはその派生型を認める
  • 共変が認められるのは、例えばIEnumeratorのように、列挙する(=参照する)だけの機能を持ち、値の代入(変更)が発生しないことが確定しているもの。
    • 逆に、Listのように、値を設定できてしまう型は共変にしてはいけない。
  • ちなみに配列では以下のような共変性による問題が起きてしまう。ゆえに配列よりコレクションが推奨される。
    • Riderでも注意される

IEnumerable<out T>とList<T>の共変性の違い確認コード
public class Parent { }
public class Child : Parent {}
public class NotChild {}

public class MyClass
{
    public void DoWithEnumerable(IEnumerable<Parent> parents) { }
    public void DoWithList(List<Parent> parents) { }
}

public class Program
{
    public static void Main()
    {
        List<Parent> parents = new List<Parent>();
        List<Child> children = new List<Child>();
        List<NotChild> notChildren = new List<NotChild>();
        
        var myClass = new MyClass();
        
        // 当たり前にOK...受け皿通りの型を渡す
        myClass.DoWithEnumerable(parents);
        // OK! 定義: IEnumerable<out T> により共変性があるのでOK...親クラスの受け皿に子クラスを渡せる
        myClass.DoWithEnumerable(children);
        // 当たり前にコンパイルエラー...親クラスを継承していない全く関係ないクラスはNG
        myClass.DoWithEnumerable(notChildren);
        
        // 当たり前にOK...受け皿通りの型を渡す
        myClass.DoWithList(parents);
        // コンパイルエラー...定義: List<T> より共変性がない
        myClass.DoWithList(children); //コンパイルエラー
    } 
}

共変性について補足

public class MyClass
{
    public void DoWithEnumerable(IEnumerable<Parent> parents)
    {
        // 当然だがIEnumerableにはインデクサによるセットはできないので、以下の行はコンパイルエラー
        parents[0] = new Parent();
        // もし上記のようなことが出来てしまうと、Childのリスト(のようなもの)に対してParentを入れるようなことが出来てしまい問題である。
        // IEnumerableはそのようなこと(代入)が出来ないため、共変(親クラスに子クラスを渡す)の性質を持っていても問題がない。
    }

    public void DoWithList(List<Parent> parents)
    {
        parents[0] = new Parent();
        // Listはインデクサによるセットが可能なので、以下の行はコンパイルエラーにならない。
        // そのためList型の場合は共変性が無く定義されている。
    }
}

反変

  • 派生型に対して基底型を引き渡せる性質のこと
  • <in T>...T型、もしくはその上位型を認める
実験コード
public class Parent
{
    public int Height { get; set;  }
    public string Name { get; set; }
}
public class Child : Parent {}

public class NotChild
{
    public int Height { get; set;  }
    public string Name { get; set; }
}

public class HeightComparer : IComparer<Parent>
{
    public int Compare(Parent? left, Parent? right)
    {
        return left.Height.CompareTo(right.Height);
    }
}

public class Program
{
    public static void Main()
    {
        List<Parent> parents = new List<Parent>
        {
            new() { Height = 160, Name = "Parent_160" },
            new() { Height = 180, Name = "Parent_180" },
            new() { Height = 170, Name = "Parent_170" },
        };
        
        List<Child> children = new List<Child>
        {
            new() { Height = 130, Name = "Child_130" },
            new() { Height = 120, Name = "Child_120" },
            new() { Height = 110, Name = "Child_110" },
        };
        
        List<NotChild> notChildren = new List<NotChild>
        {
            new() { Height = 130, Name = "NotChild_130" },
            new() { Height = 120, Name = "NotChild_120" },
            new() { Height = 110, Name = "NotChild_110" },
        };
        
        // public class IList<T>におけるSortの定義はこうなっている
        // public void Sort(IComparer<T>? comparer) => Sort(0, Count, comparer);
        
        // 当たり前にOK
        // .Sort(IComparer<Parent>)の求める引数通りの型が渡されている
        parents.Sort(new HeightComparer());

        // 反変によりOK
        // .Sort(IComparer<Child>)であり、求める引数は子クラスであるが、親クラスのIComparer<Parent>を渡してもOK
        children.Sort(new HeightComparer());
        
        // 並び替えた結果を確認
        parents.ForEach(parent => Console.WriteLine(parent.Name));
        children.ForEach(child => Console.WriteLine(child.Name));
        
        // 当たり前にコンパイルエラー...親クラスを継承していない
        // notChildren.Sort(new HeightComparer());
    } 
}

出力(ちゃんとHeightで比較されて並び替えられている)

Parent_160
Parent_170
Parent_180
Child_110
Child_120
Child_130
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

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

  • (たいていは制約はクラス制約やインターフェース制約が良い。が、独自のメソッドを強制したい場合に以下の話が出てくる)
  • Q: ジェネリッククラスが、型TにAdd()メソッドを必要としている場合、それをどう実装するか?
  • A1: 1つのアイデアとしては、IAdd<T>インターフェースを作成し、そのインターフェースにAdd()メソッドを作成する方法がある。
    • しかしこの場合、IAdd<T>インターフェースを実装したクラスを作る必要がある。もっと簡単にできないか?
  • A2: デリゲートを使うと以下のように、ジェネリッククラスで型TにAdd()メソッドがあることを強制することができる
コード
public class Example
{
    // where T : IAdd<T>とするのではなく、自分自身でAddを実装してしまうイメージ
    public T Add<T>(T left, T right, Func<T, T, T> AddFunc)
    {
        return AddFunc(left, right);
    }
}

public class Program
{
    public static void Main()
    {
        var example = new Example();
        
        int a = 5;
        int b = 6;
        var sumInt = example.Add(a, b, (x, y) => x + y);
        Console.WriteLine($"int型でのAdd: {sumInt}");
        
        string c = "Hello ";
        string d = "World!";
        var sumString = example.Add(c, d, (x, y) => x + y);
        Console.WriteLine($"string型でのAdd: {sumString}");
    }
}

出力

int型でのAdd: 11
string型でのAdd: Hello World!
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目24 親クラスやインターフェース用に特化したジェネリックメソッドを作成しないこと

  • 仮引数(メソッドを定義している場所に書いてある引数)と実引数(メソッドを呼び出すところで渡す引数)の型が、
    • 同一である場合(変換が必要ない)が最優先(ジェネリックメソッドよりも優先度は高い)
    • 変換が必要な場合(親クラスやInterfaceにキャスト)はジェネリックメソッドの方が優先される。ジェネリックは「あらゆるものに一致する」から。
  • 優先度高 同一型 > ジェネリック > 変換が必要なもの
  • 実行時にオブジェクトの型を見て呼ぶメソッドを切り替えることは可能だが、条件分岐が増えたり、パフォーマンスが落ちたりと問題がある。ゆえ、ジェネリックメソッドを優先して公開しない方がよい(ジェネリックメソッドに吸収されてしまうから)
実験コード
public class Parent {}

public interface IMessageWriter
{
    void WriteMessage();
}

public class Child : Parent, IMessageWriter
{
    void IMessageWriter.WriteMessage() => 
        Console.WriteLine("Child.WriteMessage()内");
}

public class AnotherWriter : IMessageWriter
{
    public void WriteMessage() => 
        Console.WriteLine("Writer.WriteMessage()内");
}

public class Program
{
    private static void WriteMessage(Parent parent)
    {
        Console.WriteLine("Program.WriteMessage(Parent)内");
    }

    private static void WriteMessage<T>(T obj)
    {
        Console.Write("Program.WriteMessage<T>(T)内 & ");
        Console.WriteLine(obj.ToString());
    }

    private static void WriteMessage(IMessageWriter obj)
    {
        Console.Write("Program.WriteMessage(IMessageWriter)内 & ");
        obj.WriteMessage();
    }

    public static void Main()
    {
        Child child = new Child(); 
        
        Console.WriteLine("Program.WriteMessage Childで呼び出し");
        WriteMessage(child);
        Console.WriteLine("=============");
        
        Console.WriteLine("Program.WriteMessage Childをインターフェースにキャストしてを呼び出し");
        WriteMessage((IMessageWriter)child);
        Console.WriteLine("=============");
        
        Console.WriteLine("Program.WriteMessage ChildをParentにキャストして呼び出し");
        WriteMessage((Parent)child);
        Console.WriteLine("=============");
        
        AnotherWriter anotherWriter = new AnotherWriter();
        Console.WriteLine("Program.WriteMessage AnotherWriterで呼び出し");
        WriteMessage(anotherWriter);
        Console.WriteLine("=============");
        
        Console.WriteLine("Program.WriteMessage AnotherWriterをインターフェースにキャストしてを呼び出し");
        WriteMessage((IMessageWriter)anotherWriter);
        Console.WriteLine("=============");
    }
}
Childクラスのオブジェクトを引数に渡す場合の実験

引数にChild型でそのまま渡す場合(WriteMessage(child);

  • 結果: Tが最優先、ParentとInterfaceが2番手で同等(両立不可能)
T Parent Interface 結果
最優先は? T
2番手は? × コンパイルエラー(Ambiguous invocation, 両方マッチします)
絞れば呼ばれるよね? × × Parent
絞れば呼ばれるよね? × × Interface

引数にInterfaceにキャストして渡す場合(WriteMessage((IMessageWriter)child);

  • 結果: Interfaceが最優先, Tが2番手、Parentは呼べない
T Parent Interface 結果
最優先は? Interface
2番手は? × T
Parentは呼べる? × × コンパイルエラー(Argument type 'IMessageWriter' is not assignable to parameter type 'Parent'

引数にParentにキャストして渡す場合(WriteMessage((Parent)child);

  • 結果: Parentが最優先、Tが2番手、Interfaceは呼べない
T Parent Interface 結果
最優先は? Parent
2番手は? × T
Interfaceは呼べる? × × コンパイルエラー(Argument type 'Parent' is not assignable to parameter type 'IMessageWriter'
AnotherWriterクラスのオブジェクトを引数に渡す場合の実験

引数にAnotherType型でそのままで渡す場合(WriteMessage(anotherWriter);

  • 結果: Tが最優先、Interfaceが2番手、Parentは継承してないので呼べない
T Parent Interface 結果
最優先は? T
2番手は? × Interface
Parentは呼べる? × × コンパイルエラー(Argument type 'AnotherWriter' is not assignable to parameter type 'Parent'

引数にInterfaceにキャストして渡す場合(WriteMessage((IMessageWriter)anotherWriter);

  • 結果: Interfaceが最優先、Tが2番手、Parentは継承していないので呼べない
T Parent Interface 結果
最優先は? Interface
2番手は? × T
Parentは呼べる? × × コンパイルエラー(Argument type 'IMessageWriter' is not assignable to parameter type 'Parent'
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

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

  • ジェネリッククラスではなく、通常のクラスのジェネリックメソッドとすることで、下記のような利点がある。
    • より効率的なメソッドを作成し、呼び出されることができる(例、int型に特化することでTのCompareより効率的なMaxメソッドが使える)
    • メソッドの呼び出し時に型を指定する必要がない
  • ジェネリックメソッドにした方が良い時の条件は、
    • クラスのメンバ変数としてTを保持しないとき(Tがメソッドだけで完結するとき)
    • ジェネリックインターフェイス(例: IEnumerable<T>)を実走しない時
実験コード
public static class GenericUtils<T>
{
    public static T Max(T left, T right)
    {
        Console.WriteLine("GenericUtils<T> / T max");
        return Comparer<T>.Default.Compare(left, right) < 0 ? right : left;
    }

    // GenericUtils<int>.Max(a, b);としても呼ばれるのはこちらのメソッドではない。
    // 数値型であればこちらの方が効率的
    public static int max(int left, int right)
    {
        Console.WriteLine("GenericUtils<T> / int max");
        return Math.Max(left, right);
    }
}

public static class NotGenericUtils
{
    public static T Max<T>(T left, T right)
    {
        Console.WriteLine("NotGenericUtils / T max");
        return Comparer<T>.Default.Compare(left, right) < 0 ? right : left;
    }
    
    // 数値型であればこちらの方が効率的
    public static int Max(int left, int right)
    {
        Console.WriteLine("NotGenericUtils / int max");
        return Math.Max(left, right);
    }
}

public class Program
{
    public static void Main()
    {
        int a = 1;
        int b = 2;
        int biggerByGeneric = GenericUtils<int>.Max(a, b); // <T>が<int>でも、メソッドは<T>の方が呼ばれる
        int biggerByNotGeneric = NotGenericUtils.Max(a, b); // int型のメソッドが呼ばれることが嬉しい
        Console.WriteLine($"biggerByGeneric: {biggerByGeneric}, biggerByNotGeneric: {biggerByNotGeneric}");

        Console.WriteLine("========");

        string c = "aad";
        string d = "aac";
        string biggerStringByGeneric = GenericUtils<string>.Max(c, d);
        string biggerStringByNotGeneric = NotGenericUtils.Max(c, d); // 対応する型がない場合はTが呼ばれる
        Console.WriteLine($"biggerStringByGeneric: {biggerStringByGeneric}, biggerStringByNotGeneric: {biggerStringByNotGeneric}");
    }
}

出力

GenericUtils<T> / T max
NotGenericUtils / int max
biggerByGeneric: 2, biggerByNotGeneric: 2
========
GenericUtils<T> / T max
NotGenericUtils / T max
biggerStringByGeneric: aad, biggerStringByNotGeneric: aad
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目26 ジェネリックインターフェースとともに古いインターフェースを実装すること

  • 古い.NETの関係で、ジェネリックではないインターフェース(IComparableなど)の実装もサポートする必要がある。
  • int ICompareble.CompareTo(object obj)のように、非ジェネリックインターフェースは明示的に実装する。これによって、ジェネリックインターフェースを使用させたい場面で非ジェネリックインターフェースが呼ばれることがなくなる
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目27 最小限に制限されたインターフェースを拡張メソッドにより機能拡張する

  • 拡張メソッドを利用してインターフェースに機能を追加することができる。
  • インターフェースは機能を最小限にしておき、補助的なメソッドを拡張メソッドとして定義することで、使用者側が使いやすいメソッドを増やすことができる。
実験コード
// 自作のIComparable実装コード。もちろんintなどでも実験可能
public class Customer : IComparable<Customer>, IComparable
{
    private readonly string _name;
    
    public Customer(string name)
    {
        _name = name;
    }
    
    public int CompareTo(Customer other)
    {
        return _name.CompareTo(other._name);
    }

    int IComparable.CompareTo(object obj) // 省略
}

public static class Comparable
{
    public static bool LessThan<T>(this T left, T right) where T : IComparable<T>
    {
        return left.CompareTo(right) < 0;
    }
    
    // 他の比較については省略
}

public class Program
{
    public static void Main()
    {
        // cus1の方が大きい
        var cus1 = new Customer("ac");
        var cus2 = new Customer("ab");
        Console.WriteLine($"CompareToを呼び出し: {cus1.CompareTo(cus2)}");
        Console.WriteLine($"LessThanを呼び出し: {cus1.LessThan(cus2)}");
    }
}

出力

CompareToを呼び出し: 1
LessThanを呼び出し: False
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目28 構築された型に対する拡張メソッドを検討すること

  • Tの型引数に具体的な型を指定した状態の拡張メソッドを作成することで、その型に固有の機能を使用したメソッドを作成することができる
  • 例えば、System.Linq.Enumerableクラスでは、Max(this IEnumerable<int> source)メソッドが存在する
System.Linq.Enumerableクラスのint型用Maxメソッド
namespace System.Linq
{
    public static partial class Enumerable
    {
        public static int Max(this IEnumerable<int> source)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }

            int value;
            using (IEnumerator<int> e = source.GetEnumerator())
            {
                if (!e.MoveNext())
                {
                    ThrowHelper.ThrowNoElementsException();
                }

                value = e.Current;
                while (e.MoveNext())
                {
                    int x = e.Current;
                    if (x > value)
                    {
                        value = x;
                    }
                }
            }

            return value;
        }
  • 独自で作った型に対しても、以下のように拡張メソッドを作成することができる
public static void SendEmailCoupons(this IEnumerable<Customer> customers, Coupon specialOffer)
このスクラップは2023/09/24にクローズされました