読者です 読者をやめる 読者になる 読者になる

t-hom’s diary

主にVBAネタを扱っているブログです。

VBA Application.OnKeyを使い、F1キーで任意のアクティブブックのメインプロシージャを実行させる

VBA

今回の記事はF1キーを押したときに、今使用しているブックによって実行するプロシージャを分けるというもの。

執筆のきっかけになったのはこちらの記事のコメント欄のやりとり。
blog.powerpointvba.club

OnKeyマクロを記載したブックを閉じた後,そのショートカットを実行すると,OnKeyマクロを記載していたブックを自動的に開いてそのマクロを実行しようとする

なるほど、特定ブックでのみ使用するマクロをOnKey登録し、解除を忘れる(あるいは失敗する)と上記のように想定外の事象になってしまうデメリットがある。

F1キーを特定ブックのマクロに紐づけてしまうと、Excelアプリ全体に作用してしまい、F1キーはそのブックのマクロ実行専用になってしまう。

では、発想を変えて特定ブックに紐づかない実行キーを作ってしまおうと考えたのが今回のネタ。

この記事ではその仕掛けはPersonal.XLSBに作成するが、配布を考慮するならアドイン化しておくのも良いと思う。

まずPersonal.XLSB(個人用マクロブック)のThisWorkbookシートモジュールに以下のコードを記述する。

Private Sub Workbook_Open()
    Application.OnKey "{F1}", "'MacroLauncher ""F1_Key""'"
End Sub

つまりこれで、Excelを起動した際にF1キーにMacroLouncherプロシージャ呼び出しが登録される。
文字列"F1_Key"はMacroLouncherの引数で、実際に呼び出すブックごとのマクロ名を指す。

※本当はF1というプロシージャにしたかったけど名前が不適切となるのでやめた。恐らくキーコード定数と被っているせいかと思う。

わざわざ引数にしたのは後からF3、F4なども簡単に追加できるようにするため。
※F2は個人的にExcel本来のショートカットとしてよく使うのでマクロで上書きするわけにはいかない。

次にPersonal.XLSBに標準モジュールを追加し、以下のコードを記述する。

Sub MacroLauncher(macro_name)
    Dim QuotedBookName As String
    QuotedBookName = "'" & ActiveWorkbook.Name & "'"
    
    On Error Resume Next
        Application.Run QuotedBookName & "!" & macro_name
    On Error GoTo 0
End Sub

こうするとF1キーを押した際にMacroLouncherを経由してApplication.RunでアクティブブックのF1_Keyプロシージャが呼び出されるようになる。On Errorで囲んでいるのでマクロ登録がないブックでは何も起きない。登録がない旨のメッセージを出すようにしても良いかもしれない。

これで準備は整った。
あとはショートカットを使いたいブックごとにF1_Keyプロシージャを作り、それをメインコードにするか、そこからメインコードを呼び出すようにすれば完成。

ただこのテクニックはきわめて個人的な改善なので、チームが共通で使うようなブックだと、なかなか使えない。
F1_Keyプロシージャを作らせてもらうか、それともしれっと作ってしまうか、オフィシャルにして皆にも使ってもらうかという方向になるかと。

あと発展系としてはシートごとにF1キーの意味を変えたい場合にも使えると思う。
Application.Runの引数文字列の書式は 'ブック名'!シートオブジェクト名.マクロ名 である。(試してないけど、たぶん。)

VBA ヒープソートを実装 ~関数を沢山作って複雑な問題に対処する

VBA

私は長らくヒープソートというものが理解できなかったのだが、ついに今日、なんとか動くところまで実装できたので紹介しようと思う。

ヒープソートを理解しようと思ったら、実装の前にまずヒープのノードが入れ替わるイメージを理解しておく必要がある。
そこで、こちらの動画を視聴した。

ヒープソート

めっちゃわかりやすい!

ん、理屈は分かったけど、これをいったいどうやって実装すれば。。

ということでもう一本動画を視聴。

[ソート4]ヒープソート(Heap Sort):解説

最初のほう早くて分かりにくいけど、1分10秒あたりから配列を使った実装の詳細の説明に入るのでしっかり視ておく。

さて、ヒープを配列に導入すると、親と子を指すインデックスには次の式が成り立つ。
左の子のインデックス = 親のインデックス×2+1
右の子のインデックス = 左の子のインデックス + 1

これがメインコードに登場するとごちゃっとするので、関数化してしまおう。

Function GetRightChildIndex(parent_index) As Long
    GetRightChildIndex = GetLeftChildIndex(parent_index) + 1
End Function

Function GetLeftChildIndex(parent_index) As Long
    GetLeftChildIndex = parent_index * 2 + 1
