C# アクセサ get/set の進化とその使い方

C#

初期 C# のアクセサは、 Java のそれをほんの少し改良したところから開始しました。というか、そもそもC#という言語は Java のライセンスをめぐる問題などを経てマイクロソフトが自社開発を決断した言語です。パラダイムとしては全くJava世代の言語なので似ているのはある意味当然なのですが。

バージョン1 最初の get/set

Javaのgetter/setter メソッドの冗長さを改良する形で実装されました。考え方は Java とほぼ変わりません。getter/setterは、クラス定義の度に必ず登場する、いわば大量に使われるボイラープレートです。それを少しでも簡略化しようというのは、それなりに自然な考え方でした。

C#
public class Person {
    private string name;

    public string Name {
        get { return name; }
        set { name = value; }
    }
}

C#3.0 バッキングフィールドが省略可能

バッキングフィールド(バージョン1の例でいえば、private string name; を省略可能になりました。自動実装プロパティと呼ばれます。

C#
public class Person
{
    // 自動実装プロパティ
    public string Name { get; set; }
    public int Age { get; set; }
}

この書き方は、一見するとカプセル化をほぼなくしてしまうように見えます。しかし、メンバーをカプセル化したいのであれば、強いてバージョン1の書き方をすればいいわけです。

さらに、以下のようにカプセル化を放棄してメンバーを public にしてしまうのとは、実質的な意味がかなり異なります。

C#
class Person
{  
  public string Name;
    // ...
}
// ...
person.Name = "TARO";
// ...
person.Name = "JIRO";

違いは、get/set を使うと仕様変更が容易ということです。後からバージョン1スタイルの実装に変えても、この例では、Name を参照している(XXX.Name = “TARO”;)の側はなんの変更もありません。例えば、必要があれば set にバリデーションを加えるなどといった仕様変更をしても、参照側は全く変更する必要がなく、これが大きなメリットとなります。

C# 6.0

読み取り専用のプロパティ

自動実装プロパティを読み取り専用として定義できるようになりました。setアクセサーを記述しないことで、そのプロパティはコンストラクタまたはオブジェクト初期化子でのみ設定可能となり、その後は変更できない、つまりイミュータブルとなります。

C#
public class Person
{
    public string Name { get; } // 読み取り専用プロパティ
    public int Age { get; }

    public Person(string name, int age)
    {
        Name = name; // コンストラクタで初期化
        Age = age;
    }
}

// 使用例
var person = new Person("Kid", 19);
// person.Name = "Bob"; // コンパイルエラー: 読み取り専用プロパティのため変更不可

プロパティ初期化子

自動実装プロパティで、宣言時に初期値を設定できるようになりました。

C#
public class Product
{
    public string ProductName { get; set; } = "Default Product"; // プロパティ初期化子
    public decimal Price { get; set; } = 0.00m;
}

// 使用例
var product = new Product();
product.ProductName = "...";

式形式プロパティ

読み取り専用プロパティに対して、ラムダ式のような簡潔な構文で get アクセサーを記述できるようになりました。

C#
public class Circle
{
    public double Radius { get; set; }

    // 式形式の読み取り専用プロパティ
    public double Area => Math.PI * Radius * Radius;
    public double Circumference => 2 * Math.PI * Radius;
}

C# 7.0

式形式プロパティの拡張

C# 6.0で導入された式形式のメンバーは、C# 7.0で set アクセサーにも適用できるようになりました。ワンライナーならちょっとしたロジックを組み込みこともできます。

C#
public class Person
{
    private string name;

    public string Name
    {
        get => name; // 式形式の get アクセサー
        set => name = value ?? throw new ArgumentNullException(nameof(value)); 
        // 式形式の set アクセサー(7.0での拡張)
    }

    private int age;
    public int Age
    {
        get => age;
        set
        {
            if (value < 0) throw new ArgumentOutOfRangeException(nameof(value));
            age = value;
        }
    }
}

C# 7.2

プロパティが値型や構造体を渡す時にはコピーが行われますが、構造体のサイズが大きい場合などに、これはパフォーマンスの大きなオーバーヘッドとなります。

C# 7,2では、get アクセサーが値を ref readonly で返すことができるようになりました。つまり、読み取り専用の参照値です。これで、オーバーヘッドを気にせず、しかも読み取り専用でプロパティを取得することができるようになりました。

C#
using System;
using System.Numerics;

SceneObject sean = new SceneObject();
# sean.PositionByRefReadonly = new Vector3(10.0f, 20.0f, 30.0f); コンパイルエラー

public class SceneObject
{
    private Vector3 _position = new Vector3(10.0f, 20.0f, 30.0f);

    // ref readonly getter - 参照渡しで読み取り専用
    // _position のコピーを作成せずに、その参照を返す。
    // 返された参照経由での _position の変更はできない。
    public ref readonly Vector3 PositionByRefReadonly
    {
        get { return ref _position; }
    }
}

C# 8.0

構造体の読み取り専用メンバー

構造体のメンバー(プロパティのgetterを含む)をreadonlyとマークできるようになりました。それまでは、構造体メンバー変数を readonly とマークすることは可能でした。そうすることでメンバーをイミュータブルにできたのですが、メソッドやプロパティの get を readonly マークすることはできませんでした。

readonly get プロパティが導入されたことで、getter 内で構造体のインスタンス(this)を変更しないことをコンパイラが保証できるようになりました。

C#
public struct Point
{
    public double X { get; set; }
    public double Y { get; set; }

    // 読み取り専用メンバー
    public readonly double Distance => Math.Sqrt(X * X + Y * Y); 
}

C# 9.0

レコード型の導入

record 型は、参照型でありながら、値のように扱える構造を提供します。既定でinit アクセサー(後述)を持ち、初期化時のみ設定可能ということで、イミュータブルな参照型となります。また、オブジェクト同士を値として比較することができます。

C#
public record Person(string Name, int Age);

var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);

Console.WriteLine(p1 == p2); // True(値による比較)

プロパティを明示的に定義することもできます。

C#
public record Person
{
    public string Name { get; init; }
    public int Age { get; init; }

    public Person(string Name, int Age)
    {
        this.Name = Name;
        this.Age = Age;
    }
}

// 使用例
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);

Console.WriteLine(p1 == p2); // True(値による比較)

with構文

上のPersonは、以下のように、with構文を使って一部を変更したインスタンスを作成することができます。

C#
var original = new Person("Taro", 30);
var modified = original with { Age = 31 };

Console.WriteLine(modified);    // Person { Name = Taro, Age = 31 }

init-only アクセサー

init アクセサーは、C# 9.0 で導入されたプロパティの新しいアクセサーで、「初期化時のみ」プロパティに値を設定できるという特性を持ちます。これによって、初期化後は実質的に不変(イミュータブル)となります。

C#
// オブジェクト初期化子を使用して Point を初期化
Point p1 = new Point { X = 10, Y = 20 };
// コンストラクタで初期化
Point p2 = new Point(30, 40);


public class Point
{
    public int X { get; init; } // init アクセサー
    public int Y { get; init; } // init アクセサー

    // コンストラクタで初期化
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
    public Point() { }
}

関連記事:C# init と required

コメント

タイトルとURLをコピーしました