检查一个构造函数是否调用另一个构造函数

问题描述 投票:0回答:5

在反射期间,C# 中是否可以检查一个构造函数是否调用另一个构造函数?

class Test
{
    public Test() : this( false ) { }
    public Test( bool inner ) { }    
}

我想确定每个

ConstructorInfo
是否位于调用链的末尾。

c# reflection constructor cil constructor-chaining
5个回答
3
投票

这是一个临时答案,说明我到目前为止发现的内容。

我没有找到

ConstructorInfo
的任何属性可以指示构造函数是否调用另一个构造函数。
MethodBody
的属性也没有。

我在评估 MSIL 字节代码方面取得了一些成功。我的第一个发现表明,最终调用的构造函数立即以

OpCodes.Call
开头,除了一些可能的其他
OpCodes
之外。调用其他构造函数的构造函数有“意外”
OpCodes

public static bool CallsOtherConstructor( this ConstructorInfo constructor )
{
    MethodBody body = constructor.GetMethodBody();
    if ( body == null )
    {
        throw new ArgumentException( "Constructors are expected to always contain byte code." );
    }

    // Constructors at the end of the invocation chain start with 'call' immediately.
    var untilCall = body.GetILAsByteArray().TakeWhile( b => b != OpCodes.Call.Value );
    return !untilCall.All( b =>
        b == OpCodes.Nop.Value ||     // Never encountered, but my intuition tells me a no-op would be valid.
        b == OpCodes.Ldarg_0.Value || // Seems to always precede Call immediately.
        b == OpCodes.Ldarg_1.Value    // Seems to be added when calling base constructor.
        );
}

我对 MSIL 完全不确定。也许中间不可能有无操作,或者根本不需要启动这样的构造函数,但对于我当前的所有单元测试来说,它似乎有效。

[TestClass]
public class ConstructorInfoExtensionsTest
{
    class PublicConstructors
    {
        // First
        public PublicConstructors() : this( true ) {}

        // Second
        public PublicConstructors( bool one ) : this( true, true ) {}

        // Final
        public PublicConstructors( bool one, bool two ) {}

        // Alternate final
        public PublicConstructors( bool one, bool two, bool three ) {}
    }

    class PrivateConstructors
    {
        // First
        PrivateConstructors() : this( true ) {}

        // Second
        PrivateConstructors( bool one ) : this( true, true ) {}

        // Final
        PrivateConstructors( bool one, bool two ) {}

        // Alternate final
        PrivateConstructors( bool one, bool two, bool three ) {}
    }

    class TripleBaseConstructors : DoubleBaseConstructors
    {
        public TripleBaseConstructors() : base() { }
        public TripleBaseConstructors( bool one ) : base( one ) { }
    }

    class DoubleBaseConstructors : BaseConstructors
    {
        public DoubleBaseConstructors() : base() {}
        public DoubleBaseConstructors( bool one ) : base( one ) {}
    }

    class BaseConstructors : Base
    {
        public BaseConstructors() : base() {}
        public BaseConstructors( bool one ) : base( one ) {}
    }

    class Base
    {
        // No parameters
        public Base() {}

        // One parameter
        public Base( bool one ) {} 
    }

    class ContentConstructor
    {
        public ContentConstructor()
        {
            SomeMethod();
        }

        public ContentConstructor( bool one )
        {
            int bleh = 0;
        }

        bool setTwo;
        public ContentConstructor( bool one, bool two )
        {
            setTwo = two;
        }

        void SomeMethod() {}
    }

