C# - 需要知道变量何时超出范围

时间:2016-04-22 14:42:29

标签: c# scope garbage-collection

我有一些C ++代码,我正在尝试移植到C#。原作者有一个很酷的缩进调试C ++类,其工作原理如下:

void a()
{
   indented_debug id;
   id.trace("I'm in A");
}
void b()
{
   indented_debug id;
   id.trace("I'm in B");
   a();
}
void e()
{
   a();
}
void d()
{
   e();
}
void c()
{
   indented_debug id;
   id.trace("I'm in C");
   a();
   b();
   {
      indented_debug id2;
      id2.trace("I'm still in C");
      d();
   }
}

你在输出中看到的是:

I'm in C
   I'm in A
   I'm in B
      I'm in A
   I'm still in C
      I'm in A

这使得很容易看到不仅调用函数的顺序,而且调用谁的函数。压痕(这是关键的东西)由结构和工具自动处理。破坏“indented_debug”对象。每次构造一个“indented_debug”对象时,它会递增一个“我应该缩进多少”的计数器;每次“indented_debug”对象被破坏时,它会递减该计数器。这是自动计算缩进,这是该类的关键。

当然,C#完全不喜欢这个。 C#不遗余力地确保您完全无法知道变量何时超出范围。是的,我知道垃圾收集是如何工作的,我确实喜欢它,但似乎微软可以给我们一个函数IsThisObjectUnreachable()或类似的东西。或者,属性关键字[RefCount]表示“对此对象进行引用计数而不是垃圾回收”。

我找不到任何方法来了解一个对象,知道它是否超出范围,是否有一些聪明的方法在C#中提供相同的功能?

我还应该抛出这个设计限制:我真的不想把我的所有函数都包装在“using(indented_debug id = new id){}”中,我的想法就是拥有这个调试功能,对它的影响很小代码及其可读性是可能的。

[稍后添加]

这有点棘手,稍后会像这样添加原始问题,但我需要编写一些代码而不能在评论中这样做。

StackTrace方法与我正在寻找的解决方案非常接近,让我解释一下它的样子。

public class indented_debug
{
   static int minFrame = 999;
   static void trace(string text)
   {
        StackTrace stackTrace = new StackTrace();
        StackFrame[] frames = stackTrace.GetFrames();

        if (frames.Length < minFrame)
            minFrame = frames.Length;

        String indent = new String(' ', (frames.Length - minFrame) * 3);
        Debug.WriteLine(indent + text);
   }
}

这是非常酷的,因为您甚至不需要构造一个类型为indented_debug的对象 - 缩进完全由您在堆栈中的深度控制。当然,缺点是在我的例子中,当c()调用d()时,在那里有两个额外的堆栈帧,其中没有发生跟踪,因此缩进将超过所需的。 Rob已经通过向方法添加自定义属性来提出解决方法,这确实解决了这个问题(我的示例中没有包含他的代码,您可以在下面阅读)。

但还有另一个问题,StackTrace概念不允许在函数内部进行额外的缩进(就像我在原始的c()函数中那样)。我认为代码在函数内部有额外缩进的次数非常少,因此在这些情况下添加“using”块可能是可以接受的。这意味着C#代码如下所示:

[IndentLog]
void a()
{
   indented_debug.trace("I'm in A");
}
[IndentLog]
void b()
{
   indented_debug.trace("I'm in B");
   a();
}
void e()
{
   a();
}
void d()
{
   e();
}
[IndentLog]
void c()
{
   indented_debug.trace("I'm in C");
   a();
   b();
   using (indented_debug id = new indented_debug())
   {
      indented_debug.trace("I'm still in C");
      d();
   }
}

然后构造对象'id'并且&amp;以确定的方式完成,我可以创建一个数据结构,在构造时将'id'与当前堆栈帧相关联,并在最终确定时将其解除关联。

6 个答案:

答案 0 :(得分:3)

当需要在不再需要对象时确定性地执行某些操作时,IDisposable接口是在C#中使用的机制。所以它不像C ++那样精简,但它确实可以做类似的事情:

void a()
{
   using(var id = new IndentedDebug())
   {
       id.trace("I'm in A");
   }
}
void b()
{
   using(var id = new IndentedDebug())
   {
       id.trace("I'm in B");
       a();
   }
}

并将引用计数添加到IndentedDebug.Dispose方法。

使用AOP或其他模式可能有更好的方法,但当变量超出范围时,可能做某事。

