VB.NETでもCType演算子をオーバーロードして値を変換したい

はじめに

これまた特に需要は無かったんですが、意味なくクラスを別のクラスへ変換したかったんでCTypeについて調べてみました。

演算子と関数

なんか調べてみると自分でオーバーロードするCType演算子と変換を実行するCType関数の二種類があるみたいですね。

Operator Statement | Microsoft Docs

CType 関数 (Visual Basic) | Microsoft Docs

自分で変換を定義するときは演算子オーバーロードとしてCTypeを実装し、明示的に呼び出すときはCType関数を用いて変換を指示すると自分で定義したCType演算子があればそれが呼ばれるし、それ以外にもプリミティブ型であればいい感じにコンパイラが等価なMSILとしてインラインに展開するみたいな感じっぽい?

VBのことだしやっぱりCType("1.0", Double)とかやるとみんな大好きCompilerServicesがしゃしゃり出てくるのでそう単純じゃないっぽいんですけどね。えぇ。

演算子の実装の方はと言いますと、ずいぶん前にOption Strict特集で取り上げて変換には拡大変換と縮小変換の二種類があるってやりました。*1

CTypeの方も同じように演算子を定義するときにWideningNarrowingというキーワードを使用してその変換が拡大変換か縮小変換かコンパイラに教えてやらなくてはなりませぬ。

拡大変換の方 Widening (Visual Basic) | Microsoft Docs

縮小変換の方 Narrowing (Visual Basic) | Microsoft Docs

拡大変換は常に成功しなくちゃならないとか縮小変換は失敗する可能性があるとかその辺のお話しはOption Strict特集でどうぞ。

ちなみに特定の値へ変換する愉快な関数たちについての説明も見つけたので合わせてどうぞ。

データ型変換関数 (Visual Basic) | Microsoft Docs

サンプルコード

とりあえずいろいろ試してみるためのサンプルがこちら

Module Module1

    Sub Main()
        ' プロジェクトの設定で Option Strict On にしてます

        Conversion()
        Console.WriteLine()
        Casting()

        ' 継承関係にないものはいずれもムリ
        'Dim e As HogeE = New HogeE(1)
        'Dim f As HogeF = e
        'Dim f As HogeF = DirectCast(e, HogeE)
        'Dim f As HogeF = CType(e, HogeE)

        Console.ReadLine()
    End Sub

    Public Sub Conversion()
        ' 拡大変換なので特に文句は言われない
        ' ちなみに値の変換は変換を行った時点でCType演算子によって
        ' 相手の型のインスタンスが生成されるので参照が指し示す
        ' インスタンスは別々のもの
        ' 明示的にしろ暗黙的にしろ CType が呼ばれる
        Dim a1 As HogeA = New HogeA(Integer.MaxValue)
        Dim b1 As HogeB = a1
        a1.Display()
        b1.Display()
        Console.WriteLine()

        ' 縮小変換としてCType演算子をオーバーロードしたので
        ' 暗黙の型変換はコンパイラによって禁止される
        ' ので、明示的に変換をしてやる必要がある
        Dim b2 As HogeB = New HogeB(Long.MaxValue)
        b2.Display()
        Try
            Dim a2 As HogeA = CType(b2, HogeA)
            ' 例外が発生した時点で制御は例外ハンドラのCatchのほうへ移っちゃう
            ' ので↓は実行されません
            a2.Display()
        Catch ex As OverflowException
            Console.WriteLine("まぁ、結局オーバーフローしちゃうんですけどね")
        End Try

        ' 余談ですが、Tryまで入力してVisual Studioが挿入する例外ハンドラの
        ' スニペットが
        ' > Catch ex As Exception
        ' なのはちょっと問題だと思う
        ' 例外についてよくわかってないんだろうな~この人って人が書いたコードで
        ' 例外ハンドラがすべからく Exception をキャッチしているのを見たときに
        ' そうおもいましたまる

        ' 継承関係がないのでこっちはムリぽ
        ' Dim b3 As HogeB = DirectCast(a1, HogeB)
    End Sub

    Public Sub Casting()
        ' 参照を変換している方
        ' スーパークラスの参照からサブクラスへの参照への変換は
        ' 明示的に行わなくてはならない
        ' ちなみに参照をいじってるだけなので変数に格納されている参照が
        ' 指し示すインスタンスは4つとも同一
        Dim c As HogeC = New HogeD(1)
        Dim dCType As HogeC = CType(c, HogeD)
        Dim dDirectCast As HogeC = DirectCast(c, HogeD)
        Dim dTryCast As HogeC = TryCast(c, HogeD)

        c.Display()
        dCType.Display()
        dDirectCast.Display()
        dTryCast.Display()
    End Sub