    [TestMethod]
    public void CallsOtherConstructorTest()
    {           
        Action<ConstructorInfo[]> checkConstructors = cs =>
        {
            ConstructorInfo first = cs.Where( c => c.GetParameters().Count() == 0 ).First();
            Assert.IsTrue( first.CallsOtherConstructor() );
            ConstructorInfo second = cs.Where( c => c.GetParameters().Count() == 1 ).First();
            Assert.IsTrue( second.CallsOtherConstructor() );
            ConstructorInfo final = cs.Where( c => c.GetParameters().Count() == 2 ).First();
            Assert.IsFalse( final.CallsOtherConstructor() );
            ConstructorInfo alternateFinal = cs.Where( c => c.GetParameters().Count() == 3 ).First();
            Assert.IsFalse( alternateFinal.CallsOtherConstructor() );
        };

        // Public and private constructors.
        checkConstructors( typeof( PublicConstructors ).GetConstructors() );
        checkConstructors( typeof( PrivateConstructors ).GetConstructors( BindingFlags.NonPublic | BindingFlags.Instance ) );

        // Inheritance.
        Action<ConstructorInfo[]> checkBaseConstructors = cs =>
        {
            ConstructorInfo noParameters = cs.Where( c => c.GetParameters().Count() == 0 ).First();
            ConstructorInfo oneParameter = cs.Where( c => c.GetParameters().Count() == 1 ).First();

            // Only interested in constructors specified on this type, not base constructors,
            // thus calling a base constructor shouldn't qualify as 'true'.
            Assert.IsFalse( noParameters.CallsOtherConstructor() );
            Assert.IsFalse( oneParameter.CallsOtherConstructor() );
        };
        checkBaseConstructors( typeof( BaseConstructors ).GetConstructors() );
        checkBaseConstructors( typeof( DoubleBaseConstructors ).GetConstructors() );
        checkBaseConstructors( typeof( TripleBaseConstructors ).GetConstructors() );

        // Constructor with content.
        foreach( var constructor in typeof( ContentConstructor ).GetConstructors() )
        {
            Assert.IsFalse( constructor.CallsOtherConstructor() );
        }               
    }
}

1
投票

考虑看看CecilRoslyn

Cecil 对已编译的程序集进行操作,就像反射一样。它具有构建在其之上的更高级别的库,以支持 SharpDevelop IDE 中的重构,因此它可能有一些东西可以使这变得更容易。

Roslyn 对源代码进行操作,并为您提供基于该源代码的对象模型,因此,如果您愿意针对源代码而不是二进制文件进行工作,那么使用它可能会更容易。

(我从来没有真正使用过 Cecil 来做这样的事情,我也从来没有使用过 Roslyn,所以我只能向你指出这些项目并祝你好运。如果你确实设法让某些东西发挥作用,我很想听听进展如何!)


1
投票

您可以做的是向对象添加一个属性,告诉该方面已应用。因此,您不会多次应用该方面,因为您可以检查该属性。这不是您所要求的,但它可能会帮助您解决根本问题。


0
投票

据我所知,您无法使用反射以简单的方式检查或检查代码。所有反射让您做的就是反映程序集的元数据信息。

您可以使用 GetMethodBody 来获取方法的内容,但随后您必须自己实际解析它并理解 IL。


0
投票

我写了一个MSIL分析代码来检查它是否调用

this

代码检测构造函数内调用的所有

MethodBase
对象。 方法
IsCallingOtherConstructor
检查是否在检测到的
MethodBase
对象中找到任何父类构造函数。

static bool IsCallingOtherConstructor(ConstructorInfo constructorInfo)
{
    var instructions = Instruction.GetList(constructorInfo, new[] { OperandType.InlineMethod });
    var baseType = constructorInfo.DeclaringType?.BaseType;
    if (baseType is null)
        return false;
    else
    {
        var baseConstructors = baseType.GetConstructors();
        return !instructions.Any(x => baseConstructors.Any(y => Equals(x.Operand, y)));
    }
}

这里是MSIL分析代码。

using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
    
public class Instruction
{
    private sealed class ByteBuffer
    {
        public int position;

        public ByteBuffer(byte[] buffer)
        {
            Buffer = buffer;
        }

        public byte ReadByte()
        {
            CheckCanRead(1);
            return Buffer[position++];
        }

        private byte[] ReadBytes(int length)
        {
            CheckCanRead(length);
            var bytes = new byte[length];
            System.Buffer.BlockCopy(Buffer, position, bytes, 0, length);
            position += length;
            return bytes;
        }

        public short ReadInt16()
        {
            CheckCanRead(2);
            var @short = (short)(Buffer[position]
                                  + (Buffer[position + 1] << 8));
            position += 2;
            return @short;
        }

        public int ReadInt32()
        {
            CheckCanRead(4);
            var @int = Buffer[position]
                       + (Buffer[position + 1] << 8)
                       + (Buffer[position + 2] << 16)
                       + (Buffer[position + 3] << 24);
            position += 4;
            return @int;
        }

        public long ReadInt64()
        {
            CheckCanRead(8);
            long @long = Buffer[position]
                         + (Buffer[position + 1] << 8)
                         + (Buffer[position + 2] << 16)
                         + (Buffer[position + 3] << 24)
                         + (Buffer[position + 4] << 32)
                         + (Buffer[position + 5] << 40)
                         + (Buffer[position + 6] << 48)
                         + (Buffer[position + 7] << 56);
            position += 8;
            return @long;
        }

