【问题标题】:Technical meaning of C# refC# ref 的技术含义
【发布时间】:2017-04-09 01:35:51
【问题描述】:

我了解 c# ref 允许您发送指向对象的指针。我只是想知道这对于实际发送到引擎盖下的函数意味着什么。

据我了解是这样的:

DoSomething(参考示例对象) --> 这里将一个指向exampleObject的指针发送给函数

DoSomething (exampleObject) --> 这里一个指向对象属性的指针数组被发送到函数。

这是正确的吗?第二个函数是否也会为值类型创建一个指针? (因为它们是属性并且它们的值必须保存在堆中以保持对象的引用完整性)

更清楚一点的解释: *对象A在内存位置5 *它具有对象B和对象C的属性

现在我调用 DoSomething (A) --> 由于默认情况下对象是通过指针处理的,所以我传递了一个指针值 5,以便我可以在内存地址 5 处操作该值。C# 如何知道发送对象的属性位于哪个内存地址?

现在我调用 DoSomething(参考 A) --> 此时我创建了一个指向另一个指针的新指针(比如说 6)。所以现在我传入 6,它允许我更改内存地址 6 的值,很方便地结果是 5,即对象 A 的位置。所以现在我可以更改传入的 A 对象指向的内存.

我不明白的是如何在内存中找到属性?必须在某个地方发送指针数组或指向指针数组的指针,对吗?

【问题讨论】:

  • "我了解 c# ref 允许您发送指向对象的指针。" - 这不是它的意思......当你分配 var obj = GetSomeObject(); - that 正在为一个对象分配一个引用(本质上是对指针的幻想)
  • exampleObject 已经是一个指针。它指向存储在堆上的对象。所以ref 给你一个指向指针的指针。那种在学期结束前把编程 101 课的一半都清空的细节。没有“指针数组”,存储在 exampleObject 中的指针足以定位成员。
  • @Hans Passant:我明白了,但是如果没有指针数组,如何引用对象的所有属性?需要知道它们的内存位置才能改变它们的值,对吧?
  • 属性是代码,不是数据。 CLR 知道代码存储在哪里,即时编译器生成该代码并知道如何生成 CALL 指令。字段是数据,exampleObject 指针指向它们。即时编译器知道对象内字段值的偏移量。第一个字段的偏移量为 0,第二个字段的偏移量更高,具体取决于第一个字段的类型。
  • 对,处理器有一个专用的寻址模式,可以尽可能快地做到这一点。

标签: c# pointers ref


【解决方案1】:

在第二种情况下我的理解

 DoSomething(exampleObject)

exampleObject 的地址作为DoSomething 方法的参数给出,这意味着在DoSomething 中可以重新分配对象的属性(假设使用了引用类型),但exampleObject 的assignmet 可以不要在调用者的范围内更改它,因为exampleObject的地址是在堆栈上修改的,而不是在调用者的范围内。另一方面,在第一种情况下

DoSomething(ref exampleObject)

指向exampleObject 地址的指针作为DoSomething 的参数给出,这意味着除了更改exampleObject 的属性外,还可以重新分配对exampleObject 本身的引用。因此,exampleObject 可以被交换,这种效果在调用者的范围内也是可见的。

【讨论】:

  • 如果发送了对象的地址(就像您所说的没有引用的函数),我应该能够更改对象的值,对吧? (因为你可以改变指针指向的内存地址的值)但是我不能,我只能改变它的属性。这意味着只有属性的地址可用于被调用的函数。这就是为什么我认为 C# 在发送对象时会在后台发送一个指针数组(更具体地说是指向指针数组的指针)。那么究竟发送了什么?
【解决方案2】:

没有。如果您是从 c++ 进入 c#,您可以将 ref 想象为 c++ 中的引用:

void foo(A*& a)
{ 
     // Here you can change object's state
     // Also you can change value of pointer
     // Caller will have new value of pointer
}
void foo(A* a)
{
    // Here you can change object's state
    // Also you can change value of copy of pointer
    // Caller will have old value of pointer
}

