平々毎々(アーカイブ)

はてなダイアリーのアーカイブです。

"ByRef parameter and C#" のまとめ

ByRef parameter and C# - 猫とC#について書くmatarilloの雑記という記事を(いい加減な英語で)書いたところ、id:NyaRuRuさんとYokoKenさんが反応してくれました。多謝。

はてなダイアリーのコメント欄だとコードが見づらいので、エントリを起こしなおしました。

前の記事で書いた問題

下のコードを実行すると、Method()の中で例外が発生するが、それまでにoutパラメータに加えた変更は呼び出し元に反映されているので、コンソールには"out"が出力される。

class P
{
  public static void Method(out string arg)
  {
    arg = "out";
    throw new Exception();
  }

  public static void Main()
  {
    string arg = null;
    try
    {
      Method(out arg);
    }
    catch
    {
    }
    Console.WriteLine(arg); // argは"out"
  }
}

ところが、メソッド情報を取得してInvoke()した場合はそうならない。Method()の中で例外が発生すると、outパラメータに加えた変更は呼び出し元に反映されない。

class P
{
  public static void Method(out string arg)
  {
    arg = "out";
    throw new Exception();
  }

  public static void Main()
  {
    MethodInfo target = typeof(P).GetMethod("Method");
    object[] args = new object[1];
    try
    {
      target.Invoke(null, args);
    }
    catch
    {
    }
    Console.WriteLine(args[0]); // args[0]はnull
  }
}

これはMethodInfo.Invoke()の実装がそうなってるから仕方がないようなのだけれど、じゃあ他の方法で動的にメソッドを呼び出して、out引数の変更も受け取りつつ例外もつかむことは可能か?という質問。

ちなみに、Main()とMethod()が同じ型に定義されていたり、Methodがpublic static だったりするのは、コード片をちょっとでも短くしたいから、ただそれだけで他意はない。

NyaRuRuさんのコメント(1つめ)

デリゲートに変換して呼び出せばいいよ、というコードでした。
もし動的に呼び出すメソッドの型を決めうちにしていいなら、ジェネリックは不要で、こうなる。

delegate void ByRefFunc(out string arg); // 決めうちにしたデリゲート

class P
{
  public static void Method(out string arg)
  {
    arg = "out";
    throw new Exception();
  }

  public static void Main()
  {
    MethodInfo target = typeof(P).GetMethod("Method");
    Type funcType = typeof(ByRefFunc);
    ByRefFunc func = Delegate.CreateDelegate(funcType, target) as ByRefFunc;
    string arg = null;
    try
    {
      func(out arg);
    }
    catch
    {
    }
    Console.WriteLine(arg); // argは"out"
  }
}

メソッドの引数の型を決めうちにしない場合は、ジェネリックデリゲートを使うのだけど、ローカル変数argの型も可変にしなければならないから、そのためにはジェネリックメソッドを1枚かぶせなければならない。

そうすると今度は1枚かぶせたジェネリックメソッドから変更されたout引数と例外の両方を受け取らなければならない。これは、2つまとめた型をジェネリックメソッドの戻り値にしてしまうのが簡単。NyaRuRuさんはTupleを使っている。

delegate void ByRefFunc<T>(out T arg);
delegate Tuple<object, Exception> SolverFunc(MethodInfo target);

class P
{
  public static void Method(out string arg)
  {
    arg = "out";
    throw new Exception();
  }

  // ByRefFunc<T>を呼び出すジェネリックメソッド
  public static Tuple<object, Exception> SolveIt<T>(MethodInfo target)
  {
    Type funcType = typeof(ByRefFunc<>).MakeGenericType(typeof(T));
    ByRefFunc<T> func = Delegate.CreateDelegate(funcType, target) as ByRefFunc<T>;
    T result = default(T);
    Exception resultException = default(Exception);
    try
    {
      func(out result);
    }
    catch (Exception e)
    {
      resultException = e;
    }
    return Tuple.Create((object)result, resultException);
  }

