totoのExcel備忘録

モダンExcel、VBA、BIツール等について調べたことをメモしていきます。

CDec関数で取得したDecimal型の値が「Variant型以外の型」の変数に「代入」できてしまうという話

今まで、CDec関数の戻り値として得られる十進数型(Decimal)のデータは、VBAの標準データ型に代入できないと思っていました。

Microsoft Docsをはじめ、「Decimalデータ型はバリアント型 ( Variant) のみで使用できます 」と各所で説明されているためです。

ところが、他の記事を書いている最中、CDec関数の戻り値が他のVBAの標準データ変数に何のエラーもなく「代入」できてしまうことに気づきました。

VBAの型システムの仕様を考えたら当たり前なのですが、ちょっとだけショックを受けましたし、Microsoft のレファレンスにすら書かれていなかったので、記事にしようと思います。


この記事の内容


VBAの型システムの問題

Variant型以外のデータ型の変数は暗黙的にデータを改変して受け取る

VBAプログラミングを安全に行うためには変数のデータ型を宣言するべきだ、という意見をよく見かけます。

しかしVariant型以外のVBAのデータ型(以下「基本データ型」と書きます)は、ただデータ型のチェックを行うだけではなく、「型に合致する値として解釈できる値は暗黙的に変換して受け入れる」という性質も持っています。

例えばこんな例が該当します。

【String型変数にLong型変数の格納値を代入する場合】

Sub assign_long_2_string()

    Dim i As Long
    Dim s As String

    i = 100
    s = i   '本当はここで「13 : 型が一致しません。」 と実行時エラーを出してほしいが、受け入れてしまう。
    Debug.Print s

End Sub

出力結果:
100

String型で宣言したsに、Long型のiの値を「代入」すると、暗黙的に文字列への変換が行われ、sに値が設定されます。

他の例として、数字の文字列を数値型の変数に「代入」すると、その型の値に合った数値として解釈される、といったことが起きます。

個人的には、こういう処理が暗黙且つ簡単に行われてしまうVBAの基本データ型というのは、あまり信頼できないと感じています。

VBAにおけるDecimal型とはどのようなデータ型なのか

Single型やDouble型の十進数小数の表現力不足を補うのがDecimal型

本題に入る前に、Decimal型とはどのようなデータ型なのかについて、簡単に触れておきます。

Decimal型とは、二進数表現の浮動小数点数型(Single型やDouble型)で正確に表現できない十進数の小数を正確に表現するための数値データ型です。

二進数浮動小数点型による十進数の小数点表現は不正確です。
十進数の「0.1」を2進数に変換すると「0.0001100110011…」となり、「0011」の以下が永遠に循環します。
しかし、計算機の仕組み上、数値の表現を有効桁数内で納めなくてはならないので、近接する数字へと丸められます。
結果として「0.1」はDouble型では二進数で「0.0001100110011001100110011001100110011001100110011001101」となりますが、これは十進数にすると「0.1000000000000000055511151231257827021181583404541015625」となり、0.1そのものとは全く異なる数値になります。

試しに以下のような小数の同値比較をイミディエイトウィンドウで実行してみると、Falseが返ります。

?0.1  + 0.2 = 0.3
False

このような丸め誤差は、処理する数字の桁数が多く、丸め誤差が許容できない財務処理などでは致命的になります。

Decimal型は、この誤差をなるべく小さくするために考案されたものです。
正負の符号なしの96bit(12Byte)の仮数部(整数値)と、小数部の桁数を表す指数部(スケーリングファクター:0~28で指定)、符号部からなり、最大 29 桁の有効桁数をサポートしています。

表現できる値の範囲は、小数点ありの場合、+/-7.9228162514264337593543950335、小数点なしの場合で+/-79,228,162,514,264,337,593,543,950,335です。 0でない最小の絶対値を持つ数値は+/-0.0000000000000000000000000001です。

VBAでは、標準関数ライブラリのCDec関数を使って、Decimal型の値を取得することができます。 CDec関数で取得したDecimal型の値だけで先ほどの同値評価を行うと、数学的に正しい判定結果を得ることができます。

?CDec(0.1)  + CDec(0.2) =CDec(0.3)
True

Decimal型の値はVariant型でしか扱うことができない

Microsoft DocsのDecimal型の説明では、こんなことが書かれています。

Decimalデータ型はバリアント型 ( Variant) のみで使用できます。つまり、変数をDecimal型に宣言することはできません。 ただし、 CDec関数を使用して、サブタイプがDecimalであるバリアントを作成することはできます。