End Function

それから、親を左の子と入れ替える処理、親を右の子と入れ替える処理というのも解説に登場したので、これもプロシージャ化する。

Sub SwapParentRightChild(arr, parent_index)
    Dim tmp: tmp = arr(parent_index)
    arr(parent_index) = arr(GetRightChildIndex(parent_index))
    arr(GetRightChildIndex(parent_index)) = tmp
End Sub

Sub SwapParentLeftChild(arr, parent_index)
    Dim tmp: tmp = arr(parent_index)
    arr(parent_index) = arr(GetLeftChildIndex(parent_index))
    arr(GetLeftChildIndex(parent_index)) = tmp
End Sub

子より親の方が小さければ、入れ替えるという処理をするが、このとき、両方の子が親より大きい場合、より大きい方を選択するという処理があった。
これ、言い換えると、親・左の子・右の子のうち、一番大きいものと親を入れ替えるということになる。親が一番なら入れ替えは発生しないが、とにかく誰が一番大きいかを判定する関数があると良い。

この関数は、列挙型定数でParent、LeftChild、RightChildのいずれかを返すようにしよう。

ということでまずは列挙型を作成。

Enum Role
    Parent
    LeftChild
    RightChild
End Enum

次にこれを返す関数を作成。

Function WhoIsBiggest(arr, parent_index, tail) As Role
    Const INIFINITESIMAL As Long = -2147483648#
    Dim p: p = arr(parent_index)
    Dim rc
    If GetRightChildIndex(parent_index) <= tail Then
        rc = arr(GetRightChildIndex(parent_index))
    Else
        rc = INIFINITESIMAL
    End If
    
    Dim lc
    If GetRightChildIndex(parent_index) <= tail Then
        lc = arr(GetLeftChildIndex(parent_index))
    Else
        lc = INIFINITESIMAL
    End If
    
    If p > lc And p > rc Then
        WhoIsBiggest = Parent
    ElseIf lc > p And lc > rc Then
        WhoIsBiggest = LeftChild
    ElseIf rc > p And rc > lc Then
        WhoIsBiggest = RightChild
    End If
End Function

INIFINITESIMALという定数の名前は英語の「無限小」をとったもの。
実際にはただのLong型の最小値である。
子のインデックスがtailを超えてしまう場合、参照エラーになったり確定値を参照してしまう危険がある。
そこでtailを超えた子より、必ず親が大きくなるように、子の値を入れる変数に無限小を代入している。
ここは力技でしのいだ感じ。

さて、あとは確定した最大値を取り除く処理だ。
これは配列の先頭と末尾を入れ替えてしまえば良いので、そのような関数を作る。
ただし毎回末尾と入れ替えてたら確定値まで動かしてしまうことになる。
したがって末尾はUboundではなく、別途変数で管理しよう。

ひとまずはtailという仮引数で末尾を受け取ることにする。
先頭は0で固定なので受け取る必要はない。

Sub SwapHeadTail(arr, tail)
    Dim tmp: tmp = arr(0)
    arr(0) = arr(tail)
    arr(tail) = tmp
End Sub

さて、役者は揃った。
あとはメインコードをガリガリ書くだけ。

Sub HeapSortMain()
    Dim heap(): heap = Array(8, 2, 7, 6, 9, 1, 4, 3, 5)
    Dim tail As Long: tail = UBound(heap)
    Do
        Do
            Dim cnt: cnt = 0
            Dim i
            For i = LBound(heap) To tail
                Select Case WhoIsBiggest(heap, i, tail)
                Case Role.Parent
                    cnt = cnt + 1
                Case Role.LeftChild
                    SwapParentLeftChild heap, i
                Case Role.RightChild
                    SwapParentRightChild heap, i
                End Select
            Next
        Loop While cnt <= tail
        SwapHeadTail heap, tail
        tail = tail - 1
    Loop While tail > 0
    
    For i = LBound(heap) To UBound(heap)
        Debug.Print heap(i)
    Next
End Sub

まずは最も内側のループからみていこう。

For i = LBound(heap) To tail
    Select Case WhoIsBiggest(heap, i, tail)
    Case Role.Parent
        cnt = cnt + 1
    Case Role.LeftChild
        SwapParentLeftChild heap, i
    Case Role.RightChild
        SwapParentRightChild heap, i
    End Select
Next

このループは配列を先頭から末尾(tail)まで流している。
ループの中では誰が一番大きいかを判定するWhoIsBiggestによりSwap処理を行っている。Swapが発生しなければcntが増える仕組みだ。