  public static void Main()
  {
    MethodInfo target = typeof(P).GetMethod("Method");
    ParameterInfo paramInfo = target.GetParameters()[0];
    Type paramType = paramInfo.ParameterType; // ポインタ型
    Type paramElementType = paramType.GetElementType();

    MethodInfo solver = typeof(P).GetMethod("SolveIt").MakeGenericMethod(paramElementType);
    SolverFunc func  = Delegate.CreateDelegate(typeof(SolverFunc), solver) as SolverFunc;
    Tuple<object, Exception> result = func(target);
    string arg = (string)result.Item1;
    Exception e = result.Item2;

    Console.WriteLine(arg); // argは"out"
    if (e != null)
    {
      ConsoleColor originalColor = Console.ForegroundColor;
      Console.ForegroundColor = ConsoleColor.Red;
      Console.WriteLine(e);
      Console.ForegroundColor = originalColor;
    }
  }
}

この例ならSolverFuncをSolverFuncに変えてもよさそう。

NyaRuRuさんのコメント(2つめ)

1つ目のコードは、ジェネリックメソッド SolveIt() を使っているんだけど、SolveIt() は残念ながら、ByRefFuncデリゲートに適合するメソッドしか呼び出せない。つまり、引数が違っていたり、戻り値を持ったりするメソッドの場合は対応できない。

引数と戻り値のバリエーションにあわせてデリゲートをいくつも定義しておいて、呼び出し元の方で条件分岐してもいいけど、組み合わせの数が多くなりすぎる(1つの引数ごとに無印/out/refの3通りが考えられる)。そこでメソッドを動的に生成することにする。

NyaRuRuさんは式木を使って動的生成している。(.NET 4の機能を一部使っている)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

public class P
{
  public static void Method(out string arg, int i)
  {
    arg = "out";
    throw new Exception();
  }

  public static void Main()
  {
    var mi = typeof(P).GetMethod("Method");
    var args = new object[mi.GetParameters().Length];
    var exception = default(Exception);
    var ret = mi.DynamicInvoke(null, args, out exception);
    Console.WriteLine(args[0]);
    Console.WriteLine(ret);
    Console.WriteLine(exception);
  }
}

public static class InvokeUtil
{
  public static IEnumerable<Expression> Expressions(Expression expr1, Expression expr2, IEnumerable<Expression> seq, params Expression[] exprs)
  {
    yield return expr1;
    yield return expr2;
    foreach (var item in seq) yield return item;
    foreach (var item in exprs) yield return item;
  }

  private delegate object Invoker(object target, object[] arguments, out Exception e);

  public static object DynamicInvoke(this MethodInfo method, object target, object[] arguments, out Exception exception)
  {
    var Parameters = new
    {
      Target = Expression.Parameter(typeof(object), "target"),
      Args = Expression.Parameter(typeof(object[]), "args"),
      Exception = Expression.Parameter(typeof(Exception).MakeByRefType(), "exception"),
    };
    var LocalVariables = new
    {
      instance = Expression.Parameter(method.DeclaringType, "instance"),
      locals = method.GetParameters()
        .Select(param => Expression.Variable(
          param.ParameterType.IsByRef
            ? param.ParameterType.GetElementType()
            : param.ParameterType,
          "local" + param.Position))
        .ToArray(),
    };
    Func<Expression, Type, Expression> UnboxOrDefault =
      (Expression expression, Type targetType) => Expression.Condition(
        Expression.ReferenceNotEqual(Expression.Default(typeof(object)), expression),
        targetType.IsValueType
          ? Expression.Unbox(expression, targetType)
          : Expression.Convert(expression, targetType),
        Expression.Default(targetType));
    var e = Expression.Parameter(typeof(Exception), "e");
    var call = method.IsStatic
      ? Expression.Call(method, LocalVariables.locals) as Expression
      : Expression.Call(LocalVariables.instance, method,LocalVariables.locals) as Expression;
    var retCall = (call.Type == typeof(void))
      ? Expression.Block(call, Expression.Default(typeof(object))) as Expression
      : Expression.Convert(call, typeof(object)) as Expression;

