我希望它看起来像这样: 函数
a(arg1,arg2,arg3 ...)
调用函数b(arg4,arg5,arg6 ...)
,后者又调用log()
,然后能够检测堆栈跟踪(可以通过Environment.StackTrace
完成),以及调用堆栈中每个函数(例如a
和b
)使用的值。我希望它在调试和发布模式下都能工作(或者至少在调试模式下)。
在.net中是否有可能实现这一点?
a(arg1,arg2,arg3 ...)
调用函数b(arg4,arg5,arg6 ...)
,后者又调用log()
,然后能够检测堆栈跟踪(可以通过Environment.StackTrace
完成),以及调用堆栈中每个函数(例如a
和b
)使用的值。无法证明:
在调用b
时,a的arg1
使用的堆栈空间(即IL堆栈,因此可能从未放入堆栈,而是在调用时被注册)不保证仍由arg1
使用。
扩展一下,如果arg1
是引用类型,则其所引用的对象如果在调用b
后没有使用,则不能保证不被垃圾回收。
编辑:
稍微详细解释一下,因为您的评论表明您还没有理解这个问题,并且仍然认为这应该是可能的。
Jitter使用的调用约定没有在任何相关标准的规范中指定,这使得实现者可以自由实现改进。它们确实在32位和64位版本以及不同的发布版之间存在差异。
但是,来自MS人员的文章表明所使用的约定类似于__fastcall约定。在调用a
时,arg1
将被放入ECX寄存器*中,arg2
将被放入EDX寄存器中(我假设为32位x86,使用amd64时,甚至有更多参数被注册)。arg3
将被推入堆栈中,并且确实存在于内存中。
请注意,在此时,不存在任何包含arg1
和arg2
的内存位置,它们仅存在于CPU寄存器中。
在执行方法本身的过程中,寄存器和内存将根据需要使用。然后调用了b
。
现在,如果a
需要arg1
或arg2
,则必须在调用b
之前将其推入。但是如果没有,那么它就不会 - 甚至可能会重新排序以减少这种需求。反之,这些寄存器在此时可能已经用于其他事情上了 - Jitter不是傻瓜,因此如果它需要一个寄存器或堆栈上的空间,并且在方法的其余部分中有未使用的空间,则会重复使用该空间。(同样,在此之上,C#编译器将重复使用IL生成使用的虚拟堆栈中的插槽)。
因此,当调用b
时,arg4
被放置在ECX寄存器中,arg5
被放置在EDX中,并将arg6
推送到堆栈中。此时,arg1
和arg2
不存在了,您无法像读一本书变成厕纸后那样找到它们。
有趣的是,方法很常见,会以相同的参数在相同的位置调用另一个方法,在这种情况下,ECX和EDX可以保持不变。
然后,b
返回,将其返回值放入EAX寄存器或EDX:EAX对或通过EAX指向它的内存,具体取决于大小,a
在将其返回结果放入该寄存器之前做了更多的工作,以此类推。
现在,这是假设未进行任何优化的情况。实际上,可能并没有调用b
,而是将其代码内联。在这种情况下,无论值是在寄存器还是在堆栈上 - 在后一种情况下,它们在堆栈上的位置与b
的签名无关,而与a
的执行过程中相关的值的位置有关,并且在另一个对b
的“调用”或甚至在a
的另一个“调用”的情况下会有所不同,因为在一种情况下,包括其对b
的调用在内的整个a
的调用已被内联,在另一种情况下未被内联,在另一种情况下以不同的方式内联。如果例如,arg4
直接来自另一个调用返回的值,则此时它可能在EAX寄存器中,而arg5
则在ECX中,因为它与arg1
相同,arg6
位于由使用的堆栈空间的中间某处。
另一种可能性是,对b
的调用是一个已被消除的尾调用:因为a
将立即通过b
返回其返回值(或其他可能性),所以与其将值推送到堆栈上,不如替换正在被使用的值,并更改返回地址,使从b
返回跳回调用a
的方法,跳过一些工作(并减少内存使用,以致一些函数式方法可以避免溢出堆栈,而且确实很好用)。在这种情况下,在调用b
期间,可能完全没有参数,即使那些已经在堆栈上。
这最后一种情况是否应该被认为是优化还存在争议;有些语言严重依赖它,如果不这样做,它们的性能会很差,甚至根本无法工作(而不是溢出堆栈)。
还有各种其他优化方式。理应进行各种其他优化方式——如果.NET团队或Mono团队采取了某些措施,使我的代码更快、占用更少的内存,但行为与之前相同,我个人是不会抱怨的!
这还假设编写C#代码的人从未更改过参数的值,这显然不是真的。考虑以下代码:
IEnumerable<T> RepeatedlyInvoke(Func<T> factory, int count)
{
if(count < 0)
throw new ArgumentOutOfRangeException();
while(count-- != 0)
yield return factory();
}
即使 C# 编译器和 Jitter (JIT编译器) 被设计成可以保证参数不会以上述方式发生变化,你怎么能知道在 factory
调用内部已经有哪些 count
值?即使在第一次调用时,它们也是不同的,而且上述代码并不奇怪。
因此,总结一下:
从所有这些情况中,我们怎么可能知道 arg1
是什么?
现在,考虑垃圾收集的存在。想象一下,即使在所有这些情况下,我们都能神奇地知道 arg1
是什么。如果它是指向堆上对象的引用,这可能对我们毫无帮助,因为如果上述全部意味着栈上没有更多活动的引用(这很明显确实发生了),并且 GC 启动,则该对象可能已被回收。因此,我们所能神奇地掌握的只是对不再存在的东西的引用 - 的确很可能是对现在用于其他目的的堆中某个区域的引用,从而破坏了整个框架的类型安全性!
这与反射获取 IL 根本不可比较,因为:
其他回答中有关于性能分析、AOP 和拦截的建议是你所能得到的最接近的。
*实际上,this
是实例成员的真正第一个参数。让我们假装一切都是静态的,这样我们就不必再指出这一点了。
在C#中不可能实现这一点,您应该使用AOP方法并在每次调用方法时执行方法参数日志记录。这样,您可以集中您的日志记录代码,使其可重用,然后只需要标记哪些方法需要参数日志记录。
我相信使用像PostSharp这样的AOP框架可以很容易地实现这一点。