答案 1 :(得分:2)

根据这个社区的所有重要意见,这就是我要去的地方(至少目前为止)。对不起,如果匈牙利的符号冒犯了你,我在MFC-land上花了很多时间,仍然有点喜欢它。

internal class IndentFrame
{
    public string m_type, m_function;
    public bool m_bOutput = false;
    public int m_nExtra = 0;

    public IndentFrame(StackFrame frame)
    {
        m_type = frame.GetMethod().DeclaringType.Name;
        m_function = frame.GetMethod().ToString();
    }

    public bool Matches(StackFrame frame)
    {
        return (m_type == frame.GetMethod().DeclaringType.Name)
            && (m_function == frame.GetMethod().ToString());
    }
}

public class IndentDebug : IDisposable
{
    internal static List<IndentFrame> m_frames = new List<IndentFrame>();

    public static void WriteLine(string text)
    {
        UpdateFrames();
        // Remember that this frame produced output.
        m_frames[m_frames.Count - 1].m_bOutput = true;
        // How much indent?
        int nIndent = 0;
        foreach (IndentFrame frame in m_frames)
            nIndent += (frame.m_bOutput ? 1 : 0) + frame.m_nExtra;
        String indent = new String(' ', (nIndent - 1) * 3);
        Debug.WriteLine(indent + text);
    }
    internal static void UpdateFrames()
    {
        StackTrace stackTrace = new StackTrace();
        StackFrame[] frames = stackTrace.GetFrames();

        // frames[] are ordered such that the current frame is at [0] but we
        // want the topmost frame in [0]
        Array.Reverse(frames);

        // Remove any obsolete frames from our list
        int i;
        for (i = 0; i < Math.Min(m_frames.Count, frames.Length); i++)
        {
            if (frames[i].GetMethod().DeclaringType == typeof(IndentDebug))
                break;
            if (!m_frames[i].Matches(frames[i]))
                break;
        }
        if (i < m_frames.Count)
            m_frames.RemoveRange(i, m_frames.Count - i);
        // Add any new frames
        while (m_frames.Count < frames.Length)
        {
            if (frames[m_frames.Count].GetMethod().DeclaringType == typeof(IndentDebug))
                break;
            IndentFrame frame = new IndentFrame(frames[m_frames.Count]);
            m_frames.Add(frame);
        }
    }

    internal static void UpdateIndent(int add)
    {
        UpdateFrames();
        m_frames[m_frames.Count - 1].m_nExtra += add;
    }

    public IndentDebug(string text)
    {
        IndentDebug.UpdateIndent(1);
        IndentDebug.WriteLine(text);
    }
    public void Dispose()
    {
        IndentDebug.UpdateIndent(-1);
    }
}

}

这非常接近我原来的愿望,在大多数情况下,您只需在函数中添加一行(无需额外的自定义属性):

void a()
{
   IndentDebug.WriteLine("I'm in A");
}

但是如果你想在函数中加入额外的缩进,你可以通过添加“using”语句来获得它:

void c()
{
   IndentDebug.WriteLine("I'm in C");
   a();
   b();
   using (IndentDebug id = new IndentDebug("I'm still in C"))
   {
      d();
   }
}

答案 2 :(得分:1)

这是我在LinqPad中编写的一些代码,它可以满足您的需求:

void Main()
{
    c();
}

void a()
{
    using(var id = new IndentedDebug())
    {
        id.Trace("I'm in A");
    }
}

void b()
{
    using(var id = new IndentedDebug())
    {
        id.Trace("I'm in B");
        a();
    }
}

void e()
{
    a();
}

void d()
{
    e();
}

void c()
{
    using(var id = new IndentedDebug())
    {
        id.Trace("I'm in C");
        a();
        b();
        {
            using(var id2 = new IndentedDebug())
            {
                id2.Trace("I'm still in C");
                d();
            }
        }
    }
}

class IndentedDebug : IDisposable
{
    const int indentSize = 2;
    const char indentChar = ' ';
    static int indentLevel = 0;

    private string _indentSpaces;

    public IndentedDebug()
    {
        _indentSpaces = new string(indentChar, indentSize * indentLevel);
        ++indentLevel;
    }

    public void Trace(string message)
    {
        Console.WriteLine("{0}{1}", _indentSpaces, message);
    }

    public void Dispose()
    {
        --indentLevel;
    }
}

