EnvDTE を使ってデバッグ中にコレクション操作 ~C# 編~

前回の続きです。

kendik.hatenablog.com

イミディエイトウインドウでラムダ式が使えないけど何とかしたい!ということで、前回はパッケージマネージャコンソールを使ってデバッグ中の変数の値を PowerShell の変数にコピーしてきて、コレクションに対して自由に操作できる状態を作りました。

それはそれでありがたいんですが、私のような PowerShell 弱者にとっては C# で書けた方が楽ですし、ユーザ定義型もなんちゃってじゃなくてきちんと使えた方が嬉しいですよね。

という訳で今回は前回と同じことを C# でやります。

その前に

まーみんな Build 見てて知ってるかもしれませんが、Visual Studio 2015 が RC になりました。

Visual Studio 2015 RC | Release Notes

VS2015 からはイミディエイトウィンドウでラムダ式が使えるようになるのは前回言ったとおりですが、まさかもう RC が出てくるとは。もう正直余計なことしないで普通に VS2015 使ったらいいんじゃね?ってことでテンション下がり気味でして、もう本日は駆け足で説明していきます。

基本的な操作

まずはプリミティブ型のコレクションを取得することにします。デバッグ対象のコードは以下の通りです。

var list = new List<int> {1, 2, 3, 4, 5};
foreach (var i in list)
    Console.WriteLine(i);

このデバッグ対象コードをデバッグ実行して適当にブレークします。

f:id:kendik:20150503024230p:plain

そしてデバッグ中のものとは別に VS を立ち上げて、適当にプロジェクトを開始して以下のコードを書きます。なお、参照設定で「EnvDTE」を参照に追加しておいてください。

// エラー処理は省略
var dte = (EnvDTE.DTE) System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.12.0");

var targetProcessId = System.Diagnostics.Process.GetProcessesByName("ConsoleApplication11.vshost") // デバッグ対象プログラムのプロセス名
    .Single()
    .Id;
var process = dte.Debugger.DebuggedProcesses
    .OfType<EnvDTE.Process>()
    .Single(_ => _.ProcessID == targetProcessId);

var capture = process.DTE.Debugger.GetExpression("list.ToArray()"); // 「列ビュー」要素を弾くために ToArray する
var ret = capture.DataMembers
    .OfType<EnvDTE.Expression>()
    .Select(_ => Convert.ToInt32(_.Value));

foreach (var iter in ret.Where(_ => _ % 2 == 0))
    Console.WriteLine(iter);

f:id:kendik:20150503024251p:plain

このコードを実行すると、偶数だけの要素を取得することが出来ます。

f:id:kendik:20150503024303p:plain

基本的にやっていることは前回と同じです。
前半では、前回使った $dte と同じものを取得しています。
後半では、GetExpression メソッドで取得してきた結果を新しい結果リストにコピーしています。

ユーザ定義型の場合

さて、先のコードで取得できるのはプリミティブ型のコレクションだけです。ユーザ定義型のコレクションを取得するにはどうしたらいいでしょうか。例えばデバッグ対象コードが以下のようになっているケースです。

class Program
{
    static void Main(string[] args)
    {
        var list = new List<MyClass>
        {
            new MyClass {Id = 1, Name = "一郎"},
            new MyClass {Id = 2, Name = "二郎"},
            new MyClass {Id = 3, Name = "三郎"},
        };
        foreach (var i in list)
            Console.WriteLine("Id:{0},Name:{1}", i.Id, i.Name);
    }
}
public class MyClass
{
    public int Id { get; set; }
    public string Name { get; set; }
}

基本的には int 型にキャストしている部分をユーザ定義型へのキャストに変えてやればいいのですが、__comObject からの直接のキャストは行えないので、前回 PSCustomObject を使ったようにちょっと工夫が必要になります。今回はリフレクションを使うことにしました*1
先のコードを以下のとおり書き換えます。

// エラー処理は省略
var dte = (EnvDTE.DTE) System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.12.0");

var targetProcessId = System.Diagnostics.Process.GetProcessesByName("ConsoleApplication11.vshost") // デバッグ対象プログラムのプロセス名
    .Single()
    .Id;
var process = dte.Debugger.DebuggedProcesses
    .OfType<EnvDTE.Process>()
    .Single(_ => _.ProcessID == targetProcessId);

var capture = process.DTE.Debugger.GetExpression("list.ToArray()"); // 「列ビュー」要素を弾くために ToArray する

// 以下を書き換えた
var ret = new List<ConsoleApplication11.MyClass>();
foreach (EnvDTE.Expression captureItem in capture.DataMembers)
{
    var item = new ConsoleApplication11.MyClass();
    foreach (EnvDTE.Expression captureProperty in captureItem.DataMembers)
    {
        var property = item.GetType().GetProperty(captureProperty.Name);
        var capturePropertyValue = Convert.ChangeType(captureProperty.Value, property.PropertyType);
        property.SetValue(item, capturePropertyValue);
    }
    ret.Add(item);
}

foreach (var iter in ret.Where(_ => _.Id % 2 == 0))
    Console.WriteLine("Id:{0},Name:{1}", iter.Id, iter.Name);

実行結果

f:id:kendik:20150503024317p:plain

なお、コードを見ると分かると思いますがデバッグ対象アセンブリへの参照が必要です。
そこを動的にやってもいいんですが、そうすると型変換する旨みがなくなるので、どうせデバッグ用ですし気にせず参照してやったらいいと思います。

まとめ

EnvDTE を使うことで、PowerShell や別プロセスの VS (LINQPad からも出来るので厳密には VS に限らない) を使ってコレクションを取得し、自由に操作することが出来るようになりました。

ただ、残念ながらその手順は複雑で面倒です。

今日挙げたコードもまだ不完全で、例えば例外処理だったり例えばユーザ定義型をメンバに持つユーザ定義型のコレクションだったりには対応してません (面倒で試してないだけで、技術的には可能なはず)。

正直 VS 2015 を使うか、もうプロダクションコードに一時的にデバッグ用コードを入れてしまう方が幸せになれそうです。(前回今回のエントリは何だったんだ。。。)

*1:もっと簡単な方法あったら教えてください