その外側のDoループでは、cntとtailが一致するまで繰り返しを行っている。cnt=tailということは、一度もSwapされなかったということ。つまり正しいヒープ構造になっているので、これ以上内側のForループを回す必要がなく、一巡目の最大値が確定したということ。

それからSwapHeadTailで最大値と末尾(tail)を入れ替え、tailを一つ減らしている。
これを繰り返すことで最大値がどんどん配列の底にたまっていく。

tailが0になったらループを抜けると、配列は見事昇順に並んでいるというわけだ。

私にとってややこしいのは、ヒープというツリー型の構造を無理やり配列で扱うこと。
そしてそれぞれの計算がメインコードに出てくることで頭が追い付かなくなる。

今回は関数に分割したことで、個人的にはわかりやすくできた。

これを見たみなさんがどう感じるかはまた別の話であるが。。

VBA Collectionを使って数値をランダムに並び替える

VBA

今回はn個の数値をランダムに並び替えるアルゴリズム
元ネタはこちらの記事。
blog.powerpointvba.club

上記はRnd関数のランダム性を利用したアルゴリズムで、出てきたSingle値の順位を付けることで結果的にその順位が重複のないランダムな数列になるというもの。

たとえば1~5をランダムに並び替えたいとする。

このようにRndを5回評価し、大きい順に順位をつける。

回数 Rnd結果 順位
1回目 0.4013743 2位
2回目 0.27828 3位
3回目 0.1604415 5位
4回目 0.1628216 4位
5回目 0.6465871 1位

この順位がそのまま2, 3, 5, 4, 1という数列になるわけだ。
なるほど、面白い。

では私はということで、あらかじめ用意した数値をランダムにシャッフルするというアルゴリズムでやってみよう。

出来たのがコレ。

Sub ShuffleCollection(c As Collection)
    Dim cc As Collection: Set cc = New Collection
    Dim m As Long
    Do While c.Count > 0
        m = Int(c.Count * Rnd + 1)
        cc.Add c.Item(m)
        c.Remove m
    Loop
    Set c = cc
End Sub

上記に、なんでもいいのでコレクションを渡すと、順番がバラバラになる。
実際にこれを使うコードを作ってみた。

Sub Main()
    Dim c As Collection: Set c = New Collection
    c.Add 1: c.Add 2: c.Add 3: c.Add 4: c.Add 5
    ShuffleCollection c
    Dim x
    For Each x In c
        Debug.Print x
    Next
End Sub

何度か実行してみると、確かにシャッフルされてるのがわかる。

仕組みとしては、最初はコレクションcに5つの要素が入ってるので、1~5の範囲でランダム値を生成してそれを別のコレクションccに入れる。コレクションcからは、削除する。するとcの要素は4つになるので、1~4の範囲でランダム値を生成して~と繰り返す。
cの要素が0になったら、ccにすべて移ってるので、Set c = cc としておしまい。

関数として非破壊的に作っても良いかな。

こんな感じか。

Function CreateShuffledCollection(c As Collection) As Collection
    Dim cc As Collection: Set cc = New Collection
    Dim i
    For i = 1 To c.Count
        cc.Add c.Item(i)
    Next
    
    Dim ccc As Collection: Set ccc = New Collection
    Dim m As Long
    Do While cc.Count > 0
        m = Int(cc.Count * Rnd + 1)
        ccc.Add cc.Item(m)
        cc.Remove m
    Loop
    Set CreateShuffledCollection = ccc
End Function

これなら元のコレクションは破壊されない。
ついでに1~5をAddするのも関数化してしまおう。

Function GetSequence(n As Long) As Collection
    Dim ret As Collection: Set ret = New Collection
    For i = 1 To n
        ret.Add i
    Next
    Set GetSequence = ret
End Function

元記事は配列だったな。。ということで配列変換も関数化する

Function ChangeCollectionToArray(c As Collection) As Variant
    Dim ret(): ReDim ret(0 To c.Count - 1)
    For i = LBound(ret) To UBound(ret)
        ret(i) = c.Item(i + 1)
    Next
    ChangeCollectionToArray = ret
End Function

これをメインコードにまとめると、こうなる。

Sub Main2()
    Dim arr: arr _
        = ChangeCollectionToArray( _
            CreateShuffledCollection( _
                GetSequence(5)))
    Dim x
    For Each x In arr
        Debug.Print x
    Next
End Sub

以上

VBA マクロの高速化のためのApplication設定をクラスモジュールにまとめる

VBA クラスモジュール活用

今回はちょっと変わったクラスモジュールのテクニック案。
異端扱いされそうな気がするので、思いついた私自身、採用には慎重なのだけれど、アイデアとしては面白いと思ったので備忘録として公開してしまうことにした。

さて、過去にPropertyプロシージャを使った高速化テクニックというのをやった。
thom.hateblo.jp