        public float ReadSingle()
        {
            if (!BitConverter.IsLittleEndian)
            {
                var bytes = ReadBytes(4);
                Array.Reverse(bytes);
                return BitConverter.ToSingle(bytes, 0);
            }
            else
            {
                CheckCanRead(4);
                var value = BitConverter.ToSingle(Buffer, position);
                position += 4;
                return value;
            }
        }

        public double ReadDouble()
        {
            if (!BitConverter.IsLittleEndian)
            {
                var bytes = ReadBytes(8);
                Array.Reverse(bytes);
                return BitConverter.ToDouble(bytes, 0);
            }
            else
            {
                CheckCanRead(8);
                var value = BitConverter.ToDouble(Buffer, position);
                position += 8;
                return value;
            }
        }

        public int[] ReadBranches()
        {
            var length = ReadInt32();
            var branches = new int[length];
            var offsets = new int[length];
            for (var i = 0; i < length; i++)
                offsets[i] = ReadInt32();
            for (var i = 0; i < length; i++)
                branches[i] = position + offsets[i];

            return branches;
        }

        private void CheckCanRead(int count)
        {
            if (position + count > Buffer.Length)
                throw new ArgumentOutOfRangeException();
        }

        public void MoveByte() => position++;

        private void MoveBytes(int length) => position += length;

        public void MoveInt16() => position += 2;

        public void MoveInt32() => position += 4;

        public void MoveInt64() => position += 8;

        public void MoveSingle()
        {
            if (!BitConverter.IsLittleEndian)
                MoveBytes(4);
            else
                position += 4;

        }

        public void MoveDouble()
        {
            if (!BitConverter.IsLittleEndian)
                MoveBytes(8);
            else
                position += 8;
        }

        public void MoveBranches()
        {
            var length = ReadInt32();
            for (var i = 0; i < length; i++)
                MoveInt32();
        }

        public byte[] Buffer { get; }
    }

    public static readonly OpCode[] OneByteOpcodes;

    public static readonly OpCode[] TwoBytesOpcodes;

    static Instruction()
    {
        var oneByteOpcodes = new OpCode[0xe1];
        var twoBytesOpcodes = new OpCode[0x1f];
        var opCodeFields = typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static);

        foreach (var field in opCodeFields)
        {
            var opcode = (OpCode)field.GetValue(null)!;
            if (opcode.OpCodeType == OpCodeType.Nternal)
                continue;

            if (opcode.Size == 1)
                oneByteOpcodes[opcode.Value] = opcode;
            else
                twoBytesOpcodes[opcode.Value & 0xff] = opcode;
        }

