C#でクラスを作ろう(7)/静的メンバ・静的クラス

3月 23, 2020

このシリーズについて

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

例えば、商品クラスにおける消費税率のような値は、個々の商品(=商品クラスのインスタンス)に対して別々に存在するのではなく、全ての商品で一律に8%とか10%と決まっています。

このように、個々のインスタンスの値というよりは、そのクラス自身が持つ値とみなせるようなものがあります。そういう場合は、静的メンバを使っていきます。

スポンサーリンク

静的(static)プロパティ

クラス自身が持つとみなせるプロパティは、次のようにキーワードstaticを使って定義します。

class Item
{
    //静的(static)プロパティ
    public static double TaxRate { get; set; }
    public static double ReducedTaxRate { get; set; }

    //通常のインスタンスプロパティ
    public string Name { get; set; }
    public int Price { get; set; }
}

このようにして定義された静的プロパティには、次のように「クラス名+"."」でアクセスできます。

Item.TaxRate = 0.1;
Item.ReducedTaxRate = 0.08;

定数(const)

クラス内で定義された定数は、静的プロパティと同じような振る舞いになります。

class Item
{
    public const double PERCENT = 100;
    //中略...
}
 
//使う側
double percent = Item.PERCENT;

constキーワードで定義されたフィールドは、値を変更することができません。また、クラス1つに対して唯一の値を持ちます。唯一の値を持つという性質は静的プロパティと同じものです。

静的(static)メソッド

同様に、クラス自身の動作とみなせるメソッドは、静的メソッドとすることができます。

class Item
{
    //静的(static)プロパティ
    public static double TaxRate { get; set; }
    public static double ReducedTaxRate { get; set; }
    //通常のインスタンスプロパティ
    public string Name { get; set; }
    public int Price { get; set; }
    //静的(static)メソッド
    public static void ShowClassInfo()
    {
        Console.WriteLine("このクラスはItemクラスです");
    }
    public static void ShowTaxRateInfo()
    {
        Console.WriteLine($"消費税率は{TaxRate * 100}%です");
        Console.WriteLine($"軽減税率は{ReducedTaxRate * 100}%です");
        //インスタンスプロパティにはアクセスできない
        Console.WriteLine($"商品名は{Name}です"); //※エラー!
    }
}
 
//使い方
Item.TaxRate = 0.1;
Item.ReducedTaxRate = 0.08;
Item.ShowClassInfo();
Item.ShowTaxRateInfo();

静的メソッドの中からは、静的メンバにのみアクセスできます。通常のインスタンスプロパティやインスタンスメンバにはアクセスできません。

C++では、"Item::TaxRate"や"Item::ShowClassInfo()"のように、スコープ解決演算子"::"を使ってアクセスします。

静的(static)クラス

前述のItemクラスの例では、いくつかの静的メンバをもちつつ、通常のインスタンスメンバも持っているので、newでインスタンス化できます。

一方、全てのメンバが静的(または定数)であるようなクラスも考えられます。このようなクラスは静的クラスにすることができます。

static class Tax  //クラス宣言の前にstaticを付けると静的クラスになる
{
    //定数
    public const double PERCENT = 100;
    //静的(static)プロパティ
    public static double Rate { get; set; }
    public static double ReducedRate { get; set; }
    //静的(static)メソッド
    public static void ShowClassInfo()
    {
        Console.WriteLine("このクラスはItemクラスです");
    }
    public static void ShowTaxRateInfo()
    {
        Console.WriteLine($"消費税率は{TaxRate * PERCENT}%です");
        Console.WriteLine($"軽減税率は{ReducedTaxRate * PERCENT}%です");
    }
}

静的クラスはインスタンス化することができません。次のコードはエラーになります。

Item item = new Item();  //※エラー!

クラスを静的クラスにするメリットは、そのクラスがインスタンス化できないということをはっきりとコンパイラに示すことができるという点です。静的クラス内のプロパティやメソッドには全てキーワードstaticを付けなければいけません。付け忘れるとコンパイルエラーになるので、コーディングのミスが減ります。

静的クラスはアプリケーション中でただ1つの(静的な)インスタンスを持つ、という風に解釈してもいいかもしれません。そのただ1つのインスタンスは、アプリケーションの中で常に共有されます。

C++には静的クラスという概念はありませんが、次のようにprivateなコンストラクタを定義することによって、そのクラスを外部からインスタンス化するができなくなり、静的クラスのように扱うことができるようになります。

class Tax
{
    //中略...
private:
    Tax() { }  //privateコンストラクタ
}

この方法はやや技巧的なので、privateコンストラクタの挙動を知っていないと、このコードの意図を読み取ることが出来ません。言語仕様の裏を突いて技巧的に機能を実現するというのは、可読性を低下させるのでおすすめできません。

