VB のたまご

作成日: 2017/12/11, 更新日: 2017/12/11


NameOf演算子にジェネリック定義型を渡しても、実名にはならないよという話

  •  この記事は、Visual Basic Advent Calendar 2017 の 11 日目の記事です。

  •  タイトルが日本語でおkなのですが、要するに間違った使い方をしてしまったという失敗話になります。 ジェネリック定義した T 型は、実行時は(インスタンス生成時に指定した)実在する型になるのですが、 NameOf 演算子を通した T 型は、指定した型ではなく、T 型のままでした。

  •  ちょっと何言ってるか分かんないですけどだと思うので、以下段階を追いながら記載します。

  •  参考
  • stackoverflow
    nameof with Generics
    https://stackoverflow.com/questions/29878137/nameof-with-generics
    


NameOf 演算子

  •  Visual Studio 2015 / VB 14 から NameOf 演算子が使えるようになりました。 変数名を文字列に含めている場合や、WPF 系 MVVM の変更通知プロパティ向けに使う場面が多いです。

  •  例えばこういうのがあったとして、
  • Sub GetData(key As String)
    
        If key = String.Empty Then
            Throw New ArgumentNullException("key is Nothing")
        End If
    
    End Sub
    

  •  key だと分かりづらいため、secretKey という変数名に変更した(secretKey と publicKey の2種類あるという仕様という前提)とします。
  • Sub GetData(secretKey As String)
    
        If key = String.Empty Then ' ← key に赤の下線が表示されるし、ビルドエラーで教えてくれる
            Throw New ArgumentNullException("key is Nothing") ' ← 文字列なので引っかからない(Grep モレ探しと目視確認という追加作業)
        End If
    
    End Sub
    

  •  すると、If 文の 判定中にある key は修正しない限りビルドエラーが出続けるので必ずひっかかりますが、 例外エラーを投げる際の文字列中に含まれている key はビルドエラーにならないので修正モレになる可能性があります。

  •  こういう時に、NameOf を経由しておくと変数扱いされるので、ひっかけることができるようになります。
  • Sub GetData(secretKey As String)
    
        If key = String.Empty Then ' ← key に赤の下線が表示されるし、ビルドエラーで教えてくれる
            Throw New ArgumentNullException($"{NameOf(key)} is Nothing") ' ← key に赤の下線が表示されるし、ビルドエラーで教えてくれる
        End If
    
    End Sub
    

  •  大事なポイントは、「プログラマーが暗闇の中から探し出す」のではなく「Visual Studio がプログラマーに知らせる」という負担軽減機能であるということです。


ジェネリック(使いたくなる場面)

  •  続いてはジェネリックです。

  •  Visual Studio 2003? (.NET Framework 1.1) の時は、配列よりもリストの方が扱い方が楽なため、ArrayList を多用していました。 ただし、Object 型のリストなのでどんな型も登録できるけど、Integer 型以外登録しちゃダメですよ、などと、特定の型専用として使うための精神論ルール縛りも一緒でした。
  • Dim items1 = New ArrayList() From {1, 2, 3, "four"}
    Dim maxValue = 0
    
    For Each intValue As Integer In items1 ' ←型変換できず、例外エラーが出るのでは(気付くのが実装終わって、動かしている時)
        If maxValue < intValue Then
            maxValue = intValue
        End If
    Next
    

  •  こういうのは、そもそもそういう書き方ができない仕掛けをするべきで、ダメな書き方をした時に Visual Studio に教えてもらえる状態が望ましいです。 そこで考え出されたのがジェネリックという考え方です。
  • Dim items2 = New List(Of Integer) From {1, 2, 3, "four"} ' ← "four" に赤の下線が表示されるし、ビルドエラーで教えてくれる
    maxValue = 0
    
    For Each intValue As Integer In items2
        If maxValue < intValue Then
            maxValue = intValue
        End If
    Next
    

  •  例えば、このサンプルでは、リスト宣言時に Integer 型を指定して縛っています。 これにより、リストには Integer 型データ以外は登録できず、無理やり登録しようとするとビルドエラーではじかれてしまいます。

  •  大事なポイントは、「実行時にやっと気づいて、出戻り作業させる」のではなく「コーディング中にすぐに知らせる」という負担軽減機能であるということです。


