t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBAからVBScriptへマクロを移植する方法

今回はVBScriptをやってみたいVBA使いの方へ、VBAで書いたコードをVBSに移植する方法を説明しようと思う。
特にあまり知られていないであろう最初から移植性を意識したコードを書く方法について紹介する。

執筆のきっかけになったのはこちらの記事。
chemiphys.hateblo.jp

VBScriptの作り方

テキストエディタでコードを書いて、拡張子をvbsとして保存するだけ。
実行するにはダブルクリックする。
ドラッグ&ドロップしたファイルを処理するようなスクリプトも作れる。

VBAとの違い

エントリーポイント(プログラムの開始点)がファイルの先頭

VBAの場合、Subプロシージャを複数作ってそれぞれがエントリーポイントになるけれど、VBSの場合はファイルの先頭がエントリーポイントになる。
つまりSubプロシージャなどを書かずに、いきなりMsgBox "Hello"と書けば動くのだ。

意外に知られていないのは、普通にSubプロシージャも書けるということ。
エントリーポイントからCallしてやれば動作する。

Call Main
Sub Main()
    MsgBox "Hello"
End Sub

VBAと同じようにプロシージャの集まりとして作っておいてCallするという方法をとれば、VBAプログラムとの高い互換性を保ちつつスクリプト化できるのでおすすめ。

変数宣言に型を持たせられない

VBAならDim a As Integerなどと書けるところ、VBSではDim aとしか書けない。
私はよく、Dim a 'As Integerという風にAs以降をコメント化している。

イミディエイトウインドウが使えない

つまりVBAから移植するためにはDebug.Printは別の方法で書き直さないといけない。
Debug.Printはそもそもデバッグ機能なので本番マクロで使うのもどうなのって思うけど、個人用マクロで重宝するのは確か。

無いなら作ってしまおう。

'下で定義するDebugPrintクラスをもとに変数Debugを準備
Dim Debug: Set Debug = New DebugPrint

Call Main
 
Sub Main()
    Debug.Print "こんにちは"
    Debug.Print "さようなら"
    Debug.Flush 'FlushしてはじめてPrintした内容がMsgBoxで表示される。
End Sub

'以下にDebugPrintクラスを定義
Class DebugPrint
    Private message

    Public Sub Print(msg)
        message = message & msg & vbNewLine
    End Sub

    Public Sub Flush()
        MsgBox message
        Call Clear
    End Sub

    Public Sub Clear()
        message = ""
    End Sub
End Class

こうするとVBAからの移植性はアップする。
メインコードをわざわざ書き直さなくても、Flushを呼ぶという変更だけで済む。

参照設定できないので遅延バインディングになる

参照設定が使えないため、外部オブジェクトの作成はCreateObjectで行うことになる。
VBAで参照設定をやっている場合、以下を参考にCreateObject方式に書き換える。

参照設定とCreateObjectの対応リスト - You.Activate

その他の特徴

  • Rnd関数を使用する場合、その前の任意の行に「Call Randomize()」を挿入しないとランダムにならない。
  • 配列の宣言にDim a(1 to 3)といった指定ができず、3つの要素がほしい場合はDim a(2)と書いて0,1,2の3つを作るしかない。
  • Select文がVBAより低機能で基本的な書き方しかできない。
  • 日本語変数が利用できない。

追記

Twitterで、有益なリンクが流れてきたので追記(id:imihitoさんありがとう)
VBA の機能で VBScript に含まれていない機能

VBSではCollectionが使えないようなので、代替手段として以下の記事もどうぞ。
imihito.hatenablog.jp

VBAからVBScriptへマクロを移植する方法

  1. マクロをそのまま貼り付ける。
  2. 変数宣言から型の宣言を取り除く(あるいはコメントアウト)。
  3. エントリーポイントから実行したいマクロをCallする。
  4. 参照設定からCreateObject形式に切り替える。

上記で大体動くが、Debug.Printを使っていたり、Select文の高度な機能(Is比較、n To n表記など)を利用している場合は修正が必要になる。

VBScriptからVBAへマクロを移植する方法

上位互換とは言わないが、基本的に、Sub~End Subでくくってやれば大体のスクリプトはそのまま動く。
VBAも型宣言なしで動作するし、CreateObjectにも対応しているのでVBSからVBAへは極めて移植性が高い。
ただしWScript.Echoを使っている場合はMsgBoxかDebug.Printへ変更が必要である。

長いスクリプトをそのまま貼り付けると、長いプロシージャが出来上がるのであまり良くない。
VBScriptにおいても最初からプロシージャにコードを書くようにし、適切にプロシージャを分割しておくとVBAに移植してもスッキリと見通しの良いコードになる。

VBScriptにオススメのエディタ

やはりVBAで使用するVBエディタが一番楽である。

VBAで最初からVBSへの移植性を意識したプログラムを書く場合、外部オブジェクトの利用は以下のように参照設定とCreateObjectのハイブリッドにしておくと良い。

Dim fso As FileSystemObject
Set fso = CreateObject("Scripting.FileSystemObject")

そうすると、VBエディタで書くときはオブジェクトメンバーの入力候補が出るし、VBSに直すときはこのようにAs以降をコメントアウトするだけで済む

Dim fso 'As FileSystemObject
Set fso = CreateObject("Scripting.FileSystemObject")

VBScript関連書籍