    var block = Expression.Block(
      retCall.Type,
      LocalVariables.locals.Concat(Enumerable.Repeat(LocalVariables.instance, 1)),
      Expressions(
        Expression.Assign(Parameters.Exception, Expression.Default(typeof(Exception))),
        Expression.Assign(
          LocalVariables.instance,
          UnboxOrDefault(Parameters.Target, LocalVariables.instance.Type)
        ),
        LocalVariables.locals.Select((local, index) => Expression.Assign(
          local,
          UnboxOrDefault(Expression.ArrayIndex(Parameters.Args, Expression.Constant(index)), local.Type)
        )),
        Expression.TryCatchFinally(
          retCall,
          Expression.Block(
            LocalVariables.locals.Select((local, index) => Expression.Assign(
              Expression.ArrayAccess(Parameters.Args, Expression.Constant(index)),
              Expression.Convert(local, typeof(object))
            ))
          ),
          Expression.Catch(
            e,
            Expression.Block(
              Expression.Assign(Parameters.Exception, e),
              Expression.Default(retCall.Type)
            )
          )
        )
      )
    );
    var exp = Expression.Lambda<Invoker>(block, Parameters.Target, Parameters.Args, Parameters.Exception);

    return exp.Compile()(target, arguments, out exception);
  }
}

DynamicInvoke()拡張メソッドが横長いので改行を入れたら、今度は縦長くなってしまった。

生成しようとしているメソッドはシンプルなので、落ち着いて追っていけば意味は分かるはず。ちなみに下のようなメソッドが生成される。

object __AnonymousMethod
  (object target, object[] arguments, out Exception exception)
{
  T0 local0;
  T1 local1;
  T2 local2;
  // 以下同様

  exception = default(Exception);

  // UnboxOrDefault
  TTarget instance = (target != null) ? (TTarget)target : default(TTarget);
  local0 = (arguments[0] != null) ? (T0)arguments[0] : default(T0);
  local1 = (arguments[1] != null) ? (T1)arguments[1] : default(T1);
  local2 = (arguments[2] != null) ? (T2)arguments[2] : default(T2);
  // 以下同様

  try
  {
    // ★
    return (object)(func(local0, local1, local2));
  }
  catch (Exception e)
  {
    exception = e;
    return default(TResult);
  }
  finally
  {
    arguments[0] = local0;
    arguments[1] = local1;
    arguments[2] = local2;
    // 以下同様
  }
}

MethodInfo.GetParameters()で仮引数の数や型が取れるので、それを使ってメソッドを生成している。

UnboxOrDefault部分は同じパターンの繰り返しなので、ラムダ式で対応している。(ここはILの方が簡単かも。後述。)

★部分は(インスタンスメソッドorスタティックメソッド)×(戻り値ありor戻り値なし)の4通りの場合で、それぞれ少しずつ生成されるコードを変えている。メソッドの呼び出しはExpression.Call()によって生成されるのだけれど、ありがたいことにref/outの考慮は式木コンパイラがうまいことやってくれる。(だから条件分岐は4通りで足りる)

YokoKenさんのコード (一部NyaRuRuさんの指摘をマージ)

こちらは式木を使わないLCG。ILGenerator.Emit()でILを出力直接出力するので、C#と対応付けた説明するのが面倒。だから説明はなし!

でも、出力するメソッドがシンプルなので、コード自体はそこまで長くない。

NyaRuRuさんの指摘は、OpCodes.Unbox_Any は値型だけでなくて参照型にも使えるというもの。式木版では区別していた(参照型はCastにしていた)ので、ここはILの方が楽なのかな。

using System;
using System.Reflection;
using System.Reflection.Emit;

public class P
{
  public static void Method(ref string arg)
  {
    arg = "out";
    throw new Exception();
  }

