こんにちは、働く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修飾子を使えば未然に防げた(かも)
最後までお読みいただき、ありがとうございました。
コメント