まずVBAができる人にお勧めするのは以下の1冊。

WSHクイックリファレンス 第2版

WSHクイックリファレンス 第2版

これはFileSystemObjectやDictionaryなどのWSHのオブジェクトについて使い方がわからないときにリファレンスとして重宝する。
また、VBAでもこれらのオブジェクトは多用するが、VBAの書籍よりも詳しく書かれている。
VBScriptJScriptに両対応しているのでJavaScript派の方にもおススメ。

以下はあまり良く知らないけど、実質VBScriptの関連書籍って現行品はこれくらいしか無いので一応ひととおり掲載だけしておく。

Windows自動処理のための WSHプログラミングガイド 増補改訂版

Windows自動処理のための WSHプログラミングガイド 増補改訂版

[改訂版] VBScriptポケットリファレンス (POCKET REFERENCE)

[改訂版] VBScriptポケットリファレンス (POCKET REFERENCE)

最速攻略 VBScriptサンプル大全集 Windows7/Vista/XP/2000対応

最速攻略 VBScriptサンプル大全集 Windows7/Vista/XP/2000対応

VBScript逆引き大全500の極意

VBScript逆引き大全500の極意

ただ私の場合はほぼVBAって感じのVBScriptを書くのであまり書籍を参考にした覚えはないな。
ただドラッグ&ドロップ操作などのVBSに特有の機能もあるので一冊くらい専門の本を持っておいても良いかなとは思う。

VBSでクラスを使う方法

以下の記事がおススメだけれど、まずクラスって何って方にはちょっとザックリしすぎてるかなとは思う。
http://tuka.s12.xrea.com/index.xcg?p=VBS%A4%CE%A5%AF%A5%E9%A5%B9%A5%AA%A5%D6%A5%B8%A5%A7%A5%AF%A5%C8

クラスについてよく分からない方はまずVBAでクラスを使うこちらの記事と
ateitexe.com

手前味噌だけど、以下の記事がオススメ。
thom.hateblo.jp

VBA Boolean型をTrueやFalseと比較することの是非について

f:id:t-hom:20170509093420p:plain
If文の条件式でBoolean型をTrueやFalseと比較することは昔から論争の種だった。

たとえば以下のようなIf文。

If IsNumeric(x) = True Then
If IsDate(x) = False Then

わざわざTrueやFalseと比較しなくても、このように書くことができる。

If IsNumeric(x) Then
If Not IsDate(x) Then


Boolean型を返す関数をTrueやFalseと比較するのは初心者に多い書き方であるが、上級者のなかには冗長であることは承知のうえであえてTrueやFalseと比較するという方もいる。以降、この勢力を比較派と呼ぶことにする。

そしてこれに反対する勢力、つまり後者の書き方を較派と呼ぶことにする。

※ちなみにここで論じるのはあくまでBoolean型とTrue/Falseとの比較の是非であって、それ以外の数値や文字列については非較派であっても比較演算子を用いる。もちろん。

さあ、比較派と非較派、あなたはどっち?

ちなみに私は非較派。

ここから先は、なんちゃって対談形式で双方の主張を戦わせる。
もちろん、参加者も評者も私なので、どっちにしても私が肩入れする非較派が勝つんだけど。フハハハ。

比較派と非較派の対談

非較派:
さっそくだけど、まず、If IsNumeric(x) = True Thenって書きかたは無駄が多いよね。
たとえばIsNumeric(x)がTrueを返したらIf True = True Thenになるでしょ、その次にTrueとTrueをイコールで比較するとTrueになるからIf True Thenになるでしょ。
このひと手間無駄じゃない?IsNumeric(x)がTrue/Falseを返すんだから、そのまま使えばいいじゃない。

比較派:
いやいや。無駄っていうけど最近のPCだと実行時間は殆ど変らないでしょ。コンピューターはますます速くなってくるのにそんなケチくさいこと言わなくても。それにIf IsDate(x) = False Thenのケースはどうなの。たとえばTrueを返してFalseと比較しても、Trueを返してNotで反転させてもどちらにしても手間は変わらなくない?

非較派:
うーん。それは確かに。でも美しくないんだよな。たとえ速度が変わらなくても中で無駄なことしてるってのは。それに文章としても不自然に見えるんだよね。もし「xが日付」が偽と等しければ、なんて言い方しないよね。If Not IsDate(x) Thenであれば、そのまま、もしxが日付でなければと読めるし。

比較派:
プログラミング言語は英語とはまた別でしょ。

非較派:
それはそうだけど、VBってどちらかといえば英語みたいに読めることを目指した言語じゃない?C言語だと条件は丸カッコで括るところをVBAだとIf 条件 Thenだし、AndとかOrも記号じゃなくて英単語そのまま使っちゃってるし。英語には成れないけど、なるべく英語に近づけたほうが自然だと思うな。

比較派:
なるほどね。じゃあこれはどう?If文には条件式を書くって教わるでしょ?例外が増えちゃうのは良くないと思うんだ。

非較派:
うん、そこは賛成。でも「If IsNumeric(x) Then」って書いてもちゃんと「条件式」になってるし、例外も増えないよ。

比較派:
えっ?どういうこと?

非較派:
条件式って数式の親戚みたいなものと勘違いしてない?
ここでいう条件式ってのは、Boolean型に評価される式全般のことだよ。

