1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

この記事は「オブジェクト指向入門その1 概要編」のフォローアップです。

オブジェクト指向入門 一覧

カプセル化のおさらい

カプセル化はオブジェクトの中身が外に漏れないようにすることでした。そのために考案されたのがアクセス指定子でしたね。

代表的なアクセス指定子はprivatepublicです。privateが指定されたメンバはクラス内でしか参照できません。対して、publicが指定されたメンバはコードのどこからでも参照できるようになります。

アクセス指定子とメンバアクセスの関係

アクセス指定子 クラス内 クラス外
public
private ×

これにより、インスタンスのフィールドの安全性が確保され、グローバル変数のような不確かさや、処理が追えなくなる危険性が無くなりました。

これを考えると、ベストプラクティスはフィールドは直接書き換わると問題があるので必ずprivateに、メソッドはセーフガードを入れられるので必要に応じてpublicprivateを切り替える、ということになります。

カプセル化の実現方法

ここで問題があります。privateのフィールドにどうやって値を入れるかです。外部入力無しに値が設定されるのであれば定数と変わりありません。そこで、privateフィールドに値を入れるための方法が2つほど考えられています。

コンストラクタ:ここにインスタンスを建てよう

インスタンス化宣言をする際に実行されるコードのことをコンストラクタと言います。引数を取らないものもありますが、大抵のコンストラクタは引数でprivateフィールドを初期化します。まさにResource Acquisition Is Initializationですね。簡単なコードで例を示してみます1

public class Saving
{
    private int Ryan;
    public Saving()
    {
        Ryan = 2;
        Console.WriteLine($"Ryanの値は{Ryan}です。");
    }
    public Saving(int ryan)
    {
        Ryan = ryan;
        Console.WriteLine($"Ryanの値は{Ryan}です。");
    }
    public static void Main()
    {
        Saving saving1 = new Saving(1); // 「Ryanの値は1です。」
        Saving saving2 = new Saving(); // 「Ryanの値は2です。」
        // saving1.Ryan = 5; とするとエラーになる。
    }
}

また、空のコンストラクタ2ではこのようにprivateフィールドを初期化してあげるのが基本です3。こうすることで外側からRyanが上書きされることは無くなります(そのようなコードを書いた場合、大抵はIDEで、最悪の場合はビルド時にエラーが出ます)。

アクセサ:On your mark, get set, go!

しかし、後から内部の値を確認できると助かる場合が多いですし、値を変更したいときもままあります。そこで編み出されたのがアクセサです。アクセサにはゲッターセッターがあります。前者は値を取得するメソッド、後者は値を設定するメソッドです。C#で書くとこんな感じです4

public class DaftPunk
{
    private int Lucky;
    public int GetLucky()
    {
        return Lucky;
    }
    public void SetLucky(int lucky)
    {
        Lucky = lucky;
    }
    public static void Main()
    {
        DaftPunk daftPunk = new DaftPunk();
        daftPunk.SetLucky(42);
        Console.WriteLine($"Luckyの値は{daftPunk.GetLucky()}です。"); // Luckyの値は42です。
    }
}

このコードの問題点としては、せっかくカプセル化したはずのLuckyが公開されてしまっている点がまず挙げられます。また、ボイラープレートな単純繰り返しのコードが多くなるのも面倒です。そのため、最近の風潮としては、セッターは用いられないことが多いです。セッターロボもSetLuckyも無いですからね。
ただ、セッター内で値の判定をできるという利点もあります。例えばセッターだけを抜き出すとこんな感じです。

public void SetLucky(int lucky)
{
    if (lucky < 0)
    {
        throw new ArgumentException("Luckyに負の値を入れることはできません。");
    }
    Lucky = lucky;
}

また、セッターを使わない場合に値を変えるには、変えたい値以外を丸々コピーしたインスタンスを生成する必要があります。そのためメモリ制限のある環境ではセッターを使わなければならない場合もあるはずです(GCのある言語を使う段階でそんな条件は満たさないはずですが)。もっとも、そんな場合は逆にフィールドをpublicにしてしまう方がいいと思うのですが…。

プロパティ:そう、C#ならね

アクセサの短所を挙げると以下の通りです。

  • フィールド、ゲッター、セッターの3つのコードを書かなければならない。
  • 手打ちで対応を維持するのは大変で、例えば諸々の変更でCityHunterというフィールドを取り出すメソッドがGetWild()になってしまう可能性がある。
  • 何個もフィールドがあるとタイピングの手間が倍々ゲームになる。
  • フィールドをメソッドとして扱っているのがもやもやする。
  • フィールドを使うたびに括弧が多くなって格好悪い。

このように普段使いされているのに、アクセサは難点だらけです。もっとアクセサをフィールドのように扱えないかと考えたC#の開発チームは、実にスマートな文法を生み出しました。それがプロパティです。内部的にはアクセサとして、外部的にはフィールドとしてふるまうメンバです。

public class Logan
{
    public int Lucky { get; set; }
    public static void Main()
    {
        Logan logan = new Logan();
        logan.Lucky = 42;
        Console.WriteLine($"Luckyの値は{logan.Lucky}です。"); // Luckyの値は42です。
    }
}

