クラス(class)を理解しよう!
  • C#

[C#] クラス(class)を理解しよう!

※ 当サイトは広告を含みます。

ここではC#のクラスについて学びます。クラスはC#において最も大切な機能だと思ってます。
それもあって最低限の内容を書いたつもりが、何かめっちゃ長くなりました。疲れたら数回に分けて読んでください。

クラス(class)

クラスとは特定の機能を1つにまとめたものです。正直、説明が難しいのでコードを使って話します。
最初に以下の記述方法とサンプルコードを見てください。これは車という機能をクラス化したコードです。

クラス

// クラス
public class Car
{
  // 変数 (別名: フィールド)
  private int gasoline = 2; // ガソリン

  // 関数 (別名: メソッド)
  public void Drive() // 前進する機能
  {
    if (gasoline == 0)
    {
      System.Console.WriteLine("もうガソリンがない");
      return;
    }

    gasoline--;

    System.Console.WriteLine("10km進んだ");
  }
}
管理人

昨今の車は色々できますが、ここでは前進しかできない車を想像しましょう。

りさ

ゴミじゃん。

管理人

例えだから、そういうこと言わない。

前進するためには燃料が必要です。これを変数のgasoline(ガソリン)で定義しました。
そして前進する機能はDrive()です。これは関数と思ってください。最後に、これらの機能を所有するのがCarクラスです。

このようにクラスはデータ(変数)と機能(関数)を所有します。では、このメリットは何でしょうか。
それは機能の分割です。C#では1つ1つの機能を細かくクラスに分けてコーディングします。

当然ですが、今まで学んだように1つのクラスに全部書いても機能としては成立します。
しかし、それでは1つの機能が肥大化してしまい、デバッグの難易度が上がり不具合を生む可能が増えます。
また、クラスは基本的にファイルを分割して記述するので、1ファイルに全部記述するということは、複数人の開発ができないに等しいです。

管理人

どのくらいの規模で機能を分割するかは設計者の思想によります。そして、これは経験値としか言えません。

りさ

学ぶためにもコツくらいあるのでは?

管理人

とにかく小さく物事を見ましょう。1つ1つを最小、最低限の機能で作ることを意識します。

ちなみにクラスが所有する変数や関数は総称としてメンバと呼ばれます。

POINTクラスが所有する変数や関数をメンバと呼ぶ。

インスタンス(instance)

よくクラスを設計図と表現します。これは的確な表現だと思っていて、実際に先程のCarクラスは定義しただけでは動きません。
では、試しに以下のサンプルコードを実行してみましょう。学習なのでファイルは分割しないで1ファイルに全て記述します。


namespace Sample
{
  // クラス
  public class Car
  {
    // 変数 (別名: フィールド)
    private int gasoline = 2; // ガソリン

    // 関数 (別名: メソッド)
    public void Drive() // 前進する機能
    {
      if (gasoline == 0)
      {
        System.Console.WriteLine("もうガソリンがない");
        return;
      }

      gasoline--;

      System.Console.WriteLine("10km進んだ");
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
    }
  }
}
りさ

何も起きません。

管理人

さっきも言ったけどクラスは設計図だからね。使うためには設計図から実体を作る必要があるよ。

このクラスを元に作られた実体をインスタンスと言います。もしくは似た単語でオブジェクトと呼ばれます。
正直、微妙に違うらしいのですが、ぶっちゃけ同じです。どうしても気になっちゃう人は自分で調べてください。

管理人

確かC#的な正解はインスタンスです。

すでに記憶にないかもですが、C#のクラスはカスタム型に所属します。つまり独自の型を創造したってことです。

では、その型をインスタンス化する方法ですが、最初にサンプルコードを見せます。とりあえず、以下を実行してください。


namespace Sample
{
  // クラス
  public class Car
  {
    // 変数 (別名: フィールド)
    private int gasoline = 2; // ガソリン

    // 関数 (別名: メソッド)
    public void Drive() // 前進する機能
    {
      if (gasoline == 0)
      {
        System.Console.WriteLine("もうガソリンがない");
        return;
      }

      gasoline--;

      System.Console.WriteLine("10km進んだ");
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      // インスタンス化
      Car car = new Car();

      // 機能を実行する
      car.Drive();
      car.Drive();
      car.Drive();
    }
  }
}

今回は表示されましたよね。それではコードの解説に進みましょう。

クラスは型です。つまり変数のように利用できます。違いがあるとすればnew()を記述した部分ですね。
これはC#で参照型をインスタンス化する時の記述方法でルールとなります。なので、こう書いてください。
本来なら文字列も参照型でnewが必要なんですが、文字列リテラル等の仕組みで簡略化されてます。

記述だけ抜き出すとこうなります。()の部分は少し先で補足します。

インスタンス生成

今回は文字列が表示されましたよね。しかし、これだけではクラスのメリットは伝えづらいです。
ここで追加のサンプルコードを見せるので、こちらも実行してみましょう。


namespace Sample
{
  internal class Program
  {
    public class Car
    {
      private int gasoline = 2;

      public void Drive()
      {
        if (gasoline == 0)
        {
          System.Console.WriteLine("もうガソリンがない");
          return;
        }

        gasoline--;

        System.Console.WriteLine("10km進んだ");
      }
    }

    static void Main(string[] args)
    {
      Car car1 = new Car(); // インスタンス: 1つ目
      Car car2 = new Car(); // インスタンス: 2つ目
      Car car3 = new Car(); // インスタンス: 3つ目

      car1.Drive();
      car1.Drive();
      car1.Drive();

      car2.Drive();
      car2.Drive();
      car2.Drive();

      car3.Drive();
      car3.Drive();
      car3.Drive();
    }
  }
}
管理人

どう思いますか?

りさ

インスタンスごとに機能が独立してます。

管理人

そうです。最初にクラスは設計図と言いましたよね。本当にその通りで、クラスを元にインスタンスを作ると、それらは独立した値を持ちます。

理由は単純でクラスは型だからです。要は型を元に作った変数なので、当然に変数ごとの独立した領域を持ちます。
この仕組みを利用することで、1つのコードで同じ機能を所有する実体をいくつでも生成できます。

POINTクラスは独自に作成した型である。故に変数のように利用できる。

.(ドット)演算子

クラスの機能にアクセスするためには.(ドット)演算子を利用します。
すでに先程の例で利用してますが、car.Drive();と記述した部分です。

管理人

.を入力するとIDEの機能で記述可能な選択肢が表示されます。これ神機能だと思いませんか?


アクセス修飾子

.演算子でクラスの機能にアクセスできます。そして、この機能には変数も含まれます。
つまり先程の例だと変数のgasolineにアクセスできます。では、実際にアクセスしてみましょう。


namespace Sample
{
  public class Car
  {
    private int gasoline = 2; // ガソリン

    public void Drive()
    {
      if (gasoline == 0)
      {
        System.Console.WriteLine("もうガソリンがない");
        return;
      }

      gasoline--;

      System.Console.WriteLine("10km進んだ");
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      Car car = new Car();

      car.gasoline = 10; // 変数にアクセス
    }
  }
}
りさ

ビルドが通りません...

管理人

それで合ってるよ。最初にErrorを体験して欲しかったんだ。

さて、ビルドが通りませんが、クラスの変数にはアクセスできないのでしょうか。
もちろん違います。このアクセスできない原因はアクセス修飾子が影響してます。

試しに以下の部分を変更してください。これでビルドが通ります。


private int gasoline = 2; // ガソリン


public int gasoline = 2; // ガソリン

にします。

違いは最初に付いてるprivatepublicです。これをアクセス修飾子と言います。
アクセス修飾子は他にも存在するのですが、まずはこの2つを覚えましょう。

このアクセス修飾子とは、対象の公開範囲を意味します。英語のままでpublicが最も広く、この場合はアクセスが制限されません。
対してprivateは、アクセス範囲が自身のクラス内に制限されます。つまり、クラスの外からは一切のアクセスができません。

りさ

全部publicにしたら駄目なの?

管理人

駄目です。C#(と言うかプログラミング全般)の思想として、外部公開が不要な情報は全てprivateを指定します。

理由は色々ありますが、1つはバグの発生率を下げるためです。考えてみてください。
クラスの内部でしか利用しない変数を外部から変更することに意味がありますか?

もちろん無いです。寧ろpublicを指定すると変更の可能性を与えてしまうんです。
僕は気を付けるから大丈夫って思った人、エンジニアとして人間を信じるのはNGです。

りさ

エンジニアって人間不信なの?

管理人

あいつら何もしてないのに壊れたとか言い出すからゴミ。やはり信じるべきはコンピュータ。

そもそも複数人で作る場合など、自分だけがコードを触るとは限りません。
そのときにアクセスが不要な変数や関数まで見えたら普通に邪魔だと思いませんか?

管理人

現実的な話をするとクラスの所有する変数をpublicにすることはありません。まともな人は絶対にprivateを設定します。

りさ

どうやってアクセスするの?

管理人

後に説明するプロパティって仕組みを使うよ。

フィールド(field)

クラスが所有する変数をフィールドと言います。C#で変数と言うと、概ね関数内のローカル変数を指します。
なので、フィールドと呼びましょう。ここからはフィールドと変数は分けて使っていきます。

このフィールドですが、クラス内に作ったフィールドはクラス内の全ての関数からアクセスできます。
これは動作を見たほうが早いでしょう。以下がサンプルコードになるので実行してください。


namespace Sample
{
  public class Test
  {
    private int v = 100; // フィールド

    public void Increment()
    {
      v++; // フィールドにアクセス

      System.Console.WriteLine($"v={v}");
    }

    public void Decrement()
    {
      v--; // フィールドにアクセス

      System.Console.WriteLine($"v={v}");
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      Test t = new Test();

      t.Increment();
      t.Increment();

      t.Decrement();
    }
  }
}
管理人

このように関数外で作ったフィールドにアクセスでき、その値も共有されてますね。

りさ

関数内で定義するとどうなりますか?

管理人

その場合は単なる変数、さっき伝えた関数内のローカル変数になるよ。

変数やフィールドを作成する場合はスコープを意識しましょう。クラスも関数も1つのスコープです。
参照位置より下の階層に存在するならアクセスできますが、上の階層はアクセスできません。

POINTクラスが所有する変数をフィールドと呼ぶ。

メソッド(Method)

フィールドと同様にクラスが所有する関数にも名前があり、これをメソッドと呼びます。
実はC#では全てのメソッドはクラスに属する必要があり、必然的にメソッドしか存在しません。

定義する方法は以前に説明した関数と同じです。さらにアクセス修飾子を付けることもでき、非公開メソッドを作ることもあります。

りさ

非公開って意味あるんですか?

管理人

例えばクラス内の同処理をまとめる場合に使うよ。これを外部公開する必要はないよね。

POINTクラスが所有する関数をメソッドと呼ぶ。

プロパティ(property)

フィールドへのアクセスを許可するための仕組みです。やってることは単なるメソッドなんですが、プロパティという名前が付いてます。
以下がサンプルコードです。使い方もメソッドと少し異なり、アクセス時は()を付けません。実行して動作を確認しましょう。


namespace Sample
{
  public class Car
  {
    private string owner = "None";

    // プロパティ
    public string Owner
    {
      // 取得用: フィールドを返す
      get { return owner; }

      // 代入用: valueには代入用の値が渡される
      set { owner = value; }
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      Car car = new Car();

      car.Owner = "Lisa"; // setにアクセス

      var tmp = car.Owner; // getにアクセス
      System.Console.WriteLine(tmp);
    }
  }
}

このプロパティですが、名前の通りでgetが取得用、setが代入用です。
以下のように別々のアクセス修飾子を付けることもできます。


public string Owner
{
  get { return owner; }
  private set { owner = value; }
}
りさ

これpublicの場合はフィールドをpublicにするのと何が違うんですか?

管理人

いい質問だね。確かにsetpublicなら値の変更ができるから意味なさそうだね。

プロパティを利用するメリットは値の検証です。代入時に値を判断してNGなら無視、または値を補正することができます。
例えば文字列型ならnullは結構めんどいので、代入時に無視したり空文字に変換するとかですかね。getも戻りの補正とかできます。


namespace Sample
{
  public class Car
  {
    private string owner = "None";

    // プロパティ
    public string Owner
    {
      // 戻り値を補正する
      get
      {
        return $"Owner={owner}";
      }

      // 代入可能を判定する
      set
      {
        if (String.IsNullOrEmpty(value))
          return;

        owner = value;
      }
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      Car car = new Car();

      car.Owner = "Lisa";
      car.Owner = "";

      var tmp = car.Owner;
      System.Console.WriteLine(tmp);
    }
  }
}

自動実装プロパティ

真面目にコーディングしてると、プロパティが多すぎて書くのめんどくせぇってなります。
そもそも1つ1つフィールドを用意して、それのgetとsetを常に書くこと自体が嫌になります。

そんな意見が100万件集まり、いつしか記述を省略できるようになりました。
と言うことで、まずはサンプルコードを見てみましょう。以下を実行してください。


namespace Sample
{
  public class Car
  {
    // プロパティ (省略形)
    public string Owner { get; set; } = "None";
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      Car car = new Car();

      car.Owner = "Lisa";

      var tmp = car.Owner;
      System.Console.WriteLine(tmp);
    }
  }
}