今回も基本的にはApplication設定を変更するだけなのだが、クラスモジュールを使って更に怠慢にやろうという話。

作り方

クラスモジュールを挿入し、オブジェクト名を「OneTimeSpeedBooster」に変更する。
このクラスの名前付けは超重要!!
そもそも馴染みのない異端テクニックなので、何がしたいのか名前で示さないと訳が分からなくなる。

クラスに書くコードはこちら。

Private Sub Class_Initialize()
    With Application
        .ScreenUpdating = False
        .Calculation = xlCalculationManual
        .EnableEvents = False
        .PrintCommunication = False
    End With
End Sub

Private Sub Class_Terminate()
    With Application
        .ScreenUpdating = True
        .Calculation = xlCalculationAutomatic
        .EnableEvents = True
        .PrintCommunication = True
    End With
End Sub

見ての通り、コンストラクタとデストラクタしかない。
コンストラクタで高速化設定を行い、デストラクタで標準設定にもどしている。

使い方

OneTimeSpeedBoosterを使う前に、まずは時間のかかるマクロを作ってみる。
計算が必要かつ、画面更新が発生するものが良い。

九九を100×100まで計算させつつ、セルの色をランダムで塗りつぶすようにしてみよう。

コードはこのとおり。

Sub hoge()
    Dim t As Double: t = Timer
    For i = 1 To 100
        For j = 1 To 100
            With Sheet1.Cells(i, j)
                .Value = "=" & i & "*" & j
                .Interior.Color = RandomColor
            End With
        Next
    Next
    Debug.Print Timer - t
End Sub

Function RandomColor()
    RandomColor = RGB( _
        Int(256 * Rnd), _
        Int(256 * Rnd), _
        Int(256 * Rnd))
End Function

完成!
f:id:t-hom:20170319111224p:plain

タイムは、3.7秒。はやっ!

では、ズームで10%まで小さくして、画面に入る描画範囲を増やしてみよう。
f:id:t-hom:20170319111649p:plain

タイムは、9秒。

ではこれがどれだけ早くなるか。
コードは次のとおり。

Sub hoge()
    Dim t As Double: t = Timer
    Dim booster: Set booster = New OneTimeSpeedBooster
    For i = 1 To 100
        For j = 1 To 100
            With Sheet1.Cells(i, j)
                .Value = "=" & i & "*" & j
                .Interior.Color = RandomColor
            End With
        Next
    Next
    Debug.Print Timer - t
End Sub

Function RandomColor()
    RandomColor = RGB( _
        Int(256 * Rnd), _
        Int(256 * Rnd), _
        Int(256 * Rnd))
End Function

タイムは、1.16秒!

9秒から1秒なので実に90%も高速化したことに、、、、なる、、のか。。
っ!計算に自信がない!!

で、肝心のコードだけれど、足したのは以下の1行のみ。
Dim booster: Set booster = New OneTimeSpeedBooster

名前付けが超重要と書いたのは、上手い名前を付けていても変数宣言してセットしたはいいけど何も使われていないアホなコードに見えるのに、まして下手な名前なんてつけたら。。ということ。

OneTimeSpeedBoosterのインスタンスが作られるとコンストラクタによって各種アプリケーション設定が変更され、プロシージャの終了とともにこのbooster変数は破棄されるので、デストラクタによってアプリケーション設定がもとに戻る仕組み。

あるいは、Withを使ってこの区間ブーストするみたいな表記もアリか。

Sub hoge()
    Dim t As Double: t = Timer
    With New OneTimeSpeedBooster
        For i = 1 To 100
            For j = 1 To 100
                With Sheet1.Cells(i, j)
                    .Value = "=" & i & "*" & j
                    .Interior.Color = RandomColor
                End With
            Next
        Next
    End With
    Debug.Print Timer - t
End Sub

ま、今回は思考実験みたいなものなので、あまり真に受けないでほしい。
特にマジに批判するとかはナシで!!

良いと思った方は自己責任でどうぞ!

VBA マクロが遅い・速いという議論は、要件ありきの話

VBA VBAについての考え・意見

VBAのコードについて、よく、この手法は遅いから使うなという話を聞く。高速化万歳!
またはその逆で、高速化のためにわかりやすさを犠牲にするなどナンセンスだ!という話も聞く。

この記事では前者を「$バンザイ」、後者を「$ナンセンス」と呼ぼう。

$バンザイと$ナンセンスはどちらも自分の考えが正しいと信じており、否定しようものなら青筋を立てて反論してくることだろう。
問題はそこにある。あまりにも感情移入しすぎている。