比較派:
ちょっと待って、いきなりムズっ!Boolean型は分かるけど評価って何?式全般ってさっきのIsNumeric(x)のどのへんが式なの?式ってふつうはイコールとか、プラスとかマイナスとか記号使うよね?

非較派:
うーん。見事に数学を引きずってるね。世の中には数式以外の式もあるでしょ。たとえば化学式とか。

比較派:
あーっ。あれも式か。ん?じゃあ式って言葉の定義はなんなんだ?

非較派:
ググってみたら、「ある関係・構造を表すために、記号を連ねて書いたもの。」だってさ。

比較派:
ほらやっぱ記号…。IsNumeric(x)のどの辺が記号なの?まさかカッコとか言わないよね。

非較派:
記号をググってみたら「一定の事柄を指し表すために使う、文字・しるし等の総称。」だってさ。

比較派:
え?プラスとかマイナスとか、ハテナとか、そういうのが記号でしょ?

非較派:
それは「特殊記号」。ふつうにABCも記号だし、漢字もひらがなも記号ってこと。一般的には「特殊記号」を指して記号って呼ぶほうが多いけどね。

話を戻すと、つまり式ってのは特殊記号を使った数式だけじゃなくて、何かの関係とか構造を表してたらそれで式ってこと。たとえばIsNumeric(x)だと、「関数(変数)」っていう構造だよね。プログラミングでは関数だけじゃなくて、変数とか等価式(a > bなど)、リテラル(100とか"あ"とか直接値を書いたもの)、計算式(1 + 2など)、論理式(And、Orなど)も全部式なんだ。
式の定義は「評価すると値になる」ってこと。

比較派:
さっきから言ってるその「評価」ってのは何なのさ。

非較派:
プログラミング用語としての「評価」ってのは、何かの値を確定すること。計算って言わないのは、"A" & "B"みたいな式もあるから。これは"AB"って値になるけど計算じゃないよね。

比較派:
たしかに。

非較派:
あとたとえば単体のリテラルも一応、評価はされるんだ。
"100"は評価しても"100"だと思うでしょ。もちろんそのケースが普通だけど、そうじゃないケースもある。

たとえば文字列"100"はイチマルマルっていう文字列なんだけど、Dim a As Integer: a = "100"っていう状況なら?

比較派:
ああ、Integer型の変数に入れようとしてるから評価されると数値のヒャクになるわけね。

非較派:
そゆこと。これも計算してるわけじゃないから、「評価」っていう言葉で表すんだ。

さて、ずいぶん回り道したけど、これで「If IsNumeric(x) Then」って書いてもちゃんと「条件式」になってるってことはわかって貰えた?

比較派:
まぁそう考えると定義上は条件式だね。でも…なーんかこじ付けのような気がするんだよね。
ふつうIfに書く条件式は比較演算でしょ。関数を直接書くのってやっぱ例外だと思うけど。

非較派:
いや、例外ってわけでもないよ。さっきも言ったように条件式ってのは、Boolean型に評価される式全般のことだから関数でも変数でもなんでも書ける。

比較派:
ますます例外が増えない?いま出てるだけでもこれだけあるでしょ。

  • Ifには基本的に比較演算子を使った条件式を書きます。
  • AndやOrで複数の条件を並べて書くことができます。
  • Boolean型を返す関数はそのまま書くことができます。
  • Boolean型の変数もそのまま書くことができます。

非較派:
いやいや。そもそも、基本的に比較演算を書くって前提が間違ってるよ。If文が見てるのは評価された後のTrueかFalseだけ。つまり比較演算は、TrueかFalseを返すための一つの方法に過ぎないってこと。
AndやOrで複数の条件をってのも、まるでIfが複数条件を処理してるみたいだけど、違うね。

比較派:
えっ?だって複数条件書けるじゃない。
たとえばIf a = 10 And b > 9 Thenとか。

非較派:
結果的にはね。でもそれ、Ifの機能じゃないから。
AndとかOrは論理演算っていって、両辺がTrueかFalseかしか見てないんだ。
たとえば今言ったIf a = 10 And b > 9 Thenの計算手順を分解してコードに書くとこんな感じ。

Dim a As Integer
a = 10
Dim resultA As Boolean
resultA = a = 10
'右のイコールは比較演算子、左のイコールは代入演算子であることに注意
'aは10なので、resultAにはTrueが入る。

Dim b As Integer
b = 10
Dim resultB As Boolean
resultB = b > 9
'bは9より大きいので、resultBにもTrueが入る。

Dim result As Boolean
result = resultA And resultB
'resultAとresultBはどちらもTrueなので、resultにはTrueが入る。

If result Then
    '何かする
End If

比較派:
ということは。。

非較派:
そう。例外が増えるんじゃなくて、最初からIfに例外なんて無いんだ。だってTrueかFalseしか見てないからね。
If True Thenとか、If False Thenって書く代わりに、最終的にTrueとかFalseに評価される式も書けるってだけのこと。

比較派:
なるほどね。でもやっぱ初心者には今みたいな話って難しくない?

非較派:
そうだね。話が難しいってよりも、この手の話に興味を持ってもらうのが難しいね。やっぱやりたいことがあってプログラミングするわけだから、ピンポイントでHow Toを知りたいよね。

でもそうすると「この場合はこう」っていう個別事例の積み重ねで例外だらけの知識になっちゃうからかえって覚えることも多いと思うんだ。覚えたこと以外に応用も利かなくなっちゃうし。