所以你的代码看起来并不相同,但另一方面,它没有做任何&#34;魔术&#34;或者 - 代码本身会在缩进调试结束时显示。

答案 3 :(得分:1)

  就像我在上一段中所说的那样,这不是理想的行为。与在“使用”块中包装整个函数相比,在函数顶部插入几行的影响要小得多。使人们对调试代码对整体功能的影响不那么担心。

来自OP的评论。

现在,在C#8中,您可以使用使用声明

示例:

using System;
using System.Runtime.CompilerServices;

namespace SO_36796820
{
    public class OutOfScopeEvent
    {
        public string MethodName { get; set; }
        public int StackLevel { get; set; }
    }

    public delegate void NotifyInScope(OutOfScopeEvent obj);
    public delegate void NotifyOutOfScope(OutOfScopeEvent obj);

    public class OutOfScopeToken : IDisposable
    {
        private readonly string _methodName;
        public event NotifyOutOfScope NotifyOutOfScope;
        private readonly int _stackLevel;

        public OutOfScopeToken(string methodName = "", int stackLevel = 0)
        {
            _methodName = methodName;
            _stackLevel = stackLevel;
        }

        public void Dispose()
        {
            NotifyOutOfScope?.Invoke(new OutOfScopeEvent() { MethodName = _methodName, StackLevel = _stackLevel });
        }
    }

    public class OutOfScope : IDisposable
    {
        private readonly OutOfScopeToken _outOfScopeToken;

        public event NotifyOutOfScope NotifyOutOfScope;
        public event NotifyInScope NotifyInScope;

        private int _stackLevel = 0;

        public OutOfScope()
        {
            NotifyOutOfScope += OnOutOfScope;
        }

        public OutOfScopeToken GetToken([CallerMemberName] string methodName = nameof(GetToken))
        {
            NotifyInScope?.Invoke(new OutOfScopeEvent()
            {
                MethodName = methodName,
                StackLevel = _stackLevel
            });
            var outOfScopeToken = new OutOfScopeToken(methodName, _stackLevel);
            outOfScopeToken.NotifyOutOfScope += NotifyOutOfScope;
            _stackLevel++;

            return outOfScopeToken;
        }

        private void OnOutOfScope(OutOfScopeEvent obj)
        {
            _stackLevel--;
        }

        public void Dispose()
        {
            _outOfScopeToken?.Dispose();
        }
    }

    class Program
    {
        private static OutOfScope _outOfScope;
        static void a()
        {
            using var v = _outOfScope.GetToken();
            b();
            c();
        }

        static void b()
        {
            using var v = _outOfScope.GetToken();
        }

        static void c()
        {
            using var v = _outOfScope.GetToken();
            d();
        }

        static void d()
        {
            using var v = _outOfScope.GetToken();
        }

        static void Main(string[] args)
        {
            _outOfScope = new OutOfScope();
            _outOfScope.NotifyOutOfScope += OutOfScope;
            _outOfScope.NotifyInScope += InScope;
            a();
        }

        private static void InScope(OutOfScopeEvent obj)
        {
            Console.WriteLine(new String('\t', obj.StackLevel) + " I'm in " + obj.MethodName);
        }

        private static void OutOfScope(OutOfScopeEvent obj)
        {
            Console.WriteLine(new String('\t', obj.StackLevel) + " I'm out of " + obj.MethodName);
        }
    }
}

输出:

 I'm in a
         I'm in b
         I'm out of b
         I'm in c
                 I'm in d
                 I'm out of d
         I'm out of c
 I'm out of a

答案 4 :(得分:0)

为什么不看看StackTrace类

https://msdn.microsoft.com/en-us/library/system.environment.stacktrace%28v=vs.110%29.aspx

它确切地告诉您调用哪些方法来获取当前点以及许多其他内容。

如果您仍然可以访问成员变量,那么从技术上讲,它仍然在范围内,除非您手动对其进行描述或处理。因此,如果您了解代码中的变量,那么它不会超出范围。

答案 5 :(得分:-1)

有一种非常聪明的方法可以做到这一点:

  1. 全局变量属于整个实例化类的范围。
  2. 方法变量适用于整个方法
  3. 括号中的任何变量都在该括号内。
  4. 除了首先了解范围如何工作之外,您的&#34; id.trace()&#34;方法看起来更像是控制台输出。你可以用Console.WriteLine轻松替换它(&#34;我在...&#34;);并获得相同的结果。