私が思うに、$バンザイと$ナンセンスはどちらも正しくない。個々の要件、シチュエーションが考慮されていないからだ。

$バンザイは、マクロが高々1分速くなったところでビジネス的には大して変わらないという観点が抜け落ちている。
実行に15分かかる処理が14分になったところで、気づきもしないだろうし、そのような高速化にあまり意味はないと思う。
また、月に1度しか発生しない作業なら、2分を1分に短縮したところで高々知れている。
$バンザイは、そのような高速化のためにコードをひどく分りにくくしてしまうことがある。これは全くもってナンセンスだ。

一方で$ナンセンスは、たった1分でも人間は待たされることを苦痛に感じ、ストレスを溜める生き物であるという観点が抜け落ちている。
$ナンセンスは、「業務上の影響」だけを意識するため、5分も10分も変わらないじゃないかと思うようになる。確かに月に数回ならね。
しかし毎日何回も繰り返し実行するマクロでは、5分と10分の差はとてつもなく大きい。
5分と6分で感じるストレスは大差ないかもしれない。しかし1分と1秒で感じるストレスは、1分のほうが遥かに大きい。

3秒と10秒の差も無視できない。一瞬と1秒の差も大きい。

打てば響くようにキビキビ動くマクロは使っていて気持ちいいものである。仕事のリズムが良くなり、集中力が増す。
$バンザイの主張にもうなずけるものがある。

このように、マクロが遅い・速いという議論は、ユーザーが求めるスビード感(つまり要件)ありきの話なのだ。だから、個々のコーディングテクニック、作法を指して、これは遅いから絶対ダメ、これは分りにくいから絶対ダメという主張は鵜呑みにしないほうが良い。


さて、私は最近「ハッカーと画家」という書籍を読んだ。

どの章も面白かったが、特に気に入ったのは第11章「百年の言語」だ。

その中でも格別に素晴らしいと感じた箇所を引用する。

インフラが整った現在では、長距離電話をかけている最中に時間を分刻みで気にするなんて些細なことのように思えてくる。資源があれば相手がどこにいようと電話をかけるというのはひとつの均一な行為だと考えるほうがエレガントだ。
良い無駄と悪い無駄があるってことだ。私は良い無駄のほうに関心がある。贅沢に使うことで、より単純なデザインが得られるような無駄だ。新しい、速いハードウェアのマシンサイクルを無駄に使えるというチャンスをどんなふうに利用できるだろう。

本当の非効率性とは、マシンの時間を無駄にすることではなく、プログラマの時間を無駄にすることだ。コンピュータが速くなればなるほど、このことははっきりしてくる。

※マシンの時間とは、おそらく我々の考える「時間」を指して言っているのではない。CPUやメモリの使用率の話だと思う。

私もこの意見に同意する。これは$ナンセンスが言う、5分も10分も変わらないという意見ではない。
時代の進歩とともに、それはやがて1分になり、30秒になり、一瞬になるだろうという話である。

$バンザイが目指す高速化至上主義が時代によって自動的に解決されるのであれば、これからのプログラミングはわかりやすさ、メンテナンスしやすさ、プログラマの効率にフォーカスを当てたものになるだろう。

だから、原則としてはより抽象化されたメンテナブルなコードをデザインすべきだと思う。
$ナンセンスはここでしたり顔で「ほらね」というかもしれない。

でもそれは違う。

現代においては依然としてそのような高速化は実現されておらず、たとえばExcel VBAではセルを個別に読みにいくような処理はすこぶる遅い。
そこで、配列転記のような高速化テクニックが必要になってくるわけだ。

でも最初から高速性ありきで考えるのではない。
まずはエレガント!!なグランドデザインがあり、求められるパフォーマンスが出ないときのチューニングとしての高速化テクニックを使用するのだ。

マクロの速度に関する議論は要件ありき。ただし、高速化テクニックはあくまでパフォーマンスチューニングであることに留意したい。コンピューターの、あるいはExcelの過渡期における必要悪である。決してそれがグランドデザインであってはいけない。

VBA 変数で躓いた方に贈る、くどいくらい丁寧な変数の説明

VBA

さて、今回は初心者向けの記事なので、本文を「ですます調」で書くことにする。
途中でこのように緑文字でコメントを入れる。コメントはいつもどおり「だ・である調」で書く。本文は黒文字とする。

プログラミングでは「変数(へんすう)」という道具を利用して一時的にデータを保存することができます。

数学でも変数という用語が出てきますが、いったんそれは忘れてください。数学では不定値を仮に「x」とするなどの使われ方をしますが、プログラミングで変数はデータの保存に使う道具なので、別物だと割り切って考えましょう。