        OneByteOpcodes = oneByteOpcodes;
        TwoBytesOpcodes = twoBytesOpcodes;
    }

    public static IList<Instruction> GetList(MethodBase methodBase, [Optional] OperandType[]? targetOperandTypes)
    {
        static bool TryGetByteBuffer(MethodBase method, [NotNullWhen(true)] out IList<LocalVariableInfo>? localVariableInfos, [NotNullWhen(true)] out ByteBuffer? byteBuffer)
        {
            var body = method.GetMethodBody();
            if (body is not null)
            {
                var bytes = body.GetILAsByteArray();
                if (bytes != null)
                {
                    localVariableInfos = body.LocalVariables;
                    byteBuffer = new ByteBuffer(bytes);
                    return true;
                }
            }

            localVariableInfos = null;
            byteBuffer = null;
            return false;
        }

        var body = methodBase.GetMethodBody();
        if (!TryGetByteBuffer(methodBase, out var locals, out var byteBuffer))
            return new List<Instruction>();

        var methodGenericArguments = methodBase is not ConstructorInfo ? methodBase.GetGenericArguments() : default;
        var typeGenericArguments = methodBase.DeclaringType is not null ? methodBase.DeclaringType.GetGenericArguments() : default;

        var parameters = methodBase.GetParameters();
        var module = methodBase.Module;

        object GetVariable(OpCode opCode, int index)
        {
            if (opCode.Name is not null && opCode.Name.Contains("loc"))
                return locals[index];
            else
            {
                if (!methodBase.IsStatic)
                    index--;

                return parameters[index];
            }
        }

        object? GetOperand(OpCode opCode, ByteBuffer byteBuffer)
            => opCode.OperandType switch
            {
                OperandType.InlineNone => null,
                OperandType.InlineSwitch => byteBuffer.ReadBranches(),
                OperandType.ShortInlineBrTarget => byteBuffer.position - (sbyte)byteBuffer.ReadByte(),
                OperandType.InlineBrTarget => byteBuffer.position - byteBuffer.ReadInt32(),
                OperandType.ShortInlineI => opCode == OpCodes.Ldc_I4_S ? (sbyte)byteBuffer.ReadByte() : byteBuffer.ReadByte(),
                OperandType.InlineI => byteBuffer.ReadInt32(),
                OperandType.ShortInlineR => byteBuffer.ReadSingle(),
                OperandType.InlineR => byteBuffer.ReadDouble(),
                OperandType.InlineI8 => byteBuffer.ReadInt64(),
                OperandType.InlineSig => module.ResolveSignature(byteBuffer.ReadInt32()),
                OperandType.InlineString => module.ResolveString(byteBuffer.ReadInt32()),
                OperandType.InlineTok => module.ResolveMember(byteBuffer.ReadInt32(), typeGenericArguments, methodGenericArguments),
                OperandType.InlineType => module.ResolveType(byteBuffer.ReadInt32(), typeGenericArguments, methodGenericArguments),
                OperandType.InlineMethod => module.ResolveMethod(byteBuffer.ReadInt32(), typeGenericArguments, methodGenericArguments),
                OperandType.InlineField => module.ResolveField(byteBuffer.ReadInt32(), typeGenericArguments, methodGenericArguments),
                OperandType.ShortInlineVar => GetVariable(opCode, byteBuffer.ReadByte()),
                OperandType.InlineVar => GetVariable(opCode, byteBuffer.ReadInt16()),
                _ => throw new NotSupportedException(),
            };

        static void Move(OpCode opCode, ByteBuffer byteBuffer)
        {
            switch (opCode.OperandType)
            {
                case OperandType.InlineNone:
                    break;
                case OperandType.ShortInlineBrTarget:
                case OperandType.ShortInlineVar:
                case OperandType.ShortInlineI:
                    byteBuffer.MoveByte();
                    break;
                case OperandType.InlineVar:
                    byteBuffer.MoveInt16();
                    break;
                case OperandType.InlineBrTarget:
                case OperandType.InlineI:
                case OperandType.InlineSig:
                case OperandType.InlineString:
                case OperandType.InlineTok:
                case OperandType.InlineMethod:
                case OperandType.InlineType:
                case OperandType.InlineField:
                    byteBuffer.MoveInt32();
                    break;
                case OperandType.InlineI8:
                    byteBuffer.MoveInt64();
                    break;
                case OperandType.ShortInlineR:
                    byteBuffer.MoveSingle();
                    break;
                case OperandType.InlineR:
                    byteBuffer.MoveDouble();
                    break;
                case OperandType.InlineSwitch:
                    byteBuffer.MoveBranches();
                    break;
                default:
                    throw new NotSupportedException();
            }
        }

        var result = new List<Instruction>(byteBuffer.Buffer.Length / 3);
        var previous = default(Instruction);
        while (byteBuffer.position < byteBuffer.Buffer.Length)
        {
            var ilOpCode = byteBuffer.ReadByte();
            var opCode = ilOpCode != 0xfe ? OneByteOpcodes[ilOpCode] : TwoBytesOpcodes[byteBuffer.ReadByte()];
            object? operand;
            if (targetOperandTypes is null || targetOperandTypes.Any(x => Equals(x, opCode.OperandType)))
                operand = GetOperand(opCode, byteBuffer);
            else
            {
                operand = null;
                Move(opCode, byteBuffer);
            }
            var instruction = new Instruction(byteBuffer.position, opCode, operand);

            if (previous != null)
            {
                instruction.Previous = previous;
                previous.Next = instruction;
            }

            result.Add(previous = instruction);
        }

        return result.AsReadOnly();
    }

    private Instruction? previous;
    private Instruction? next;

    public Instruction(int offset, OpCode opCode, object? operand)
    {
        Offset = offset;
        OpCode = opCode;
        Operand = operand;
    }

    public int Offset { get; }

    public OpCode OpCode { get; }

    public object? Operand { get; }

    public Instruction Previous { get => previous ?? throw new NullReferenceException(nameof(previous)); set => previous = value; }

    public Instruction Next { get => next ?? throw new NullReferenceException(nameof(next)); set => next = value; }
}
© www.soinside.com 2019 - 2024. All rights reserved.