为啥 Emit 中具有显式重载的接口实现对于公共和非公共的表现不同?

Posted

技术标签:

【中文标题】为啥 Emit 中具有显式重载的接口实现对于公共和非公共的表现不同?【英文标题】:Why does interface implementation in Emit with explicit overload behave different for public and non-public?为什么 Emit 中具有显式重载的接口实现对于公共和非公共的表现不同? 【发布时间】:2013-02-03 12:40:55 【问题描述】:

我已经使用 Reflection.Emit 很长时间了,但这次它没有任何意义......在我的程序的某个地方,我正在使用 emit 实现接口。例如:

typeBuilder.AddInterfaceImplementation(intf);

因为我正在实现多个接口并且接口可以从其他接口继承,所以我对方法/接口进行了去重。 (虽然这里不相关,但我将在我的示例中使用一些众所周知的接口)。例如,如果我同时实现 IList 和 IDictionary,它们都实现了 ICollection,而我只实现 ICollection 一次。

之后,我开始使用生成的方法和接口列表向 typeBuilder 添加方法。没什么特别的,只是:

MethodBuilder mb = typeBuilder.DefineMethod(
    name,
    MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual |
    MethodAttributes.Final | specialAttributes, CallingConventions.HasThis,
    returnType,
    parameterTypes);

// [...] emit code that doesn't really matter here

typeBuilder.DefineMethodOverride(mb, baseMethodFromInterface);

请注意,我明确定义了方法覆盖。我这样做是因为名字可能会发生冲突,f.ex。在上面的示例中,IList 和 ICollection 都公开了一个 Count getter (name = get_Count),这将导致名称冲突。

现在假设我在生成方法时使用了名称“Count”。正如我之前注意到的,有几个从 IList 派生的接口实现了这个属性。我感到困惑的是,显然现在“Count”也隐式地继承自其他 Count 方法——即使我没有将其定义为 Override ......但前提是我将属性公开为公共。 (例如,specialAttributes = MethodAttributes.Public)。发生的情况是 PEVerify 会报错,但代码会运行得很好:

[IL]: Error: [c:\tmp\emit\Test.dll : Test::get_Count][offset 0x00000012] Method is not visible.

为了解决这个问题,我尝试更改 specialAttributes = MethodAttributes.Private - 但由于一个小错误,我没有显式地实现所有 Count 事物(使用 DefineMethodOverride)。奇怪的是,CreateType 现在告诉我“Count [...] 没有实现”。 - 例如它找不到充当覆盖的 Count 方法。

但是,由于我使用的是 DefineMethodOverride,我想知道为什么它首先会起作用?换句话说:“私人”错误是有道理的,它在使用公共方法时起作用的事实在 IMO 中不起作用。

所以对于我的问题:即使您显式将覆盖定义为另一个方法的覆盖,为什么 .NET 会隐式覆盖具有相同名称的公共方法(这听起来像是 .网...)?为什么这行得通?当您将方法公开为公共时,为什么 PEVerify 会给出错误?

更新

显然 PEVerify 错误是无关的:在将所有内容设为私有并显式实现所有方法之后,PEVerify 仍然给出相同的错误。该错误与调用错误的方法有关,例如:

// Incorrect: attempt to call a private member -> PEVerify error
callvirt instance void [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::System.Collections.IDictionary.Remove(object)

// Correct: call the interface using a vtable lookup
callvirt instance void [mscorlib]System.Collections.IList::Remove(object)

不过,这只是一个支线,问题仍然存在。

更新 +1

我制作了我认为最小的测试用例。这基本上会生成一个 DLL,您应该使用您喜欢的工具对其进行检查。请注意,我在这里只实现 1 个方法,而不是 2 个(!)第二个方法被“自动”覆盖,即使我明确告诉 .NET 该方法实现了第一个方法。

public interface IFoo

    int First();
    int Second();


public class FooGenerator

    static void Main(string[] args)
    
        CreateClass();
    

    public static void CreateClass()
    
        // Create assembly
        var assemblyName = new AssemblyName("test_emit.dll");
        var assemblyBuilder =
            AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName,
            AssemblyBuilderAccess.RunAndSave, @"c:\tmp");

        // Create module
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("test_emit", "test_emit.dll", false);

        // Create type : IFoo
        var typeBuilder = moduleBuilder.DefineType("TestClass", TypeAttributes.Public);
        typeBuilder.AddInterfaceImplementation(typeof(IFoo));

        ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(
            MethodAttributes.Public | MethodAttributes.HideBySig |
            MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
            CallingConventions.HasThis, Type.EmptyTypes);

        // Generate the constructor IL. 
        ILGenerator gen = constructorBuilder.GetILGenerator();

        // The constructor calls the constructor of Object
        gen.Emit(OpCodes.Ldarg_0);
        gen.Emit(OpCodes.Call, typeof(object).GetConstructor(
            BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, Type.DefaultBinder, Type.EmptyTypes, null));
        gen.Emit(OpCodes.Ret);

        // Add the 'Second' method
        var mb = typeBuilder.DefineMethod("Second",
            MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual |
            MethodAttributes.Final | MethodAttributes.Public, CallingConventions.HasThis,
            typeof(int), Type.EmptyTypes);

        // Implement
        gen = mb.GetILGenerator();

        gen.Emit(OpCodes.Ldc_I4_1);
        gen.Emit(OpCodes.Ret);

        typeBuilder.DefineMethodOverride(mb, typeof(IFoo).GetMethod("First"));

        typeBuilder.CreateType();
        assemblyBuilder.Save("test_emit.dll");
    

【问题讨论】:

是您定义公共方法的行为,例如。在 C# 中编写相同的代码时,不计算 .net 的默认行为吗?如果是这样,那么您将需要分别覆盖 IList.Count 和 IDictionary.Count。我猜这样做,你不应该有名称冲突,因为 IDictionary 不继承 IList 所以实现会有所不同。 如果您在 C# 中执行此操作,则不会添加 DefineMethodOverride 。如果您单独实施它们,我看不到将它们公开的方法。尽管如此,还有其他语言需要考虑,其中一些可能能够覆盖方法并给它们一个不同的名称(这是有效的!) 如 kvb 所述,您将不得不使用 IList.Count 和 IDictionary.Count。您将方法本身定义为公共的,但您将指定 IList.Count 和 IDictionary.Count 的 methodInfo,它们是私有的。通过这样做,您将只能在将对象强制转换为 IList 时访问 IList 的 Count,因为覆盖是显式的而不是隐式的。 是的,我知道 C# 语法,但这可能不是一个正当的理由。另见msdn.microsoft.com/en-us/library/…:它明确声明“使用不同的名称”,没有限制。在 .NET 中还需要考虑 javascript、Python、Ruby、VB 和许多其他语言......如果这个约束不适用于所有可能的语言,我不会感到惊讶。 场景完全有效。您定义了第二种方法,并为第一种方法调用了 DefineOverride。第二个方法应该自动覆盖接口的第二个,第二个方法定义将用于第一个方法覆盖。因此,您在第二次发出的任何代码都将首先反映。这种情况仅是有效的,因为返回类型和参数类型是相同的。这不适用于返回类型和参数类型不同的场景 【参考方案1】:

如果您有多个具有相同名称和签名的方法,则您的类型无效。如果您想显式实现一个接口,那么通常您会使用具有不同名称的私有方法(例如,C# 编译器使用类似 IList.Count 而不是 Count)。

更新

在您更新后,我现在看到我的诊断有些倒退。如果一个类被声明为实现一个接口并且有一个没有匹配方法覆盖的接口方法,则 CLR 将搜索具有相同名称和签名的方法。没有办法告诉 CLR 不要这样做(除了为该接口方法提供不同的特定覆盖),并且相同的具体方法可以覆盖多个接口方法,设计。同样,将您的具体方法称为Third 并显式覆盖FirstSecond 也是非常好的。

更新 2

我会尝试回答您的其他问题,但“为什么”问题总是很难回答。首先,向接口添加方法是一个严重的重大变化,基本上永远不会发生 - 如果您向接口添加新方法,那么您会期望所有实现该接口的类(如果您的接口是公共的)将中断,因为他们声称实现了接口但缺少方法。

用一个具体的实现覆盖多个接口方法似乎没有什么缺点。通常,这些方法来自不同的接口,因此这将是一种避免创建多个相同实现的便捷方法(假设不同的接口方法具有相同的语义,因此通过单个方法覆盖它们是有意义的)。同样,CLR 通过名称+签名查找方法很方便,而不需要显式映射。您的案例基本上是这些更通用机制的一个非常奇怪的极端案例实例,其中来自同一接口的多个方法由一个插槽实现,一次通过显式覆盖,一次通过默认查找。这很奇怪,CLR 似乎不值得明确注意这种特殊情况,尤其是考虑到它不太可能(或不可能)从 C# 源代码生成。

【讨论】:

虽然您说得有道理,但我在回答这个问题时遇到了麻烦。这里要考虑的不仅仅是 C#……在 IL 中,在 IL 中给你的重载另一个名字是完全合法的,例如您可以将 IList.Count 命名为 -well- IsReadOnly 之类的其他名称,并使用 DefineMethodOverride 告诉 .NET 它是 IList.Count 的覆盖 :-) 如果有人决定向 IList (IsReadOnly) 添加另一个属性,您会期望代码不会编译 - 但实际上它会!如果没记错的话,你甚至可以在 IL 中定义多个具有相同名称和签名的方法,只要它们具有不同的返回类型。 我说的是 IL 级别,而不是 C#:一个类型中不能有两个具有相同名称和签名的方法,或者类型格式错误(并且返回类型计为签名的一部分) .您是对的,方法可以具有任意名称,并且仍然覆盖具有不同名称的接口方法。事实上,这就是我建议您需要实现的(将您的覆盖命名为 "IList.Count" 而不仅仅是 "Count")。 啊,是的,没错,我们同意这一点。这意味着我的问题是,如果我有 IList::Count 并使用显式 DefineMethodOverride 将其实现为 MyType::IsReadOnly,我不明白为什么它也会隐式覆盖 IList::IsReadOnly - 毕竟,那不是我指定的。 (假设它当然具有相同的签名)...所以显然DefineMethodOverride 只有 includes 覆盖,但不 exclude 所有其他人,我觉得这值得商榷行为,因为如果界面更改,这可能会破坏内容。那么为什么会这样呢? 说实话,如果没有重现,甚至很难理解你实际观察到的行为,更不用说你观察它的原因了。如果您可以针对所遇到的问题创建一个简单的自包含示例,将会非常有帮助。 我添加了一个最小的独立测试用例来显示发生了什么。如果不清楚,请告诉我。

以上是关于为啥 Emit 中具有显式重载的接口实现对于公共和非公共的表现不同?的主要内容,如果未能解决你的问题,请参考以下文章

为啥要显式实现接口?

为啥在具有多个接口() 的对象中实现 QueryInterface() 时我需要显式向上转换

为啥接口的显式实现不能公开?

C# 多接口继承不允许具有相同名称的公共访问修饰符

我怎样才能“显式”地快速实现一个协议?如果不可能,为啥?

为啥继承的受保护运算符=()具有公共访问权限