プログラミングで変数に保存するのは「数」に限らず、「人の名前、物の名前、セリフ、日付、正しいか間違いかの判定結果」など様々です。

データを保存するには、「変数名 = データ」の形で記述します。ここでも、数学の「イコール」は一旦忘れてください。このイコールは「この変数にこのデータを保存してください」というコンピューターへの指示に使う記号です。

ではやってみましょう。
「こんにちは」という文字を「あいさつ」という変数に入れるには、このように書きます。

Sub サンプルマクロ()
     あいさつ  = "こんにちは"
End Sub

「え?」と思われた方もいるでしょう。変数って「x」とか「y」のことじゃないの?と。いえ、数学の変数とはもはや別物で、好きな名前をつけることができます。英数字しか使えないプログラミング言語が多いので一般的には「greeting」といった英語で命名することが多いですが、VBAでは漢字仮名も使えるので別に日本語の名前をつけても構いません。

変数は人が読んで意味の分かる名前を付けることが推奨されています。

さて、保存した値を利用するには、単に値の代わりに変数を書くだけです。保存と合わせてやってみましょう。

Sub サンプルマクロ()
    あいさつ = "こんにちは"
    MsgBox あいさつ
End Sub

これで「こんにちは」というメッセージを表示させることができました。でもこれだと何が嬉しいのかわかりませんよね。だって最初からこう書けばいいじゃないですか。

Sub サンプルマクロ()
    MsgBox "こんにちは"
End Sub

では次の例をみてみましょう。

Sub サンプルマクロ()
    yourName = InputBox("あなたの名前を入力してください。")
    MsgBox "こんにちは" & yourName  & "さん"
End Sub

このマクロは、ユーザーに名前を入力させてその結果を表示します。

以下のように変数を使わずに書くこともできますが、なんだか長ったらしくなって読みにくいですよね。

Sub サンプルマクロ()
    MsgBox "こんにちは" & InputBox("あなたの名前を入力してください。")  & "さん"
End Sub

それに、以下のように名前を2回表示させようと思ったら、やっぱり変数を使うしかありません。

Sub サンプルマクロ()
    yourName = InputBox("あなたの名前を入力してください。")
    MsgBox "こんにちは" & yourName  & "さん。"
    MsgBox yourName  & "さんはお元気ですか?"
End Sub

もし変数がなければ、名前を表示させるたびに尋ねないといけませんから。

このように変数は、「データを保存する道具」であると理解してください。そしてデータを保存することを、専門的には「代入」といいます。これも数学から用語だけ持ってきたような代物ですので、「代入」と聞いたら、「ああ、保存することだな」と思っていただければ結構です。

また、これからはデータのことを「値」と呼びます。数学用語ですが、プログラミングでは数値だけではなく、先ほどのように"こんにちは"という言葉も値です。

ですから、「変数にデータを保存する」ことを、一般的には「変数に値を代入する」といいます。数学チックな言い方なので嫌な思い出のある方もいるでしょうけど、書籍などではこの言い回しが一般的なので、慣れてください。所詮、数学とは別物です。

コメント
ここまで変数の本質的な説明を行ってきたので、いまさら箱だのメモだのといった説明は蛇足かもしれない。というか変数には形も色もないのだけれど、「具体的に変数とは何なんだろう」という余計な疑問を呼び起こし、混乱を増長させるような気がする。
ただ前回書いた「概念形成」の話を加味すると、実験的に一旦ここでは箱とかメモとかタグとか、色々な説明を混ぜておきたい。
ここまでの説明で十分納得された方は、「混乱を避けるために続きを読まない」という選択肢もあることにご留意いただければと思う。

変数はよく、箱のようだとか、メモのようだと説明されます。

たとえば「あいさつ」と書かれた箱に「こんにちは」という言葉が入っているところをイメージしてください。一度箱に保存すれば、いつでも中身を確かめることができます。また、中身を入れ替えることもできます。

こんな風に書くと、最初は「こんにちは」、つぎに「こんばんは」が表示されます。

Sub サンプルマクロ()
    あいさつ = "こんにちは"
    MsgBox あいさつ
    あいさつ = "こんばんは"
    MsgBox あいさつ
End Sub

最初に保存した「こんにちは」は上書きされて無くなってしまいます。この点、サイズの許すかぎり何個でも入る「箱」とは違いますね。

次に、「あいさつ」とタイトルが書かれたメモ用紙に「こんにちは」と書き込むところをイメージしてみましょう。実際のメモは余白が許す限り何個でも書けてしまいますが、実際の変数は上書きされますから、たとえ話はあくまでたとえ話だということを理解しておいてください。

