C#でクラスを作ろう(5)/コンストラクタ

3月 18, 2020

このシリーズについて

C言語やC++言語などを学んではいるけどクラスをあまり作ったことが無い、という方を対象にしています。
このシリーズでは、C#でクラスを作るための基本的な構文を解説しています。C++やJavaなどと共通している概念も多いですが、サンプルコードは基本的にC#で解説します。ところどころ、C++特有の概念を解説することもあります。

クラスには、インスタンスの生成時に呼び出されるコンストラクタという特殊なメソッドを定義することができます。

スポンサーリンク

コンストラクタ

まずは構文を見てみましょう。

class Item
{
    public string Name { get; set; }
    public int Price { get; set; }
    public double TaxRate { get; set; }
 
    //コンストラクタ
    public Item(string name, int price, double taxRate)
    {
        this.Name = name;
        this.Price = price;
        this.TaxRate = taxRate;
        Console.WriteLine("初期化完了");
    }
}
 
//使い方
Item item = new Item("りんご", 100, 0.08);

コンストラクタは、クラス名と同じ名前(上の例では"Item")のメソッドとして定義します。コンストラクタは引数を持つことができますが、返り値はありません。

コンストラクタは、newによってインスタンスが生成されるときに呼び出されます。"Constructor(構築子)"の名が示す通り、インスタンスを初期化するためのロジックを記述するためのものです。

上の例のItemクラスは、何か商品を表わすクラスです。このクラスのインスタンスは、少なくとも商品の名前・価格・消費税率が定まっていないと意味がありません。Itemクラスのインスタンスを生成するときは必ず最低限の初期化をしなさいという決まり事を、コンストラクタによって規定するわけです。

コンストラクタは一般的に、フィールドやプロパティの初期値を与えるために使用されます。ただし、それ以上の初期化ロジック(上の例では"初期化完了"と出力する)を書いてもかまいません。

デフォルトコンストラクタ

コンストラクタを定義しなかった場合は、内部で自動的に引数無しのコンストラクタが生成されます。

Item item = new Item();

newで生成するときはこのように空カッコ(引数無し)の形しか使えません。そして、特別な初期化ロジックは何も実行されません。この、内部で自動的に生成される何もしないコンストラクタのことを、デフォルトコンストラクタといいます。

もし独自のコンストラクタを1つでも定義した場合は、デフォルトコンストラクタは生成されません。つまり、以下のコードはエラーになります。

class Item
{
    //中略...
    //独自のコンストラクタ
    public Item(string name, int price, double taxRate)
    {
        this.Name = name;
        this.Price = price;
        this.TaxRate = taxRate;
        Console.WriteLine("初期化完了");
    }    
}
 
//使い方
Item item = new Item();  //※エラー!

既に独自のコンストラクタを定義しているので、空カッコによるデフォルトコンストラクタを呼び出すことはできません。

コンストラクタのオーバーロードとthis初期化子

引数の違う何種類かのコンストラクタを定義することもできます。これはメソッドのオーバーロードと同じ仕組みです。

class Item
{
    public string Name { get; set; }
    public int Price { get; set; }
    public double TaxRate { get; set; }
 
    //コンストラクタ1
    public Item(string name, int price)
    {
        this.Name = name;
        this.Price = price;
        this.TaxRate = 0.1;
        Console.WriteLine("初期化完了");
    }
 
    //コンストラクタ2
    public Item(string name, int price, double taxRate)
    {
        this.Name = name;
        this.Price = price;
        this.TaxRate = taxRate;
        Console.WriteLine("初期化完了");
    }
}
 
//使い方
Item item1 = new Item("はさみ", 500);        //コンストラクタ1を使用
Item item2 = new Item("りんご", 100, 0.08);  //コンストラクタ2を使用

この場合、消費税率を省略してtaxRateを0.1(10%)で初期化するコンストラクタ1と、消費税率も引数で指定するコンストラクタ2を定義しています。

多くの場合、引数が違うだけのコンストラクタの処理は、中身がほぼ共通しています。例えばコンストラクタ1は、

    //コンストラクタ1
    public Item(string name, int price)
    {
        コンストラクタ2(name, price, 0.1);  //このようには書けないが、やりたいことはコレ
    }

という処理と同等です。しかし、コンストラクタは普通のメソッドではないので、上記のような書き方はできません。このような場合には、this初期化子という構文を使います。

    //コンストラクタ1
    public Item(string name, int price)
        : this(name, price, 0.1);
    {
    }

この書き方によって、コンストラクタ1の最初でコンストラクタ2を呼び出すことができます。もしその後にコンストラクタ1特有の処理を追加したい場合は、カッコ{}の中にその処理を書くこともできます。

