a == b かつ !(a = b)

C# の言語仕様には、表題の通り

  • a == b
  • !(a <= b)
  • !(a >= b)

を全て満たす値の組み合わせがある、という小話。なお、ユーザー定義の演算子で〜というオチではない。

(続きを読む、で隠してみた)

ref: リフトとは? - 当面C#と.NETな記録

具体的には、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

という挙動を実現できる。ただし、見ての通り定義しなければならない演算子オーバーロードが多いため、いささか面倒である。