Hello World
Spiga

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

2013-03-24 21:45 by 老赵, 4524 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

15 条回复

  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不行吗?

  9. 175114
    60.207.51.*
    链接

    175114 2015-11-04 02:43:56

    挺好。http://www.175114.com/

  10. temple run 3
    123.27.109.*
    链接

    temple run 3 2020-08-12 11:10:13

    Your feedback helps me a lot, A very meaningful event, I hope everything will go well

  11. zoroseo2020
    35.215.172.*
    链接

    zoroseo2020 2021-08-06 20:34:38

    只要自己能够有非常好的发挥必然能够让自己可以在游戏中得到非常好的一种娱乐收益幸运飞艇。这时候,只有拥有属于自己的幸运飞艇开户账号之后

  12. brutut
    35.215.141.*
    链接

    brutut 2022-03-25 14:42:53

    纵使你天生就拿到一副好牌,也不能保证你人生的棋局会步步顺 畅,也未必能保证你在生活的博弈中稳操胜券。好的人生棋局,要靠 自己步步为营,努力去争取。 幸运飞艇走势图福彩双色球走势图幸运时时彩走势图

  13. entertainment news
    103.149.59.*
    链接

    entertainment news 2022-09-14 14:09:10

    I had the choice to discover impossible data from your blog articles.

  14. kericnnoe
    111.242.194.*
    链接

    kericnnoe 2022-12-21 04:07:02

    無論你是想賺錢、想認識妹子交女友魔龍傳奇遊戲都是可以幫你實現的

  15. amel
    35.215.165.*
    链接

    amel 2023-08-04 14:14:08

    八名吴士哈哈大笑,齐声高歌:“我剑利兮敌丧胆,我剑捷兮敌无首!” 忽听得咩咩羊叫,一个身穿浅绿福彩3D 幸运飞艇衫子的少女赶着十几头山羊,从长街东端走来。这群山羊来到吴士之前,便从他们身边绕过。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我