演算子をオーバーロードするには?[C#/VB].NET TIPS

自作のクラスに対して演算子をオーバーロードすることで、演算子の振る舞いを変更し、より簡潔にコードを記述できるようになる。その方法を解説する。

» 2018年04月11日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載「.NET TIPS」

 演算子をオーバーロードすると(VBでは「演算子プロシージャを作ると」)、演算子の既定の動作を変更できる。例えば、等しいかどうかを調べる「==」演算子(C#)/「=」演算子(VB)は既定では参照の比較になっているが、オーバーロードして値の比較に変えられる。あるいは、複素数を表す構造体を作るとき、複素数の加算をAddメソッドとして実装するといちいちメソッド呼び出しの形式で記述しなければならないが、「+」演算子を定義すれば簡潔に加算を記述できる。本稿では、このような利点のある演算子オーバーロードについて解説する。

POINT 演算子のオーバーロード

演算子のオーバーロードまとめ 演算子のオーバーロードまとめ


 特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。

 なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017以降が必要である。サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。

using static System.Console;

Option Strict On
Imports System.Console

本稿のサンプルコードに必要な宣言(上:C#、下:VB)

ベースとなる複素数構造体

 次のコードに、複素数を表す構造体を示す。本稿では、このComplexNumber構造体をベースとしてコードを追加していく。

public struct ComplexNumber
{
  // 公開メンバ
  public readonly int Real; // 実部
  public readonly int Imaginary; // 虚部

  // コンストラクタ
  public ComplexNumber(int real, int imaginary)
  {
    Real = real;
    Imaginary = imaginary;
  }

  // ToStringメソッドのオーバーライド
  public override string ToString()
    => $"{Real}{Imaginary:+#;-#;+0;}i";
}

……省略……

// Mainメソッド内
var c1 = new ComplexNumber(1, 2);
var c2 = new ComplexNumber(3, -3);
var c3 = new ComplexNumber(-1, 0);
WriteLine($"{c1}, {c2}, {c3}");
// 出力:1+2i, 3-3i, -1+0i

Public Structure ComplexNumber
  ' 公開メンバ
  Public ReadOnly Real As Integer ' 実部
  Public ReadOnly Imaginary As Integer ' 虚部

  ' コンストラクタ
  Public Sub New(real As Integer, imaginary As Integer)
    Me.Real = real
    Me.Imaginary = imaginary
  End Sub

  ' ToStringメソッドのオーバーライド
  Public Overrides Function ToString() As String
    Return $"{Real}{Imaginary:+#;-#;+0;}i"
  End Function
End Structure

……省略……

' Mainプロシージャ内
Dim c1 = New ComplexNumber(1, 2)
Dim c2 = New ComplexNumber(3, -3)
Dim c3 = New ComplexNumber(-1, 0)
WriteLine($"{c1}, {c2}, {c3}")
' 出力:1+2i, 3-3i, -1+0i

解説のベースにする構造体(上:C#、下:VB)
複素数を表すイミュータブルな(変更不可能な)構造体である。インスタンスを作った後では値を変更できないので、効率を重視してプロパティではなくメンバを公開している。
ToStringメソッドはオーバーライドしてあり、その出力例を末尾に示した。

「+」演算子をオーバーロードするには?

 operatorキーワードを使い、静的メソッドとして実装する(次のコード)。Addメソッドとして実装した例も併せて載せてある。呼び出す側のコードは、Addメソッドよりも演算子のオーバーロードの方が簡潔で直観的になる。

// ComplexNumber構造体内

// 加算をAddメソッドとして実装した場合
public ComplexNumber Add(ComplexNumber c)
  => new ComplexNumber(this.Real + c.Real, this.Imaginary + c.Imaginary);

// +演算子のオーバーロード
public static ComplexNumber operator +(ComplexNumber c1, ComplexNumber c2)
  => new ComplexNumber(c1.Real + c2.Real, c1.Imaginary + c2.Imaginary);

// Mainメソッド内

var c1 = new ComplexNumber(1, 2);
var c2 = new ComplexNumber(3, -3);

// Addメソッドを呼び出す
WriteLine($"({c1}).Add({c2}) ⇒ {c1.Add(c2)}");
// 出力:(1+2i).Add(3-3i) ⇒ 4-1i

// 演算子オーバーロードを呼び出す
WriteLine($"({c1}) + ({c2}) ⇒ {c1 + c2}");
// 出力:(1+2i) + (3-3i) ⇒ 4-1i

' ComplexNumber構造体内

' 加算をAddメソッドとして実装した場合
Public Function Add(c As ComplexNumber) As ComplexNumber
  Return New ComplexNumber(Me.Real + c.Real, Me.Imaginary + c.Imaginary)
End Function

'  +演算子プロシージャ
Public Shared Operator +(c1 As ComplexNumber, c2 As ComplexNumber) As ComplexNumber
  Return New ComplexNumber(c1.Real + c2.Real, c1.Imaginary + c2.Imaginary)
End Operator

' Mainプロシージャ内

Dim c1 = New ComplexNumber(1, 2)
Dim c2 = New ComplexNumber(3, -3)

' Addメソッドを呼び出す
WriteLine($"({c1}).Add({c2}) ⇒ {c1.Add(c2)}")
' 出力:(1+2i).Add(3-3i) ⇒ 4-1i

' 演算子プロシージャを呼び出す
WriteLine($"({c1}) + ({c2}) ⇒ {c1 + c2}")
' 出力:(1+2i) + (3-3i) ⇒ 4-1i

複素数の加算をAddメソッドと演算子のオーバーロードで実装した例(上:C#、下:VB)
このように2つの引数を取る演算子のオーバーロードでは、引数の少なくとも1つは自身と同じ型でなければならない。ここでは引数の2つともComplexNumber型の「+」演算子のみオーバーロードしているが、片方が例えば整数型のオーバーロードなどを追加してもよい。
呼び出すときは、演算子のオーバーロードの方がメソッドより簡潔に書ける。また、「c1.Add(c2)」では「Add」の意味を考えないと加算しているのだと分からないが、「c1 + c2」ならば直観的に加算だと分かる。

演算子のオーバーロードはオーバーロードではない!?

 演算子のオーバーロードは、厳密に言えばオーバーロードではない。例えば、構造体を定義しただけでは(構造体内に)「+」演算子は存在しない。シグネチャの異なる複数のメソッドを実装するのがオーバーロード(overload=過積載)なのだから、1つだけ「+」演算子を実装してもオーバーロードしたことにはならないのである。組み込み型に対する「+」演算子はあるので、それに追加するという意味合いでの「オーバーロード」なのであろう。そのためか、「利用者定義演算子」(User-Defined Operators)と呼ばれることもある。また、VBではオーバーロードではなく、「演算子プロシージャ」と呼ばれている。

 ちなみに、「+」演算子をIL(中間言語)にコンパイルした結果を見てみると、整数に対する「+」演算子はILのadd命令に、また、文字列に対する「+」演算子はStringクラスのConcatメソッド呼び出しに変換されている。それら組み込み型の中に「+」演算子の実装は存在しないのである。対して、「+」演算子のオーバーロードは、(それを定義したクラス/構造体の中に)op_Additionという名前の静的メソッドとしてコンパイルされる。


オーバーロード可能な演算子

 全ての演算子がオーバーロードできるわけではない。その可否を次の表に示す。

演算子の種類 演算子 可否 備考
単項演算子 +/-/!/~/++/--/true/false  
2項演算子 +/-/*///%/&/|/^/<</>>  
比較演算子 ==/!=/</>/<=/>= ペアで実装しなければならない
「==」と「!=」のペア/「<」と「>」のペア/「<=」と「=>」のペア
論理演算子 &&/|| ×  
インデックス付け演算子 [] × 代わりにインデクサを実装する
キャスト (T)x(C#)
CType(VB)
×/〇 C#:代わりに「explicit」演算子/「implicit」演算子を実装する
VB:CType演算子をオーバーロードできる
代入演算子 +=/-=/*=//=/%=/&=/|=/^=/
<<=/>>=
× ただし、2項演算子(例えば「+」)をオーバーロードすれば、対応する代入演算子(例えば「+=」)の挙動も変わる
その他 =/./?:/??/->/=>/f(x)/as/checked/
unchecked/default/delegate/is/new/
sizeof/typeof
×  
オーバーロードの可否
演算子は基本的にC#のものだけを載せているが、VBでも同様である。
比較演算子とキャストについては、後述する。

「==」演算子をオーバーロードするには?

 比較演算子をオーバーロードするときは、ペアで実装しなければならない。片方だけの実装ではコンパイルエラーになる(次の画像)。

比較演算子はペアでオーバーロードしないとコンパイルエラーになる(C#)
比較演算子はペアでオーバーロードしないとコンパイルエラーになる(VB) 比較演算子はペアでオーバーロードしないとコンパイルエラーになる(上:C#/下:VB)
Visual Studio 2017で、「==」演算子のオーバーロードだけを書いたところ。
C#では、EqualsメソッドとGetHashCodeメソッドをオーバーライドしていないという警告も出ている。

 さらに、「==」演算子/「!=」演算子のペアでは、EqualsメソッドとGetHashCodeメソッドもオーバーライドする必要がある(C#では実装しないと警告が出る)。実装例を次のコードに示す。

// ComplexNumber構造体内

public static bool operator ==(ComplexNumber c1, ComplexNumber c2)
  => (c1.Real == c2.Real) && (c1.Imaginary == c2.Imaginary);

public static bool operator !=(ComplexNumber c1, ComplexNumber c2)
  => !(c1 == c2);

public override bool Equals(object obj)
{
  if (obj is ComplexNumber c)
    return (this == c);
  return false;
}
public override int GetHashCode()
  => Real ^ Imaginary;

// Mainメソッド内

var c1 = new ComplexNumber(1, 2);
var c2 = new ComplexNumber(3, -3);

WriteLine($"({c1}) == ({c2}) ⇒ {c1 == c2}");
// 出力:(1+2i) == (3-3i) ⇒ False
WriteLine($"({c1}) != ({c2}) ⇒ {c1 != c2}");
// 出力:(1+2i) != (3-3i) ⇒ True

' ComplexNumber構造体内

Public Shared Operator =(c1 As ComplexNumber, c2 As ComplexNumber) As Boolean
  Return (c1.Real = c2.Real) AndAlso (c1.Imaginary = c2.Imaginary)
End Operator

Public Shared Operator <>(c1 As ComplexNumber, c2 As ComplexNumber) As Boolean
  Return Not (c1 = c2)
End Operator

Public Overrides Function Equals(obj As Object) As Boolean
  If (TypeOf obj Is ComplexNumber) Then
    Return (Me = CType(obj, ComplexNumber))
  End If
  Return False
End Function

Public Overrides Function GetHashCode() As Integer
  Return Real Xor Imaginary
End Function

' Mainプロシージャ内

Dim c1 = New ComplexNumber(1, 2)
Dim c2 = New ComplexNumber(3, -3)

WriteLine($"({c1}) = ({c2}) ⇒ {c1 = c2}")
' 出力:(1+2i) = (3-3i) ⇒ False
WriteLine($"({c1}) <> ({c2}) ⇒ {c1 <> c2}")
' 出力:(1+2i) <> (3-3i) ⇒ True

複素数の等値比較演算子のオーバーロードを実装した例(上:C#、下:VB)
等値比較演算子をオーバーロードするときは、「==」演算子と「!=」演算子のオーバーロードおよびEqualsメソッドとGetHashCodeメソッドのオーバーライドの4つを記述する。
等値比較演算子のオーバーロードでは、循環参照にならないよう特に注意してほしい。Equalsメソッドと「==」演算子がお互いを呼び出しあってしまったり、「==」演算子の中で「==」演算子を呼び出してしまったりすると、無限ループに陥ってしまう。
なお、C#の「if (obj is ComplexNumber c)」という記述については、「特集:C# 7の新機能詳説:第3回 型による分岐の改良」を参照してもらいたい。

キャストのオーバーロードは?

 型のキャストは、C#ではexplicit演算子/implicit演算子を実装する。VBではCType演算子をオーバーロードできる(次のコード)。

 キャストには、暗黙の型変換と明示的変換がある。C#では、暗黙の型変換はimplicit演算子で、明示的変換はexplicit演算子で実装する。VBでは、WideningキーワードかNarrowingキーワードを付ける。

 暗黙の型変換を実装しておくと、演算子のオーバーロードを減らせることがある。例えば、本稿のここまでのサンプルコードでは「+」演算子も「==」演算子も自身の型(ComplexNumber型)同士のオーバーロードしか定義していない。しかし、整数からComplexNumber型への暗黙の型変換を実装すると、ComplexNumber型と整数との間でも「+」演算子と「==」演算子が機能するようになる。

// ComplexNumber構造体内

// 整数からComplexNumberへの暗黙の型変換
public static implicit operator ComplexNumber(int n)
  => new ComplexNumber(n, 0);

// Mainメソッド内

var c1 = new ComplexNumber(1, 2);
var c3 = new ComplexNumber(-1, 0);

// ComplexNumberと整数の加算
WriteLine($"({c1}) + 2 ⇒ {c1 + 2}");
// 出力:(1+2i) + 2 ⇒ 3+2i

// ComplexNumberと整数の等値比較
WriteLine($"({c3}) == -1 ⇒ {c3 == -1}"); 
// 出力:(-1+0i) == -1 ⇒ True

' ComplexNumber構造体内

' 整数からComplexNumberへの暗黙の型変換
Public Shared Widening Operator CType(n As Integer) As ComplexNumber
  Return New ComplexNumber(n, 0)
End Operator

' Mainプロシージャ内

Dim c1 = New ComplexNumber(1, 2)
Dim c3 = New ComplexNumber(-1, 0)

' ComplexNumberと整数の加算
WriteLine($"({c1}) + 2 ⇒ {c1 + 2}")
' 出力:(1+2i) + 2 ⇒ 3+2i

' ComplexNumberと整数の等値比較
WriteLine($"({c3}) = -1 ⇒ {c3 = -1}")
' 出力:(-1+0i) = -1 ⇒ True

型キャストのオーバーロードの例(上:C#、下:VB)
キャストのオーバーロードでは、暗黙か明示的かの指定が必須である(C#ではimplicit演算子/explicit演算子、VBではWideningキーワード/Narrowingキーワード)。
この例のように整数からComplexNumber型への暗黙の型変換を定義することで、整数とComplexNumber型との演算では、整数がComplexNumber型へと自動的にキャストされ、ComplexNumber型同士の演算子オーバーロードが呼び出される。

まとめ

 演算子をオーバーロードすると、その型を使った四則演算や等値比較などが書けるようになる。どのような処理でも実装できるが、演算子の本来の意味(例えば「+」演算子なら足し算)から懸け離れた処理を実装してしまうと混乱を招くので気を付けよう。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。