Closed18

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

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

項目11 独自のAPIでは変換演算子を定義しない

  • 暗黙的な型変換を行うと、その変換で生成されたオブジェクトがすぐにガベージコレクターで削除されることになる。
  • 自分が作成するクラスで、変換演算子を作るのは避けるべきだ。
暗黙的な型変換が利用され、問題ない例と問題になる例
public class Shape { }

public class Point { }

public class Circle : Shape
{
    public Point center;
    public double radius;
    
    public Circle(Point c, double r)
    {
        center = c;
        radius = r;
    }

    // 暗黙的な型変換を定義する
    public static implicit operator Ellipse(Circle c)
    {
        return new Ellipse(c.center, c.center, c.radius, c.radius);
    }
}

public class Ellipse : Shape
{
    public Point center1;
    public Point center2;
    public double radius1;
    public double radius2;
    
    public Ellipse(Point c1, Point c2, double r1, double r2)
    {
        center1 = c1;
        center2 = c2;
        radius1 = r1;
        radius2 = r2;
    }
}

public class Program
{
    public static void Main()
    {
        var c = new Circle(new Point(), 1.0);
        
        Ellipse e = c; // 暗黙的な型変換
        Console.WriteLine(e.GetType()); // Ellipse
        
        // 暗黙的な型変換
        Console.WriteLine(ComputeArea(c)); // 3.141592653589793
        // ↑このメソッド内の暗黙的な変換で新たにcから生成されたEllipseは、すぐにガベージコレクターで破棄される
        // だが、面積が知りたいだけなので、この挙動は問題ない

        // 暗黙的な型変換は新しいインスタンスを作ってreturnしているので、c自体には変更は無い
        Console.WriteLine(c.GetType()); // Circle
        
        // 暗黙的な型変換
        Flatten(c);
        Console.WriteLine(c.radius); // 1
        // Flattenメソッドによって押しつぶされたEllipseは、すぐにガベージコレクターで破棄される
        // このように、オブジェクト自体に変更を残したい場合は、暗黙的な型変換で新たなインスタンスが生成されると意味がない。
    }

    private static double ComputeArea(Ellipse e)
    {
        return Math.PI * e.radius1 * e.radius2;
    }

