概要
オブジェクトを作成するためには、オブジェクトを正しく初期化してやる必要があります。 そのために、オブジェクトの構築のためのコンストラクターと呼ばれる特殊なメソッドが用意されています。
ポイント
-
コンストラクターで初期化
- new したときに呼び出される特殊なメソッド。
- 型名と同じ名前で定義する。
- 例えば、class Person { public Person(string name) { ... } ... }
コンストラクター
コンストラクターはインスタンスを正しく初期化するための特別なメソッドです。 コンストラクターは以下のように、型名と同じ名前のメソッドを書くことで定義できます。
class SampleClass
{
// ↓これがコンストラクター
SampleClass()
{
// インスタンスの初期化用のコードを書く
}
}
他のメソッドと異なり、戻り値の型は書きません(コンストラクターは戻り値を返すことは出来ません)。
例えば、名簿作成のために個人情報を表す Person
というクラスを作ったとします。
説明を簡単にするために、この名簿では名前と年齢だけを管理することにします。
そのため、Person
は name
と age
という2つのメンバーのみを定義します。
class Person
{
public string name; // 名前
public int age; // 年齢
}
ここで、Person
クラスのインスタンスを生成する際、
名前を ""
(空の文字列)で、年齢を 0
で初期化したいとします。
そのためには以下のようなコンストラクターを作成します。
class Person
{
public string name; // 名前
public int age; // 年齢
// ↓これが Person クラスのコンストラクター
public Person()
{
name = "";
age = 0;
}
}
コンストラクターは new
を用いてインスタンスを作成する際に呼び出されます。
例えば、下記のようなコードを実行した場合、
using System;
class Test
{
public Test()
{
Console.Write("Test クラスのコンストラクターが呼ばれました\n");
}
}
class ConstructorSample
{
static void Main()
{
Console.Write("Main の先頭\n");
Test t = new Test(); // ここで Test のコンストラクターが呼ばれる
Console.Write("Main の末尾\n");
}
}
以下のような出力が得られます。
Main の先頭 Sample クラスのコンストラクターが呼ばれました Main の末尾
また、コンストラクターには引数を与えることもできます。
例えば、先ほどの Person
クラスで、
インスタンスの作成時に名前と年齢の値を設定したい場合、
以下のようなコンストラクターを作成します。
class Person
{
public string name; // 名前
public int age; // 年齢
// ↓引数つきの Person クラスのコンストラクター
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
}
この例で使われている
this
というキーワードは、
作成するインスタンス自身を格納する特別な変数です。
そのため、この例では this.name
は Person
クラス内で定義された name
のことになります。
一方、this
の付いていない方の name
は、コンストラクターの引数として定義した name
のことです。
引数つきのコンストラクターを呼び出すためには、new
を使ってインスタンスを生成する際に、以下のようにして引数を渡します。
型名 変数名 = new 型名(引数リスト);
(後述しますが、C# 9.0 からは new
の後ろの型名を省略できることがあります。)
例えば、先ほど定義したPerson
クラスのコンストラクターを呼び出すためには以下のようにします。
Person p = new Person("ビスケット・クルーガー", 57);
Console.Write(p.age); // 57 と表示される
また、コンストラクターはオーバーロードすることができます。
例えば、Person
クラスに、名前と年齢を引数として与えるコンストラクターと、何も引数を与えないコンストラクターの両方を定義することができます。
class Person
{
public string name; // 名前
public int age; // 年齢
// ↓引数なしの Person クラスのコンストラクター
public Person()
{
this.name = "";
this.age = 0;
}
// ↓引数つきの Person クラスのコンストラクター
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
}
サンプル
using System;
/// <summary>
/// 名簿用の個人情報記録用のクラス。
/// とりあえず、名前と年齢のみ。
/// </summary>
class Person
{
// public なフィールド
public string name; // 氏名
public int age; // 年齢
// 定数
const int UNKNOWN = -1;
const string DEFAULT_NAME = "デフォルトの名無しさん";
/// <summary>
/// 名前と年齢を初期化
/// 与えられた年齢が負のときは年齢不詳とみなす
/// </summary>
/// <param name="name">氏名</param>
/// <param name="age">年齢</param>
public Person(string name, int age)
{
this.name = name;
this.age = age > 0 ? age : UNKNOWN;
}
/// <summary>
/// 名前のみを初期化
/// 年齢は不詳とする
/// </summary>
/// <param name="name">氏名</param>
public Person(string name) : this(name, UNKNOWN)
{
}
/// <summary>
/// デフォルトコンストラクター
/// 氏名・年齢ともに不詳
/// </summary>
public Person() : this(null, UNKNOWN)
{
}
/// <summary>
/// 文字列化
/// 氏名が不詳のときには NONAME に設定された名前を返す
/// 年齢が不詳の時には名前のみを返す
/// 氏名・年齢が分かっているときには「名前(xx歳)」という形の文字列を返す
/// </summary>
public override string ToString()
{
if(name == null)
return DEFAULT_NAME;
if(age == UNKNOWN)
return name;
return name + "(" + age + "歳)";
}
}//class Person
//----------------------------------------------------
// メインプログラム
class ConstructorSample
{
static void Main()
{
Person p1 = new Person("ちゆ", 12);
Person p2 = new Person("澪");
Person p3 = new Person();
Console.Write("{0}\n{1}\n{2}\n", p1, p2, p3);
}
}
ちゆ(12歳) 澪 デフォルトの名無しさん
変数初期化子
フィールドに初期値を与えるだけなら、 コンストラクターを使わなくても、以下の様な書き方で初期化できます。
class Person
{
public string name = "";
public int age = 0;
}
こういう書き方を変数初期化子(variable initializer)と言います。変数初期化子は、フィールドと定数に対して付けることができます。
コンストラクター初期化子
場合によっては、あるコンストラクターから別のコンストラクターを呼びだしたいことがあります。 このような場合に、以下のような書き方で、別のコンストラクターを呼び出すことができます。
class Person
{
public string name;
public int age;
public Person()
: this("", 0) // ↓のPerson(string, int) が呼ばれる。
{
}
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
}
この書き方をコンストラクター初期化子(constructor initializer)と言います。
(別項で説明するbase
と区別してthis初期化子と言うこともあります。)
ちなみに、初期化子とコンストラクターの実行順序は、 変数初期化子 → コンストラクター初期化子 → コンストラクター本体の順になります。 また、変数初期化子は、メンバーの宣言順と同じ順序で呼び出されます。
using System;
class Member
{
public Member(int n)
{
Console.Write("変数初期化子 {0}\n", n);
}
}
class Test
{
Member x = new Member(1);
Member y = new Member(2);
public Test()
: this(0)
{
Console.Write("引数なしのコンストラクター\n");
}
public Test(int x)
{
Console.Write("引数 x = {0} 付きのコンストラクター\n", x);
}
}
class Program
{
static void Main()
{
new Test();
}
}
変数初期化子 1 変数初期化子 2 引数 x = 0 付きのコンストラクター 引数なしのコンストラクター
オブジェクト初期化子
Ver. 3.0
C# 3.0 から、以下のような記法でメンバーを初期化できるようになりました。
Point p = new Point{ X = 0, Y = 1 };
ちなみに、このコードの実行結果は以下のようなコードと等価です。
Point p = new Point();
p.X = 0;
p.Y = 1;
詳細は「初期化子」で説明します。
コンストラクターの逆操作
詳しくは後々説明していきますが、コンストラクターと逆の操作を行うものが2つあります。
1つは、デストラクター(destructor)です。 プログラムを書く上で、「確保したら必ず後片付けが必要なリソース」と言うものが存在します。 コンストラクターでリソースを確保したら、セットで後片付けを書く場所がデストラクターです。
using System.Buffers;
class Resource
{
private byte[] _rentalArray;
// コンストラクターで「借りてくる」
public Resource() => _rentalArray = ArrayPool<byte>.Shared.Rent(100);
// 借りたものは返さないといけない。そのために使うのがデストラクター
~Resource() => ArrayPool<byte>.Shared.Return(_rentalArray);
}
詳しくは「デストラクター」で説明します。
もう1つは、分解(deconstruct)です。 コンストラクターは複数の値を1つの複合型にまとめる操作でもあります。 この意味でのコンストラクターにあたるのが分解処理です。
class Point
{
public int X;
public int Y;
// 複数の値を組み合わせる
public Point(int x, int y) => (X, Y) = (x, y);
// 複数の値にばらす
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
static class Program
{
static void Main()
{
// 組み合わせる
var p = new Point(1, 2);
// ばらす
var (x, y) = p;
}
}
詳しくは「複合型の分解」で説明します。
ターゲットからの new 型推論
Ver. 9.0
C# 9.0 から、状況によっては new 型名()
の 型名
の部分を省略できるようになりました。
ターゲット型からの推論が効くことが条件で、
例えば、以下のような書き方をできます。
(この機能を target-typed new と呼んだりします)。
// new Person(17, new DateTime(1964, 9, 25)) と同じ意味
Person p = new(17, new(1964, 9, 25));
record Person(int Age, DateTime Birthday);
1つ目の new
は左辺の Person p
から、2つ目の new
はコンストラクター引数の DateTime Birthday
から型を推論できるので、自動的に Person
、DateTime
に型を決定します。
ローカル変数の場合には var
が使えるのでそれほど便利ではないんですが、フィールド初期化子やメソッドの引数などでは便利です。
using System.Collections.Generic;
class Sample
{
// フィールドに対しては var が使えない。
// 代わりに new 型推論を使うと便利なことがある(特に、型名が長い時)。
Dictionary<string, List<(int x, int y)>> _cache = new();
}
using System.Collections.Generic;
static void m(Dictionary<string, string> options) { }
m(new()
{
{ "define", "DEBUG" },
{ "o", "true" },
{ "w", "4" },
});
型名の省略をできるだけの機能で、
元々 new T(a, b, ...)
みたいに書けて、型 T
を推論できるのであれば、new(a, b, ...)
と書くことができます。
using System.Globalization;
// new UnicodeCategory() とは元々書けるので、new() と省略可能。
UnicodeCategory c1 = new();
// new UnicodeCategory(1) とは元々書けないので、new(1) もダメ。
UnicodeCategory c2 = new(1);
// new (int x, int y)(1, 2) とは書けないんだけど、
// new ValueTuple<int, int>(1, 2) とは書けて、new(1, 2) はこの意味になる。
(int x, int y) t = new(1, 2);
// 配列とか dynamic は元々 new int[]() とか new dynamic() と書けないので、new() もダメ
int[] a = new();
dynamic d = new();
ちなみに、null 許容型 に対する new()
は、元となる型(T?
に対する T
型) の方の意味になります。
using System;
void m(DateTime? d) => Console.WriteLine(d);
m(default); // これは null の意味になる。何も表示されない。
m(new()); // これは new DateTime() の意味になる。 0001/01/01/ 0:00:00
また、throw new()
は throw new Exception()
の意味になったりします。
演習問題
問題1
前節クラスの問題 1の Point
構造体および Triangle
クラスに、
以下のようなコンストラクターを追加せよ。
/// <summary>
/// 座標値 (x, y) を与えて初期化。
/// </summary>
/// <param name="x">x 座標値</param>
/// <param name="y">y 座標値</param>
public Point(double x, double y)
/// <summary>
/// 3つの頂点の座標を与えて初期化。
/// </summary>
/// <param name="a">頂点A</param>
/// <param name="b">頂点B</param>
/// <param name="c">頂点C</param>
public Triangle(Point a, Point b, Point c)