情報学入門(データ演習) / 第 9 章

構造化プログラミング(1)

Sub と Function ── 大きな問題を小さく割る

この章のねらい

1. なぜ分割するのか

元資料 4 枚目に、こうあります ── 「これまで習ったプログラムを機能毎に分割し、わかり易く記述する」。 これが構造化プログラミングです。

なぜ分けるのか。元資料は目的を「わかり易く」と書いていますが、 具体的な利点は 4 つあります。

逆に言えば、1 つの Sub が長くなってきたら、分ける合図です。

1.1 3 つの基本構造

元資料 4 枚目は 「連続、順次(sequence)、分岐(branch)、繰り返し(iteration、loop)の構造を明確にし」と 4 つ並べていますが、「連続」と「順次」は同じもの(sequence)の別訳です。 構造化プログラミングの基本構造は3 つです (E. W. ダイクストラらが 1960 年代に整理したもので、 この 3 つだけであらゆるプログラムが書けることが知られています ── だから GoTo が要らないのです。第 5 章 §2)。

① 順次(sequence)

┌───┐ │ 処理1 │ └───┘ ↓ ┌───┐ │ 処理2 │ └───┘

上から順に実行する。第 1〜4 章

② 分岐(branch)

◇ 条件 / \ 真 偽 │ │ 処理A 処理B \ /

条件で選ぶ。第 5・6 章

③ 繰り返し(loop)

┌→ ◇ 条件 │ │真 │ ┌───┐ │ │ 処理 │ │ └───┘ └───┘

条件が続く間くり返す。第 7・8 章

3 つの構造には共通の性質があります ── 入口が 1 つ、出口が 1 つ。 だから箱の中に箱を入れても、外から見れば「1 つの処理」として扱えます。 この性質があるおかげで、プログラムを部品として組み合わせられるのです。 GoTo は、この「出口が 1 つ」を壊してしまいます。

2. 副手続き(Sub Procedure)

Sub 手続き名(引数,…,引数)
    手続きの本体
End Sub

ここまで書いてきた Sub Macro1() と同じものです。 違いは引数(ひきすう)を受け取れることと、 他の手続きから呼び出せることです。

Sub Macro1()
    ' 見出しを作る仕事を、別の Sub に任せる
    Call midashi(1, "科目", "点数")
    Call midashi(5, "氏名", "学年")

    Cells(2, 1) = "情報学入門": Cells(2, 2) = 88
    Cells(6, 1) = "山田":       Cells(6, 2) = 2
End Sub

Sub midashi(ByVal gyou As Integer, ByVal a As String, ByVal b As String)
    Cells(gyou, 1) = a
    Cells(gyou, 2) = b
    Cells(gyou, 1).Interior.Color = RGB(0, 63, 140)
    Cells(gyou, 1).Font.Color = RGB(255, 255, 255)
    Cells(gyou, 2).Interior.Color = RGB(0, 63, 140)
    Cells(gyou, 2).Font.Color = RGB(255, 255, 255)
End Sub

6 行の色づけを 2 回書かずに済みました。 見出しの見た目を変えたくなったら、midashi の中を 1 か所直すだけです。 第 1 章の「時間割の表」で同じことを 4 回書いた面倒さを思い出してください ── これがその解決です。

3. 関数副プログラム(Function)

Function 関数名(引数,…,引数)
    関数の本体
End Function

返り値は、関数名に代入する。

元資料 6 枚目の例です。

Function f(ByVal i)
    f = 10
End Function

f = 10f は変数ではなく関数の名前です。 ここに代入した値が、呼び出した側に返ります。 これは VBA 独特の書き方なので、最初は戸惑うところです。

Sub と Function の違い
SubFunction
値を返すか返さない返す(関数名に代入)
呼び方Call midashi(1, "a", "b") x = kaijo(5)式の中で使う
向いている仕事何かをする(書く・色をつける) 何かを求める(計算する)
マクロとして実行できるできない(値を返す先がない)

迷ったときの決め方。結果を受け取りたいか」で決めます。 x = 何か(…) と書きたいなら Function何かをして(…) だけなら Sub
Excel の関数(SUMAVERAGE)と同じ感覚です ── 実は Function で書いた関数は、 シートのセルから =kaijo(5) のように呼び出せます(ユーザ定義関数)。 これが VBA の便利なところの 1 つです。

3.1 例題:階乗を求める(元資料 12 枚目)

n を引数として、階乗 n!(=1×2×…×n)を計算する関数を作り、 n = 1 から 10 までの値を求めよ。

Sub Macro2()
    Dim n As Integer

    Cells(1, 1) = "n": Cells(1, 2) = "n!"

    For n = 1 To 10
        Cells(n + 1, 1) = n
        Cells(n + 1, 2) = kaijo(n)      ' 関数を式の中で使う
    Next n
End Sub