    // 楕円を押しつぶすメソッド
    private static void Flatten(Ellipse e)
    {
        e.radius1 /= 2;
        e.radius2 *= 2;
    }
}```
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目12 省略可能なパラメータを使ってメソッドの多重定義を減らそう

  • 名前付き変数とすることで、以下のようなメリットがある。
    • メソッドの定義数が少なくなる(メソッド定義の中でデフォルト値を設定することによって)
    • 同じ型の引数が複数ある場合などは、それらの引数の順番が間違っていないか分かりやすい
  • しかし、一度決めて公開したAPIの場合は、公開後にpublicなメソッドの名前付き引数を変更するのは他のクラスにとって破壊的変更になるので避けるべきだ。そのような場合はメソッドを多重定義しよう。
実験コード
public class Program
{
    public static void Main()
    {
        Introduce("Taro", "pizza");
        Introduce(name: "Taro", food: "pizza");
        Introduce(food: "pizza", name: "Taro");
        Introduce("Taro", food: "pizza");
        Introduce(name: "Taro", "pizza");
        // Introduce(food: "pizza", "Taro"); // エラー: Named argument 'food' is used out-of-position but is followed by an unnamed argument
    }
    
    private static void Introduce(string name, string food)
    {
        Console.WriteLine($"My name is {name} and I like {food}.");
    }
}
  • ILになると、メソッドの定義側には名前を含むが、メソッドの呼び出し側では名前が入らなくなる。よって、順番さえ変えなければ、名前付き引数を変更したコンポーネントをリリース可能(差し替え可能)
    • ただし、メソッド利用側のコンポーネントをビルドしようとするとコンパイルエラーになる。(メソッド利用側がずっとILのままでC#として編集されないことはないだろうから、ほぼ確実に破壊的変更になる)
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目13 独自の型は可視性を制限しよう

  • 当たり前だができるだけ可視性を小さくしたほうが、コードの変更の際に考えることが少なくなって良い。
  • 具体的な実現方法
    • クラスの中に、protectedやprivateでネストしたクラスを作ること
    • publicなインターフェースだけを他のアセンブリに公開し、その実装クラスはinternalとすること。
ネストクラス
  • ネストされたクラスは、そのクラス自身に可視性がpublicでも、包み込む側のクラス経由でないとアクセスできない
public class Program
{
    public static void Main()
    {
        var outer = new Outer();
        var inner = new Outer.Inner();
        // var dirrectInnter = new Inner(); // コンパイルエラー
    }
}

public class Outer
{
    public class Inner { }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目14 継承するよりインターフェースを定義して実装しよう

  • インターフェイスは「AはBのように振る舞う」、抽象は「AはBの一種である」
拡張メソッドはそれが定義されているクラスと関係無いクラスに対して作れるか?
  • 結論、できる。
public interface IFoo
{
    void IFooMethod();
}

public class Foo : IFoo
{
    public void IFooMethod() => Console.WriteLine("IFooMethod");
    public void FooMethod() => Console.WriteLine("FooMethod");
}

// 拡張メソッドを実装するクラス
// このクラス自体はIFooやFooとは関係がない。
public static class Bar
{
    public static void IFooFromBar(this IFoo iFoo)
    {
        iFoo.IFooMethod();
    }

    public static void FooFromBar(this Foo foo)
    {
        foo.FooMethod();
    }
}

public class Program
{
    public static void Main()
    {
        var foo = new Foo();
        foo.IFooMethod();
        foo.IFooFromBar();
        foo.FooMethod();
        foo.FooFromVar();
    }
}

出力

IFooMethod
IFooMethod
FooMethod
FooMethod
System.Linq.Enumerableクラスの実装でIEnumerable<T>インターフェースが拡張されている
  • System.Linq.Enumerableクラスに、IEnumerable<T>インターフェース用の拡張メソッドがたくさん用意されている。
  • 本来のIEnumerable<T>インターフェースに用意されているメソッドは GetEnumerator()だけであるので、たくさんの力をSystem.Linq.Enumerableクラスから授かっている。
public class Program
{
    public static void Main()
    {
        var list = new List<int> { 1, 2, 3, 4, 5 };
        var evenNumbers = list.Where(n => n % 2 == 0);
    }
}
  • 上記のWhereの定義をたどると、System.Linq.Enumerableクラス(partialクラス)で、IEnumerable<T>インターフェースに対する拡張メソッドがたくさん用意されている
namespace System.Linq
{
    public static partial class Enumerable
    {
        public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
        {
            if (source == null)
            {...実装...
  • 上記Whereメソッド引数のIEnumerable<T>の定義はこちら
namespace System.Collections.Generic
{
    // Implement this interface if you need to support foreach semantics.
    public interface IEnumerable<out T> : IEnumerable
    {
        // Returns an IEnumerator for this enumerable Object.  The enumerator provides
        // a simple way to access all the contents of a collection.
#if MONO
        [System.Diagnostics.CodeAnalysis.DynamicDependency(nameof(Array.InternalArray__IEnumerable_GetEnumerator) + "``1 ", typeof(Array))]
#endif
        new IEnumerator<T> GetEnumerator();
    }
}
IEnumerable<T>の拡張メソッドを体感する
using System.Collections;

public class MyData
{
    public string name { get; set; }
    
    public MyData(string name)
    {
        this.name = name;
    }
}

public class MyDataCollection : IEnumerable<MyData>
{
    private List<MyData> _data = new List<MyData>
    {
        new ("Taro"),
        new ("Jiro"),
        new ("Saburo"),
    };

    public IEnumerator<MyData> GetEnumerator()
    {
        return _data.GetEnumerator();
    }

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

public class Program
{
    public static void Main()
    {
        var collection = new MyDataCollection();
        // Whereがcollectionに実装されているかのように振る舞う。実際はSystem.Linq.Enumerableクラスに書かれたIEnumerable<T>の拡張メソッド
        var ans = collection.Where(data => data.name == "Jiro"); 
        Console.WriteLine(ans.First().name);
    }
}

出力

Jiro
  • メソッドの戻り値で、具体的な型ではなくインターフェイスを返すことで、返却される側の出来ることを制限することができる。
    • たとえばIEnumerable<T>を返却することで、中身を削除したり変更したりすることができない。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目15 インターフェイスメソッドと仮想メソッドの違い

newを使った隠蔽の挙動

newを使って隠蔽しても、親やInterfaceにすると元のメソッドが呼ばれる

public interface IMessage
{
    void Message();
}

public class ParentMessage : IMessage
{
    public void Message()
    {
        Console.WriteLine("Message() from ParentMessage");
    }
}

public class ChildMessage : ParentMessage
{
    public new void Message()
    {
        Console.WriteLine("Message() from ChildMessage");
    }
}

public class Program
{
    public static void Main()
    {
        var child = new ChildMessage(); // 隠蔽
        child.Message();
        var parent = (ParentMessage)child; // 親
        parent.Message();
        var iMessage = (IMessage)child; // インターフェース
        iMessage.Message();
    }
}

出力

Message() from ChildMessage
Message() from ParentMessage
Message() from ParentMessage

もしChildMessageがIMessageを実装したら、以下のように出力が変わる

public class ChildMessage : ParentMessage, IMessage
{
    public new void Message()
    {
        Console.WriteLine("Message() from ChildMessage");
    }
}

出力

Message() from ChildMessage
Message() from ParentMessage
Message() from ChildMessage

virtual / overrideで実装すると全てでChildの実装となる

public class ParentMessage : IMessage
{
    public virtual void Message()
    {
        Console.WriteLine("Message() from ParentMessage");
    }
}

public class ChildMessage : ParentMessage
{
    public override void Message()
    {
        Console.WriteLine("Message() from ChildMessage");
    }
}

出力

Message() from ChildMessage
Message() from ChildMessage
Message() from ChildMessage

abstractを用いる

  • インターフェイスによる契約を、この抽象クラスから派生する全ての具象クラスに任せることになる。
public abstract class ParentMessage : IMessage
{
    public abstract void Message();
}

public class ChildMessage : ParentMessage
{
    public override void Message()
    {
        Console.WriteLine("Message() from ChildMessage");
    }
}

出力

Message() from ChildMessage
Message() from ChildMessage
Message() from ChildMessage
基底クラスが実装を持つので子クラスがInterfaceの実装をしなくてよいケース
public class ParentMessage
{
    public void Message()
    {
        Console.WriteLine("Message() from ParentMessage");
    }
}

public class ChildMessage : ParentMessage, IMessage
{
    // ParentMessageの中でMessage()が実装されているので、ChildMessageで再実装する必要はない
}

出力

Message() from ParentMessage
Message() from ParentMessage
Message() from ParentMessage
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目16 通知のイベントパターンを実装しよう

  • イベントは、自分に興味を持つクライアントを知る必要がない。クライアント側が自分でイベントに対して登録をする(+=)
Loggerを例としたeventの実装
public class Logger
{
    public static Logger Singleton { get; }
    static Logger()
    {
        Singleton = new Logger();
    }
    
    private Logger() {}

    public event EventHandler<LoggerEventArgs> Log;
    
    public void AddMsg(int priority, string message)
    {
        Log?.Invoke(this, new LoggerEventArgs { Priority = priority, Message = message });
    }
}

public class LoggerEventArgs : EventArgs
{
    public int Priority { get; set; }
    public string Message { get; set; }
}

// Consoleにログを出力するクラス
public class ConsoleLogger
{
    static ConsoleLogger()
    {
        Logger.Singleton.Log += (sender, loggerArgs) => Console.WriteLine($"Sender: {sender}, Priority: {loggerArgs.Priority}, Message: {loggerArgs.Message}");
    }
}

public class Program
{
    public static void Main()
    {
        var logger = Logger.Singleton;
        var _ = new ConsoleLogger();
        logger.AddMsg(1, "test");
    }
}

出力

Sender: Logger, Priority: 1, Message: test
  • もしイベント数が多くなっていったら、EventHanderを格納するListを作るなどして工夫できる。
EventHanderListを用いて格納する具体的な実装
using System.ComponentModel;

public class Logger
{
    private static readonly EventHandlerList _handlers = new ();

    public static void AddLogger(string system, EventHandler<LoggerEventArgs> ev)
    {
        _handlers.AddHandler(system, ev);
    }
    
    public static void RemoveLogger(string system, EventHandler<LoggerEventArgs> ev)
    {
        _handlers.RemoveHandler(system, ev);
    }

    public static void AddMsg(string system, int priority, string message)
    {
        if (string.IsNullOrEmpty(system)) return;
        
        var handler = _handlers[system] as EventHandler<LoggerEventArgs>;
        var args = new LoggerEventArgs { Priority = priority, Message = message };
        handler?.Invoke(null, args);
        
        var handlerNotCast = _handlers[system]; // Delegate?型
        // handlerNotCast.Invoke(null, args); コンパイルエラー
    }
}

public class LoggerEventArgs : EventArgs
{
    public int Priority { get; set; }
    public string Message { get; set; }
}

public class Program
{
    public static void Main()
    {
        Logger.AddLogger("sample",
            (sender, e) => Console.WriteLine($"Sender: {sender}, Priority: {e.Priority}, Message: {e.Message}"));
        Logger.AddMsg("sample", 1, "test");
    }
}

出力

Sender: , Priority: 1, Message: test
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目17 内部オブジェクトへの参照を返さないように注意しよう

  • 参照型のプロパティは、リードオンリーで公開したとしても、呼び出し側から変更することが可能である。
リードオンリーなプロパティを変更できる例
public class BusinessPerson
{
    public List<ImportantData> Data { get; } // リードオンリー

    public BusinessPerson()
    {
        Data = new List<ImportantData>
        {
            new("Taro"),
            new("Jiro"),
            new("Saburo"),
        };
    }
}

public class ImportantData
{
    public string Name { get; }
    public ImportantData(string name) => Name = name;
}

public class Program
{
    public static void Main()
    {
        var bp = new BusinessPerson();
        Console.WriteLine($"Clear前: {bp.Data.Count}");
        bp.Data.Clear();
        Console.WriteLine($"Clear後: {bp.Data.Count}");
    }
}

出力

Clear前: 3
Clear後: 0 // リードオンリーなはずなの状態が変更されている!!
  • 元々 List<T>として公開していた部分を、IEnumerable<T>として公開すれば、Clearなどはできなくなる。
制限されたインターフェイスで返すコード
public class BusinessPerson
{
    public IEnumerable<ImportantData> Data { get; } // リードオンリー
    // 略

public class Program
{
    public static void Main()
    {
        var bp = new BusinessPerson();
        foreach (var data in bp.Data)
        {
            Console.WriteLine(data.Name);
        }
        // bp.Data.Clear(); // コンパイルエラー
    }
}

出力

Taro
Jiro
Saburo
  • IEnumerableではCountメソッドが使えない。IReadOnlyCollectionで返すと使える
public class BusinessPerson
{
    public IReadOnlyCollection<ImportantData> Data { get; } // リードオンリー
    // 略

public class Program
{
    public static void Main()
    {
        var bp = new BusinessPerson();
        foreach (var data in bp.Data)
        {
            Console.WriteLine(data.Name);
        }
        Console.WriteLine(bp.Data.Count);
        // bp.Data.Clear(); // コンパイルエラー
    }
}

出力

Taro
Jiro
Saburo
3
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目18 イベントハンドラよりオーバーライドが好ましい時

  • 何かのイベントに対応するメソッドを作成する時、イベントハンドラを作って登録するか、基底クラスのメソッドをオーバーライドするか?を検討する。
  • override void OnMouseDown(MouseButtonEventArgs e)のように、基底クラスの仮想メソッドをオーバーライドする方が、独自にイベントハンドラを作って割り当てるよりも良いことがある。
    • イベントハンドラの場合、登録されたイベントのうちどこかでエラーがでた場合、他に登録されていたイベントが発火されないことになる。
  • イベントは、実行時に結合出来るので柔軟性が高いという利点がある。複数のイベントハンドラを同じイベントにフックできる。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目19 基底クラスに定義のあるメソッドを多重定義しない

  • 基底クラスで定義されたメソッドを派生で多重定義(オーバーロード)するとどのメソッドが呼ばれるかの把握が難しくなる。
挙動の実験コード
public class Fruit {}
public class Apple : Fruit {}

public class Animal
{
    public void Eat(Apple parm) =>Console.WriteLine("Animal.Eat(Apple)");
}

public class Tiger : Animal
{
    // Riderによる警告:
    // より具体的な基底クラス メソッド 'Animal.Eat(Apple)' を隠しているため、'Tiger.Eat(Fruit)' を変更または削除してください
    public void Eat(Fruit parm) => Console.WriteLine("Tiger.Eat(Fruit)");
    
    public new void Eat(Apple parm) => Console.WriteLine("Tiger.Eat(Apple)");
}

public class Animal2
{
    public virtual void Eat(Apple parm) => Console.WriteLine("Animal2.Eat(Apple)");
}

public class Tiger2 : Animal2
{
    public override void Eat(Apple parm) => Console.WriteLine("Tiger2.Eat(Apple)");
}


public class Program
{
    public static void Main()
    {
        Animal animal = new Animal();
        animal.Eat(new Apple()); // Animal.Eat(Apple) ← 当たり前

        Tiger tiger = new Tiger();
        tiger.Eat(new Apple()); // Tiger.Eat(Apple) ← newで隠蔽していて、子が呼ばれる
        tiger.Eat(new Fruit()); // Tiger.Eat(Fruit) ← 子の方が広い定義でオーバーロードしている

        Animal animaledTiger = new Tiger();
        animaledTiger.Eat(new Apple()); // Animal.Eat(Apple) newで隠蔽している子のメソッドではなく、親が呼ばれる
        // animaledTiger.Eat(new Fruit()); // Argument type 'Fruit' is not assignable to parameter type 'Apple'
        
        Animal2 animal2 = new Animal2();
        animal2.Eat(new Apple()); // Animal2.Eat(Apple) ← 当たり前

        Tiger2 tiger2 = new Tiger2();
        tiger2.Eat(new Apple()); // Tiger2.Eat(Apple) ← overrideしている子が呼ばれる
        
        Animal2 animaledTiger2 = new Tiger2();
        animaledTiger2.Eat(new Apple()); // Tiger2.Eat(Apple) overrideしているので子(Tiger)の方が呼ばれる
    }
}
実験コード(省略可能引数を使ってオーバーロード)
public class Fruit {}
public class Apple : Fruit {}

public class Animal
{ 
    public void Eat2(Fruit parm) => Console.WriteLine("Animal.Eat2(Fruit)");
    
    public virtual void Eat3(Fruit parm) => Console.WriteLine("Animal.Eat3(Fruit)");
}

public class Tiger : Animal
{
    public void Eat2(Fruit parm, Fruit parm2 = null) => Console.WriteLine("Tiger.Eat2(Fruit, Fruit)");
    
    public override void Eat3(Fruit parm) => Console.WriteLine("Tiger.Eat3(Fruit)");
}

public class Program
{
    public static void Main()
    {
        Tiger tiger = new Tiger();
        tiger.Eat2(new Apple()); // Tiger.Eat2(Fruit, Fruit)
        ((Animal)tiger).Eat2(new Apple()); // Animal.Eat2(Fruit)

        Animal animaledTiger = new Tiger();
        animaledTiger.Eat2(new Apple()); // Animal.Eat2(Fruit)
        ((Tiger)animaledTiger).Eat2(new Apple()); // Tiger.Eat2(Fruit, Fruit)
        
        tiger.Eat3(new Apple()); // Tiger.Eat3(Fruit)
        animaledTiger.Eat3(new Apple()); // Tiger.Eat3(Fruit)
    }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目20 オブジェクトの結合はイベントによって実行時に強まる

  • イベントは、コンパイル時ではなく、実行時にクラス間の結合を強める。なので注意しよう!
  • また、イベントの購読側が、イベントソース側に対して変更を加える可能性がある。
    • イベントの引数が可変型であり、その引数の中身を見てイベントソース側でキャンセル処理が走る場合など。
EventArgs args = new EventArgs();
handler?.Invoke(this, args);
if (args.IsCanceled) return;
途中でキャンセルが走る場合に全てのEventが終了する例
public class WorkerEngine
{
    public event EventHandler<WorkerEventArgs> Handler;

    public void StartEvent()
    {
        for (int i = 0; i < 5; i++)
        {
            var args = new WorkerEventArgs(i);
            Handler?.Invoke(this, args);
            if (args.Cancel) return;
        }
    }
}

public class WorkerEventArgs : EventArgs
{
    public WorkerEventArgs(int value)
    {
        Console.WriteLine($"Args constructed: {value}");
        Number = value;
    }
    public int Number { get; }
    public bool Cancel { get; set; }
}

// ただ数値を出力するだけのLogger
public class SimpleLogger
{
    public SimpleLogger(WorkerEngine engine)
    {
        engine.Handler += (sender, args) => Console.WriteLine($"SimpleLogger {args.Number}");
    }
}

// 数値が2の時だけキャンセルするLogger
public class CancelLogger
{
    public CancelLogger(WorkerEngine engine)
    {
        engine.Handler += (sender, args) =>
        {
            Console.WriteLine($"CancelLogger {args.Number}");
            if (args.Number == 2) args.Cancel = true;
        };
    }
}

public class Program
{
    public static void Main()
    {
        var engine = new WorkerEngine();
        var cancelLogger = new CancelLogger(engine);
        var simpleLogger = new SimpleLogger(engine);
        engine.StartEvent();
    }
}

出力


Args constructed: 0
CancelLogger 0
SimpleLogger 0
Args constructed: 1
CancelLogger 1
SimpleLogger 1
Args constructed: 2
CancelLogger 2
SimpleLogger 2
  • イベントリスナの存続期間は、イベントソース側のオブジェクトの存続期間に一致する。
    • イベントリスナ側がDispose()されたあとでも、購読を解除しなければ、再びイベントが通知される可能性がある。これは避けなければならない。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

脱線 Eventをコンパイラはどう変換するか?

  • SharpLabを使って、event周りのadd, removeへの変換を見てみる。
元々のC#コード
using System;
using System.Linq;
using System.Collections.Generic;

public class Logger
{
    public event EventHandler<LoggerEventArgs> Log;
    
    public void AddMsg(int priority)
    {
        Log?.Invoke(this, new LoggerEventArgs(priority));
    }
}

public class LoggerEventArgs : EventArgs
{
    public int Priority { get; }

    public LoggerEventArgs(int priority)
    {
        Priority = priority;
    }
}

public class Program
{
    public static void Main()
    {
        var logger = new Logger();
        logger.Log += (sender, loggerArgs) => Console.WriteLine($"Sender: {sender}, Priority: {loggerArgs.Priority}");
        logger.AddMsg(5);
    }
}
コンパイラ変換後のC#コード
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
using System.Threading;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]

[NullableContext(1)]
[Nullable(0)]
public class Logger
{
    [CompilerGenerated]
    private EventHandler<LoggerEventArgs> m_Log;

    public event EventHandler<LoggerEventArgs> Log
    {
        [CompilerGenerated]
        add
        {
            EventHandler<LoggerEventArgs> eventHandler = this.Log;
            while (true)
            {
                EventHandler<LoggerEventArgs> eventHandler2 = eventHandler;
                EventHandler<LoggerEventArgs> value2 = (EventHandler<LoggerEventArgs>)Delegate.Combine(eventHandler2, value);
                eventHandler = Interlocked.CompareExchange(ref this.Log, value2, eventHandler2);
                if ((object)eventHandler == eventHandler2)
                {
                    break;
                }
            }
        }
        [CompilerGenerated]
        remove
        {
            EventHandler<LoggerEventArgs> eventHandler = this.Log;
            while (true)
            {
                EventHandler<LoggerEventArgs> eventHandler2 = eventHandler;
                EventHandler<LoggerEventArgs> value2 = (EventHandler<LoggerEventArgs>)Delegate.Remove(eventHandler2, value);
                eventHandler = Interlocked.CompareExchange(ref this.Log, value2, eventHandler2);
                if ((object)eventHandler == eventHandler2)
                {
                    break;
                }
            }
        }
    }

    public void AddMsg(int priority)
    {
        EventHandler<LoggerEventArgs> log = this.Log;
        if (log != null)
        {
            log(this, new LoggerEventArgs(priority));
        }
    }
}

public class LoggerEventArgs : EventArgs
{
    [CompilerGenerated]
    private readonly int <Priority>k__BackingField;

    public int Priority
    {
        [CompilerGenerated]
        get
        {
            return <Priority>k__BackingField;
        }
    }

    public LoggerEventArgs(int priority)
    {
        <Priority>k__BackingField = priority;
    }
}

public class Program
{
    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

        [Nullable(new byte[] { 0, 1 })]
        public static EventHandler<LoggerEventArgs> <>9__0_0;

        [NullableContext(1)]
        internal void <Main>b__0_0([Nullable(2)] object sender, LoggerEventArgs loggerArgs)
        {
            DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(20, 2);
            defaultInterpolatedStringHandler.AppendLiteral("Sender: ");
            defaultInterpolatedStringHandler.AppendFormatted<object>(sender);
            defaultInterpolatedStringHandler.AppendLiteral(", Priority: ");
            defaultInterpolatedStringHandler.AppendFormatted(loggerArgs.Priority);
            Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear());
        }
    }

    public static void Main()
    {
        Logger logger = new Logger();
        logger.Log += <>c.<>9__0_0 ?? (<>c.<>9__0_0 = new EventHandler<LoggerEventArgs>(<>c.<>9.<Main>b__0_0));
        logger.AddMsg(5);
    }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目21 イベントをvirtual宣言するのは避けよう

  • 基底クラスのイベントをvirtual宣言し、派生クラスでoverrideしたとき、基底クラスのイベントは隠蔽されるため予想通りにイベントが発火されない。
イベントがoverrideされているかいないかでの挙動の違い
public class LoggerBase
{
    public virtual event EventHandler<LoggerEventArgs> Log;
    
    public void AddMsg(int priority)
    {
        Console.WriteLine($"LoggerBase: AddMsg {priority}");
        Log?.Invoke(this, new LoggerEventArgs(priority));
    }
}

public class LoggerDerived : LoggerBase
{
    // public override event EventHandler<LoggerEventArgs> Log; 
}

public class LoggerEventArgs : EventArgs
{
    public int Priority { get; }

    public LoggerEventArgs(int priority)
    {
        Priority = priority;
    }
}

public class Program
{
    public static void Main()
    {
        var logger = new LoggerDerived();
        logger.Log += (sender, loggerArgs) => Console.WriteLine($"Sender: {sender}, Priority: {loggerArgs.Priority}");
        logger.AddMsg(5);
    }
}
  • overrideがコメントアウトされている場合の出力
LoggerBase: AddMsg 5
Sender: LoggerDerived, Priority: 5
  • overrideがコメントアウトされておらず、有効な場合の出力
LoggerBase: AddMsg 5
コンパイラによってEventHandlerがnewで隠蔽されていることが分かる
  • 上記のコードのoverrideをコメントアウトせずに、有効にしたものをSharpLabに変換させる
  • LoggerDerivedで、private new EventHandler<LoggerEventArgs> m_Log;となっている。
public class LoggerBase
{
    [CompilerGenerated]
    private EventHandler<LoggerEventArgs> m_Log;

    public virtual event EventHandler<LoggerEventArgs> Log
    {
        [CompilerGenerated]
        add
        {
            EventHandler<LoggerEventArgs> eventHandler = this.Log;
......

public class LoggerDerived : LoggerBase
{
    [CompilerGenerated]
    private new EventHandler<LoggerEventArgs> m_Log;

    public override event EventHandler<LoggerEventArgs> Log
    {
        [CompilerGenerated]
        add
        {
            EventHandler<LoggerEventArgs> eventHandler = m_Log;
派生クラスでaddとremoveを用いればoverrideしてもイベントは呼ばれる
  • これはEventHanderがフィールドとして扱われている時
public class LoggerBase
{
    public virtual event EventHandler<LoggerEventArgs> Log;
    
    public void AddMsg(int priority)
    {
        Console.WriteLine($"LoggerBase: AddMsg {priority}");
        Log?.Invoke(this, new LoggerEventArgs(priority));
    }
}

public class LoggerDerived : LoggerBase
{
    public override event EventHandler<LoggerEventArgs> Log
    {
        add => base.Log += value;
        remove => base.Log -= value;
    }
}

出力

LoggerBase: AddMsg 5
Sender: LoggerDerived, Priority: 5
イベントをプロパティ的に扱う場合のadd / remove
using System.Runtime.CompilerServices;

public class LoggerBase
{
    protected EventHandler<LoggerEventArgs> log;
    public virtual event EventHandler<LoggerEventArgs> Log
    {
        [MethodImpl(MethodImplOptions.Synchronized)]
        add => log += value;
        [MethodImpl(MethodImplOptions.Synchronized)]
        remove => log -= value;
    }

    public void AddMsg(int priority)
    {
        Console.WriteLine($"LoggerBase: AddMsg {priority}"); 
        log?.Invoke(this, new LoggerEventArgs(priority));
    }
}

public class LoggerDerived : LoggerBase
{
    public override event EventHandler<LoggerEventArgs> Log
    {
        [MethodImpl(MethodImplOptions.Synchronized)]
        add => log += value;
        [MethodImpl(MethodImplOptions.Synchronized)]
        remove => log -= value;
    }
}

public class LoggerEventArgs : EventArgs
{
    public int Priority { get; }

    public LoggerEventArgs(int priority)
    {
        Priority = priority;
    }
}

public class Program
{
    public static void Main()
    {
        var logger = new LoggerDerived();
        logger.Log += (sender, loggerArgs) => Console.WriteLine($"Sender: {sender}, Priority: {loggerArgs.Priority}");
        logger.AddMsg(5);
    }
}

出力

LoggerBase: AddMsg 5
Sender: LoggerDerived, Priority: 5
  • override部分がまるまるコメントアウトでも同じ
public class LoggerDerived : LoggerBase
{
    // public override event EventHandler<LoggerEventArgs> Log
    // {
    //     [MethodImpl(MethodImplOptions.Synchronized)]
    //     add => log += value;
    //     [MethodImpl(MethodImplOptions.Synchronized)]
    //     remove => log -= value;
    // }
}

出力

LoggerBase: AddMsg 5
Sender: LoggerDerived, Priority: 5
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目22 明瞭で最小で完全なメソッドグループを作ろう

  • まず、同じ名前のメソッドで、シグネチャによって挙動が異なるものを複数作ってはならない。
  • 多義性が発生すると、コンパイラがメソッドを選ぶことになるが、開発者が意図したものになるかはわからない。よって多義性をなくすことは重要。
  • 多義性が発生しない1つの方法は、「多重定義を漏れなく作ること」である。
多重定義を漏れなく作った例
public class Program
{
    private void Scale(short scaleFactor) => Console.WriteLine("Scale(short)");
    private void Scale(int scaleFactor) => Console.WriteLine("Scale(int)");
    private void Scale(float scaleFactor) => Console.WriteLine("Scale(float)");
    private void Scale(double scaleFactor) => Console.WriteLine("Scale(double)");
    
    public static void Main()
    {
        Program program = new Program();
        
        short shortValue = 5;
        int intValue = 5;
        float floatValue = 5.0f;
        double doubleValue = 5.0;
        
        program.Scale(shortValue);
        program.Scale(intValue);
        program.Scale(floatValue);
        program.Scale(doubleValue);
    }
}

出力

Scale(short)
Scale(int)
Scale(float)
Scale(double)
  • もし、shortとintを引数にとる定義をコメントアウトすると、出力は以下のようになる
    • shortやintの場合にどのメソッドが呼び出されるかわかりにくくなる。
Scale(float)
Scale(float)
Scale(float)
Scale(double)
ジェネリックメソッドも含めた呼び出し優先順
public static class Utilities
{
    public static double Max(double left, double right)
    {
        Console.WriteLine("double");
        return Math.Max(left, right);
    }
    
    public static T Max<T>(T left, T right) where T : IComparable<T>
    {
        Console.WriteLine($"T: {typeof(T)}");
        return left.CompareTo(right) > 0 ? left : right;
    }
}

public class Program
{
    public static void Main()
    {
        Utilities.Max(1.0d, 2.0d); // double
        Utilities.Max(1.0, 2.0d); // double
        Utilities.Max(1.0, 2.0); // double
        Utilities.Max(1.0f, 2.0d); // double
        Utilities.Max(1, 2.0d); // double
        
        Utilities.Max(1.0f, 2.0f); // T: System.Single
        Utilities.Max(1, 2); // T: System.Int32
        Utilities.Max(1, 2.0f); // T: System.Single
    }
}
  • ジェネリックメソッドとのマッチでは、型の1つが完全に一致し、変換の必要がない。候補となりそうな他のメソッドがあっても、暗黙的な変換が必要ならば、そのまま適用できるジェネリックメソッドのほうがより適したメソッドとみなされる。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目23ではなく脱線

  • そもそもpartial classやpartial methodに普段馴染みがないので、今回はその基本機能に触れる程度に留める
  • partial classは、全てclass宣言の前にpartialをつける必要がある(元々partialとなることを想定していなかったクラスにあとからpartialを外付けすることはできない)
partialクラスについて
MyPartial.cs
public partial class MyPartial
{
    public string Name { get; set; }
    
    public void Show()
    {
        Console.WriteLine($"Show! Name: {Name}");
    }
}
MyPartial2.cs
public partial class MyPartial
{
    public void Greet()
    {
        Console.WriteLine($"Greet! Name: {Name}");
    }
}
Program.cs
public class Program
{
    public static void Main()
    {
        MyPartial myPartial = new MyPartial();
        myPartial.Name = "Taro";
        myPartial.Show();
        myPartial.Greet();
    }
}

出力

Show! Name: Taro
Greet! Name: Taro
partialメソッドについて
MyPartial.cs
public partial class MyPartial
{
    public string Name { get; set; }

    partial void Hello();

    public void CallHello()
    {
        Hello();
    }
}
MyPartial2.cs
public partial class MyPartial
{
    partial void Hello()
    {
        Console.WriteLine($"Hello! Name: {Name}");
    }
}
Program.cs
public class Program
{
    public static void Main()
    {
        MyPartial myPartial = new MyPartial();
        myPartial.Name = "Taro";
        myPartial.CallHello();
    }
}

出力

Hello! Name: Taro
  • MyPartial2の方のHello()の実装部分を全てコメントアウトしても、コンパイルエラーとならない。
    • メソッドの実態がなくてもOK
    • 出力は何もされない。

C# 9.0から追加された新しいpartial methodについて

  • 今までは、「自動生成されたコードが先にあり、そこに後付で任意の独自コードを人間が追加する」という目的だった。
  • 新しいpartial methodは、「手動コードを先に作っておき、後付で自動生成コードを追加する」となっている。
  • 仕様の相違
    • アクセス修飾子の指定が必須(従来のpartial methodとの区別をアクセス修飾子の有無でつけている)
    • 戻り値の型が自由
C#9.0から追加されたpartial method
MyPartilal.cs
public partial class MyPartial
{
    public string Name { get; set; }

    public partial void Hello();
    // もし相手側(実装側)が存在しないと以下のコンパイルエラーとなる。
    // Partial method 'void Hello()' must have an implementation part because it has accessibility modifiers
}
MyPartilal2.cs
public partial class MyPartial
{
    // こちら側の実装コードが、一般的にはC#によって自動生成されることが想定される。
    public partial void Hello()
    {
        Console.WriteLine($"Hello! Name: {Name}");
    }
    // もし相手側(宣言側)が存在しないと以下のコンパイルエラーとなる。
    // No defining declaration found for implementing declaration of partial method 'void Hello()'
}
Program.cs
public class Program
{
    public static void Main()
    {
        MyPartial myPartial = new MyPartial();
        myPartial.Name = "Taro";
        myPartial.Hello(); // publicメソッドも可能
    }
}

出力

Hello! Name: Taro
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目24 IClonableは設計の選択肢を狭めるので避けよう

  • ICloneableでは、浅いコピーと深いコピーの両方をサポートすることが求められている。
    • 浅いコピーとは、クラスの参照型フィールドは、元と同じオブジェクトを参照しても良いということ。
    • 深いコピーは、参照型のフィールドでも新しくオブジェクトを作る必要があるということ。
  • フィールドのクラスに対して数珠つなぎに、深いコピーまでサポートしているかを確認する必要があり、大きな負担を強いる。
  • structなどの値型は、普通に代入しただけでコピーが作成されるので、深いコピーの条件を満たす(structのフィールドに参照型がない場合)。ICloneableのCloneメソッドは戻り値がobjectであり、ボックス化が生じるため、Cloneを呼ぶほうがパフォーマンスは低下する。
  • ICloneableを実装すると、その派生クラスでも正しくICloneableを実装する必要がある。
    • そのクラスのメンバーフィールドは、値型かICloneableを実装する参照型に限定される
派生クラスでCloneメソッドを実装しなかった場合の例
  • DerivedTypeに対してCloneメソッドを実装していないため、Cloneが呼ばれた際に複製されるのはBaseTypeの方になる。
public class BaseType : ICloneable
{
    public string name;
    
    public object Clone()
    {
        var copy = new BaseType();
        copy.name = name;
        return copy;
    }
}

public class DerivedType : BaseType
{
    public int age;
}

public class Program
{
    public static void Main()
    {
        var baseOriginal = new BaseType();
        baseOriginal.name = "Taro";
        var baseCopy = baseOriginal.Clone() as BaseType;
        Console.WriteLine($"baseCopy.name: {baseCopy.name}");
        
        var derivedOriginal = new DerivedType();
        derivedOriginal.name = "Jiro";
        derivedOriginal.age = 20;
        BaseType baseTypeDerivedCopy = derivedOriginal.Clone() as BaseType;
        Console.WriteLine($"derivedCopy.name: {baseTypeDerivedCopy.name}");
        
        DerivedType derivedCopy = derivedOriginal.Clone() as DerivedType;
        Console.WriteLine($"derivedCopy.name: {derivedCopy.name}");
    }
}

出力

baseCopy.name: Taro
derivedCopy.name: Jiro
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目25 配列をパラメータとして使うのはparams配列だけにしよう

  • 配列は共変性を持つため、引数に渡す時には基底クラス型に対しても入れることが出来る(コンパイルエラーを通る)
配列の共変性によってランタイムエラーが発生する例
public class Program
{
    public static void Main()
    {
        var array = new string[] { "a", "b", "c" };
        Replaces(array);
    }

    private static void Replaces(object[] param)
    {
        for (int index = 0; index < param.Length; index++)
        {
            param[index] = index;
        }
    }
}

出力

Unhandled exception. System.ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array.
  • Riderも警告を出してくれる
  • これはListを使う場合には不可能
public class Program
{
    public static void Main()
    {
        var array = new List<String> { "a", "b", "c" };
        Replaces(array);  // コンパイルエラー
        // Argument type 'System.Collections.Generic.List<string>' is not assignable to parameter type 'System.Collections.Generic.IList<object>'
    }

    private static void Replaces(IList<object> param)
    {
        for (int index = 0; index < param.Count(); index++)
        {
            param[index] = index;
        }
    }
}
共変性のそのほかの例
public class Animal
{
    public void Eat() => Console.WriteLine("Animal Eating.");
}

public class Dog : Animal
{
    public void Bark() => Console.WriteLine("Dog Barking.");
}

public interface IAnimalFactory<out T> where T : Animal
{
    T CreateAnimal();
}

public class DogFactory : IAnimalFactory<Dog>
{
    public Dog CreateAnimal() => new Dog();
}

public class Program
{
    public static void Main()
    {
        DogFactory dogFactory = new DogFactory();
        Dog dog = dogFactory.CreateAnimal();
        dog.Eat();
        dog.Bark();

        // もしoutがない場合、コンパイルエラー Cannot convert initializer type 'DogFactory' to target type 'IAnimalFactory<Animal>'
        IAnimalFactory<Animal> animalFactory = new DogFactory();
        Animal animal = animalFactory.CreateAnimal();
        animal.Eat();
    }
}
共変性が問題になるケースその2
  • 以下は問題なく動作する例
public class Base
{
    public static Base Factory => new Base();
    public virtual void WriteType() => Console.WriteLine("I am Base");
}

public class Derived1 : Base
{
    public new static Derived1 Factory => new Derived1();
    public override void WriteType() => Console.WriteLine("I am Derived1");
}

public class Derived2 : Base
{
    public new static Derived2 Factory => new Derived2();
    public override void WriteType() => Console.WriteLine("I am Derived2");
}

public class Program
{
    public static void Main()
    {
        Base[] storage = new Base[3];
        FillArray(storage, () => Base.Factory);
        storage[1].WriteType();
        FillArray(storage, () => Derived1.Factory);
        storage[1].WriteType();
        FillArray(storage, () => Derived2.Factory);
        storage[1].WriteType();
    }

    private static void FillArray(Base[] array, Func<Base> generator)
    {
        for (int i = 0; i < array.Length; i++)
        {
            array[i] = generator();
        }
    }
}

出力

I am Base
I am Derived1
I am Derived2
  • もし以下のように変わると、ランタイムでエラーとなる
public class Program
{
    public static void Main()
    {
        Base[] storage = new Derived1[3];
配列が反変性をサポートしないので、意図したコードがコンパイルエラーとなる例
  • まずはコンパイルエラーに通る例
- Base, Derived1, Derived2の定義は上の例と同じ
public class Program
{
    public static void Main()
    {
        Derived1[] array = new Derived1[3];
        FillArray(array);
        foreach (var item in array)
        {
            item.WriteType();
        }
    }

    private static void FillArray(Derived1[] array)
    {
        for (int i = 0; i < array.Length; i++)
        {
            array[i] = new Derived1();
        }
    }
}

出力

I am Derived1
I am Derived1
I am Derived1
  • コンパイルエラーになる例
    • Base[]の中に、Derivedオブジェクトを入れることはできるはずだが、コンパイルは通らない
public class Program
{
    public static void Main()
    {
        Base[] array = new Derived1[3];
        FillArray(array); // Argument type 'Base[]' is not assignable to parameter type 'Derived1[]'
  • 引数の中身を書き換えないのであれば、IEnumerable<T>を利用するのがよい。

params配列を使うケース

  • メソッドに任意の数の引数を渡す方法の中には、params配列を使う方法もある。
    • 引数無しでメソッドを呼び出すことができる。
  • 引数の共変性の問題は通常の配列を用いる場合と変わらない。だがそのようなケースに直面する機会は少なくなる。
    • 渡された配列のためのストレージはコンパイラ自身が作成する。また、その作成されたストレージをコンパイラ自ら変更することは意味のないことである。
params配列を使って引数を渡すケース
public class Program
{
    public static void Main()
    {
        var array = new string[] { "a", "b", "c" };
        WriteContents1(array);
        // WriteContents1(); 引数0はコンパイルエラー
        WriteContents2("a", "b", "c");
        WriteContents2();
    }

    private static void WriteContents1(string[] array)
    {
        Console.WriteLine("WriteContents1");
        foreach (var item in array)
        {
            Console.WriteLine(item);
        }
    }

    private static void WriteContents2(params string[] array)
    {
        Console.WriteLine("WriteContents2");
        foreach (var item in array)
        {
            Console.WriteLine(item);
        }
    }
}

出力

WriteContents1
a
b
c
WriteContents2
a
b
c
WriteContents2
  • 今までの配列で起きていた問題が起きにくいことを感じられる例
public class Program
{
    public static void Main()
    {
        var result = WriteContents2("a", "b", "c", 1, 3);
        foreach (var item in result)
        {
            Console.WriteLine(item);
        }
    }

    private static string[] WriteContents2(params object[] array)
    {
        string[] result = new string[array.Length];
        for (int i = 0; i < array.Length; i++)
        {
            // result[i] = array[i]; // Cannot convert source type 'object' to target type 'string'
            result[i] = array[i].ToString();
        }
        return result;
    }
}

出力

a
b
c
1
3
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目26 イテレータや非同期メソッドのエラーは、ローカル関数で即座に報告できる

  • イテレータや非同期メソッドは、実行が遅くなるためエラーの報告が遅れる。
  • メソッドの中でイテレータや非同期部分と、引数の検証などの前段階の部分を分離することで、即時にエラー通知を受け取ることができる。
イテレータメソッドでエラーの発生が後ろになる例
public class Program
{
    public IEnumerable<T> GetItems<T>(IEnumerable<T> sequence, int frequency)
    {
        if (sequence == null) throw new ArgumentNullException("sequence is null");
        if (frequency <= 0) throw new ArgumentOutOfRangeException("frequency <= 0");

        int index = 0;
        foreach (T item in sequence)
        {
            if (index % frequency == 0)
            {
                yield return item;
            }
            index++;
        }
    }

    public static void Main()
    {
        var program = new Program();
        IEnumerable<int> sequence = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        IEnumerable<int> items = program.GetItems(sequence, -1);
        Console.WriteLine("program.GetItemsの1行あと");
        foreach (int item in items)
        {
            Console.WriteLine(item);
        }
    }
}

出力

program.GetItemsの1行あと
Unhandled exception. System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'frequency <= 0')
イテレータ部分とラッパー部分に分割することでエラー報告を早める例
  • 例は上記と同じで、以下のように変更する
  • イテレータメソッド部分は、本来必要な引数のチェックを含まないので、そのメソッド単体でのアクセスはとても小さくしたい。ローカル関数とすることでアクセスを最小にすることが出来る。
  • 概念の復習
    • イテレータメソッド...yield return文を使って、シーケンスを順番に列挙処理へと返していくメソッド。戻り値の型は、IEnumerable<T>またはIEnumerableである。
public class Program
{
    public IEnumerable<T> GetItems<T>(IEnumerable<T> sequence, int frequency)
    {
        if (sequence == null) throw new ArgumentNullException("sequence is null");
        if (frequency <= 0) throw new ArgumentOutOfRangeException("frequency <= 0");

        return GetItemsImpl();

        IEnumerable<T> GetItemsImpl()
        {
            int index = 0;
            foreach (T item in sequence)
            {
                if (index % frequency == 0)
                {
                    yield return item;
                }
                index++;
            }
        }
    }

    public static void Main()
    {
        var program = new Program();
        IEnumerable<int> sequence = new List<int> { 1, 2, 3, 4, 5, 6};
        IEnumerable<int> items = program.GetItems(sequence, -1);
        Console.WriteLine("program.GetItemsの1行あと");
        foreach (int item in items)
        {
            Console.WriteLine(item);
        }
    }
}

出力

Unhandled exception. System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'frequency <= 0')

実行後すぐにエラーとなる。foreachの中での呼び出しを待つ必要がない。

このスクラップは25日前にクローズされました