なぜ、インスタンスの理解が重要なのか、その理由
この記事では、当社の新人エンジニア研修の参考に**C#**を解説します。
前回はstaticメソッドについて学びました。今回はいよいよインスタンスの活用について解説します。
これまで話を単純化するために「インスタンス」を避けてきましたが、C#でのオブジェクト指向の本質はクラスからインスタンスを生成し、各インスタンスがデータと振る舞いを持つところにあります。staticメソッドだけではなく、インスタンスを活用する方法をしっかり学びましょう。
1. オブジェクト指向とは何だったか?
オブジェクト、クラス、インスタンス
- オブジェクト … 「モノ」を指す抽象的概念。データと動作(メソッド)を一体に扱う
- クラス … オブジェクト(モノ)の「設計図」
- インスタンス … そのクラスを元に「new」して実体化された、具体的なオブジェクト
C#でもクラスからインスタンスを生成して使うのが基本スタイルです。
例:スマートフォン
- 属性(データ): メーカー、OS、バッテリー容量
- 動作(メソッド): 電話をかける、アプリを開く …
- スマートフォンというクラスを設計し、実際に工場で作られた具体的なスマホが「インスタンス」に当たります。
2. フィールド(インスタンス変数)を持ったクラスの定義
例:新人エンジニアクラス
話を簡単にするため、新人エンジニアクラスが「社員番号(id)」と「名前(name)」という2つの情報だけを持つとします。C#のクラスであれば、以下のように定義できます。

namespace Chap09
{
class NewEngineer1
{
// フィールド(または「インスタンス変数」)
public int Id;
public string Name;
}
}
public int Id;
は「社員番号」のインスタンス変数public string Name;
は「名前」のインスタンス変数
注意: 実際のC#開発では、フィールドを
public
にせず、private
+ プロパティ(property) で公開することが多いです。ここでは学習のために簡略化しています。
この時点ではメインメソッドがないので、実行はできません。「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);
}
}
}
NewEngineer1 se1;
- これは「
NewEngineer1
型の変数se1
を宣言」している。まだ何も入っていない。
- これは「
se1 = new NewEngineer1();
new
演算子で「NewEngineer1
クラスのインスタンス」をヒープ領域に生成し、その参照をse1
に代入。- コンストラクタ
NewEngineer1()
が暗黙的に呼ばれ、インスタンスを初期化。
Console.WriteLine(se1.Id);
- フィールド
Id
の値を出力。まだ値を設定していないので、C#では既定値0が出力されます。(int
の既定値が0)
- フィールド
Console.WriteLine(se1.Name);
- こちらは文字列型の既定値が
null
なので、何もない状態が出力されます。
- こちらは文字列型の既定値が
<実行結果>
0
(空行またはnull)
C#でのフィールドの既定値は以下のように決まっています。
bool
→false
- 数値型 (
int
,long
,double
etc.) →0
(各型に応じた0) char
→'\0'
(Unicode NUL)- 参照型 (
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
ここで使っている Id
と Name
は、各インスタンスが固有に持つデータなので「インスタンス変数」とも呼びます。
3. メソッドを持ったクラスの定義
例:show()
メソッドを追加
同じ新人エンジニアクラスにメソッドを持たせることもできます。次の NewEngineer2
は、Show()
メソッドを定義して、一度にId
とName
を表示します。
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です。
Show()
はいわゆるインスタンスメソッド。呼び出し方は変数名.メソッド名()
。Show()
内ではId
やName
を直接参照できる(同じインスタンス内なので)。
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
,struct
など- 変数そのものがデータの実体を持つ
- 参照型:
string
, 配列 (T[]
), ユーザー定義クラス (class
)、object
など- 変数には実体(オブジェクト)へのポインタ/アドレス(参照)が入る
NewEngineer2 se2 = new NewEngineer2();
の場合、se2
には インスタンスそのものではなく「ヒープ上のインスタンスを指す参照」が格納されます。
1つのインスタンスを複数の変数で参照
NewEngineer2 se1 = new NewEngineer2();
se1.Id = 1;
se1.Name = "yamazaki";
NewEngineer2 se2 = se1; // 同じ参照をコピー
se2.Show();
se2
も同じオブジェクトを指すため、se2.Show()
の結果は se1.Show()
と全く同じ出力になります。
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 とは
null
は参照型変数が「どのオブジェクトも指していない」特別な状態null
と""
(空文字) は別物null
は0
や"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
内のse
とyamazaki
が同じオブジェクトを指します。
メソッド内でオブジェクトを変更すると呼び出し元にも反映
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. コンストラクタでインスタンスの初期化
C#で、クラス名と同じ名前のメソッドが「コンストラクタ」です。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
this.Id = id;
のthis
は「このインスタンス自身」を指すキーワード。フィールドと引数の区別のためによく使われます。- コンストラクタを定義しなかった場合、C#では「デフォルトコンストラクタ」が自動生成されます(引数なしでインスタンス生成可能になる)。
インスタンス再利用のために変数に代入する
new Sample("imai");
// これだと生成したインスタンスが変数に入らず、そのまま使い捨て
Sample sample = new Sample("imai");
// sample変数に保持し、あとで sample.DoSomething() のように再利用できる
コンストラクタのオーバーロード
コンストラクタもメソッド同様にオーバーロードできます。引数の数や型が違うコンストラクタを複数定義可能です。
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 : 名無し
}
}
}
- C#では
: this(...)
構文を使ってコンストラクタの中で別のコンストラクタを呼び出せます。
11. インスタンス変数と static変数 の使い分け
- インスタンス変数 … 各インスタンスが固有に持つ情報(例:
Id
,Name
) - 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
Count
はstatic int
なので、インスタンスごとに別々の値を持たない。「全体でただ1つ」Id
やName
は各インスタンス固有の情報
staticメソッドとインスタンスメソッドの使い分け
- インスタンスメソッド … 個々のインスタンスが持つ情報(
this
)を操作したい場合。 - staticメソッド … インスタンスに依存しない処理(汎用的ユーティリティなど)。
Javaの Math.random()
と同様、C#の System.Math
クラスも静的メソッド (Math.Sin(...)
, Math.Cos(...)
等) を多く持ち、「インスタンスを生成するまでもない共通処理」を表現します。
また、Mathクラスのようにインスタンスを作ってほしくない場合は、C#なら private constructor
(Java同様)でブロックすると同様の効果が得られます。
public static class MyUtility
{
// 静的クラスとするとインスタンス生成不可
public static double MyStaticMethod()
{
...
}
}
C#では static class
と書くと、そのクラスはすべて静的メンバーだけを持つようになります(インスタンス化禁止)。
これで、インスタンスでデータと処理を再利用可能部品にする方法をC#向けに解説しました。
次回は「継承を使ってクラスをグループ化する」、オブジェクト指向特有の継承・ポリモーフィズムの世界へ入っていきます。お楽しみに。