連載
» 2017年06月28日 05時00分 UPDATE

.NET TIPS:Caller Info属性で呼び出し元の情報を得るには?[C#/VB]

Caller Infoと呼ばれる属性を使って、メソッド呼び出し時に、それを呼び出した側のコードのソースファイル名/行番号/メソッド名といった情報を取得する方法を説明する。

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

連載目次

 呼び出されたメソッドの側で、呼び出し元の情報を取得したいことがあるだろう。INotifyPropertyChangedインタフェースを実装するときや、詳細なログ出力をしたいときなどだ。呼び出し元のファイル名/行番号/メソッド名を、呼び出されたメソッドの側でCaller Info属性を使えば取得できる(呼び出し元に追加のコードは必要ない)。本稿では、そのCaller Info属性の使い方を解説する。

 なお、Caller Info属性はVisual Studio 2012から利用できるが、本稿のサンプルコードにはそれより新しい内容も含んでいる。サンプルコードをそのまま試すには、Visual Studio 2017が必要である。

Caller Info属性で呼び出し元の情報を得るには?

 Caller Info属性(日本語訳は「呼び出し元情報の属性」だが、本稿では先に広まった呼称の方を使う)には、3種類ある(いずれもSystem.Runtime.CompilerServices名前空間)。

  • CallerFilePath属性: 呼び出し元のソースファイルのフルパス
  • CallerLineNumber属性: 呼び出し元の行番号
  • CallerMemberName属性: 呼び出し元のメソッドなどのメンバー名

 これらの属性を、呼び出されるメソッドのオプション引数(省略可能な引数)に付ければよい。その解決はコンパイル時に行われる(コンパイル後に難読化ツールを使っても元の名前や行番号などは維持される)。

 3つの属性を全て使ったメソッドの例を示す(次のコード)。このメソッドを呼び出した結果については後述する。

using System.Runtime.CompilerServices;
using static System.Console;

……省略……

public static void GetCallerInfoSample(
              string msg,
              [CallerMemberName] string memberName = "",
              [CallerFilePath] string filePath = "",
              [CallerLineNumber] int lineNumber = -1
            )
{
  // サンプルとしてフルパスの表示は長いので、ファイル名だけにする
  string fileName = System.IO.Path.GetFileName(filePath);
  WriteLine($"{msg}:{memberName}, {fileName}, {lineNumber}");
}

Imports System.Runtime.CompilerServices
Imports System.Console

……省略……

Public Sub GetCallerInfoSample(
            msg As String,
            <CallerMemberName> Optional memberName As String = "",
            <CallerFilePath> Optional filePath As String = "",
            <CallerLineNumber> Optional lineNumber As Integer = -1)
  ' サンプルとしてフルパスの表示は長いので、ファイル名だけにする
  Dim fileName As String = System.IO.Path.GetFileName(filePath)
  WriteLine($"{msg}:{memberName}, {fileName}, {lineNumber}")
End Sub

Caller Info属性を使ったメソッドの例(上:C#、下:VB)
Caller Info属性を付ける引数は、省略可能でなければならない。
CallerMemberName属性を付けた「memberName」引数には、呼び出し元のメンバー名が入ってくる。同様に、CallerFilePath属性を付けた「filePath」引数には呼び出し元のソースファイルのフルパスが、CallerLineNumber属性を付けた「lineNumber」引数には呼び出し元の行番号が入ってくる。

メソッドの中から呼び出す例

 上に示した「GetCallerInfoSample」メソッドを、コンソールアプリのMainメソッドの中から呼び出してみよう(次のコード)。単純なメソッド呼び出しの他に、ラムダ式を使ったデリゲートからも呼び出してみる。また、C#では、ローカル関数(C# 7の新機能)からも呼び出している。

 コメントに付けた出力結果を見てもらうと、いずれもCallerMemberNameは「Main」メソッドであると表示されている。デリゲートの変数名やローカル関数の名前にはならない。

using System;
using System.Runtime.CompilerServices;
using static System.Console;

class Program
{
  public static void GetCallerInfoSample(
    ……省略(前掲)……
  }

  static void Main(string[] args)
  {
    GetCallerInfoSample("Mainメソッドから呼び出し"); // 行21
    // 出力:Mainメソッドから呼び出し:Main, Program.cs, 21

    Action<string> func = (s) => GetCallerInfoSample(s); // 行24
    func.Invoke("Mainメソッド内のdelegateから呼び出し");
    // 出力:Mainメソッド内のdelegateから呼び出し:Main, Program.cs, 24

    // ローカル関数(C# 7の新機能)
    void InnerMethod()
    {
      GetCallerInfoSample("Mainメソッド内のローカル関数から呼び出し"); // 行31
    }
    InnerMethod();
    // 出力:Mainメソッド内のローカル関数から呼び出し:Main, Program.cs, 31

#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Runtime.CompilerServices
Imports System.Console

Module Module1

  Public Sub GetCallerInfoSample(
    ……省略(前掲)……
  End Sub

  Sub Main()
    GetCallerInfoSample("Mainメソッドから呼び出し") ' 行17
    ' 出力:Mainメソッドから呼び出し:Main, Module1.vb, 17

    Dim func As Action(Of String) = Sub(s) GetCallerInfoSample(s) ' 行20
    func.Invoke("Mainメソッド内のdelegateから呼び出し")
    ' 出力:Mainメソッド内のdelegateから呼び出し:Main, Module1.vb, 20

#If DEBUG Then
    ReadKey()
#End If
  End Sub
End Module

Mainメソッド中で「GetCallerInfoSample」メソッドを呼び出すコンソールアプリ(上:C#、下:VB)
ラムダ式やローカル関数の中から呼び出しても、Mainメソッドから呼び出されたと表示される。
また、ラムダ式やローカル関数では、それらを呼び出しているところの行番号ではなく、ラムダ式やローカル関数の中で「GetCallerInfoSample」メソッドを呼び出している行が報告される。

コンストラクタやプロパティなどから呼び出す例

 コンストラクタ/オーバーロードしたメソッド/プロパティの中から呼び出した場合は、次のコードのようになる。オーバーロードしたメソッドやプロパティでもメンバー名/行番号は正しく得られるのだが、どのオーバーロードなのか、あるいはgetter/setterのいずれかであるかまでは区別できない。

public class SampleClass
{
  static SampleClass()
  {
    Program.GetCallerInfoSample("SampleClassの静的コンストラクタから呼び出し"); // 行5
  }

  public SampleClass()
  {
    Program.GetCallerInfoSample("SampleClassのコンストラクタから呼び出し"); // 行10
  }

  public void Method1()
  {
    Program.GetCallerInfoSample("SampleClassのMethod1メソッド(引数なし)から呼び出し");
  }

  public void Method1(string message)
  {
    Program.GetCallerInfoSample(message); // 行20
  }

  private string _member;
  public string Member
  {
    get => _member;
    set
    {
      Program.GetCallerInfoSample("SampleClassのMemberプロパティから呼び出し"); // 行29
      _member = value;
    }
  }
}

……省略……

// 以下は、先ほどのコンソールアプリのMainメソッド内に記述する

// SampleClassのコンストラクタから呼び出し
var sampleClass = new SampleClass();
// 出力:SampleClassの静的コンストラクタから呼び出し:.cctor, SampleClass.cs, 5
// 出力:SampleClassのコンストラクタから呼び出し:.ctor, SampleClass.cs, 10

// SampleClassのオーバーロードしたメソッドから呼び出し
sampleClass.Method1("SampleClassのMethod1メソッド(1引数)から呼び出し");
// 出力:SampleClassのMethod1メソッド(1引数)から呼び出し:Method1, SampleClass.cs, 20

// SampleClassのMemberプロパティから呼び出し
sampleClass.Member = "test";
// 出力:SampleClassのMemberプロパティから呼び出し:Member, SampleClass.cs, 29

Public Class SampleClass

  Shared Sub New()
    Module1.GetCallerInfoSample("SampleClassの静的コンストラクタから呼び出し") '行4
  End Sub

  Public Sub New()
    Module1.GetCallerInfoSample("SampleClassのコンストラクタから呼び出し") '行8
  End Sub

  Public Sub Method1()
    Module1.GetCallerInfoSample("SampleClassのMethod1メソッド(引数なし)から呼び出し")
  End Sub

  Public Sub Method1(message As String)
    Module1.GetCallerInfoSample(message) ' 行16
  End Sub

  Private _member As String
  Public Property Member As String
    Get
      Return _member
    End Get
    Set(value As String)
      Module1.GetCallerInfoSample("SampleClassのMemberプロパティから呼び出し") ' 行25
      _member = value
    End Set
  End Property

End Class

……省略……

' 以下は、先ほどのコンソールアプリのMainメソッド内に記述する

' SampleClassのコンストラクタから呼び出し
Dim sampleClass = New SampleClass()
' 出力:SampleClassの静的コンストラクタから呼び出し:.cctor, SampleClass.vb, 4
' 出力:SampleClassのコンストラクタから呼び出し:.ctor, SampleClass.vb, 8

' SampleClassのオーバーロードしたメソッドから呼び出し
sampleClass.Method1("SampleClassのMethod1メソッド(1引数)から呼び出し")
' 出力:SampleClassのMethod1メソッド(1引数)から呼び出し:Method1, SampleClass.vb, 16

' SampleClassのMemberプロパティから呼び出し
sampleClass.Member = "test"
' 出力:SampleClassのMemberプロパティから呼び出し:Member, SampleClass.vb, 25

コンストラクタなどから「GetCallerInfoSample」メソッドを呼び出す例(上:C#、下:VB)
CallerMemberNameは、静的コンストラクタから呼び出した場合は「.cctor」、通常のコンストラクタからの場合は「.ctor」となる。ここには載せていないが、ファイナライザから呼び出した場合のメンバー名は「Finalize」になる。
プロパティから呼び出したときのCallerMemberNameはプロパティ名になる(getterかsetterかは分からない)。オーバーロードしたメソッドから呼び出したときも、同様にメソッド名だけが分かる(どのオーバーロードなのかは分からない)。
Caller Info属性をロギングに利用するときなどは、プロパティのgetter/setterやオーバーロードを識別するために呼び出し元の行番号も忘れずに書き出すようにしよう。

まとめ

 メソッドの引数にCaller Info属性を使うと、呼び出し元のソースコードのファイル名/行番号/メンバー名を取得できる。

 うまく使うと、例えばINotifyPropertyChangedインタフェースの実装を簡潔に記述できる(サンプルコードは.NET TIPS「構文:文字列にクラス名などを間違えないようにコーディングするには?[C# 6.0]」に掲載)。

利用可能バージョン:.NET Framework 4.5以降(Visual Studio 2012以降)
カテゴリ:クラスライブラリ 処理対象:属性
使用ライブラリ:CallerFilePath属性(System.Runtime.CompilerServices名前空間)
使用ライブラリ:CallerLineNumber属性(System.Runtime.CompilerServices名前空間)
使用ライブラリ:CallerMemberName属性(System.Runtime.CompilerServices名前空間)
関連TIPS:オプション引数が使えるメソッドを作るには?[C#/VB]
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
関連TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?


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

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

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

メールマガジン登録

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