Staticプロシージャというものがある
Staticプロシージャというものがある
知ってました?
私は全然知らなかった。
何気なく『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
の値がインクリメントされるはずだ。
いざ、実行!
ほ、ほんまや……。
おわりに
何か、使いどころはあるかなあ。
小ネタでした。
PublicNotCreatableの意味
PublicNotCreatableの意味
クラスモジュール使いであっても、あっちこっちで使い回すようなクラスモジュールを作ったりしなければ、存在にすら気づかないのが、クラスモジュールのInstancing
プロパティではなかろうか。
Instancing
プロパティに、「Private
」と「PublicNotCreatable
」の二種類の設定値があることは知っていたが、余り深く意味を考えたことがなかった。
特に、「PublicNotCreatable
」の方は、「NotCreatable
」の文字だけを見て、
インスタンスが作れねえのかよ、不便だな
とか、わけのわからない理解をしていた。
ちょっとづつわかってきたかも知れないので、メモ。
Private/Publicの違い
まず、「Private
」と「Public
」の違い。
これは、
別プロジェクトに対して
「Private
」か「Public
」か、ということ。
VBAによるマクロ作成の場合、1ブック(プロジェクト)内で閉じた処理であることが多いので、普通に使っている限り、まず意識することがないポイントだと思う。
たとえば、
このようなプロジェクト構成になっているとする。(おい、ファイル名は綴りが間違えてるじゃねえかw)
「Referring
」というプロジェクトが、「Referred
」というプロジェクトを参照している。
参照しているということは、「Referring
」(参照元)から「Referred
」(参照先)へのつながりをつけているということ。
つながりがついているから、「Referring
」(参照元)からは、「Referred
」(参照先)の中の「Public
」なものについては〈見える〉(呼び出せる)ということになる。
ここで言う〈見える〉というのは、コードウインドウ等に〈表示できる〉ということではない(それは、〈ユーザーから見える〉ということだ)。
再度、プロジェクト エクスプローラー画像を。
ご覧のとおり、参照先である「Referred
」には、「HogePrivate
」と「HogePublic
」という二つのクラスモジュールが置かれている。
クラス名からも明らかなとおり、「HogePrivate
」は、Instancing
プロパティを「Private
」に、「HogePublic
」の方は「PublicNotCreatable
」に設定している。
ご覧のように、入力時のヒントには「HogePublic
」の方しか出てこない。
これが、〈見える〉ということだ。
Referring
プロジェクトからは、Referred
プロジェクト内のクラスのうち、Private
なHogePrivate
クラスは〈見えない〉が、Public
なHogePublic
クラスは〈見える〉ということだ。
では、NotCreatableとは
最初、「NotCreatable
」という字面から、「インスタンスが作れないクラス」のことだと思っていた。
しかし、Instancing
プロパティを「PublicNotCreatable
」に設定していても、普通にNew
でインスタンス化することができていたので(当たり前)、「わけわからんなー」で済ませていたのだった。
これは要するに
別プロジェクト内でインスタンスが作れない
ということだったのだ。
今回の例の場合だと、
Referringプロジェクト上で、Referredプロジェクト下のクラスのインスタンスを作ることはできない
というだけのことだった。
このように、「不正」呼ばわりされて、インスタンスが作れないのだ。
もちろん、上の例での変数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
モジュール上のメソッドを呼び出そうと試みる。
ちなみに、プロジェクトエクスプローラーはこの状態。
「シートモジュール活用その2.xlsm」の標準モジュールから呼び出すのだ。
しかし、このように、Public
メソッドであっても呼び出すことができない。
参照設定していないからである。
そこで、
このように参照設定する。
再度、挑戦。
今度は、Intellisenseが働いている!
よく見ると、ヒントが表示されるのはPublic
指定のshowMessagePublic
メソッドだけで、Friend
指定のshowMessageProtected
の方は表示されていない。
呼び出し用プロシージャを実行してみる。
このように、Friend
指定の方は呼び出すことができない。
一方、
Public
指定の方は、呼び出すことができた。
おわりに
別プロジェクトからオブジェクトモジュールのメソッドを呼び出したり、プロパティを参照したりする場面自体、VBAでは想定しにくいので、いまいち使いどころがよくわからないが、Public
でもなく、Private
でもないこの振る舞いを理解しておくと、いづれ何かの機会に役立つかもしれない???
テスト用メソッドもPrivete指定にする
テスト用メソッドもPrivete指定にする
Privete指定でもVBE上で実行できる
これは、考えてみたら当り前なんだけれど、全然気づいていなかった。
イミディエイト・ウインドウ上での実行
次のようなメソッド群を用意する。
リスト1 標準モジュール
Private Sub showMessage() Call MsgBox("ち~んw") End Sub Private Sub testShowMessage() Call showMessage End Sub
上が呼び出され用のメソッドで、下が呼び出しテスト用のメソッド。いづれもPrivete
指定にしてある。
たとえば、testShowMessage
をイミディエイト・ウインドウから呼び出そうとして、
Call testShowMessage
と書いて[Enter]したとすると、
こうなる。
しかし、これは、
?Test.testShowMessage
と書くことで実行可能である。
Privete
メソッドでも、モジュール名を付ければイミディエイトから呼び出せるのである。
ただし、他のモジュールから呼び出すことはできない。
コチラをどうぞ。
テスト用コードをコメントにしておく
そこで思いついたアイディアがこれ。
こんな風に、メソッドのすぐ近くに、イミディエイト・ウインドウに書くテストコードを貼り付けておく。
おわりに
このようにしておくことで、テスト用メソッドをPrivate
指定にしたときの面倒さが軽減される?
>
コマンドボタンから呼び出すメソッドはPrivete指定で良い
Priveteメソッドでもコマンドボタンから呼び出すことができる
常識ですか?
私は存じ上げませんでしたので、今まで何でもかんでもPublic
指定にしていた。
そのせいで、「マクロの登録」ウインドウなんかを開くと、
こんなひどい有様にw
Privete指定のメソッドを作成する
実験用なので、なげやりなメソッドにする。
リスト1 標準モジュール
Private Sub showMessage() Call MsgBox("ち~んw") End Sub
何のことはない、単に「ち~んw
」というメッセージを表示するだけのメソッド。
コマンドボタンに登録する
このメソッドをワークシート上のコマンドボタンに登録する。
ボタンを右クリックして[マクロの登録]を選択する。
Privete
指定ゆえ、当然この欄には出てこない。
仕方がないので、手打ち。
一応モジュール名から入力しているけれど、プロジェクト内で名前の重なりがなければメソッド名だけでもオッケー。
実行テスト
このとおり、ちゃんと呼び出せている。
おわりに
標準モジュールのアクセス修飾子には「Public
」/「Privete
」という両極端の二つしかないため、なかなか扱いづらいのだけれど、少なくともコマンドボタン等から呼び出すものについてはPublic
にせずに済むことがわかった。これでまた一つ、オレオレコーディング規約に新たな項目が加わった。
素因数のセットを取得するFunciton
素因数のセットを取得するFunction
前二回
の集大成として、素因数のセットを取得するメソッドを作ってみた。
コード
モジュールごとコードを載っけておく。
リスト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
にテキトーな引数を渡して実行。
うまくいっている。
おわりに
あいかわらず、仕事の役に立ちそうにはありません。
頭の体操。
次の素数を取得する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([引数]
)
の形で、イミディエイト・ウインドウ上で実行。
ちゃんと取得できている……よね?
おわりに
これで、素因数分解をするプログラムを書く準備ができた。
面白くなってきたぞ。
仕事の役には立たないだろうけど。