0
1

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 1 year has passed since last update.

VBAでカプセル化のミスがスタック領域の不足を起こした件

Last updated at Posted at 2020-09-30

あらすじ

VBAのコードを書いているとき、カプセル化のためのコードで、
意図せぬ無限再帰をしてしまい、
エラーメッセージ「スタック領域が不足しています。」を
発生させてしまいました。
いわゆるスタックオーバーフローです。

スタック領域

今回のお題はスタックオーバーフローです。
「スタック領域が不足しています。」と言われたら、
8割の確率で、再帰呼び出しによるスタックオーバーフローを疑っていいと思います。

本来のコード

まず、変数myNamePrivateにして外部からアクセスできなくしています。
これがカプセル化です。
次に、Property GetmyNameを文章に当てはめて取得できるようにしています。
myNameに値を登録する場合にはProperty Letを使えるようにしています。

NameRemember.cls
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の中で、自分自身に代入しているため、
無限に再帰呼び出しが行われます。

NameRemember.cls
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

なぜ起こしてしまったのか

意図的に、以下のような再帰呼び出しの無限ループを起こすことも可能です。

LoopRecurse.bas
Sub Recursion(i)
    i = i + 1
    Call Recursion(i)
End Sub

ご覧の通り、上記のサンプルコードは関数の無限再帰です。
プロパティで同じことが起きる可能性を警戒していませんでした。

Name、myName、newNameなど、似た名前ばかりなため、
テキトーに書いて直すのを忘れたんですね。
まだまだ経験不足だからかもしれません。

対策

とはいえ、経験不足というのは言い訳です。
解決方法を考えましょう。

根本的な発生対策は思いつきません。
もし知っている、思いつく人がいれば、
コメント欄で伝えて下さるとLGTMします。
というかフォローします。

取り合えずの対策

細かい低いテストコードを書いて早めに気が付く方法があります。
このテストコードには、RubberDuckを使用しています。

test.bas
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 TestSetNameSetNameが最後まで正常に動作するかを確認しています。
また、Sub TestGetNameGetNameが正常な値を返すかを確認しています。

この方法ではミスを見つけることはできますが、
未然に防ぐことはできません。

コードスニペット

vscodeなどには、コードスニペットやEmmetといった、
テンプレートを自動挿入してくれる機能が付いています。

これらを使用すると、クラスモジュール内でPrivate myName As Stringを書いている段階でProperty GetProperty Letの構文が自動挿入されたりします。

以下のサンプルコードのようなレベルが、
1行目を書いた直後に自動挿入されれば、
無限再帰は起こりえないでしょう。

NameRemember.cls
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!

0
1
0

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?