为啥在没有 ref 的情况下将 list 传递给一个类似于通过 ref 传递的函数?

Posted

技术标签:

【中文标题】为啥在没有 ref 的情况下将 list 传递给一个类似于通过 ref 传递的函数?【英文标题】:Why is list when passed without ref to a function acting like passed with ref?为什么在没有 ref 的情况下将 list 传递给一个类似于通过 ref 传递的函数? 【发布时间】:2011-11-11 09:42:58 【问题描述】:

如果我没有弄错这个严重的错误,那么这种行为对我来说很奇怪。我将在下面发布示例代码,而不是解释,请告诉我为什么我得到输出 x 而不是 y。

    private void button1_Click(object sender, EventArgs e)
    
        List<int> l = new List<int>()  1, 2, 3 ;
        Fuss(l);
        MessageBox.Show(l.Count.ToString());
    

    private void Fuss(List<int> l)
    
        l.Add(4);
        l.Add(5);
    

我假设输出应该是 3。但我得到的输出是 5。我知道如果我这样做,输出可能是 5:

    private void button1_Click(object sender, EventArgs e)
    
        List<int> l = new List<int>()  1, 2, 3 ;
        Fuss(ref l);
        MessageBox.Show(l.Count.ToString());
    

    private void Fuss(ref List<int> l)
    
        l.Add(4);
        l.Add(5);
    

【问题讨论】:

您可能还应该阅读 Jon Skeet 的 Parameter Passing in C#。 【参考方案1】:

它不像是通过 ref 传递的。

void ChangeMe(List<int> list) 
  list = new List<int>();
  list.Add(10);

void ChangeMeReally(ref List<int> list) 
  list = new List<int>();
  list.Add(10);

试试看。你注意到区别了吗?

如果您在没有 ref 的情况下传递列表(或任何引用类型)的内容(因为正如其他人所说,您传递的是对堆上对象的引用,因此更改了相同的“内存”) )。

但是你不能改变“list”,“list”是一个指向List类型对象的变量。如果您通过引用传递它,您只能更改“列表”(使其指向其他地方)。你会得到一份引用的副本,如果更改,只能在你的方法中观察到。

【讨论】:

【参考方案2】:

参数在 C# 中按值传递,除非它们用 refout 修饰符标记。对于引用类型,这意味着引用是按值传递的。因此,在Fuss 中,l 指的是与其调用者相同的List&lt;int&gt; 实例。因此,调用者将看到对此List&lt;int&gt; 实例的任何修改。

现在,如果您将参数l 标记为refout,则参数通过引用传递。这意味着在Fuss 中,l 是存储位置的别名,用作调用方法的参数。说清楚:

public void Fuss(ref List<int> l)

被调用

List<int> list = new List<int>  1, 2, 3 ;
Fuss(list);

现在,在Fuss 中,llist 的别名。特别是,如果您将List&lt;int&gt; 的新实例分配给l,调用者也会看到分配给变量list 的新实例。特别是,如果你说

public void Fuss(ref List<int> l) 
    l = new List<int>  1 ;

那么调用者现在将看到一个包含一个元素的列表。但是如果你说

public void Fuss(List<int> l) 
    l = new List<int>  1 ;

并调用

List<int> list = new List<int>  1, 2, 3 ;
Fuss(list);

那么调用者仍将看到list 具有三个元素。

清除?

【讨论】:

我明白这一点。你说在第二种情况下(没有参考),如果我们尝试更改l,它不会反映在调用者中。但它是如何反映在我没有参考而提出的问题中的? 我认为@Steven 的回答为我澄清了你的回答。那就是没有 ref 我可以更改列表的内容,但不能更改列表本身!有了 ref,我似乎都可以做到。 @nawfal:在第二种情况下,被调用者和调用者都引用List&lt;int&gt; 的同一个实例,所以他们当然都看到了变化。在第一种情况下,被调用者和调用者都使用相同的存储位置。所以不仅仅是他们都看到了相同的List&lt;int&gt;,而且他们都可以改变彼此正在查看的List&lt;int&gt;的实例! 我明白你在说什么。非常感谢。但我从 FishBasketGordo 和 Steven 那里得到了更清楚的说明。让我标记他们的。你们三个一起让我明白了。【参考方案3】:

对于像 List 这样的引用类型,ref 和 non-ref 之间的区别不在于您是否传递了一个引用(这种情况总是会发生),而是该引用是否可以更改。试试下面的

private void Fuss(ref List<int> l)

    l = new List<int>  4, 5 ;

您会看到计数为 2,因为该函数不仅操作了原始列表,还操作了引用本身。

【讨论】:

【参考方案4】:

“列表”类型的变量、参数或字段,或任何其他引用类型,实际上并不包含列表(或任何其他类的对象)。相反,它将包含类似“Object ID #29115”的内容(当然,不是这样的实际字符串,而是位的组合,这基本上意味着)。在其他地方,系统将有一个称为堆的索引对象集合;如果某个 List 类型的变量持有“Object ID #29115”,那么堆中的 object #29115 将是 List 的实例或从其派生的某种类型。

如果 MyFoo 是一个 List 类型的变量,像 'MyFoo.Add("George")' 这样的语句实际上不会改变 MyFoo;相反,它的意思是“检查存储在 MyFoo 中的对象 ID,并调用存储在其中的对象的“Add”方法。如果 MyFoo 在执行语句之前持有“Object ID #19533”,它会在之后继续这样做,但 Object ID #19533 将调用其 Add 方法(可能会更改该对象)。相反,像“MyFoo = MyBar”这样的语句将使 MyFoo 拥有与 MyBar 相同的 object-id,但实际上不会对问题。如果 MyBar 在语句之前持有“Object ID #59212”,那么在语句之后,MyFoo 也将持有“ObjectId #59212”。对象 ID #19533 和对象 ID#59212 都不会发生任何事情。

【讨论】:

【参考方案5】:

ByRef 和 ByVal 仅适用于值类型,而不适用于引用类型,它们总是像“byref”一样被传递。

如果您需要谨慎修改列表,请使用“.ToList()”函数,您将获得列表的克隆。

请记住,如果您的列表包含引用类型,那么您的“新”列表包含指向与原始列表相同的对象的指针。

【讨论】:

老兄!你的这个“.ToList()”为我节省了一天! 此外,您还可以使用 .clone() 方法【参考方案6】:

列表已经是引用类型,所以当你将它们传递给一个方法时,你就是在传递一个引用。任何Add 调用都会影响调用者中的列表。

通过ref 传递List&lt;T&gt; 的行为本质上类似于将双指针传递给该列表。这是一个插图:

using System;
using System.Collections.Generic;

public class Test

    public static void Main()
    
        List<int> l = new List<int>()  1, 2, 3 ;
        Fuss(l);
        Console.WriteLine(l.Count); // Count will now be 5.

        FussNonRef(l);
        Console.WriteLine(l.Count); // Count will still be 5 because we 
                                    // overwrote the copy of our reference 
                                    // in FussNonRef.

        FussRef(ref l);
        Console.WriteLine(l.Count); // Count will be 2 because we changed
                                    // our reference in FussRef.
    

    private static void Fuss(List<int> l)
    
        l.Add(4);
        l.Add(5);
    

    private static void FussNonRef(List<int> l)
    
        l = new List<int>();
        l.Add(6);
        l.Add(7);
    

    private static void FussRef(ref List<int> l)
    
        l = new List<int>();
        l.Add(8);
        l.Add(9);
    

【讨论】:

【参考方案7】:

只有像 int、double 等基本类型是按值传递的。

复杂类型(如列表)通过引用传递(准确地说是按值传递的指针)。

【讨论】:

这是错误的。除非标有refout,否则所有参数均按值传递。 您说“只有原始类型...通过值传递。复杂类型...通过引用传递。”正如我所说,所有 参数都是按值传递的,除非标有refout,而不是“仅原始类型”。 “复杂”不是一个定义明确的术语;我假设您的意思是引用类型。作为引用类型的参数类型是按值传递的,除非标有refout 不,我的意思是该方法不会复制对象,而只会复制其地址。像在 c++ 中一样考虑它,在那里你没有关键字'ref'。再仔细阅读。我同意你关于“复杂”的观点,引用会更好。 C# 不是 C++。 “不,我的意思是该方法不复制对象,而只复制它的地址。”抛开草率(“方法不复制对象”)不谈,值在传递给方法之前被复制。总是,总是,总是(除非标有refout)。对于值类型,实例就是值。对于引用类型,值就是引用。请注意,这与所指不同! 括号里你写的和我一样。所以我再次不明白出了什么问题。添加 ref 意味着将指针传递给指针,但仍然是按值传递 - 所以根据你的说法,它是......按值传递。

以上是关于为啥在没有 ref 的情况下将 list 传递给一个类似于通过 ref 传递的函数?的主要内容,如果未能解决你的问题,请参考以下文章

如何在没有时间的情况下将 LocalDate 保存到 MongoDB(即使我只保存日期,为啥 mongo 会随时间保存日期)?

在没有 hwdownload 的情况下将 ffpmeg OpenCL 过滤器输出传递给 NVenc?

如何在没有新服务器请求的情况下将下载的图像从父路由传递到子路由

如何在没有 foreach 的情况下将项目从列表复制到列表?

在 `System.Windows.Forms` 控件中没有 `ref` 关键字的情况下,如何通过引用传递

为啥 C# 区分 ref 和 out? [复制]