こんな感じに1行で書くことができます。ちなみに読み取り専用でgetのみ記述、後は片方だけprivateを記述することもできます。
正直、色々な記述ができるので、使いたいと思った人は自分で調べましょう。ただ、慣れるまでは普通の記述を使うことを推奨します。


public string Owner { get; private set; } = "None";

ちょっとした解説です。先程のプロパティですが、どこにもフィールドを書いてませんよね。
これは自動実装と呼ばれる通り、省略形に合わせて内部的に勝手にコードを生成してくれます。

先程の例だと内部的にはこんな感じのコードが自動生成されてます。ただし、自動生成されたフィールドを直接利用することはできません。
この場合はクラス自身も、setプロパティを経由して値を代入、getプロパティを経由して値を取得します。


private string _owner = "None";
public string Owner
{
  get { return _owner; }
  private set { _owner = value; }
}
管理人

大体こんな感じの自動生成だった気がする。間違ってたらめんご!

りさ

ちゃんと調べてください。

管理人

ぶっちゃけ間違ってても影響しないのでヨシ!

thisキーワード

このthisキーワードを使うと、クラス内で自分自身を指し示すことができます。1番使うのはフィールドやメソッドを呼び出すときに明示的な指定をするため。
あまり良くないのですが、ローカル変数はフィールドと同名の変数名が定義できます。このときの両者を区別するためにフィールド側にthisを使います。
また、自分自身を引数(パラメータ)としてメソッドに渡したい場合もthisを使います。以下がサンプルコードです。