これだけではpublicのフィールドと見た目上変わりありませんが、private set;とすることでクラス外でプロパティの変更を許可しないようにできます(さらにget;のみにするとコンストラクタ上でのみ変更可能になります)。また、アクセサのようにフィールドを利用することも可能です(というより、こちらがプロパティの最初の実装)。これが必要になるのはプロパティ変更を他のオブジェクトに通知したい場合です。

public class MasterOfDisguise : INotifyPropertyChanged
{
    private string _Target;
    public event PropertyChangedEventHandler PropertyChanged;
    public string Target
    {
        get
        {
            return _Target;
        }
        set
        {
            if(value != _Target)
            {
                _Target = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Target)));
            }
        }
    }
}

将来的には(投入時期は未定ですが)privateフィールドを作らなくてもfieldというキーワードで代用できるようになる予定です。そうすればINotifyPropertyChangedの実装がはかどりますね!

なお、privateフィールドを省いた場合も、内部的にはフィールドが生成され、アクセス検証が働くという仕様になっています。そういうことを気にしなくてコーディングできるのは本当に気持ちがいいですね。

オブジェクト初期化子:値を直接代入したい!

諸々の事情(コンストラクタを作るのが面倒だったりEntity FrameworkのLINQ to Entityで型からSELECT文を生成する時など)でコンストラクタを使えないということがあります。分かりやすいのがコレクション(List<T>などのデータの集まり)です。これをいちいちコンストラクタで定義していたら柔軟性が足りなくなってしまいますし、List<T>.Add()メソッドで逐次値を追加していくのもどこまでが初期化か不明瞭ですし、そもそもデータ追加をしたくないコレクションもあります。そこでMSの偉い人は直接中身を初期化することを思いつきました。以下のコードを見てください。

public class Singer
{
    public string Song { get; init; }
    public static void Main()
    {
        Singer 本田美奈子 = new Singer { Song = "Amaging Grace" };
        // 本田美奈子.Song = "1986年のマリリン" とはできない!
        Console.WriteLine($"本田美奈子.さんが{本田美奈子.Song}を歌います。"); // 本田美奈子.さんがAmaging Graceを歌います。
    }
}

コンストラクタが無くても、このようにプロパティを初期化できます。init;は比較的最近(C# 9)に追加された「initオンリーセッター」というアクセサ的なもの(get;set;init;をまとめた正式名称は不明)で、これを指定するとコンストラクタもしくはオブジェクト初期化子でのみ値を設定可能にするものです。set;を省いたgetオンリープロパティがオブジェクト初期化子を受け付けない仕様(どう考えても設計ミス。おそらくオブジェクト初期化子の存在を忘れていた)なので、このような新しいキーワードが導入されました。init;set;に変えてもコードは動きます(古いバージョンではそうしなければならないです)が、危険です。ちなみに当該プロパティをpublicフィールドに変更しても動作します。

正直コンストラクタを使っていれば通常は十分ですが、オブジェクト初期化子が走るのはデフォルトコンストラクタが走ってからということは覚えておくといいでしょう。つまり、コンストラクタで値を入れる処理があると「暫定的な値で初期化」→「初期化子で指定した値が入力」という2度手間になります。なのでオブジェクト初期化子を使う必要がある場合はデフォルトコンストラクタは空のメソッドにすべきです。

private メソッドを使うべきかどうか

privateメソッドを使う動機としては、複数のメソッドやアクセサから読み出す共通処理を実装するというのがほとんどでしょう。ただ、その内容によっては別のオブジェクト(大抵は操作対象)に処理を移管したほうがいい時があります。場合によっては新しいクラスを作る必要もあるかもしれません。その時はぜひリファクタリングをしていきましょう。

ただ、privateメソッドを使うなというのは原理主義的です。できるなら避けたほうがいいとは思いますが、手間もその分増えます。なので、いざという時には敢えて使用する勇気も必要だと思います。

まとめ

  1. カプセル化とは、オブジェクトの状態をオブジェクト外にさらさないことである。
  2. それを実現するために、OO言語にはアクセス指定子というものがある。
  3. 大抵の場合、メソッドはpublicに、フィールドはprivateにする。
  4. フィールドに値を入れるにはコンストラクタの引数で初期化したり、セッターで代入する。
  5. しかし、今セッターを使うべき理由は少ないので、可能な限り避けたほうが良い。
  6. フィールドの値を利用することは多いので、ゲッターは割とよく使う。
  7. 値を投入するための機構として、C#はプロパティとオブジェクト初期化子を備えている。
  1. ただの例示用の、実用性はがアウトオブ眼中のコードなので、Ryanを活用できていません。実際にはRyanを使ったメソッドがあることでしょう。また、privateメンバの名前の頭に_を付ける流儀もありますが、今回はシンプルさのために普通に書いています。

  2. コンストラクタ内でコンストラクタを読み出すこともできますが、今回は説明のために冗長な書き方をしています。今回の引数無しコンストラクタを引数ありコンストラクタを読み出す形で書き直すとpublic Saving() : Saving(2) { }となります。

  3. C#ではprivate int Ryan = 2;と書くことでも初期化できますが、コンストラクタ内で明示的に初期化してあげるほうがコードを見る人(それは未来のあなたでもあります)にとっても優しいでしょう。

  4. C#では後述のプロパティがあるため、アクセサは全くと言っていいほど使われませんが、他の言語に移行する場合を踏まえて、基本形を覚えておくに越したことは無いでしょう。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?