递归,返回后带有字符数组的 C#

本文关键字:字符 数组 返回 递归 | 更新日期: 2023-09-27 18:37:07

在下面的代码中,当我在调试器中单步执行代码时检查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);
}

递归,返回后带有字符数组的 C#

您这里有两个问题。

首先,"之前/之后返回"问题意味着您看到两个不同的执行帧 - 也就是说,在调试器中,堆栈跟踪将在彼此之上显示一堆PrintReverse,因为每个都存在于自己的上下文中,并发具有自己的状态。它几乎(尽管不是真的)像该方法的"实例",您会看到两个不同的实例。

其次,因为每个都有自己的状态,所以每个中的局部变量(包括至关重要的参数)也是重复的。它们最初指向同一个堆对象(初始 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() 将字符重新分配给指向其他位置,但原始变量仍然引用旧位置。

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

如果不refArray.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
    }

想想执行链。 您正在递归调用缩小数组的方法,直到您到达 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 纠正我,希望我这次做对