namespace Sample
{
  public class Car
  {
    private int gasoline = 2;

    public void Drive()
    {
      var gasoline = 100; // ローカル変数

      System.Console.WriteLine($"フィールド:{this.gasoline}");
      System.Console.WriteLine($"ローカル変数:{gasoline}");
    }
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      Car car = new Car();
      car.Drive();
    }
  }
}
管理人

フィールドとローカル変数に同じ名前を付けるのはバグの元なので本当にオススメしません。

りさ

違う名前ならthis書く必要なくない?

管理人

派閥によります。ちなみに僕は付ける派です。

りさ

また宗教問題...

管理人

少なくとも自分の中で付ける付けないは統一しましょう。両方採用が1番駄目です。

コンストラクタ

コンストラクタとはインスタンス生成時に必ず呼ばれる特殊なメソッドです。
特殊なため記述方法が決まってます。まずは記述ルールとサンプルコードで動作を確認しましょう。

コンストラクタ

namespace Sample
{
  internal class Program
  {
    public class Car
    {
      private int gasoline;

      // コンストラクタ (引数なし)
      public Car()
      {
        gasoline = 10;
      }

      // コンストラクタ (引数あり)
      public Car(int gasoline)
      {
        this.gasoline = gasoline;
      }

      public void Drive()
      {
        if (gasoline == 0)
        {
          System.Console.WriteLine("もうガソリンがない");
          return;
        }

        gasoline--;

        System.Console.WriteLine($"10km進んだ。ガソリン残量={gasoline}");
      }
    }

