Staticプロシージャというものがある

Staticプロシージャというものがある

知ってました?

私は全然知らなかった。

f:id:akashi_keirin:20190626205315j:plain

何気なく『Programming Excel with VBA and .NET』という本を読んでいたら、58ページに、

Private Static Sub Proc3()
    ' In Static Procedures, all local variables are Static.
    ' Static procedures may be Private, Public, or Friend.
End Sub

と書いてあった。

マ、マジっすか……?!

全然知らなかったよ!

実験

標準モジュールに次のコードを書いて実験。

リスト1 標準モジュール
Private Static Sub testStaticProcedure()
  Dim i As Long
  i = i + 1
  Call MsgBox("ち~んw:" & i & "回目")
End Sub

こいつを何度も実行する。

In Static Procedures, all local variables are Static.

これが本当なら、実行するごとにiの値がインクリメントされるはずだ。

いざ、実行!

f:id:akashi_keirin:20190626205325g:plain

ほ、ほんまや……。

おわりに

何か、使いどころはあるかなあ。

小ネタでした。

PublicNotCreatableの意味

PublicNotCreatableの意味

クラスモジュール使いであっても、あっちこっちで使い回すようなクラスモジュールを作ったりしなければ、存在にすら気づかないのが、クラスモジュールのInstancingプロパティではなかろうか。

Instancingプロパティに、「Private」と「PublicNotCreatable」の二種類の設定値があることは知っていたが、余り深く意味を考えたことがなかった。

特に、「PublicNotCreatable」の方は、「NotCreatable」の文字だけを見て、

インスタンスが作れねえのかよ、不便だな

とか、わけのわからない理解をしていた。

ちょっとづつわかってきたかも知れないので、メモ。

Private/Publicの違い

まず、「Private」と「Public」の違い。

これは、

別プロジェクトに対して

Private」か「Public」か、ということ。

VBAによるマクロ作成の場合、1ブック(プロジェクト)内で閉じた処理であることが多いので、普通に使っている限り、まず意識することがないポイントだと思う。

たとえば、

f:id:akashi_keirin:20190623083527j:plain

このようなプロジェクト構成になっているとする。(おい、ファイル名は綴りが間違えてるじゃねえかw)

Referring」というプロジェクトが、「Referred」というプロジェクトを参照している。

参照しているということは、「Referring」(参照元)から「Referred」(参照先)へのつながりをつけているということ。

つながりがついているから、「Referring」(参照元)からは、「Referred」(参照先)の中の「Public」なものについては〈見える〉(呼び出せる)ということになる。

ここで言う〈見える〉というのは、コードウインドウ等に〈表示できる〉ということではない(それは、〈ユーザーから見える〉ということだ)。

再度、プロジェクト エクスプローラー画像を。

f:id:akashi_keirin:20190623083527j:plain

ご覧のとおり、参照先である「Referred」には、「HogePrivate」と「HogePublic」という二つのクラスモジュールが置かれている。

クラス名からも明らかなとおり、「HogePrivate」は、Instancingプロパティを「Private」に、「HogePublic」の方は「PublicNotCreatable」に設定している。

f:id:akashi_keirin:20190623083530g:plain

ご覧のように、入力時のヒントには「HogePublic」の方しか出てこない。

これが、〈見える〉ということだ。

Referringプロジェクトからは、Referredプロジェクト内のクラスのうち、PrivateHogePrivateクラスは〈見えない〉が、PublicHogePublicクラスは〈見える〉ということだ。

では、NotCreatableとは

最初、「NotCreatable」という字面から、「インスタンスが作れないクラス」のことだと思っていた。

しかし、Instancingプロパティを「PublicNotCreatable」に設定していても、普通にNewインスタンス化することができていたので(当たり前)、「わけわからんなー」で済ませていたのだった。

これは要するに

別プロジェクト内でインスタンスが作れない

ということだったのだ。

今回の例の場合だと、

Referringプロジェクト上で、Referredプロジェクト下のクラスのインスタンスを作ることはできない

