传递引用的列表 - 帮我解释这种行为

144

请看以下程序:

class Test
{
    List<int> myList = new List<int>();

    public void TestMethod()
    {
        myList.Add(100);
        myList.Add(50);
        myList.Add(10);

        ChangeList(myList);

        foreach (int i in myList)
        {
            Console.WriteLine(i);
        }
    }

    private void ChangeList(List<int> myList)
    {
        myList.Sort();

        List<int> myList2 = new List<int>();
        myList2.Add(3);
        myList2.Add(4);

        myList = myList2;
    }
}

我以为myList会被按引用传递,输出结果会是

3
4
列表确实是“传引用”的,但只有sort函数会产生影响。以下语句myList = myList2;没有任何效果。
因此,实际输出结果为:
10
50
100

你能帮我解释一下这种行为吗?如果确实 myList 不是按引用传递的(从 myList = myList2 似乎是这样),那么 myList.Sort() 是如何生效的?

我曾经认为即使执行了那个语句也不会生效,输出应该是:

100
50
10

1
只是一个观察(我意识到这里的问题已经被简化了),但如果ChangeList实际上创建了一个新列表,似乎最好让它返回一个List<int>而不是void - Jeff B
8个回答

268

最初,可以用以下方式图形化表示:

初始状态

然后,应用排序:myList.Sort(); 排序集合

最后,当你执行myList' = myList2时,你失去了其中一个引用,但不是原始的引用,集合仍然保持排序。

失去引用

如果使用按引用传递(ref),那么myList'myList将变成相同的(只有一个引用)。

注意:我使用myList'来表示你在ChangeList中使用的参数(因为你给了与原始名称相同的名称)


132

你传递的是一个列表的引用,但你没有通过引用传递列表变量 - 所以当你调用 ChangeList 时,变量的值(即引用 - 类似于指针)被复制了一份,而在 ChangeList 内部更改参数的值 (参数的值) 不会被 TestMethod 观察到。

尝试:

private void ChangeList(ref List<int> myList) {...}
...
ChangeList(ref myList);

然后将 本地变量 myRef(在TestMethod中声明)的引用传递;现在,如果在ChangeList内重新分配参数,则也会重新分配TestMethod内部的变量。


我确实可以做到,但我想知道排序是如何生效的。 - coder_bro
7
当你调用ChangeList时,只有引用被复制,而原对象不变。如果你对该对象进行更改,所有引用该对象的地方都会看到这个更改。 - Marc Gravell

24

以下是一个易于理解的方法:

  • 你的列表是在堆上创建的对象。变量myList是指向该对象的引用。

  • 在C#中,你从不传递对象,而是通过值传递它们的引用。

  • 当你通过在ChangeList中访问经过传递的引用来访问列表对象(例如进行排序)时,原始列表会被更改。

  • ChangeList方法中的赋值是给引用值的,因此不会对原始列表进行更改(仍然在堆上但不再在方法变量上引用)。


13

这个链接将帮助你理解C#中的按引用传递。

基本上,当一个引用类型的对象以值的形式传递给一个方法时,只有那个对象可用的方法可以修改对象的内容。

例如,List.sort()方法会改变List的内容,但如果你将其他对象分配给同一变量,那么该分配就局限于该方法。因此myList保持不变。

如果我们使用ref关键字传递引用类型的对象,则可以将其他对象分配给同一变量,并且这会更改整个对象本身。

(编辑:这里是上面链接更新后的文档版本。)


8

C# 通过值传递时只进行浅复制,除非所涉及的对象实现了 ICloneable 接口(显然 List 类没有这么做)。

这意味着它复制了 List 本身,但列表内部对象的引用仍然保持相同;也就是说,指针仍然引用原始 List 中的相同对象。

如果您更改新 List 引用的内容,您将同时更改原始 List(因为它引用了相同的对象)。然而,您随后会完全更改 myList 引用,得到一个新的 List,只有原始 List 引用那些整数。

阅读 这篇 MSDN 文章关于“传递引用类型参数” 的内容,以获取更多信息。

"如何在C#中克隆泛型列表"这篇来自StackOverflow的文章讨论了如何制作List的深拷贝。


4

使用ref关键字。

查看明确的参考此处以了解传递参数。
具体来说,请参阅this,以了解代码的行为。

编辑:Sort在相同引用上(通过值传递)工作,因此值被排序。然而,将新实例分配给参数不起作用,因为参数是按值传递的,除非您放置ref

放置ref允许您将指针更改为List的新实例。如果没有ref,则可以在现有参数上工作,但无法使其指向其他内容。


4
虽然我同意以上所有人所说的内容。但是我对这段代码有不同的看法。 基本上,你将新列表分配给了局部变量myList而不是全局变量。 如果你将ChangeList(List myList)的签名更改为private void ChangeList(),则会看到输出为3, 4。
以下是我的推理... 尽管通过引用传递了列表,但可以将其视为按值传递指针变量 当你调用ChangeList(myList)时,你正在传递指向(Global)myList的指针。现在这个指针存储在(local)myList变量中。因此,你的(local)myList和(global)myList现在指向同一个列表。 现在你进行排序 => 它起作用,因为(local)myList正在引用原始(global)myList 接下来,你创建一个新列表并将指针分配给(local)myList。但是一旦函数退出,(local)myList变量就被销毁了。 希望有所帮助
class Test
{
    List<int> myList = new List<int>();
    public void TestMethod()
    {

        myList.Add(100);
        myList.Add(50);
        myList.Add(10);

        ChangeList();

        foreach (int i in myList)
        {
            Console.WriteLine(i);
        }
    }

    private void ChangeList()
    {
        myList.Sort();

        List<int> myList2 = new List<int>();
        myList2.Add(3);
        myList2.Add(4);

        myList = myList2;
    }
}

0

一个引用类型的对象会被分配两部分内存,一部分在栈中,一部分在堆中。栈中的部分(也称为指针)包含对堆中部分的引用 - 实际值存储在堆中。

当不使用 ref 关键字时,只会创建栈中部分的副本并传递给方法 - 引用同一堆中的部分。因此,如果您更改堆中的某些内容,则这些更改将保留下来。如果您更改复制的指针 - 通过将其分配给引用堆中其他位置 - 它不会影响方法外部的原始指针。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接