ネームタグに例える説明もあります。たとえば、「あいさつ」と書かれたタグが、「こんにちは」という文に括り付けてあるイメージです。紐をほどいて、「こんばんは」に括りなおすこともできますね。このたとえはなかなか秀逸で、オブジェクト変数にもそのまま使えます。

しかし、いずれにしても所詮たとえ話なのでそこは割り切っておいてください。変数の本質は「データを保存する」という機能であり、何か具体的な色や形があるわけではないのです。

さて、変数のことがなんとなく理解できましたか?
正直いまひとつピンとこないという方も多いと思います。

ではもう少し具体的な話をしましょう。
具体的に、変数に保存したデータはどこにあるのでしょうか。
それは、コンピューターのメモリです。


コメント
またここで、実装に踏み込んだ説明をするべきかどうか悩むが、とりあえず書く。上記の説明でスッキリ理解できている人は読まなくても良いけど、逆にたとえ話で混乱が深まったという方には良い処方箋になるのではないかと思う。

メモリにはアドレス(番地)がついていて、たとえば4GBなら、0番地~21億4748万3647番地まであります。
このどこかに保存されているのです。10桁の数値で表すことができますね。

たとえばこんなプログラムがあったとします。

Sub サンプルマクロ()
    数値1 = 20
    数値2 = 10= 数値1 + 数値2= 数値1 - 数値2= 数値1 * 数値2= 数値1 / 数値2
    MsgBox 数値1 & " + " & 数値2 & " = " &MsgBox 数値1 & " - " & 数値2 & " = " &MsgBox 数値1 & " * " & 数値2 & " = " &MsgBox 数値1 & " / " & 数値2 & " = " &End Sub

するとコンピューターは変数名とその保存場所を示した表をメモリに作り出し、実際の値はメモリ上に保存されています。
メモリのどこに保存されるかはコンピューターが管理しているので、こちらでは指定できません。

たとえばこのあたりだとしましょう。

変数名 アドレス
数値1 2486820
数値2 2486788
2486756
2486740
2486724
2486708

VBAでは直接メモリのアドレスに値を入れたり取り出したりすることはできず、変数を介して間接的にアクセスするしかありませんが、仮に変数が存在せずアドレスを指定できるとしたら、以下のようなプログラムになるでしょう。

Sub サンプルマクロ()
    a2486820 = 20
    a2486788 = 10
    a2486756 = a2486820 + a2486788
    a2486740 = a2486820 - a2486788
    a2486724 = a2486820 * a2486788
    a2486708 = a2486820 / a2486788
    MsgBox a2486820 & " + " & a2486788 & " = " & a2486756
    MsgBox a2486820 & " - " & a2486788 & " = " & a2486740
    MsgBox a2486820 & " * " & a2486788 & " = " & a2486724
    MsgBox a2486820 & " / " & a2486788 & " = " & a2486708
End Sub

すごく分かりにくいですよね。
我々プログラマーは、データを保存して、再利用したいだけなのに、いちいちどこに保存するか番号で指定しないといけないなんてうんざりしますよね。ですから、保存場所の管理はコンピューターに任せて、我々はデータを名前で呼びましょう。

これが変数です。

さて、ここから先にまたオブジェクト変数という手ごわい奴がいますが、これは別の記事で触れているので割愛します。

さらに詳しいメモリの働きについては、こちらをご参照ください。
thom.hateblo.jp

上記を理解したうえで、オブジェクト変数について以下の記事を読んでいただけるとよく理解できるのではないかと思います。
thom.hateblo.jp

以上。

あとがきコメント
今回の記事は前回書いた以下の考察を実際の説明に生かしてみようと思って書いた。

thom.hateblo.jp


しかし、たとえ話を切り出すタイミングが難しく、それほどうまくいったとは思っていない。
むしろ本質的な変数の機能の説明の後にあえてたとえ話を持ち出す必要もなかったかなという思いが強いのだけれど、何かしらイメージとして形と結びついていたほうが記憶にとどまりやすいかもしれないという考えもあり、結局、形をもつ箱やメモ、タグなどのたとえを紹介することにした。
もう少し表現や説明順序を洗練させていきたいと思う。

変数・関数・オブジェクトが「いまひとつ分からない」という感覚の正体を考察

プログラミング

プログラミングでは日常生活であまり耳にしない言葉がたくさん出てくる。

たとえば、変数・関数・オブジェクトなど。

説明を聞いてスッと理解できる方もいれば、なかなか意味が分からずに苦労したという方もいるだろう。

さっぱり分からないということはない。そして使ってみろと言われて使えないわけでもない。…にもかかわらず、いまひとつ分からない。なんかもやもやする。スッキリしない。

では一体なにが分からないのだと聞かれても困る。なんだかよく分からないとしか答えようがない。そんな経験はないだろうか。

