a == b かつ !(a = b)
C# の言語仕様には、表題の通り
- a == b
- !(a <= b)
- !(a >= b)
を全て満たす値の組み合わせがある、という小話。なお、ユーザー定義の演算子で〜というオチではない。
(続きを読む、で隠してみた)
具体的には、a と b が Nullable<> 型で、双方が null の場合に上の条件を満たす。
というのも、C# 言語仕様の§14.2.7 (Lifted operators) にある通り、
- <=
- =>
- >
演算子は、片方あるいは両方のオペランドが null であった場合に false を返す仕様なためである。これは、< や > 演算子のみならず、<= や => 演算子であっても当てはまる。
そのため、
struct Hoge{} Hoge? a = null; Hoge? b = null; Console.WriteLine(a == b); // true Console.WriteLine(a <= b); // false Console.WriteLine(a >= b); // false
のようなコードや、
int? a = null; int? b = null; Console.WriteLine(a == b); // true Console.WriteLine(a <= b); // false Console.WriteLine(a >= b); // false
のようなコードは、a == b であるにも関わらず、a <= b や a >= b が false となる。
したがって、特に Generic なコードを書く場合、
- a == b ならば a <= b
- a <= b かつ b <= a ならば a == b
といった(普通の順序なら当然な)前提を持たずにコードを書くように注意する必要があるだろう。
ちなみに、自分の作った構造体について、この挙動を回避したい場合、Nullable な型についても演算子オーバーロードを定義することで回避できる。
具体的には、
struct Hoge { public int Value; public static bool operator ==(Hoge lhs, Hoge rhs) { return lhs.Value == rhs.Value; } public static bool operator !=(Hoge lhs, Hoge rhs) { return lhs.Value != rhs.Value; } public override int GetHashCode() { return this.Value.GetHashCode(); } public override bool Equals(object obj) { if (!(obj is Hoge)) return false; return ((Hoge)obj).Value == this.Value; } public static bool operator <=(Hoge lhs, Hoge rhs){ return lhs.Value <= rhs.Value; } public static bool operator >=(Hoge lhs, Hoge rhs) { return lhs.Value >= rhs.Value; } public static bool operator <=(Hoge? lhs, Hoge? rhs) { if (lhs == null) return (rhs == null); return lhs.Value <= rhs.Value; } public static bool operator >=(Hoge? lhs, Hoge? rhs) { if (lhs == null) return (rhs == null); return lhs.Value >= rhs.Value; } }
のような構造体を作ることで、
struct Hoge{} Hoge? a = null; Hoge? b = null; Console.WriteLine(a == b); // true Console.WriteLine(a <= b); // true Console.WriteLine(a >= b); // true