質問者
「DBNull.Value」はなぜCStr()できない?

全般的な情報交換
-
以下は実行できるのに、
sStr = "NULLは" & DBNull.Value & "です"
sStr = "NULLは" & DBNull.Value.ToString & "です"以下はエラーとなります。
sStr = "NULLは" & CStr(DBNull.Value) & "です"
同じことをやるにしてもエラーになる場合とならない場合があるのはなぜでしょう?
なぜこのような仕様なのでしょう?
ハッキリ言って、気持ち悪いです。
VisualStudio2015です。- 種類を変更済み 立花楓Microsoft employee, Moderator 2018年2月16日 8:14 質問ではないため
すべての返信
-
また前のスレッド(URL 下記)のような話がしたいのでしょうか?
IsDbNull、IsNothingを廃止し、その逆の関数を追加してほしい。
https://social.msdn.microsoft.com/Forums/ja-JP/4dd1af56-84c8-460b-8b94-51ce0a7dc051/isdbnullisnothing?forum=vbgeneralja -
DBNullクラスは、VBに備わった機能ではなく、別物です。他言語にも備わっている、というのも違っています。
DBNullクラスは、クラスライブラリから提供されているものです。クラスライブラリを、VBからでもC#からでも使えるようになっている、ということです。
したがって、仕様の思想として、VBの思想、C#の思想、クラスライブラリの思想と3様なので、多少相違することが現実にあります。クラスライブラリは、いろんな提供者が有り得るので、さらに思想が不統一になり得ます。
統一を図ることは建設的ですし、お勧めすることはご自身でクラスライブラリを設計・実装されることです。その上で、言語に備わる「気持ち悪い」仕様は使わないようにすると、スッキリするんじゃないかと。
同じMicrosoftなら統一しろよ、というご意見は、一方で、歴史的経緯とのバランスの問題になります。
小生も同じようなことを感じたのが、浮動小数点数の小数点以下を丸めて整数にする操作です。VBに備わるCInt関数、C#に備わる整数型へのキャスト、クラスライブラリから提供されるMath.Truncateメソッド、それぞれ実装も動作も速さも違っていて面食らいました。が、結局、歴史的経緯を引きずっているからで、自分のやりたいことに合せて選ぶしかない、と思っています。
- 編集済み 外池 2018年2月16日 8:03
-
実行されるコードは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
動作としては
- NothingであればNothingを返す
- Stringにキャスト可能であればキャスト結果を返す
- IConvertibleを実装していてBoolean、SByte、Byte、Int16、UInt16、Int32、UInt32、Int64、UInt64、Decimal、Single、Double、Char、DateTime、Stringのいずれかの型であればそれぞれの型からStringに変換してその結果を返す
- IConvertibleを実装しているその他の型はFall through to error
- IConvertibleを実装していない場合はChar配列にキャスト可能であればキャスト結果をStringとして返す
- 他は全部変換エラー
のようですね。これらはVisual Basic言語仕様で
仕様の問題は dotnet/vblang リポジトリで作成する必要があります。 見つけたエラーを修正したい場合は、同じレポジトリにプル要求を送信できます。
だそうです。気持ち悪いのであればぜひ改善を提案しましょう!
-
違っていたらごめんなさい。
実行可は 文字列連結なので、
DBNull.Value と書いても、暗黙的に .ToString が連結されて処理されるが、
何の Object でも受け入れる CStr は .ToString を呼ばずに、生のまま処理して、
エラーを吐く。
そう言う事では無いのですか?。
Dim t1 = DBNull.Value 'Dim t2 As String = DBNull.Value 'Error Dim t3 As String = "t3" & DBNull.Value
- 編集済み ShiroYuki_Mot 2018年2月16日 9:48 Code 追加
-
引用したものであれば"C:\Windows\Microsoft.NET\Framework\v4.0.30319\Microsoft.VisualBasic.dll"をコンパイルする際に使用されたソースコードです。細かなリビジョンは異なるでしょうが。ですので、実際に上記のコードが実行されています。
内部でCStrを呼び出しているように見えますが、呼び出し前にIConvertibleで変換しているためConversions.ToString()の他のオーバーロードに解決されます。そのため再帰呼び出しにはなりません。
-
それはそうと、CStr他の変換関数はVB.NETで導入されたものではなく、VBの長い歴史の中でずっと生き続けていたものを再現したものです。
そして当時のVBを思い出してもらえればわかると思いますが、組み込み型は極少数で、残りはすべてObjectでした。そしてCStr他の変換関数は組み込み型の変換を対象としており、Objectに対してはエラーを返していたのは当時からのことです。
# Set構文が懐かしい。CStrが提供されたのがどのバージョンからかは知りませんが、何を今さら、とは思います。
-
-
.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
- 編集済み 魔界の仮面弁士MVP 2018年2月16日 12:26
-
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 に対する連結と同様に扱うことになっていました。
-
そして当時のVBを思い出してもらえればわかると思いますが、組み込み型は極少数で、残りはすべてObjectでした。
.NET 以前の話をしているのであれば、DefObj ステートメントを使うでもしない限り、既定のデータ型は Object ではなく Variant であるはずですよ。
16bit バージョン時代の VB まで遡ると、Boolean 型や Date 型もありませんでしたから、組み込み型は確かに VB6 や VBA7.1 よりも少ないですね。Variant 型がサポートしている内部処理形式の型も今より少なかったです。オブジェクト型の扱いも微妙に異なっていましたし。
→追記:Boolean 型や Date 型は、VB4 からの機能です。(VB4 には32bit版と16bit版があります)
CStrが提供されたのがどのバージョンからかは知りませんが
Variant は VB2 で追加された型らしいですが未確認。→追記:CStr 関数および Variant 型は、いずれも VB1 には存在していませんでした。これらは VB2 からの機能ですね。
ただ、VB2 当時から Null 値、Empty 値、Error 値などは存在しており、これらは Variant 型でしか扱えない値として定義されていました。
→追記:間違い。CVErr 関数やバリアントの Error 型は VB3 以下では使えません。これらは VB4 からの機能です。
- 編集済み 魔界の仮面弁士MVP 2018年2月17日 12:13 Error 型をサポートしているバージョンが間違っていた
-
歴史的経緯とか言われちゃうと納得するしかないんですが、本当の意味での納得ではないですねぇ。
そもそも&演算子はよくて、CStrで囲んだとたんにエラーになる、というのが解せん。
もともと問題なく動作していたのが、関数化してCStrをかました途端にエラーになった。
なんでだろう?と原因を探っていく中で気づいたわけですが、そういう経緯のためにどうしても気持ち悪さを強く感じてしまう。いっけんうまくいきそうで実はいかなかったり、似たような方法なのにかたや動作してかたや動作しなかったり…
こういうのって「クセのある言語」なんですよね。
手を加えたことで今回のような地雷を他にも埋め込んでないか不安になってしまう。 -
.NET 以前の話をしているのであれば、DefObj ステートメントを使うでもしない限り、既定のデータ型は Object ではなく Variant であるはずですよ。
Variantは組み込み型ですよね? CVar変換関数もありますし。リンク先でも「オブジェクト」と表現され、Data TypesにもThe Variant Data Typeとは別にThe Object Data Typeが紹介されています。この後者を指して表現しました。 -
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) であれば問題ありません。
-
佐祐理さんが書かれていた、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 と自前で宣言するのが一般的)
- 編集済み 魔界の仮面弁士MVP 2018年2月17日 13:42 Set ステートメントについて記載
-
-
旧言語の話ばかり書いても仕方ないので、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 の時は例外 InvalidCastExceptionnull 許容型であれば、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関数に変わるものは? -
私が述べたいのは、その「歴史」において:
- 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 上を彷徨っていたところ、このようなページを見つけました。既定値が規定値になっている点は見なかった事にしよう。
-
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 になる
-
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 を使いにくいと思う人も居るかもしれませんけれど。 -
詭弁であることを自覚してください。
CStrについて掘り下げている投稿への返信としながらCStrの動作には一切触れず、スレッドで誰一人として話題にしていない変数の既定のデータ型について議論を始めたり、毎回毎回「残りはすべてObjectでした」を引用しておきながらObject導入前のVB2を対象に議論を展開したり、私の言及していないNull値について議論を始めたり、しているわけです。
私の投稿への返信とする必要性が全くないにも関わらず執拗に返信を続け、話題を脱線し続け、そこまでしておいて「私は私の視点で、Null や CStr が初登場したVB2 当時からの話を述べさせていただいたわけですが、そこに反論や否定という意図はあったわけではありません。」という発言は全く理解できません。
-
CStr 関数と似た動作をする変換関数として、Str 関数というものがあります。こちらの動作も調べてみたところ、バージョンごとに仕様変化がみられました。
- VB1.0(DOS/WIN):Str 関数、Str$ 関数いずれも存在しない。
- VB2.0~VB3.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」を返す方が都合が良かったのでしょう。