【问题标题】:What happens when a pass-by-value is returned as a reference?当传递值作为引用返回时会发生什么?
【发布时间】:2011-09-30 03:41:30
【问题描述】:

好的,在你跳起来之前,你需要了解什么是 pass-by-value 与 pass-by-ref 相比。您可能不同意按值传递的这种定义,但这仅仅是语义,因为真正的问题是堆栈分配和堆分配之间发生了什么。

按值传递:复制要传递的对象,并将对象的副本作为参数提交给函数(好的,OO 纯粹主义者喜欢将其称为“方法”-语义!)。因此,在函数的结束/返回时,无论对对象的副本做了什么,原始对象都不会被修改。

所以 Java(可能还有 C#)是一种按值传递的语言。有些人认为它们是传递引用,但实际上传递的参数是引用。所以引用的副本被传递给函数。也就是说,引用是按值传递的 arg,因为原始引用在函数结束/返回时没有改变。

既然我们已经解决了这个问题,并且开始接受我的值传递定义,那么问题来了。

因此,函数参数是原始对象/引用的副本。它在堆栈上分配。堆栈很好,因为分配的值在函数结束/返回时被简单地立即丢弃。当我的函数从堆栈中获取按值传递的 arg 并将其返回时会发生什么。看,它在堆栈上。该对象/引用的堆栈分配是否被复制并重新分配到堆上?

在 Java 和 C# 中究竟/准确地发生了什么?

【问题讨论】:

    标签: c# java pass-by-value


    【解决方案1】:

    听起来你在问这样的东西在 Java 中的作用是什么:

    public static void f(Object object) {
        return object;
    }
    
    public static void g() {
        Dog dog = new Dog("Spike");
        System.println(f(dog));
    }
    

    如果是这样,答案是,当 g 被调用时:

    1. 内存分配在堆上,g 中名为 dog 的堆栈分配变量用于引用该内存。 dog 的“值”是对对象的引用;它占用了一个单词的记忆。

    2. 此值的副本通过寄存器或堆栈传递给 f。 f 获得自己的堆栈帧,除非被编译器优化掉。但是假设它确实得到了一个堆栈帧。一个简单的单词,包含地址副本的值被放置在这个堆栈帧中。实际上,这与以某种方式传递一个普通的旧整数没有什么不同,正如您正确指出的那样,Java 中的所有内容都是按值传递的。

    3. 当 f 返回时,它将对象的值传递给它的调用者,它本身只是一个指向原始 Dog 对象的内存字。这个简单的指针值通常通过寄存器传回。重点是,只传回一个字。

    【讨论】:

      【解决方案2】:

      在C#上,栈上只创建结构体,不可能在栈上创建对象。

      创建新结构和对象时有所不同。

      当你使用 new 关键字创建一个新对象时,无论如何,该对象总是在堆中创建,这是在 C# 中创建对象的唯一方法。垃圾收集器不会为堆上的对象释放内存,直到没有其他对它的引用;直到所有对该对象的引用都超出范围为止。

      当您使用 new 关键字创建新结构时,无论如何,该结构总是在堆栈中创建。当您将一个结构分配给另一个结构时,会发生成员级副本,而不是对象的引用副本。

      当一个对象按值传递给方法时,您在方法中收到的是对该对象的引用的值(指向它的指针:所有 C# 对象都存储为指向它们在内存中位置的指针)。当一个结构体按值传递给一个方法时,你收到的是它的成员副本。

      注意:当你在将对象传递给方法时使用ref关键字时,意味着该方法可以改变引用所指向的内存位置。

      最后,您无法在方法内部的堆栈中创建对象,并且通过返回传递给您的方法的对象,您将返回与接收到的相同的引用。当您返回一个已传递的结构时,将返回一个按成员计算的副本。

      在java上,概念类似,只是没有结构,也没有refout参数。

      【讨论】:

      • 如果您进入并删除有关“无论如何总是在堆栈上创建结构”的位,我将删除反对票。这根本不是真的。一方面,如果结构是类的成员,它在哪里?更多信息请见:blogs.msdn.com/b/ericlippert/archive/2010/09/30/…blogs.msdn.com/b/ericlippert/archive/tags/value+types
      • @Anthony:他其实是对的,当newobj MSIL 指令用于创建结构实例时,新对象留在堆栈上。 "However, the newobj instruction can be used to create a new instance of a value type on the stack" 当然不是所有的值类型实例都是在栈上创建的,但是用newobj 创建的实例是。
      • @Sid:你的错误是忽略了引用变量本身,它与它指向的类实例不同。引用变量可以在堆栈上,就像问题断言一样。
      • @Ben,你有理由相信newobj 在结构上使用new 时会在每个点上使用吗?我看到很多initobj。你看到了什么?
      • @Anthony:initobj 根本不创建变量,它会将已分配的空间归零。如果我们将 MSIL 抛在脑后并考虑 C# new 运算符,则会创建一个临时本地(在“x86 调用堆栈”上,而不是 MSIL 堆栈上)。临时用作赋值的 RHS,它可能会修改引用类实例的一部分(在堆上),但临时不在堆上。只有在应用优化后才能删除临时的。
      【解决方案3】:

      在 C# 中,按值返回引用。

      在下面的示例中,输入的内容与返回的内容相同。

      Distance FindMinimum (Distance threshold)
      {
          Distance min = null;
          foreach (Distance compare in AllDistance) {
              if (compare > threshold && (min == null || compare < min))
                  min = compare;
          }
          if (min == null)
              return threshold;
          return min;
      }
      

      在下面的示例中,返回对新找到的距离对象的引用。

      Distance FindNewThreshold (Distance threshold)
      {
          foreach (Distance compare in AllDistance) {
              if (compare < threshold)
                  threshold = compare;
          }
          return threshold;
      }
      

      在上述两种情况下,传入的原始对象都没有改变。但在下面的例子中,原来的对象会被替换掉。

      void FindNewThreshold (Distance threshold, ref Distance output)
      {
          foreach (Distance compare in AllDistance) {
              if (compare < threshold)
                  threshold = compare;
          }
          output = threshold;
      }
      
      void Test ()
      {
          Distance d = new Distance (50);
          Distance o;
          AllDistance.Add(new Distance(10));
          FindNewThreshold (d, ref o);
          Console.WriteLine ("{0} {1}", d, o);
      }
      

      这将产生“50 10”。更改 o 将影响 AllDistance 中的第一个对象。

      【讨论】:

      • 请根据内存分配动态详细说明。
      【解决方案4】:

      考虑“int”的情况。

      public int returnIt(int arg) { return arg;}
      

      以及对函数的调用

      int in  = 6;
      int out = returnIt(in);
      

      当函数被调用时,'in'的内容被复制到栈中。

      当函数执行'return arg'时,内容被复制到(嗯,我不知道在JVM中的什么地方,在某些架构中它是到一个寄存器中,有些是到堆栈的当前顶部)。

      然后'arg'从堆栈中被回收,但它的值已经被复制了。

      当赋值发生时,它不是从 'arg' 复制,而是从返回值的位置复制。

      (当然,这可能在“现实生活”中都被优化掉了,举个简单的例子)

      这是你问的吗?

      【讨论】:

      • 熊,你的意思是 {int out = returnIt(in)} 而不是 {int out = returnIt(int)}?
      • 确实我做到了。谢谢你的收获。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-07-10
      • 2021-07-12
      • 1970-01-01
      • 1970-01-01
      • 2021-03-08
      • 2017-06-20
      • 2022-01-07
      相关资源
      最近更新 更多