Closed9

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

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

項目1 ローカル変数の型をなるべく暗黙的に指示すること

  • 人間が変数の型を指定するよりも、コンパイラが指定したとき(varを利用することで実現)の方が適切な場合がある。
    • ex: 本来IQueryable<string>とするべきである変数の型が、人間が不適切にIEnumerable<string>と決めてしまうことでパフォーマンスを下げる。
  • 変数の型は、変数名や、その変数に代入する際のメソッド名から大体分かるでしょ?
  • 数値型(int, float, double)についてはvarを利用せずに型を明記する
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目2 constよりもreadonlyを使用すること

  • readonlyは実行時定数であるため、柔軟性がある(別アセンブリでコンパイル済みのものも、readonly定数であれば変更が反映される)
    • constはコンパイル時に決定されて定数自体でILが置き換わる。
  • constの方がパフォーマンスは高いがその差はわずかでしかなく、柔軟性の方が重要である場合の方が多い。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目3 キャストにはisまたはasを使用すること

  • as
    • 変換できない場合はnullとなる。=> nullチェックだけすればOK。
  • キャスト
    • 変換できない場合は例外を出す。=> nullチェック以外に例外のキャッチも必要。
public class Program
{
    public static void Main()
    {
        // object o = new MyType(); // 出力: "mt is MyType", "mt2 is MyType"
        // object o = null; // 出力: "mt is not MyType, "mt2 is not MyType"
        object o = new SecondType(); // 出力: "mt is not MyType", "cast failed"

        
        // asを利用
        MyType mt = o as MyType;
        if (mt != null)
        {
            Console.WriteLine("mt is MyType");
        }
        else
        {
            Console.WriteLine("mt is not MyType");
        }

        // キャストを利用
        try
        {
            MyType mt2;
            mt2 = (MyType)o;
            if (mt2 != null)
            {
                Console.WriteLine("mt2 is MyType");
            }
            else
            {
                Console.WriteLine("mt2 is not MyType");
            }
        }
        catch (InvalidCastException)
        {
            Console.WriteLine("cast failed");
        }
    }
}

public class MyType {}
public class SecondType {}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目4 string.Format()を補間文字列に置き換える

  • string.Format()は、コンパイラが置換される引数の個数と、実際に指定された引数の数が一致するか検証しない。よって以下のコードはは実行時エラー。
  • 補間文字列とは、$"nameは{name}"の記法
    public static void Main()
    {
        var sample = "Sample";
        Console.WriteLine(String.Format("This is {0}! {1}", sample));
        // 実行時エラー: System.FormatException: Index (zero based) must be greater than or equal to zero and less than the size of the argument list.
    }
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目5 カルチャ固有の文字列よりもFormattableStringを使用すること

  • FormattableString型であることで、国ごとの表記の違いなどに柔軟に対応しやすい
  • 普通に利用する限り同じものが出力される模様(マシンに設定されているカルチャ)。FormattableStringだとその後変換できる、ということかな。
    public static void Main()
    {
        string first = $"今日は{DateTime.Now.Month}{DateTime.Now.Day}日です。";
        Console.WriteLine(first); // 今日は9月10日です。

        FormattableString second = $"今日は{DateTime.Now.Month}{DateTime.Now.Day}日です。";
        Console.WriteLine(second); // 今日は9月10日です。
        
        var third = $"今日は{DateTime.Now.Month}{DateTime.Now.Day}日です。"; // これはstring型になる
        Console.WriteLine(third); // 今日は9月10日です。
    }
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

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

  • 文字列で指定すると型の安全性が損なわれ、ツールのサポートもなくなる。
  • nameof()を用いよう
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

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

  • EventHandlerをnullチェックしたあとに、別のスレッドが割り込んでそのイベントハンドラが登録解除されることがある。
  • ?.を用いるとその問題が発生しないらしい
    • "?."演算子の左辺が厳密に一度しか評価されない

// スレッドセーフではない書き方
if (eventHandler != null)
{
    eventHandler();
}

// スレッドセーフな書き方
eventHandler?.Invoke();
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

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

  • ボックス化とは?が分かりやすく書かれている記事
  • 例えば、補間文字列では変数はSystem.Objectとして扱われるため、そのままint型の変数を渡すとボックス化されてしまう。
  • num.ToString()とすることで、Int32の構造体でoverrideされたToStringメソッドが呼ばれるため、ボックス化されずに、値型のままで処理することができる
int num = 5;
Console.WriteLine($"番号は{num}"); // ボックス化される
Console.WriteLine($"番号は{num.ToString()}"); // ボックス化されない
  • classとstructで挙動が違うことによって発生するバグ
    • コレクションの中に値型を格納すると、ボックス化される
public struct Person // classでも実験する
{
    public string Name { get; set; }
    public override string ToString()
    {
        return Name;
    }
}

public class Program
{
    public static void Main()
    {
        var persons = new List<Person>();
        Person p = new Person { Name = "Old Name" };
        persons.Add(p);
        // structの場合、ここでコピーが作成される(1回目)
        
        Person p2 = persons[0];
        p2.Name = "New Name";
        // structの場合、ここでコピーが作成される(2回目)
        
        Console.WriteLine(persons[0].ToString());
        // structの場合、ここでコピーが作成される(3回目)
        // 出力: Personがstructの時はOld Name, classの時はNew Name
    }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

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

  • newは一切使わない、くらいに覚えてよいと思う(個人的感想)
  • newでは、オブジェクトが同じでも、変数の型が違うと別の挙動となる
public class ParentClass
{
    public virtual void VirtualMethod()
    {
        Console.WriteLine("ParentClass - VirtualMethod");
    }
    
    public void NewedMethod()
    {
        Console.WriteLine("ParentClass - NewedMethod");
    }
}

public class ChildClass : ParentClass
{
    public override void VirtualMethod()
    {
        Console.WriteLine("ChildClass - VirtualMethod");
    }

    public new void NewedMethod()
    {
        Console.WriteLine("ChildClass - NewedMethod");
    }
}

public class Program
{
    public static void Main()
    {
        object c = new ChildClass();
        
        ParentClass parent = c as ParentClass;
        ChildClass child = c as ChildClass;
        
        Console.WriteLine("--- VirtualMethod ---");
        parent.VirtualMethod();
        child.VirtualMethod();

        Console.WriteLine("--- NewedMethod ---");
        parent.NewedMethod();
        child.NewedMethod();
    }
}

出力

--- VirtualMethod ---
ChildClass - VirtualMethod
ChildClass - VirtualMethod
--- NewedMethod ---
ParentClass - NewedMethod
ChildClass - NewedMethod
  • あるライブラリを拡張して独自メソッドを作っていたら、そのライブラリの新バージョンで、自分が作っていた独自メソッドと同じ名前を追加されてしまった!のような時だけ、場合によってはnewの使用を検討することがある、くらい。
このスクラップは2023/09/10にクローズされました