如果 类 将对象引用作为它们的值,为什么 "new" 关键字不覆盖它们?

If classes hold an object reference as their value, why doesn't the "new" keyword overwrite them?

我正在努力理解传递引用与传递=值=值的所有含义。

我了解在 C# 中,除非明确说明,否则您始终按值传递变量。但是,由于非基本类型将引用作为它们的值,因此从技术上讲,您是在传递这些引用。 所以,这就是为什么如果我有一本名为 属性 的 class 书。我可以这样

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 的新实例没有覆盖旧实例?

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

因此,正如您所说,引用将按值传递,“new”将 return 引用新对象,但它会覆盖旧引用的本地副本。为避免这种情况,您必须使用关键字“ref”传递书籍,然后它就像“参考书的参考”。

如果你想用新创建的书替换原来的书,你需要 refout 关键字:

void ChangeBookName(ref Book book, string name)
{
    book = new Book(name);
}

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

This means that the reference to original book is replaced by the newly created book and the orginal book is marked as obsolete.

也许这有助于...

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

我发现将程序中的几乎每个变量都视为其自身的引用更有帮助,“通过 value/reference”指的是调用时是否创建新引用一种方法。

所以你有你的台词:

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

在我们执行这一行之后,您的程序中立即有一个变量名称,book1,它引用了内存地址 0x1234 中包含“Fight club”的某个数据块

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);

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

我们很少做参考;如果你想换一本新书,你真的应该 return 方法中的它,并将参考文献 b 更改为新 returned 的书。当人们想从一个方法中 return 不止一件事时,他们会使用 ref 但实际上这表明你应该使用不同的 class 作为 return 类型


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

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