В список добавляется копия или ссылка

Год уже ковыряюсь в c#, и только сейчас задумался над вопросом, он меня поставил в большой ступор. Условно, есть пустой список и объект. В список добавляем объект, далее объект ссылаем на новый объект. Раньше считал, что и элемент списка будет изменен. Ведь это всё ссылки. Но значение в списке остается старым, из старой ссылки. Как объяснить это с помощью теории?

string firstResult = "";
string secondResult = "";

List<Person> persons = new List<Person>();

Person p = new Person() { Id = 1, Name = "A"};
persons.Add(p);

firstResult = $"result: {persons[0].Name}";

// "result: A"
 
p = new Person() {Id = 2, Name = "B"} ;

secondResult = $"result: {persons[0].Name}";

// "result: A" ```

Ответы (4 шт):

Автор решения: MBo

Вот смотрите:

p = new Person()

вы создали объект, и его адрес ("квартира 666")записали на листочек (p)

persons.Add(p);

Здесь вы переписали в записную книжку (persons) адрес с листочка p, теперь в книжке есть запись "квартира 666"

p = new Person()

На листочкe стёрли старую запись, адрес теперь другой, сделали новую запись ("квартира 777"). Но записная книжка об этом не знает, и в ней по-прежнему записано "квартира 666"

→ Ссылка
Автор решения: Faraday

Потому что в переменную вы записываете ссылку. Когда вы закидываете ссылку в список, то по факту, вы просто присваиваете эту ссылку другой переменной. Ну вот чисто для примера, есть класс какой-то

public class Class { }

И переменные

Class a = new Class(); // a = v1
Class b = new Class(); // a = v1, b = v2

Что будет далее?

a = b // a = v2, b = v2

Теперь у нас в a и в b хранится ссылка на один объект. Если я сделаю вот так:

b = new Class(); // a = v2, b = v3

Тогда у нас a до сих пор ссылается на объект v2, а переменная b имеет теперь ссылку на v3, это логично

Раньше считал, что и элемент списка будет изменен

Спросить "Почему вы так считали?" будет не уместно, так что просто так не делайте больше и всё будет хорошо. Ссылка есть ссылка, переменная это просто место для хранения. Сами места для хранения сохраняют как бы... Значимое значение ссылки, а не саму ссылку... (Только не ругаейтесь за формулировку). В общем, изучите тему указателей в C++, тогда вы всё поймёте

→ Ссылка
Автор решения: InterceptorTSK

Суть: есть куча, там объекты. Грубо на пальцах: вы вообще не можете влиять на объекты (за исключением того, что вы можете создать объект). А дальше (почти всегда) оно пускается на "самотёк". В этом и есть смысл кучи как таковой, потому что в таком случае за кучей не нужно следить. Она сама за собой следит, через коллектор.

Но что бы иметь возможность достучаться до объекта - нужна ссылка, и по сути это переменный указатель, а в рамках самоуправляющейся среды дот.нета - это ссылка/референс.

Сделано это для того, что бы объекты можно было перемещать в куче, дефрагментировать кучу, сжимать кучу и так далее. Но реальный физический адрес на объект - меняется, и вам за этим следить не нужно, потому что у вас ссылки. На объекты кучи.

Как только на объект кучи не ведёт ни одна ссылка - объект становится не нужен. Ссылок нет же - тогда вы до объекта не достучитесь, значит объект можно уничтожить. Этим и занимается коллектор. Сколько бы вы не создавали объектов - коллектор за ними следит. И за ссылками на эти объекты тоже следит.

Суть конкретно:

object obj0 = new object();
// ссылка obj0, она ссылается на объект в куче

object obj1 = obj0;
// теперь две ссылки ссылаются на один и тот же объект в куче

object.ReferenceEquals(obj0, obj1);
// выведет True, этот метод сравнивает именно ссылки (адреса) на объект
// сами объекты не сравниваются никак, зачем?

object obj2 = new object();
// ещё один объект в куче, теперь их два

object.ReferenceEquals(obj0, obj2);
// выведет False, ссылки (адреса) указывают на разные объекты
object.ReferenceEquals(obj1, obj2);
// выведет False, ссылки (адреса) указывают на разные объекты

// однако же ссылке можно ПЕРЕНАЗНАЧИТЬ объект
// obj0 = obj2;
// obj1 = obj2;
// теперь все три ссылки будут указывать на один и тот же (второй) объект

// однако же на первый объект теперь ссылок нету, это проверит коллектор и
// уничтожит (освободит память), которую занимал первый объект в куче

