あらすじ
VBAのコードを書いているとき、カプセル化のためのコードで、
意図せぬ無限再帰をしてしまい、
エラーメッセージ「スタック領域が不足しています。」を
発生させてしまいました。
いわゆるスタックオーバーフローです。
スタック領域
今回のお題はスタックオーバーフローです。
「スタック領域が不足しています。」と言われたら、
8割の確率で、再帰呼び出しによるスタックオーバーフローを疑っていいと思います。
本来のコード
まず、変数myName
をPrivate
にして外部からアクセスできなくしています。
これがカプセル化です。
次に、Property Get
でmyName
を文章に当てはめて取得できるようにしています。
myName
に値を登録する場合にはProperty Let
を使えるようにしています。
Private myName As String
Public Property Get GetName() As String
GetName = "I am " + myName + "."
End Property
Public Property Let SetName(ByVal newName As String)
myName = newName
End Property
誤ったコード
以下の誤ったコードでは、
Property Let
の中で、自分自身に代入しているため、
無限に再帰呼び出しが行われます。
Private myName As String
Public Property Get GetName() As String
GetName = "I am " + myName + "."
End Property
Public Property Let Name(ByVal newName As String)
Name = newName '←ここがミス
End Property
なぜ起こしてしまったのか
意図的に、以下のような再帰呼び出しの無限ループを起こすことも可能です。
Sub Recursion(i)
i = i + 1
Call Recursion(i)
End Sub
ご覧の通り、上記のサンプルコードは関数の無限再帰です。
プロパティで同じことが起きる可能性を警戒していませんでした。
Name、myName、newNameなど、似た名前ばかりなため、
テキトーに書いて直すのを忘れたんですね。
まだまだ経験不足だからかもしれません。
対策
とはいえ、経験不足というのは言い訳です。
解決方法を考えましょう。
根本的な発生対策は思いつきません。
もし知っている、思いつく人がいれば、
コメント欄で伝えて下さるとLGTMします。
というかフォローします。
取り合えずの対策
細かい低いテストコードを書いて早めに気が付く方法があります。
このテストコードには、RubberDuckを使用しています。
Option Explicit
Option Private Module
'@TestModule
'@Folder("Tests")
Private Assert As New Rubberduck.AssertClass
Private Fakes As New Rubberduck.FakesProvider
'@ModuleInitialize
Private Sub ModuleInitialize()
'this method runs once per module.
Set Assert = New Rubberduck.AssertClass
Set Fakes = New Rubberduck.FakesProvider
End Sub
'@ModuleCleanup
Private Sub ModuleCleanup()
'this method runs once per module.
Set Assert = Nothing
Set Fakes = Nothing
End Sub
'@TestInitialize
Private Sub TestInitialize()
'This method runs before every test in the module..
End Sub
'@TestCleanup
Private Sub TestCleanup()
'this method runs after every test in the module.
End Sub
'@TestMethod("Small")
Private Sub TestSetName()
On Error GoTo TestFail
'Arrange:
Dim HumanName As NameRemember
'Act:
HumanName.SetName("GOD")
'Assert:
Assert.Succeed
TestExit:
Exit Sub
TestFail:
Assert.fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod("Small")
Private Sub TestGetName()
On Error GoTo TestFail
'Arrange:
Dim HumanName As NameRemember
Dim TestName As String
'Act:
TestName = "GOD"
HumanName.SetName(TestName)
'Assert:
Assert.isEqual HumanName.GetName, "I am " + TestName + "."
TestExit:
Exit Sub
TestFail:
Assert.fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
動作として、Sub TestSetName
はSetName
が最後まで正常に動作するかを確認しています。
また、Sub TestGetName
はGetName
が正常な値を返すかを確認しています。
この方法ではミスを見つけることはできますが、
未然に防ぐことはできません。
コードスニペット
vscodeなどには、コードスニペットやEmmetといった、
テンプレートを自動挿入してくれる機能が付いています。
これらを使用すると、クラスモジュール内でPrivate myName As String
を書いている段階でProperty Get
とProperty Let
の構文が自動挿入されたりします。
以下のサンプルコードのようなレベルが、
1行目を書いた直後に自動挿入されれば、
無限再帰は起こりえないでしょう。
Private my_Name As String
Public Property Get Get_Name() As String
Get_Name = my_Name
End Property
Public Property Let Set_Name(ByVal input_Value As String)
my_Name = input_Value
End Property
これらの自動化は、単にキー入力の数を減らすだけではなく、
今回のようなミスを事前に防ぐ効果もあるといえます。
しかし、現状VBE(Visual Basic Editor)にそのような機能はありません。
命名規則
また、SetName、GetNameなど、明らかにそれと分かる
命名規則にして習慣化するとミスが防げるかもしれません。
まとめ
今のところ確実に未然防止する方法はありません。
スタックオーバーフローには気を付けましょう。
Excelsior!