Edited at

VBAで配列のNull判定にSgn関数を使ってはいけない

More than 1 year has passed since last update.

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 には、任意の有効な 数値式を指定できます。


【MSDN】Sgn関数

この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関数が認識して

戻り値がゼロになっているのではないかと思います。

image.png

※Variant構造体のしくみについては以下ページを参考にしました。

VBAの配列が 静的配列 であることを確認する方法について

ただし、割り当て済み(アドレス値が0以外)の配列であっても、戻り値がゼロになる場合があります。

ゼロになる場合を再現させる簡単なサンプルコードはちょっと用意できなかったのですが(単純なサンプルコードだと、ReDimしても確保するメモリアドレスが変わらないので再現できず)実体験として、割り当て済みの配列にも関わらず、ゼロになる場合がありました。

(2018/2/12)

サンプルソースを用意できました。


TestClass.cls

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



Module.bas

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)

image.png

どちらにしろ、仕様外の使い方はしてはならないということですね。