15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-02-05

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

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

15
10
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?