    static void Main(string[] args)
    {
      Car car1 = new Car();    // インスタンス生成 (引数なし)
      Car car2 = new Car(100); // インスタンス生成 (引数あり)

      car1.Drive();
      car2.Drive();
    }
  }
}

インスタンスを生成をすると自動的にコンストラクタが呼び出されます。つまりnewした瞬間が呼び出しのタイミングです。

また、コンストラクタはメソッドと似てますが戻り値がありません。これは自動で呼び出せる都合で戻り値を取得できないからです。
変わりに引数を与えることは可能で、先程の例では2つのコンストラクタを作りました。このように引数の型や数が異なれば無限に作れます。

管理人

同じ名前のメソッドを複数定義できる仕組みをオーバーロード(overload)と言います。これについては別の機会に説明します。

では、先程のコンストラクタですが、どんなメリットがあるのでしょうか。これには必ず呼び出される事実が関係します。
例えばある機能を実行するために絶対に代入が必要なフィールドがあった場合、クラスの利用者には事前に呼び出してもらう必要があります。

さて、ここで必要な呼び出しを忘れたらどうなりますかね。何と悲しいことに全てが破綻します。
こういった事実をなくすためにコンストラクタが存在するのです。

りさ

初期値の代入ならフィールド作ったときに書けばよくない?

管理人

フィールドやプロパティで記述できるのはあくまで値の代入だけ。コンストラクタはメソッドと同種だから処理を書くことができるよ。