【讨论】:

    【解决方案3】:

    如果我们知道或期望DoSomething 可以执行“计算值或创建新对象并将其返回给我们”的行为,我们可以使用DoSomething(ref exampleObject)。而且我们知道DoSomething 的声明是否也包含ref 关键字。

    如果在调用之前您的 exampleObject 包含某个值,或者它指向某个对象(甚至为 null),那么在调用之后它可能包含一个新值,或者它可能指向一个新对象,或者它可能已被更改为空。

    ref 没有确定性义务发生这种情况,只是 如果 DoSomething 决定 exampleObject应该改变,然后我们将得到那个新的值或对象(或空)。否则我们会保留以前的内容。

    值类型(数字、布尔值、结构)和引用类型(对象)都是如此,这就是为什么我一直说“新值或新对象或 null”的原因。

    编辑:
    我故意避免提及指针。除非你在做低级的事情,否则在 C#/.NET 中你几乎不需要知道指针。了解概念和支配它们的规则通常就足够了。

    【讨论】:

      【解决方案4】:

      不,这个不正确。

      ref 带来的是引用语义。

      当您调用函数 foo(T arg) - 无论 T 是值类型还是引用类型(我怎么强调都不为过) - foo 会得到 T 的副本。

      值类型和引用类型的区别在于,值类型的names指的是有趣的东西,而引用类型的names指的是引用(或指针)有趣的东西。因此,当你复制一个值类型的对象时,你会得到一个有趣的东西的副本,当你复制对有趣的东西的引用时。

      我在 Google 上找到的一些图片,而不是我自己画的:

      图像中的副本基本上是您调用foo(T arg时发生的情况@)正在谈论。在这两种情况下,我们arg 都包含传递给foo 的内容的副本。如果Tint,这就是您感兴趣的数字。如果TCircle,那么arg 是参数传递给foo 的“地址”1包含。我们可以谈论箭头,而不是谈论指针或引用。引用类型的变量包含指向有趣内容的箭头,而值类型的变量包含内容本身。

      以下代码证明了这一点:

      using System;
      class MainClass {
        public static void Main (string[] args) {
          Foo foo1 = new Foo();
          foo1.i = 1;
          Console.WriteLine("Main(): foo1.i: {0}", foo1.i);
      
          doStuffToFoo(foo1, 5);
          Console.WriteLine("Main(): foo1.i: {0}", foo1.i);
      
          Console.WriteLine();
      
          Bar bar1;
          bar1.j = 1;
          Console.WriteLine("Main(): bar1.j: {0}", bar1.j);
      
          doStuffToBar(bar1, 5);
          Console.WriteLine("Main(): bar1.j: {0}", bar1.j);
        }
      
        static void doStuffToFoo(Foo aFoo, int aInt) {
          aFoo.i = aInt;
          aFoo = new Foo();
          aFoo.i = 123;
          Console.WriteLine("doStuffToFoo: aFoo.i: {0}", aFoo.i);
        }
      
        static void doStuffToBar(Bar aBar, int aInt) {
          aBar.j = aInt;
          aBar = new Bar();
          aBar.j = 123;
          Console.WriteLine("doStuffToBar: aBar.j: {0}", aBar.j);
        }
      }
      
      class Foo {
        public int i;
      }
      
      struct Bar {
        public int j;
      }
      

      在尝试或阅读之前,看看您是否理解它打印的内容。

      doStuffToFoo 中的变量aFoofoo1副本。干净利落。 aFoofoo1 是不同的变量(存储在堆栈上的不同位置 - Main 的堆栈帧中的 foo1doStuffToFoo 的堆栈帧中的 aFoo - 假设有一个堆栈,因为the stackis an implementationdetail),它们都包含指向同一个 Foo 对象的箭头。

      当您引用这两个变量的字段时,它们通过各自的箭头访问相同的Foo 对象。但是当您将new Foo() 分配给aFoo 时,它只会将aFoo 中的箭头更改为指向新的Foo 对象。它对指向第一个Foo 对象的另一个箭头没有任何作用,它安全地存储在foo1 变量中,并且无法访问doStuffToFoo 中的代码。这就是为什么您在 Main 中看不到“123”的原因。

      所有这些都将我们带到refref T v 表示 v 不是类型 T 的新变量(如果 T 是值类型,则包含一些有趣的东西;如果 T 是引用类型,则指向一些有趣的东西的箭头)。而v 是作为参数传递的任何内容的别名。它是其他变量的第二个名称

      如果您将以下代码添加到上面的示例中,您会看到它生效。

      /***** inside Main: *****/
      Foo foo2 = new Foo();
      Foo foo3 = foo2;
      foo2.i = 1;
      Console.WriteLine("Main(): foo2.i: {0}", foo2.i);
      Console.WriteLine("Main(): foo3.i: {0}", foo3.i);
      
      doStuffToFooRef(ref foo2, 5);
      Console.WriteLine("Main(): foo2.i: {0}", foo2.i);
      Console.WriteLine("Main(): foo3.i: {0}", foo3.i);
      /***** until here *****/
      
      static void doStuffToFooRef(ref Foo aFoo, int aInt) {
        aFoo.i = aInt;
        aFoo = new Foo();
        aFoo.i = 123;
        Console.WriteLine("doStuffToFooRef: aFoo.i: {0}", aFoo.i);
      }
      

      现在aFoofoo2 的另一个名称。当您分配给“aFoo”时,您分配给“foo2”,因为这两个名称都引用同一个变量(存储位置)。这就是在doStuffToFooRef 分配期间发生的事情:

      i 仍然为零,因为它在我们放在那里之前是 123。)

      在这里试试:https://repl.it/E80a

      C# ref 在这方面就像 C++ 引用,正如 Александр Лысенко 在他的回答中所写的那样。虽然从技术上讲,ref SomeReferenceType x 可能是“指向指针的指针”,正如某些人所说,但这并没有抓住ref 的真正含义,而是详述了不需要的实现细节总是正确的(它可能会被优化)。这意味着添加ref 会添加一个指针。那么引用类型的 ref-of-a-ref 是指向指针的指针吗?每个额外的ref 是否都会增加一个间接级别?


      1 就本次讨论而言,我们并不关心它是否真的是一个地址或其他东西。由于对象可能在内存中移动,我们理解它可能不是主机/操作系统的虚拟内存地址,而是广义上的地址。
      有些人区分引用和指针,后者是数字内存地址,但其他人也不害怕将这些东西称为指针,因为它们指向对象。例如,Java 语言规范 (§4.3.1) 明确表示:

      引用值(通常只是references)是指向这些对象的指针,以及一个特殊的空引用,它不引用任何对象。

      他们强调“引用”——他们引入的新术语——但在他们看来,指针对读者来说是显而易见的,而且对象也不是不可移动的 AFAIK。

      【讨论】:

        【解决方案5】:

        我总是比较巫毒娃娃的指针——无论你对巫毒娃娃做什么,都会影响巫毒娃娃“指向”的人。

        所以 C# 引用就像一个巫毒娃娃 - 它附着在一个对象上,您可以通过消耗这个巫毒娃娃来影响该对象。

        但是如果我突然想让巫毒娃娃指向与之前不同的人,会发生什么?使用巫毒娃娃会影响它指向的人 - 而不是巫毒娃娃本身。

        解决办法是什么?创造另一个巫毒娃娃 - 原来的巫毒娃娃!这样我就可以弄乱原来的娃娃,而不仅仅是它指向的人。

        所以 C# 中的 ref 是对引用的引用 - 它允许您修改引用,而不仅仅是它指向的对象。

        在幕后,引用只是指示对象在 RAM 中的位置的数字。 ref 引用是另一个数字,但它表示原始引用在 RAM 上的位置,而不是它指向的对象所在的位置。

        【讨论】:

        • 为什么你认为指向巫毒娃娃的巫毒娃娃也不能惹第二个巫毒娃娃指向的人?如果您认为每个 ref 都是一个指针(“只是表示对象在 RAM 上的位置的数字”)指向原始对象引用而不是指向对象本身,那么问题与Hans Passant - 一个 ref 到一个 ref 到一个 ref 怎么样?
        • @conio 我同意更好的短语是“不仅与”,固定
        猜你喜欢
        • 2012-12-08
        • 1970-01-01
        • 1970-01-01
        • 2011-04-20
        • 1970-01-01
        • 1970-01-01
        • 2013-11-27
        • 2013-09-13
        • 2023-04-09
        相关资源
        最近更新 更多