なぜ、配列の理解が重要なのか、その理由

この記事では、当社の新人エンジニア研修の参考にC#を解説します。

前回は「繰り返しで単純作業をコンピュータに任せる」について学びました。コンピュータの力を実感できたかと思います。

今回は配列の作成と使用について解説します。これまで学んだ通り、変数に値を格納して処理するのが基本でしたが、変数が10個、20個、100個… と増えてくると大変です。

そこで便利なのが配列です。配列とは「同じ型の複数の要素を並べて管理するデータ構造」です。C#では、インデックス(添字)を使って要素にアクセスします。

新人エンジニア
配列のイメージ

1. 配列の使い方

1) 配列を表す変数を宣言する

int[] ages;

  • int[] は「整数型(int)の配列」を表します。
  • ages は「intの配列型」の変数名です(まだ要素は確保していない)。

2) 配列の要素を確保する

ages = new int[3];

  1. new int[3] で「要素数3のint配列」をヒープ(メモリ)に生成し、先頭の参照を返します。
  2. この段階で ages[0], ages[1], ages[2] が使えます(初期値は 0)。
新人エンジニア
配列の初期値は0である

3) 添字(インデックス)を用いて要素に値を代入する

ages[0] = 10;
ages[1] = 20;
ages[2] = 51;


配列インデックスは 0から開始 し、(要素数 - 1) が最大インデックスです。

4) 配列の要素を参照(読み出し)する

Console.WriteLine(ages[2]); // 51


ages[2] の値を取り出してコンソールに表示します。

サンプル:配列とfor

using System;

namespace Chap06
{
    public class Example01
    {
        public static void Main()
        {
            int[] ages;
            ages = new int[3];

            ages[0] = 10;
            ages[1] = 20;
            ages[2] = 51;

            for (int i = 0; i < 3; i++)
            {
                Console.WriteLine(ages[i]);
            }
        }
    }
}

<実行結果>

10
20
51

配列と繰り返し文は非常に相性が良いです。

配列の長さ Length

C#ではages.Length プロパティを使うことで、配列の要素数を取得できます。

for (int i = 0; i < ages.Length; i++)
{
    Console.WriteLine(ages[i]);
}

配列の要素数が変更されても自動的に対応できます(マジックナンバーの回避)。

配列の初期化リテラル

配列を一度に初期化する方法もあります。

int[] ages = { 10, 20, 51 };

この場合、要素数は 3 と自動的に確定します。

例題

以下の配列の場合、ages.lengthの値と最大の添字を答えなさい。

int[] ages = {10, 20, 51, 52, 53};
int[] ages = {10};

2. IndexOutOfRangeException

C#で、存在しない配列要素を指定すると IndexOutOfRangeException が発生します。

int[] ages = new int[3];
ages[3] = 10; // 配列は要素数3なので、有効インデックスは0~2のみ

<実行時エラー>

Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.

C#のコンパイラは、このエラーをコンパイル時には検出してくれません。実行時にインデックスが不正と判明して初めてエラーが出ます。

3. 参照とメモリのイメージ

ここまで ages[0], ages[1] などの要素に実際のデータが入ることを学びました。では、ages 自体には何が入っているのでしょうか?

C#ではクラスや配列は“参照型”

int[] ages = new int[3];
Console.WriteLine(ages);

を実行すると、C#ではたとえば System.Int32[] のような型情報が表示されるでしょう(環境により異なりますが、System.Int32[] とか System.Int32[3] などと表示する場合もあります)。

この中身は配列オブジェクトへの参照です。

スタック領域とヒープ領域

  • スタック領域に配列変数agesが置かれる(内部にヒープ領域へのポインタ/参照を保持)
  • ヒープ領域に「要素数3のint配列」が確保される
  • ages は、ヒープ上にある配列オブジェクトを指し示す“参照”を保持します
  • ages[0], ages[1], ages[2] はヒープ内に確保された実際のデータです

イメージ図(簡略化):

スタック領域はローカル変数やメソッドが格納される領域です。【stack】には英語で「積み重ねた」という意味があります。積み重ねられた本のように、後から入れたものが先に取り出される構造をしています【Last-In First-Out(LIFO)】。

ヒープ領域はインスタンスが格納される領域です。個人的には“インスタンス”領域と呼んでもいいと思うのですが、英語の【heap】に「山積み、山盛り」という意味があり、メモリ容量が大きくなる可能性があるインスタンスはこのヒープ領域に格納されるのです。

例えば、スタック領域に格納されるプリミティブ型のデータは大きくてもdouble型の64bitです。対して配列などのインスタンスはどれだけ大きくなるかわからないためヒープ領域に格納します。例えるなら、家具屋さんで、店頭にはカタログだけ用意して、大きな商品は倉庫に置いておくようなイメージでしょうか。

メモリ内の動作のイメージ①
メモリ内の動作のイメージ①

②メモリにはアドレスがあります。

ローカル変数に格納されるのは、配列が格納される予定のヒープ領域の先頭アドレス(ここでは15db9742)です。(下図参照)

メモリ内の動作のイメージ②
メモリ内の動作のイメージ②

③要素がヒープ領域に格納されます。(下図参照)

メモリ内の動作のイメージ③
メモリ内の動作のイメージ③

値型と参照型の違い

  1. int, double, bool などの組み込み値型はスタック上に直接データを保持します。
  2. 配列やクラスのインスタンスは参照型となり、変数にはオブジェクトの参照(アドレス)が入ります。

