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

VBAで動的配列が割り当て済みかどうかを調べる方法として、
「Sgn関数を使う」
という方法をネット上でちらほら見かけますが、
Null判定にSgn関数を使ってはいけません。

```vbnet
'↓↓↓こういうことをやってはいけない！

If Sgn(配列変数) = 0 Then
  'Nullである
End If

If Sgn(配列変数) <> 0 Then
  'Nullではない
End If
```


ちょっと試す分には、上記通りに動くので気づきにくいですが、
<span style="font-size: 120%"><b>「Nullではない」場合にも、戻り値としてゼロが返ってくることがあります。</b></span>

数年前、これでどハマリして、
「同じ入力条件なのに、なぜかたまに結果が変わる」
という事象が発生して、原因究明にかなり苦労しました。


そもそも、Sgn関数は、引数で渡した数字の正負を判定して返す関数です。

| numberの値 | 戻り値 |
|:-:|:-:|
|number > 0|1|
|number = 0|0|
|number < 0|-1|


MSDNにも、「引数には数値式を設定してね」と書いてあります。

>数値の符号を示す、サブタイプが整数型 ( Integer ) であるバリアント型 ( Variant ) の値を返します。
>構文
>Sgn(number)
>必須の 引数number には、任意の有効な 数値式を指定できます。

[【MSDN】Sgn関数](https://msdn.microsoft.com/ja-jp/vba/language-reference-vba/articles/sgn-function)


このSgn関数に数式以外、たとえば配列変数を渡すと、
こういう結果が返ってきます。

```vbnet
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結果。

```vbnet
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](https://qiita-image-store.s3.amazonaws.com/0/236394/98a2b293-d22d-9061-c9b3-3e2b64ddb28a.png)

※Variant構造体のしくみについては以下ページを参考にしました。
[VBAの配列が 静的配列 であることを確認する方法について](http://www5f.biglobe.ne.jp/~f-lap/tips_staticarray.htm)

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

(2018/2/12)
サンプルソースを用意できました。

```vbnet: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
```
```vbnet: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判定するときには、ちょっと面倒でも、判定用のオリジナル関数を作って判定するのが正しいと思います。

私は昔、どハマリしたときには以下のような関数を用意して対応しました。

```vbnet
'--------------------------------------------------------------
'機能：引数が配列か判定し、配列の場合は空かどうかも判定する
'戻り値：判定結果（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では百発百中落ちる）

```vbnet
Dim v_sList(3) As Variant   '静的配列
Debug.Print "Sgn -> " & Sgn(v_sList)
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/236394/f3a44a6c-d4c6-7b6e-eb0a-f7586fda7949.png)

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