というだけのことだった。

f:id:akashi_keirin:20190623083538g:plain

このように、「不正」呼ばわりされて、インスタンスが作れないのだ。

もちろん、上の例での変数hogeインスタンスをぶち込む方法はある。

これまで当ブログのコメント欄等でも言及があったように、参照先(クラスを持っている側のプロジェクト。今回の例で言うとReferredプロジェクト。)にインスタンスを返すメソッドを置いておけば良い。

参考までに今回の例の場合だと、次のようなメソッドを置くことになる。

リスト1 ReferredプロジェクトのThisWorkbookモジュール
Public Function getHogeinstance() As HogePublic
  Set getHogeinstance = New HogePublic
End Function

そして、参照元HogePublicクラスを呼び出す側のプロジェクト。今回の例で言うとReferringプロジェクト。)で次のように書く。

スト2 Referringプロジェクトの標準モジュール
Dim hoge As HogePublic
Set hoge = Referred.ThisWorkbook.getHogeInstance

ちなみに、ThisWorkbookの後に「.」(ピリオド)を入力してもヒントが出ないので不安になるが、大丈夫。

おわりに

最初にも書いたとおり、そもそもVBAでは、複数プロジェクトにまたがるような処理を書くことがあまりないので、

Privateに対するPublicはあるのに、NotCreatableに対するCreatableがない

という非対称性に気づく機会が少ないのだと思う。

汎用的なオレオレメソッドが溜まってきて、それをあちこちで使い回そうという気持ちになったときに、初めてぶつかる問題なのかも。

アクセス修飾子「Friend」とは?

アクセス修飾子「Friend」とは?

勘違いから、マヌケな記事を書いてしまった。

訂正記事を書くのがめんどくさいので(←コラ!)、バッサリ削除して書き直す。

アクセス修飾子「Friend」は、入門書の類にはまず出てこない。

名前が「Friend」なのに、友達がいないみたいでかわいそうだ。

FriendとPublicはどう違うのか

とりあえず、個人用マクロブック(プロジェクト名「PersonalUtils」)のSheet1オブジェクトモジュールに、次の二つのメソッドを置いてみた。

リスト1 PersonalUtils.Sheet1モジュール
Option Explicit

Friend Sub showMessageProtected()
  Call MsgBox("ち~んw")
End Sub

Public Sub showMessagePublic()
  Call MsgBox("ち~んw")
End Sub

メソッドの内容はどうでもいいので、なげやりなものにしている。

一つ目のshowMessageProtectedメソッドがFriend指定、二つ目のshowMessagePublicメソッドがPublic指定。

呼び出す

で、こうして作成した個人用マクロブックのSheet1モジュール上のメソッドを呼び出そうと試みる。

ちなみに、プロジェクトエクスプローラーはこの状態。

f:id:akashi_keirin:20190621211720j:plain

「シートモジュール活用その2.xlsm」の標準モジュールから呼び出すのだ。

f:id:akashi_keirin:20190621211757g:plain

しかし、このように、Publicメソッドであっても呼び出すことができない。

参照設定していないからである。

そこで、

f:id:akashi_keirin:20190621211723j:plain

このように参照設定する。

再度、挑戦。

f:id:akashi_keirin:20190621211725g:plain

今度は、Intellisenseが働いている!

よく見ると、ヒントが表示されるのはPublic指定のshowMessagePublicメソッドだけで、Friend指定のshowMessageProtectedの方は表示されていない。

呼び出し用プロシージャを実行してみる。

f:id:akashi_keirin:20190621211734g:plain

このように、Friend指定の方は呼び出すことができない。

一方、

f:id:akashi_keirin:20190621211744g:plain

Public指定の方は、呼び出すことができた。

おわりに

別プロジェクトからオブジェクトモジュールのメソッドを呼び出したり、プロパティを参照したりする場面自体、VBAでは想定しにくいので、いまいち使いどころがよくわからないが、Publicでもなく、Privateでもないこの振る舞いを理解しておくと、いづれ何かの機会に役立つかもしれない???

