Подтвердить что ты не робот

Структурные ссылочные типы

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

public struct MyIPEndPoint
{
    public String IP;
    public UInt16 Port;

    public MyIPEndPoint(String ipAddress, UInt16 portNumber)
    {
        IP = ipAddress;
        Port = portNumber;
    }

    public override string ToString()
    {
        return IP+":"+Port;
    }
}

...

static int Main(string[] args)
{
    MyIPEndPoint address1 = new MyIPEndPoint("127.0.0.1", 8080);
    MyIPEndPoint address2 = address1;

    address2.IP = "255.255.255.255";
    address2.Port = 9090;

    Console.WriteLine(address1);
    Console.WriteLine(address2);
}

Вывод:

127.0.0.1:8080
255.255.255.255:9090

Почему IP (строка, являющаяся ссылочным типом) address1 не изменяется? Такое же поведение возникает, если я заменяю string на IPAddress для представления IP внутри MyIPEndPoint: хотя IPAddress является классом (который является ссылочным типом), он не ведет себя как ссылочный тип. Почему?

В самом деле, если я обертываю string, которые представляют IP, используя новый простой класс MyIP, поведение изменяется.

public class MyIP
{
    public string IpAsString;

    public MyIP(string s)
    {
        IpAsString = s;
    }
    public override string ToString()
    {
        return IpAsString;
    }
}

Конечно, вы также должны настроить структуру MyIPEndPoint следующим образом:

public struct MyIPEndPoint
{
    public MyIP IP;   // modification
    public UInt16 Port;

    public MyIPEndPoint(String ipAddress, UInt16 portNumber)
    {
        IP = new MyIP(ipAddress);   // modification
        Port = portNumber;
    }

    public override string ToString()
    {
        return IP+":"+Port;
    }
}

Наконец, в Main я изменил только инструкцию:

MyIPEndPoint address1 = new MyIPEndPoint("127.0.0.1", 8080);
MyIPEndPoint address2 = address1;

address2.IP.IpAsString = "255.255.255.255";   // modification
address2.Port = 9090;

Console.WriteLine(address1);
Console.WriteLine(address2);

Теперь вывод:

255.255.255.255:8080
255.255.255.255:9090

Я ожидал этот выход в первом случае. Почему в первом случае ссылка не ведет себя так, как ожидалось?

4b9b3361

Ответ 1

Вы правильно поняли, что с structs address1 и address2 не являются одним и тем же объектом. Значения были скопированы. Однако для поля это простой случай переназначения. Это не имеет никакого отношения к тому факту, что строка является ссылочным типом или любыми специальными правилами или любым предложением неизменности. Вы просто переназначили свойство или поле с другим значением.

someStruct.SomeString = "A";
anotherStruct = someStruct;
anotherStruct.SomeString = "B"; // would never affect someStruct

Вы перезаписали ссылку в этом примере. Тот факт, что на короткое время оба поля структур содержат одну и ту же ссылку, не имеют значения. В вашем втором примере вы сделали что-то совсем другое.

someStruct.IP.SomeString = "A";
anotherStruct = someStruct;
anotherStruct.IP.SomeString = "B"; 

В этом случае значение IP не изменилось. Часть состояния IP изменилась. Каждое поле структуры по-прежнему ссылается на один и тот же IP-адрес.

Проще говоря,

var foo = new Foo(); // Foo is class
var other = foo; 
// other and foo contain same value, a reference to an object of type Foo
other = new Foo(); // was foo modified? no! 

int x = 1;
int y = x;
y = 2; // was x modified? of course not.

string s = "S";
string t = s;
t = "T"; // is s "T"? (again, no)

Значения переменных и полей сохраняются. Для классов эти значения являются ссылками на объекты. Две переменные или поля могут содержать одну и ту же ссылку, но это не означает, что эти переменные сами связаны. Во всяком случае, они не связаны, они просто имеют общую ценность. Когда вы заменяете значение для одной переменной или поля, другая переменная не изменяется.


Не по конкретной теме, но стоит отметить, что изменчивые структуры рассматриваются многими как зло. Другие не совсем придерживаются того же взгляда или, по крайней мере, не так религиозно. (Тем не менее, стоит также отметить, что если Address был классом, то address1 и address2 будут иметь одинаковое значение (ссылка на объект Address), а изменение состояния адреса 1 будет видно через адрес2, если ни один из адресов1 или address2 сами переназначаются.)

Если это фактическое представление вашего кода, было бы целесообразно провести некоторое исследование изменчивых структур, чтобы вы, по крайней мере, имели полное понимание различных ошибок, которые вы можете встреча.

Ответ 2

Рассмотрим ваш первый пример. У вас есть два ящика с надписью "адрес один" и "адрес два". Оба ящика пусты.

MyIPEndPoint address1 = new MyIPEndPoint("127.0.0.1", 8080);     

Теперь вы получаете лист бумаги, и вы пишете "127.0.0.1, 8080" на этой бумаге и помещаете его в ящик 1.

MyIPEndPoint address2 = address1;      

Теперь вы берете ксерокс и делаете ксерокопию бумаги в ящике "адрес один" и помещаете копию в "адрес двух ящиков".

address2.IP = "255.255.255.255";     
address2.Port = 9090; 

Теперь вы берете бумагу в адрес два ящика и выцарапываете то, что там было, и заменяете его новым текстом.

Бумага в ящике не изменилась. Он все тот же, что и когда-либо.

Теперь рассмотрим ваш второй пример. Теперь у вас есть два пустых ящика, как и раньше, и книга пустой бумаги.

MyIPEndPoint address1 = new MyIPEndPoint("127.0.0.1", 8080); 

Вы выбираете страницу книги наугад и назовите ее "REFERENCE ONE" . На этой странице вы пишете "127.0.0.1". Вы берете кусок свободной бумаги и пишите "REFERENCE ONE, 8080" на этой бумаге и вставляете ее в ящик с надписью "адрес один".

MyIPEndPoint address2 = address1;  

Вы делаете фотокопию бумаги в "адрес один" и помещаете копию в "адрес два".

address2.IP.IpAsString = "255.255.255.255"; 

Вы открываете ящик "address two" и видите, что он говорит "REFERENCE ONE" . Вы просматриваете книгу, пока не найдете страницу с надписью "REFERENCE ONE" . Вы поцарапаете то, что там, и замените его новым текстом.

address2.Port = 9090;  

Вы открываете ящик "адрес два" и вычеркиваете "8080" и заменяете его "9090". Вы оставляете СПРАВОЧНИК ОДИН, где он находится.

И теперь, когда вы закончите, ящик "address one" содержит "REFERENCE ONE, 8080", "адрес два" содержит "REFERENCE ONE, 9090", а в книге есть страница "REFERENCE ONE: 255.255. 255,255".

Теперь вы понимаете разницу между ссылочными и значениями типов?