例題

以下の処理が行われると何が表示されますか?また、それはなぜですか?

int a = 1;
int b = a;
a = 2;
Console.WriteLine($"{a}:{b}");  // 出力は「2:1」

int[] a1 = { 1 };
int[] a2 = a1;
a1[0] = 2;
Console.WriteLine($"{a1[0]}:{a2[0]}");  // 出力はどうなる?

4. 配列の要素をまとめて表示する

配列の全要素を一度に出力したいとき、C#では以下の2つの方法があります。

方法1:string.Join を使う

using System;

namespace Chap06
{
    public class Example05
    {
        public static void Main()
        {
            int[] ages = { 10, 20, 51 };
            // 文字列化して出力
            Console.WriteLine(string.Join(", ", ages)); 
        }
    }
}

<実行結果>

10, 20, 51

string.Join は第1引数に区切り文字、第2引数に配列を渡すと「10, 20, 51」のような文字列を生成します。

方法2:次のforeach 文を使う

5. foreach

foreach 文は配列などの列挙可能なオブジェクトに対して、先頭から末尾まで自動で繰り返してくれます。

foreach (型 変数 in 配列)

using System;

namespace Chap06
{
    public class Example06
    {
        public static void Main()
        {
            int[] ages = { 10, 20, 51 };

            foreach (int age in ages)
            {
                Console.WriteLine(age);
            }
        }
    }
}

<実行結果>

10
20
51
新人エンジニア
foreach 文のイメージ

注意:age は「一時変数」

foreach (int age in ages)
{
    age *= 2; 
}

このように一時変数ageに新たな値を代入しても、もとの配列内容は変わりません。C#では age は配列要素のコピーに過ぎず、配列自体の要素を書き換えているわけではありません。

  1. メリット: インデックス管理をせずに書けるため、IndexOutOfRangeException のリスク減
  2. 制限: 先頭から順にしか処理できず、要素を飛ばして走査したり、逆順で走査したりするには工夫が必要
  3. 適用範囲: 配列やList<T>などのコレクションに対して使えます(コレクションは後述)。

例題

次のfor文の繰り返し回数を答えなさい。

int[] a = new int[3] { 0, 1, 2 };
foreach (int i in a) { }   // 繰り返し回数は?

int[] b = { 1, 2, 3, 4, 5 };
foreach (int i in b) { }  // 繰り返し回数は?

char[] c = { 'A', 'B', 'C', 'D' };
foreach (char d in c) { } // 繰り返し回数は?

6. 多次元配列(2次元配列の例)

C#でも2次元以上の多次元配列を扱うことができます。見た目は行列のようですが、実際はいくつかの方法があります。ここでは「配列の配列」(ジャグ配列)を紹介します。

新人エンジニア
2次元配列の本当の姿

6-1. ジャグ配列(Jagged Array)

C#で int[][] scores = new int[][] { ... }; のように書くと「配列の配列」が作れます。長さの異なる配列を持つことも可能です。ジャグ配列といいます。

using System;

namespace Chap06
{
    public class Example08
    {
        public static void Main()
        {
            int[][] scores = {
                new int[] { 10, 20, 30, 40 },
                new int[] { 50, 60, 70, 80 },
                new int[] { 90, 10, 20, 30 }
            };

            Console.WriteLine(scores[0][3]); // 40
            Console.WriteLine(scores[2][3]); // 30

            // 二重の foreach
            foreach (int[] row in scores)
            {
                foreach (int val in row)
                {
                    Console.Write($"{val},");
                }
            }
        }
    }
}

<実行結果>

40
30
10,20,30,40,50,60,70,80,90,10,20,30,

「配列の中にint[]を3個格納」しており、それぞれが4要素の配列を持っています。

7. 配列とオブジェクト指向

配列は非常に便利ですが、要素数を後から変更できないなどの制限があります。C#では可変長のコレクションとして List<T> などが用意されており、実務ではそちらを使う機会が多いでしょう。

例:配列で5名の生徒の成績を管理する

using System;

namespace Chap06
{
    public class Example09
    {
        public static void Main()
        {
            int[] scores = { 80, 75, 100, 90, 80 };
            string[] names = { "Tom", "John", "Mary", "Ken", "Jimmy" };

            for (int i = 0; i < scores.Length; i++)
            {
                Console.WriteLine($"{names[i]}'s score is {scores[i]}");
            }
        }
    }
}

<実行結果>

Tom's score is 80
John's score is 75
Mary's score is 100
Ken's score is 90
Jimmy's score is 80

int型とString型のように違った型を1つのデータにまとめるにはどうしたら良いでしょうか?

古典的には以下のように「別々の配列」で同じインデックスを共有して表現していました。(インデックス0番の人の名前はTom、スコアは80点のように)

しかしオブジェクト指向(クラス設計)が普及してからは、1人分のデータを1つのオブジェクトとしてまとめるやり方が主流となっています。

メージは下図の通りです。

新人エンジニア
何でも配列に入れていた時代もあった!

<まとめ:隣の人に正しく説明できたらチェックを付けましょう>

□ 配列は同じ型の複数の変数が並んだものと考えられる

□ 配列の添字(index)は0~(要素数-1)である

□ 拡張foreach文は配列の操作を簡潔に記述できます。

□ 参照型にはオブジェクト(配列含む)のアドレスが入っている

次回は、「7章. 文字列を扱ってユーザーにメッセージを伝える」を学びます。

最後までお読みいただきありがとうございます。