フィールドやプロパティの初期化

一般的には、コンストラクタに書く処理の内容は、フィールドやプロパティの初期化がほとんどです。それをわざわざコンストラクタに書くのは面倒なので、次のように書くこともできます。

class Item
{
    public string Name { get; set; } = "";  //プロパティを初期化
    public int Price { get; set; } = 0;
    public double TaxRate { get; set; } = 0.1;
    public int _someField = 0;  //フィールドでもOK
}

このように、フィールドやプロパティの宣言時に初期値を書いておけば、コンストラクタの実行よりも前に、この初期化が行われます。

C++ではこの書き方はできません。フィールドの初期化にはコンストラクタを使う必要があります。

オブジェクト初期化子

さらに、クラス側ではコンストラクタやプロパティの初期値が無い場合でも、生成する側である種の初期化を行うこともできます。

class Item
{
    public string Name { get; set; }
    public int Price { get; set; }
    public double TaxRate { get; set; }
    public int _someField;
}
 
//使い方
Item item = new Item() { Name = "りんご", Price = 100, TaxRate = 0.08, _someField = 0 };

newでコンストラクタ(上の例の場合はデフォルトコンストラクタ)を呼び出してから、フィールドやプロパティを初期化しています。この記法をオブジェクト初期化子といいます。

コンストラクタが引数無しで、かつオブジェクト初期化子を記述している場合に限り、"new Item()"のカッコ"()"を省略できます。

この書き方が優れているのは、フィールドやプロパティを初期化した後のインスタンスがitemに代入されるということです。つまり、これは式(Expression)です。

一方、普通に初期化処理を書き下した場合は、文(Statement)になります。

//普通に書き下した場合は文になる
Item item = new Item();
item.Name = "りんご";
item.Price = 100;
item.TaxRate = 0.08;
item._someField = 0;

何が違うのかというと、オブジェクト初期化子を使って式(Expression)にした場合は、次のような書き方もできるということです。

OtherFunction(new Item() { Name = "りんご", /*中略...*/ });
(new Item() { Name = "りんご", /*中略...*/ }).SomeMethod();

なかなかいびつな形ですが、newとオブジェクト初期化子を含む全体が1つの「式」なので、このように別のメソッドの引数にしたり、初期化済みのインスタンスのメソッドをすぐに呼び出したりすることができます。

※2番目の例は、インスタンスをどの変数にも代入していないので、これ以上何もできませんが(^^;)

オブジェクト初期化子も、C++では使用することができません。

2段階のオブジェクト生成

このようにコンストラクタは便利な機能ですが、返り値を返すことができないという弱点があります。例えば、コンストラクタ内部でエラーが発生した場合に、それを知らせるのはやや困難です。

class Item
{
    //中略...
    //コンストラクタ
    public Item(string name, int price, double taxRate)
    {
        this.Name = name;
        if (price < 0)
            throw new Exception("価格が負の値");
        this.Price = price;
        this.TaxRate = taxRate;
        Console.WriteLine("初期化完了");
    }    
}

例えば、priceに負の値を指定してコンストラクタを呼び出した場合に、初期化を失敗したことを利用者側に知らせたいと考えます。ただし、返り値として例えばfalseを返すようなことはできないので、上の例のように例外を投げるか、何かフラグを立てて終了するしかありません。

そこで、言語機能としてコンストラクタはほとんど使わず、別の初期化メソッドを使って2段階でオブジェクトを生成しようという思想もあります。

class Item
{
    //中略...
    //コンストラクタは定義しない
    //独自の初期化メソッド
    public bool Initialize(string name, int price, double taxRate)
    {
        this.Name = name;
        if (price < 0)
            return false;
        this.Price = price;
        this.TaxRate = taxRate;
        Console.WriteLine("初期化完了");
        return true;
    }    
}
 
//使い方
Item item = new Item();
if (item.Initialize("りんご", 100, 0.08") == false)
    Console.WriteLine("初期化エラー");

このように、1段階目のコンストラクタでは何もせず、2段階目のInitializeメソッドで実際の初期化を行います。Initializeメソッドの戻り値がtrueかfalseかによって、初期化が成功したか失敗したかを知ることができます。

これはあくまで取り決めにすぎません。2段階目のメソッド名をInitにする人もいればCreateにする人もいます。

クラスをライブラリとして提供する人の思想によっては、その人の作ったクラスは全て2段階でオブジェクトを生成するようになっている、ということもあります。そのあたりは提供者側の取り決めに従って、適切にクラスを使っていきましょう。

スポンサーリンク