2012年12月2日日曜日

[ExcelVBA] 動的配列における初期要素の扱いについて


アドベントカレンダー 2日目

問題点:
動的配列でReDimする前に一番最後の要素を
調べようとするとエラーとなるので
どうするの?

って話ですけど、
一番てっとりばやいのは、配列の要素を管理する変数を作って
追加する時にインクリメントしてあげればよいわけですが
別関数に処理をまかせ、特定の条件の時のみ配列に追加する
となると、いちいち配列と、配列の要素数を管理する変数を
毎回引き渡すとなるとちょっと美しくないですよね。

そんなわけで、配列の要素数を管理する変数なしで行うとしたら
SGN関数をつかって、配列の宣言がされてなかったら0が返ってくる修正を利用して処理を分岐するか
On Error Resume Nextを使ってUboundで最終要素にアクセスしてエラーがあれば0でReDimするって
方法あたりが考えられますよね。


たとえば、こんな感じ。
以下は、AddEven関数内で5回ループし、偶数だったら配列に追加する処理です。

Option Explicit

Sub SampleCode()
    Dim EvenNumber()    As String

    'SGN関数を使った場合
    Call AddEvenNumber1(EvenNumber)

    '配列の初期化
    Erase EvenNumber
    
    'On Error Resume Nextを使った場合
    Call AddEvenNumber2(EvenNumber)
End Sub

Sub AddEvenNumber1(ByRef EvenNumber() As String)
    Dim E               As Long
    Dim i               As Integer
    Dim RandomNumber    As Integer
        
    Randomize
    
    For i = 1 To 5
        '1~100の乱数を発生
        RandomNumber = Int(Rnd() * 100) + 1
        
        '奇数の時は次のループへ
        If RandomNumber Mod 2 = 1 Then GoTo NextForLoop
        
        If Sgn(EvenNumber) = 0 Then
            E = 0
            
        Else
            E = UBound(EvenNumber) + 1
        
        End If

        ReDim Preserve EvenNumber(E)
        EvenNumber(E) = RandomNumber
NextForLoop:
    Next
    
End Sub

Sub AddEvenNumber2(ByRef EvenNumber() As String)
    Dim i               As Integer
    Dim RandomNumber    As Integer
        
    Randomize
    
    For i = 1 To 5
        '1~100の乱数を発生
        RandomNumber = Int(Rnd() * 100) + 1
        
        '奇数の時は次のループへ
        If RandomNumber Mod 2 = 1 Then GoTo NextForLoop
        
        On Error Resume Next
        ReDim Preserve EvenNumber(UBound(EvenNumber) + 1)
        If Err.Number <> 0 Then ReDim Preserve EvenNumber(0)
        On Error GoTo 0
        
        EvenNumber(UBound(EvenNumber)) = RandomNumber
NextForLoop:
    Next
    
End Sub



では、せっかくなんでどのやり方が一番最速か調べてみます。
純粋に配列の初期要素問題がどう影響するかだけを見たいので
乱数やら、偶数の時だけ配列に追加とかは除外します。
単純に10万回ループさせて値を追加し続けてみます。
1.SGN関数を使う

2.On Error Resume Nextを使う

3.要素数管理用の変数を使う(引数に渡す)
→IF分を減らした方が早いんじゃないかという疑惑から

4.要素数管理用の変数を使う(引数で渡さない:グローバル宣言)
→IF分なくしても、引数を増やすと時間がかかるんじゃないかという疑惑から。

ソースコードは最後に掲載

1.SGN2.Error3.要素数管理4.要素数管理
1回目1.0470.8120.7030.734
2回目0.7340.6870.7500.735
3回目0.6090.6560.7030.796
4回目0.7190.7660.7030.703
5回目0.5620.7180.8750.828
6回目0.5630.7970.7350.750
7回目0.5780.7500.7500.750
8回目0.5470.7970.9530.703
9回目0.5940.8120.7810.656
10回目0.5160.8590.8440.750
平均0.64690.76540.77970.7405

一応、SGNを使った場合が一番早いようですが、
10万回ループで0.1秒差なので、どれを使ったもかわらないですね。


ソースコード:
Option Explicit

Dim EE As Long

Declare Function GetTickCount Lib "kernel32" () As Long

Sub SampleCode()
    Dim BaseTime        As Long
    Dim E               As Long: E = -1
    Dim EvenNumber()    As String

    '基準タイム
    BaseTime = GetTickCount

    'SGN関数を使った場合
    Call AddArray1(EvenNumber)

    Debug.Print "SGN :" & (GetTickCount - BaseTime) / 1000

    '配列の初期化
    Erase EvenNumber
    
    '基準タイム
    BaseTime = GetTickCount
    
    'On Error Resume Nextを使った場合
    Call AddArray2(EvenNumber)

    Debug.Print "Err :" & (GetTickCount - BaseTime) / 1000

    '配列の初期化
    Erase EvenNumber
    
    '基準タイム
    BaseTime = GetTickCount
    
    'On Error Resume Nextを使った場合
    Call AddArray3(EvenNumber, E)

    Debug.Print "要素:" & (GetTickCount - BaseTime) / 1000

    '配列の初期化
    Erase EvenNumber
    
    EE = -1
    
    '基準タイム
    BaseTime = GetTickCount
    
    'On Error Resume Nextを使った場合
    Call AddArray4(EvenNumber)

    Debug.Print "要素:" & (GetTickCount - BaseTime) / 1000
    
    
    Debug.Print "---"
End Sub

Sub AddArray1(ByRef EvenNumber() As String)
    Dim E               As Long
    Dim i               As Long
    Dim RandomNumber    As Integer
    
    For i = 1 To 100000
    
        If Sgn(EvenNumber) = 0 Then
            E = 0
        Else
            E = UBound(EvenNumber) + 1
        End If

        ReDim Preserve EvenNumber(E)
        EvenNumber(E) = i
NextForLoop:
    Next
    
End Sub

Sub AddArray2(ByRef EvenNumber() As String)
    Dim i               As Long
    Dim RandomNumber    As Integer
    
    For i = 1 To 100000
        On Error Resume Next
        ReDim Preserve EvenNumber(UBound(EvenNumber) + 1)
        If Err.Number <> 0 Then ReDim Preserve EvenNumber(0)
        On Error GoTo 0
        
        EvenNumber(UBound(EvenNumber)) = i
NextForLoop:
    Next
    
End Sub

Sub AddArray3(ByRef EvenNumber() As String, ByRef E As Long)
    Dim i               As Long
    Dim RandomNumber    As Integer
    
    For i = 1 To 100000
        E = E + 1
        ReDim Preserve EvenNumber(E)
        EvenNumber(E) = i
NextForLoop:
    Next
    
End Sub

Sub AddArray4(ByRef EvenNumber() As String)
    Dim i               As Long
    Dim RandomNumber    As Integer
    
    For i = 1 To 100000
        EE = EE + 1
        ReDim Preserve EvenNumber(EE)
        EvenNumber(EE) = i
NextForLoop:
    Next
    
End Sub

0 件のコメント: