初期化子とexpression-bodiedプロパティを書き間違えてハマった話

C#

こんにちは、働くC#プログラマーのさんさめです。

expression-bodiedなプロパティ
というものをご存じでしょうか。

言葉は知らなくとも、
以下のような書き方です、と提示すれば
「ああ、これのことね」
となるかもしれません。

public class Pereson
{
    public string FirstName { get; set; }
    public string FamilyName { get; set; }

    // expression-bodiedなプロパティの例
    public string FullName => $"{FamilyName} {FirstName}";
    // 普通に宣言しようとするとこうなる
    //public string FullName
    //{
    //    get
    //    {
    //        return $"{FamilyName} {FirstName}";
    //    }
    // }
}

このように、プロパティ(関数)の本体が式1つで済むときに
「=>」を使って、
簡略化できる記法です。

複数の「{}」や「get」,「return」を省略できるので、
非常に便利に使っています。

ところが、本日わたくしさんさめは、
普通の初期化子と、このexpression-bodiedなプロパティを
書き間違えてしまい、原因の分かりにくいバグに苦しめられました。

備忘として書き残しておきます。

スポンサーリンク

初期化子とexpression-bodiedプロパティを書き間違えた

さて、さっそく本題ですが、
本来ただの初期化子として書くべきところを、
さんさめは以下のように書いてしまいました。

// 本当はこう書きたかった
//private Dictionary<string, string> MyDic
//    = new Dictionary<string, string>();
// こう書いてしまった
private Dictionary<string, string> MyDic
    => new Dictionary<string, string>();

そんなつもりのない箇所に、
つい手癖で「=>」を書いてしまったのですね。

さて、これによって何が起きたのでしょうか。

上記の変数を使用するサンプルコードを書いてみます。

class Program
{
    private Dictionary<string, string> MyDic
         => new Dictionary<string, string>();

    static void Main(string[] args)
    {
        var a = new Program();

        a.MyDic["hoge"] = "foo";
        if (a.MyDic.ContainsKey("hoge"))
        {
            // すぐ上で辞書に追加したはずなのにここに来ない!
            Console.WriteLine(a.MyDic["hoge"]);
        }
    }
}

辞書に要素を追加してすぐに、
ContainsKeyでキーがあるかチェックしているのですが、
この条件には絶対にヒットしません

一体何が起きているのでしょうか?

プロパティにアクセスするたびに新規インスタンスを返していた

冒頭で申し上げましたように、
expression-bodiedプロパティは、
内部の「処理が式1つ」の時に簡略化するための記法です。

つまり、初期化の時に一回だけnewが呼ばれるつもりが、
なんとプロパティにアクセスするたびに
新しくDictionaryインスタンスをnewして返す処理をする、
というプロパティ定義になってしまっています。

つまり、先ほどのサンプルコードの処理を
詳しく補足すると以下のようになります。

// ここで代入したMyDicと…
a.MyDic["hoge"] = "foo";
// ここでキーがあるかチェックしているMyDicの実体は別物!!
if (a.MyDic.ContainsKey("hoge"))
{
    // 仮にこの処理に入ったとしても、
    // ここでアクセスしているMyDicも実体は別物!!!
    Console.WriteLine(a.MyDic["hoge"]);
}

まったく参照を保持していないため、
アクセスした端からインスタンスを捨てていたのでした。

さんさめ
さんさめ

これはアカン…

さんさめは一体どう書けば、
この分かりにくいバグを未然に防げたのでしょうか?

readonly修飾子かget onlyの明示指定をすべき

今回のような使い方では、
初期化したあと再代入するつもりはありませんでした。

なので、きちんと「readonly」修飾子を付ける
というのが1つ対策になりえます。

readonly 修飾子と、
expression-bodiedに使われる「=>」は
併用しようとするとコンパイルエラーになり、
打ち間違いに気づけます。

もしくは、少し冗長になりますが、
get onlyであることを明示的に指定する方法もあります。

こういうことです

この場合も、「=>」併用しようとすると
コンパイルエラーになります。

やや不安が残る。現象に遭遇したときにすぐ気づけるか?

しかしどちらも「ちゃんと書くように気を付ける」
になっていて、
実際にミスしてしまったときに気づけるかは
これを覚えているか次第になってしまいます…。

expression-bodiedなプロパティを使うと、
こういうことが起きうる、
というのは覚えておいた方が良さそうです。

まとめ

まとめです。

  • 初期化子とexpression-bodiedなプロパティは、
    記述がよく似ている
  • しかし処理内容は全く別物になる可能性がある
  • 初期化でnewをしている場合は書き間違えると大惨事
  • 今回の件はreadonly修飾子を使えば未然に防げた(かも)

最後までお読みいただき、ありがとうございました。

コメント

タイトルとURLをコピーしました