Получается так, что ссылки и объекты чрезвычайно мало связаны. Связь именно только через указание, и никакой иной связи - нет. И не должно быть. Внимательно отслеживайте вашу программу, и тогда всё получится. Но указание можно ПЕРЕНАЗНАЧИТЬ. Это именно ваш случай и есть.

Ваша программа:

string firstResult = "";
string secondResult = "";
// здесь кстати object.ReferenceEquals(firstResult, secondResult) возвратит True
// а почему? ;)

List<Person> persons = new List<Person>();
// здесь вы замутили внутренний массив ссылок, они все указывают на null
// на самом деле вы ничего не замутили, потому что внутренний массив даже
// не создастся, потому что к нему никто не обращался (пока ещё нет)

Person p = new Person() { Id = 1, Name = "A"};
// здесь вы замутили объект в куче, и ссылку "p" на этот объект
// ссылка есть - объект "живёт"
persons.Add(p);
// вы добавили ссылку в массив ссылок
// здесь и создаётся массив ссылок, и в этот массив помещается ссылка "p"
// теперь persons[0] указывает на объект с Name = "А"

firstResult = $"result: {persons[0].Name}";
// persons[0] - это ссылка, она не null

// "result: A"

p = new Person() {Id = 2, Name = "B"};
// здесь вы создаёте второй объект, помещаете его в кучу, но на этот объект
// ссылается "p", вы "p" ПЕРЕНАЗНАЧИЛИ на новый объект с Name = "В"

secondResult = $"result: {persons[0].Name}";
// здесь вы опять запросили ссылку (она добавлена в массив), ссылка вам
// возвращается, и она как вела так и ведёт на тот самый
// первый объект с Name = "А", который вы добавляли в массив изначально
// именно потому и будет опять
// "result: A"

Что бы понять именно равенство ссылок (не объектов, а ссылок на них) - выше приведены примеры с object.ReferenceEquals(), и оно приведено не просто так. Поэкспериментируйте с этим.

→ Ссылка
Автор решения: krupennikov

Ответ: В список добавляется копия ссылки (в Вашем коде).

Развёрнутый ответ (для Вашего кода):

Вы затёрли адрес (ссылку) на объект в памяти новым адресом (ссылкой) в памяти на новый созданный объект. При добавлении в массив persons, добавляется адрес объекта на который ссылается переменная p, а не сама переменная p, то есть копируется адрес (ссылка) из p. По факту у вас получается две разных однотипных переменных persons[0] и p, ссылающихся на один и тот же объект, но в последствии у переменной p Вы меняете адрес объекта, другого объекта.

Объявление переменной просто строкой:

Person p;

по факту означает, что эта переменная не ссылается ни на какой объект в памяти.

Слово new означает, что Вы создаёте новый объект в новом участке памяти, соответственно по новому адресу (ссылке) в памяти. Например, просто напишите следующую строчку:

new Person() { Id = 2, Name = "B" };

и ошибок не будет и никаких действий Вы над этим объектом совершить тоже не сможете, так как адрес (ссылка) на созданный объект ни какой переменной не присвоена. А в памяти этот объект будет существовать, до тех пор, пока его не удалит сборщик мусора.

В Вашем случае, следующий код будет аналогичным и возможно более понятным:

Person p;    
p = new Person() { Id = 1, Name = "A" };
p = new Person() { Id = 2, Name = "B" };

то есть сначала Вы создаёте первый новый объект в памяти и ссылку (адрес) присваиваете переменной p, потом создаёте второй новый объект в памяти уже с совсем другим адресом и снова присваиваете той же переменной p. А тот адрес, который был до этого естественно затёрся, исчез. Но сам первый объект из памяти никуда не делся, вы просто потеряли адрес (ссылку) расположения этого объекта в памяти. Позже объекты в памяти без активных ссылок удаляются сборщиком мусора для освобождения памяти.

Но в Вашем коде, ссылка (адрес) на первый объект является активной, так как была добавлена в массив List persons под индексом 0. Хочу заметить, что добавлен (скопирован) именно адрес (ссылка) на объект в памяти из переменной p, а не сама переменная p, и уж тем более не сам объект.

Чем отличается вот такая запись:

persons.Add(new Person() { Id = 3, Name = "C" });

По сути ни чем. Компилятор (если образно) преобразует эту короткую запись в следующие строки уже за Вас, можно сказать дописывает, так как программисты очень ленивые и не любят много писать:

Person p1;
p1 = new Person();
p1.Id = 3;
p1.Name = "C";
persons.Add(p1);
→ Ссылка