構文:文字列にクラス名などを間違えないようにコーディングするには?[C# 6.0].NET TIPS

C# 6で追加されたnameof演算子を使うことで、クラス名/変数名/プロパティ名などを安全に文字列化できる。名前にまつわるバグを減らしてくれるうれしい機能だ。

» 2016年07月20日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

対象:Visual Studio 2015(C# 6.0)以降


 コード内に、クラス名や変数名などを文字列リテラルとして書くことがある。エラーメッセージに変数名を埋め込んだり、イベント通知にプロパティ名を含めたりする場合だ。ところが文字列では、タイプミスしていてもコンパイラはチェックしてくれない。名前を変えるときには修正漏れを起こしやすい。何とかならないだろうか? C# 6.0で導入されたnameof演算子を使えば、そんな悩みも解決する。本稿ではその使い方を解説しよう。

 なお、本稿はC#で説明するが、Visual Studio 2015のVisual Basic(VB 14)でも利用できる。

nameof演算子の基本

 nameof演算子を使うと、名前空間/型/メソッド/プロパティ/変数などの単純な名前(=名前空間やクラス名などで修飾されていない名前)の文字列が取得できる。次のコードに簡単な例を示す。なお、nameof演算子は、コンパイル時に文字列リテラルに置き換えられるので、実行速度に影響しない。

using System;
using static System.Console;
namespace SampleNamespace
{
  class Sample{ public void SampleMethod() { } }

  class Program
  {
    static void Main(string[] args)
    {
      // クラス名
      WriteLine($"Sampleクラス:{nameof(Sample)}");
      // ⇒ Sampleクラス:Sample

      // 参考:クラス名のフルネームを取得するにはtypeofを使う
      WriteLine($"Sampleクラスのフルネーム:{typeof(Sample).FullName}");
      // ⇒ Sampleクラスのフルネーム:SampleNamespace.Sample

      // メソッド名
      WriteLine($"SampleクラスのSampleMethod:{nameof(Sample.SampleMethod)}");
      // ⇒ SampleクラスのSampleMethod:SampleMethod

      // オブジェクトのメソッド名も取得できる
      var s = new Sample();
      WriteLine($"SampleオブジェクトのSampleMethod:{nameof(s.SampleMethod)}");
      // ⇒ SampleオブジェクトのSampleMethod:SampleMethod

      // 変数は、その変数名が取得できる(変数の型名ではない)
      WriteLine($"Sampleオブジェクトを持つ変数:{nameof(s)}");
      // ⇒ Sampleオブジェクトを持つ変数:s
      Action a = () => s.SampleMethod();
      WriteLine($"delegate変数:{nameof(a)}");
      // ⇒ delegate変数:a

      // 名前空間も取得できる。ただし、末尾のみ
      WriteLine($"System.Text名前空間:{nameof(System.Text)}");
      // ⇒ System.Text名前空間:Text

      // 参考:名前空間のフルネームは、typeofを応用すれば取得可能
      var sbName = typeof(System.Text.StringBuilder).FullName;
      var systemText = sbName.Remove(sbName.LastIndexOf('.'));
      WriteLine($"System.Text名前空間:{systemText}");
      // ⇒ System.Text名前空間:System.Text

#if DEBUG
      ReadKey();
#endif
    }
  }
}

nameof演算子を使ったコンソールアプリの例
クラス名/メソッド名/変数名/名前空間名をnameof演算子で取得する例である。
なお、型名なしでWriteLineメソッドを呼び出す書き方については、「.NET TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]」を、また、文字列の先頭に「$」記号を付ける書き方は、「.NET TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]」をご覧いただきたい。