VB6、および現在のVBAでは、Variant型でしかDecimal型を扱えないようです*1。 小数の扱いが他の数値型と異なるためでしょう。 VBAの世界では、他のシステムからパラメータとして受け取るVariant型の値やCDec関数の戻り値の中身として取得するする以外、Decimal型を扱う方法がないようです。

この点、後継言語のVB.NETではDecimal型が独立した型としてサポートされているようで、やはりVBAは古い言語ということなのでしょう。

ただしこの「Variant型でしか使えない」という点、他のデータ型にそもそも代入が出来ないと思っていたのですが、それは間違いだったようです。

Decimal型の値がVariant型以外の変数に「代入」出来てしまう

実際のコードでDecimal型の値をあらゆる型の変数に代入してみる

今回の本題は、CDec関数によって作成したDecimal型のデータが、Variant型以外のVBAの基本データ型の変数に「代入」が出来てしまうよというお話です。
つまり、他の値型同様、Decimal型の値も受け手の変数にとって都合の良い型で解釈されて変数に値が格納されてしまうということです。

具体的なコード例で示してみることにします。 あらゆるデータ型の変数を用意し、CDec関数の値を「代入」して、どのようなデータ型の値として格納されているかを出力します。 型チェックによるエラーが起きた場合、エラー番号とDescriptionを出力します。

【全てVBAのデータ型に十進数型の値を代入するコード】

'それぞれの型の変数を用意し、CDec(1/3)の結果を代入して出力する
'エラー情報も取りたいので、変数への代入前にErr.Clearを毎回コールする

