こんばんわ。最近自作のVBAモジュールのライブラリ化を夢見てアレコレしていることりちゅんです。
ライブラリ化を進めるに当たって、今まで適当に省略して済ませてきたモジュール名の明示が重要な課題となってきました。
そんな中、良い感じのアイディアが浮かんだので紹介しておきます。
VBAの組み込み関数で、「VBA.」を省略している箇所を検知したくなった。
— ことりちゅん@えくせるちゅんちゅん (@KotorinChunChun) October 1, 2019
こんなのを目視で探していたら発狂してしまう。
だが、私は気がついた。
同名のSubをPublicで宣言すれば良いということに。 pic.twitter.com/CAZMGlIPuv
一般VBAerとツヨツヨVBAerの違い?
本記事では暫定的に、
VBAユーザーの9割以上を占めると思われる普通の人・・・一般人
と
ごく一部の本格的にプログラミングをしている人・・・ツヨツヨ
と呼びます。
分かりやすいように省略しただけで、深い意味はありません。
VBAでは予約語を除く関数等は、全てライブラリ(プロジェクト)名.クラス(モジュール)名.メンバ
という構成になっています。(たぶん)
でもVBAでプログラミングする時は、いきなりメンバ
から書き始めても使えるように出来ています。
従って、普通の人は以下の様にコーディングします。
Sub Test1_1() Const TXT = "あいうえお" Dim s As String s = Left(TXT, 2) Debug.Print s 'あい と出力 End Sub
しかし、ツヨツヨな方の書いたソースコードを見ると、十中八九、以下のようにコーディングされています。
Sub Test1_2() Const TXT = "あいうえお" Dim s As String s = VBA.Strings.Left$(TXT, 2) Debug.Print s 'あい と出力 End Sub
違うのはs = VBA.Strings.Left$(TXT, 2)
の部分。
この場合、VBA
がライブラリ名でStrings
がモジュール名になります。
Left
がLeft$
になっているのは、実は戻り値型を文字列型と明示することで高速化できるからだと思われます。
どの関数が何のモジュールに属しているかは、オブジェクトブラウザを見ればすぐに分かります。
尚、先日の記事でもやりましたが、ライブラリ名.関数名(VBA.Left
)や、モジュール名.関数名(Strings.Left
)なんて表記もできます。
それにしても、毎回VBA.Strings.
なんて入力するのは面倒です。
なんでこんなことをするんでしょう?
ライブラリ名.モジュール名を省略したことによる問題点
何故ツヨツヨな人は省略をしないのか、私には心の内は分からないのですが、ある程度想像は付きます。
- 組み込み関数と同名の関数が作成されると、そちらを使用するようになるため。
- 省略表記だと「参照設定の破損。プロジェクトの破損。VBAのバグ」などにより、環境に依存したコンパイルエラーが起こりやすいため。
- 開発環境等の都合により省略できないようになっているため?
- 私の知らない理由
(詳しい方、他に理由があれば教えて下さい)
なにはともあれ、私もツヨツヨなコーディングに挑戦することにしました。
省略された事を検知する方法
ツヨツヨコーディングするぜ!!!と宣言したは良いものの、膨大なソースコードから省略されている場所を探し出して置き換えていくのは困難を極めます。
一件づつ置換していくという方法もありますが、将来を考えるといつか絶対忘れます。
なんとかして、自動的に検知する方法はないだろうかと考えたところ、いつもお世話になっている方法が浮かびました。
下記の記事では、偽のプロパティで上書きするモジュールを作成する事で、Excel固有のRange
やWorksheets
やCells
などを省略表記をするとコンパイルエラーが起こるようにしています。
そう、上書きすれば良いのです!!!
リライトです!
実は異なるプロジェクト(ライブラリ)で宣言された関数と、自身のプロジェクトで宣言された関数が同名の時、自身のプロジェクトの関数を優先する。という暗黙の了解があるんですよね。
この性質を生かして、VBAの組み込み関数も上書きしてしまえば良いわけです。
VBA組み込み関数の省略を機械的に検知する
というわけで、早速こんなプロシージャを作ってみました。
Sub IsArray(): End Sub
大成功!
我ながら天才的な発想である
が、実は「予約語」となっている関数が多くて、完全にはブロックできていない。
自作関数の省略を検知する
私のアイディアは、組み込み関数だけでは終わらない。
自作したモジュールの省略も、この方法で検知できることに気がついてしまいました。
全ての関数を別のモジュールに複製するです。
そんな中、関数だけ別途定義しなおさなくても、モジュール丸ごと複製すれば良いのではないかと返事を頂き、ごもっともだと思いました。
再現プログラムは以下のような感じになります。
'kccLib_Stringモジュール Function LeftStr(s, d) LeftStr = Split(s, d)(0) End Function
'省略検知用モジュール(再定義。またはモジュールの複製) Sub LeftStr(): End Sub
'利用者モジュール Sub Test2_1() Const TXT = "あいうえお" Dim s As String s = LeftStr(TXT, "う") Debug.Print s End Sub 'あい が出力
大成功!
問題点1
自作関数の省略検知では、新しいプロシージャを作るたびにダミーのモジュールを更新しなければならないので、若干の手間がかかります。
本体のプロシージャが名変更されたり削除された場合に、変更内容が複製しなおさないとダミーのプログラムを呼んでしまうためコンパイルエラーが出ないという危険性があります。
従って、普段は記載せず、必要な時に複製して確認する。という使い方をすべきでしょう。
常駐させてもインテリセンスからは除外されないので、使えるかのように見えてしまいます。
リアルタイムに常時保護するような使い方には向いていないと思われます。
また、ダミーの関数が呼ばれてしまう恐れもあるため、常駐させるなら停止するように以下の様にしておくと良さそうです。
Private Const ERRMSG = "このプロシージャは廃止されました" Sub MyFunc(): Err.Raise 9999, , ERRMSG: End Sub
Sub Test_Subブロック() 'Err.Raiseで実行時エラーが限界 Call MyFunc 'Sub によりブロックできる記述 ' Call MyFunc(1) ' Dim v: v = MyFunc(1) ' Set MyFunc = 1 ' Set MyFunc = Nothing End Sub
追記:よく考えたら、Property Set
で上書きしたほうが良さそうです。
Property Set MyFunc(dummy): End Property
この記法なら、元々関数として使われていた記法を完全にブロックできます。
Sub Test_Setブロック() 'Property Set によりブロックできる記述 ' Call MyFunc ' Call MyFunc(1) ' Dim v: v = MyFunc(1) ' Set MyFunc = 1 'Err.Raiseで実行時エラーが限界 'だが、元々が関数であればこの記法はあり得ない。 Set MyFunc = Nothing End Sub
ただ、これを追記するのは手間がかかりすぎるため、ソースコードを解析してこのコードを発生させるプログラムを用意したほうが良さそうです。
(気が向いたら作るかも?)
問題点2
実は異なるモジュールで宣言された関数と、自身のモジュールで宣言された関数が同名の時、自身のモジュールを優先する。という、これまた暗黙の了解があります。
だから以下のように同名関数を宣言した状態で利用すると、意図せぬ関数を呼んでしまかもしれないのです。
'利用者モジュール '上書き Function LeftStr(s, d) LeftStr = s End Function '利用者モジュール Sub Test2_1() Const TXT = "あいうえお" Dim s As String s = LeftStr(TXT, "う") Debug.Print s End Sub 'あい が出力
残念ながら、良い解決方法は浮かびませんでした。
が、ですね。これはこれで使えることに気が付きました。(問題3へ)
問題点3
現状の方法では、全てのモジュールで省略が禁止されてしまいます。
これは以下の全ての状況に言えることです。
ここまでの主張と逆行するみたいですが、省略を認めたい時に面倒という問題が発生します。
具体的には、
なんかが上げられます。
そのためには先ほどあげた問題2の方法が使えます。
今度は使用を認めたいモジュールにPrivate
でラッパー関数やプロパティを記述すれば良いのです。
Private Property Get Rows(Optional v1, Optional v2) As Range Set Rows = Excel.Rows(v1, v2) End Property
面倒ですが、省略を認めたいものだけ必要なテストモジュールの先頭に記載しておきましょう。
省略を認めない方法
実は自作のモジュール名の記述を強制するには、もう一つ良い方法があります。というか、こっちが正攻法です。
標準モジュールではなくクラスモジュールを作成し、オプションで「既定のインスタンス」にしておくことで、やりたいことが実現できると思います。
作り方を簡単に言うと、クラスモジュールを一旦ファイルにエクスポートして、テキストエディタで開き、
Attribute VB_PredeclaredId = False
を
Attribute VB_PredeclaredId = True
に変更してインポートし直せば完成です。
詳しい話は省略しますが、この方法でモジュール名の記載を強制することができるので便利です。
リアルタイムに保護したい場合は、この方法を使うべきでしょう。
組み込み関数の省略表記を無効化するモジュール
最後に今回作成した組み込み関数の省略禁止モジュールを記載する。
ドキュメントとオブジェクトブラウザを使って一通り網羅したつもりですが、足りないものがあれば教えて頂けると幸いです。
尚、VBAの「予約語」で上書きできないものが多数見つかったため、完全にはブロックできていない。
(コメントアウトされているものが、予約後だったもの)
また、常時発動しないように条件付きコンパイルDISABLE_GLOBAL = 1
を採用しているので注意されたし。
'DisableVBAGlobal ' 組み込み関数の省略表記を禁止するモジュール ' 'えくせるちゅんちゅん ' '2019/10/02 : 公開 '2019/10/03 : 訂正 ' '本モジュールを有効化したい場合は True 使いたくない場合は False を記載 #Const DISABLE_GLOBAL = True #If DISABLE_GLOBAL Then Option Explicit '数学関数 VBA.Mathメンバ 'Public Sub Abs(): End Sub Public Sub Atn(): End Sub Public Sub Cos(): End Sub Public Sub Exp(): End Sub Public Sub Log(): End Sub Public Sub Rnd(): End Sub 'Public Sub Sgn(): End Sub Public Sub Sin(): End Sub Public Sub Sqr(): End Sub Public Sub Tan(): End Sub Public Sub Round(): End Sub 'その他から移動 'データ型変換関数 VBA.Conversion 'Public Sub CBool(): End Sub 'Public Sub CByte(): End Sub 'Public Sub CCur(): End Sub 'Public Sub CDate(): End Sub 'Public Sub CDbl(): End Sub 'Public Sub CDec(): End Sub 'Public Sub CInt(): End Sub 'Public Sub CLng(): End Sub 'Public Sub CLngPtr(): End Sub '追加 'Public Sub CSng(): End Sub 'Public Sub CStr(): End Sub 'Public Sub CVar(): End Sub Public Sub CVDate(): End Sub 'Public Sub CVErr(): End Sub '変換関数から移動 Public Sub Error(): End Sub '変換関数から移動 'Public Sub Fix(): End Sub '数学関数から移動 Public Sub Hex(): End Sub '変換関数から移動 'Public Sub Int(): End Sub '数学関数から移動 Public Sub MacID(): End Sub 'その他から移動 Public Sub Oct(): End Sub '変換関数から移動 Public Sub Str(): End Sub '変換関数から移動 Public Sub Val(): End Sub '変換関数から移動 '文字列関数 VBA.Strings Public Sub Asc(): End Sub '変換関数から移動 Public Sub AscB(): End Sub '変換関数から移動 Public Sub AscW(): End Sub '変換関数から移動 Public Sub Chr(): End Sub '変換関数から移動 Public Sub ChrB(): End Sub '変換関数から移動 Public Sub ChrW(): End Sub '変換関数から移動 Public Sub Filter(): End Sub 'その他から移動 Public Sub Format(): End Sub '変換関数から移動 Public Sub FormatCurrency(): End Sub 'その他から移動 Public Sub FormatDateTime(): End Sub 'その他から移動 Public Sub FormatNumber(): End Sub 'その他から移動 Public Sub FormatPercent(): End Sub 'その他から移動 Public Sub InStr(): End Sub Public Sub InStrB(): End Sub Public Sub InStrRev(): End Sub Public Sub Join(): End Sub Public Sub LCase(): End Sub Public Sub Left(): End Sub Public Sub LeftB(): End Sub 'Public Sub Len(): End Sub 'Public Sub LenB(): End Sub Public Sub Ltrim(): End Sub Public Sub Mid(): End Sub Public Sub MidB(): End Sub Public Sub MonthName(): End Sub Public Sub Replace(): End Sub Public Sub Right(): End Sub Public Sub RightB(): End Sub Public Sub Rtrim(): End Sub Public Sub Space(): End Sub Public Sub Split(): End Sub Public Sub StrComp(): End Sub Public Sub StrConv(): End Sub 'Public Sub String(): End Sub Public Sub StrReverse(): End Sub Public Sub Trim(): End Sub Public Sub UCase(): End Sub Public Sub WeekdayName(): End Sub 'VBA.[_HiddenModule] 'Public Sub Array(): End Sub 'Public Sub Input(): End Sub 'Public Sub InputB(): End Sub Public Sub ObjPtr(): End Sub '追加 Public Sub StrPtr(): End Sub '追加 Public Sub VarPtr(): End Sub '追加 Public Sub Width(): End Sub '追加 'VBA.Information Public Sub Erl(): End Sub '追加 Public Sub Err(): End Sub '追加 Public Sub IMEStatus(): End Sub Public Sub IsArray(): End Sub Public Sub IsDate(): End Sub Public Sub IsEmpty(): End Sub Public Sub IsError(): End Sub Public Sub IsMissing(): End Sub Public Sub IsNull(): End Sub Public Sub IsNumeric(): End Sub Public Sub IsObject(): End Sub Public Sub QBColor(): End Sub Public Sub RGB(): End Sub Public Sub TypeName(): End Sub Public Sub VarType(): End Sub 'VBA.Interaction Public Sub AppActivate(): End Sub '追加 Public Sub Beep(): End Sub '追加 Public Sub CallByName(): End Sub Public Sub Choose(): End Sub Public Sub Command(): End Sub Public Sub CreateObject(): End Sub Public Sub DeleteSetting(): End Sub '追加 'Public Sub DoEvents(): End Sub Public Sub Environ(): End Sub Public Sub GetAllSettings(): End Sub Public Sub GetObject(): End Sub Public Sub GetSetting(): End Sub Public Sub IIf(): End Sub Public Sub InputBox(): End Sub Public Sub MacScript(): End Sub Public Sub MsgBox(): End Sub Public Sub Partition(): End Sub Public Sub SaveSetting(): End Sub '追加 Public Sub SendKeys(): End Sub '追加 Public Sub Shell(): End Sub Public Sub Switch(): End Sub 'VBA.FileSystem Public Sub ChDir(): End Sub '追加 Public Sub ChDrive(): End Sub '追加 Public Sub CurDir(): End Sub Public Sub Dir(): End Sub Public Sub EOF(): End Sub Public Sub FileAttr(): End Sub Public Sub FileCopy(): End Sub '追加 Public Sub FileDateTime(): End Sub Public Sub FileLen(): End Sub Public Sub FreeFile(): End Sub Public Sub GetAttr(): End Sub Public Sub Kill(): End Sub '追加 Public Sub Loc(): End Sub Public Sub LOF(): End Sub Public Sub MkDir(): End Sub '追加 Public Sub Reset(): End Sub '追加 Public Sub RmDir(): End Sub '追加 'Public Sub Seek(): End Sub Public Sub SetAttr(): End Sub '追加 'VBA.DateTime Public Sub Calendar(): End Sub '追加 'Public Sub Date(): End Sub Public Sub DateAdd(): End Sub Public Sub DateDiff(): End Sub Public Sub DatePart(): End Sub Public Sub DateSerial(): End Sub Public Sub DateValue(): End Sub Public Sub Day(): End Sub Public Sub Hour(): End Sub Public Sub Minute(): End Sub Public Sub Month(): End Sub Public Sub Now(): End Sub Public Sub Second(): End Sub Public Sub Time(): End Sub Public Sub Timer(): End Sub Public Sub TimeSerial(): End Sub Public Sub TimeValue(): End Sub Public Sub Weekday(): End Sub Public Sub Year(): End Sub 'VBA.Financial Public Sub DDB(): End Sub Public Sub FV(): End Sub Public Sub IPmt(): End Sub Public Sub IRR(): End Sub Public Sub MIRR(): End Sub Public Sub NPer(): End Sub Public Sub NPV(): End Sub Public Sub Pmt(): End Sub Public Sub PPmt(): End Sub Public Sub PV(): End Sub Public Sub Rate(): End Sub Public Sub SLN(): End Sub Public Sub SYD(): End Sub '該当ライブラリ無し Public Sub LoadPicture(): End Sub 'Public Sub Spc(): End Sub 'Public Sub Tab(): End Sub 'Public Sub LBound(): End Sub 'Public Sub UBound(): End Sub #End If
参考資料
自作関数については、既出のアイディアであったことが判明した。
我ながら、ナイスなアイディアである。
— チン☆テクラ (@akashi_keirin) October 2, 2019
😁https://t.co/SCz0PenT5T
まとめ
ソースコードを極力省略しない記法に揃えることで、バグの起こりにくい強靭なプログラムを作ることができる。
思わぬ所で時間を食ってしまったが、これでまた一歩、私の野望に近づいたと信じたい。
以上
何か御座いましたらコメント欄、またはTwitterからどうぞ♪
それではまた来週♪ ちゅんちゅん(・8・)