なぜ、インスタンスの理解が重要なのか、その理由

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

前回は「メソッドを定義して処理を再利用する」について学びました。処理を部品として再利用できるようになりましたね。

今回はいよいよインスタンスの活用について解説します。データと手続きを一体化したインスタンスによって、よりプログラムの部品化が進むことになります。

これまで話を単純化するために「インスタンス」を避けてきましたが、C#でのオブジェクト指向の本質はクラスからインスタンスを生成し、各インスタンスがデータと振る舞いを持つところにあります。staticメソッドだけではなく、インスタンスを活用する方法をしっかり学びましょう。

1. オブジェクト指向とは何だったか?

1.オブジェクト指向とは何だったか?

Objectとは「モノ」のことでした。

例えば、スマートフォンを考えてみましょう。スマートフォンには、メーカー、OS、バッテリー容量などの属性(データ)があり、通話する、アプリを開くといった動作(メソッド)を持っています。このように、データと動作をセットにしたものがオブジェクトです。

この記事の1回目でお話ししたように、オブジェクト指向とは他の工業製品と同じように部品を組み合わせることでプログラム全体を作り上げるという考え方でした。

例えば、新人エンジニアの皆さんがお使いのパソコンも多くの部品から構成されています。私たちは、それぞれの部品の内部構造を知らなくても、それらを組み合わせて使うことができます。大多数の人は“CPU”と“メモリ”の区別も曖昧なままスマホやパソコンを使っています。それでもスマホとパソコンをつないでデータを交換したりインターネットを見たりすることはできます。また、それぞれの部品単位でアップデートすることもできます。例えば、古くなったハードディスクを新しいものに交換する、などといったことができます。

この考え方をプログラムに応用して、再利用性やメンテナンス性の高いプログラム部品でシステムを組むのがオブジェクト指向です。

つまり、オブジェクトはプログラムの部品です。そして、オブジェクトの設計図がクラスです。クラスには、オブジェクトに共通する属性(情報や機能)を定義します。この設計図をもとに作成した実際の部品がインスタンスです。

人によって言葉の使い方に多少のずれがありますが、この研修では、

オブジェクト(抽象概念) = クラス(設計図) + インスタンス(具体的なもの)

という言葉で使い分けています。

単にオブジェクトといった場合には、それがクラスのことを指すこともあれば、インスタンスのことを指すこともあります。ですから本書ではオブジェクトという言葉の使用は極力避けて、クラスとインスタンスという言葉を使うようにしています。

繰り返しになりますが、オブジェクト指向とは、プログラムを「部品の組み合わせ」として設計し、再利用性やメンテナンス性を高める考え方です。

  1. オブジェクト: データとその振る舞いをまとめたもの(抽象概念)。
  2. クラス: オブジェクトの設計図。
  3. インスタンス: クラスを元に実際に作成されたオブジェクト。

例えば、今現在あなたが見ているこの画面も、ウインドウというインスタンスの上にメニューというインスタンスが乗っていて、クリックの情報を受け取るインスタンスがある、という風になっているというと少しはイメージの助けになりますでしょうか?

論より証拠、以下のプログラムを実行していただくとお分かりただけると思います。

using System;
using System.Windows.Forms;

class Example00
{
    [STAThread]
    static void Main()
    {
        Form form = new Form();
        form.Width = 1000;
        form.Height = 500;
        Application.Run(form);
    }
}

例えばウインドウサイズを変更したいとき、どこをいじれば良いか分かりますか?

これは比喩的な表現ですが、パソコンクラスというものを作ったとして、あなたや隣の人の机に乗っている具体的なパソコンがインスタンスです。新入社員クラスがあったとして、あなたやあなたの隣の人がそのインスタンスです。

2. フィールド(インスタンス変数)を持ったクラスの定義

例えば新入社員の皆さんで下図の名簿を作ったとします。名簿の項目はクラスに当たります。そして一人一人の行はインスタンスです。

例:新人エンジニアクラス

話を簡単にするため、新人エンジニアクラスが「社員番号(id)」と「名前(name)」2つの情報だけを持つとします。C#のクラスであれば、以下のように定義できます。