というか、多くの人がこのように感じながら日々使っているうちに慣れてしまった方が多いのではないかと思う。

そして他の人に教えるときも「慣れるしかない」などとしたり顔で言ってしまうんだ。

でも、本当に慣れるしかないのだろうか。

「慣れるしかない」というのは、場合によっては自分がうまく説明できないのを誤魔化したように聞こえることがある。完璧に説明できなくてもいいから、もう少し納得度の高い答えを示せないだろうか。

それで私はここ数日、この「いまひとつ分からない」という感覚の正体をもう少し正確に言語化できないかと悩んでいた。まず敵を知らなければ対処のしようがないからだ。

それでいろいろ調べていたところ、認知心理学の分野で「概念形成」という言葉を見つけた。

何かを理解したというのは、言い換えると頭の中で概念が形成されたということで、「いまひとつ分からない」というのはつまり、概念形成がうまくできていないという状態なのではないか。

ではどうすればうまく概念を形成することができるのか。
その前に、概念という言葉をはっきり定義しておこう。概念を形成しようというのだから、肝心のそれが何なのかはっきりわかっておく必要がある。

ここで国語辞典を取り出してくる。

岩波国語辞典より

概念-同類のものに対していだく意味内容。
ア)同類のもののそれぞれについての表象から共通部分をぬき出して得た表象。
イ)対象を表す用語について、内容がはっきり決められ、適用範囲も明確な、意味。
ウ)俗に、複雑なものに対する大まかな認識内容のこと。

このうち(ウ)の意味で理解している方も多いと思うが、今回扱うのは(ア)と(イ)である。

で、またここでややこしい言葉が。
表象(ひょうしょう)とは。。。

岩波国語辞典より

現在の瞬間に知覚してはいない事物や現象について、心に描く像。イメージ。

い、、一応知覚も調べておくか。。

岩波国語辞典より

感覚器官を通じて、外界の事物を見分け、とらえる働き。視覚・聴覚・嗅覚・味覚・触覚など。

つまり、たとえば「変数」に対する概念を形成するとは、数ある「変数」に対する脳内イメージのうち、共通するイメージを抜き出したものを脳内に形成するということを指す。

よく変数は箱に例えられる。私は箱だと説明されて「なるほど箱か」と何も疑問に思わなかったので良かったけれど、一定以上の人がこの説明で躓くらしい。

そこでしばしば、「変数=箱」モデルはあまり上手い例えではないとの指摘がなされる。そしてメモに例えたり、名札に例えたり、実際にどうなっているのかメモリの動きを説明したりという試みが行われている。

しかし先ほどの概念形成の定義を考えると、どれが良いということではないような気がしてきた。

もう一度言うと、「変数」に対する概念を形成するとは、数ある「変数」に対する脳内イメージのうち、共通するイメージを抜き出したものを脳内に形成するということを指す。

変数とは箱であるという説明をいくら繰り返しても、共通するイメージは「箱」そのものである。一方、メモであるという説明を繰り返しても、共通するイメージは「メモ」そのものである。

箱という説明で躓いた人がメモという説明でなるほど!となるのは、メモに例えるのが箱に例えるよりも優れているということではなく、複数の事例を見ることで共通する特徴を捉えやすくなるからだろう。

変数がハコにもメモにも似ているのであれば、まず消去法で変数が立方体である可能性は消える。

では具体的なメモリの動きを説明するのはどうか。複数の人に説明してみて、これは変数の概念を形成するのに極めて効果的だった。しかしこれも、ハコ・メモといった既存の説明を受けた後でのことであるから、メモリの説明単体で変数を説明するのは難しいかもしれない。

それに変数におけるメモリの働きはたまたま実装がそうなっているということにすぎず、変数の本質かと言われるとやっぱり違う気がする。

プログラミングにおける変数とは本来、色も形もない純粋な概念なのだろう。だから人に説明するにはハコやメモといった比喩や、実装という現実を使って、事例の形で言語化するしかない。

よく分からなくても使っているうちに慣れるというのは、つまりコーディングを通じて脳内にたくさんの事例をため込めば自然と概念が形成されるということ。

だから、「いまひとつ分からない」から「はっきり理解できる」に移行するために「慣れる」という手段は有効であるが、「慣れるしかない」わけでもない。

いろんな切り口から説明すれば共通項が概念として形成されるし、たとえ話だけで理解されない場合は具体的な実装に踏み込んでみるとスッキリ理解していただけることが多い。

それでもうまく説明できないときは、自分の力不足であることを認めたうえで、申し訳ないけれど実際に使いながら慣れてくださいというと好印象だと思う。

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