Sub cdec_assign_test()
    On Error Resume Next
    Debug.Print "CDec(2/3)" & "の代入結果:": Debug.Print
    
    Err.Clear: Dim byteVar As Byte: byteVar = CDec(2 / 3): Debug.Print "Byte型" & vbTab & ": [value]: " & vbTab & byteVar & vbTab & "[type name]: " & TypeName(byteVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Err.Clear: Dim boolVar As Boolean: boolVar = CDec(2 / 3): Debug.Print "Boolean型" & vbTab & ": [value]: " & vbTab & boolVar & vbTab & "[type name]: " & TypeName(boolVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Err.Clear: Dim curVar As Currency: curVar = CDec(2 / 3): Debug.Print "Currency型" & vbTab & ": [value]: " & vbTab & curVar & vbTab & "[type name]: " & TypeName(curVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Err.Clear: Dim dateVar As Date: dateVar = CDec(2 / 3): Debug.Print "Date型" & vbTab & ": [value]: " & vbTab & dateVar & vbTab & "[type name]: " & TypeName(dateVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Err.Clear: Dim dblVar As Double: dblVar = CDec(2 / 3): Debug.Print "Double型" & vbTab & ": [value]: " & vbTab & dblVar & vbTab & "[type name]: " & TypeName(dblVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Err.Clear: Dim intVar As Integer: intVar = CDec(2 / 3): Debug.Print "Integer型" & vbTab & ": [value]: " & vbTab & intVar & vbTab & "[type name]: " & TypeName(intVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Err.Clear: Dim lngVar As Long: lngVar = CDec(2 / 3): Debug.Print "Long型" & vbTab & ": [value]: " & vbTab & lngVar & vbTab & "[type name]: " & TypeName(lngVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Err.Clear: Dim lnglngVar As LongLong: lnglngVar = CDec(2 / 3): Debug.Print "LongLong型" & vbTab & ": [value]: " & vbTab & lnglngVar & vbTab & "[type name]: " & TypeName(lnglngVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Err.Clear: Dim objVar As Object: Let objVar = CDec(2 / 3): Debug.Print "Object型(Let)" & vbTab & ": [value]: " & vbTab & objVar & vbTab & "[type name]: " & TypeName(objVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
                                                        Err.Clear: Set objVar = CDec(2 / 3): Debug.Print "Object型(Set)" & vbTab & ": [value]: " & vbTab & objVar & vbTab & "[type name]: " & TypeName(objVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Err.Clear: Dim sngVar As Single: sngVar = CDec(2 / 3): Debug.Print "Single型" & vbTab & ": [value]: " & vbTab & sngVar & vbTab & "[type name]: " & TypeName(sngVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Err.Clear: Dim strVar As String: strVar = CDec(2 / 3): Debug.Print "String型" & vbTab & ": [value]: " & vbTab & strVar & vbTab & "[type name]: " & TypeName(strVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Err.Clear: Dim varVar As Variant: varVar = CDec(2 / 3): Debug.Print "Variant型" & vbTab & ": [value]: " & vbTab & varVar & vbTab & "[type name]: " & TypeName(varVar): If Err.Number <> 0 Then Debug.Print "[error number]: " & Err.Number & vbTab & Err.Description
    Debug.Print
End Sub

===========
CDec(2/3)の代入結果:

Byte型:
[value]:    1   [type name]: Byte

Boolean型:
[value]:    True    [type name]: Boolean

Currency型:
[value]:    0.6667  [type name]: Currency

Date型:
[value]:    16:00:00    [type name]: Date

Double型:
[value]:    0.666666666666667   [type name]: Double

Integer型:
[value]:    1   [type name]: Integer

Long型:
[value]:    1   [type name]: Long

LongLong型:
[value]:    1   [type name]: LongLong

Object型(Let):
[error number]: 91  オブジェクト変数または With ブロック変数が設定されていません。

Object型(Set):
[error number]: 91  オブジェクト変数または With ブロック変数が設定されていません。

Single型:
[value]:    0.6666667   [type name]: Single

String型:
[value]:    0.666666666666667   [type name]: String

Variant型:
[value]:    0.666666666666667   [type name]: Decimal

なんと、Variant型以外の型であっても、値型を受け付けないObject型以外は全て、CDec関数の戻り値を受け入れてしまいます。
しかもTypeName関数で取得する型情報は、受け入れ側の型です。

何が起きているのか

どうやら、CDec関数によって生成したDecimal型データであっても、それぞれの型が持つ変換ルールに従って数値または文字列表現として評価され、改変された値が変数に格納されるようです。 数値型やBoolean型への「代入」の場合、四捨五入による丸めと評価が行われているようですが、どのようなコンテクストで変換されているのか、正確には分かりません。

しかし、Variant型しか適合する型のない特殊な値型であるDecimal型の値まで暗黙的に変換されてしまうとは、実に恐ろしいです。
VBAにおける基本データ型は本当に要注意です。

「Decimal型はVariant型でしか使用できない」とは

もう一度Microsoft Docsを読んでみる

では、そもそも「Decimal型はVariant型でしか使用できない」とはどういうことだったのでしょう。

Microsoft DocsのDecimal型の説明をもう一度よく読んでみます。

Decimalデータはバリアント型 ( Variant) のみで使用できます。つまり、変数をDecimal型に宣言することはできません。 ただし、 CDec関数を使用して、サブタイプがDecimalであるバリアントを作成することはできます。

この「バリアント型のみで使用できます」という部分の目的語は、Decimal型の型定義そのものであるということを理解しないといけないですね。

つまり、As Decimalなどといった形で変数宣言時に型の定義を利用することができないし、DimでDecimal型に対応したサイズや構造のメモリを確保することもないということですね。

Decimal型はAs句による型指定のサポート外なので、インテリセンスで型名が表示されない
Decimal型はAs句による型指定のサポート外なので、インテリセンスで型名が表示されない

このことは、Decimal型という型情報が、バリアント型のインスタンスが内部で保持している型情報(つまりサブタイプ)としてしか存在できないという意味にもなります。 確かに先ほどのコードサンプルでも、”Decimal”という型情報を保持できていたのはVariant型変数だけでした。

ただし、CDec関数などで得られたバリアント型のDecimal型の値を他のデータ型の変数に渡そうとするとどうなるのか、ということについては明示的に触れられてはいません。 「Decimal型のデータを他の型の変数で受け取ることが出来ない」とは言っていないわけです。

Decimal型に適合する変数タイプが存在しないということなので、私はてっきり型チェックで弾かれる仕様なのかなと思っていたわけですが。。

そこはやはりVBA、CDec関数で作成したDecimal型であっても、値の受け取り手の基本データ型変数の都合に合わせて値とデータ型が改変されてしまうということのようです。


最後に

以上、Decimal型データを他の基本データ型変数で受け取った場合の暗黙の値変換についてまとめてみました。

今回の記事を通じて、VBAの奥深さを実感頂けたら嬉しいです。

本記事の内容に誤り等がありましたらご指摘いただけると助かります。

それでは、また!

本記事のまとめ

  • VBAの基本データ型の変数は、型が違うデータが代入されても、型変換して受け入れられる値であれば暗黙的に型変換して受け入れてしまう
  • Decimal型は十進数で数値を表現する型だが、VBAではVariant型の内部表現としてしか扱えない
  • VBAの基本データ型にDecimal型の値を「代入」すると、数値型の値として解釈され、変数側で受け入れ可能な型の値に変換して受け入れてしまう
  • 「Decimal型はVariant型でしか使えない」とは、Decimal型の型情報をVariant型でしか扱えないという意味であり、他の基本データ型への値の受け渡しは可能(ただし型情報は失われる)

*1:「サブタイプがDecimal」とは、Variant型が内部で認識している型の種類がDecimalということです。このVariant型の内部の型の扱いについては、別の機会にVariant型の記事を書く予定です。