テスト用メソッドもPrivete指定にする

テスト用メソッドもPrivete指定にする

Privete指定でもVBE上で実行できる

これは、考えてみたら当り前なんだけれど、全然気づいていなかった。

f:id:akashi_keirin:20190617075231g:plain

イミディエイト・ウインドウ上での実行

次のようなメソッド群を用意する。

リスト1 標準モジュール
Private Sub showMessage()
  Call MsgBox("ち~んw")
End Sub

Private Sub testShowMessage()
  Call showMessage
End Sub

上が呼び出され用のメソッドで、下が呼び出しテスト用のメソッド。いづれPrivete指定にしてある。

たとえば、testShowMessageをイミディエイト・ウインドウから呼び出そうとして、

Call testShowMessage

と書いて[Enter]したとすると、

f:id:akashi_keirin:20190617075242g:plain

こうなる。

しかし、これは、

?Test.testShowMessage

と書くことで実行可能である。

Priveteメソッドでも、モジュール名を付ければイミディエイトから呼び出せるのである。

ただし、他のモジュールから呼び出すことはできない。

akashi-keirin.hatenablog.com

コチラをどうぞ。

テスト用コードをコメントにしておく

そこで思いついたアイディアがこれ。

f:id:akashi_keirin:20190617075223j:plain

こんな風に、メソッドのすぐ近くに、イミディエイト・ウインドウに書くテストコードを貼り付けておく。

おわりに

このようにしておくことで、テスト用メソッドをPrivate指定にしたときの面倒さが軽減される?

>

コマンドボタンから呼び出すメソッドはPrivete指定で良い

Priveteメソッドでもコマンドボタンから呼び出すことができる

常識ですか?

私は存じ上げませんでしたので、今まで何でもかんでもPublic指定にしていた。

そのせいで、「マクロの登録」ウインドウなんかを開くと、

f:id:akashi_keirin:20190617072247j:plain

こんなひどい有様にw

Privete指定のメソッドを作成する

実験用なので、なげやりなメソッドにする。

リスト1 標準モジュール
Private Sub showMessage()
  Call MsgBox("ち~んw")
End Sub

f:id:akashi_keirin:20190617072250j:plain

何のことはない、単に「ち~んw」というメッセージを表示するだけのメソッド。

コマンドボタンに登録する

このメソッドをワークシート上のコマンドボタンに登録する。

ボタンを右クリックして[マクロの登録]を選択する。

f:id:akashi_keirin:20190617072253j:plain

Privete指定ゆえ、当然この欄には出てこない。

仕方がないので、手打ち。

f:id:akashi_keirin:20190617072256j:plain

一応モジュール名から入力しているけれど、プロジェクト内で名前の重なりがなければメソッド名だけでもオッケー。

実行テスト

f:id:akashi_keirin:20190617072301g:plain

このとおり、ちゃんと呼び出せている。

おわりに

標準モジュールのアクセス修飾子には「Public」/「Privete」という両極端の二つしかないため、なかなか扱いづらいのだけれど、少なくともコマンドボタン等から呼び出すものについてはPublicにせずに済むことがわかった。これでまた一つ、オレオレコーディング規約に新たな項目が加わった。

素因数のセットを取得するFunciton

素因数のセットを取得するFunction

前二回

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

の集大成として、素因数のセットを取得するメソッドを作ってみた。

コード

モジュールごとコードを載っけておく。

リスト1 標準モジュール
Option Explicit

Private Const MAX_NUMBER As Long = (2 ^ 31) - 1
Private Const OVER_FLOW_NUMBER As String = _
  "引数が大きすぎます。"
Private Const NOT_NATURAL_NUMBER As String = _
  "引数は自然数でなければいけません。"

