13

见代码片段

public interface I0
{
    void f0();
}
public struct S0:I0
{
    void I0.f0()
    {

    }
}
public class A<E> where E :I0
{
    public E e;
    public void call()
    {
        e.f0();
    }
}

这是 call() 的 IL 代码

.maxstack 8
L_0000: ldarg.0 
L_0001: ldflda !0 Temp.A`1<!E>::e
L_0006: constrained !E
L_000c: callvirt instance void Temp.I0::f0()
L_0011: ret 

请参阅受约束的参考

约束前缀也可以用于调用值类型的接口方法,因为实现接口方法的值类型方法可以使用 MethodImpl 进行更改。如果不使用约束前缀,编译器将被迫在编译时选择绑定到哪个值类型的方法。使用 constrained 前缀允许 MSIL 在运行时而不是在编译时绑定到实现接口方法的方法。

这意味着它将调用一个包含 f0 接口方法代码的方法,而无需对结构进行装箱。

在 C# 中是否存在与上述 GenericClass 一样的其他没有装箱的接口方法?

4

2 回答 2

18

这取决于...您特别说您不想要泛型...唯一的其他选择是非泛型类中的泛型方法。你可以让编译器发出调用的唯一其他时间是如果你调用,或(from ) on a ,因为那些是 then - 如果有 an它们将是;如果它没有,它们将是。这就是为什么您应该始终使用这 3 个;p 但我离题了。一个简单的例子是具有一些静态方法的实用程序类 -扩展constrainedToString()GetHashCode()Equals()objectstructconstrainedstructoverridecalloverridecallvirtoverridestruct方法将是一个理想的示例,因为您还可以获得编译器将在公共/隐式 API 和扩展/显式 API 之间自动切换的优势,而您无需更改代码。例如,以下(显示隐式和显式实现)没有装箱,只有一个call和一个constrained+ callvirt,它将通过callJIT 实现:

using System;
interface IFoo
{
    void Bar();
}
struct ExplicitImpl : IFoo
{
    void IFoo.Bar() { Console.WriteLine("ExplicitImpl"); }
}
struct ImplicitImpl : IFoo
{
    public void Bar() {Console.WriteLine("ImplicitImpl");}
}
static class FooExtensions
{
    public static void Bar<T>(this T foo) where T : IFoo
    {
        foo.Bar();
    }
}
static class Program
{
    static void Main()
    {
        var expl = new ExplicitImpl();
        expl.Bar(); // via extension method
        var impl = new ImplicitImpl();
        impl.Bar(); // direct
    }
}

这是 IL 的关键部分:

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 1
    .locals init (
        [0] valuetype ExplicitImpl expl,
        [1] valuetype ImplicitImpl impl)
    L_0000: ldloca.s expl
    L_0002: initobj ExplicitImpl
    L_0008: ldloc.0 
    L_0009: call void FooExtensions::Bar<valuetype ExplicitImpl>(!!0)
    L_000e: ldloca.s impl
    L_0010: initobj ImplicitImpl
    L_0016: ldloca.s impl
    L_0018: call instance void ImplicitImpl::Bar()
    L_001d: ret 
}
.method public hidebysig static void Bar<(IFoo) T>(!!T foo) cil managed
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor()
    .maxstack 8
    L_0000: ldarga.s foo
    L_0002: constrained. !!T
    L_0008: callvirt instance void IFoo::Bar()
    L_000d: ret 
}

然而,扩展方法的一个缺点是它在堆栈上做一个额外的副本struct参见反正)。如果是这种情况,参数会很有帮助,但请注意扩展方法不能有参数 - 所以你不能使用扩展方法来做到这一点。但请考虑:ldloc.0refref this

Bar(ref expl);
Bar(ref impl);

和:

static void Bar<T>(ref T foo) where T : IFoo
{
    foo.Bar();
}

这是:

L_001d: ldloca.s expl
L_001f: call void Program::Bar<valuetype ExplicitImpl>(!!0&)
L_0024: ldloca.s impl
L_0026: call void Program::Bar<valuetype ImplicitImpl>(!!0&)

和:

.method private hidebysig static void Bar<(IFoo) T>(!!T& foo) cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: constrained. !!T
    L_0007: callvirt instance void IFoo::Bar()
    L_000c: ret 
}

仍然没有装箱,但现在我们也从不复制结构,即使是显式情况。

于 2012-11-26T08:17:54.760 回答
0

由于接口被视为引用类型,因此没有办法在接口引用的结构上调用方法,而不必先对底层结构进行装箱。

当使用强制类型的泛型方法来实现接口时,C# 编译器简单地将实际的实现细节和调用约定提升到运行时。幸运的是,C# 编译器足够聪明,可以指示 JIT 编译器,该类型确实实现了接口 X,并且可能是一个结构。有了这些信息,JIT 编译器可以计算出如何调用接口 X 声明的方法 Y。

上述技巧不适用于非泛型方法调用,因为当 X 是非密封类或接口时,JIT 编译器无法确定参数 X 表示的实际类型。因此,如果传递的接口表示的类型是非密封类和处理结构和密封类的直接方法调用,C# 编译器无法生成处理查找表的 JIT。

理论上,当使用动态时,您可以防止装箱,但是引入 DLR 的性能损失可能根本不会产生任何好处。

于 2012-11-26T06:28:46.633 回答