比較派:
なるほど。ある程度のレベルになったらやっぱりこういう基礎をしっかり学び直して、自由にプログラムが組めるといいよね。

ってことで時間も迫ってきましたので本日の対談はここまで。
ありがとうございました。

あとがき

ただいまブログ開設から2年と4か月ちょい。文体もいい感じで崩れてきたかなということで、新たな試みである対談形式にチャレンジ。

これはやっぱ書いててこっぱずかしいな。ノリで乗り切るしかない感じがする。

こういうのってやっぱキャラ立てが重要かなと。最初片方をごてごての関西弁にしようかと思ったけど、なんかその安易さが見透かされそうで恐れをなして逃げた。

結局あまり冒険しない感じの文体で書き上げてしまったのでノッペリしてつまんなかったかもしれないけど、チキンハートなのでそこはご容赦を。

さて、内容の結論。
私の立場は非較派なので、If文の条件式でBoolean型をTrueやFalseと比較しないという項目をコーディングガイドラインに追加しようかなとは思ってるんだけれど、究極的には好みの問題でもあるので、どう書くのが絶対解というのはないかなと。

Boolean型がTrueやFalseと比較されてるのを見るとなんか気持ち悪いけれど、他人が個人的に書いてるコードにいちいち目くじらを立てるつもりはない。

ただやっぱ少なくともIfの仕組みを知ったうえでの選択であってほしいなとは思う。

ブクマで興味深い意見をいただいたので追記

それがし比較派でござる。関数名が長くなってしまうと、If の次の「NOT」を見落としやすいので「=False」の書き方に統一しようぜっていう、前の会社にいた時の規約がクセになっております。

なるほど、日本人らしい感想ですね。
アメリカンならNotを見逃すなんてことはなく、むしろ日本語の最後のちゃぶ台返し「もにょもにょもにょ~では無い」が信じれらないって感じでしょうけども。私も日本人なのでNotを見逃したらヤバイ感はわからないでもなく。
まあでも英語も勉強した身としては慣れるとNotが非常にわかりやすいので、どっち派になるのかはその人のバックグラウンドにもよるのかも。

私は冒頭にNot(違うんたぜ)を持ってくる英語の明快さを支持します。逆に日本語の、さんざん話しといて最後に否定できちゃう文法ってこれズルくない?感が理解できるようになってきたといいますか。。

英語圏の方からすると、いまさら「ない」かよ、先いえよ「Not」って。という感覚ですかね。

VBAでアホな実験 ~ Not演算子は何個連続で使えるか

二重否定文…それは時に控えめな肯定を、時に強い肯定を生み出す日本語のテクニックである。

控えめな肯定の例「できなくもない」
強い肯定の例「やらねばならない」

なんてことを考えてたら、そういやNotって二個以上つなげても大丈夫なんかしらと、アホなことを思いついたので、これは検証せねばならない。

実際にやってみた。

Sub hoge()
    a = 100
    If Not Not IsNumeric(a) Then
        MsgBox "数値に違いない!!"
    Else
        MsgBox "数値ではない"
    End If
End Sub

f:id:t-hom:20170430053159p:plain

おお、どうやら問題ないようだ。

では4個ならどうか。

Sub fuga()
    a = 100
    If Not Not Not Not IsNumeric(a) Then
        MsgBox "数値と言えなくもない"
    Else
        MsgBox "数値ではない"
    End If
End Sub

f:id:t-hom:20170430053339p:plain

いけてるね。
ちょっとこの先は数値ではなくTrueに対して反転していこう。

Sub piyo()
    If Not Not Not Not Not Not Not Not Not Not _
    Not Not Not Not Not Not Not Not Not Not _
    Not Not Not Not Not Not Not Not Not Not _
    Not Not Not Not Not Not Not Not Not Not _
    Not Not Not Not Not Not Not Not Not Not _
    Not Not Not Not Not Not Not Not Not Not _
    True Then
        MsgBox "Hello"
    Else
        MsgBox "GoodBye"
    End If
End Sub

こんなのも問題なく実行できた。


これ無限につなげていけるんじゃないの?
と行コピーしまくってたら…
f:id:t-hom:20170430054504p:plain

えっ、そんなところに限界が!?

数えてみると、行継続文字は24個、つまり25行目までで限界らしい。

またひとつ賢くなった。
いや、ひとつアホになったかな。。

いいだろう。
それなら改行なしでやってやる。

てことでNotをコピペで注入して右へ右へと伸ばしていく。
f:id:t-hom:20170430055532p:plain

あれっ?
f:id:t-hom:20170430055752p:plain
いつのまにやら二行目に。。。

一行目の最終を確認すると、1023桁。
f:id:t-hom:20170430060006p:plain

このあとスペース1個は入ったけれど、それ以上1文字も入らない。
つまりVBEの1行の最大桁数は1024桁だということ!

まぁどうでもいい知識。

ということは、Notの限界個数は計算で求まる。
まず一行目は「If 」がスペース込みで3文字、最後の行結合文字「_」で1文字、あとはスペース込みの「Not 」4文字が何個入るかということ。

(1024-3-1) / 4 = 255個

255個入る!と思ったら最初のゼロ文字目を1桁と数えるらしく、結局254個だった。文字数でいうと1行に1023文字しか入らないわけね。
気を取り直して二行目。

