Pythonの知識でVBAをやってみる

## はじめに
ExcelファイルをPythonで処理しようと考えていたが、VBAで書かなければならなくなった。
頑張ってVBAでコードを書き始めた時に引っかかったところをまとめたメモです。

## 最初の一歩
### おおよそはPythonの書き方と同じ（暴論）
VBAはブロックの先頭に `:` をつけないで、ブロックの終端に処理が必要、と覚えておけばおおよそ対応できそうです。
```VB
' : は不要
Sub Test()
    ' メッセージボックスを表示する
    MsgBox "Hello World！"
' ブロック終端。関数(Sub)の場合は End Subが必要
End Sub
```

### 様々な宣言は大文字で始まる
大文字・小文字はVBAのエディタで勝手に修正されるので、あまり気にしないでも良さそう。
変数などの大文字・小文字も自動で修正されるのはありがたい…

### おまじない
```VB
Option Explicit
```
時々ファイルの先頭についているやつです。
これをファイル先頭に記載すると、以降は変数の宣言が必須になります。
参考: https://learn.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/option-explicit-statement

## 変数
### 型と一緒に「変数のみ」宣言する
Pythonの型ヒント付き変数 `hoge: int` に近いです。
```VB
' Dim 変数名(日本語OK) As 型
Dim hoge As Long
Dim ほげ As String

' _ から始まる変数名は不可
' Dim _hoge As Long ' エラー

' 宣言時に代入は不可
' Dim hoge As Long = 42  ' エラー

' 型の指定が無いと、なんでも入れられる Variant 型になる
Dim anything ' As Variant
```

### ただし、定数の宣言は同時に値も代入する
ややこしい…
```VB
Const FUGA As Long = 0
```

### 値の代入はオブジェクト型のみ `Set` を付ける
`Long`, `String` のように、どのタイプの値が入るか決まっている場合は データ型
WorksheetやRangeのように、PythonのClassっぽい役割を持つのが オブジェクト型（暴論）
```VB
Dim hoge As Long
hoge = 42 ' データ型に値を入れる場合はそのまま = でつなげばよい

' オブジェクト型の代入は頭に `Set` を付ける必要がある
Dim testSheet As Worksheet
' testSheet = ThisWorkbook.Sheets("Sheet1") ' エラー
Set testSheet = ThisWorkbook.Sheets("Sheet1")

' これは許されないが
' Set hoge = 42

' これが許される時がある… 
' デフォルトプロパティというらしく、プロパティを省略するとデフォルトで `Value` が選択される
' F1の値が整数だった場合にはこのコードが通ってしまう
hoge = testSheet.Range("F1") ' testSheet.Range("F1").Value と同じ
Debug.Print hoge ' イミディエイトウィンドウにhogeの値が出力される
```

## 条件式
### イコールは `=` ノットイコールは `<>`
まだ `<>` はわかるが、 `=` はちょっとびっくりした…
```VB
' If の末尾には Thenを付ける。 Pythonの `:` みたいなやつ
If hoge = "pen" Then
    MsgBox "This is a " & hoge ' 文字列は & で結合できる
' elif ではなく ElseIf
ElseIf hoge <> "pen" Then
    MsgBox "This is a not pen"
Else
    MsgBox "Elseの場合はThenが不要"
' If文の終端を明示する必要がある
End If
```

### And Or は全ての条件式を実行する
Pythonは短絡評価なので評価が不要な式は実行しないですが、
VBAは完全評価なので全ての式を実行します
```VB
' まだ宣言しただけなので、オブジェクトへの関連付けが無い状態(= Nothing)
Dim testSheet As Worksheet

' testSheet.name = "Sheet1" まで実行してしまうので、エラー
If testSheet is Nothing Or testSheet.Name = "Sheet1" Then
    Debug.Print "!!!"
End If
```

### 三項演算子のことは…
`IIF` というのがあるらしいが、短絡評価ではないのでPythonと動作が変わる可能性があります。
忘れましょう。

