替换声明类型的C#方法,该方法实现接口并从基继承

替换声明类型的C#方法,该方法实现接口并从基继承,c#,unit-testing,pointers,virtual,unsafe,C#,Unit Testing,Pointers,Virtual,Unsafe,为了单元测试遗留代码,我试图在运行时交换方法的内容。我一直在研究这些答案 这是我到目前为止所拥有的完整代码示例 using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; namespace Foo.Bar { public interface IFoo { string Apple(); } pub

为了单元测试遗留代码,我试图在运行时交换方法的内容。我一直在研究这些答案

  • 这是我到目前为止所拥有的完整代码示例

    using System;
    using System.Diagnostics;
    using System.Reflection;
    using System.Runtime.CompilerServices;
    
    namespace Foo.Bar {
    
        public interface IFoo {
            string Apple();
        }
    
        public class Bar {
    
            protected virtual object One() {
                return null;
            }
    
            protected virtual object Two() {
                return null;
            }
    
            protected virtual object Three() {
                return null;
            }
    
            /* Uncomment this to generate a null reference */
            //protected virtual object Four() {
            //    return null;
            //}
    
        }
    
        public class Foo : Bar, IFoo {
    
            public string Apple() {
                return "Apple";
            }
    
            public string Orange() {
                return "Orange";
            }
    
            /* Uncommenting this fixes the null reference */
            //public override int GetHashCode() {
            //    throw new NotImplementedException();
            //}
    
            public void ReplaceMethod(Delegate targetMethod, Delegate replacementMethod) {
    
                MethodInfo methodToReplace = targetMethod.Method;
                MethodInfo methodToInject = replacementMethod.Method;
    
                RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
                RuntimeHelpers.PrepareMethod(methodToInject.MethodHandle);
    
                if (methodToReplace.IsVirtual)
                    ReplaceVirtualInner(methodToReplace, methodToInject);
                else
                    ReplaceStandard(methodToReplace, methodToInject);
    
            }
    
            private void ReplaceStandard(MethodInfo methodToReplace, MethodInfo methodToInject) {
    
                IntPtr targetPtr = methodToInject.MethodHandle.Value;
                IntPtr replacePtr = methodToReplace.MethodHandle.Value;
    
                unsafe
                {
                    if (IntPtr.Size == 4) {
    
                        int* inj = (int*)replacePtr.ToPointer() + 2;
                        int* tar = (int*)targetPtr.ToPointer() + 2;
    
                        if (Debugger.IsAttached) {
    
                            byte* injInst = (byte*)*inj;
                            byte* tarInst = (byte*)*tar;
    
                            int* injSrc = (int*)(injInst + 1);
                            int* tarSrc = (int*)(tarInst + 1);
    
                            *tarSrc = (((int)injInst + 5) + *injSrc) - ((int)tarInst + 5);
                        }
                        else {
                            *tar = *inj;
                        }
                    }
                    else {
    
                        long* inj = (long*)replacePtr.ToPointer() + 1;
                        long* tar = (long*)targetPtr.ToPointer() + 1;
    
                        if (Debugger.IsAttached) {
    
                            byte* injInst = (byte*)*inj;
                            byte* tarInst = (byte*)*tar;
    
                            long* injSrc = (long*)(injInst + 1);
                            long* tarSrc = (long*)(tarInst + 1);
    
                            *tarSrc = (((long)injInst + 5) + *injSrc) - ((long)tarInst + 5);
                        }
                        else {
                            *tar = *inj;
                        }
                    }
                }
    
    
            }
    
            private void ReplaceVirtualInner(MethodInfo methodToReplace, MethodInfo methodToInject) {
    
                unsafe
                {
                    UInt64* methodDesc = (UInt64*)(methodToReplace.MethodHandle.Value.ToPointer());
                    int index = (int)(((*methodDesc) >> 32) & 0xFF);
    
                    if (IntPtr.Size == 4) {
                        uint* classStart = (uint*)methodToReplace.DeclaringType.TypeHandle.Value.ToPointer();
                        classStart += 10;
                        classStart = (uint*)*classStart;
    
                        uint* tar = classStart + index;
                        uint* inj = (uint*)methodToInject.MethodHandle.Value.ToPointer() + 2;
    
                        if (Debugger.IsAttached) {
    
                            byte* injInst = (byte*)*inj;
                            byte* tarInst = (byte*)*tar;
    
                            uint* injSrc = (uint*)(injInst + 1);
                            uint* tarSrc = (uint*)(tarInst + 1);
    
                            *tarSrc = (((uint)injInst + 5) + *injSrc) - ((uint)tarInst + 5);
                        }
                        else {
                            *tar = *inj;
                        }
    
                    }
                    else {
    
                        ulong* classStart = (ulong*)methodToReplace.DeclaringType.TypeHandle.Value.ToPointer();
                        classStart += 8;
                        classStart = (ulong*)*classStart;
    
                        ulong* tar = classStart + index;
                        ulong* inj = (ulong*)methodToInject.MethodHandle.Value.ToPointer() + 1;
    
                        if (Debugger.IsAttached) {
    
                            byte* injInst = (byte*)*inj;
                            byte* tarInst = (byte*)*tar;
    
                            ulong* injSrc = (ulong*)(injInst + 1);
                            ulong* tarSrc = (ulong*)(tarInst + 1);
    
                            *tarSrc = (((ulong)injInst + 5) + *injSrc) - ((ulong)tarInst + 5);
                        }
                        else {
                            *tar = *inj;
                        }
    
                    }
    
                }
            }
    
        }
    
    }
    
    用法

        Foo.Bar.Foo foo = new Foo.Bar.Foo();
    
        foo.ReplaceMethod(
            ((Func<string>)foo.Apple),
            ((Func<string>)foo.Orange)
        );
    
        var result = foo.Apple(); // this is "Orange" :)
    
    代码是有效的,然而,这里是事情变得奇怪的地方;如果基类(目标方法的声明类型)有3个以上的虚拟方法,实现了一个接口并且没有覆盖任何方法,则会引发NullReferenceException


    请问,有人能解释一下发生了什么并帮助我对代码有更深入的理解吗?

    您所问的实际上是typemock单元测试框架中的一个功能,它允许您在一行简单的代码中将模拟方法的实现更改为另一个,例如:

    [TestMethod]
    public void TestMethod1()
    {
        var real = new Foo();
    
        Isolate.WhenCalled(() => real.Apple()).DoInstead(x => { return real.Orange(); });
    
        Assert.AreEqual("Orange", real.Apple());
    }
    

    您可以了解有关此功能的更多信息。

    因为您是为了单元测试而这样做的,所以我将研究Microsoft Fakes框架。您也可以这样做,而无需操作指针。此外,AFAIC您要替换的方法的大小必须与您要替换的方法的内存大小相匹配。@zaitsman这看起来非常有用,不幸的是,它似乎需要Visual Studio Enterprise的时间才能取出乞讨碗
    [TestMethod]
    public void TestMethod1()
    {
        var real = new Foo();
    
        Isolate.WhenCalled(() => real.Apple()).DoInstead(x => { return real.Orange(); });
    
        Assert.AreEqual("Orange", real.Apple());
    }