クラスモジュールを用いたStackの改良

クラスモジュールを用いたStackの改良

前回

akashi-keirin.hatenablog.com

の続き。

ちょっと改良した。

クラス名の見直し

StringStackオブジェクトの各要素がStackStringというのは、余りにもわかりにくすぎるので、StringStackItemに改めた。少し長くなるけど、この方がいい。Javaとかだったらもっとえげつなく長いクラス名とか普通にあるし。わかりやすさ優先。

あと、オレオレコーディング規約で、「プロパティはパスカル、メソッド名はキャメル」という謎ルールを課しているので、Pushメソッド、PopメソッドをそれぞれpushItempopItemに改めた。

プロパティの追加

せっかくデータの集合なのに、データの総数がわからなかったり、○○個目のデータが参照できたりしないのでは不便(Popすると、値は得られるが消えてしまう。)。

そこで、アイテム総数を返すCountプロパティ、上から○○個目のアイテムを返すItemプロパティを追加した。

改良後のコード

クラスモジュール StringStack
Option Explicit

Private topItem As StringStackItem
Private count_ As Long    '……(1)'

Public Property Get Count() As Long  '……(2)'
  Count = count_
End Property

Public Property Get Top() As String
  If ItemExists Then
    Top = topItem.Value
  Else
    Top = ""
  End If
End Property

Public Property Get Item( _
            ByVal Index As Long) As String    '……(3)'
  Dim ret As StringStackItem
  If ItemExists Then    '……(4)'
    Set ret = topItem    '……(5)'
    If Index = 1 Then GoTo Finalizer
    Dim i As Long    '……(6)'
    For i = 2 To Index
      Set ret = ret.NextItem
    Next
  Else
    Set ret = Nothing
  End If
Finalizer:
  If ret Is Nothing Then Item = "": Exit Property
  Item = ret.Value
End Property

Public Property Get ItemExists() As Boolean
  ItemExists = Not (topItem Is Nothing)
End Property

Public Sub pushItem(ByVal argValue As String)
  Dim newTopItem As New StringStackItem
  newTopItem.Value = argValue
  Set newTopItem.NextItem = topItem
  Set topItem = newTopItem
  count_ = count_ + 1    '……(7)'
End Sub

Public Function popItem() As String
  Dim ret As Variant
  If Me.ItemExists Then
    ret = topItem.Value
    Set topItem = topItem.NextItem
    count_ = count_ - 1    '……(8)'
  End If
  popItem = ret
End Function

追加したのは、まず(1)の

Private count_ As Long

というモジュールレベル変数。こいつで、アイテム数を保持する。

(2)の

Public Property Get Count() As Long
  Count = count_
End Property

Countプロパティを生やす。ReadOnly。

(3)の

Public Property Get Item( _
            ByVal Index As Long) As String
  Dim ret As StringStackItem
  If ItemExists Then    '……(4)'
    Set ret = topItem    '……(5)'
    If Index = 1 Then GoTo Finalizer
    Dim i As Long    '……(6)'
    For i = 2 To Index
      Set ret = ret.NextItem
    Next
  Else
    Set ret = Nothing
  End If
Finalizer:
  If ret Is Nothing Then Item = "": Exit Property
  Item = ret.Value
End Property

Itemプロパティを生やす。

引数Indexを受け取って、上からIndex番目のアイテムの値(String)を返す。

(4)の

If ItemExists Then

で条件分岐。アイテムがなかったら、Else節へ飛んで、retNothingをセット。別に無くても良いけど、明示する。

アイテムがある場合は、(5)の

Set ret = topItem
If Index = 1 Then GoTo Finalizer

で、retに一番上に積まれているアイテムをセット。引数Index1だったら即Finalizerラベルへ飛び、値を返す。

Index2以上のときは、(6)の

Dim i As Long
For i = 2 To Index
  Set ret = ret.NextItem
Next

で、必要な回数だけNextItemを順に手繰っていって、目的のStringStackItemを取得する。

ちなみに、Index1未満の数やCountの値を超える数値が渡されたときの対応は未実装。

あと、(7)と(8)は、pushItempopItemメソッド実行時にそれぞれcount_の値を増減しているだけ。

クラスモジュール StringStackItem
Option Explicit

Private value_ As String
Private nextItem_ As StringStackItem

Public Property Let Value(ByVal argValue As String)
  value_ = argValue
End Property
Public Property Get Value() As String
  Value = value_
End Property

Public Property Set NextItem(ByVal argItem As StringStackItem)
  Set nextItem_ = argItem
End Property
Public Property Get NextItem() As StringStackItem
  Set NextItem = nextItem_
End Property

こちらの方は変更なし。クラス名を変えただけ。

使ってみる

次のコードで実験。