  public static void Main()
  {
    object[] args = new object[1];
    try
    {
      MethodInfo targetMethod = typeof(P).GetMethod("Method");
      targetMethod.InvokeStrictly(null, args);
    }
    catch (Exception ex)
    {
      Console.WriteLine(ex.ToString());
      Console.WriteLine();
    }
    Console.WriteLine(args[0]);
    Console.ReadLine();
  }
}

public static class MethodInfoExtension
{
  public static object InvokeStrictly(this MethodInfo source, object obj, object[] parameters)
  {
    ParameterInfo[] paramInfos = source.GetParameters();
    if ((parameters == null) || (paramInfos.Length != parameters.Length))
    {
      throw new ArgumentException();
    }

    Type[] paramTypes = new[] { typeof(object[]) };
    DynamicMethod invokerBuilder = new DynamicMethod(string.Empty, typeof(object), paramTypes, true);

    ILGenerator ilGenerator = invokerBuilder.GetILGenerator();
    Label exBlockLabel = ilGenerator.BeginExceptionBlock();

    for (int i = 0; i < paramInfos.Length; i++)
    {
      var paramInfo = paramInfos[i];
      bool paramIsByRef = paramInfo.ParameterType.IsByRef;
      var paramType = paramIsByRef ? paramInfo.ParameterType.GetElementType() : paramInfo.ParameterType;

      ilGenerator.DeclareLocal(paramType);

      ilGenerator.Emit(OpCodes.Ldarg_0);
      ilGenerator.Emit(OpCodes.Ldc_I4, i);
      ilGenerator.Emit(OpCodes.Ldelem_Ref);
      Label label1 = ilGenerator.DefineLabel();
      ilGenerator.Emit(OpCodes.Brfalse, label1);

      ilGenerator.Emit(OpCodes.Ldarg_0);
      ilGenerator.Emit(OpCodes.Ldc_I4, i);
      ilGenerator.Emit(OpCodes.Ldelem_Ref);
      ilGenerator.Emit(OpCodes.Unbox_Any, paramType);
      ilGenerator.Emit(OpCodes.Stloc_S, (byte)i);

      ilGenerator.MarkLabel(label1);

      if (paramIsByRef)
      {
        ilGenerator.Emit(OpCodes.Ldloca_S, (byte)i);
      }
      else
      {
        ilGenerator.Emit(OpCodes.Ldloc_S, (byte)i);
      }
    }

    LocalBuilder resultLocal = ilGenerator.DeclareLocal(typeof(object), false);
    ilGenerator.Emit(OpCodes.Call, source);
    if (source.ReturnType == typeof(void))
    {
      ilGenerator.Emit(OpCodes.Ldnull);
    }
    ilGenerator.Emit(OpCodes.Stloc_S, resultLocal);
    ilGenerator.Emit(OpCodes.Leave, exBlockLabel);

    ilGenerator.BeginFinallyBlock();
    for (int i = 0; i < paramInfos.Length; i++)
    {
      var paramInfo = paramInfos[i];
      bool paramIsByRef = paramInfo.ParameterType.IsByRef;
      var paramType = paramIsByRef ? paramInfo.ParameterType.GetElementType() : paramInfo.ParameterType;

      ilGenerator.Emit(OpCodes.Ldarg_0);
      ilGenerator.Emit(OpCodes.Ldc_I4, i);
      ilGenerator.Emit(OpCodes.Ldloc_S, (byte)i);
      if (paramType.IsValueType)
      {
        ilGenerator.Emit(OpCodes.Box, paramType);
      }
      ilGenerator.Emit(OpCodes.Stelem, typeof(object));
    }
    ilGenerator.EndExceptionBlock();

    ilGenerator.Emit(OpCodes.Ldloc_S, resultLocal);
    ilGenerator.Emit(OpCodes.Ret);

    var invoker = (Func<object[], object>)invokerBuilder.CreateDelegate(typeof(Func<object[], object>));
    return invoker(parameters);
  }
}

(余談)Stack Overflowで質問してみたけど、"Good Question."状態で終わってしまった。フォローアップも投稿しておこうかな。