### matchの代わりは存在する
`Select Case` が使えます。
ただ書き方は結構違います。できることが多いと困る…
```VB
Select Case x
    Case 0
        Debug.Print "x = 0"
    Case Is < 0
        Debug.Print "x < 0"
    Case 1, 2
        Debug.Print "x = 1～2"
    Case 3 To 10
        Debug.Print "x = 3～10"
    Case Else
        Debug.Print "x > 10"
End Select
```

## ループ
### breakはある
`Exit For` を使用します。
```VB
' VBAのForはPythonのrangeを指定したときの動作に近い
' For 初期化 To 終了条件
For i = 1 To 10
    If sht.Cells(i, 1).Value = 5 Then
        Exit For ' Pythonの break
    End If
    Debug.Print sht.Cells(i, 1).Value
Next i ' iを1つ増やす
```

### continue はない
GoTo を使って疑似的にループを飛ばす処理がメジャーっぽいです。
あきらめてネストを1段深くした方がラベル重複による変な場所へ飛ぶ事故は減りそうな気がしますが…
```VB
' 要素を取りだすPythonっぽいForを使いたい場合はFor Eachを使う
For Each rng In testSheet.Range("A1:A10")
    ' 本当は rng.Value = "" の時にcontinueしたい…
    If rng.Value <> "" Then
        ' セルに値があるときは処理をする
    End If
Next rng ' 次の要素を取り出す
```

## 複数の値を保持する変数
### listの代わり: Array か Collection
~~配列のインデックスは 1 始まりなので注意が必要です。~~ もっと複雑でした。追記にて修正。
ArrayはC言語のlistに近く、どこかのタイミングで配列数を指定しないといけません。
`ReDim` で配列数を変更できるが、おそらく「要素を１個足す -> ReDimで配列を1個増やす」で疑似的に要素数不定にさせることは非推奨なはず…
```VB
' ()を付けるとArrayになる
Dim x(3) As Long
x(1) = 1
x(2) = 2
x(3) = 3
```

**追記:**
https://qiita.com/kz-muri/items/73f96908b764399f577e#comment-3fd137a8fa205b637576
https://qiita.com/kz-muri/items/73f96908b764399f577e#comment-37b5de896e8204b6ac9f
で頂いたコメントの通り、インデックスの始まりは0であったり、1であったりします。VBA難しい…
```VB
Sub test()
    Dim x(5) As Long ' 末尾のインデックスを指定しているイメージ？
    Debug.Print LBound(x) & ", " & UBound(x) ' 0, 5
    Debug.Print VarType(x) ' 8195 = vbLong + vbArray
    
    Dim y As Variant
    y = ActiveSheet.Range("A1:A5").Value ' 5セル分の値を配列に
    Debug.Print LBound(y) & ", " & UBound(y) ' 1, 5
    Debug.Print VarType(y) ' 8204 = vbVariant + vbArray
    
    Dim z(1 To 5) As Long ' Option Baseの値にとらわれずに1始まりにするならこうか…？
    Debug.Print LBound(z) & ", " & UBound(z) ' 1, 5
End Sub
```

さらに1始まりを設定しても0から始まる配列もある。 なんだこれ！！！
```VB
Option Base 1
Sub test2()
    Dim x(5) As Long ' 末尾のインデックスを指定しているイメージ？
    Debug.Print LBound(x) & ", " & UBound(x) '1, 5
    Debug.Print VarType(x) ' 8195 = vbLong + vbArray

    Dim s As Variant
    s = Split("1,2,3,4,5", ",") 
    Debug.Print LBound(s) & ", " & UBound(s) '0, 4
    Debug.Print VarType(s) ' 8200 = vbString + vbArray
End Sub
```