新人エンジニア
クラスとインスタンスを名簿に例えると
namespace Chap09
{
    class NewEngineer1
    {
        // フィールド(または「インスタンス変数」)
        public int Id;
        public String Name;
    }
}
  1. int Id; は「社員番号」のインスタンス変数
  2. String Name; は「名前」のインスタンス変数

この時点ではメインメソッドがないので、実行はできません。「NewEngineer1クラス」を使ってテストしたいなら、別にエントリーポイント(Mainメソッド)を用意する必要があります。

インスタンスの生成とフィールドの使用

次は、クラスを使うクラスを作って試しましょう。

namespace Chap09
{
    public class Example01
    {
        public static void Main(string[] args)
        {
            NewEngineer1 se1 = new NewEngineer1();
            Console.WriteLine(se1.Id);
            Console.WriteLine(se1.Name);
        }
    }
}

  1. NewEngineer1 se1;
    • これは「NewEngineer1型の変数se1を宣言」している。まだ何も入っていない。
  2. se1 = new NewEngineer1();
    • new 演算子で「NewEngineer1クラスのインスタンス」をヒープ領域に生成し、その参照をse1に代入。
    • コンストラクタ NewEngineer1() が暗黙的に呼ばれ、インスタンスを初期化。
  3. Console.WriteLine(se1.Id);
    • フィールドIdの値を出力。まだ値を設定していないので、C#では既定値0が出力されます。(intの既定値が0)
  4. Console.WriteLine(se1.Name);
    • こちらは文字列型の既定値が null なので、何もない状態が出力されます。

<実行結果>

0
(空行またはnull)

C#でのフィールドの既定値は以下のように決まっています。

  1. boolfalse
  2. 数値型 (int, long, double etc.) → 0 (各型に応じた0)
  3. char'\0' (Unicode NUL)
  4. 参照型 (string, カスタムクラスなど) → null

フィールドに値を代入

namespace Chap09
{
    public class Example02
    {
        public static void Main(string[] args)
        {
            NewEngineer1 se1 = new NewEngineer1();
            se1.Id = 1;
            se1.Name = "yamada";
            Console.WriteLine(se1.Id);
            Console.WriteLine(se1.Name);
        }
    }
}

<実行結果>

1
yamada

ここで使っている IdName は、各インスタンスが固有に持つデータなので「インスタンス変数」とも呼びます。

3. メソッドを持ったクラスの定義

例:show()メソッドを追加

同じ新人エンジニアクラスにメソッドを持たせることもできます。次の NewEngineer2 は、Show()メソッドを定義して、一度にIdNameを表示します。

namespace Chap09
{
    class NewEngineer2
    {
        public int Id;
        public string Name;

        // インスタンスメソッド
        public void Show()
        {
            Console.WriteLine($"私のIDは{Id}、名前は{Name}です。");
        }
    }
}

namespace Chap09
{
    public class Example03
    {
        public static void Main()
        {
            NewEngineer2 se2 = new NewEngineer2();
            se2.Id = 2;
            se2.Name = "tabuchi";
            se2.Show();
        }
    }
}

<実行結果>

私のIDは2、名前はtabuchiです。
  1. Show() はいわゆるインスタンスメソッド。呼び出し方は 変数名.メソッド名()
  2. Show() 内では IdName を直接参照できる(同じインスタンス内なので)。

4. インスタンスを複数生成

namespace Chap09
{
    public class Example04
    {
        public static void Main()
        {
            NewEngineer2 yamazaki = new NewEngineer2();
            yamazaki.Id = 1;
            yamazaki.Name = "yamazaki";
            yamazaki.Show(); // 私のIDは1、名前はyamazakiです。

            NewEngineer2 imai = new NewEngineer2();
            imai.Id = 2;
            imai.Name = "imai";
            imai.Show(); // 私のIDは2、名前はimaiです。
        }
    }
}

同じクラスNewEngineer2から複数インスタンスをnewで作って、各々のフィールドを設定できます。

ToString() メソッド呼び出し時の表示