INotifyPropertyChangedインタフェースの実装例

 実際の使い方を、INotifyPropertyChangedインタフェースを実装するクラスで見てみよう。ここでは、プロパティの名前を取得するためにnameof演算子を使う。

 まず従来の書き方から見ていこう。次のコードの「SampleClass」クラスは、2つのプロパティ名を文字列リテラルで記述している。例外のコンストラクタに渡す引数と、イベントを起動するときの引数だ。

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class SampleClass : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;

  // PropertyChangedイベントを発火させるメソッド
  protected void OnPropertyChanged(
                    [CallerMemberName] string propertyName = null)
  {
    PropertyChanged?.Invoke(this,
                             new PropertyChangedEventArgs(propertyName));
  }

  // メンバ変数を変更し、PropertyChangedイベントを発火させるための、
  // ジェネリックなメソッド
  protected bool SetProperty<T>(ref T storage, T value,
                                [CallerMemberName] string propertyName = null)
  {
    if (Equals(storage, value))
      return false;
    storage = value;
    OnPropertyChanged(propertyName);
    return true;
  }

  // 値を保持しているプロパティ
  private DateTimeOffset m_dt;
  public DateTimeOffset Date
  {
    get { return m_dt; }
    set
    {
      if (value == DateTimeOffset.MinValue)
      {
        // 従来の書き方
        throw new ArgumentOutOfRangeException("Date");
      }
      if (SetProperty<DateTimeOffset>(ref m_dt, value))
      {
        // DateStringプロパティにも変更があったと通知する
        // 従来の書き方
        OnPropertyChanged("DateString");
      }
    }
  }

  // 他のプロパティに依存している、読み取り専用のプロパティ
  // 「Date」プロパティが変わると、この「DateString」プロパティも変わる
  public string DateString
  {
    get { return m_dt.ToString("yyyy/MM/dd"); }
  }
}

INotifyPropertyChangedインタフェースの実装例(従来の書き方)
XAMLでUIを構築するアプリ(WPFアプリやUWPアプリなど)では、データバインディングのためにINotifyPropertyChangedインタフェースを実装したデータクラスを頻繁に作成する。そこでは、この例に示すように、プロパティ名を文字列リテラルで記述する場面も多い。そして厄介なことに、「OnPropertyChanged」メソッドに渡すプロパティ名を間違えていても、ビルドエラーにならないばかりか、実行時にもエラーにならない。ただデータバインディングが期待通りには動作しないだけなのだ。
なお、実際の開発プロジェクトでは、PropertyChangedイベント/OnPropertyChangedメソッド/SetProperty<T>メソッドだけを実装した基本クラスを作り、それを継承してそれぞれのデータクラスを書くようにするとよい。
また、OnPropertyChangedメソッド内に登場する「?.」という記述については、「.NET TIPS:構文:nullチェックを簡潔に記述するには?[C# 6.0]」をご覧いただきたい。

 上のコードで「OnPropertyChanged」メソッドに渡しているプロパティ名「DateString」を書き間違えても、ビルドエラーにもならず、実行時エラーも出ない。このうっかりミスは、発見しにくいのである。また、例外のエラーメッセージに使われるプロパティ名「Date」の間違いも、発見しにくい。例外を発生させるテストはあまり頻繁には行われないものだし、テストから漏れることさえちょくちょくあるからだ。

 nameof演算子を使って、次のコードのように「Date」プロパティのsetterを書き換えれば、もう間違えることはない。

public DateTimeOffset Date
{
  get { return m_dt; }
  set
  {
    if (value == DateTimeOffset.MinValue)
    {
      // 従来の書き方
      //throw new ArgumentOutOfRangeException("Date");
      // nameof演算子を使う
      throw new ArgumentOutOfRangeException(nameof(Date));
    }
    if (SetProperty<DateTimeOffset>(ref m_dt, value))
    {
      // DateStringプロパティにも変更があったと通知する
      // 従来の書き方
      //OnPropertyChanged("DateString");
      // nameof演算子を使う
      OnPropertyChanged(nameof(DateString));
    }
  }
}

INotifyPropertyChangedインタフェースの実装例(nameof演算子を利用)
「Date」プロパティのみを示す。
nameof演算子を使えば、「Date」や「DateString」を書き間違えても、その場でVisual Studioが警告してくれる。もちろんビルドエラーにもなる。

まとめ

 クラス名や変数名などを文字列リテラルとしてコーディングしなければならない場面では、できるだけnameof演算子を使おう。つまらないバグに悩まされずに済む。

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

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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