りさ

なるほど。if文とかfor文とかですか?

管理人

そうだね。初期化ってのは代入だけが全てじゃなく、必要なメソッドを呼んだりすることも含まれるよ。if文で設定値の分岐とかもありえるよね。

プロパティの初期化

インスタンス生成時のみプロパティ経由の初期化(代入)を記述することができます。
これは具体例を見るのが早いので、先にサンプルコードで動作を確認しましょう。


namespace Sample
{
  public class Car
  {
    public string Owner { get; set; } = "None";
    public double Speed { get; set; } = 180;
  }

  internal class Program
  {
    static void Main(string[] args)
    {
      Car car = new Car()
      {
        Owner = "Lisa",  // ここで代入できる
        Speed = 490.484, //   〃
      };

      System.Console.WriteLine(car.Owner);
      System.Console.WriteLine(car.Speed);
    }
  }
}
管理人

こうやって書けばコンストラクタを呼び出すときに一緒に初期化してくれます。

りさ

コンストラクタの中で実行するのと何が違うんですか?

管理人

これは単にプロパティが呼ばれてるだけだよ。コンストラクタ経由で実装すると引数を作るのが大変だからね。

設計上、setではアクセス範囲が広すぎる場合が多々あります。
この場合はinitキーワードを利用することで、初期化が可能な範囲を狭めることもできます。

デストラクタ

デストラクタとは、コンストラクタの反対でインスタンスを破棄した場合に必ず呼び出される特殊なメソッドです。
こちらも特殊なため記述方法が決まってます。同じく記述ルールとサンプルコードで動作を確認しましょう。

デストラクタ

namespace Sample
{
  internal class Program
  {
    public class Car
    {
      // コンストラクタ
      public Car()
      {
        System.Console.WriteLine($"コンストラクタ");
      }

      // デストラクタ
      ~Car()
      {
        System.Console.WriteLine($"デストラクタ");
      }
    }

    static void Main(string[] args)
    {
      Car car = new Car();
      car = null;
    }
  }
}
りさ

えっ、呼び出されてなくない?

管理人

いやぁ、気がついてしまったか。たぶん、先程の簡易コードでは呼び出されないと思います。

この動作には理由があって、少し深い話になります。まぁ、眠くても適当に聞いてください。

そもそもC#を問わず、プログラムは動的リソース(メモリ領域を追加で借りるようなイメージ)を確保したら利用後に開放する必要があります。
このメモリの開放を忘れると、メモリリークと呼ばれるメモリ大量消費のエラーを起こして大抵はプロセスが落ちます。

この仕組みですが、C言語やC++では全てをエンジニア自身で管理する必要があり、プログラムの質がエンジニアの技量に左右されました。
しかし、割りと開放を忘れることがあってメモリリークすることは珍しくなく、最悪なのはメモリリークは発見が非常に困難なバグになります。

と言う経緯から、C#を含む最近のプログラミング言語は完全に自動でメモリ管理してくれます。なんて素晴らしいのでしょうか。
そしてC#ではメモリ管理の仕組みをガベージコレクション(garbage collection)と呼びます。ちなみに略してGCと書きます。

この仕組みが導入されてるため、C#では通常の処理を記述する限りはデストラクタは不要です。基本的に記述する必要がありません。
さらにデストラクタは呼び出しの瞬間を指定できない欠点もあります。まぁ、これを欠点と呼ぶのは設計者様に失礼なので仕様ですね。

C#ではインスタンスが利用されなくなったら自動的に破棄されるルールになってるのですが、そのタイミングは完全に.NETに依存します。
ちなみに利用されてないとは、誰も参照してない状態を言います。例えば該当する変数にnullを代入するとか。
そして、この状態になり時が来ると.NETが勝手に開放します。このタイミングをコントロールするのは不可能です。

管理人

そんな理由でデストラクタは呼び出されませんでした。

りさ

なるほど。でも、記述できるってことは必要性があるんじゃないんですか?

管理人

高度な処理をする場合は必要かもしれない。もしくは業務によると思う。正直、僕の範囲だと使った記憶ない。

あとがき

かなり長くなってしまいましたが、C#は全てがクラスで構築されるため、クラスを理解することが最も大切です。
むしろクラスのメリットを利用しないならC#を使う意味がなく、別の言語を選択したほうがマシとも言えます。

◆ C#に関する学習コンテンツ

この記事は参考になりましたか?

👆このブログを支援する

関連記事

コメント

この記事へのコメントはありません。