C#でインスタンス変数を Console.WriteLine(se2) のように直接書くと、ToString() が呼ばれて文字列が返されます。未オーバーライドのままだと Namespace.ClassName やハッシュコードが出力されます。

Console.WriteLine(yamazaki);
// 例: Chap09.NewEngineer2

ToString()をオーバーライドすれば、任意の文字列を返すことができます。が、ここではまだ説明しません(継承ポリモーフィズムで詳説)。

5. 参照型とプリミティブ型

C#でも変数には値型参照型があります。

  • 値型: bool, int, doubleなど
    • 変数そのものがデータの実体を持つ
  • 参照型: string, 配列 (T[]), ユーザー定義クラス (class)、objectなど
    • 変数には実体(オブジェクト)へのポインタ/アドレス(参照)が入る

NewEngineer2 se2 = new NewEngineer2(); の場合、se2 には インスタンスそのものではなく「ヒープ上のインスタンスを指す参照」が格納されます。

新人エンジニア
C#の型の体系

1つのインスタンスを複数の変数で参照

NewEngineer2 se1 = new NewEngineer2();
se1.Id = 1;
se1.Name = "yamazaki";

NewEngineer2 se2 = se1; // 同じ参照をコピー
se2.Show();

se2 も同じオブジェクトを指すため、se2.Show() の結果は se1.Show() と全く同じ出力になります。

新人エンジニア
一つのインスタンスを2つの参照が指している状態

6. 参照型の配列

インスタンスを配列にまとめることもできます。

namespace Chap09
{
    public class Example06
    {
        public static void Main()
        {
            NewEngineer2[] seArray = new NewEngineer2[3];

            // インスタンスを生成して配列の各要素に代入
            seArray[0] = new NewEngineer2();
            seArray[1] = new NewEngineer2();
            seArray[2] = new NewEngineer2();

            seArray[0].Id = 1; seArray[0].Name = "tabuchi";
            seArray[1].Id = 2; seArray[1].Name = "shinohara";
            seArray[2].Id = 3; seArray[2].Name = "kokubun";

            foreach (NewEngineer2 e in seArray)
            {
                e.Show();
            }
        }
    }
}

<実行結果>

私のIDは1、名前はtabuchiです。
私のIDは2、名前はshinoharaです。
私のIDは3、名前はkokubunです。

配列の要素は最初null

値型の配列と異なり、「new NewEngineer2[3]」 だけではインスタンスが生成されません。配列の各要素に参照を格納するため、seArray[i] = new NewEngineer2(); が必要です。

もしこれを忘れると、seArray[0]null のままなので、後で seArray[0].Name = "tabuchi"; のようにアクセスすると NullReferenceException が発生します。

7. NullReferenceException

C#で「存在しないオブジェクトへの参照」を使おうとすると、NullReferenceException が発生します。

NewEngineer2[] seArray = new NewEngineer2[3];
// まだ要素はnull
seArray[0].Id = 1;  // ← ここで NullReferenceException

エラー例:

Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.

seArray[0]null なのに seArray[0].Id とアクセスしようとしたためです。

null とは

  1. null は参照型変数が「どのオブジェクトも指していない」特別な状態
  2. null""(空文字) は別物
  3. null0"null" とも違う
string str = null;
Console.WriteLine(str == null);      // True
Console.WriteLine(str == "null");    // False
Console.WriteLine(str == "");        // False

C#のガーベージコレクションにより、参照されなくなったオブジェクトは自動的にメモリ解放されます(いつ解放されるかはランタイム依存)。

8. 参照による値渡しと値渡し

C#では、引数の受け渡しはすべて“値渡し” ですが、参照型の場合「参照(アドレス)の値」がコピーされて渡るため、結果的に同じオブジェクトを共有します。

例: 参照(オブジェクト)を引数に渡す

static void Show(NewEngineer1 se)
{
    Console.WriteLine($"{se.Id}:{se.Name}");
}

public static void Main()
{
    NewEngineer1 yamazaki = new NewEngineer1 { Id = 1, Name = "yamazaki" };
    Show(yamazaki);
}

