【问题标题】:If classes hold an object reference as their value, why doesn't the "new" keyword overwrite them?如果类将对象引用作为其值,为什么“new”关键字不覆盖它们?
【发布时间】:2020-12-18 13:56:43
【问题描述】:

我很难理解 pass-by-reference 与 pass=by=value 的所有含义。

我了解在 C# 中,除非明确说明,否则您始终按值传递变量。但是,由于非原始类型将引用作为其值,因此您在技术上是在传递这些引用。 所以,这就是为什么如果我有一个带有 Name 属性的类 Book。我可以这样

Book book1 = new Book("Fight club");
ChangeBookName(book1, "The Wolfman");

void ChangeBookName(Book book, string name){
    book.Name = name;
}

然后执行Console.WriteLine(book1.name) 会输出“The Wolfman”,因为即使有按值传递,该值也是对象在内存中位置的引用,因此更改它也会更改原始对象。

但是,如果我做类似的事情

Book book1 = new Book("Fight club");
    ChangeBookName(book1, "The Wolfman");
    
    void ChangeBookName(Book book, string name){
        book = new Book(name);
    }

那么book1.Name实际上不会是“狼人”,它仍然是“搏击俱乐部”。

幕后发生了什么? new 关键字是否创建了新的对象引用?但是,传递的原始值发生了什么?为什么 Book 的新实例没有覆盖旧实例?

【问题讨论】:

  • This question 是关于 Java 的,但其中提到的要点也可以在 C# 中应用。这就是这两种语言的相似之处。

标签: c# pointers pass-by-reference pass-by-value


【解决方案1】:

如果您忘记所有“通过引用传递”与“通过值传递”,这可能更容易理解 - 它们是糟糕的短语,正如您所发现的那样,因为它们往往会让您认为书籍记忆数据在传递给方法时被复制,或者原始数据被传递。 “通过引用的副本传递”和“通过原始引用传递”可能会更好——类实例总是通过引用传递

我发现将程序中的几乎每个变量都视为本身的引用会更有帮助,而“按值/引用传递”是指在调用方法时是否创建新引用。

所以你有你的线路:

Book book1 = new Book("Fight club");

在我们执行这一行之后,你的程序中有一个变量名,book1,它引用了内存地址 0x1234 处的一些数据块,其中包含“搏击俱乐部”

ChangeBookName(book1, "The Wolfman");

我们调用 ChangeBookName 方法,c# 建立另一个引用,称为 book,因为它在方法签名中就是这么说的,也指向地址 0x1234。

您有两个引用,一个数据块。 book 引用将在方法结束时丢失 - 它的生命周期仅在方法的 { } 之间

如果您使用此额外的 book 引用来更改数据的某些内容:

book.Name = "The wolfman";

然后第一个引用,book1 会看到变化——它指向相同的数据,数据发生变化。

如果您将这个额外的book 引用指向内存中其他地方的全新数据块:

 book = new Book("The wolfman");

您现在有两个引用,两个数据块 - book1 指向 0x1234 处的“搏击俱乐部”,而 book 指向 0x2345 处的“狼人”。方法结束时,wolfman 数据和book 引用将丢失

关于对一个数据块有两个引用的关键点是您可以更改数据的某些属性并且两个引用都能看到它?但是,如果您将其中一个引用指向新的数据块,则原始引用仍然指向原始数据

如果您希望一种方法能够将数据块换成全新的数据块,并让原始参考体验更改,您可以使用 ref 关键字。从概念上讲,这会导致 C# 根本不复制引用,而是重用相同的引用(尽管名称不同)

void ChangeBookForANewOne(ref Book tochange){
  tochange = new Book("Needful things");
}

Book b = new Book("Fight club");
ChangeBookForANewOne(b);

在这段代码中,只有一个对一个数据块的引用。将数据块更改为方法内的新数据块会导致在方法退出时记住更改

我们很少做 ref;如果你想将你的书换成新书,你应该从方法中真正返回它并将参考 b 更改为新返回的书。当人们想要从一个方法返回多个东西时,人们会使用 ref,但这实际上表明您应该使用不同的类作为返回类型


同样的概念也适用于值类型(通常是原始的东西,如 int),但细微的差别是,如果你将一个 int 传递给一个方法,那么你最终会得到两个变量名,但内存中还有两个 int;如果您在方法内增加 int,则原始 int 不会改变,因为为方法调用的生命周期建立的附加变量是内存中的不同数据 - 数据确实被复制并且您在内存中有两个变量和两个数字。 Ref 样式行为对于此类事情更有用且更常见,例如 int.TryParse - 它返回一个 true 或 false 指示解析是否成功,但为了将解析后的值返回给您,它需要使用您传递的原始变量在,而不是它的副本。

为此,TryParse 使用了 ref 的变体,称为 out - 方法变量上的一个标记,指示“此方法肯定会为您传入的变量分配一个值;如果您要给它一个变量已经初始化为一个肯定会被覆盖的值”。相反, ref 表示“您可以传递一个已初始化为值的变量,我可能会使用该值并且可能会覆盖它/将其指向内存中的新数据”。如果你有一个不需要取值但肯定会覆盖的方法,比如我之前的 ChangeForANewBook,你真的应该使用out - 在 100% 的情况下,ChnageForANewBook 会覆盖传入的内容,这可能会导致意外的数据丢失开发商。将其标记为 out 意味着 C# 将确保仅使用/传入空白引用,有助于防止意外的数据丢失

【讨论】:

  • 好答案!如果您从未使用过指针和(取消)引用,那可能会非常令人困惑。我同意更好的解决方案是返回所需的东西,而不是通过 ref 做事。但如果你要回馈一些简单的东西,我会选择一个元组而不是整个类(例如一个字符串和一个 int)。
  • 是的,我要添加一个关于元组的脚注;它们本质上是“编译器帮助你创建的临时类”,因此黄金法则“升级你返回的类而不是使用 ref”仍然有效,只是更难看穿使用元组的语法糖 :)跨度>
【解决方案2】:
Book book1 = new Book("Fight club");          // <-- book1 holds a ref to 'Fight Club'
ChangeBookName(book1, "The Wolfman");         // <-- a copy of that ref is passed as an argument
// ...                                        // <-- book1 still holds the original ref to 'Fight Club'
    
void ChangeBookName(Book book, string name){  // <-- receives the copy of the ref to 'Fight Club'
    book = new Book(name);                    // <-- overwrites it with a ref to 'The Wolfman'
}                                             // <-- lifetime of the temp copy ends here
                                              // <-- 'The Wolfman` object becomes eligible for gc

【讨论】:

    【解决方案3】:

    所以,正如您所说,引用将按值传递,“new”将返回对新对象的引用,但它会覆盖旧引用的本地副本。为避免这种情况,您必须使用关键字“ref”传递 book,然后就像“reference to reference of book”。

    【讨论】:

      【解决方案4】:

      如果您想用新创建的书籍替换原始书籍,您需要 ref 或 out 关键字:

      void ChangeBookName(ref Book book, string name)
      {
          book = new Book(name);
      }
      
      // ...
      Book book1 = new Book("Fight club");
      ChangeBookName(ref book1, "The Wolfman");
      

      这意味着对原书的引用被新创建的书取代,原书被标记为过时。

      也许这会有所帮助...

      【讨论】:

        猜你喜欢
        • 2011-03-08
        • 2012-01-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-05-13
        • 2016-01-23
        相关资源
        最近更新 更多