trsing’s diary

勉強、読んだ本、仕事で調べたこととかのメモ。

EFFECTIVE C# 6.0/7.0 読書メモ 項目6~項目10

項目6 文字列指定のAPIを使用しないこと

文字列ベースのAPIやライブラリのデメリット
・型の安全性が損なわれる
・ツールによるサポートもなくなる
・静的型付け言語における多数の利点を失う

シンボル名が必要な時はnameof()式を利用するとよい。メリットは、
・シンボル名の変更が反映される。
・静的解析ツールを使用すれば、引数の名前が間違った位置で使用されている場合に、間違いや不整合を検出できる。

nameof演算子は型や変数、インターフェイス名前空間に対して機能する。修飾されていない名前、完全修飾名のいずれであっても機能し、常にローカル名を返す。これにより一貫性が保たれている。

> nameof(System.Int32.MaxValue)
"MaxValue"
> nameof(Int32.MaxValue)
"MaxValue"

ジェネリック型定義に対しては、クローズジェネリック型(型パラメータが指定されている)に対してのみ機能する。

> class MyClassG<T> { }
> nameof(MyClassG)//オープン
(1,8): error CS0305: ジェネリック 種類 'MyClassG<T>' を使用するには、1 型引数が必要です。
> nameof(MyClassG<int>)//クローズ
"MyClassG"

7 デリゲートを使用してコールバックを表現する

デリゲートを用いてコールバックを実装するメリット
・クライアント側で必要となる条件が比較的単純
・デリゲートの対象は実行時に決定できる
・複数のクライアントを対象としたデリゲートを用意することができる

デリゲートはメソッドへの参照を持ったオブジェクト(Cの関数ポインタみたいなもの)。一つのデリゲートに複数のメソッドを登録することができ(マルチキャストデリゲート)、すべてのメソッドが一回の呼び出しに集約される。

注意点
・例外に対して安全ではない
デリゲートは例外をキャッチしないため実行中のメソッドが例外をスローした場合、その時点で終了する(残りのメソッドは実行されない)。

・返り値は最後に実行されたメソッドの返り値になる
デリゲートに複数登録した場合、最後に登録されたメソッドの返り値になる。
各メソッドの返り値を確認したい場合、各メソッドを明示的に呼び出すコードを使用する。

foreach(Func<bool>pr in cp.GetInvocationList())//デリゲートに登録されたメソッドのリストから各メソッドを取得
    bContinue &= pr();//各メソッドの返り値を確認

項目8 イベントの呼び出し時にnull条件演算子を用意すること

null条件演算子を使用するとイベントの呼び出しがスレッドセーフで簡潔になる。

イベントを呼び出す方法
1.そのままつかう

private EventHandler<int> Updated;
Updated(this);

Updatedイベントにアタッチされたイベントハンドラがない状態(null)ではNullReferenceException例外がスローされる

2.nullチェックする

if(Updated != null)
    Updated(this);

nullチェックとイベント呼び出しとの間に別のスレッドが実行され、Updatedが変更される可能性がある。登録解除された場合ハンドラはnullになるのでNullRefereneException例外がスローされる(見つけづらい上に修正しづらいバグ)。

3.ハンドラをローカル変数に割り当て、割り当てたローカル変数に対してnullチェックし、実行する。

var handler = Updated;
if(handler != null)
    handler(this);

スレッドセーフ (ローカル変数にはUpdatedが参照しているすべての元のハンドラを参照するマルチキャストデリゲートが格納される。別スレッドによりイベントからハンドラが登録解除されても、ローカル変数には影響がない)。 しかし可読性が悪く、書く量も多い。

4.null条件演算子を使用する。

Updated?.Invoke(this);

正解。スレッドセーフで簡潔。
※?.演算子の直後に括弧を続ける事ができないのでInvokeメソッドを呼び出す必要がある

項目9 ボックス化およびボックス化解除を最小限に抑える

ボックス化とボックス化解除は望んでいない場所でコピーを作成することがあり、それにより不具合が起こることがある。 ボックス化、ボックス化解除ではヒープの確保やコピーが生じるためパフォーマンスの低下を招く。

ボックス化、ボックス化解除

.NET FrameworkはSystem.Objectという、すべてのオブジェクトの親である参照型を定義している
・値型はデータを保持するコンテナであり、多態性を持たない型
ボックス化、ボックス化解除はこのギャップを埋めるために用意された機能。

ボックス化
値型を参照型に変換する。ボックスをヒープ上に確保し、値型のコピーを格納する。

ボックス化解除
ボックス化された値型のコピーを作成して返す。ボックスの中身にアクセスする際は毎回コピーされた新しい値が返される

注意点
System.Objectを引数に取る場合はボックス化、ボックス化解除が発生する。
補完文字列はSystem.Objectへの参照の配列を使用して作成される。そのため、値型を使うとボックス化が生じる。
また、ボックス内のオブジェクトに対してToString()を呼び出してボックス内の値にアクセスするため、アンボックス化が起こる。 補完文字列を繰り返し使用する場合などはパフォーマンスに注意すること。これを回避するにはToStringであらかじめ文字列インスタンスへ変換しておくとよい。

不具合の例
.NET 1.xのコレクションに格納する場合、System.Objectインスタンスへの参照が保持される。つまり値型をコレクションに追加すると、ボックス化が行われる。このコレクションからオブジェクトを取り出すとコピーが作成される。このコピーに変更を行ってもコレクション内のオブジェクトに影響はない。

項目10 親クラスの変更に応じる場合のみnew修飾子を使用すること

new修飾子を使用して非virtualメソッドを再定義すると型の挙動が曖昧になる。

非virtualメソッドは静的に結び付けられている(コンパイル時の型により規定される)。
実行時の型が派生クラスでも、型が元クラスであれば元クラスのものが実行され、型が派生クラスなら派生クラスのものが実行される。

> public class MyClass { public void MagicMethod() { Console.WriteLine("MyClass"); } }
> public class MyOtherClass : MyClass { public new void MagicMethod() { Console.WriteLine("MyOtherClass"); } }
> object c = new MyOtherClass() as object;
> ((MyClass)c).MagicMethod();
MyClass
> ((MyOtherClass)c).MagicMethod();
MyOtherClass

メソッドにnewを使用すべき場面
派生クラスですでに使用済みのメソッド名が新しいバージョンの親クラスに定義されたメンバと競合した場合。
この場合の解決策としては、
・派生クラスのメソッド名を変更する。
・new修飾子を使用して再定義する。
メソッドを使用するすべてのコードを変更できるなら前者のほうが良い(長期運用に耐えられるだろう)。 そうでないならnew修飾子を使用する(一時しのぎだが)。

基本的にnew修飾子は使用すべきでない。使う場合も十分検討すること。