目次

キーワード

概要

オブジェクト指向とは」で「オブジェクトは内部の実装がどうなっているのかを隠蔽し、可能な操作と属性のみを公開する」と書きました。 しかし、今までのサンプルではまず、クラスの定義の仕方などを覚えてもらうためにこのような実装の隠蔽については何も説明していませんでした。

ここでは、 クラスの内部実装を隠蔽するためにクラスのメンバー変数やメソッドにアクセシビリティを設定する方法を説明し、 なぜクラスの内部実装を隠蔽する必要があるのかを説明します。

ポイント
  • オブジェクト指向の中核概念その1: 実装の隠蔽(カプセル化)。

  • 外(クラス利用側)から見た振る舞いと中身(実装側)はわけて考える。

  • 中身は隠す(利用者に見せない)。

  • 目的:

    • 不正な書き換えを防止する。

    • 実装を変更したときに、利用者側まで変更する必要をなくす。

アクセシビリティ

クラスのメンバー変数やメソッドにはアクセシビリティ(Accessibility: アクセスできる度合い)というものがあります。 アクセシビリティとは、変数やメソッドに対して、どこからアクセスできるかという制限の度合いのことで、 以下のようなものがあります。

アクセシビリティ 説明
public どこからでもアクセス可能
protected クラス内部と、派生クラスの内部からのみアクセス可能
internal 同一プロジェクト内のクラスからのみアクセス可能
protected internal 同一プロジェクト内のクラス内部、または派生クラスの内部からのみアクセス可能
private クラス内部からのみアクセス可能

以下のように変数の前にキーワードを付けることでアクセシビリティを制御することが出来ます。

アクセシビリティ 変数宣言やメソッド定義

派生クラスについては後ほど説明します。

アクセス権限のない場所からクラスのメンバーにアクセスしようとするとエラーになります。 例えば、アクセシビリティをprivateにした変数に、クラスの外部からアクセスしようとするとエラーになります。 とりあえず、今のところはクラスの外部に公開したいものはpublicに、そうでないものはprivateにするとだけ覚えておいてください。

ちなみに、別項(トップ レベルのアクセシビリティ)で説明しますが、(トップ レベルにある)クラス自身に対するアクセシビリティは public もしくは internal のみになります。

サンプル
class A
{
  public    int pub; // どこからでもアクセス可能
  protected int pro; // クラス内部と派生クラス内部からアクセス可能
  private   int pri; // クラス内部からのみアクセス可能

  public void function1()
  {
    // クラス内部
    pub = 1; // OK
    pro = 2; // OK
    pri = 3; // OK
  }
}

class B : A
{
  public void function2()
  {
    // 派生クラス内部
    pub = 1; // OK
    pro = 2; // OK
    pri = 3; // エラー
  }
}

class AccessibilitySample
{
  static void Main()
  {
    A a = new A();
    // クラス A の外部
    a.pub = 1; // OK
    a.pro = 2; // エラー
    a.pri = 3; // エラー
  }
}

このソースをコンパイルしようとすると、以下のようなエラーが出ます。

test.cs(23,3): error CS0122: 'A.pri' is inaccessible due to its protection level
test.cs(34,3): error CS0122: 'A.pro' is inaccessible due to its protection level
test.cs(35,3): error CS0122: 'A.pri' is inaccessible due to its protection level

実装の隠蔽

通常、内部の実装がどうなっているのかを隠蔽(要するに private にする)し、可能な操作のみを公開(public)することが望ましいとされています。 簡単に言うと、メンバー変数はクラス外部から直接アクセス出来ないようにして、オブジェクトの状態の変更はすべてメソッドを通して行うべきだということです

例として、「クラス」で作った複素数クラスについて考えてみましょう。 以前は実装の隠蔽は行っていませんでしたが、 ちゃんと実装を隠蔽するように作り直して見ましょう。

class Complex
{
  // 実装は外部から隠蔽(privateにしておく)
  private double re; // 実部を記憶しておく
  private double im; // 虚部を記憶しておく

  // 実部を取り出す
  public double Re(){return this.re;}

  // 実部を書き換え
  public void Re(double x){this.re = x;}

  // 虚部を取り出す
  public double Im(){return this.im;}

  // 虚部を書き換え
  public void Im(double y){this.im = y;}

  // 絶対値を取り出す
  public double Abs()
  {
    return Math.Sqrt(re*re + im*im);// Math.Sqrt は平方根を求める関数
  }
}

見ての通り、以前のものと比べてかなり回りくどくて面倒くさいものになっています。 なぜこのようにわざわざ回りくどい書き方をしなければいけないのか疑問に感じるかと思いますが、 クラスの内部実装を隠蔽する意義は、大きく分けて以下の2つがあります。

  • オブジェクトの不正な書き換えを防止する。

  • クラスの実装を変更した際、利用側のコードを修正する必要をなくす

オブジェクトの不正な書き換え防止する

コンストラクタ」で、 Person というクラスを作りました。 ここで、年齢が負の数になるのはおかしいので、 コンストラクタで年齢が負の数にならないようにチェックを行うように改良してみましょう。

class Person
{
  public string name; // 名前
  public int age;     // 年齢

  public Person()
  {
    this.name = "";
    this.age  = 0;
  }

  public Person(string name, int age)
  {
    this.name = name;
    this.age  = age > 0 ? age : 0; // age が負だった場合、0歳にしておく
  }
}

しかし、現時点ではクラスの外部からPersonクラスのメンバーageを直接書き換えれてしまうため、 年齢が負の数にならないように強制することは無理です。 例えば、以下のサンプルのようにすると無理やり年齢を負の数に設定することができます。

