Hello World
Spiga

如何在不装箱的前提下调用“显式实现”的接口方法?

2013-03-24 21:45 by 老赵, 3022 visits

上篇文章谈了针对一个struct对象使用using语句的时候是否会装箱的问题,结论是“不会”。尽管using语句是针对IDisposable接口的,但我们在调用的时候其实已经明确了目标方法,因此根本不需要装箱(或查找虚函数表)了。后来有同学在微博上提出,这点在《CLR via C#》这本书里提到过,于是我去翻了翻,原来是这样的:

internal struct Point : IComparable {
    private readonly Int32 m_x, m_y;

    // Constructor to easily initialize the fields
    public Point(Int32 x, Int32 y) {
        m_x = x;
        m_y = y;
    }

    // Implementation of type-safe CompareTo method
    public Int32 CompareTo(Point other) {
        // Use the Pythagorean Theorem to calculate
        // which point is farther from the origin (0, 0)
        return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y));
    }

    public Int32 CompareTo(Object o) {
        if (GetType() != o.GetType()) {
            throw new ArgumentException("o is not a Point");
        }

        // Call type-safe CompareTo method
        return CompareTo((Point)o);
    }
}

public static class Program {
    public static void Main() {
        // Create two Point instances on the stack.
        Point p1 = new Point(10, 10);
        Point p2 = new Point(20, 20);

        // p1 does NOT get boxed to call CompareTo.
        // p2 does NOT get boxed because CompareTo(Point) is called.
        Console.WriteLine(p1.CompareTo(p2)); // "-1"

        // p1 DOES get boxed, and the reference is placed in c.
        IComparable c = p1;
        ...
    }
}

我参考的是其第四版电子版,这段代码出现在第5章,133页开始。那位同学说,你看这里在调用CompareTo方法的时候,书上写的很明白没有装箱嘛。我看了之后表示这跟前一篇谈的内容其实并没有太多联系。假如从IL上看,这次调用是这样的:

.method public hidebysig static 
    void Main () cil managed 
{
    // Method begins at RVA 0x26b8
    // Code size 36 (0x24)
    .maxstack 3
    .locals init (
        [0] valuetype Point p1,
        [1] valuetype Point p2
    )

    IL_0000: ldloca.s p1
    IL_0002: ldc.i4.s 10
    IL_0004: ldc.i4.s 10
    IL_0006: call instance void Point::.ctor(int32, int32)
    IL_000b: ldloca.s p2
    IL_000d: ldc.i4.s 20
    IL_000f: ldc.i4.s 20
    IL_0011: call instance void Point::.ctor(int32, int32)
    IL_0016: ldloca.s p1
    IL_0018: ldloc.1
    IL_0019: call instance int32 Point::CompareTo(valuetype Point)
    IL_001e: call void [mscorlib]System.Console::WriteLine(int32)
    IL_0023: ret
} // end of method Program::Main

请注意IL_0019,它明确地指出此次call指令的目标是Point类型的CompareTo,与IComparable接口没有任何关系。我们可以从Point定义中删除这个接口,这都不影响此次调用。而假如您返回上一篇文章,就会发现using语句生成的IL会查找IDisposable上的方法:

IL_0017: constrained. .DisposableStruct
IL_001d: callvirt instance void [mscorlib]System.IDisposable::Dispose()

我们都知道,把一个struct对象赋值到IDisposable引用上之后会产生装箱,这才是上一篇文章中“疑惑”的由来。假如我们只是要直接调用Dispose方法,自然就不会想这么多了。我又粗略翻了翻《CLR via C#》相关内容,发现它没有提到过这一点,看来我也算弥补一个空白了,表示略为开心。

不过话又说回来,假如有一个类型,它是这么实现的:

public struct DisposableStruct : IDisposable {
    void IDisposable.Dispose() { }
}

换句话说,这个struct并没有定义一个Dispose方法,而是显式实现了某个接口方法(不仅限于IDisposable接口)。在这个情况下,您有什么办法在C#里调用这个接口方法呢?当然,调用时不能在堆上产生任何对象。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