Show(yamazaki)yamazaki 参照のコピーが渡り、Show内のseyamazakiが同じオブジェクトを指します。

メソッド内でオブジェクトを変更すると呼び出し元にも反映

static void Modify(NewEngineer1 se)
{
    se.Name += "_san";
}

public static void Main()
{
    NewEngineer1 yamazaki = new NewEngineer1 { Id = 1, Name = "yamazaki" };
    Modify(yamazaki);
    Console.WriteLine(yamazaki.Name); // yamazaki_san
}

呼び出し先メソッドで Nameを書き換えると、呼び出し元でも変更が見える。

プリミティブ(値型)の場合

static void PlusOne(int i)
{
    i++;
    Console.WriteLine($"呼び出し先のi: {i}");
}

public static void Main()
{
    int i = 10;
    Console.WriteLine($"呼び出し元のi: {i}");
    PlusOne(i);
    Console.WriteLine($"呼び出し元のi: {i}");
}

<実行結果>

呼び出し元のi: 10
呼び出し先のi: 11
呼び出し元のi: 10

値型ではメソッド呼び出し時に「値のコピー」が渡されるため、呼び出し先で変更しても呼び出し元に影響しません。

新人エンジニア
参照による値渡しと単なる値渡し




9. メソッドの戻り値に参照を使う

メソッドの戻り値にクラスのインスタンスを返すと、実質的に複数の値(フィールド)をまとめて返せます。

static NewEngineer2 Compare(NewEngineer2 se1, NewEngineer2 se2)
{
    return (se1.Id > se2.Id) ? se1 : se2;
}

public static void Main()
{
    NewEngineer2 e1 = new NewEngineer2 { Id = 8, Name = "shinohara" };
    NewEngineer2 e2 = new NewEngineer2 { Id = 2, Name = "imai" };
    NewEngineer2 bigger = Compare(e1, e2);
    bigger.Show();
}

ここで Compare メソッドは「idが大きいほうのインスタンス」を戻り値として返します。戻ってきたインスタンスを呼び出し元で再利用可能です。

10. コンストラクタでインスタンスの初期化

クラス名と同じ名前のメソッドが「コンストラクタ」です。new クラス名(...) で呼び出され、インスタンス生成時に一度だけ自動実行されます。

基本構文

class クラス名
{
// コンストラクタ
public クラス名(引数リスト)
{
// インスタンスの初期化処理
}
}


コンストラクタには戻り値の型を付けません。(void も書きません)

例: NewEngineer3 クラス

namespace Chap09
{
    class NewEngineer3
    {
        public int Id;
        public string Name;

        // コンストラクタ
        public NewEngineer3(int id, string name)
        {
            this.Id = id;
            this.Name = name;
        }
    }
}

namespace Chap09
{
    public class Example15
    {
        public static void Main()
        {
            NewEngineer3 se = new NewEngineer3(3, "tabuchi");
            Console.WriteLine($"{se.Id} : {se.Name}");
        }
    }
}

<実行結果>

3 : tabuchi

  1. this.Id = id;this は「このインスタンス自身」を指すキーワード。フィールドと引数の区別のためによく使われます。
  2. コンストラクタを定義しなかった場合、C#では「デフォルトコンストラクタ」が自動生成されます(引数なしでインスタンス生成可能になる)。

インスタンスを変数に代入する意味

もしも、インスタンスをヒープ領域に作って使い捨てにするのであれば、以下のように書けば実現できます。

new Sample("imai");

using System;

namespace P09
{
    class ConstructorTest1
    {
        static void Main(string[] args)
        {
            new Sample("imai");
        }
    }

    class Sample
    {
        private string name;

        public Sample(string name)
        {
            Console.WriteLine("コンストラクタが呼ばれてインスタンスが作られました。");
            this.name = name;
            Greet();
        }

        public void Greet()
        {
            Console.WriteLine("私の名前は:" + name);
        }
    }
}

しかし、これではインスタンスを再利用できません。

再利用するために以下のように変数に入れるのです。

Sample sample = new Sample("imai");

