連載
» 2017年12月20日 05時00分 公開

.NET TIPS:構文:定数(変更できないもの)を作るには?[C#/VB]

.NETでは「変更できない値」をconstキーワード/readonly修飾子/読み取り専用プロパティなどを使って宣言できる。それらの使いどころや違いをまとめよう。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 定数/readonly修飾子/不変な読み取り専用プロパティは、いずれも変更できない。どれも定数のようなものである。本稿ではその違いを整理して解説する。また、不変なクラスと構造体の作り方も説明する。

POINT 定数/readonly修飾子/不変な読み取り専用プロパティの使い分け

定数/readonly修飾子/不変な読み取り専用プロパティの使い分けまとめ 定数/readonly修飾子/不変な読み取り専用プロパティの使い分けまとめ
不変な読み取り専用プロパティは新しいバージョンによる簡潔な書き方を載せているが、もちろん従来バージョンでも実装できる(後述)。


 特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。

 なお、一部を除き.NET Frameworkの初期バージョンから利用できるが、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017 Update 5(バージョン15.5)以降が必要である。また、サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。

using System;
using static System.Console;

Imports System.Console

本稿のサンプルコードに必要な宣言(上:C#、下:VB)

定数とその使いどころ

 定数は、メソッド内およびクラスや構造体などのメンバに記述できる(次のコード)。

class Program
{
  // メンバに定義した定数
  const int N1 = 1; // 数値
  const string S1 = "定数1"; // 文字列
  const ConsoleColor C1 = ConsoleColor.Blue; // 列挙体(enum)
  const object O1 = null; // 参照型はnullのみ可能
  //const DateTimeOffset D1 = default; // 構造体は不可(コンパイルエラー)

  static void Main(string[] args)
  {
    // メソッド内に定義した定数
    const int N2 = 2; // 数値
    const string S2 = "定数2"; // 文字列
    const ConsoleColor C2 = ConsoleColor.Green; // 列挙体(enum)
    const object O2 = null; // 参照型はnullのみ可能
    //const DateTimeOffset D2 = default; // 構造体は不可(コンパイルエラー)
  }
}

Module Module1
  ' メンバに定義した定数
  Const N1 As Integer = 1 '数値
  Const S1 As String = "定数1" ' 文字列
  Const C1 As ConsoleColor = ConsoleColor.Blue ' 列挙体(Enum)
  Const O1 As Object = Nothing ' 参照型はNothingのみ可能
  'Const D1 As DateTimeOffset = #2017/12/24# ' 構造体は不可(コンパイルエラー)

  Sub Main()
    ' メソッド内に定義した定数
    Const N2 As Integer = 2 ' 数値
    Const S2 As String = "定数2" ' 文字列
    Const C2 As ConsoleColor = ConsoleColor.Green ' 列挙体(Enum)
    Const O2 As Object = Nothing ' 参照型はNothingのみ可能
    'Const D2 As DateTimeOffset = #2017/12/24# ' 構造体は不可(コンパイルエラー)
  End Sub
End Module

定数の例(上:C#、下:VB)

 定数はコンパイル時に解決される。コードに記述した定数の部分は、コンパイル後は実際の値(例えば上のコードのN2であれば整数の2)に置き換えられるのである。

 そのためパフォーマンスはよいが、定数を変更したときにはその定数を使っているコードを全てコンパイルし直さねばならない。クラスライブラリなどの複数のアプリから使われるコードで定数をpublicにすると、もはやその定数を変更するのは不可能といえるだろう。publicにしたい場合は、後述の読み取り専用プロパティにすべきである。また、構造体とnull以外の参照型は定数にできないので、後述するreadonly修飾子か読み取り専用プロパティを使う。

 定数の使いどころは次のようにまとめられる。

  • メソッド内
  • privateメンバ
  • internal/Friendメンバ(パフォーマンスが特に重要な場合)
  • 参照型と構造体はreadonly修飾子か読み取り専用プロパティを使う
  • publicにしたいものは読み取り専用プロパティにする

 次にreadonly修飾子の使いどころを見てみる。

readonly修飾子とその使いどころ

 メンバ変数の宣言にreadonly修飾子を付けると、読み取り専用になる(次のコード)。その値やオブジェクトは、実行時に初期化される。初期化は、変数の宣言箇所かコンストラクタで行う。

class Program
{
  private readonly int NR1 = 10;
  // 省略するが、文字列/列挙体も可能
  private readonly UriBuilder OR1 = new UriBuilder(); // 参照型もOK
  private readonly DateTimeOffset DR2 = DateTimeOffset.Now; // 構造体もOK
}

Module Module1
  Private ReadOnly NR1 As Integer = 10
  ' 省略するが、文字列/列挙体も可能
  Private ReadOnly OR1 As UriBuilder = New UriBuilder() ' 参照型もOK
  Private ReadOnly DR2 As DateTimeOffset = DateTimeOffset.Now ' 構造体もOK
End Module

readonly修飾子を指定する例(上:C#、下:VB)
ここでは宣言箇所で初期化も行っている(コンストラクタで行う例は後述する読み取り専用プロパティのサンプルコードに掲載)。

 readonly修飾子は、(定数と異なり)参照型と構造体でも使える。逆に、定数のようにメソッド内に書くことはできない。

 readonly修飾子を付けたメンバ変数は、初期化時以外に代入できないことを除けば、通常のメンバ変数と同じだ。従って、インスタンスメンバと静的メンバの違いがある(次のコード)。

class Program
{
  // readonly修飾子を付けた静的メンバ変数
  static private readonly DateTimeOffset DR1 = DateTimeOffset.Now;
  // readonly修飾子を付けたインスタンスメンバ変数
  private readonly DateTimeOffset DR2 = DateTimeOffset.Now;

  // async MainはC# 7.1以降
  static async System.Threading.Tasks.Task Main(string[] args)
  {
    // 静的メンバ
    WriteLine($"DR1={Program.DR1:ss.fff}");
    // 出力例:DR1=33.874

    // インスタンスメンバ
    var instance1 = new Program();
    WriteLine($"DR2={instance1.DR2:ss.fff}");
    // 出力例:DR2=34.096

    // 約1秒間、待機する
    await System.Threading.Tasks.Task.Delay(1000);

    // 静的メンバ(1秒前と同じ結果)
    WriteLine($"DR1={Program.DR1:ss.fff}");
    // 出力例:DR1=33.874

    // 新しく生成したインスタンスメンバ(この時刻で初期化される)
    var instance2 = new Program();
    WriteLine($"DR2={instance2.DR2:ss.fff}");
    // 出力例:DR2=35.154

#if DEBUG
    ReadKey();
#endif
  }
}

readonly修飾子に対するstaticの効果(C#)
少々長いコードなのでC#のみとさせていただいた。
静的メンバは、アプリ内で最初に使われるときに初期化される。アプリ内で唯一のものとなる。
インスタンスメンバは、インスタンスが作られるたびに初期化される。インスタンスごとに別のものとなる。そのため、この例のように初期化時の時刻を保持するようにした場合、インスタンスを作るごとに異なる値になる。
なお、このコードはC# 7.1の機能を使っているので、コンパイルするにはVisual Studio 2017 Update 3(15.3)以降を使い、ビルドに使用する言語バージョンにC# 7.1以降を指定する必要がある(詳細は「Dev Basics/Keyword:C# 7.1」参照)。

 なお、参照型のメンバ変数にreadonly修飾子を付けた場合、そのメンバ変数に代入することはできないが、そのメンバ変数が参照するオブジェクトのプロパティやメンバ変数を変更することはできてしまうので注意が必要だ(次のコード)。この問題に対処するには、メンバ変数を後述する不変なクラスにする。また、配列やコレクションにreadonly修飾子を付けた場合もその要素は変更可能であり、対処するにはReadOnlyCollection<T>クラス(System.Collections.ObjectModel名前空間)などを利用する。

class Program
{
  private readonly UriBuilder OR1 = new UriBuilder();

  static void Main(string[] args)
  {
    var instance1 = new Program();
    //instance1.OR1 = new UriBuilder(); // コンパイルエラー
    instance1.OR1.Scheme = "https"; // プロパティやメンバ変数は書き換え可能

#if DEBUG
    ReadKey();
#endif
  }
}

readonlyを付けてもオブジェクトの内部は変更できる(C#)
readonly修飾子を付けたメンバ変数OR1には、代入できない。ただし、OR1が参照するオブジェクトのプロパティやメンバ変数は書き換え可能だ。

 readonly修飾子を付けたメンバ変数は、実行時に初期化される。そのため、その初期値を変更しても、(定数とは異なり)利用している側のコードをコンパイルし直す必要はない。ただし、メンバ変数を外部に公開するのは推奨されないので、そういう場合には読み取り専用プロパティを使う方がよい。

 readonly修飾子の使いどころは次のようにまとめられる。

  • 参照型か構造体のprivateメンバ
  • インスタンスごとに初期化したいprivateメンバ
  • 公開したいものは読み取り専用プロパティにする

 次に不変な読み取り専用プロパティの使いどころを見てみる。

不変な読み取り専用プロパティとその使いどころ

 読み取り専用のプロパティは、外部からは書き換えできない。そこで、プロパティのバッキングフィールドを初期化時以外には変更できないようにすれば、不変な読み取り専用プロパティになる。

 従来のプロパティの書き方をする場合は、バッキングフィールドにreadonly修飾子を付ける(次のコード)。これでクラス内からもプロパティが変更されないことを保証できる。

public class MyClass1
{
  private readonly string _name;
  public string Name { get { return _name; } }

  private readonly DateTimeOffset _created = DateTimeOffset.Now;
  public DateTimeOffset Created { get { return _created; } }

  public MyClass1(string name)
  {
    _name = name;
  }
}

Public Class MyClass1
  Private ReadOnly _name As String
  Public ReadOnly Property Name As String
    Get
      Return _name
    End Get
  End Property

  Private ReadOnly _created As DateTimeOffset = DateTimeOffset.Now
  Public ReadOnly Property Created As DateTimeOffset
    Get
      Return _created
    End Get
  End Property

  Public Sub New(name As String)
    _name = name
  End Sub
End Class

不変な読み取り専用プロパティの例(上:C#、下:VB)
Nameプロパティ(のバッキングフィールド)は、コンストラクタで初期化され、その後は変更できない。
Createdプロパティは、インスタンス生成時に初期化され、やはりその後は変更できない。

 「公開するならメンバ変数ではなくプロパティにすべき」と分かってはいても、しかし上のコードのように長々と書くのは勘弁してほしいと思うだろう。

 Visual Studio 2015からは、自動実装プロパティで簡潔に実装できるようになっている(次のコード)。

public class MyClass2
{
  public string Name { get; }
  public DateTimeOffset Created { get; } = DateTimeOffset.Now;
  public MyClass2(string name) => Name = name;
}

Public Class MyClass2
  Public ReadOnly Property Name As String
  Public ReadOnly Property Created As DateTimeOffset = DateTimeOffset.Now
  Public Sub New(name As String)
    Me.Name = name
  End Sub
End Class

不変な読み取り専用の自動実装プロパティの例(上:C#、下:VB)
Visual Studio 2015からの機能である。C#では、readonly修飾子を付けたメンバ変数とタイプ量はほぼ同じだ。もはや、定数以外は全てこの書き方に統一してもよいくらいである。
なお、このC#のコードはC# 7.0(Visual Studio 2017)の新機能も使っている(ラムダ式によるコンストラクタ定義)。

 なお、上のような全てのメンバ変数/プロパティが(初期化後は)不変なクラスは、「不変クラス」(immutable class/イミュータブルなクラス)と呼ばれる。参照型のメンバ変数にreadonly修飾子を付けてもそのオブジェクト内のメンバは書き換え可能だと先ほど指摘したが、それを避けるにはこのような不変クラスを作ってメンバ変数を置き換える。

 不変な読み取り専用プロパティの使いどころは次のようにまとめられる。

  • 公開するもの
  • Visual Studio 2015以降では、readonly修飾子を付けたメンバ変数の代替

 最後にC# 7.2で導入された不変性を強制する構造体を紹介する。

不変性を強制する構造体(C# 7.2)

 上記のようなイミュータブルなクラスにするのは開発者の責任だ。イミュータブルにしたつもりが、うっかり実装し間違えることもあり得る。

 構造体でも事情は同じだった。ところがC# 7.2では、構造体に不変性を強制できるようになったのである。構造体定義にreadonly修飾子を付けると、メンバ変数にreadonly修飾子が必須になり、プロパティも読み取り専用しか許されなくなる(次のコード)。

public readonly struct MyStruct2
{
  //public string Name { get; set; } // コンパイルエラー
  public string Name { get; } // 読み取り専用が強制される

  //private string _name2; // コンパイルエラー
  private readonly string _name2; // readonlyが強制される
  public string Name2 { get => _name2; }

  public MyStruct2(string name)
  {
    Name = name;
    _name2 = name;
  }
}

readonly structの例(C#)
C# 7.2の新機能である。このコードをコンパイルするにはVisual Studio 2017 Update 5(15.5)以降を使い、ビルドに使用する言語バージョンにC# 7.2以降を指定する必要がある。

まとめ

 定数はメソッド内またはメンバで、公開しない数値/文字列/列挙体に用いる。それ以外のメンバでは、Visual Studio 2015以降は読み取り専用の自動実装プロパティを使うとよい。Visual Studio 2015以前では、公開しないものにはreadonly修飾子を付けたメンバ変数を使う。また、オブジェクト自体を不変にする方法も解説した。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。