Function kaijo(ByVal n As Integer) As Double
    Dim i As Integer
    Dim k As Double

    k = 1
    For i = 1 To n
        k = k * i
    Next i

    kaijo = k                            ' 関数名に代入 = 返り値
End Function

戻り値の型に注意。 上の関数は As Double にしています。 As Integer にすると 8! = 40320 で範囲外エラーになり、 As Long でも 13! = 6,227,020,800 であふれます。 階乗は爆発的に増えるので(第 8 章の倍々貯金より速い)、 Double にしておくのが無難です。 上のプログラムの As DoubleAs Integer に変えて、 何番目で止まるか確かめてみてください。

3.2 例題:関数による総和(元資料 11 枚目)

Sub Macro3()
    Cells(1, 1) = "1〜10 の和":   Cells(1, 2) = souwa(10)
    Cells(2, 1) = "1〜100 の和":  Cells(2, 2) = souwa(100)
    Cells(3, 1) = "1〜1000 の和": Cells(3, 2) = souwa(1000)

    ' 公式 n(n+1)/2 と比べてみる
    Cells(5, 1) = "公式 1000×1001/2"
    Cells(5, 2) = 1000 * 1001 / 2
    Cells(6, 1) = "一致したか"
    Cells(6, 2) = IIf(souwa(1000) = 1000 * 1001 / 2, "一致", "不一致")
End Sub

Function souwa(ByVal n As Long) As Long
    Dim i As Long
    Dim s As Long

    s = 0
    For i = 1 To n
        s = s + i
    Next i

    souwa = s
End Function

関数にすると「何度でも呼べる」のがはっきりします。 第 7 章では 1〜N の和を求めるたびにプログラムを書き直していました。 関数にしておけば souwa(10)souwa(1000)引数を変えて呼ぶだけです。 なお、この計算には n(n+1)/2 という公式があるので、 本当はループを回す必要がありません ── プログラムを書く前に、数学で解けないか考えるのも大事な習慣です。

4. ByVal と ByRef ── 引数の渡し方

元資料 8〜10 枚目のところです。ここが第 9 章でいちばん重要で、 同時にいちばん間違いやすい部分です。

ByVal ── 値を渡す

a呼ぶ側
→ コピー →
x呼ばれた側

値の写しを渡します。 呼ばれた側で x を書きかえても、 呼んだ側の a は変わりません

ByRef ── 参照を渡す

a呼ぶ側
← 同じ箱 →
x呼ばれた側

箱そのものを渡します。 呼ばれた側で x を書きかえると、 呼んだ側の a も変わります

元資料 9 枚目の図は、ByRef を「参照を渡す」、 ByVal を「変数を渡す」と説明しています。 しかし ByVal で渡されるのは変数ではなく「値のコピー」です。 「変数を渡す」という言い方は、むしろ ByRef(箱そのものを渡す)の説明に聞こえるので、 取り違えのもとになります。 この教材では ByVal = 値を渡す/ByRef = 参照(箱)を渡すと書き分けました。

VBA では、何も書かないと ByRef になります。 これは他の多くの言語(値渡しが既定)と逆なので、覚えておいてください。 書きかえるつもりがない引数には、必ず ByVal と書くのが安全です ── そうしておけば、うっかり呼び出し元の値を壊すことがありません。

4.1 値の交換で違いを見る(元資料 8 枚目の課題)

2 つの変数の値を入れ替える副プログラムを作る。 3 つの整数を入力し、この副プログラムを利用して小さい順に並べ替えて表示する。

値を入れ替えるには、この副プログラムが呼び出し元の変数を書きかえられなければなりません。 つまり ByRef が必須です。 ByVal で書くとどうなるか、実際に見てみましょう。

Sub Macro4()
    Dim a As Integer, b As Integer

    ' ByRef で交換する
    a = 3: b = 7
    Cells(1, 1) = "ByRef で交換": Cells(1, 2) = "前 a=" & a & " b=" & b
    Call swapRef(a, b)
    Cells(1, 3) = "後 a=" & a & " b=" & b
    Cells(1, 3).Interior.Color = RGB(198, 239, 206)

    ' ByVal で交換する(つもり)
    a = 3: b = 7
    Cells(3, 1) = "ByVal で交換": Cells(3, 2) = "前 a=" & a & " b=" & b
    Call swapVal(a, b)
    Cells(3, 3) = "後 a=" & a & " b=" & b
    Cells(3, 3).Interior.Color = RGB(255, 199, 206)

    Cells(5, 1) = "ByVal だと入れ替わっていない ── コピーを交換しただけ"
End Sub

Sub swapRef(ByRef x As Integer, ByRef y As Integer)
    Dim t As Integer
    t = x: x = y: y = t
End Sub

Sub swapVal(ByVal x As Integer, ByVal y As Integer)
    Dim t As Integer
    t = x: x = y: y = t
End Sub

