VB Structure の CA1815 警告対策:Equals・GetHashCode・IEquatable 実装ガイド

CA1815 の警告が VB の Structure で急に出てきて戸惑った経験はありませんか?本記事では「値型は Equals と等値演算子を実装すべき」という .NET Code Analysis の指摘を、実務でそのまま使えるサンプルコードとともに丁寧に解説します。

目次

CA1815「値型は Equals と等値演算子を実装すべき」とは何か

.NET のコード解析(旧 FxCop/Code Analysis/Roslyn アナライザー)には、 CA1815: Value types should override Equals and operator Equals というルールがあります。VB では Structure を定義したとき、次のようなメッセージが Public Structure の行に表示されることがあります。

「値型は Equals と等値演算子を実装すべき」という警告は、 「値型の等値比較の意味を自分で定義しなさい」 という .NET からのお願いです。デフォルト実装(ValueType.Equals)は 反射ベースで遅く、意図した比較とズレる可能性があるためです。

項目内容
警告 IDCA1815
対象構造体(値型)
条件Equals / GetHashCode / 等値演算子を未実装
狙い意味の明確な等値比較とパフォーマンス向上

特に、構造体を Dictionary や HashSet のキーとして使う可能性がある場合、 Equals と GetHashCode の実装は必須と考えておきましょう。

なぜ VB の Structure で CA1815 が出るのか

VB の Structure は C# の struct と同じく値型です。 しかし、等値比較(Equals や = 演算子)を何も実装していないと、 実際に行われている比較は次のようになります。

比較の種類既定の動作問題点
Object.Equals(obj)ValueType.Equals によるフィールドごとの比較(反射ベース)遅い/フィールドの解釈(意味)が不明瞭
= 演算子ユーザー定義の演算子が無ければ使用不可(コンパイル エラー)直感的な構造体同士の比較が書けない
GetHashCode()ValueType.GetHashCode の既定実装Equals と整合性が取れている保証がない

つまり「動くけれど遅い」「意味があいまい」「コレクションで壊れるかもしれない」という状態です。 これを解消するのが CA1815 の目的です。

結論:実装すべきメンバー一覧