(1023 - 1) / 4 = 255.5

ということでNotが255個、試してみたらちゃんと入った。

最終行は「True Then」を引いて、

(1023 - 9) / 4 = 253.5

Notが253個

トータルすると、
254個 + (255個 * 23行) + 253個 = 6372個!!

理論上、Not演算子は6372個連続で使えるということに。

ではジャンジャンコピーしていこう。
f:id:t-hom:20170430061806p:plain

あれ・・・?
f:id:t-hom:20170430050848p:plain

からの。。
f:id:t-hom:20170430050543p:plain
そんなまさか!

Excel様の堪忍袋の緒が切れた。。
エラーも出してくれない。

さすがにそんなアホはもう知らんと。

どうやら一定数を超えるとバグるらしい。MSさん、しっかりデバッグしてくださいよ~。
…まずはお前の頭をデバッグしてこいと言われそうだな。

さて、仕方ないのでここから数個づつ配置しながらバグが発生するポイントを探ったところ…

ついに。。完成!
f:id:t-hom:20170430052035p:plain

今回の検証では3621個というのが限界値のようだ。
ちなみに環境はWin7 32bit Office2010 Pro 32bit。

はい、おしまい。
あー疲れた。

VBA クラスモジュールでRangeの仕組みを説明する

昨日公開した以下の記事について、比喩ではちょっと難しいという声をいただいたので、今回はクラスモジュールを使って私が想像するRangeの仕組みを具体的なコードで解説しようと思う。
thom.hateblo.jp

まずはSheetの模擬クラスとRangeの模擬クラスを用意する。
本物と同じ名前にしてしまうと、メインコードで使う際にちゃんと自作の方を使ってるの?という疑惑につながるので、オブジェクトの名前はオリジナルにしておこう。

Sheetの代わりにSheeeet、Rangeの代わりにoRange(オレンジ)。
f:id:t-hom:20170417214655p:plain

まずSheeeetのコードはこちら

Private table(1 To 9, 1 To 9) As String

Property Get oRange(adddresss As String) As oRange
    Dim ret As oRange: Set ret = New oRange
    Dim x As Long: x = Ato1(Left(adddresss, 1))
    Dim y As Long: y = CLng(Right(adddresss, 1))
    ret.Init Me, x, y
    Set oRange = ret
End Property

'列のアルファベット(A~I)を列番号1~9に変換する関数
Function Ato1(char As String) As Integer
    Ato1 = InStr(1, "ABCDEFGHI", char)
End Function

Friend Property Let Value(r As Long, c As Long, v As String)
    table(r, c) = v
End Property

Friend Property Get Value(r As Long, c As Long) As String
    Value = table(r, c)
End Property

次にoRangeのコード

Private parent_ As Sheeeet
Private row_ As Long
Private column_ As Long

Sub Init(p As Sheeeet, r As Long, c As Long)
    If parent_ Is Nothing Then
        Set parent_ = p
        row_ = r
        column_ = c
    Else
        Err.Raise vbObject, "oRange", "このoRangeはすでに初期化されています。"
    End If
End Sub

Property Let Value(v As String)
    parent_.Value(row_, column_) = v
End Property

Property Get Value() As String
    Value = parent_.Value(row_, column_)
End Property

そして標準モジュールに書くメインコードがこちら

Sub Rangeモドキ実験()
    Dim sh As Sheeeet: Set sh = New Sheeeet
    
    'まずは普通に値を設定して表示
    sh.oRange("A1").Value = "Hello!"
    sh.oRange("B3").Value = "GoodBye!"
    MsgBox sh.oRange("A1").Value
    MsgBox sh.oRange("B3").Value
    
    'オブジェクト比較でFalseになる実験
    MsgBox sh.oRange("A2") Is sh.oRange("A2")
    
    'oRange型変数に入れて使用
    Dim r1 As oRange
    Set r1 = sh.oRange("C4")
    Dim r2 As oRange
    Set r2 = sh.oRange("C5")
    r1.Value = "Konnichiwa"
    r2.Value = "Sayonara"
    
    MsgBox r1.Value
    MsgBox r2.Value
End Sub

これでメインコードを実行すると、本物のRangeオブジェクトと同じようにValueの設定、取得ができる。
まあ偽物なので定義した以上の機能はないけど。本物のシートに書き込まれるわけではなく、シートを模したSheeeetオブジェクト内の配列に値が設定されるだけ。

さて、解説だけど全部は面倒なのでポイントだけ。
まずSheeeetクラスの以下の1行。

Private table(1 To 9, 1 To 9) As String

これが9×9のセルのを模したもの。実際のシート上の値はここに保存される。
しょぼいけどまぁ説明用サンプルなので。

次にSheeeetクラスのoRangeプロパティ。

Property Get oRange(adddresss As String) As oRange
    Dim ret As oRange: Set ret = New oRange
    Dim x As Long: x = Ato1(Left(adddresss, 1))
    Dim y As Long: y = CLng(Right(adddresss, 1))
    ret.Init Me, x, y
    Set oRange = ret
End Property

プロパティ名のoRangeと型名のoRangeはたまたま同じ名前になってるだけなので注意。これがRangeの理解がややこしくなる原因の一つ。名前が一緒なのでプロパティとオブジェクトを混同しがち。