End Module

' 値として変換を定義
Class HogeA
    Private value As Integer

    Public Sub New(value As Integer)
        Me.value = value
    End Sub

    Public Sub Display()
        Console.WriteLine(value)
    End Sub

    Public Shared Widening Operator CType(source As HogeA) As HogeB
        ' 拡大変換であり変換時にエラーが生じることはないので Widening で
        ' CType演算子をオーバーロード
        ' Widening で宣言するとコンパイラは Option Strict が On でも
        ' 暗黙的に変換することを許可する
        Return New HogeB(source.value)
    End Operator
End Class

Class HogeB
    Private value As Long

    Public Sub New(value As Long)
        Me.value = value
    End Sub

    Public Sub Display()
        Console.WriteLine(value)
    End Sub

    Public Shared Narrowing Operator CType(source As HogeB) As HogeA
        ' ここで System.OverflowException の可能性があるので
        ' 演算子は Narrowing で宣言
        ' Narrowingの場合は変換時にエラーの発生する可能性があるというこ
        ' となので Option Strict が On の場合はコンパイラは暗黙的に変換
        ' を禁止する
        Dim v = CType(source.value, Integer)
        Return New HogeA(v)
    End Operator
End Class

' 継承関係にあるので参照の変換が可能
Class HogeC
    Protected value As Integer

    Public Sub New(value As Integer)
        Me.value = value
    End Sub

    Public Overridable Sub Display()
        Console.WriteLine("HogeC:{0}", value)
    End Sub
End Class

Class HogeD
    Inherits HogeC

    Public Sub New(value As Integer)
        MyBase.New(value)
    End Sub

    Public Overrides Sub Display()
        Console.WriteLine("HogeD:{0}", value)
    End Sub
End Class

' 定義は同じだけど継承関係も値として変換するための
' 演算子の定義もしてない全く関係のないクラス
Class HogeE
    Private value As Integer

    Public Sub New(value As Integer)
        Me.value = value
    End Sub
End Class

Class HogeF
    Private value As Integer

    Public Sub New(value As Integer)
        Me.value = value
    End Sub
End Class

' 継承もするし変換も定義する
' 継承関係にあるクラスへの CType 演算子は許されないんDA☆
'Class HogeG
'    Private value As Integer

'    Public Sub New(value As Integer)
'        Me.value = value
'    End Sub

'    Public Shared Widening Operator CType(g As HogeG) As HogeH
'        Return New HogeH(g.value)
'    End Operator
'End Class

'Class HogeH
'    Inherits HogeG

'    Private value As Integer

'    Public Sub New(value As Integer)
'        MyBase.New(value)
'    End Sub

'    Public Shared Widening Operator CType(h As HogeH) As HogeG
'        Return New HogeG(h.value)
'    End Operator
'End Class

要点だけまとめてみると

  • CType関数を呼び出すと
    • CType演算子が定義されている場合はその演算子が呼ばれる
    • 継承関係にある場合は参照の型がそれぞれに目的の型に変換される
  • DirectCast演算子を呼び出すと参照の型の変換のみとなる(変換に失敗した場合は例外をスローする)
  • TryCast演算子は参照の型の変換を試み、失敗した場合はNothingを返す
  • 継承関係にあるクラスへのCType演算子の定義は許されない

って感じみたいです。

おまけでDirectCastTryCastの説明もどうそ。

DirectCast 演算子 (Visual Basic) | Microsoft Docs

TryCast 演算子 (Visual Basic) | Microsoft Docs

まとめ

用語の定義や使い方が色々とふわふわした感じで非常に気持ち悪いですが、一応独自クラスから独自クラスへの値としての変換を実装できました。 ただ、多分実用で実装することは滅多にないと思います。リフレクションの方が多用しそうですね。

ちなみにちなみにこれは勝手な想像ですがDirectCastTryCastは等価なMSILに置き換えられるだけなので演算子CTypeコンパイラが内部的に適切なメソッド呼び出しやMSILに置き換えるため関数ってことなんですかね。 勝手な想像ですよ。根拠はどこにも無いですし。

おわり

jyuch.hatenablog.com

*1:ページの最後にリンクを張っておきました。