'///素因数のセットを配列にして返す'
Public Function getFactorizatedNumbers( _
            ByVal targetNumber As Long) As Long()
  '引数が不正だったらエラーを吐く'
  Call raiseErrorIfInvalidArg(targetNumber)
  Dim ret() As Long
  Dim n As Long
  n = 0
  ReDim ret(n)
  '1だったらreturn'
  If targetNumber = 1 Then _
    ret(n) = targetNumber: GoTo Finalizer
  '素数だったらreturn'
  If isPrimeNumber(targetNumber) Then _
    ret(n) = targetNumber: GoTo Finalizer
  '素因数を配列化'
  Dim tmp As Long
  tmp = targetNumber
  Dim primeNumber As Long
  primeNumber = 2
  'tmpが素数になるまでループ'
  Do While Not isPrimeNumber(tmp)
    '素数で割り切れたら、その素数を配列に入れ、tmpを素数で割った商'
    'にして、次のループへ'
    If tmp Mod primeNumber = 0 Then
      ret(n) = primeNumber
      n = n + 1
      ReDim Preserve ret(n)
      tmp = tmp / primeNumber
      GoTo Continue
    End If
    '割り切れなかったら次の素数をセット'
    primeNumber = getNextPrimeNumber(primeNumber)
Continue: DoEvents
  Loop
  ret(n) = tmp
Finalizer:
  getFactorizatedNumbers = ret
End Function

'///素数かどうかを判定する'
Public Function isPrimeNumber( _
            ByVal targetNumber As Long) As Boolean
  isPrimeNumber = False
  '引数が不正だったらエラーを吐く'
  Call raiseErrorIfInvalidArg(targetNumber)
  '1だったらFalse'
  If targetNumber = 1 Then Exit Function
  '2だったらTrue'
  If targetNumber = 2 Then GoTo Finalizer
  '中央の値までループして合成数判定'
  Dim turningPoint As Long
  'ループするのは平方根の近似値までで良い'
  turningPoint = Int(Sqr(targetNumber))
  Dim i As Long
  For i = 2 To turningPoint
    If targetNumber Mod i = 0 Then
'      Debug.Print i & "で割り切れる。"'
      Exit Function
    End If
  Next
Finalizer:
  isPrimeNumber = True
End Function

'次の素数を取得する'
Private Function getNextPrimeNumber( _
             ByVal targetNumber As Long) As Long
  Dim ret As Long
  ret = targetNumber + 1
  Do While Not isPrimeNumber(ret)
    ret = ret + 1
  Loop
  getNextPrimeNumber = ret
End Function

'///引数不正時にエラーを吐く'
Private Sub raiseErrorIfInvalidArg( _
              ByVal targetNumber As Long)
  '1よりも小さな数字を受け取ったらエラーを吐く'
  If targetNumber < 1 Then _
    Call Err.Raise(Number:=10001, _
                   Source:="Arg is not natural number.", _
                   Description:=NOT_NATURAL_NUMBER)
  'オーバーフローする場合エラーを吐く'
  If targetNumber > MAX_NUMBER Then _
    Call Err.Raise(Number:=10002, _
                   Source:="Arg will be over flow.", _
                   Description:=OVER_FLOW_NUMBER)
End Sub

処理の手順は、コメントに記したとおり。

VBAには、ループ処理にcontinueがないので、擬似的にcontinueを実現しようとすると無理が出る。

上掲コード中だと、Doループが

  Do While Not isPrimeNumber(tmp)
    '素数で割り切れたら、その素数を配列に入れ、tmpを素数で割った商'
    'にして、次のループへ'
    If tmp Mod primeNumber = 0 Then
      ret(n) = primeNumber
      n = n + 1
      ReDim Preserve ret(n)
      tmp = tmp / primeNumber
      GoTo Continue
    End If
    '割り切れなかったら次の素数をセット'
    primeNumber = getNextPrimeNumber(primeNumber)
Continue: DoEvents
  Loop

こうなる。ラベルにGoToすることで、continueっぽくしているけれど、Continueがラベルである以上、インデントが崩れてしまう。

もちろん、上掲の場合ならElseを使えばいいのだけれど、Elseはなるべく使いたくないので……(可読性が落ちる。)。

実験