このプロパティプロシージャでは内部でoRangeオブジェクトを生成して、それを返している。
ret変数に新規のoRangeオブジェクトを格納し、そのoRangeオブジェクトのInitプロシージャにMe(つまりSheeeetオブジェクト自身)を渡している。

そしてoRangeオブジェクトのInitプロシージャではこれを仮引数pで受け取り、自身のparent_変数にセットしている。このときに行と列の情報も受け取って自分のrow_とcolumn_に代入する。

Sub Init(p As Sheeeet, r As Long, c As Long)
    If parent_ Is Nothing Then
        Set parent_ = p
        row_ = r
        column_ = c
    Else
        Err.Raise vbObject, "oRange", "このoRangeはすでに初期化されています。"
    End If
End Sub

これでoRangeが準備できたので、Valueプロパティで値をいじるとparent_変数にセットしたSheeeetオブジェクトのValueプロパティが操作される。

Property Let Value(v As String)
    parent_.Value(row_, column_) = v
End Property

Property Get Value() As String
    Value = parent_.Value(row_, column_)
End Property

つまりデータはあくまでSheeeetオブジェクト内にあり、oRangeは自身が参照を保持しているSheeeetオブジェクトのプロパティを通じて操作しているということ。

ちなみにSheeeet側のValueプロパティはFriendスコープにしているけれど、今回SheeeetのValueはoRangeによってのみ操作されるのであって、メインコードから直接操作はしませんよという意味づけのためにFriendスコープにした。
Friendは同じプロジェクト内でのみ参照できるというスコープである。今回のメインコードでは同じプロジェクト内に書いたので特に操作を制限するといった効果はない。

Excelブック1つごとに1つのプロジェクトしか作れないので、Friendスコープを活かそうと思ったらアドイン化して参照設定するしかないけれど。。
【参考】
thom.hateblo.jp

4/18追記

複数セルは?と突っ込まれそうなので予め言い訳しておくと、複数セルに対応させるとコードの複雑度が増すので今回はあえて単一セルのみサポートするRange型もどきとした。
Range型はエージェントのように機能するという点を解説するにはこれで十分なので。

ちなみに複数セルに対応するには、oRange型の内部で座標をコレクションか配列で持たせ、Valueにアクセスされたら座標をForかFor Eachでまわしながら連続でSheeeetにアクセスすれば良い。広い範囲を扱う場合は開始セルと終了セルを持たせておけばOK。ただし飛び地を扱うにはもう一工夫必要。などなど考えていくと結構面倒なコードになる。

そんなに沢山のソートアルゴリズム、意味あるの?って方へ

みなさん本日は情報処理試験お疲れ様でした。
(私は今回申し込みすらしてないけど)

本当は試験前にこの記事を書きたかったんだけど文章悩んでるうちに試験が終わってしまった。まぁ今回ダメだった方、決意を新たにした方もいると思うので一応公開するか。

さて、何かを学習しようとしたときに障害になる考え方がある。
「それ、意味あるの?」ってやつだ。

試験のためと割り切れる方は良いけど、試験でしか役に立たないような知識を無理やり頭に詰め込むのってなんか空しい。

そこで今回は情報処理試験で出てくるソートアルゴリズムの存在意義について書こうと思う。まずもって数が多すぎる。そんなにいる?意味ある?

そもそも自前でソートなんてする?

まあ普通はプログラミング言語にはソート機能が組み込まれていたり、専用のライブラリなんかも充実してるので、いまどき自前でソートなんてやらんでしょって意見もある。

ところが、そうでもない。たとえばVBAでプログラムを組んでてデータをオブジェクトでコレクションに入れて管理しようと思うと、コレクションにはソート機能が無いし、そもそもオブジェクトなのでどのプロパティーをキーにソートするのかなど細かく調整したい場合もある。それで、コレクションをソートするプログラムを組んだことがある。

各ソートの存在意義

選択ソート

まず一番小さい数を探して、次に二番目に小さい数を探して。。というソート。すこぶる遅い。
このソートの存在意義は、実装がとにかく簡単ってことと、前提知識なしに誰でも思いつくソートであること。

挿入ソート

データを適切な位置に挿入していくソート。普段は遅いけど、ほぼ整列済のデータに対しては数あるソートの中でも最も高速になることがある。この特性が挿入ソートの存在意義。実装も簡単。
クイックソートで大体並んで来たら挿入ソートに切り替えると高速になるそうだ。

バブルソート

隣どうしのデータを比較して交換するソート。このソートの存在意義は、直観的に理解できることと実装が簡単であることだ。いくら自前ソートの機会が減ってるとはいえプログラマを自認するならバブルソートくらいは書けないとね。

クイックソート

名前どおり高速なソート。このソートの存在意義はとにかく速いことだが、アルゴリズムは少し複雑になる。しかし常に最速かというとそんなことはなく、データの特性によってはすこぶる遅くなる。

シェルソート

改良版の挿入ソート。かなり高速なソートで、データの特性によって並はずれて遅くなるということもない。比較するデータの間隔のチューニングが難しい。

ヒープソート

配列を木構造に見立てたソート。このソートもかなり高速で、データの特性によって並はずれて遅くなるということもない。

マージソート

データを分割して分割して分割してこれ以上分割できなくなったところで統合して統合して統合してという順でソートしていく。統合時にソートされる。

このソートは比較的メモリ使用量が大きいけれどかなり高速である。

