【问题标题】:How to emit default(TimeSpan) as the default value of an optional parameter如何发出 default(TimeSpan) 作为可选参数的默认值
【发布时间】:2016-09-30 18:18:19
【问题描述】:

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

void Foo(TimeSpan ts = default(TimeSpan))

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

【问题讨论】:

  • 添加您的代码 - 一个显示您的问题并实际编译和运行的最小示例。请注意,ParameterBuilder.SetConstant 的文档 明确 提到了支持的类型,而 TimeSpan 根本不包括在内(根据规范,它不是标准类型,就像 decimal 一样)。
  • 另外,你为什么要这样做?应该如何使用动态方法?您是否使用它来编译您稍后引用的程序集?默认值是编译时特性,因此除非您从普通 C# 代码调用该方法(无反射等),否则默认值没有任何意义。你想做什么,为什么你认为默认参数是一个好方法?
  • 我发现您的问题非常有趣,但您确实应该添加您尝试过的代码示例,以及调用 SetConstant 时遇到的异常。好的问题往往会吸引很多好的答案,我个人认为这是一个非常有趣的问题。
  • 如果其中一个答案确实回答了您的问题,请考虑接受以下任何一个答案。这将为下一个访问这个 oage 的人节省时间

标签: c# dynamic reflection reflection.emit


【解决方案1】:

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

生成的 IL 与 C# 编译器生成的不完全相同(因为缺少参数初始化),我只能猜测其他 .NET 语言会如何处理这样的声明,但它确实节省了一些非常难看的东西在System.Reflection.Emit 的内部卑躬屈膝(并且生成的 IL 通过验证——运行时本身对默认声明不做任何事情)。

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

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

【讨论】:

    【解决方案2】:

    这非常困难,需要大量使用反射来解决 .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");
    

    以这种方式设置默认值不会更新堆栈大小,这意味着您必须在获得 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# 编译成的内容相同。

    【讨论】:

      猜你喜欢
      • 2011-01-11
      • 2017-09-09
      • 2019-05-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多