如何将默认值(TimeSpan)作为可选参数的默认值

时间:2016-06-01 11:42:52

标签: c# dynamic reflection reflection.emit

我想发出与下面完全相同的动态方法:

void Foo(TimeSpan ts = default(TimeSpan))

使用ildasm,我可以看到它已被编译为nullref。 但是,根据我能得到的结果,如果我想通过Emit代码实现相同的功能,我可以调用名为ParameterBuilder.SetConstant的方法,当可选值类型为TimeSpan时,它将抛出异常。 我甚至反编译了SetConstant方法,它明确处理DateTime(但不是TimeSpan)。 Nullref也是不可接受的。从该代码开始,似乎无法将默认值(TimeSpan)设置为默认值。 有人可以帮忙吗?

2 个答案:

答案 0 :(得分:1)

这很难,需要大量使用反射来解决.net框架的局限性。

正如您所指出的,您可以反汇编ParameterBuilder.setConstant。此方法调用内部方法:

[SecuritySafeCritical]
public virtual void SetConstant(object defaultValue)
{
    TypeBuilder.SetConstantValue(this.m_methodBuilder.GetModuleBuilder(), this.m_pdToken.Token, (this.m_iPosition == 0) ? this.m_methodBuilder.ReturnType : this.m_methodBuilder.m_parameterTypes[this.m_iPosition - 1], defaultValue);
}

你也可以反汇编,看看抛出异常的位置(当type是值类型时):

if (destType.IsValueType && (!destType.IsGenericType || !(destType.GetGenericTypeDefinition() == typeof(Nullable<>))))
        {
            throw new ArgumentException(Environment.GetResourceString("Argument_ConstantNull"));
        }
        TypeBuilder.SetConstantValue(module.GetNativeHandle(), tk, 18, null);

幸运的是,您可以在此处调用相同的方法,但可以从mscorlib动态调用:

AssemblyName aName = new AssemblyName("DynamicAssemblyExample");
        AssemblyBuilder ab =
            AppDomain.CurrentDomain.DefineDynamicAssembly(aName, AssemblyBuilderAccess.RunAndSave);
        ModuleBuilder mb =
            ab.DefineDynamicModule(aName.Name, aName.Name + ".dll");

        TypeBuilder tb = mb.DefineType("MyClass", TypeAttributes.Public);

        MethodBuilder meb = tb.DefineMethod("Foo", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, typeof(void), new Type[] { typeof(TimeSpan) });

        ParameterBuilder pb = meb.DefineParameter(1, ParameterAttributes.Optional | ParameterAttributes.HasDefault, "ts");

        MethodInfo getNativeHandle = typeof(ModuleBuilder).GetMethod("GetNativeHandle", BindingFlags.NonPublic | BindingFlags.Instance);
        object nativeHandle = getNativeHandle.Invoke(mb, new object[0]);

        int tk = pb.GetToken().Token;

        MethodInfo setConstantValue = typeof(TypeBuilder).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(mi => mi.Name == "SetConstantValue" && mi.GetParameters().Last().ParameterType.IsPointer).First();

        setConstantValue.Invoke(pb, new object[] { nativeHandle, tk, /* CorElementType.Class: */ 18, null });

        ILGenerator ilgen = meb.GetILGenerator();

        FieldInfo fi = typeof(ILGenerator).GetField("m_maxStackSize", BindingFlags.NonPublic | BindingFlags.Instance);
        fi.SetValue(ilgen, 8);

        ilgen.Emit(OpCodes.Ret);


        tb.CreateType();
        ab.Save("DynamicAssemblyExample.dll");

以这种方式设置默认值不会更新stacksize,这意味着您必须在获取ILGenerator后立即手动设置(再次通过反射):

FieldInfo fi = typeof(ILGenerator).GetField("m_maxStackSize", BindingFlags.NonPublic | BindingFlags.Instance);
            fi.SetValue(ilgen, 8);

这会生成以下IL:

.method public hidebysig static 
    void Foo (
        [opt] valuetype [mscorlib]System.TimeSpan ts
    ) cil managed 
{
    .param [1] = nullref
    // Method begins at RVA 0x2050
    // Code size 1 (0x1)
    .maxstack 8

    IL_0000: ret
} // end of method MyClass::Foo

这与您提供的C#编译的内容相同。

答案 1 :(得分:1)

根据您想要实现的目标,确切地说,可能有一种更简单的方法。如果您调用ParameterBuilder.DefineParameter(1, ParameterAttributes.Optional, "Foo"),结果参数将被声明为可选参数,但没有显式默认值。在C#中使用此程序集时,您将无法获得默认值的IntelliSense,但编译器仍允许您在不显式提供值的情况下调用该方法,如果这样做,它将通过default(TimeSpan)。< / p>

生成的IL与C#编译器生成的内容不同(因为缺少参数初始化),我只能猜测其他.NET语言会用这样的声明做什么,但它确实可以节省一些非常难看的在System.Reflection.Emit的内部内部进行了修改(并且生成的IL通过了验证 - 运行时本身对默认声明没有任何作用)。

请注意,正是因为运行时没有对默认值声明做任何事情(需要任何工具这样做)在动态方法中发出默认值是一种奇怪的做法,应该几乎没有实际的应用程序,因为任何知道调用该方法的代码也应该知道要传递什么值(在保存到磁盘的程序集中定义它们是有意义的,编译器可以读取它)。

如果该方法确实是动态的,您可能希望生成该方法的多个重载,一个带参数,一个没有(而一个没有可以调用另一个)。这实现了与带有可选参数的方法相同的效果,并且动态调用者也可以更轻松地处理。