ジェネリック(定義する側)

  •  ここから、やっとタイトルの件が始まりますが、前提となる仕様がちょっと複雑なので、もう少し長くなります。 よくある多層レイヤーのうち、データ層とデータアクセス層を作っているとして、ここでは仮に、好きな音楽、好きな食べ物、という風に好きなもの DB の各テーブルとやり取りするとします。

  •  データ層は以下のように作っています。
  • ' 基底クラス(メンバーは省略)
    Class DataLayerBase
    End Class
    
    ' 各継承先クラス(メンバーは省略)。DB テーブル名と同じ名前
    Class Music
        Inherits DataLayerBase
    End Class
    
    Class Food
        Inherits DataLayerBase
    End Class
    

  •  データアクセス層は以下のように作っています。
  • ' 基底クラス
    ' DataLayerBase クラスを継承した任意のデータクラスの型だけ受け取れる
    Class DataAccessLayerBase(Of TDataClass As {DataLayerBase, New})
    
        ' テーブル名とデータクラス名が違うだけで、CRUD 機能は同じなので、基底クラス側で提供しようと考えた
    
        ' Select
        Function GetData() As IEnumerable(Of TDataClass)
    
            Dim items As IEnumerable(Of TDataClass) = Nothing
            Dim query = $"SELECT * FROM {NameOf(TDataClass)}"
    
            ' SQLConnection とか使って取得したとして
            Return items
    
        End Function
    
        ' Insert, Delete, Update は省略
    
    End Class
    
    ' 各継承先クラス
    Class DataAccessMusic
        Inherits DataAccessLayerBase(Of Music)
    
        ' こういう基本機能が、クラスを継承するだけで使えるようになるので、各テーブルでは個別処理だけ準備する
    
        ' Select
        'Function GetData() As IEnumerable(Of Music)
        'End Function
    
        ' Insert, Delete, Update は省略
    
    End Class
    
    Class DataAccessFood
        Inherits DataAccessLayerBase(Of Food)
    
        ' 同上
    
    End Class
    

  •  データアクセス層の基底クラス内にあるのが、タイトルの件になります。
  • Dim query = $"SELECT * FROM {NameOf(TDataClass)}"
    

  •  こういう取得側を書いたとして、
  • Dim da As DataAccessMusic = New DataAccessMusic
    Dim items As IEnumerable(Of Music) = da.GetData()
    

  •  これを実行するとどうなるかというと、中ではこうなります。
  • SELECT * FROM TDataClass
    

  •  Σ(゚д゚lll)ガーン
  • 変わっていないじゃんorz

  •  で、調べた結果、正しく書き直したのがこちら。
  • Dim query = $"SELECT * FROM {GetType(TDataClass).Name}"
    

  •  これを実行すると、
  • SELECT * FROM Music
    

  •  とデータクラスの名前(=テーブルの名前)に変わってくれます。


ちょっと、言いたい事がよく分からんかった

  •  と思うので、補足です。
  • Class Class1(Of T)
    
        Dim query As String = $"SELECT * FROM {NameOf(T)}"
    
    End Class
    

  •  T 型は、実装時は T のままなのでよく分からんですが、実行時になると、具体的に存在する任意の型として扱われます。 例えば T が Integer 型だったり、ユーザー定義の DataClass 型(この記事では Music 型、Food 型)だったりします。 これはインスタンス生成時とかに、型を指定するタイミングで決まります。

  •  なので、実装時、頭の中では、T を任意の型として脳内変換しながら扱うことになるのですが、
  • Dim query As String = $"SELECT * FROM {NameOf(T)}"
    

  •  と書いた時、実行時には T が具体的に存在する任意の型になっているので、NameOf 演算子を通した場合、その型名になるはず。という思い込みをしていました。 結果として、NameOf 演算子はジェネリック定義型であってもそのまま文字列にする挙動みたいでした。

  •  めでたし、めでたし。