45
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社ラグザイアAdvent Calendar 2023

Day 11

C#のプロパティの使いどころ

Last updated at Posted at 2023-12-10

はじめに

C#にはプロパティという便利な機能があります。
コードレビューをしていて、プロパティの必要性について言語化するのが難しかったので改めてプロパティについてまとめました。

プロパティを使う前提知識

オブジェクト指向であるC#はクラスのデータであるフィールドと振る舞いであるメソッドを持ちます。
フィールドとメソッドはアクセス修飾子をつけることで、公開範囲をそれぞれ設定できます。

  • クラスの外部に公開:public
  • クラスの外部に公開しない:private

フィールドはクラス内部のデータであり、基本的にprivateにします。メソッドは場合によりけりです。
ではクラスのデータを外部から使いたくなったときはどうすればいいでしょうか?Javaのように外部からのアクセス用のメソッドを作るのでしょうか。
C#ではプロパティと呼ばれるメソッドを使います。

こちらの記事がわかりやすいです。

プロパティ

いきなりですが、サンプルコードで示します。
以下、C#のバージョンは9.0で説明します。

Product.cs
public class Product
{
    // フィールド(非公開)
    private decimal _price;

    // プロパティ(公開)
    public decimal Price
    {
        get => _price;
        set
        {
            if (value < 0)
            {
                throw new ArgumentException("価格は0以上でなければなりません。");
            }
            _price = value;
        }
    }

    // コンストラクタ
    public Product(decimal price)
    {
        Price = price;
    }
}

このようにC#ではプロパティというメソッドによってフィールドに外部からアクセスするのが一般的です。
フィールドの読み込み,書き込みについて、それぞれ公開度を設定できます。
フィールドをpublicにすると、それぞれ公開度を設定することはできなくなってしまうのでプロパティを使います。

setterがシンプルな場合は自動実装プロパティをよく使います。これによりフィールドを記述する必要がなくなります。

車輪の再発明防止としてのプロパティ

クラスからデータを呼び出すときにフィールドには用意されていないが、何度も使いたいデータや判定があります。これをクラス内外どちらに用意するかは開発者に委ねられていますが、クラス内に書くことを推奨します。
例として、Productが1000円以上で材料に牛乳を含むか判定する処理について考えます。

Product.cs
public class Product
{
    public decimal Price { get; set; }
    public IEnumerable<string> Ingredients { get; set; } 

    public Product(decimal price, IEnumerable<string> ingredients)
    {
        Price = price;
        Ingredients = ingredients;
    }
}

Productを使うStoreクラスとServiceクラスで開発者が違う場合、同じ判定をしたいはずなのに判定文の書き方が変わることがよくあります。

Store.cs
public class Store
{
    public void StoreMethod(Product product)
    {
        if (product.Price >= 1000)
        {
            foreach (var ingredient in product.Ingredients)
            {
                if (ingredient == "牛乳")
                {
                    // 特別なビジネスロジック
                }
            }
        }
    }
}
Service.cs
public class Service
{
    public void ServiceMethod(Product product)
    {
        if (product.Price >= 1000 && product.Ingredients.Any(i => i == "牛乳"))
        {
            // 特別なビジネスロジック
        }
    }
}

後から見返すとStoreとServiceで同じ処理がしたかったのすらわからなくなります。
これを防ぐためにProductクラスで処理をプロパティにしておくとよいです。

Product.cs
public class Product
{
    public decimal Price { get; set; }
    public IEnumerable<string> Ingredients { get; set; }

    // 1000円以上で材料に牛乳を含むか判定かどうかを判定するプロパティ
    public bool IsPricedOver1000AndContainsMilk =>
        Price >= 1000 && Ingredients.Any(i => i == "牛乳");

    public Product(decimal price, IEnumerable<string> ingredients)
    {
        Price = price;
        Ingredients = ingredients;
    }
}
Store.cs
public class Store
{
    public void StoreMethod(Product product)
    {
        if (product.IsPricedOver1000AndContainsMilk)
        {
            // 特別なビジネスロジック
        }
    }
}
Service.cs
public class Service
{
    public void ServiceMethod(Product product)
    {
        if (product.IsPricedOver1000AndContainsMilk)
        {
            // 特別なビジネスロジック
        }
    }
}

Productクラスが判定用のプロパティIsPricedOver1000AndContainsMilkを持つことで車輪の再発明を防ぐことができました。テストコードを書くときもIsPricedOver1000AndContainsMilkだけ書けばよいので品質の担保がなしやすいです。
イテレータで繰り返し判定をする処理はLINQを書くとわかりやすいです。LINQは知らないと書かないため(当たり前ですが…)、一度勉強することを強く薦めます。

その他、所感など

C#はプロパティを使わなくてもメンバ変数やメソッドを使っても実装できてしまいます。C#を学びたてのころは私もやっていましたが…困るのは後で修正することになったエンジニアです。

以下はコードレビューでプロパティを使って下さい、と指摘された場合に浮かんできそうな疑問と回答です。

publicなメンバ変数でよくない?

確かにメンバ変数をpublicにすることで外部から呼び出しも編集もできます。しかし、C#ではpublicなメンバ変数は推奨されません。公開したいデータはプロパティとして用意します。

DLLなど配布している場合も外部からのアクセスはプロパティ経由が望ましいです。
publicなメンバ変数として公開すると、あとからバリデーションなどのロジックを組み入れるのは困難です。以前のDLLに依存したクライアントアプリがある場合にはバイナリ互換性が維持されなくなり、クラッシュなどの要因となります。

データを呼び出すメソッドを作ればよくない?

メソッドでもプロパティと同じ機能は持たせられますが(というかプロパティはメソッドの一種なのですが)、コードの記述量、可読性を考えると自作メソッドを作るうまみがありません。
ただし、以下の場合はプロパティではなくメソッドにしています。

  1. 連続して呼び出すと異なる値が返ってくる処理
  2. コストがかかる重めの処理
  3. 引数がないと返り値を判断できない処理

2がプロパティではなく、メソッドを使う理由は言語化できていません。
これは次回の課題です。

45
36
8

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
45
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?