VBAで動的配列が割り当て済みかどうかを調べる方法として、
「Sgn関数を使う」
という方法をネット上でちらほら見かけますが、
Null判定にSgn関数を使ってはいけません。
'↓↓↓こういうことをやってはいけない!
If Sgn(配列変数) = 0 Then
'Nullである
End If
If Sgn(配列変数) <> 0 Then
'Nullではない
End If
ちょっと試す分には、上記通りに動くので気づきにくいですが、
「Nullではない」場合にも、戻り値としてゼロが返ってくることがあります。
数年前、これでどハマリして、
「同じ入力条件なのに、なぜかたまに結果が変わる」
という事象が発生して、原因究明にかなり苦労しました。
そもそも、Sgn関数は、引数で渡した数字の正負を判定して返す関数です。
numberの値 | 戻り値 |
---|---|
number > 0 | 1 |
number = 0 | 0 |
number < 0 | -1 |
MSDNにも、「引数には数値式を設定してね」と書いてあります。
数値の符号を示す、サブタイプが整数型 ( Integer ) であるバリアント型 ( Variant ) の値を返します。
構文
Sgn(number)
必須の 引数number には、任意の有効な 数値式を指定できます。
このSgn関数に数式以外、たとえば配列変数を渡すと、
こういう結果が返ってきます。
Sub Test()
Dim v_sList(3) As Variant'静的配列
Dim v_dList() As Variant'動的配列
Dim ret As Integer
Debug.Print "【静的配列】"
ret = Sgn(v_sList)
Debug.Print "Sgn -> " & ret
Debug.Print "【動的配列(割り当て前)】"
ret = Sgn(v_dList)
Debug.Print "Sgn -> " & ret
Debug.Print "【動的配列(割り当て後)】"
ReDim v_dList(3)
ret = Sgn(v_dList)
Debug.Print "Sgn -> " & ret
End Sub
---------- 結果 ----------
【静的配列】
Sgn -> -8768
【動的配列(割り当て前)】
Sgn -> 0
【動的配列(割り当て後)】
Sgn -> -32640
MSDNの仕様では、0, -1, 1のいずれかしか返されないことになっているけれど、
静的配列や、割り当て後の動的配列の場合には変な値が返ってくる。
たぶん、配列の割り当てメモリアドレスとSgn関数の戻り値が関連しているのかな、と思います。
以下、配列のアドレス値とSgn結果。
Private Declare Sub GetMem4 Lib "msvbvm60" (ByRef Addr() As Any, RetVal As Long)
Sub MemoryTest()
Dim v_sList(3) As Variant'静的配列
Dim v_dList() As Variant'動的配列
Dim retAddr As Long
Dim ret As Integer
Debug.Print "【静的配列】"
GetMem4 v_sList, retAddr
Debug.Print "Addr -> " & retAddr
ret = Sgn(v_sList)
Debug.Print "Sgn -> " & ret
Debug.Print "【静的配列(値設定後)】"
v_sList(0) = 123
GetMem4 v_sList, retAddr
Debug.Print "Addr -> " & retAddr
ret = Sgn(v_sList)
Debug.Print "Sgn -> " & ret
Debug.Print "【動的配列(割り当て前)】"
GetMem4 v_dList, retAddr
Debug.Print "Addr -> " & retAddr
ret = Sgn(v_dList)
Debug.Print "Sgn -> " & ret
Debug.Print "【動的配列(割り当て後)】"
ReDim v_dList(3)
GetMem4 v_dList, retAddr
Debug.Print "Addr -> " & retAddr
ret = Sgn(v_dList)
Debug.Print "Sgn -> " & ret
Debug.Print "【動的配列(再割り当て後)】"
ReDim v_dList(5)
GetMem4 v_dList, retAddr
Debug.Print "Addr -> " & retAddr
ret = Sgn(v_dList)
Debug.Print "Sgn -> " & ret
Debug.Print "【動的配列(値設定後)】"
v_dList(0) = 10
GetMem4 v_dList, retAddr
Debug.Print "Addr -> " & retAddr
ret = Sgn(v_dList)
Debug.Print "Sgn -> " & ret
End Sub
---------- 結果 ----------
【静的配列】
Addr -> 28045724
Sgn -> -8768
【静的配列(値設定後)】
Addr -> 28045724
Sgn -> -8768
【動的配列(割り当て前)】
Addr -> 0
Sgn -> 0
【動的配列(割り当て後)】
Addr -> 213168776
Sgn -> -8064
【動的配列(再割り当て後)】
Addr -> 213168776
Sgn -> -8064
【動的配列(値設定後)】
Addr -> 213168776
Sgn -> -8064
- 割り当てるとSgnの結果が変わる
- 配列の値を設定をしてもSgnの結果は変わらない
という結果なので、配列の割り当てメモリアドレスの値がSgn関数の戻り値に影響していると考えられます。
割り当て前の動的配列は、アドレス値が0なので、引数としてゼロが渡されたものとしてSgn関数が認識して
戻り値がゼロになっているのではないかと思います。
※Variant構造体のしくみについては以下ページを参考にしました。
VBAの配列が 静的配列 であることを確認する方法について
ただし、割り当て済み(アドレス値が0以外)の配列であっても、戻り値がゼロになる場合があります。
ゼロになる場合を再現させる簡単なサンプルコードはちょっと用意できなかったのですが(単純なサンプルコードだと、ReDimしても確保するメモリアドレスが変わらないので再現できず)、実体験として、割り当て済みの配列にも関わらず、ゼロになる場合がありました。
(2018/2/12)
サンプルソースを用意できました。
Private Declare Sub GetMem4 Lib "msvbvm60" (ByRef Addr() As Any, RetVal As Long)
Private v_dList() As Variant
Function MemoryTest() As Boolean
Dim retAddr As Long
Dim ret As Integer
MemoryTest = True
ReDim v_dList(1000)
Debug.Print "【結果】"
GetMem4 v_dList, retAddr
Debug.Print "Addr -> " & retAddr
ret = Sgn(v_dList)
Debug.Print "Sgn -> " & ret
If ret = 0 Then
MemoryTest = False
End If
End Function
Sub Test()
Dim arrayTest(1000) As TestClass
Dim cnt As Integer
Dim flag As Boolean
cnt = 0
Do
Set arrayTest(cnt) = New TestClass
flag = arrayTest(cnt).MemoryTest
cnt = cnt + 1
Loop While flag And cnt < 1000
End Sub
「無限ループでゼロが返ってきたらブレーク」方式だとフリーズしたので、とりあえず1000回ごとにループさせる方式です。これを何回か実行したところ、Sgnがゼロになる結果が出ました。
・・・(中略)・・・
【結果】
Addr -> 338382800
Sgn -> -21248
【結果】
Addr -> 338382848
Sgn -> 0
アドレス値が338382848のときに、Sgnの戻り値がゼロになりました。
というわけで、
- Sgn関数の引数に数式以外のもの渡すと、仕様外の値が返ってくる
- 割り当て済みの配列でも、ゼロが返ってくる場合がある
ということから、Null判定にSgn関数を使用するのは誤りといえます。
Null判定するときには、ちょっと面倒でも、判定用のオリジナル関数を作って判定するのが正しいと思います。
私は昔、どハマリしたときには以下のような関数を用意して対応しました。
'--------------------------------------------------------------
'機能:引数が配列か判定し、配列の場合は空かどうかも判定する
'戻り値:判定結果(1:配列 / 0:空の配列 / -1:配列ではない
'--------------------------------------------------------------
Public Function isArrayEx(varArray As Variant) As Long
On Error GoTo ERROR_
If IsArray(varArray) Then
isArrayEx = IIf(UBound(varArray) >= 0, 1, 0)
Else
isArrayEx = -1
End If
Exit Function
ERROR_:
If Err.Number = 9 Then
isArrayEx = 0
Else
'想定外エラー
End If
End Function
ちなみに、以下のようにSgn関数の引数に配列を渡して、その結果をいったん変数に格納せずに直接使うと、異常終了します。
(少なくともExcel2013では百発百中落ちる)
Dim v_sList(3) As Variant '静的配列
Debug.Print "Sgn -> " & Sgn(v_sList)
どちらにしろ、仕様外の使い方はしてはならないということですね。