【问题标题】:Recursion, C# with array of char after return递归,返回后带有char数组的C#
【发布时间】:2012-06-16 14:48:20
【问题描述】:

在下面的代码中,当我在调试器中单步执行代码时检查Chars 变量时,char 数组的大小在最后一次迭代的返回行之前为 0,但在返回行之后为 1 和继续增长到原来的大小。

为什么会这样?提前感谢您的帮助。

static void Main(string[] args)
{
    string str = "Hello";
    PrintReverse(str.ToArray()); // prints "olleH"
    Console.Read();
}

static void PrintReverse(char[] Chars)
{
    Console.Write(Chars[Chars.Length - 1]);
    Array.Resize(ref Chars, Chars.Length - 1);
    if (Chars.Length == 0) return;
    PrintReverse(Chars);
}

【问题讨论】:

  • 哪个字符数组?您的问题不清楚。
  • 我对此进行了测试,它只是反向打印了字符串。我必须将 System.Linq 命名空间添加到我的项目中,因为您调用的是 str.ToArray() 而不是 str.ToCharArray()

标签: c# arrays recursion char return


【解决方案1】:

想想执行链。您正在递归调用缩小数组的方法,直到达到 0,然后返回调用者(相同的方法),因此当您遍历调用堆栈时,您会看到增长回原始大小。

因此不会发生额外的逻辑,因为递归调用是方法中的最后一次调用,但是您会看到调试器结束每个调用,而数组大小又比之前的调用大 1它。

如果您改为通过引用传递数组,则数组大小将在到达调用堆栈时保持大小为 0,因为对 Array.Resize 的每次连续调用都会创建一个新数组并更新所有引用到新数组,而不仅仅是该调用的本地引用。 (如果您不通过引用传递,它只会更新引用的副本,而不更新之前调用中的那些)。

这是因为 Array.Resize 创建了一个新数组并更新了引用以指向新数组而不是旧数组,并且通过不通过引用传递,您发送的是原始数组的引用副本而不是对数组的实际引用,因此对 Array.Resize 的调用不会更新旧引用。

static void PrintReverse(ref char[] Chars)
{
    Console.Write(Chars[Chars.Length - 1]);
    Array.Resize(ref Chars, Chars.Length - 1);
    if (Chars.Length == 0) return;
    PrintReverse(ref Chars);
}

感谢 Groo 纠正我,希望我这次是对的

【讨论】:

  • 但是这样做的原因是,在内部,Array.Resize 总是创建一个 new 数组实例,然后复制元素,然后替换引用。这就是为什么以前的数组实例留在堆栈上的原因。如果Array.Resize 以不同的方式操作并改变原始数组,它将保持更改。
  • 您的评论不正确。首先,Array.Resize 既不是“变异元素”,也不是变异数组本身。它在内存中创建一个新实例,复制元素,然后切换引用以使Chars 指向新实例。旧数组不会被收集,只是因为前一个堆栈帧仍然持有它的引用。其次,按值传递数组不会“创建数组的新实例”,它只会创建对数组的引用的副本。输入方法时,不会进行复制。
  • @Groo 你是对的,对此做了一些测试,一个相当复杂的小问题。希望我的回答现在是正确的,谢谢您的纠正。
【解决方案2】:

尝试将ref 添加到参数声明中,现在应该可以按您预期的方式工作了。

没有ref,对Array.Resize的调用只能修改本地数组引用,不能修改从Main传入的引用。

    static void Main(string[] args)
    {
        string str = "Hello";
        var array = str.ToArray();
        PrintReverse(ref array);
        Console.Read();
        Debug.Assert(array.Length == 0);
    }
    static void PrintReverse(ref char[] Chars)
    {
        Console.Write(Chars[Chars.Length - 1]);
        Array.Resize(ref Chars, Chars.Length - 1);
        if (Chars.Length == 0) return;
        PrintReverse(ref Chars);
    }

编辑:

我误认为 ref 导致了浅克隆,这就是证据:

    static void Main(string[] args)
    {
        var array = new[] { new object() };
        TestRef(ref array, array);
    }

    static void TestRef(ref object[] arrayByRef, object[] arrayByValue)
    {
        Debug.Assert(ReferenceEquals(arrayByRef, arrayByValue)); //no difference whether passed by ref or value, if there was a shallow clone happening, this would fail
        Array.Resize(ref arrayByRef, 2);
        Debug.Assert(!ReferenceEquals(arrayByRef, arrayByValue)); //only now do they differ
    }

【讨论】:

  • 他在问为什么数组在调试器中的大小越来越大,AFAIK 程序正在按他的预期工作
  • @KDiTraglia 随着堆栈的展开,它似乎从 0 增长到 5,但实际上他正在查看 6 个不同的数组。我的解决方案解释了他如何在需要时避免这种情况,并试图解释为什么会发生这种情况。
  • 是的,这很好,我会编辑我的答案,因为它有复选标记
  • “如果没有ref,它将适用于新的浅层克隆” - 你能详细说明一下吗?
  • @Groo - 不,我不能,因为我错了!请参阅我的更新答案。
【解决方案3】:

这里有两个问题。

首先,“返回前/返回后”问题意味着您看到了两个不同的执行帧——也就是说,在调试器中,堆栈跟踪将显示一堆PrintReverses,因为每个都在那里在它自己的上下文中,有它自己的状态,同时。它几乎(虽然不是真的)像那种方法的“实例”,你看到的是两个不同的。

第二,因为每个都有自己的状态,所以每个中的局部变量——包括,关键是参数——也是重复的。它们最初指向同一个堆对象(您的初始 Char 数组),但它们都是不同的变量。

现在,看看这段代码:

char[] test1 = new char[] { '1', '2', '3' }, test2 = test1;
Array.Resize(ref test2, 2);
MessageBox.Show(new string(test1) + " - " + new string(test2)); // result: 123 - 12

如果你运行它,你会看到虽然变量最初引用的是同一个对象,但 Array.Resize 创建了一个新对象并更改了传入变量的引用以指向新的一个。第一个变量中的引用仍然指向旧的(不可变的)对象。

这就是您的情况,仅使用 Chars 参数。在每种方法中,您使用 Array.Resize() 重新分配 Chars 以指向其他位置,但原始变量仍然引用旧位置。

【讨论】:

    猜你喜欢
    • 2017-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-09
    • 1970-01-01
    • 2011-06-26
    • 2019-08-04
    • 2019-01-12
    相关资源
    最近更新 更多