Person p = new Person("範馬刃牙", -5); // 年齢に負の値を設定しようとしても
Console.Write("{0}は{1}歳です。\n",  // 0歳に修正されている
              p.name, p.age);        // (「範馬刃牙は0歳です」と表示される)

p.age = -5;                          // でも、ageを直接書き換えてしまえば
Console.Write("{0}は{1}歳です。\n",  // 負の年齢になってしまう
              p.name, p.age);        // (「範馬刃牙は-5歳です」と表示される)

この問題を解決するためには、メンバー変数ageは外部からは直接アクセスできないようにして、メソッドを通してageの値を設定、取得する必要があります。

class Person
{
  public string name; // 名前
  private int age;    // 年齢

  public Person()
  {
    this.name = "";
    this.age  = 0;
  }

  public Person(string name, int age)
  {
    this.name = name;
    SetAge(age);
  }

  public int GetAge()
  {
    return this.age;
  }

  public void SetAge(int age)
  {
    this.age  = age > 0 ? age : 0; // age が負だった場合、0歳にしておく
  }
}
クラスの実装を変更した際、利用側のコードを修正する必要をなくす

クラスの実装を隠蔽しない場合、どのような不具合が生じるかを説明するため、 まず、以下のコードについて考えてみましょう。

using System;

// クラス定義
class Complex
{
  public double re; // 実部を記憶しておく(外部からの読み出し・書き換えも可能)
  public double im; // 虚部を記憶しておく(外部からの読み出し・書き換えも可能)

  // 絶対値を取り出す
  public double Abs()
  {
    return Math.Sqrt(re*re + im*im);// Math.Sqrt は平方根を求める関数
  }
}

// クラス利用側
class ConcealSample
{
  static void Main()
  {
    Complex c = new Complex();
    c.re = 4; // メンバー変数に直接アクセス
    c.im = 3; // メンバー変数に直接アクセス
    Console.Write("|c| = {0}\n", c.Abs());
  }
}

クラス」で説明しましたが、複素数クラスの実装方法には、 上述のコードのような「実部と虚部をメンバー変数に記憶しておく」方法のほかに、 「絶対値と偏角をメンバー変数に記憶しておく」方法があります。 そして、加減算を行う回数よりも乗除算を行う回数のほうがはるかに多い場合、 後者のほうが計算量が少なくなります。

例えば、この複素数クラスを利用するプログラムがあったとして、 そのプログラムでは加減算よりも乗除算の回数のほうがはるかに多いため、 後者の方式に変更したくなったとします。 この場合、以下のようにクラスの側だけでなく、クラスの利用側のコードも修正する必要があります。

using System;

// クラス定義
class Complex
{
  public double abs; // 絶対値を記憶しておく(外部からの読み出し・書き換えも可能)
  public double arg; // 偏角を記憶しておく(外部からの読み出し・書き換えも可能)

  // 実部・虚部を書き換え
  public void Set(double x, double y)
  {
    this.abs = Math.Sqrt(x*x + y*y);
    this.arg = Math.Atan2(y, x);
  }
}

// クラス利用側
class ConcealSample
{
  static void Main()
  {
    Complex c = new Complex();
    c.Set(4, 3); // クラス利用側のコードも修正が必要
    Console.Write("|c| = {0}\n", c.abs);
  }
}

このように、 クラスの実装方法を変更するたびに、利用側のコードまで修正する必要があると、 プログラムを作るのも保守するのも大変になります。

このような問題は、以下のように実装を隠蔽することで避けることができます。

using System;

// クラス定義
class Complex
{
  // 実装は外部から隠蔽(privateにしておく)
  private double re; // 実部を記憶しておく
  private double im; // 虚部を記憶しておく

  public double Re(){return this.re;}    // 実部を取り出す
  public void Re(double x){this.re = x;} // 実部を書き換え

  public double Im(){return this.im;}    // 虚部を取り出す
  public void Im(double y){this.im = y;} // 虚部を書き換え

  public double Abs(){return Math.Sqrt(re*re + im*im);}  // 絶対値を取り出す
}

// クラス利用側
class ConcealSample
{
  static void Main()
  {
    Complex c = new Complex();
    c.Re(4); // メソッドを通してオブジェクトの状態を変更
    c.Im(3);
    Console.Write("|c| = {0}\n", c.Abs());
  }
}

このコードの実装方法を 「実部と虚部をメンバー変数に記憶しておく」方法から 「絶対値と偏角をメンバー変数に記憶しておく」方法に変更する場合、 以下のように、クラス利用側のコードに手を加える必要は一切ありません。

using System;

// クラス定義
class Complex
{
  // 実装は外部から隠蔽(privateにしておく)
  private double abs; // 絶対値を記憶しておく
  private double arg; // 偏角を記憶しておく

  // 実部を取り出す
  public double Re()
  {
    return this.abs * Math.Cos(this.arg);
  }

  // 実部を書き換え
  public void Re(double x)
  {
    double im = this.abs * Math.Sin(this.arg);
    this.abs = Math.Sqrt(x*x + im*im);
    this.arg = Math.Atan2(im, x);
  }

  // 虚部を取り出す
  public double Im(){return this.abs * Math.Sin(this.arg);}

  // 虚部を書き換え
  public void Im(double y)
  {
    double re = this.abs * Math.Cos(this.arg);
    this.abs = Math.Sqrt(y*y + re*re);
    this.arg = Math.Atan2(y, re);
  }

  public double Abs(){return this.abs;}  // 絶対値を取り出す
}

// クラス利用側
class ConcealSample
{
  static void Main()
  {
    Complex c = new Complex();
    c.Re(4); // クラス利用側は一切変更せず
    c.Im(3);
    Console.Write("|c| = {0}\n", c.Abs());
  }
}

更新履歴

ブログ