この記事について
こちらは完全に個人の勉強用メモです。
Effective C# 6.0/7.0 第1章を読んだのでメモになります。
著者について
Bill Wagnerは、もっとも重要なC#開発者の1人であり、
ECMA C# Standards Committee のメンバーである。
彼は Humanitarian Toolbox の President であり、
Mixrosoft Regional Director の役職を持ち、.NET MVPを11年受賞し、
最近では .NET Foundation Advisory Council に任命されている。
Bill は新規事業から大企業まで数億の会社で働き、
ソフトウェア開発のプロセスを改善し、
それらのソフトウェア開発チームを成長させてきた。
現在は Microsoft の .NET の Core content チームに勤め、
C# 言語と .NET Core に関心のある開発者のために
学習用マテリアルを作っている。
Bill はイリノイ大学アーバナ・シャンペーン校で
コンピュータサイエンスの学士号を授かっている。
※Effective C# 6.0/7.0 帯より
※日本語版の初版:2018年09月05日
項目1:ローカル変数の型をなるべく暗黙的に指定すること
varの利点
- ローカル変数の型を宣言する場合は、varを推奨
- varは型をコンパイラに伝えないが、代わりにコンパイラが型を宣言する
- 開発者がコードに注力しやすくなり理解しやすくなる
- 型チェック違反はコンパイラが警告してくれる
- 何かしらのキャストをしない限り、コンパイラの決定は覆らない
- DBからの値の型やクエリを厳格にしてしまうと、情報が失われてしまうケースがある
varを使わないケース
- ただし、以下のケースはvarを使わない
- メソッドの返り値を保持する変数に対してvarを使用すると混乱する
- varはコンパイラが型を決定するため、開発者がコードを読んだ場合と、コンパイラが決定した場合で型が異なるという状況が起こる
- 組み込みの数値型ローカル変数に対しての使用
- 計算の結果によって、Intやfloatになってしまい値の正確性に問題が生じる
- 変数名から情報を読み取れない
- メソッドの返り値を保持する変数に対してvarを使用すると混乱する
項目2:constよりもreadonlyを使用すること
「コンパイル時定数」と「実行時定数」という2つの定数
- C#には「コンパイル時定数」と「実行時定数」という2つの定数がある
- それぞれ全く異なる挙動をする
- 「実行時定数」をなるべく使用する
「コンパイル時定数(const)」の特徴
- const で宣言
- メソッド内でも宣言可能
- const が使えるのは「整数型、浮動小数型、列挙型、文字列型、null」だけ
- 「実行時定数」と比べると
- 実行速度的に若干有利
- 柔軟性では劣る
- 使用される場面
- パフォーマンスの要求かつ、定数値が将来のリリースにわたって変更されない場合のみ
「実行時定数(readonly)」の特徴
- readonly で宣言
- メソッド内での宣言は不可
- readonly にできるのは「全ての型」
「コンパイル時定数」と「実行時定数」のもっとも大きな違い
- コンパイル時定数
- オブジェクトコード内の定数値に置き換えられる
- 実行時定数
- readonly 変数を参照するだけなので、値を直接参照しない
- つまり
- アセンブリを置き換えても安全なのは「実行時定数(readonly)」
- const は「コンパイル時に値が利用可能 なければならないケース
- すなわり、「属性の引数 / switch caseのラベル / enumの定義 / リリースをまたいでも普遍な体を定義する」
- 基本は readonly
項目3:キャストにはisまたはasを使用すること
as演算子とis演算子について
- キャスト演算子「()演算子」は情報が失われることがあるので、むやみに使用しない
- as演算子が使える場合は、「常にasを使用する」
- 2つとも実行時の型が要求された方と一致する場合のみ成功する
- as演算子は、変換できないときに nullを返す
- そのため、asは返り値がnullかどうかの確認で良いが、キャストの場合にはnullの確認と例外処理が必要になり冗長
MyType t = o as Mytype;
if (t != null)
{
// MyType型の変数 t を使った処理
}
else
{
// 例外時の処理
}
項目4:string.Format()は補間文字列に置き換える
- 文字列の中に変数を埋め込める機能
- 以下のように展開できる
Console.WriteLine($"顧客名は{c?.name ?? "名前が見つかりません"}");
Console.WriteLine($@"顧客名は
{c?.name ?? "名前が見つかりません"}"); // 改行パターン
項目5:カルチャ固有文字列よりもFormattableStringを使用すること
- コードが実行されるマシンに設定されているカルチャにもどついて文字列がフォーマットされる
- 特定のカルチャが必要である場合、文字列補完を明示的にFormattableStringとして作成してから、特定の活茶を使用して文字列に変換するだけで済む
FormattableString today = $"Today is {DateTime.Now.Month}/{DateTime.Now.Day}";
public static string ToGerman(FormattableString src)
{
return string.Format(
System.Globalization.CultureInfo.
CreateSpecificCulture("de-de"),
src.Format,
src.GetArguments());
}
public static string ToFrenchCanada(FormattableString src)
{
return string.Format(
System.Globalization.CultureInfo.
CreateSpecificCulture("fr-CA"),
src.Format,
src.GetArguments());
}
「FormattableStringクラス」と「補間文字列」
https://docs.microsoft.com/ja-jp/dotnet/api/system.formattablestring?view=net-6.0
https://qiita.com/yaegaki/items/6e75f957a7a6c23ee22e
- C#独自の要素として補間文字列を使用した場合、FormattableStringとして変数に代入できる。
// 左辺をFormattableStringにできる
FormattableString s = $"";
// 文字列補間を使用しない場合は代入できない
// 型 'string' を 'System.FormattableString' に暗黙的に変換できない
FormattableString s = "";
// varを使用した場合はstringになる
var s = $"";
項目6:文字列指定のAPIを使用しないこと
- 名前空間/型/メソッド/プロパティ/変数などを文字列リテラルとして記述するとタイプミスや元のコードが変更されても検知できない
- 型の安全性を保つために、nameof演算子を使うこと
項目7:デリゲートを使用してコールバックを表現する
- デリゲートはタイプセーフなコールバックを定義できる
- C#の構文ではデリゲートをラムダ式として表現できるようになっている
- 全てのLINGはデリゲートが基本になっている
- 全てのデリゲートはマルチキャスト
項目8:イベントの呼び出し時にnull条件演算子を使用すること
- イベントにハンドラが登録されていない場合などレースコンディションが起こる場合がある
- C#6.0 から導入されたnull条件演算子を使うとこれらに対応したコードを簡潔に記述できる
- 以下の古いコードは問題がある
- Updatedイベントにアタッチされたイベントハンドラがない状態で実行されると、NullReferenceException例外がスローされる
- その場合の返り値はnull
- 従って、イベントハンドラがnullかどうかチェックが必要
public class EventSource
{
private EventHandler<int> Updated;
public void RaiseUpdates()
{
counter++;
Updated(this, counter);
}
private int counter;
}
- 単純に
if (Updated != null)
だと呼び出しと登録解除のタイミングで例外になるケースが稀にある - null条件演算子("?.")を使うとコードを単純化かつ、安全にできる
public void RaiseUpdates()
{
counter++;
Updated?.Invoke(this, counter);
}
- C#では、null条件演算子の後に
()
をつけられないため、Invokeメソッドの呼び出しが必要 - いついかなる場合でも、この手法を採用すべき
項目9:ボックス化およびボックス化解除を最小限に抑える
- 値型はデータを保持するコンテナであり、多様性を持たない型
- .NET Framework は System.Object という全てのオブジェクトの親である参照型を定義している
- 目的が相反しているため、2つのギャップを埋めるために、「ボックス化」と「ボックス化解除」という機能が用意されている
- 常にパフォーマンスを落とす操作である
- オブジェクトの一時的なコピーを生成するため、場合によっては潜在的なバグを引き起こす
- 可能な限りボックス化とボックス化解除は発生しないよう注意すべき
ボックス化とは
- 値型を不定な参照型オブジェクトのメンバとすることによって、参照型であることが必要な場面においても値型を使用できるようにする仕組み
ボックス化解除とは
- ボックス化された値型のコピーを取り出すこと
ジェネリック型である程度、ボックス化とボックス化解除を回避できる
- しかし、>NET Framework には System.Object を引数にとるような場面がある
- これは自動的に起こること
ボックス化とボックス化解除が発生するケース
- コンパイラは System.Object のような参照型が必要な場所で値型が使用されていると、ボックス化とボックス化解除の命令を生成する
- 補間文字列も System.Object への参照の配列を使用して作成されるため、ボックス化の処理が発生する
Console.WriteLine(
$"いくつかの数値: {firstNumber}, {secoundNumber}, {thirdNumber}");
上記の例は、数値は値型であって、値から文字列を作成するためにコンパイラが生成したメソッドを渡すことができるようにするためにボックス化が必要になる
Console.WriteLine(
$@"いくつかの数値: {firstNumber.ToString()},
{secoundNumber.ToString()}, {thirdNumber.ToString()}");
修正版については、整数の既知の型使用しているため、値型(整数)が暗黙的に System.Object へと変換されることはない
項目10:親クラスの変更に応じる場合のみnew修飾子を使用すること
- メソッドにnewを使用すべき場面が1つだけある
- 派生クラスですでに使用済みのメソッド名が、新しいバージョンの親クラスに定義されたメンバと競合した場合にnew修飾子を指定する
- 外部ライブラリ中で定義されている別ライブラリを作成した時に、同名のメソッドがあった場合など
文献