在反射期间,C# 中是否可以检查一个构造函数是否调用另一个构造函数?
class Test
{
public Test() : this( false ) { }
public Test( bool inner ) { }
}
ConstructorInfo
是否位于调用链的末尾。
这是一个临时答案,说明我到目前为止发现的内容。
我没有找到
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() );
}
}
}
您可以做的是向对象添加一个属性,告诉该方面已应用。因此,您不会多次应用该方面,因为您可以检查该属性。这不是您所要求的,但它可能会帮助您解决根本问题。
据我所知,您无法使用反射以简单的方式检查或检查代码。所有反射让您做的就是反映程序集的元数据信息。
您可以使用 GetMethodBody 来获取方法的内容,但随后您必须自己实际解析它并理解 IL。
我写了一个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; }
}