中級以上の書籍にはこのようなテクニックが多数載っていると思いますが、たとえ自分がそのような技巧的なテクニックを理解できるレベルに達したとしても、多用するべきではありません。テクニカルにプログラムを書くというのは、重要な目的ではないからです。

グローバル変数

古いプログラミング言語ではグローバル変数(どこからでもアクセスできる変数)を多用することがありましたが、近年のプログラミング言語ではグローバル変数の使用は推奨されていません。どこからでもアクセスできるということは、いつ値が変更されるかわからないということです。これは、予期することが困難なバグを生み出す原因にもなります。

その一方で、アプリケーション全体からいつでもアクセスできる変数というのは便利なものです。アプリケーションがただ1つだけ持つような設定情報、例えばアプリケーション名、データ保存先ファイル名、そのアプリケーションを使っているユーザー情報などです。

それらの情報は、ある種のグローバルな静的クラスの中に持たせておくことで、グローバル変数として扱うことができます。一般的にグローバル変数は推奨されていないとはいえ、何もかも規則に縛られるのではなく、有効な場面では柔軟にグローバル変数を使っていきましょう

static class GLOBAL
{
    public const string APP_NAME = "オセロ対戦ゲーム";
    public static string DataFilePath { get; set; }
    public static UserInfo UserInfo { get; set; }
}
 
//使い方
GLOBAL.DataFilePath = @"C:\data.txt";
GLOBAL.UserInfo = new UserInfo();

クラス名は何でもいいですが、GLOBAL、Application、Appなど、アプリケーション中で唯一のものであるということを示す名称で統一しておくといいでしょう。

僕の場合、GLOBALだとC#のキーワードglobal(滅多に使わない)と似てるからアレだし、ApplicationやAppは他の既定のクラス(System.Windows.Forms.Application)などとバッティングしそうなので、"UserApp"をさらにキータイプ数を少なくした短縮形"UApp"を使ってます。

何でもかんでもグローバル変数にすると、多くの方が警告しているようにバグの原因になってしまいます。局所的にしか使わない変数やオブジェクトは、なるべく局所的なスコープで完結させるべきです。例えば、次のような例ではグローバル変数を使わないほうがいいでしょう。

一時変数をグローバルにしない

良くない例

static class GLOBAL
{
    public double temp;
}
 
GLOBAL.temp = radius * radius * radius;
GLOBAL.temp = GLOBAL.temp * Math.PI;
GLOBAL.temp = GLOBAL.temp * 4 / 3;
double volume = GLOBAL.temp;  //球の体積

複雑な計算をする場合、一時変数を使うことがあります。また、ループカウンタなども一時変数です。グローバル変数をこれらの一時変数用として使うことは好ましくありません。

変数たった1個分のメモリ領域さえ節約しないといけないような1980年代のプログラミングではこのような手法も使われたことがありますが、現代のプログラミングではこのような節約にはほとんど意味がありません。むしろ、余計な関心事を減らし、プログラミングにおけるバグを抑えて生産性を上げることのほうが、はるかに重要です。

引数で受け渡せるものはグローバルにしない

メソッドというスコープを超えて値を引き渡したいときは、引数と戻り値を使います。

良い例

void Main()
{
    Console.Write("ファイル名を入力 : ");
    string path = Console.ReadLine();
    string readValue = ReadFile(path);
    Console.WriteLine($"ファイルの中身は、${readValue}です");
}
 
string ReadFile(string path)
{
    using (var sr = new StreamReader(path))
    {
        string readValue = sr.ReadLine();
        return readValue;
    }
}

呼び出し側のMainメソッドと呼び出される側のReadFileメソッドで値を相互に引き渡すには、このように引数と戻り値を使うようにしましょう。引数の数が多くなりそうなら、引数を1つにまとめたクラスを定義するといいと思います。

この受け渡しが面倒だからと言って、グローバル変数に頼るのは良くありません。

良くない例

static class GLOBAL
{
    public static string arg;
    public static string ret;
}
 
void Main()
{
    Console.Write("ファイル名を入力 : ");
    GLOBAL.arg = Console.ReadLine();
    ReadFile();
    Console.WriteLine($"ファイルの中身は、${GLOBAL.ret}です");
}
 
void ReadFile()
{
    using (var sr = new StreamReader(GLOBAL.arg))
    {
        GLOBAL.ret = sr.ReadLine();
    }
}

すっきりするのでこの書き方をしたくなる誘惑に駆られるかもしれませんが、これはかなり高い確率でバグを生み出します。どの変数がどこで使われているのかを把握するのが難しくなり、プログラミング中の脳のリソースを無駄に使ってしまいます。その結果、ミスをしやすくなります。

静的メンバ・静的クラスを使って慣れよう

クラス自身が持つ値や処理は静的プロパティ・静的メソッドに。最初のうちはどれを静的にすればいいのか判断が難しいと思いますが、いろいろ使ってみて慣れていきましょう。

スポンサーリンク