none
「DBNull.Value」はなぜCStr()できない? RRS feed

  • 全般的な情報交換

  • 以下は実行できるのに、

    sStr = "NULLは" & DBNull.Value & "です"
    sStr = "NULLは" & DBNull.Value.ToString & "です"

    以下はエラーとなります。

    sStr = "NULLは" & CStr(DBNull.Value) & "です"

    同じことをやるにしてもエラーになる場合とならない場合があるのはなぜでしょう?
    なぜこのような仕様なのでしょう?
    ハッキリ言って、気持ち悪いです。
    VisualStudio2015です。

    2018年2月16日 5:34

すべての返信

  • また前のスレッド(URL 下記)のような話がしたいのでしょうか?

    IsDbNull、IsNothingを廃止し、その逆の関数を追加してほしい。
    https://social.msdn.microsoft.com/Forums/ja-JP/4dd1af56-84c8-460b-8b94-51ce0a7dc051/isdbnullisnothing?forum=vbgeneralja

    2018年2月16日 6:20
  • 「なぜでしょう?」の直接の回答としては、似た機能に見えるが、以下のとおり実装がまったく異なる、ということに尽きるかと。

    1) DBNull.Value.ToStringメソッドは、DBNullクラスに備わった機能である。

    2) CStr関数はVisual Basicに備わった機能である。



    • 編集済み 外池 2018年2月16日 6:38
    2018年2月16日 6:33
  • SurferOnWwwさん
    したくてしてるとお考えですか?

    外池さん
    DBNullクラスだってVBに備わった機能ですよね?他言語にも備わってるのでしょうけど。
    なぜ統一を図ろうという動きにならないのでしょうね?

    2018年2月16日 7:27
  • DBNullクラスは、VBに備わった機能ではなく、別物です。他言語にも備わっている、というのも違っています。

    DBNullクラスは、クラスライブラリから提供されているものです。クラスライブラリを、VBからでもC#からでも使えるようになっている、ということです。

    したがって、仕様の思想として、VBの思想、C#の思想、クラスライブラリの思想と3様なので、多少相違することが現実にあります。クラスライブラリは、いろんな提供者が有り得るので、さらに思想が不統一になり得ます。

    同じMicrosoftなら統一しろよ、というご意見は、一方で、歴史的経緯とのバランスの問題になります。

    小生も同じようなことを感じたのが、浮動小数点数の小数点以下を丸めて整数にする操作です。VBに備わるCInt関数、C#に備わる整数型へのキャスト、クラスライブラリから提供されるMath.Truncateメソッド、それぞれ実装も動作も速さも違っていて面食らいました。が、結局、歴史的経緯を引きずっているからで、自分のやりたいことに合せて選ぶしかない、と思っています。

    統一を図ることは建設的ですし、お勧めすることはご自身でクラスライブラリを設計・実装されることです。その上で、言語に備わる「気持ち悪い」仕様は使わないようにすると、スッキリするんじゃないかと。



    • 編集済み 外池 2018年2月16日 8:03
    2018年2月16日 7:31
  • 歴史的経緯ねぇ。
    CStr()も、DBNullクラスに合わせてエラーにしないようにすればいいと思ってしまうのですが、それだとどんな問題があると思いますか?
    そしてそれは、「統一されてない」という問題より、なぜ上回ると思いますか?
    2018年2月16日 8:03
  • VBのCxxx系統の関数の実装の仕組みからして、難しいんでしょうね。コンパイル時にインライン展開するそうなので、どんなクラスでも引数にとれる、とは行かないのかな?

    仕様がハッキリしていて使う側が容易に選べるのであれば、小生にとっては多少の不統一は気になりません。
    2018年2月16日 8:25
  • ありがとうございました。
    2018年2月16日 8:32
  • 実行されるコードはReference Sourceで公開されているConversions.ToString(Object)ですね。コードは次のようになっています。

    Public Shared Shadows Function ToString(ByVal Value As Object) As String
    
        If Value Is Nothing Then 
            Return Nothing
     
        Else 
            Dim StringValue As String = TryCast(Value, String)
     
            If StringValue IsNot Nothing Then
                Return StringValue
            End If
        End If 
    
        Dim ValueInterface As IConvertible 
        Dim ValueTypeCode As TypeCode 
    
        ValueInterface = TryCast(Value, IConvertible) 
    
        If Not ValueInterface Is Nothing Then
    
            ValueTypeCode = ValueInterface.GetTypeCode() 
    
            Select Case ValueTypeCode 
                Case TypeCode.Boolean 
                    Return CStr(ValueInterface.ToBoolean(Nothing))
     
                Case TypeCode.SByte
                    Return CStr(ValueInterface.ToSByte(Nothing))
    
                Case TypeCode.Byte 
                    Return CStr(ValueInterface.ToByte(Nothing))
     
                Case TypeCode.Int16 
                    Return CStr(ValueInterface.ToInt16(Nothing))
     
                Case TypeCode.UInt16
                    Return CStr(ValueInterface.ToUInt16(Nothing))
    
                Case TypeCode.Int32 
                    Return CStr(ValueInterface.ToInt32(Nothing))
     
                Case TypeCode.UInt32 
                    Return CStr(ValueInterface.ToUInt32(Nothing))
     
                Case TypeCode.Int64
                    Return CStr(ValueInterface.ToInt64(Nothing))
    
                Case TypeCode.UInt64 
                    Return CStr(ValueInterface.ToUInt64(Nothing))
     
                Case TypeCode.Decimal 
                    Return CStr(ValueInterface.ToDecimal(Nothing))
     
                Case TypeCode.Single
                    Return CStr(ValueInterface.ToSingle(Nothing))
    
                Case TypeCode.Double 
                    Return CStr(ValueInterface.ToDouble(Nothing))
     
                Case TypeCode.Char 
                    Return CStr(ValueInterface.ToChar(Nothing))
     
                Case TypeCode.DateTime
                    Return CStr(ValueInterface.ToDateTime(Nothing))
    
                Case TypeCode.String 
                    Return CStr(ValueInterface.ToString(Nothing))
     
                Case Else 
                    ' Fall through to error
            End Select 
    
        Else
            Dim CharArray As Char() = TryCast(Value, Char())
     
            If CharArray IsNot Nothing Then
                Return New String(CharArray) 
            End If 
        End If
     
        Throw New InvalidCastException(GetResourceString(ResID.InvalidCast_FromTo, VBFriendlyName(Value), "String"))
    
    End Function

    動作としては

    1. NothingであればNothingを返す
    2. Stringにキャスト可能であればキャスト結果を返す
    3. IConvertibleを実装していてBoolean、SByte、Byte、Int16、UInt16、Int32、UInt32、Int64、UInt64、Decimal、Single、Double、Char、DateTime、Stringのいずれかの型であればそれぞれの型からStringに変換してその結果を返す
    4. IConvertibleを実装しているその他の型はFall through to error
    5. IConvertibleを実装していない場合はChar配列にキャスト可能であればキャスト結果をStringとして返す
    6. 他は全部変換エラー

    のようですね。これらはVisual Basic言語仕様

    仕様の問題は dotnet/vblang リポジトリで作成する必要があります。 見つけたエラーを修正したい場合は、同じレポジトリにプル要求を送信できます。

    だそうです。気持ち悪いのであればぜひ改善を提案しましょう!

    2018年2月16日 8:35
  • > したくてしてるとお考えですか?

    前のような話になる ⇒ 質問者さんは聞く耳持たないようだ ⇒ 話をするだけ時間も無駄になりそう・・・と思って聞きました。

    どうもそうなりそうですね。

    2018年2月16日 8:46
  • 違っていたらごめんなさい。
    実行可は 文字列連結なので、
    DBNull.Value と書いても、暗黙的に .ToString が連結されて処理されるが、
    何の Object でも受け入れる CStr は .ToString を呼ばずに、生のまま処理して、
    エラーを吐く。
    そう言う事では無いのですか?。

            Dim t1 = DBNull.Value
            'Dim t2 As String = DBNull.Value    'Error
            Dim t3 As String = "t3" & DBNull.Value

    2018年2月16日 9:31
  • 回答しておきながら質問もアレですが・・・、

    佐佑理さんが紹介されているコードって、どういう位置づけのものなんでしょうか? 動作の定義を示す標準実装?
    何故尋ねるかと言うと・・・、CStr関数の動作を示すものとして読み始めて、結局CStr関数が使われているゆえ・・・。

    すいません、Reference Sourceに馴染みがないもので。

    • 編集済み 外池 2018年2月16日 9:33
    2018年2月16日 9:31
  • 何故尋ねるかと言うと・・・、CStr関数の動作を示すものとして読み始めて、結局CStr関数が使われているゆえ・・・。

    CStrの実体であるConversions.ToStringはオーバーロードされていて、switch内のCStrはそれぞれ適切なConversions.ToStringが実行されます。


    • 編集済み Hongliang 2018年2月16日 9:39
    2018年2月16日 9:39
  • 引用したものであれば"C:\Windows\Microsoft.NET\Framework\v4.0.30319\Microsoft.VisualBasic.dll"をコンパイルする際に使用されたソースコードです。細かなリビジョンは異なるでしょうが。ですので、実際に上記のコードが実行されています。

    内部でCStrを呼び出しているように見えますが、呼び出し前にIConvertibleで変換しているためConversions.ToString()の他のオーバーロードに解決されます。そのため再帰呼び出しにはなりません。

    2018年2月16日 9:44
  • それはそうと、CStr他の変換関数はVB.NETで導入されたものではなく、VBの長い歴史の中でずっと生き続けていたものを再現したものです。

    そして当時のVBを思い出してもらえればわかると思いますが、組み込み型は極少数で、残りはすべてObjectでした。そしてCStr他の変換関数は組み込み型の変換を対象としており、Objectに対してはエラーを返していたのは当時からのことです。
    # Set構文が懐かしい。

    CStrが提供されたのがどのバージョンからかは知りませんが、何を今さら、とは思います。

    2018年2月16日 9:56
  • Honaliangさん、佐祐理さん、ありがとうございます。

    VBのドキュメントには、Cxxxタイプの変換関数は、メソッド呼び出しではなくインライン展開でコンパイルされて速いかも、という趣旨の説明がありますが、CStr関数は該当しない、ということなんですね。

    あと、クラスライブラリで提供されるクラスのToString()を使うより、CStrを使うことを考えろ、その方がVBの他の仕様と調和するから、ロケールのCulture情報の反映のこともあるから、という説明もあり・・・、

    この辺りのドキュメントの説明が気持ち悪いように思えてきました。
    2018年2月16日 11:34
  • 小生が「歴史的経緯」と申し上げたのは、まさに、そのことです。.Netになる前のVBにはあったと記憶しています。VB6にはありますね。VB4やVB5はもう覚えてないですが・・・。
    2018年2月16日 11:40
  • .NET 以前の Visual Basic の実装に近づけるため、あえてそういう動作になるよう設計されたのではないでしょうか。


    '①【VBA】
    Dim v As Variant
    Dim s1 As String, s2 As String, s3 As String
    v = Null
    s1 = v    '実行時エラー 94
    s2 = v & "XYZ"    'v = "XYZ"
    s3 = CStr(v)    '実行時エラー 94
    '②【VB.NET】
    Dim v As DBNull = DBNull.Value
    Dim s1 As String = v    'コンパイルエラー BC30311
    Dim s2 As String = v & "XYZ"    's2 = "XYZ"
    Dim s3 As String = CStr(v)    'コンパイルエラー BC30311
    Dim s4 As String = v.ToString()    's4 = ""
    '③【VB.NET】
    Dim v As Object = DBNull.Value
    Dim s1 As String = v    '例外 InvalidCastException
    Dim s2 As String = v & "XYZ"    's2 = "XYZ"
    Dim s3 As String = CStr(v)    '例外 InvalidCastException
    Dim s4 As String = v.ToString()    's4 = ""

    '④【VBA】
    Dim v As Variant
    Dim s1 As String, s2 As String, s3 As String
    v = Empty
    s1 = v    's1 = ""
    s2 = v & "XYZ"    's2 = "XYZ"
    s3 = CStr(v)    's3 = ""
    '⑤【VB.NET】
    Dim v As Object = Nothing
    Dim s1 As String = v    's1 = Nothing
    Dim s2 As String = v & "XYZ"    's2 = "XYZ"
    Dim s3 As String = CStr(v)    's3 = Nothing
    Dim s4 As String = v.ToString()    '例外  NullReferenceException

    2018年2月16日 12:24
  • VB2012~VB2015 等に付属の「Visual Basic 言語仕様書 Version 11.0」を見てみると、
    連結演算子すなわち「& 演算子」に対して、下記のように記述されていました。

    言語仕様書を見る限り、Visual Basic では & 演算時に、DBNull を特別扱いしていることが分かるかと思います。


    11.16 連結演算子

    "連結演算子" は、null 許容バージョンの組み込みの値型を含む、すべての組み込みの型に対して定義されています。この演算子は上記の型および System.DBNull の間の連結に対しても定義されます。これは、Nothing 文字列として取り扱われます。連結演算子は、そのすべてのオペランドを String に変換します。式では、厳密な型指定規則が使用されているかどうかにかかわらず、String への変換はすべて拡大変換と見なされます。System.DBNull 値は、String として型指定されたリテラル Nothing に変換されます。値が Nothing の null 許容値型も、String として型指定されたリテラル Nothing に変換され、実行時エラーはスローされません。

    連結演算の結果は、2 つのオペランドを左から右の順に連結した文字列となります。値 Nothing は、空のリテラル文字列 "" として扱われます。



    ちなみに上記には、null 許容型(いわゆる Nullable 型)に関する記述もありますが、この項目は仕様書 Version 9.0 以降で追加されていました(8.0 では記載されていない)。しかし Version 8.0 当時の言語仕様書においても、DBNull に関する定義は現在同様、Nothing に対する連結と同様に扱うことになっていました。

    2018年2月16日 14:36
  • そして当時のVBを思い出してもらえればわかると思いますが、組み込み型は極少数で、残りはすべてObjectでした。

    .NET 以前の話をしているのであれば、DefObj ステートメントを使うでもしない限り、既定のデータ型は Object ではなく Variant であるはずですよ。

    16bit バージョン時代の VB まで遡ると、Boolean 型や Date 型もありませんでしたから、組み込み型は確かに VB6 や VBA7.1 よりも少ないですね。Variant 型がサポートしている内部処理形式の型も今より少なかったです。オブジェクト型の扱いも微妙に異なっていましたし。

    →追記:Boolean 型や Date 型は、VB4 からの機能です。(VB4 には32bit版と16bit版があります)

    CStrが提供されたのがどのバージョンからかは知りませんが

    VB2 の時には CStr は存在していたと記憶しています。
    Variant は VB2 で追加された型らしいですが未確認。

    →追記:CStr 関数および Variant 型は、いずれも VB1 には存在していませんでした。これらは VB2 からの機能ですね。

    ただ、VB2 当時から Null 値、Empty 値、Error 値などは存在しており、これらは Variant 型でしか扱えない値として定義されていました。

    →追記:間違い。CVErr 関数やバリアントの Error 型は VB3 以下では使えません。これらは VB4 からの機能です。



    • 編集済み 魔界の仮面弁士MVP 2018年2月17日 12:13 Error 型をサポートしているバージョンが間違っていた
    2018年2月16日 18:03
  • 歴史的経緯とか言われちゃうと納得するしかないんですが、本当の意味での納得ではないですねぇ。
    そもそも&演算子はよくて、CStrで囲んだとたんにエラーになる、というのが解せん。
    もともと問題なく動作していたのが、関数化してCStrをかました途端にエラーになった。
    なんでだろう?と原因を探っていく中で気づいたわけですが、そういう経緯のためにどうしても気持ち悪さを強く感じてしまう。

    いっけんうまくいきそうで実はいかなかったり、似たような方法なのにかたや動作してかたや動作しなかったり…
    こういうのって「クセのある言語」なんですよね。
    手を加えたことで今回のような地雷を他にも埋め込んでないか不安になってしまう。

    2018年2月17日 0:15
  • 小生の経験則ですが、
    ーイロイロ他にやることがあるとき、ちゃんと動いている箇所に、手を加えるべきでない。
    ー時間があるから整理したい(リファクタリングしたい)なら、ボトルネック等の改善要素がある箇所からやるべき。
    ーなんにせよ、仕様や実装(動作)の由来がどこにあるか、理解した上で手を加えるべき。
    ー1箇所書き換えるごとにテストするぐらいの慎重さも必要。


    • 編集済み 外池 2018年2月17日 2:15
    2018年2月17日 1:41
  • .NET 以前の話をしているのであれば、DefObj ステートメントを使うでもしない限り、既定のデータ型は Object ではなく Variant であるはずですよ。
    Variantは組み込み型ですよね? CVar変換関数もありますし。リンク先でも「オブジェクト」と表現され、Data TypesにもThe Variant Data Typeとは別にThe Object Data Typeが紹介されています。この後者を指して表現しました。
    2018年2月17日 2:19
  • Nothing と DBNull を混同しておられるのではないでしょうか?

    大前提として、NULL とは何らかの値を意味している訳ではありません。概念上は「そもそも値が存在しない」ことを示しています。変換すべき値が存在していない状態なのですから、他の型に直接変換することができない道理です。その意味において、CStr に渡そうとしているのが、そもそも手順として間違っていることになります。

    データベースとしての NULL の意味についての解説は、他の記事に任せるとして、.NET Framework における DBNull もそれにならい、他の型に直接変換できないよう設計されています。ですから VB.NET においても、CStr(DBNull.Value) や CInt(DBNull.Value) はエラーとなるようになっています。ただし CStr(Nothing) や CInt(Nothing) であれば問題ありません。

    以下、DBNull クラスの解説から引用:

    DBNull クラスは存在しない値を表します。たとえば、データベースで、テーブルの行の中の列にデータが含まれていない場合があります。その列は単に値を持っていないのではなく、存在しないと見なされます。DBNull オブジェクトは存在しない列を表します。さらに、COM 相互運用機能では、DBNull クラスを使用して、VT_NULL バリアント (存在しない値を示す) と VT_EMPTY バリアント (指定されていない値を示す) を区別します。
    オブジェクト指向プログラミング言語の Nothing の概念と DBNull オブジェクトを混同しないでください。オブジェクト指向プログラミング言語では、Nothing はオブジェクトへの参照がないことを示します。DBNull は、初期化前の状態に戻されたバリアントまたは存在しないデータベース列を表します。

    DBNull の解説にはバリアントの話が掲載されているので、VBA の Variant 型で扱える Null キーワードについても確認してみましたが、こちらも同様の注意書きがありますね。

    キーワード Null は、有効な値が変数に格納されていないことを示します。キーワード Empty とは異なります。

    VBA においても、CStr(Null) や CInt(Null) はエラーとなる仕様ですが、 CStr(Empty) や CInt(Empty) であれば問題ありません。

    2018年2月17日 3:09
  • 佐祐理さんが書かれていた、VBの長い歴史における「当時のVB」がどのバージョンを指しているのか分からなかったのですが、下記はひとまず VB2 を基準について述べてみます。

    VB2 当時で言うと、CVar 関数はありましたが、Object というデータ型は存在していません。そのため『残りはすべてObjectでした。』という表現に違和感を覚えたのが先の投稿を書いた理由です。OOP としての「オブジェクト」は存在していましたが、データ型としての「Object」は、Variant 型の内部処理形式としてさえ存在していません。一応、Set ステートメントはあったのですが、これについては最後に述べます。

    歴史を紐解くため、古い環境を引っ張り出してみたところ、VB2 当時の組み込みデータ型は、Integer / Long / Single / Double / Currency / String / String * n / Variant しか無いことを確認できました。(この他にユーザー定義型がありますが、これは組み込みの型ではないので除外します。)
    なお、Variant 型 / CStr 関数 / CVDate 関数 / CVar 関数が追加されたのも VB2 からです。VB1 に CStr 関数が無かったのは私としても意外でしたが。

    さておき、VB2~VB3 当時、Variant 型の内部処理形式としてサポートされていたのは、Empty / Null / Integer / Long / Single / Double / Currency / Date / String の 9 種類です。Date はまだ Variant でしか扱えませんでしたし、Variant に配列やオブジェクトやバイナリを代入することさえできませんでした。

    Object 型を扱えるようになったのは VB4 になってからです。この頃になると、Variant 型にも大幅な回収が行われ、VarType 関数が返す値に vbVariant / vbDataObject / vbObject / vbError / vbBoolean / vbByte / vbArray が追加されるようになっています。組み込み型として、Date / Byte / Boolean / Object 型も追加され、クラスを自作することも可能となりました。佐祐理さんが書かれていた「当時のVB」というのは、恐らくはVB4~VB6 世代のことだったのかな、と思います。

    話を VB2 世代に戻すと、Object 型はないものの、As Form や As Form1 や As Control や As ListBox といった表現は可能であり、以下のような使い方ができました。といっても、Set ステートメントがサポートしているのは、フォームとコントロールに限定されていたため、実際に使われることは稀でした。せいぜい、MDI フォーム アプリケーションを作成する際ぐらいだったと認識しています。(Err も当時は As ErrObject なオブジェクトではなく、Err 関数でしたね)

    Dim f As Form2  'あるいは Dim f As Form
    Dim b As CommandButton
    Set f = New Form2   'フォームのインスタンス化は VB2 からの新機能
    f.Caption = "New Form"
    f!Text1.Text = "New Text"
    'Set b = f!Command1  'コンパイルエラー(As Control になら Set 可能)
    'Set b = f.Command2  'コンパイルエラー(As Control になら Set 可能)
    Set b = Me!Command1  'これはOK(この場合は自フォームのボタンを指す)
    b.Caption = Time$
    f.Show 1    'vbModal 定数はまだ存在しない(Global Const MODAL = 1 と自前で宣言するのが一般的)
    2018年2月17日 13:26
  • スレッドの趣旨を無視した反論のための反論はやめてください。本スレッドはCStr変換関数がエラーを返すこと、文字列化の処理に一貫性が見られないことを議論しています。その上で私はCStr変換関数の歴史的な挙動について掘り下げています。

    また式のデータ型と変数のデータ型とについて混乱が見受けられます。変数や変数の既定のデータ型については誰も議論していません。CStr変換関数が引数に与えられた式によってどのような挙動を示すかです。つまり「Dim f As Form2」はどうでもよくむしろ「CStr(f)」の結果を議論してください。

    2018年2月17日 14:29
  • 旧言語の話ばかり書いても仕方ないので、VB.NET の話に戻します。

    ハッキリ言って、気持ち悪いです。

    DBNull に扱いにくさがあるという点は、私も感じています。

    しかし DBNull を他の型に変換できず、型変換関数に渡すこともできないという点については、そもそもの仕様として「そういうもの」として設計されているわけですから、そこはまずは受け入れるしか無いと思います。

    Dim x As Object = DBNull.Value
    Dim a As Boolean = True
    Dim b As Boolean = False
    Dim c As Boolean = DBNull.Value    'これは不可
    Dim d As Boolean = CBool(x)        'これは不可
    

    なぜこのような仕様なのでしょう?

    何故そうなっているのかという点については、別の投稿にて既に述べさせて頂きましたが、値が無いことを示す物であることから、あえてそういう実装になっているのでしょうね。互換性の問題もありますし、それ自体は間違っているとは思いません。

    ただ自分の場合は、DBNull を極力使わないようにしています。

    DBNull を使うのは、主に ADO.NET 利用時かと思いますが、自分のところでは DataSet を通じて利用するのではなく、nuget から Dapper を参照設定に追加し、任意クラスのコレクションとして受け取る機会が増えてきました。これなら DBNull を使わずに NULL データを表現することもできるので重宝しています。(ただし Dapper を使うなら、VB ではなく C# を採用した方が書きやすいと思いますので、ここでは具体的な使い方は触れずにおきます。)

    そしてデータベースとしての NULL は、DBNull で表現するのではなく、Nullable 型 (null 許容値型) を活用して Nothing で表現するようにすることで、扱いが楽になるかと思います。

    Dim x As Object = Nothing    'DBNull ではなく Nothing を使って表現する
    Dim a As Boolean? = True     '
    Dim b As Boolean? = False
    Dim c As Boolean? = Nothing  'これは OK
    Dim d As Boolean = CBool(x)  'これは False と同義
    

    Nullable 型に対して演算処理を行った場合、中身が Nothing だと演算結果も Nothing のままです。この点はデータベースでの NULL 演算に近いので、DBNull よりもより直観的に処理できるかと思います。

    Dim i1 As Integer? = 5
    Dim i2 As Integer? = i1 + 10    '結果は 15
    Dim i3 As Integer? = Nothing
    Dim i4 As Integer? = i3 + 10    '結果は Nothing
    Dim i5 As Integer = Nothing
    Dim i6 As Integer? = i5 + 10    '結果は 10 (i5 は Nullable ではないため)
    
    Dim b As Boolean? = Nothing
    If b Then
        MsgBox(1)
    Else
        MsgBox(2)   'ここが呼ばれる
    End If
    
    If Not b Then
        MsgBox(3)
    Else
        MsgBox(4)   'ここが呼ばれる
    End If

    そもそも ADO.NET が実装されたのは、最初の .NET Framework 1.0 の頃ですし、私としては、古い設計であるように感じています。

    VB2005 で Nullable(Of Integer) がサポートされ、VB2008 で Integer? と表記できるようになったのですから、DBNull の代わりに Nothing を利用できるよう、ADO.NET でも null 許容型をサポートして欲しいと思ったのですが、残念ながら現在においても
     dataTable1.Columns.Add("ID", GetType(Integer?))
    などという使い方はできない仕様になっています。

    その代わり、null 許容型 で受け取ることが可能な Filed(Of ) が用意されています。既にご存知かもしれませんが。

    Dim s As String = row.Filed(Of String)("EMail")       'DBNull の時には Nothing が返される
    Dim d1 As Decimal? = row.Field(Of Decimal?)("Bonus")  'DBNull の時は Nothing が返される
    Dim d2 As Decimal = row.Field(Of Decimal)("Bonus") 'DBNull の時は例外 InvalidCastException

    null 許容型であれば、NULL データは DBNull ではなく Nothing として返却されますので、CStr(v) や CInt(v) による型変換でもエラーにはならないというメリットがあります。

    また、Nothing として得られることから、If 演算子 Null 条件演算子 が利用可能になる点も便利かと。

    Dim s As String = "メールアドレスは" & If(strEmail, "<未設定>") & "です"

    なお上記サンプルで用いた If 演算子は、Access VBA の Nz 関数/Oracle の NVL 関数/SQL Server の IsNull 関数に相当する機能ですね。Access における Nz 関数の実装をまねて、VB.NET で DBNull 対応の Nz や、VB6 で Null 対応の Nz を自作した実装例が幾つかあるので、紹介させていただきます。

    C# と VB.NET の質問掲示板:プロシージャの戻り値がnullの時
    VB2005- Wiki : Nz 関数
    VB初心者友の会:Nz関数に変わるものは?

    2018年2月17日 16:08
  • 私が述べたいのは、その「歴史」において:

    • Null キーワードは VB2 から登場したものである。
    • Null は「値が存在していない」事を示しており、初期値(Empty)とは異なる存在として作られた。
    • CStr も VB2 時点で登場したが、VB1 であれ VB6 であれ、Null は String 型に代入できず、CStr 関数での変換もできない。
    • そもそも Null は、Variant 型でしか扱えないので、CInt 等も含めて一切の型変換を受け付けない。

    という点です。変換すべき値が存在していないことを示すため、あえて型変換を失敗させる実装になっているものと認識しています。

    そして Null キーワードの性質として、他の型に変換できない仕様になっている以上、CStr 関数が失敗するのも同じ理由である、というのが私の考えです。

    その上で私はCStr変換関数の歴史的な挙動について掘り下げています。

    それは分かりますし、佐祐理さんが CStr 関数の歴史を掘り下げることを否定するわけでも無いのですが、オブジェクトに対してエラーを返さないパターンもあったわけで、私の認識とは若干異なっているように感じています。

    'VB2
    Sub Command1_Click ()
        MsgBox CStr(Me!Command1)    'これはエラーにならない
       'MsgBox VarType(Me!Command1) '「型が一致しません」
    End Sub

    とはいえそれは、佐祐理さんが仰っていた「当時」がどの時点の話なのかとか、どこまでを「組み込み型」として扱うのか、といった点で相違が生じているだけのことだと思っています。

    それゆえ私は私の視点で、Null や CStr が初登場したVB2 当時からの話を述べさせていただいたわけですが、そこに反論や否定という意図はあったわけではありません。違和感の理由を確認したいという思いならばありましたが。

    むしろ「CStr(f)」の結果を議論してください。

    私としては、型変換できないものを CStr(f) することがそもそもの間違いだと考えているので、CStr に着目して議論することには、さほど価値を感じていません。

    むしろ視点を変えて、NULL 値 の意味や歴史について考える事で、元質問者の「なぜ &演算子はよくて」や「なぜこのような仕様なのでしょう」への回答にも辿り着けるものと思っています。別の投稿で Nullable を紹介しているのも、その思いからです。

    そんなことを思いながら Web 上を彷徨っていたところ、このようなページを見つけました。既定値が規定値になっている点は見なかった事にしよう。

    Nothing と DBNull

    2018年2月17日 18:26
  • sStr = "NULLは" & DBNull.Value & "です"

    別の投稿で、仕様書を引用しつつ述べさせていただきましたが、上記は
     sStr = "NULLは" & Nothing & "です"
    に相当する処理として扱われる仕様です。その結果、この代入式の右辺は
     sStr = "NULLはです。"
    へと解釈されることになっています。

    ついでに言えば、VB2 時代の「& 演算子」のヘルプを見ても、一方が Null 値だった場合には空文字列として連結する仕様であることが明記されていました。ですから歴史的経緯からみても、VB.NET の & 演算子の動作は、今の仕様であるべきでしょう。

    とはいえ、NULL 値を文字列連結したときの動作というのは処理系によっても変わるものであり、上記はあくまで「Visual Basic の文字列連結演算子がそういう言語仕様になっている」だけに過ぎません。
    たとえば MySQL においては、一つでも NULL 値が混じっていると、文字列結合した結果が NULL になってしまう仕様ですし、JScript においては、VARIANT NULL と VARIANT STRING を連結する場合に、NULL 値を "null" という文字列に置換してから連結するようになっています。

    ついでに、& 演算子だけでなく + 演算子による連結も見てみましょう。

    a = 123 + DBNull.Value + "456"   'コンパイルエラー
    b = "123" + DBNull.Value + 789   '912 となる

    前者は、Int32 + DBNull の演算が定義されていないため、左側の + が加算演算として処理されてエラーとなります。
    後者は、左側の + が文字列連結として処理され、結果として b = CDbl("123" & Nothing) + 789 と扱われるためです。

    ちなみに同じ処理を C# で実行した場合、前者は同様の理由でコンパイルエラーとなりますが、後者は "123789" になる仕様です。

    また、上記相当の処理を VB2 や VBA で実行すると、.NET の時とは若干異なる結果になります。.NET 以前の VB では、Null 値との + 演算が定義されており、その結果が Null となる仕様だからです。

    Dim n As Variant : n = Null
    a = 123 + n + "456" '結果は Null になる b = "123" + n + 789 '結果は Null になる

    何故、旧 VB の仕様を引き継がなかったのか疑問は残りますが、上記の 旧 VB の動作は、VB.NET では DBNull ではなく、Nullable(Of ) 型へと引き継がれています。

    Dim n As Integer? = Nothing
    Dim a = 123 + n + "456"   '結果は Double? 型の Nothing になる
    Dim b = "123" + n + 789   '結果は Double? 型の Nothing になる

    ただし、nullable な数値型以外の Nothing の場合は、別の動作となります。たとえば上記の n を String 型にした場合、結果は Nothing になりません。これは、+ 演算子の仕様が「一方が数値なら Double での加算、両方が文字列なら & 演算」という仕様だからです。

    Dim n As String = Nothing
    Dim a = 123 + n + "456"   '結果は Double 型の 579.0 になる
    Dim b = "123" + n + 789   '結果は Double 型の 912.0 になる
    


    2018年2月17日 20:10
  • sStr = "NULLは" & DBNull.Value.ToString & "です"

    ToString メソッドが返却する値は String 型ですので、これが成功する理由は自明ですね。
    ちなみに DBNull の ToString メソッドは、常に String.Empty を返す仕様 です。

    ただし、なぜ String.Empty を返す仕様となったのかという経緯までは私には分かりません。とはいえ、空文字列で困る理由も思いつかないので、これについては特に気持ち悪さは感じませんでした。

    sStr = "NULLは" & CStr(DBNull.Value) & "です"

    問題はこれですよね。上記はコンパイルエラーになるでしょうし、CStr に渡す値が DBNull 型変数ではなく、DBNull を代入した Object 型変数であれば、実行時に InvalidCastException がスローされることになるはずです。

    これについて私自身は、「DBNull は他の型への直接変換が禁止されている仕様である」という見解を持っています。

    その理由については他の投稿で述べていますが、少なくとも VB2 当時にそういう意図で設計されたものであったようですから、この動作が歴史的な経緯でそうなっている説を私は否定しません。そして、変換できないとされているものを CStr に渡したことが、そもそもの間違いであったのだと考えています。

    私自身、VB2 の頃から Visual Basic を利用してきたことで、旧 VB における『Null の使い方が不正です』や『型が一致しません』といったエラーに慣れてしまい、変換できないのが当たり前だと刷り込まれてしまっただけなのかもしれないのですが、いずれにせよ現状の VB.NET 実装として CStr だけが特別なわけではなく、CInt であろうと CBool であろうと、すべての CType 処理が失敗するようになっているわけですし、「DBNull を他の型にキャストできない」という動作は、 Visual Basic に限定した話ではなく、.NET 対応言語においても言えることなので、とりたてて CStr の動作について気持ち悪さを覚えることは無かったです。元質問者さんはそうではなかったようですが。

    その一方で、そもそも NULL 値の表現に DBNull を使うことが望ましいのか否か、と考えることはあります。

    データベースアクセスに用いられる OLE DB や ODBC との互換性を考えると、.NET 1.0 と共にリリースされた最初の ADO.NET バージョンの実装時、DBNull の実装を、旧 VB の Null (すなわち VARIANT NULL)と相互運用可能な形としたことは理にかなっていたと思います。

    ですがその相互運用ゆえに、事前に NULL 判定(IsDBNull や型付DataSet における IsHogeNull メソッドなど)をしておかないと変換に失敗する可能性があるという使いにくさも、そのまま引き継いでしまっているように見えます。歴史的な経緯で仕方なかったにしても、もう少し手厚いサポートがあれば良いのになぁ…と思ったこともしばしば。(たとえば Oracle の NVL などのような、DBNull 向けの関数を標準的に用意しれもらうだけでも。)

    で、現在においては、その解決策の一つが、CLR2 以降の Nullable(Of ) なのかな、と思ってみたり。
    今更 DataSet を亡き者にする訳にもいかないので、DBNull とは引き続き付き合っていかねばならない点は変わらないですし、逆に Nullable を使いにくいと思う人も居るかもしれませんけれど。

    2018年2月17日 21:32
  • 詭弁であることを自覚してください。

    CStrについて掘り下げている投稿への返信としながらCStrの動作には一切触れず、スレッドで誰一人として話題にしていない変数の既定のデータ型について議論を始めたり、毎回毎回「残りはすべてObjectでした」を引用しておきながらObject導入前のVB2を対象に議論を展開したり、私の言及していないNull値について議論を始めたり、しているわけです。

    私の投稿への返信とする必要性が全くないにも関わらず執拗に返信を続け、話題を脱線し続け、そこまでしておいて「私は私の視点で、Null や CStr が初登場したVB2 当時からの話を述べさせていただいたわけですが、そこに反論や否定という意図はあったわけではありません。」という発言は全く理解できません。

    2018年2月17日 22:42
  • CStr 関数と似た動作をする変換関数として、Str 関数というものがあります。こちらの動作も調べてみたところ、バージョンごとに仕様変化がみられました。

    • VB1.0(DOS/WIN):Str 関数、Str$ 関数いずれも存在しない。
    • VB2.0VB3.0:Str(Null) および Str$(Null) は共にエラー。
    • VB4.0(16bit/32bit)~VB6.0:Str(Null) は Null 値を返す。Str$(Null) はエラーとなる。
    • VBScript(7.8以下):Str 関数、Str$ 関数いずれも存在しない。
    • VBA(7.1以下):VB6 と同じ実装。
    • VB.NET 2002(7.0)~VB.NET 2003(7.1):手元に環境が無く未調査。
    • VB2005(8.0)~VB2017(15.0):Str(DBNull.Value) は、文字列値 "Null" を返す。Str$ は Str と同じ関数を指す。

    ★以下私見★

    .NET 版の Str の動作と、それ以前のバージョンの Str の動作が異なっていますが、元になった動作は VB4~VB6 時代の実装を基準として改修されたものであるようにに思います。

    VB.NET 版の実装が DBNull 値ではなく "Null" 値を返すよう変更された理由は、Str 関数の戻り値が As String になっているためでしょう(VB6 までのバージョンでは、Str はバリアント型、Str$ は文字列型を返す関数として定義されています)。

    .NET で $ 系関数が廃止されるということは、.NET 1.0 ベータ 2 (2001年後半)の段階からアナウンスされていたと記憶しています。もしも .NET になっても Str$ 関数を残すという選択肢がとられていたのであれば、もしかしたら Str 関数は Object 型を戻り値とするようになり、DBNull.Value が返却されるように実装されていたのかもしれませんが、実際には Str$ はなくなり、Str に一本化されました。

    あるいは .NET 化に伴い、Str を廃止することも出来なくはなかったのかも知れません(当時、VBScript には Str 関数が搭載されていませんでした)。しかし、多数の旧 VB6 のプログラムからの移植性を考え、機能としては廃止せずに残すという選択肢がとられたのだと推察します。Str 関数という機能を残すにあたり、NULL 値を許容するという動作はそのまま残されたようです。

    この関数が、Nothing でも String.Empty でも "DBNull" でもなく、"Null" を返すよう実装された理由ですが、これは .NET 以前の VB において、Str(Null) を Debug.Pring した場合に、「Null」と出力されることを踏襲したためだと思います。VBA の Print # ステートメントで Null 値を出力する場合にも「Null」という文字列になりますし、VB.NET で PrintLine(1, DBNull.Value) を実行したときの「Null」という文字列になりますから、Str(Null) も文字列「Null」を返す方が都合が良かったのでしょう。

    2018年2月19日 5:53