CA1815 を真っ当に解消するには、次の 3 点を実装するのが実務的にベストです。

  • Equals(Object) のオーバーライド(フィールド同士の等値判定)
  • = と <> の演算子オーバーロード(C# の == / != 相当)
  • GetHashCode と IEquatable(Of T) の実装(ハッシュコレクションとパフォーマンス対策)
メンバー目的
Equals(Object)基底クラスから呼ばれた時の比較ロジックを提供
IEquatable(Of T).Equalsボックス化なしでの高速・型安全な比較
GetHashCodeEquals と整合したハッシュ値の提供(Dictionary / HashSet 向け)
Operator = / <>構造体同士の直感的な比較式をサポート

これらをきちんと実装することで、CA1815 の警告解消だけでなく、 パフォーマンス・可読性・バグ低減の面でもメリットがあります。

推奨実装例:UShort フィールドを 2 つ持つ構造体

ここでは、質問でよく挙がる「UShort を 2 つ持つだけの単純な構造体」を例に、推奨実装を示します。


Imports System
Imports System.Runtime.InteropServices

&lt;StructLayout(LayoutKind.Sequential)&gt;
Public Structure blahStructDemo
    Implements IEquatable(Of blahStructDemo)

    ' UShort は 16bit 符号なしなので U2 指定が自然
    &lt;MarshalAs(UnmanagedType.U2)&gt;
    Public blah1 As UShort

    &lt;MarshalAs(UnmanagedType.U2)&gt;
    Public blah2 As UShort

    ' 型安全な比較(ボックス化なし)
    Public Overloads Function Equals(other As blahStructDemo) As Boolean _
        Implements IEquatable(Of blahStructDemo).Equals

        Return blah1 = other.blah1 AndAlso blah2 = other.blah2
    End Function

    ' Object.Equals のオーバーライド(ボックス化ありの呼び出し用)
    Public Overrides Function Equals(obj As Object) As Boolean
        If TypeOf obj Is blahStructDemo Then
            Return Equals(DirectCast(obj, blahStructDemo))
        End If
        Return False
    End Function

    ' Equals と整合するハッシュコード
    Public Overrides Function GetHashCode() As Integer
        ' 幅広いターゲットで動く組み合わせ例
        Dim hash As Integer = 17
        hash = hash * 31 + blah1.GetHashCode()
        hash = hash * 31 + blah2.GetHashCode()
        Return hash

        ' .NET 5+ なら以下でも可:
        ' Return HashCode.Combine(blah1, blah2)
    End Function

    Public Shared Operator =(left As blahStructDemo, right As blahStructDemo) As Boolean
        Return left.Equals(right)
    End Operator

    Public Shared Operator &lt;&gt;(left As blahStructDemo, right As blahStructDemo) As Boolean
        Return Not left.Equals(right)
    End Operator
End Structure
  

このサンプルをそのまま自分の構造体に当てはめれば、CA1815 の警告は解消され、 等値比較の意味もコードから一目で分かるようになります。

IEquatable(Of T).Equals 実装のポイント

IEquatable(Of T) の実装は、構造体の等値比較の「本体」です。 ここで「何をもって等しいとするか」を決めます。

  • 比較対象は同じ型(blahStructDemo)のみ
  • 比較するフィールドは、等価性に意味を持つものだけに絞る
  • 比較順は基本的にどちらでもよいが、差が出やすいフィールドを先にすると高速化になることもある

今回の例では、全てのフィールド(blah1 と blah2)が意味を持つため、単純に 2 つとも比較しています。


Public Overloads Function Equals(other As blahStructDemo) As Boolean _
    Implements IEquatable(Of blahStructDemo).Equals

    Return blah1 = other.blah1 AndAlso blah2 = other.blah2
End Function
  

AndAlso を使っているのは「左側が違った時点で右側は比較しない」ためです。 これにより、異なるケースでは不要な比較をスキップでき、わずかですが効率も良くなります。

Object.Equals のオーバーライドが必要な理由

IEquatable(Of T) だけを実装しても、CA1815 の警告は基本的に解決しません。 多くの API(例えば ArrayList や非ジェネリックなコレクション)は Object.Equals を前提としているため、 こちらもオーバーライドして、IEquatable の実装に委譲する必要があります。


Public Overrides Function Equals(obj As Object) As Boolean
    If TypeOf obj Is blahStructDemo Then
        Return Equals(DirectCast(obj, blahStructDemo))
    End If
    Return False
End Function
  

ポイントは、「型チェック → キャスト → IEquatable(Of T).Equals 呼び出し」という流れに統一している点です。 これにより、比較ロジックが一箇所に集約され、保守性が高まります。

GetHashCode 実装と Equals の整合性

Equals をオーバーライドしたら、必ず GetHashCode もオーバーライドしましょう。 ここを怠ると、Dictionary(Of TKey, TValue) や HashSet(Of T) で キーが見つからない・重複するなど、非常に厄介なバグに繋がります。

実装の基本ルールは次の通りです。

  • Equals で等しいと判定される 2 つの値は、必ず同じハッシュコードを返すこと
  • 可能な限りハッシュ値は一様に分布するようにすること
  • 実装はシンプルに保つ(必要以上に複雑なアルゴリズムは不要)

Public Overrides Function GetHashCode() As Integer
    Dim hash As Integer = 17
    hash = hash * 31 + blah1.GetHashCode()
    hash = hash * 31 + blah2.GetHashCode()
    Return hash
End Function
  

17 や 31 はよく使われる「適当な」素数で、 フィールドのハッシュコードを順に混ぜ合わせるシンプルなパターンです。 .NET 5 以降であれば、標準の HashCode.Combine を使うと、より簡潔に書けます。


' .NET 5 以降の場合
Public Overrides Function GetHashCode() As Integer
    Return HashCode.Combine(blah1, blah2)
End Function
  

演算子 = / <> をオーバーロードする理由

VB では、構造体同士の比較に = を使うためには、 自分で Operator = を定義する必要があります。


Public Shared Operator =(left As blahStructDemo, right As blahStructDemo) As Boolean
    Return left.Equals(right)
End Operator

Public Shared Operator &lt;&gt;(left As blahStructDemo, right As blahStructDemo) As Boolean
    Return Not left.Equals(right)
End Operator
  

これで、以下のような自然なコードが書けるようになります。


Dim a As blahStructDemo = ...
Dim b As blahStructDemo = ...

If a = b Then
    ' 同じときの処理
End If

If a &lt;&gt; b Then
    ' 違うときの処理
End If
  

実装はあくまで Equals に委譲しているため、 「Equals と演算子の結果が食い違う」といった事故も防げます。

MarshalAs(UnmanagedType.U2) と UShort フィールドの関係

質問のケースでは、構造体が P/Invoke(ネイティブ API 呼び出し)に使われることが多いはずです。 そのため、StructLayout と MarshalAs の指定も重要になります。

16bit の整数を扱う場合の対応関係は以下のようになります。

ネイティブ側の型VB 側の型MarshalAs 指定
unsigned shortUShortUnmanagedType.U2
shortShortUnmanagedType.I2

つまり、VB のフィールドを UShort にしたのであれば、 MarshalAs(UnmanagedType.U2) を指定するのが自然です。 ネイティブ側が符号付き 16bit の場合は、VB 側も Short に揃えるのが基本です。

さらに <StructLayout(LayoutKind.Sequential)> を付けることで、 フィールドの並びとメモリレイアウトをネイティブ側の構造体と一致させています。 これは P/Invoke を行う構造体ではほぼ必須の指定です。

値型構造体をハッシュキーとして使うときの注意

自作構造体を Dictionary や HashSet のキーとして使う場合、 挿入後に値を変更しないというルールがとても重要です。

例えば、次のようなコードは危険です。


Dim dict As New Dictionary(Of blahStructDemo, String)()

Dim key As New blahStructDemo With {.blah1 = 1, .blah2 = 2}
dict.Add(key, "value")

' ここでキーを変更してしまう
key.blah2 = 999

' 同じキーで取得できると思っても…
Dim result As String = dict(key) ' 実行時例外になる可能性
  

ハッシュキーとして構造体を使う場合は、次のような設計を検討しましょう。

  • フィールドを ReadOnly にして構造体を不変(イミュータブル)にする
  • 値を変えたい場合は「新しいインスタンスを作る」ような設計にする
  • ハッシュキーとして使う構造体は、「小さく・単純に」保つ

CA1815 に従って等値比較をきちんと実装するとともに、 「不変にするかどうか」も合わせて検討すると、実運用でのトラブルが大きく減らせます。

本当に比較しない構造体なら CA1815 を抑制する選択肢も

ここまで見ると「すべての構造体で Equals をガチガチに実装しないといけないの?」と思われるかもしれませんが、 そうとも限りません。

例えば次のような構造体は、比較自体がそもそも不要なケースもあります。

  • 純粋に P/Invoke のためだけの「レイアウト用コンテナ」
  • 単に API の引数として渡すだけで、アプリ側では比較・コレクション格納を一切しない

こういったケースでは、プロジェクトの方針として CA1815 の警告を抑制してしまうのも現実的な判断です。

.editorconfig でルールを無効化する例

プロジェクト全体、または特定フォルダ配下で CA1815 を無効にしたい場合は、 .editorconfig で次のように設定できます。


[*.vb]
dotnet_diagnostic.CA1815.severity = none
  

「原則は実装する」「比較もコレクション格納も絶対にしない構造体だけ抑制する」といったルールを チームで決めておくと、品質とコストのバランスが取りやすくなります。

実務で使えるチェックリスト

最後に、CA1815 対応や値型設計で迷ったときに見返せるチェックリストをまとめます。

項目チェック内容
等値の定義「どのフィールドが等値性に効くのか」を明文化しているか
IEquatable(Of T)ボックス化を避けるために実装しているか
Object.EqualsIEquatable の実装に素直に委譲しているか
GetHashCodeEquals と整合したハッシュコードになっているか
演算子 = / <>Equals と同じ意味になるように定義されているか
不変性ハッシュキーなどに使う構造体は不変にできているか
P/Invokeネイティブ型と VB 側の型/MarshalAs の組み合わせが一致しているか
警告抑制本当に比較不要な構造体だけに限定して CA1815 を抑制しているか

上記のサンプルコードとチェックリストをベースに、自分のプロジェクトで使っている構造体を一度見直してみると、 後々効いてくるバグの芽をかなり減らせるはずです。

まとめ

CA1815 の「値型は Equals と等値演算子を実装すべき」という警告は、 単なるノイズではなく、値型設計を見直すためのきっかけになります。

  • 既定の ValueType.Equals に頼るのではなく、自分で等値の意味を定義する
  • IEquatable(Of T)・Equals(Object)・GetHashCode・演算子 =/<> をセットで実装する
  • P/Invoke 用の構造体では、StructLayout や MarshalAs の指定も合わせて確認する
  • 本当に比較不要な純粋レイアウト用の構造体だけ、方針を決めた上で警告を抑制する

この記事の実装例をテンプレートとして使えば、VB の Structure に出る CA1815 警告を安全に解消しつつ、 保守しやすく高速な値型設計を行うことができます。自分のプロジェクトの構造体を一つずつ改善していき、 「よくわからない警告」から「品質を守るガイド」に変えていきましょう。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次