PowerShellのコレクション比較演算子について

PowerShellのコレクションに対して-eq演算子で比較すると不可解な結果が返ってきてずっと頭をかしげていた。

PS > $a = @(1,2,3,4,5)
PS > $a -eq 2
2

なんて比較をするとbool値じゃなくて比較した右オペランドの値と同じ値が返ってくる。この挙動の意味がさっぱりわからなくて、ずっとバグだと思っていた。
とこらが「PowerShell in Action」を読んだら、

オペランドが配列またはコレクションの場合、比較演算子はそのコレクションにおいて右オペランドと一致する要素を返します。

と書いてあって初めてこれが仕様だと知った。

単純にコレクションの中に特定の要素を調べるには、

PS > $a = @(1,2,3,4,5)
PS > $a -contains 2
True

のように-contains演算子を使って、その結果をbool値で取得できる。比較演算子といえば通常bool値で結果を返すものと刷り込まれているので、それ以外の結果が返ってくるコレクションに対する-eq演算子の挙動を理解することができなかった。まだまだ頭がカタイということか。。。

しかしこういう挙動をすることで得をするシチュエーションがいまいち想像できない。どういう使い方を想定しているんだろう?

例えばコレクションの中にある特定の要素をあるかどうか調べて、その要素に対して何かの処理をする場合を考えてみると、
「-contains」の場合

$a = @(1,2,3,4,5)
if($a -contains 4) {
    # 4に何かする
} else {
    # 無かった時
}

「-eq」の場合

$a = @(1,2,3,4,5)
$m = $a -eq 4
if($m -ne $null) {
    # 4に何かする
} else {
    # 無かった時
}

どちらにしても大して変わらない・・・と思っていたが、書いていて気づいた。-containsの場合は含まれている事はわかっても実際のその要素を取る手段がない!といってもこの場合、右オペランドの値がそのままコレクションに含まれている値と同値だから取る必要もないといえばない。
しかしそれが通用するのは比較する値がプリミティブ型の場合のみだろう。プリミティブでない型の場合、同値であったとしてもそのオブジェクトが持つ全ての情報が一致しているとは限らないので、右オペランドの値をそのまま使うわけにはいかない。そもそも-containsにしても-eqにしても実際にどのような比較が行われているのだろう?Equalsメソッドか?==演算子か?それともIComparable.CompareToだろうか?
すごく気になったので調べてみる。

とりあえずC#で以下のクラスを作って、DLLにコンパイルした。

namespace PSTest {
    public class Hoge {
        private int id;
        private string name;
        
        public int ID {
            get { return id; }
            set { id = value; }
        }
        public string Name {
            get { return name; }
            set { name = value; }
        }
        
        public Hoge() {
        }
        public Hoge(int id, string name) {
            this.id = id;
            this.name = name;
        }
    }
}

そして、このDLLを読み込んでとりあえず1つインスタンス

PS > $o1 = New-Object PSTest.Hoge(1, "Bob")

そしてこのオブジェクトを含んだ配列を作る。

PS > $a = @($o1, (New-Object PSTest.Hoge(2, "John"))

とりあえず-containsで比較

PS > $a -contains $o1
True

まぁ当然か、次-eq

PS > $a -eq $o1

ID Name
-- ----
 1 hoge

まったく同じ属性を持つ別オブジェクトと比較してみると、

PS > $o2 = New-Object PSTest.Hoge(1, "Bob")
PS > $a -contains $o2
False
PS > $a -eq $o2

両方とも当然、結果はネガティブ。
この段階ではまだ何で比較しているかははっきりしない。おそらくこの時点ではobject.ReferenceEquals(つまりobject.Equals)だろう。

それをはっきりさせるために、object.Equalsメソッドをオーバーライドする。

public override bool Equals(object obj) {
    if(!(obj is Hoge)) return false;
    
    return ((Hoge)obj).id.Equals(id);
}
public override int GetHashCode() {
    return id.GetHashCode();
}

これに変更して、先程と同じ処理をやってみると

PS > $o1 = New-Object PSTest.Hoge(1, "Bob")
PS > $a -contains $o1
True
PS > $a -eq $o1

ID Name
-- ----
 1 hoge
PS > $o2 = New-Object PSTest.Hoge(1, "Bob")
PS > $a -contains $o2
True
PS > $a -eq $o2

ID Name
-- ----
 1 hoge

予想通りというかなんというか、Equalsメソッドで比較していたみたい。そりゃ.NETの標準から外れた事はしないわな。他にも試してみたい事があるけど眠たくなってきたのでもう寝る。