リスト1 標準モジュール
Private Sub testStringStack()
  Dim strStack As New StringStack
  With strStack
    Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Call .pushItem("1番サード岩鬼")
    Debug.Print .Top & " をPushしました。"
    Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Call .pushItem("2番セカンド殿馬")
    Debug.Print .Top & " をPushしました。"
    Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Call .pushItem("3番レフト微笑")
    Debug.Print .Top & " をPushしました。"
    Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Call .pushItem("4番キャッチャー山田")
    Debug.Print .Top & " をPushしました。"
    Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Debug.Print "===================="
    Dim i As Long
    For i = 1 To .Count
    Debug.Print "上から" & StrConv(CStr(i), vbWide) & "番目は、" & _
                .Item(i) & " です。"
    Next
    Debug.Print "===================="
    Do While .ItemExists
      Debug.Print .popItem & " をPopしました。"
      Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Loop
  End With
End Sub

計四つのアイテムをPush。そのたびにアイテム数を出力。その後、アイテムを上から順に表示し、すべてのアイテムをPop。そのたびに残りアイテム数を表示する、というもの。

実行すると、

f:id:akashi_keirin:20191115080757j:plain

このとおり。

おわりに

実に面白い。

2019.11.16追記

改めて見直してみたら、ItemプロパティやpopItemメソッドがString型の値を返すというのはわかりにくい。素直にStringStackItemオブジェクトを返す方が、名前に合っているような気がする。

また、Topプロパティが値を返すのも変だ。StackオブジェクトのTopにあるのはStringStackItemオブジェクトなのだから、素直にStringStackItemオブジェクトを返す方が自然だ。

よって、次のように修正することにした。

クラスモジュール StringStack
Option Explicit

Private topItem As StringStackItem
Private count_ As Long

Public Property Get Count() As Long
  Count = count_
End Property

Public Property Get Top() As StringStackItem
  Dim ret As StringStackItem
  Set ret = Nothing
  If ItemExists Then Set ret = topItem
  Set Top = ret
End Property

Public Property Get Item( _
              ByVal Index As Long) As StringStackItem
  Dim ret As StringStackItem
  Set ret = Nothing
  'IndexがおかしかったらNothingを返す'
  If Index < 1 Then GoTo Finalizer
  If Index > Me.Count Then GoTo Finalizer
  'アイテムがあったら返す。なかったらNothing'
  If ItemExists Then
    Set ret = topItem
    If Index = 1 Then GoTo Finalizer
    Dim i As Long
    For i = 2 To Index
      Set ret = ret.NextItem
    Next
  Else
    Set ret = Nothing
  End If
Finalizer:
  Set Item = ret
End Property

Public Property Get ItemExists() As Boolean
  ItemExists = Not (topItem Is Nothing)
End Property

Public Sub pushItem(ByVal argValue As String)
  Dim newTopItem As New StringStackItem
  newTopItem.Value = argValue
  Set newTopItem.NextItem = topItem
  Set topItem = newTopItem
  count_ = count_ + 1
End Sub

Public Function popItem() As StringStackItem
  Dim ret As Variant
  If Me.ItemExists Then
    Set ret = topItem
    Set topItem = topItem.NextItem
    count_ = count_ - 1
  End If
  Set popItem = ret
End Function

ついでに、Indexプロパティに不正な値が渡されたときの対応も追加した。

本来エラーを投げるべきなんだろうけど、めんどくさいからNothingを返す仕様にした。

この変更により、Topプロパティ、Itemプロパティ、popItemメソッドの全てがStringStackItemオブジェクトを返すようになった。

それに伴い、テスト用コード(リスト1)も修正が必要。

スト2 標準モジュール
Private Sub testStringStack()
  Dim strStack As New StringStack
  With strStack
    Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Call .pushItem("1番サード岩鬼")
    Debug.Print .Top.Value & " をPushしました。"
    Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Call .pushItem("2番セカンド殿馬")
    Debug.Print .Top.Value & " をPushしました。"
    Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Call .pushItem("3番レフト微笑")
    Debug.Print .Top.Value & " をPushしました。"
    Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Call .pushItem("4番キャッチャー山田")
    Debug.Print .Top.Value & " をPushしました。"
    Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Debug.Print "===================="
    Dim i As Long
    For i = 1 To .Count
    Debug.Print "上から" & StrConv(CStr(i), vbWide) & "番目は、" & _
                .Item(i).Value & " です。"
    Next
    Debug.Print "===================="
    Do While .ItemExists
      Debug.Print .popItem.Value & " をPopしました。"
      Debug.Print "現在の保有アイテム数は、" & .Count & " 個です。"
    Loop
  End With
End Sub

ひつこいけれど、Topプロパティ、Itemプロパティ、popItemメソッドの全てがStringStackItemオブジェクトを返すので、その値が欲しいときはValueプロパティを参照する必要がある。

その分、少しコードの量は増えた。(Top.Valueなどという実に安っぽい記述が頻出することにもなったw)

もちろん、実行結果は上と同じ。