何でほしいのか?
例えば以下のようなコードを書いちゃった場合、意図しない無限再帰が発生する。
using System;
namespace ConsoleApplication5
{
internal class Program
{
private static void Main(string[] args)
{
var a = new Sample();
var b = new Sample();
Console.WriteLine(a == b);
}
}
public class Sample
{
public int Value { get; set; }
public override bool Equals(object obj)
{
var target = obj as Sample;
if (target == null) return false;
return target.Value == Value;
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public static bool operator ==(Sample a, Sample b)
{
//ここで無限再帰が発生する。
//ホントは if ((object)a == null && (object)b == null) return true;
//とすべき。
if (a == null && b == null) return true;
//こちらも同様無限再帰が発生する。
//ホントは if ((object)a == null || (object)b == null) return true;
//とすべき。
if (a == null || b == null) return false;
return a.Value == b.Value;
}
public static bool operator !=(Sample a, Sample b)
{
return !(a == b);
}
}
}
一般的には、コメントに書いたように評価対象をSystem.Objectにキャストすれば何の問題も無いワケだけど、C♯組み込みのis演算子を模して、以下のような演算子があれば便利だと思ったのが、事の発端(operator ==のみを抜粋)
public static bool operator ==(Sample a, Sample b)
{
//汎用的なnull比較演算子と仮定。
if (a is null && b is null) return true;
//汎用的なnull比較演算子と仮定。
if (a is null || b is null) return false;
return a.Value == b.Value;
}
もちろん、"is null"なんて演算子はないし、作れない。ならば何とかできないかなと考えてみた。
拡張メソッドを使う
コトで、"."でつなげる必要はあるけど、可能じゃ無いかなって。以下はそのサンプル
using System;
namespace ConsoleApplication5
{
internal class Program
{
private static void Main(string[] args)
{
var a = new Sample();
var b = new Sample();
Console.WriteLine(a == b);
}
}
//System.Objectに対する拡張メソッド。
public static class SimpleExtends
{
public static bool IsNull(this object obj)
{
return obj == null;
}
public static bool IsNotNull(this object obj)
{
return obj != null;
}
}
public class Sample
{
public int Value { get; set; }
public override bool Equals(object obj)
{
var target = obj as Sample;
if (target == null) return false;
return target.Value == Value;
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public static bool operator ==(Sample a, Sample b)
{
if (a.IsNull() && b.IsNull()) return true;
if (a.IsNull() || b.IsNull()) return false;
return a.Value == b.Value;
}
public static bool operator !=(Sample a, Sample b)
{
return !(a == b);
}
}
}
実際、これで全てうまくいく。
Null許容型はどうなるの?との疑問は当然あるけど、Ecma-335:2006に
If the value type is a nullable type—defined as an instantiation of the value type System.Nullable—the result is a null reference or bitwise copy of its Value property of type T,depending on its HasValue property (false and true, respectively).
と記載されているので、boxingの段階でnullになるから、問題は無いコトになる。
効率が悪くてもこれ以上できない理由
先の例は、値型を片っ端からboxingしちゃうので、効率が悪いから、場合分けしようと当然考える訳だけど、
残念ながら、これを以下のように拡張すると、コンパルエラーが発生する。
using System;
namespace ConsoleApplication5
{
internal class Program
{
private static void Main(string[] args)
{
var a = new Sample<int>();
var b = new Sample<int>();
Console.WriteLine(a == b);
}
}
//参照型用
public static class RefTypeExtends
{
public static bool IsNull<T>(this T obj)
where T : class
{
return (object)obj == null;
}
public static bool IsNotNull<T>(this T obj)
where T : class
{
return (object)obj != null;
}
}
//値型用
public static class ValTypeExtends
{
public static bool IsNull<T>(this T value)
where T : struct
{
return false;
}
public static bool IsNotNull<T>(this T value)
where T : struct
{
return true;
}
}
//Null許容型用
public static class NullableExtends
{
public static bool IsNull<T>(this T? value)
where T : struct
{
return !value.HasValue;
}
public static bool IsNotNull<T>(this T? value)
where T : struct
{
return value.HasValue;
}
}
public class Sample<T> where T : IEquatable<T>
{
public T Value { get; set; }
public override bool Equals(object obj)
{
var target = obj as Sample<T>;
return target != null && target.Value.Equals(Value);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public static bool operator ==(Sample<T> a, Sample<T> b)
{
if (a.IsNull() && b.IsNull()) return true;
if (a.IsNull() || b.IsNull()) return false;
if (a.Value.IsNull() && b.Value.IsNull()) return true;
if (a.IsNull() || b.IsNull()) return false;
return a.Value.Equals(b.Value);
}
public static bool operator !=(Sample<T> a, Sample<T> b)
{
return !(a == b);
}
}
}
これが解決できない理由は、こいつをコンパイルする時点で、どこのIsNullメソッド又は、IsNotNullメソッドを呼び出すのか確定できないからじゃ無いかなと思います。
で、まとめ
- 拡張メソッドを作成することで、"."でつなげることによって、IsNullおよび、IsNotNullメソッドを定義できた。
- System.Objectが引数であっても、Nullable型は破綻無く判定できる。
- Boxingを減らすためのアプローチは残念ながら不可能。
だいたいこのようなコトになるのでは無いかなということで。