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

5
在C#中进行反射时,是否有可能检查一个构造函数是否调用另一个构造函数?
class Test
{
    public Test() : this( false ) { }
    public Test( bool inner ) { }    
}

我想要确定每个ConstructorInfo是否在调用链的末端。


为什么你需要这样做?调用另一个构造函数被编译为普通的方法调用,所以我认为你需要读取该方法的IL来实现这一点。 - svick
@svick 我正在使用 AspectJ,我想找到将调用以应用该方面的最终构造函数。 - Steven Jeuris
1
考虑查看CecilRoslyn。Cecil操作编译后的程序集,就像反射一样,但它在其顶部构建了更高级别的库以支持SharpDevelop IDE中的重构,因此它可能有一些东西可以使这更容易。Roslyn基于源代码运行,并为您提供基于该代码的对象模型,因此,如果您愿意针对源代码而不是二进制文件进行工作,则可能会更容易。 - Joe White
这些信息在Visual Studio的对象浏览器中也没有显示。例如,在VS中键入System.Net.HttpListenerBasicIdentity并按F12以转到元数据定义。您会看到一个公共实例构造函数。这是在System.dll程序集中的。单击基类并再次按F12将带您进入GenericIdentity类。这是在一个不同的程序集mscorlib.dll中。而且,GenericIdentity没有公共或受保护的无参数构造函数(续)。 - Jeppe Stig Nielsen
我们可以得出结论,HttpListenerBasicIdentity 的可见构造函数要么链接 base(string)base(string, string) 构造函数,要么链接 this(...) 到某个不可见的 (privateinternal) 实例构造函数。检查 IL 代码,我们发现它实际上链接了 base(string, string) - Jeppe Stig Nielsen
显示剩余2条评论
4个回答

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() );
        }               
    }
}

这里是一个有用的资源,可以找到所有字节码 - Steven Jeuris
1
如果构造函数调用内部构造函数,则RET之前的字节将为Ldloc_0,如果不调用任何内容,则为Stloc_0。尚未检查是否调用了基本构造函数。 - annakata
1
很抱歉要告诉你,但是看起来你的代码并不正确,尽管我欣赏你的努力。首先,构造函数(ctor)总是会调用另一个构造函数或基类构造函数(除非它是一个结构体且构造函数没有显式调用另一个构造函数,或者它是System.Object)。其次,Ldarg_1/Ldarg0等与是否为基类构造函数无关,你必须实际检查操作数并确定所调用的方法,以判断是否为基类构造函数。 - yoel halb
1
Ldarg_1代表传递的第一个参数,Ldarg_2代表第二个参数,以此类推。而Ldarg_0指的是this指针,与基类构造函数(base ctor)和本类构造函数(this ctor)无关。 - yoel halb
1
@annakata Ldloc_0将第一个本地变量加载到堆栈上,而Stloc_0将堆栈中的第一个值保存在第一个本地变量中,与调用的方法无关。要检查调用的方法,实际上必须检查操作数。 - yoel halb
显示剩余3条评论

1
你可以在对象中添加一个属性来告诉应用了该方面。这样,您就不会多次应用该方面,因为您可以检查该属性。这不是您要求的,但它可能有助于解决您的潜在问题。

1
这并不能保证每个可能的构造函数调用都会执行方面代码。除非你是在暗示我添加到每个构造函数中的代码知道一个属性,使其仅执行一次?那可能行得通。 - Steven Jeuris

1
考虑查看CecilRoslyn
Cecil在编译的程序集上运行,就像Reflection一样。它有更高级别的库构建在其之上,以支持SharpDevelop IDE中的重构,因此它可能有一些东西可以使这个过程更容易。
Roslyn在源代码上运行,并基于此提供对象模型,因此如果您愿意针对源代码而不是二进制文件进行工作,那么使用它可能会更容易。
(我从未真正使用Cecil来做任何类似这样的事情,也从未使用过Roslyn,所以我除了指向这些项目并祝你好运之外,无能为力。如果您成功地得到了一些工作,我很想听听它的进展!)

我接受了这个答案,因为它可能比我发表的那个更好。不过要提醒一下,我还没有试过! - Steven Jeuris

0
据我所知,你不能轻松地使用反射检查或检视代码。反射只能让你反射程序集的元数据信息。
你可以使用 GetMethodBody 获取方法内容,但是你需要自己解析并理解 IL 代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接