8 条回复

  1. straybird
    111.67.103.*
    链接

    straybird 2013-03-25 02:16:40

    struct DisposableStruct : IDisposable, IComparable<DisposableStruct>
    {
        private int value;
        public DisposableStruct(int i)
        {
            value = i;
        }
    
        void IDisposable.Dispose()
        {
            Console.WriteLine("Explicit Interface IDisposable : {0}" , value.ToString());
        }
    
        int IComparable<DisposableStruct>.CompareTo(DisposableStruct other)
        {
            Console.WriteLine("Explicit Interface IComparable : {0}, {1}" , value.ToString(), other.value.ToString());
            return value.CompareTo(other.value);
        }
    }
    

    不知道老师的题目是针对所有显式实现的接口,还是只针对 IDisposable 接口呢? 如果只针对 IDisposable 接口的话,用老师上一篇文章提到的 using 即可,显式实现与普通实现 IDisposable 的IL代码是一样的。 如果是针对所有显式实现接口的话就不懂了,求解答。

  2. 老赵
    admin
    链接

    老赵 2013-03-25 09:09:21

    @straybird

    完全不懂你这段代码是想说明什么……显然这里需要推广到任何接口的嘛,否则题目问的没意义……

  3. 崔维福
    218.249.50.*
    链接

    崔维福 2013-03-25 18:53:10

    用LinqPad写了一下

    void Main() {
        Call(new FooStruct ());
    }
    
    public static void Call<T>(T t) where T : struct, IFoo {
        t.DoSomething();
    }
    
    public interface IFoo {
        void DoSomething();
    }
    
    public struct FooStruct : IFoo {
        [MethodImpl(MethodImplOptions.NoInlining)]
        void IFoo.DoSomething() { }
    }
    

    生成的IL代码如下

    // Call:
    IL_0000:  ldarga.s    00 
    IL_0002:  constrained. 01 00 00 1B 
    IL_0008:  callvirt    UserQuery+IFoo.DoSomething
    IL_000D:  ret
    
  4. darklx
    114.83.134.*
    链接

    darklx 2013-03-30 18:54:29

    老赵,我想到了使用Emit动态生成调用Dispose方法的思路,配合泛型缓存、扩展方法,可以比较完美的实现调用显式实现的IDisposable接口

    这是实践的代码:

    public struct DisposableStruct : IDisposable
    {
        void IDisposable.Dispose()
        {
            Console.WriteLine("struct Dispose called");
        }
    }
    
    public static class DisposableStructManager<T> where T : struct, IDisposable
    {
        /// <summary>
        /// T为当前值类型
        /// </summary>
        readonly static System.Action<T> disposeDelegate;
    
        /// <summary>
        /// 编译发出T类型下的Dispose调用的委托方法,T可作为参数传入,做为this使用
        /// </summary>
        static DisposableStructManager()
        {
            System.Type structType = typeof(T);
            //查找Dispose方法
            MethodInfo disposeMethod = structType.GetMethod("System.IDisposable.Dispose", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
    
            Type[] methodParameters = new Type[] { structType };
            DynamicMethod method = new DynamicMethod("DynamicDispose", null, methodParameters, structType, false);
            ILGenerator ilGen = method.GetILGenerator();
            ilGen.DeclareLocal(structType);
            //disposeDelegate委托的第一个参数,即T对象的值
            ilGen.Emit(OpCodes.Ldarga_S, 0);
            ilGen.Emit(OpCodes.Constrained, structType);
            ilGen.Emit(OpCodes.Callvirt, disposeMethod);
            ilGen.Emit(OpCodes.Ret);
            disposeDelegate = (System.Action<T>)method.CreateDelegate(typeof(System.Action<T>), null);
        }
    
        public static void Dispose(T obj)
        {
            disposeDelegate(obj);
        }
    }
    
    
    public static class DisposableStructExtensions
    {
        /// <summary>
        /// 再加一个扩展方法
        /// </summary>
        public static void DisposeStruct<T>(this T obj) where T : struct, IDisposable
        {
            DisposableStructManager<T>.Dispose(obj);
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            DisposableStruct dispStruct = new DisposableStruct();
            dispStruct.DisposeStruct();
        }
    }
    
  5. darklx
    114.83.134.*
    链接

    darklx 2013-04-05 07:09:40

    相比我上面的,想到了有更好的更简单的做法。

    public static class DisposableUtility
    {
    
        /// <summary>
        /// 调用值类型显示实现的IDisposable.Dispose方法
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj"></param>
        public static void Dispose<T>(ref T obj) where T : struct, IDisposable
        {
            obj.Dispose();
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            DisposableStruct dispStructRef = new DisposableStruct();
            //采用ref传递参数,避免值类型被复制,如果在值类型的Dispose实现中有对值类型内部值类型变量的操作(如isDisposed=true),则可能发生意想不到的结果
            DisposableUtility.Dispose(ref dispStructRef);
        }
    }
    
  6. 老赵
    admin
    链接

    老赵 2013-04-05 10:02:30

    @darklx

    不错。

  7. 程序诗人
    192.193.132.*
    链接

    程序诗人 2013-04-10 16:13:32

    @darklx

    我完全没看懂你这么写的意义,能够赐教一下?

  8. StrongBird
    168.159.144.*
    链接

    StrongBird 2013-04-26 17:06:04

    @darklx

    这里不加ref不行吗?

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我