swapVal の中身は swapRef と 1 文字も違いません。 それでも結果が違うのは、引数の渡し方だけが違うからです。 ByVal の側では、コピーの xy を きちんと交換しています ── ただしそれはコピーの中での出来事で、 手続きが終わると消えてしまうのです。

4.2 3 つの整数を小さい順に並べる

交換の副プログラムができたので、それを部品として使います。 3 つの数を並べるには、隣どうしを比べて、順が逆なら交換するを繰り返せばよい。

Sub Macro5()
    Dim a As Integer, b As Integer, c As Integer

    a = Cells(1, 2).Value
    b = Cells(1, 3).Value
    c = Cells(1, 4).Value

    ' 隣どうしを比べて、逆なら交換する
    If a > b Then Call swap2(a, b)      ' a が最小になる(a,b の間で)
    If a > c Then Call swap2(a, c)      ' a が 3 つの最小になる
    If b > c Then Call swap2(b, c)      ' 残り 2 つを整える

    Cells(3, 1) = "小さい順"
    Cells(3, 2) = a
    Cells(3, 3) = b
    Cells(3, 4) = c

    Cells(5, 1) = "比較は 3 回。交換は必要なときだけ"
End Sub

Sub swap2(ByRef x As Integer, ByRef y As Integer)
    Dim t As Integer
    t = x
    x = y
    y = t
End Sub

3 回の比較で、なぜ必ず並ぶのか。 1 回目と 2 回目で a が 3 つのうち最小になります (a より小さいものが来たら必ず交換するので)。 残るのは bc の 2 つだけなので、3 回目で決まります。
この「隣どうしを比べて交換する」やり方をバブルソートといい、 数が増えても同じ考え方で並べられます(第 12 章)。 部品(交換)を作ってから、それを組み合わせて大きな仕事をする ── これが構造化プログラミングの実際の姿です。

交換に t(一時変数)が必要な理由。 x = y と書いた瞬間に、もとの x の値は失われます。 だから先に t = x で退避しておくのです。 x = y : y = x と書くと、両方とも同じ値になってしまいます ── 上のプログラムで t の行を消して試してみてください。

5. まとめ

  1. 構造化プログラミングの基本構造は順次・分岐・繰り返しの 3 つ (「連続」と「順次」は同じもの)
  2. 3 つとも入口 1 つ・出口 1 つ。だから部品として組み合わせられる
  3. Sub何かをするFunction何かを求める
  4. Function の返り値は関数名に代入する
  5. ByVal は値のコピー、ByRef は箱そのもの。 VBA は省略すると ByRef
  6. 値の交換には ByRef が必須。一時変数を忘れると両方同じ値になる
  7. 階乗は爆発的に増える。戻り値の型は Double にしておく

章末問題

付録:元資料からの修正一覧

もとにしたのは「情報学入門(VBA)150-2019」全 19 枚です。

元資料この教材種別
連続、順次(sequence)、分岐(branch)、繰り返し(iteration、loop)」と 4 つ並べている 「連続」と「順次」は同じもの(sequence)の別訳。基本構造は 3 つ。 この 3 つだけであらゆるプログラムが書けること(=GoTo が要らない理由)を追記不正確
9 枚目の図で ByVal を「変数を渡す」と説明 渡すのは値のコピー。「変数を渡す」は ByRef の説明に聞こえて取り違えのもと。 ByVal = 値/ByRef = 参照(箱)と書き分けた不正確
VBA は省略すると ByRef になることに触れていない 他の言語と逆なので明記。書きかえない引数には ByVal を書く習慣を推奨不足
Function f(Byval i) の綴り(Byval VBA は大文字小文字を区別しないので動くが、正しくは ByVal表記
ByVal で交換しようとするとどうなるかが示されていない 中身が 1 文字も違わない 2 つの Sub を並べ、結果が違うことを実行して確かめられるようにした不足
階乗の関数の戻り値の型に触れていない Integer では 8! で、Long でも 13! であふれる。Double にする不足
交換に一時変数が必要な理由が書かれていない x = y の時点でもとの x が失われる。消して試せるようにした不足
Sub と Function の使い分けの指針がない 比較表を追加(「結果を受け取りたいか」で決める)。 Function はセルから =kaijo(5) と呼べること(ユーザ定義関数)も追記不足
なぜ分割するのかが「わかり易く」の一言4 つの利点として具体化した不足

元資料で正しかったこと。 Sub 手続き名(引数,…,引数)〜 End SubFunction 関数名(引数,…,引数)〜 End Function の書式、 「返り値は、関数名に代入する」という説明(VBA 独特で、ここを外すと動きません)、 ByRef が「参照を渡す」という説明、 そして「2 つの変数を入れ替える副プログラムを作り、それを使って 3 つの整数を並べ替える」という 課題の設計(部品を作ってから組み合わせる)は、いずれも的確です。