CollectionはPythonのlist…というよりはキーを省略できるnamedtupleの方が近いかも？。
追加・削除の制約なし。ただし、中身の型は指定できません。
```VB
' Collectionの場合は最初にNewを付けるとSetしなくて良い。Asって単なる型宣言じゃないのか…？
Dim coll As New Collection
'Dim coll2 As Collection
'Set coll2 = New Collection ' これも動く。

coll.Add "hoge" ' Arrayと違い、要素を追加する場合は 変数名.Addを使う
coll.Add "fuga"
coll.Add "piyo", "p" ' キーを付ける場合は後ろに記載
Debug.Print coll(1) ' hoge Collectionのインデックスは1始まり
Debug.Print coll(3) ' piyoをインデックスで取得。
Debug.Print coll("p") ' piyoをキーで取得
```


### dictの代わりもある
Python 3.7以降の仕様と異なり、代入した順番は保証されません。
```VB
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
dict.Add "x", 100 ' Collectionの時と順番が違い、キー, バリュー の順で指定
dict.Add "y", 200
Debug.Print dict("y") '200
```

### 列挙体もある
Enumをクラス経由ではなく直接定義します。
最初の数を指定してあげれば後は自動で値が入る、pythonの `auto()` のような機能があって便利です。
```VB
Enum Numbers
    foo = 1
    bar ' 2
    baz ' 3
End Enum

Sub Test()
    Debug.Print Numbers.bar ' 2
End Sub
```

## 関数
### Sub と Function
関数（ **追記:**「プロシージャ」なので関数とは違うらしい… 参考: https://learn.microsoft.com/ja-jp/office/vba/language/concepts/getting-started/calling-sub-and-function-procedures ）
の定義方法はなぜか複数あります。ざっくりとした違いは以下。
- Subは戻り値が無く、ボタンに登録できる関数
- Functionは戻り値を定義できるが、ボタンには登録できない関数

Functionの戻り値用変数は **その関数名である** ことに注意。

```VB
Sub Test()
    ' 戻り値のない関数を実行するときは頭に `Call` をつける
    Call TestSub(1)
    Debug.Print TestFunction(1)
End Sub

Sub TestSub(x As Long)
    Debug.Print x
End Sub

' 変数宣言時、戻り値の型も指定する
Function TestFunction(x As Long) As Long
    ' 戻り値の設定
    TestFunction = x + 1
End Function
```

### 引数は基本「参照渡し」
値渡しを明示しない、 もしくは引数に `ByRef`を付けると参照渡しになります。
引数に `ByVal` と明示すると値渡しになります。
```VB
Sub Test()
    Dim x As Long
    x = 1
    
    Call TestSub(x)
    Debug.Print x ' 参照渡しのため、引数に渡した変数の値が2になっている
    
    Debug.Print TestFunction(x) ' 戻り値は3だが
    Debug.Print x ' 値渡しのため、引数に渡した変数の値は2のまま
End Sub

' 参照渡し
Sub TestSub(x As Long)
    x = x + 1
End Sub

' 値渡し
Function TestFunction(ByVal x As Long) As Long
    x = x + 1
    TestFunction = x
End Function
```

## エラー
### おまじない
`On Error GoTo ラベル名` でエラー発生時に指定したラベルまで飛びます。
`err.Description` でエラーの内容を取得可能です。

```VB
Sub Test()
    On Error GoTo Label
    ' 何らかの処理
    Exit Sub ' 正常終了時はここで終了
Label:
    MsgBox "エラー内容: " & err.Description
End Sub
```

### エラーを出す
呼び出し元にエラーを投げ返したい場合は `err.Raise` を使用します。
エラー番号は 513～65535がユーザー定義の範囲だが、1004などの値が既に使われているので、注意が必要です。
参考: https://excel-ubara.com/excelvba4/EXCEL242.html

## いったんここまで
関数を Call する際に()を省略できたり、エラーを握りつぶして処理を続行するなどの細かい部分や、クラスに関してなどはまた別の記事で作成予定です…