VB13以前でもNull条件演算子を使いたい

はじめに

ちょっと前にVB14の新機能についての話をしたじゃないですか。 その中にNull条件演算子なんてものがあったと思います。

弊社はEntity FrameworkみたいなライブラリでFirstOrDefaultみたいなクエリを割合ぽいぽい投げ返ってきた値がNothingかそうでないかで処理を分岐させることが多いのですが、

Dim hoge = db.Hoges.Where(Function(it) it.UniqueKey = "HOGE").FirstOrDefault()
Dim hogeName As String

If Not hoge Is Nothing Then
    hogeName = hoge.Name
Else
    hogeName = Nothing
End If

みたいなコードをほぼ毎回のように書かなくてはならず非常にげんなりします。 まぁ、設計がクソなのも一理ありますが。

そこでNull条件演算子を使いたいのですが、現在使用している環境では使えないという最高にクソな感じに仕上がっており*1日夜冗長でクソなコードを生成している次第でございます。うおォン 俺はまるでクソコードジェネレータだ

あああああ

かくなる上はメタプログラミングを用いてごり押しで解決したいところなのですが、さすがに構文に介入することはできません。 コンパイル時に介入してASTを弄れれば話は別かもしれませんが。

Roslyn? なにそれ? おいしいの?

というわけで拡張メソッドでそれっぽく仕上げてお茶を濁すほかありません。

というわけでほい。

Module NullableExtension

    <Extension>
    Public Function N(Of TModel, TResult As Class)(value As TModel, expr As Func(Of TModel, TResult)) As TResult
        If value Is Nothing Then
            Return Nothing
        Else
            Return expr(value)
        End If
    End Function

    <Extension>
    Public Function Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult)) As Nullable(Of TResult)
        If value Is Nothing Then
            Return Nothing
        Else
            Return expr(value)
        End If
    End Function

    <Extension>
    Public Function Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, Nullable(Of TResult))) As Nullable(Of TResult)
        If value Is Nothing Then
            Return Nothing
        Else
            Return expr(value)
        End If
    End Function

End Module

とくに解説することも無いのですが、

N(Of TModel, TResult As Class)(value As TModel, expr As Func(Of TModel, TResult))

N(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult))

オーバーロード不可なのはなんとなくわかりますが、

Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult))

Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, Nullable(Of TResult)))

オーバーロードできるんですね。驚きです。

んで、こんな感じに使います。

Dim a = New Hoge() With {.Id = 1, .Name = "Hoge", .ParentId = 0}
Dim b = New Hoge() With {.Id = 1, .Name = "Hoge", .ParentId = Nothing}
Dim c As Hoge

Console.WriteLine(a.Ns(Function(it) it.Id))
Console.WriteLine(a.N(Function(it) it.Name))
Console.WriteLine(a.Ns(Function(it) it.ParentId))
        
Console.WriteLine(b.Ns(Function(it) it.Id))
Console.WriteLine(b.N(Function(it) it.Name))
Console.WriteLine(b.Ns(Function(it) it.ParentId))

Console.WriteLine(c.Ns(Function(it) it.Id))
Console.WriteLine(c.N(Function(it) it.Name))
Console.WriteLine(c.Ns(Function(it) it.ParentId))

おわりに

なんというか、拡張メソッドの乱用ですよね。

Optionalみたいなアプローチではなくnullだろうがなんだろうがゴリ押しで値を取得するみたいなアプローチ、私は結構好きです。 ただ、実際に他人のコードでこんなのを見たら『うわぁ・・・』は必至ですね。 取り合えず設計をもう少しどうにかしろよ的な。

余談ですが、VBのラムダってFunction(it) it...みたいに冗長なのが嫌いなんですよね。 同じ意味を持つC#のそれと比較してもit => it.NameFunction(it) it.Nameの差の大きいこと。

まぁ、そもそもC#/VBののラムダの内部に可変ステートを抱えられる仕様はちょっとどうかな~と思います。 シングルスレッドならともかく、マルチスレッドでうっかり可変ステートを外部に出してしまうとスレッドセーフの要件の一つである『不変なステートは常にスレッドセーフ』が途端に崩壊してしまうのでマルチスレッドで使うときはハラハラします。

とにかく、Decimalの内部構造を取り出して桁数を解析する以来のバッドノウハウの再来ということで。

github.com

おわり

*1:そもそもYield Returnが使えない。複数行ラムダはぎりぎり使える。あっ(察し)