C#递归编程中类的引用行为:链表生成时变量指向差异问题
现有可接收int类型数组、将其转换为递归链表对象的ListGenerator实现,代码如下:
public class ListNode { public int val; public ListNode next; public ListNode(int x) { val = x; } public ListNode(int val = 0, ListNode next = null) { this.val = val; this.next = next; } } public static ListNode GenerateList(int[] nums) { if (nums == null || nums.Length == 0) { return null; } var i = 0; var first = new ListNode(nums[i]); var current = first; while (++i < nums.Length) { current.next = new ListNode(nums[i]); current = current.next; } return first; }
已知C#中的类为引用类型,执行current.next = new ListNode(nums[i]);代码后,first变量会与current同步变化,该表现符合预期。
但执行current = current.next;后,first与current的值不再一致:实际表现为first的next属性已被赋值,但current的next属性为null。按照对引用类型的常规认知,first应当与current始终保持一致,该实际表现与认知存在偏差,需要明确该现象的底层原理。
你对引用类型的认知存在一个关键误区:引用类型变量本身只是存储堆内存地址的「指针」,两个变量指向同一个对象时,修改对象的属性会在所有引用处同步可见,但给变量本身重新赋值,只会改变当前变量存储的地址,不会影响其他变量,也不会修改原对象的内容。
逐行拆解代码执行逻辑就能完全理清现象:
- 初始化阶段:执行
var first = new ListNode(nums[i]); var current = first;时,栈上的两个引用变量first和current存储了完全相同的堆内存地址,都指向刚创建的第一个链表节点,此时两个变量确实指向同一个对象。 - 执行
current.next = new ListNode(nums[i]);时:逻辑是通过current存储的地址找到堆上的第一个节点,修改这个节点的next字段,让它指向新创建的第二个节点。由于first也指向这个第一个节点,因此通过first访问next时自然能看到修改后的值,这就是你观察到的「同步变化」。 - 执行
current = current.next;时:这一步没有修改任何堆上的节点对象,只是把栈上current变量存储的内存地址,替换为current.next对应的地址——也就是刚创建的第二个节点的地址。此时两个变量的指向已经完全分离:first存储的地址没有任何变化,仍然指向第一个节点,第一个节点的next字段仍然保留着指向第二个节点的引用current已经不再指向第一个节点,转而指向刚创建的第二个节点,而新节点初始化时next默认值为null,因此此时访问current.next得到的就是null
可以用一个生活化的类比辅助理解:你把自己家的地址写在两张便签上,一张标注first,一张标注current。你拿着current便签找到家,在门上贴了一张写着邻居家地址的纸条(对应修改节点的next属性),这时候不管拿哪张便签找到你家,都能看到门上贴的邻居地址。之后你把current便签上的地址涂改成邻居家的地址,这时候current便签指向的是邻居家,邻居家门上本来就没贴其他地址纸条(对应next为null),但first便签上还是你家的地址,你家门上的邻居地址纸条也没有被改动,两张便签对应的内容自然就不再一致。
内容的提问来源于stack exchange,提问作者user4661780