次のような実験用コードを準備。

スト2 標準モジュール
Private Sub testGetFactorizedNumbers( _
              ByVal targetNumber As Long)
  '引数targetNumberの素因数セットを取得して配列化'
  Dim ar() As Long
  ar = getFactorizatedNumbers(targetNumber)
  '配列の要素を一つづつ取り出して「*」でつなぐ'
  Dim i As Long
  Dim str As String
  For i = LBound(ar) To UBound(ar)
    str = str & CStr(ar(i)) & " * "
  Next
  '右端の余分な文字列を除去'
  str = Left(str, Len(str) - 3)
  'イミディエイトに表示'
  Debug.Print CStr(targetNumber) & " = " & str
End Sub

このtestGetFactorizedNumbersにテキトーな引数を渡して実行。

f:id:akashi_keirin:20190601210644j:plain

うまくいっている。

おわりに

あいかわらず、仕事の役に立ちそうにはありません。

頭の体操。

次の素数を取得するFucntion

次の素数を取得するFunction

数字を受け取って、その数字よりも大きい最小の素数を返すメソッドを作った。

コード

リスト1 標準モジュール
Private Function getNextPrimeNumber( _
             ByVal targetNumber As Long) As Long
  Dim ret As Long
  ret = targetNumber + 1
  Do While Not isPrimeNumber(ret)
    ret = ret + 1
  Loop
  getNextPrimeNumber = ret
End Function

見てのとおり、途中、素数判定には自作のisPrimeNumberメソッドを用いている。

isPrimeNumberメソッドのコードをリスト2に示す。

スト2 標準モジュール
Option Explicit

Private Const MAX_NUMBER As Long = (2 ^ 31) - 1
Private Const OVER_FLOW_NUMBER As String = _
  "引数が大きすぎます。"
Private Const NOT_NATURAL_NUMBER As String = _
  "引数は自然数でなければいけません。"

Public Function isPrimeNumber( _
            ByVal targetNumber As Long) As Boolean
  isPrimeNumber = False
  '引数が不正だったらエラーを吐く'
  Call raiseErrorIfInvalidArg(targetNumber)
  '1だったらFalse'
  If targetNumber = 1 Then Exit Function
  '2だったらTrue'
  If targetNumber = 2 Then GoTo Finalizer
  '中央の値までループして合成数判定'
  Dim turningPoint As Long
  'ループするのは平方根の近似値までで良い'
  turningPoint = Int(Sqr(targetNumber))
  Dim i As Long
  For i = 2 To turningPoint
    If targetNumber Mod i = 0 Then
'      Debug.Print i & "で割り切れる。"'
      Exit Function
    End If
  Next
Finalizer:
  isPrimeNumber = True
End Function

Private Sub raiseErrorIfInvalidArg( _
              ByVal targetNumber As Long)
  '1よりも小さな数字を受け取ったらエラーを吐く'
  If targetNumber < 1 Then _
    Call Err.Raise(Number:=10001, _
                   Source:="Arg is not natural number.", _
                   Description:=NOT_NATURAL_NUMBER)
  'オーバーフローする場合エラーを吐く'
  If targetNumber > MAX_NUMBER Then _
    Call Err.Raise(Number:=10002, _
                   Source:="Arg will be over flow.", _
                   Description:=OVER_FLOW_NUMBER)
End Sub

引数がおかしかったときにはエラーを吐くようにしている。

2の31乗マイナス1(2147483647)までならオーバーフローしなかったので、最大値を2の31乗マイナス1にしておいた。

実験

標準モジュールのオブジェクト名を「MathUtil」にしている(本当は「Math」にしたかったんだが名前が衝突するので……。)ので、

?MathUtil.getNextPrimeNumber([引数])

の形で、イミディエイト・ウインドウ上で実行。

f:id:akashi_keirin:20190601203804j:plain

ちゃんと取得できている……よね?

おわりに

これで、素因数分解をするプログラムを書く準備ができた。

面白くなってきたぞ。

仕事の役には立たないだろうけど。