c#で値の未設定/デフォルト値を見分ける方法
概要
何も値が設定されていない時は、明示的に null を設定した時と挙動を分けたいことがある。
しかし、c# には javascript の undefine リテラルに相当するリテラルが存在せず、オブジェクトに何も設定せずに参照すると default(T) になるため何も値を設定していないのか、default(T) を設定されたのか分からず困る。
例えば以下のような、作成・更新データ形式をクラス構造で表現している時。
public class MemberManager { public void Create(Member member) { // 作成処理:member.Id のデータを作成。 // member.Age が null なら、データを null // member.Name が null なら、データを null } public void Update(Member member) { // 更新処理:member.Id に該当するデータを更新という前提。 // member.Age が デフォルト値(0)なら、データを 0 にする? それとも更新しない? // member.Name が デフォルト値(null) なら、データを null にする? それとも更新しない? } } public class Member { public string Id { get; set; } public int Age { get; set; } public string Name { get; set; } }
作成時には、データある/なしの場合は、値を指定する/ null で可能なのだが、
更新時には、これ以外に 「更新しない」という挙動が基本的に必要だ。そうしなければユーザの意図せず値がデフォルト値に更新されてしまうからだ。
これをどのように実現するかをメモっておく。
解決方法考察
- 更新しないための「定数」を用意
- Update引数をMember →Dictionary
- 値の未設定/デフォルト値設定を分別できるオブジェクトにする
更新しないための「定数」を用意
「更新しない」という定義がなけりゃそう言った意味の定数を定義すれば良い。という考え方。
この方法はわかりやすいが危なっかしいコードになりやすい。コード例を表すと。
public class MemberManager { public void Create(Member member) { // 作成処理:member.Id のデータを作成。 // member.Age が null なら、データを null // member.Name が null なら、データを null } public void Update(Member member) { // 更新処理:member.Id に該当するデータを更新という前提。 if (member.Age == Member.AgeNotUpdate) // Age を更新しない if (member.Name == Member.NameNotUpdate) // Name を更新しない } } public class Member { public static readonly int AgeNotUpdate = 13579; public static readonly string NameNotUpdate = "########"; public string Id { get; set; } public int Age { get; set; } public string Name { get; set; } }
このコードを使う時は、下記のように使う。
var data = new Member(); data.Id = "123456"; data.Age = 23; // data.Age = 13579; の場合は変更しようとしても更新されない data.Name = null; //null にしたい場合 // data.Name = Member.NameNotUpdate; //これで更新されない。ただし分かり辛い... new MemberManager().Update(data);
このコードの問題は、その「定数値」の更新処理ができなくなること。
もし、その値を偶然指定されてしまった場合は分かり辛いバグになること。
この方式自体が使う側にとっては直感的に分かり辛いこと。
Update引数をMember →Dictionary
そもそもオブジェクトがデフォルト値と明示的に設定されたかを区別できないことが問題なのだから、データ定義を変えればいい。という考え方。
この方法は「定数」による解決で発生する問題は解決できる。
しかし、Dictionary はデータ構造を共通にしないといけないため、型チェックができずメンテナンス性が悪くなりやすい。
コード例を表すと。
public class MemberManager { public void Create(Member member) { // 作成処理:member.Id のデータを作成。 // member.Age が null なら、データを null // member.Name が null なら、データを null } public void Update(Dictionary<Member.Key, object> member) { // 更新処理:member.Id に該当するデータを更新という前提。 if (!member.ContainsKey(Member.AgeKey)) // Age を更新しない if (!member.ContainsKey(Member.NameKey)) // Name を更新しない } } public class Member { // ハッシュキー public static readonly Key IdKey = new Key("Id"); public static readonly Key AgeKey = new Key("Age"); public static readonly Key NameKey = new Key("Name"); public class Key { public string Value { get; } private Key(string value) { Value = value; } } public string Id { get; set; } public int Age { get; set; } public string Name { get; set; } }
このコードを使う時は、下記のように使う。
var data = new Dictionary<Member.Key, object>() { { Member.IdKey, "123456" } { Member.AgeKey, 23 } { Member.NameKey, "AAA" } // 更新しない時はこの記載を外す。 //{ Member.NameKey, null } // null にしたければ null を代入する。 } new MemberManager().Update(data);
「更新しない」ケース、null、値の欠けもない。完璧に見える。だが、このコードは型チェックができない。
つまり以下の書き方も可能だ。
var data = new Dictionary<Member.Key, object>() { { Member.IdKey, "123456" } { Member.AgeKey, "AAA" } // 本来整数なのに文字列でも入ってしまう { Member.NameKey, "AAA" } } new MemberManager().Update(data);
また、データ型定義を変更した場合もコンパイルエラーが出ないためメンテナンスには苦労すると思われる。
値の未設定/デフォルト値設定を分別できるオブジェクトにする
Dictionary のいいところを利用して、型のコンパイルエラーも検知したいという仕様。オブジェクトの皮を被ったDictionary を作る。
コード例を表すと。
public class MemberManager { public void Create(Member member) { // 作成処理:member.Id のデータを作成。 // member.Age が null なら、データを null // member.Name が null なら、データを null } public void Update(Member member) { var dictionary = member.ToDictionary(); // 更新処理:member.Id に該当するデータを更新という前提。 if (!dictionary.ContainsKey(Member.AgeKey)) // Age を更新しない if (!dictionary.ContainsKey(Member.NameKey)) // Name を更新しない } } public class Member { // ハッシュキー public static readonly Key IdKey = new Key("Id"); public static readonly Key AgeKey = new Key("Age"); public static readonly Key NameKey = new Key("Name"); public class Key { public string Value { get; } private Key(string value) { Value = value; } } // 内部データ private Dictionary<Member.Key, object> _dictionary = new Dictionary<Member.Key, object>(); public Dictionary<Member.Key, object> ToDictionary() { return _dictionary; } // プロパティの動作挙動定義 private T Get<T>(Member.Key key) { return _dictionary.containsKey(key) ? (T)_dictionary[key] : default(T); } private void Set<T>(Member.Key key, T value) { if (_dictionary.containsKey(key)) _dictionary[key] = value; else _dictionary.Add(key, value); } // _dictionary から値を取得、値を設定 public string Id { get { return Get<string>(Member.IdKey); } set { Set(Member.IdKey, value); } } public int Age { get { return Get<int>(Member.AgeKey); } set { Set(Member.AgeKey, value); } } public string Name { get { return Get<string>(Member.NameKey); } set { Set(Member.NameKey, value); } } }
このコードを使う時は、下記のように使う。
var data = new Member(); data.Id = "123456"; data.Age = 23; data.Name = "AAA"; //更新しない場合は記載しない。これで_dictionary には含まれないないので更新対象にならない。 // data.Name = null; //null にしたい場合 new MemberManager().Update(data);
これだと型チェックもできるし、定義していない/明示的にnullを設定したかを判断できる。