Dim で宣言できる元資料 4 枚目に、こうあります。
今まで扱ってきた値は、1 つ 1 つ別々になっていました。
例えば、5 人の体重のデータをプログラミングすると
w0 = 50: w1 = 58: w2 = 63: w3 = 69: w4 = 77
のように書く必要がありました。これらをまとめて表す方法が配列です。
w(0) = 50: w(1) = 58: w(2) = 63: w(3) = 69: w(4) = 77
見た目はほとんど同じですが、決定的な違いがあります。
w0 の「0」は名前の一部ですが、
w(0) の「0」は数です。
だから w(i) と書けます ── i を変数にできるのです。
そして変数にできれば、ループで回せます。
これが配列のすべての値打ちです。
配列とは ── いくつかの変数を集めたもの(元資料 3 枚目)。
配列も変数と同じように宣言します(元資料 4 枚目)。
Dim w(5) As Integer
ここが最大の注意点です。
Dim w(5) で作られる箱は 5 個ではなく 6 個です ──
w(0), w(1), w(2), w(3), w(4), w(5)。
括弧の中の数は「いくつ」ではなく「添字の上限」だからです。
上の図で [5] が点線になっているのはそのためです。 5 人ぶんのデータ(w(0)〜w(4))を入れたので、w(5) は使われずに残っています。 これは間違いではありませんが、「5 人だから w(5)」と覚えると必ず混乱します。
| 書き方 | できる箱 | 個数 | 使いどころ |
|---|---|---|---|
Dim w(4) As Integer | w(0)〜w(4) | 5 | 0 から数えたいとき(余りが出ない) |
Dim w(5) As Integer | w(0)〜w(5) | 6 | 1 から使いたいとき(w(0) を捨てる) |
Dim w(1 To 5) As Integer | w(1)〜w(5) | 5 | いちばん誤解がない。これを勧めます |
なぜ「1 から使う」ことが多いのか。
Excel の行と列は 1 から始まるので(第 7 章 §4)、
Cells(i, 1) と a(i) の i をそろえると
プログラムが読みやすくなるからです。
そのために w(0) を 1 つ捨てる ── というのが実際によく使われる書き方です
(元資料も Dim a(11) と宣言して a(1)〜a(10) を使っています)。
はっきり書きたいなら Dim w(1 To 5) です。
LBound・UBound で下限・上限を確かめられます。
Sub Macro1()
Dim a(5) As Integer
Dim b(1 To 5) As Integer
Dim i As Integer
Cells(1, 1) = "Dim a(5)"
Cells(1, 2) = "下限": Cells(1, 3) = LBound(a)
Cells(1, 4) = "上限": Cells(1, 5) = UBound(a)
Cells(2, 1) = "個数": Cells(2, 2) = UBound(a) - LBound(a) + 1
Cells(4, 1) = "Dim b(1 To 5)"
Cells(4, 2) = "下限": Cells(4, 3) = LBound(b)
Cells(4, 4) = "上限": Cells(4, 5) = UBound(b)
Cells(5, 1) = "個数": Cells(5, 2) = UBound(b) - LBound(b) + 1
' 宣言した直後の中身は 0
Cells(7, 1) = "a(3) の初期値": Cells(7, 2) = a(3)
' 範囲外を触るとエラー(' を外して試してください)
' Cells(8, 2) = a(9)
End Sub
5 人の体重を配列に入れて、合計と平均を求めます。 人数が 5 人でも 500 人でも、プログラムはほとんど変わりません。
Sub Macro2()
Dim w(1 To 5) As Integer
Dim i As Integer
Dim n As Integer
Dim s As Long
n = 5
' ① シートから配列へ読み込む
For i = 1 To n
w(i) = Cells(i + 1, 1).Value
Next i
' ② 配列を使って計算する(もうシートを見なくてよい)
s = 0
For i = 1 To n
s = s + w(i)
Next i
' ③ 結果を書き出す
Cells(8, 1) = "合計": Cells(8, 2) = s
Cells(9, 1) = "平均": Cells(9, 2) = s / n
' 平均より重い人に印をつける
For i = 1 To n
If w(i) > s / n Then Cells(i + 1, 2) = "↑ 平均より上"
Next i
End Sub
「読み込む → 計算する → 書き出す」の 3 段に分かれているところに注目してください。
配列に入れてしまえば、②の計算はシートのことを知らなくてよいのです。
第 10 章の「関数は計算するだけにする」と同じ考え方で、
データの置き場所と計算を切り離すと、あとで扱いやすくなります。
なお、②で w(i) の代わりに毎回 Cells(i+1,1).Value と
書くこともできますが、セルを読むのは配列を読むよりずっと遅いので、
件数が多いときは配列に移してから計算するのが定石です。
以下のような 10 日間の気温データを読み込み、最高気温と最低気温を求めて表示しなさい。
元資料 12 枚目のプログラムです。そして 14 枚目に、こう書かれています。
実は、さきほど入力したプログラムは、間違っている。
最高気温と、最低気温が逆に表示される。
プログラムを修正して、正しい表示となるようにせよ。
これは、とてもよい課題です。 「動くけれど答えが違う」という不具合は、実際にいちばん多く、いちばん厄介です (エラーで止まってくれるほうがまだ親切なのです)。 まず本当に逆に出ることを自分の目で確かめてから、原因を探しましょう。
Function condition(ByVal m, ByVal n) As Boolean
If m > n Then
condition = True
Else
condition = False
End If
End Function
Sub Macro610()
Dim i As Integer
Dim a(11) As Integer
For i = 1 To 10
a(i) = Cells(1, i).Value
Next
Dim maxi As Integer, mini As Integer
maxi = 1
mini = 1
For i = 2 To 10
If condition(a(maxi), a(i)) Then maxi = i
If condition(a(i), a(mini)) Then mini = i
Next
Cells(2, 1).Value = Format(a(maxi), "最高気温##度")
Cells(2, 2).Value = Format(a(mini), "最低気温##度")
End Sub
「最高気温24度」「最低気温37度」と出ました。 データの最大は 37、最小は 24 ですから、たしかに逆です。
condition(m, n) は m > n のとき True を返す関数です。
これを踏まえて、ループの中の 2 行を読みます。
2 行とも、引数の順序が入れかわっています。
condition 自体は正しく「左 > 右」を判定しているので、
関数は無罪。呼び出し方が逆だったのです。
だから maxi は最小値の位置を、
mini は最大値の位置を指してしまい、
表示だけが「最高/最低」と書いてあるので、逆に見えるわけです。
| 元のまま(誤り) | 直したもの | |
|---|---|---|
| 最大を探す | condition(a(maxi), a(i)) |
condition(a(i), a(maxi)) |
| 最小を探す | condition(a(i), a(mini)) |
condition(a(mini), a(i)) |
Function condition(ByVal m, ByVal n) As Boolean
' この関数は変えていない
If m > n Then
condition = True
Else
condition = False
End If
End Function
Sub Macro611()
Dim i As Integer
Dim a(1 To 10) As Integer ' 1 To 10 と書いて意図をはっきりさせる
For i = 1 To 10
a(i) = Cells(1, i).Value
Next i
Dim maxi As Integer, mini As Integer
maxi = 1
mini = 1
For i = 2 To 10
If condition(a(i), a(maxi)) Then maxi = i ' i のほうが大きければ更新
If condition(a(mini), a(i)) Then mini = i ' 候補のほうが大きければ更新
Next i
Cells(2, 1).Value = Format(a(maxi), "最高気温##度")
Cells(2, 2).Value = Format(a(mini), "最低気温##度")
Cells(3, 1).Value = "最高は " & maxi & " 日目"
Cells(3, 2).Value = "最低は " & mini & " 日目"
End Sub
関数を使わずに素直に書くと、間違えにくくなります。
condition のような関数をはさむと、
引数の順序を頭の中で入れかえて考えなければならないので、
かえって誤りやすいのです。
比較をその場に書いてしまうほうが読みやすいことがあります。
If a(i) > a(maxi) Then maxi = i ' 「大きければ最大候補を更新」とそのまま読める If a(i) < a(mini) Then mini = i ' 「小さければ最小候補を更新」
第 10 章で「部品に分けよう」と言いましたが、 分けたほうが分かりにくくなるなら、分けないのが正解です。 判断の基準は「読んで意味が通るか」です。
Sub Macro612()
Dim a(1 To 10) As Integer
Dim i As Integer
Dim maxi As Integer, mini As Integer
For i = 1 To 10
a(i) = Cells(1, i).Value
Next i
maxi = 1: mini = 1
For i = 2 To 10
If a(i) > a(maxi) Then maxi = i
If a(i) < a(mini) Then mini = i
Next i
Cells(2, 1) = Format(a(maxi), "最高気温##度")
Cells(2, 2) = Format(a(mini), "最低気温##度")
' 最高・最低の日に色をつける
Cells(1, maxi).Interior.Color = RGB(255, 199, 206)
Cells(1, mini).Interior.Color = RGB(183, 230, 240)
Cells(4, 1) = "赤が最高、水色が最低の日"
End Sub
初期値を 0 にしてはいけません。
maxi = 1 のように1 番目のデータを最初の候補にするのが定石です。
もし「最大値を入れる変数を 0 で始める」と書くと、
気温が全部マイナスの日(冬)には、答えが 0 になってしまいます。
データの中の 1 つを初期値にすると、この心配がありません。
以下のような成績データについて、1 次元配列を使って「国語」と「数学」の順位を計算せよ。
順位の考え方はとても単純です ── 自分より点数が高い人の数を数えて、それに 1 を足す。 1 位なら「自分より高い人は 0 人」なので 0 + 1 = 1 位です。
Sub Macro3()
Dim n As Integer
Dim i As Integer, j As Integer
Dim kok(1 To 20) As Integer ' 国語
Dim sug(1 To 20) As Integer ' 数学
Dim goukei(1 To 20) As Integer
Dim jun As Integer
n = 8 ' 人数
' 読み込む
For i = 1 To n
kok(i) = Cells(i + 1, 2).Value
sug(i) = Cells(i + 1, 3).Value
goukei(i) = kok(i) + sug(i)
Next i
' 国語の順位
For i = 1 To n
jun = 1
For j = 1 To n
If kok(j) > kok(i) Then jun = jun + 1 ' 自分より上を数える
Next j
Cells(i + 1, 4) = jun
Next i
' 数学の順位
For i = 1 To n
jun = 1
For j = 1 To n
If sug(j) > sug(i) Then jun = jun + 1
Next j
Cells(i + 1, 5) = jun
Next i
' 合計と総合順位
For i = 1 To n
Cells(i + 1, 6) = goukei(i)
jun = 1
For j = 1 To n
If goukei(j) > goukei(i) Then jun = jun + 1
Next j
Cells(i + 1, 7) = jun
If jun = 1 Then Cells(i + 1, 7).Interior.Color = RGB(255, 235, 156)
Next i
End Sub
同点のとき、この方法は「同じ順位」になります。 上のデータで有吉と浅岡はどちらも数学 73 点・76 点ではありませんが、 同点を作って試してみてください ── 両方とも同じ順位になり、 その次の人は順位が飛びます(1, 2, 2, 4 …)。 これはスポーツの順位づけと同じ流儀で、意図してこうなっています。 「1, 2, 2, 3」としたいなら別の数え方が必要です。
なお、この方法は 1 人の順位を求めるのに全員と比べるので、 n 人なら n² 回の比較になります。 人数が多いときは先に並べ替えてから順位を振るほうが速くなります(第 12 章)。
元資料 8 枚目の「動的配列の宣言」です。 件数が実行するまで分からないときに使います。
Dim a() As Integer ' 括弧を空にして宣言
ReDim a(1 To n) ' 大きさが決まってから確保
ReDim Preserve a(1 To m) ' 中身を残して作り直す
Sub Macro4()
Dim a() As Integer ' 大きさを決めずに宣言
Dim i As Integer
Dim n As Integer
Dim s As Long
' ① まず件数を数える(空になるまで下へ)
n = 0
Do While Cells(n + 2, 1).Value <> ""
n = n + 1
Loop
' ② 決まった大きさで確保する
ReDim a(1 To n)
' ③ 読み込む
For i = 1 To n
a(i) = Cells(i + 1, 1).Value
s = s + a(i)
Next i
Cells(9, 1) = "件数": Cells(9, 2) = n
Cells(10, 1) = "合計": Cells(10, 2) = s
Cells(11, 1) = "平均": Cells(11, 2) = Format(s / n, "#.##")
Cells(12, 1) = "配列の上限": Cells(12, 2) = UBound(a)
Cells(13, 1) = "データを増やしても、プログラムは変えなくてよい"
End Sub
これが実務での基本形です。 「20 人ぶん」と決め打ちにすると、21 人目が来たときに直さなければなりません。 まず数えて、それから確保するようにしておけば、 データが増えてもプログラムはそのままです。 上のモデルで、A8 に点数を書き足して実行しなおしてみてください ── 件数が自動で 7 になります。
添字を 2 つにすると、表の形のまま扱えます。 第 4 章の行列がまさにこれでした。
Dim t(1 To 4, 1 To 3) As Integer ' 4 行 3 列 = 12 個の箱
Sub Macro5()
Dim t(1 To 4, 1 To 3) As Integer
Dim i As Integer, j As Integer
Dim gyoukei As Long ' 行の合計
Dim souke As Long ' 全体の合計
' 読み込む
For i = 1 To 4
For j = 1 To 3
t(i, j) = Cells(i + 1, j + 1).Value
souke = souke + t(i, j)
Next j
Next i
Cells(1, 5) = "学部計"
Cells(1, 6) = "割合(%)"
' 学部ごとの合計と、全体に対する割合
For i = 1 To 4
gyoukei = 0
For j = 1 To 3
gyoukei = gyoukei + t(i, j)
Next j
Cells(i + 1, 5) = gyoukei
Cells(i + 1, 6) = Format(gyoukei / souke * 100, "#.#")
Next i
' 学年ごとの合計
Cells(6, 1) = "学年計"
For j = 1 To 3
gyoukei = 0
For i = 1 To 4
gyoukei = gyoukei + t(i, j)
Next i
Cells(6, j + 1) = gyoukei
Next j
Cells(6, 5) = souke
Cells(6, 5).Interior.Color = RGB(255, 235, 156)
Cells(8, 1) = "縦に足すのも横に足すのも、ループの順を入れかえるだけ"
End Sub
学部ごとの合計と学年ごとの合計で、ループの内と外が入れかわっているだけです。
t(i, j) という書き方をしていれば、
どちらの向きにも同じように足せます。
これは表を Cells で直接扱っていてもできますが、
配列にしておくと何度も読み直しても速いという利点があります。
w0 の 0 は名前の一部、w(0) の 0 は数。
だから w(i) と書けて、ループで回せるDim w(5) は 6 個(0〜5)。
はっきり書くなら Dim w(1 To 5)Dim a() → 数える → ReDimもとにしたのは「情報学入門(VBA)160-2019」全 20 枚です。
| 元資料 | この教材 | 種別 |
|---|---|---|
5 人ぶんのデータに対して Dim w(5) as Integer と宣言している |
正しく動くが、できる箱は 6 個(w(0)〜w(5))で 1 つ余る。
「5 人だから w(5)」と読むと混乱するので、括弧の中は「個数」ではなく「添字の上限」だと明示し、
Dim w(1 To 5) という書き方を勧めた | 不足 |
as Integer(小文字) | VBA は大小を区別しないので動くが、慣例は As Integer | 表記 |
| 最大値・最小値の初期値の決め方について説明がない | 1 番目のデータを最初の候補にするのが定石。 0 から始めると全部マイナスのデータで破綻することを追記 | 不足 |
condition 関数を使う設計だが、
引数の順序を頭の中で入れかえる必要があり誤りやすい |
比較をその場に書いた版(If a(i) > a(maxi) Then)も示し、
分けたほうが分かりにくくなるなら分けないという判断基準を書いた | 不足 |
| 順位計算で、同点のときどうなるかに触れていない | 同じ順位になり次が飛ぶ(1, 2, 2, 4)。意図してそうなっていることを明記 | 不足 |
| 動的配列の宣言が書式のみ | 「まず数えて、それから ReDim」という実務の基本形として、
データを増やしても直さなくてよい例を示した | 不足 |
| エラトステネスのふるいを配列で書き直す課題(15・16 枚目) | 第 10 章で配列を使って実装済みなので、この章では繰り返さない | 重複 |
元資料の「間違いを直せ」という課題について。
14 枚目の「実は、さきほど入力したプログラムは、間違っている」は
意図された課題であり、誤りではありません。
むしろこの授業でいちばん優れた課題設計だと思います ──
「エラーは出ないが答えが違う」という、実務でもっとも多く、
もっとも見つけにくい種類の不具合を扱っているからです。
そこでこの教材では、まず本当に逆に出ることを実行して見せ、
原因(condition の引数の順序)を分解し、
2 通りの直し方を並べる、という構成にしました。
元資料で正しかったこと。
「配列とはいくつかの変数を集めたもの」「各箱は必ず同じ型」「添字は 0 から始まる」という説明、
w0 = 50: w1 = 58 … から w(0) = 50: w(1) = 58 … への
導入の流れ、condition 関数そのもの(m > n で True)、
気温データ(24〜37)に対する正解(最高 37 度・最低 24 度)── いずれも正確です。
12 枚目のプログラムはこのページで実行し、資料の言うとおり
「最高気温24度・最低気温37度」と逆に出ることを確認しました。