using System;

namespace P09
{
    class ConstructorTest2
    {
        static void Main(string[] args)
        {
            Sample sample = new Sample("imai");
            Console.WriteLine(); // 空行を出力
            sample.Greet();
        }
    }
}

コンストラクタのオーバーロード

コンストラクタもメソッド同様にオーバーロードできます。引数の数や型が違うコンストラクタを複数定義可能です。

class NewEngineer4
{
    public int Id;
    public string Name;

    // (1) 引数2つ
    public NewEngineer4(int id, string name)
    {
        this.Id = id;
        this.Name = name;
    }

    // (2) 引数なし → (1)のコンストラクタを呼び出す
    public NewEngineer4()
        : this(0, "名無し")
    {
    }
}

namespace Chap09
{
    public class Example16
    {
        public static void Main()
        {
            NewEngineer4 se = new NewEngineer4(); // 引数なし
            Console.WriteLine($"{se.Id} : {se.Name}"); 
            // => 0 : 名無し
        }
    }
}

: this(...) 構文を使ってコンストラクタの中で別のコンストラクタを呼び出せます。

11. インスタンス変数と static変数 の使い分け

  1. インスタンス変数 … 各インスタンスが固有に持つ情報(例: Id, Name
  2. static変数 (クラス変数) … クラス全体で共有したい情報(例: 全新人エンジニアの総数)
class NewEngineer6
{
    public static int Count;
    public int Id;
    public string Name;

    public NewEngineer6(int id, string name)
    {
        this.Id = id;
        this.Name = name;
        Count++;
        Show();
    }

    public void Show()
    {
        Console.WriteLine($"{Id} : {Name} : {Count}人目です。");
    }
}

public class Example19
{
    public static void Main()
    {
        new NewEngineer6(4, "imai");
        new NewEngineer6(3, "shinohara");
        new NewEngineer6(2, "tabuchi");
        Console.WriteLine($"クラスの総人数: {NewEngineer6.Count}");
    }
}

<実行結果>

4 : imai : 1人目です。
3 : shinohara : 2人目です。
2 : tabuchi : 3人目です。
クラスの総人数: 3

Countstatic int なので、インスタンスごとに別々の値を持たない。「全体でただ1つ」

IdName は各インスタンス固有の情報

以下の「メモリの3つの領域のイメージ」も参考にして下さい。

新人エンジニア
メモリの3つの領域のイメージ

staticメソッドとインスタンスメソッドの使い分け

  1. インスタンスメソッド … 個々のインスタンスが持つ情報this)を操作したい場合。
  2. staticメソッド … インスタンスに依存しない処理(汎用的ユーティリティなど)。

C#の System.Math クラスはstaticメソッド (Math.Sin(...), Math.Cos(...) 等) を多く持ち、「インスタンスを生成するまでもない共通処理」を表現します。

また、Mathクラスのようにインスタンスを作ってほしくない場合は、private constructor でブロックします。

public static class MyUtility
{
    // 静的クラスとするとインスタンス生成不可
    public static double MyStaticMethod()
    {
        ...
    }
}

C#では static class と書くと、そのクラスはすべて静的メンバーだけを持つようになります(インスタンス化禁止)。


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

□ オブジェクト指向は、再利用性やメンテナンス性の高い部品(クラス)でシステムを組む考え方である

□ オブジェクト(抽象概念) = クラス(設計図) + インスタンス(具体的なもの)

□ 個々のインスタンス固有の変数を「インスタンス変数」(またはフィールド)と呼ぶ

□ C#で使用できる変数の型には値型と参照型があり、参照型変数にはインスタンスへの参照が入る

□ インスタンスの参照をメソッドの引数や戻り値に使うことで、多くの情報をまとめて受け渡しできる

□ コンストラクタはインスタンスを生成するときに自動的に呼ばれるメソッドで、クラス名と同名・戻り値なし

□ インスタンス変数を扱うのがインスタンスメソッド、クラス全体で共有したい情報や処理はstaticを使う

次回は、「10章. 継承を使ってクラスをグループ化する」を学びます。


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