加えて重要なのは安定ソートであるということ。安定ソートとは、たとえば氏名で並び替えた後に都道府県で並び替えると、各都道府県データのなかでは氏名順の並びをキープしているようなソートのこと。

クイックソートシェルソートヒープソートなどの他の高速ソートは不安定ソートなので、都道府県順で並び替えると氏名の順はぐちゃっとなってしまうけど、マージソートは崩れない。

他の安定ソートとしてはバブルソート、挿入ソートがある。遅いけど。

高速かつ安定ソートで、さらにデータの並び順で速度が変わることがないと三拍子そろった個人的には最強ではないかと思うソート。

ただまあクイックソートほど速くはないし、メモリを食うソートなのでこれもケースバイケース。

まとめ

確かに安定・高速・簡潔・元の並びに依存しないと四拍子そろった最強のソートがあればこんなにたくさんソートを覚える必要がなかったかもしれないが、逆にいえばこの多様性こそがプログラミングの学習において重要であるともいえる。

「並び替え」というひとつの課題でこれだけ多彩な回答が存在するというのは、自分が作ったプログラムに対してもより良い方法があるのではないかという考察のきっかけになる。

今回取り上げたソートはすべて基本情報の出題範囲であるが、それぞれに長所・短所があって面白い。

最後にこの記事の執筆のきっかけになったサイトを紹介する。
www.toptal.com

これを見て、シェルソートはやっ!!まじで!?と思ったのがきっかけ。
このソートは完全にノーマークだった。

VBA Rangeオブジェクトはシートを操作するエージェント

VBAで扱える代表的なExcelのオブジェクトにWorkbook、Worksheet、Rangeなどがある。

  • ひとつのブックにはひとつのWorkbookオブジェクトが対応している。
  • ひとつのシートにはひとつのWorksheetオブジェクトが対応している。

ではひとつのセルに対応する固有のオブジェクトは?



5秒、4、3、2、1、はい。




Cellと答えたあなた。ぶっぶー。

Rangeと答えたあなた。ぶっぶっぶー。

実はひとつのセルに対応する固有のオブジェクトは存在しないってのが答え。

「固有の」ってとこがミソ。

まずCellというオブジェクトは存在せず、WorksheetオブジェクトのCellsプロパティが返すのは全セルを表すRangeオブジェクトである。このことは以下の記事で詳しく書いた。
thom.hateblo.jp

そもそもセルがいくつあるか考えてみよう。Excel2007以降、扱えるセル範囲は16,384桁1,048,576行となっている。つまり単純に掛け算して約170個のセルが存在することになる。これらにそれぞれ対応するオブジェクトが存在するとしたらExcelだけでPCのメモリを食いつぶしてしまう。

じゃあいったいRangeとは何なのか。

Rangeとはシートを操作するエージェント(代理人)のようなものである。

ひとつ実験をしてみよう。
オブジェクトが一致するかどうかを比較する方法として、Is演算がある。
もしSheets(1).Range("A1")がひとつの固有なRangeオブジェクトを返すなら、以下はTrueになるはずだ。

Sub hoge()
    MsgBox Sheets(1).Range("A1") Is Sheets(1).Range("A1")
End Sub

実際に実行してみるとわかるが、上記のマクロはFalseになる。
同じセルを指してるのになんで!?と思うかもしれないけれど、これがエージェントと表現した所以。

つまりRangeとはWorksheetに代わってセルを操作する代理人(エージェント)である。

こんな風に、同じA1セルを管理していても、同じエージェントであるとは限らない。
f:id:t-hom:20170416013829p:plain

だからこうやってIsで比較すると。
f:id:t-hom:20170416014104p:plain

違うんだ!と。
f:id:t-hom:20170416014354p:plain

つまりWorksheetオブジェクトのRangeプロパティは評価するたびに新しいRangeオブジェクトを生成しているのだ。

ちなみに変数に入れてあげれば当然一致する。

Sub hoge()
    Dim r As Range
    Set r = Sheets(1).Range("A1")
    MsgBox r Is r
End Sub

これはTrue。

ここで面白いのが、以下のようにOffsetをかますと不一致になるという点。

Sub hoge()
    Dim r As Range
    Set r = Sheets(1).Range("A1")
    MsgBox r Is r.Offset(0, 0)
End Sub

Offset(0, 0)ということは実質的におなじセルを指してるのに不一致。
つまりOffsetは内部で新しいRangeエージェントを作って返してるようだ。

以前クラスモジュール内で自身と同じ型の別インスタンスを返すという処理をやったけど、まさにあれと同じ。
thom.hateblo.jp

Offsetプロパティは内部でRangeをNewして返してるイメージ。実際に中身見たわけではないので具体的な実装は知らないけど。

なお、同じセルを指しているかどうかを調べたかったらAddressプロパティをイコールで比較してあげればよい。

Sub hoge()
    Dim r As Range
    Set r = Sheets(1).Range("A1")
    MsgBox r.Address = r.Offset(0, 0).Address
End Sub

これはTrueになる。

ちなみにシートの場合はどうかというと、

Sub hoge()
    MsgBox Sheets(1) Is Sheets(1)
End Sub

このように普通にIsで比較してもTrueが返される。

以下のように表現を変えてみても同じくTrue。

Sub hoge()
    MsgBox Sheets(1) Is Sheets("Sheet1")
End Sub

以下のように一旦別シート指してから戻しても同じようにTrueを返す。

Sub hoge()
    Dim sh As Worksheet
    MsgBox Sheets(1) Is Sheets(1).Next.Previous
End Sub

つまりこれが固有オブジェクトであるということ。

ひとつのセルに対応する固有のオブジェクトが存在しないという意味が伝わっただろうか。

オブジェクト指向における「メッセージ」とは何か

現在主に認知されているオブジェクト指向

私がオブジェクト指向というとき、それは基本的にC++から始まりJavaに受け継がれた、あのオブジェクト指向を指す。つまり、カプセル化、継承、多態性で説明されるアレだ。

VBAは継承、多態性のサポートがほとんど無いけれど、その中心となる考え方はC++オブジェクト指向から受け継いだものだと思われる。

結論から言ってしまうと、このようなC++系統のオブジェクト指向において、メッセージという言葉は単にプロシージャ呼び出し※のことだ。
VBA用語です。C++なら関数呼び出し。

つまりオブジェクト同士がメッセージをやりとりするというのは単に比喩表現であって、実態としてはプロシージャ呼び出しとその戻り値の受け取りに過ぎないってこと。

冒頭で述べたオブジェクト指向言語においてメッセージという比喩は混乱の元になるので使わないほうが良いと思う。UMLを学習するとシーケンス図で出てきてしまうけど。。

もうひとつのオブジェクト指向

さて、冒頭で「あのオブジェクト」と言ったのは、そうじゃないオブジェクト指向があるから。その代表はSmalltalk(スモールトーク)である。

最近以下の書籍でSmalltalkを少し齧っている。

SMALLTALKで学ぶ オブジェクト指向プログラミングの本質

SMALLTALKで学ぶ オブジェクト指向プログラミングの本質

しかし、はっきり言って、まえがきからして何を書いているのがサッパリわからない。。理解できないのは説明が悪いというより、そもそもこのSmalltalkという言語の特殊性が原因かもしれない。読めそうなところからぼちぼちトライしているところ。

さて、Smalltalkという言語はまさにオブジェクト同士のメッセージのやりとりというアイデアを比喩ではなくそのまま文法に取り入れた言語である。

たとえば 1 + 2 という計算式は、一般的に演算子「+」が「1」 と「2」 を取って計算すると認識されているが、Smalltalkでは違う。
「1」というオブジェクトに「+ 2」というメッセージを送信すると考えるのである。

またSmalltalkにはIfやforなどの制御構造が構文ではなくメッセージになっている。
真偽値オブジェクトに対し、IfTrue、IfFalseなどのメッセージを送るイメージ。

ってもまだうまくイメージできないのだけど。

たとえば以下のVBAコードは、

If 1 + 2 = 3 Then Debug.Print "Hello, world"

Smalltalkではこうなる。

(1 + 2 == 3) ifTrue: [Transcript show: 'Hello, world.']

1 + 2 == 3が評価されるとVBAでいうところのBoolean型になり、この真偽オブジェクトにifTrueメッセージを送る。その引数(?)に手続きを渡している。。という解釈であってるのだろうか。

このように、Smalltalkはすべてがオブジェクトとメッセージで成り立つ言語。そしてこちらのオブジェクト指向における「メッセージ」がまさに本来の「メッセージ」なのだ。

何いってるかわかんないだろう?実は私もだ。HA HA HA
ずっとC++系統のオブジェクト指向を学んできた私にとってSmalltalkは、まるで宇宙人だ。言葉どころか、身振り手振りすら通じない。

さっぱり分からない。実に面白い。

ガリレオ [DVD]

ガリレオ [DVD]

オブジェクト指向は一つではない

つまりオブジェクト指向言語には大きく分けてSmalltalkから始まった「オブジェクトとメッセージ」を中心に据えたものと、C++から始まった「カプセル化、継承、多態性」を中心に据えたものの2種類に分かれ、これらは全くといっていいほど異なっている。

もともとその源流となったのはSimulaという言語だった。Simulaはプログラミングにクラスやオブジェクトの概念を導入した初めての言語だ。しかしこのときにはまだオブジェクト指向という言葉自体は無かったそうだ。

Smalltalkを設計したアラン・ケイさんと、C++を設計したビャーネ・ストロヴストルップさんはどちらもSimulaという言語を見て「これは画期的なアイデアだ!」と思ったに違いない。しかしその着眼点は全く別のものだった。

こうして世の中に2つのオブジェクト指向が生まれ、「オブジェクト指向とは」という説明における大きな混乱の元になっている。メッセージという言葉はその典型で、これは明らかにSmalltalk系統のオブジェクト指向の考え方だ。すべてをオブジェクトとして扱うというのもSmalltalkの話であってC++系統のオブジェクトではそうではない。

これとは別の評価軸として、クラスベースかプロトタイプベースかという分類もあるので、オブジェクト指向が2種類であると言い切るにはちょっと複雑すぎるのだけど。

まとめ

個人的には、C++系統のオブジェクト指向の説明においてメッセージという用語はもう使わない方が良いんじゃないかと思う。私は長いこと、メソッドに渡される実引数のことと勘違いしていたが、それもメッセージという用語に「送信」というイメージがつきまとうためである。

C++系統のオブジェクト指向でプロシージャ呼び出しをメッセージ送信に見立てるのはちょっと無理があるんじゃないかな。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。