Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Struct containing reference types

Tags:

c#

.net

A struct is a value type, so if I assign a struct to another struct, its fields will be copied in the second struct. But, what happens if some fields of the struct are a reference type?

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

The output is:

127.0.0.1:8080
255.255.255.255:9090

Why the IP (a string, that is a reference type) of address1 does not change? The same behavior occurs if I replace string with IPAddress to represent the IP within MyIPEndPoint: although IPAddress is a class (that is a reference type), it does not behave as a reference type. Why?

Indeed, if I wrap the string which represent the IP using a new simple class MyIP, the behavior changes.

public class MyIP
{
    public string IpAsString;

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

Of course you should also adjust the MyIPEndPoint struct in the following way:

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

Finally in the Main I changed only a statement:

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

Now the output is:

255.255.255.255:8080
255.255.255.255:9090

I was expecting this output in the first case. Why in the first case does the reference not behave as expected?

like image 461
enzom83 Avatar asked Feb 08 '12 23:02

enzom83


Video Answer


2 Answers

Consider your first example. You have two drawers, labelled "address one" and "address two". Both drawers are empty.

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

Now you get a piece of paper, and you write "127.0.0.1, 8080" on that paper, and put it in drawer 1.

MyIPEndPoint address2 = address1;      

Now you take a photocopier and make a photocopy of the paper in the "address one" drawer, and put the copy in the "address two drawer".

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

Now you take the paper in the address two drawer and scratch out what was there, and replace it with the new text.

The paper in drawer one didn't change. It's still the same as it ever was.

Now consider your second example. Now you have two empty drawers, same as before, and a book of blank paper.

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

You pick a page of the book at random and title it "REFERENCE ONE". On that page you write "127.0.0.1". You take a piece of loose paper and write "REFERENCE ONE, 8080" on that paper and stick it in the drawer labelled "address one".

MyIPEndPoint address2 = address1;  

You make a photocopy of the paper in "address one" and put the copy in "address two".

address2.IP.IpAsString = "255.255.255.255"; 

You open up drawer "address two" and see that it says "REFERENCE ONE". You look through the book until you find a page that says "REFERENCE ONE". You scratch out what is there and replace it with the new text.

address2.Port = 9090;  

You open up drawer "address two" and scratch out the "8080" and replace it with "9090". You leave the REFERENCE ONE where it is.

And now when you are done, drawer "address one" contains "REFERENCE ONE, 8080", drawer "address two" contains "REFERENCE ONE, 9090", and the book has a page that says "REFERENCE ONE: 255.255.255.255".

Now do you understand the difference between reference and value types?

like image 68
Eric Lippert Avatar answered Oct 08 '22 18:10

Eric Lippert


You have correctly understood that with structs, address1 and address2 are not the same object. The values were copied. However, for the field, this is a simple case of reassignment. It has nothing to do with the fact that string is a reference type or any special rules or any suggestion of immutability. You have simply reassigned a property or field with another value.

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

You have overwritten the reference in this example. The fact that for a brief moment, both structs' fields contained the same reference is of no importance. In your second example, you did something very different.

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

In this case, the value of IP has not changed. Part of IP's state has changed. Each struct's field is still referencing the same IP.

Put in simpler terms

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)

Variables and fields hold values. For classes, those values are references to objects. Two variables or fields can hold the same reference, but that does not mean those variables themselves are linked. They are not connected in anyway, they simply hold a common value. When you replace a value for one variable or field, the other variable is not affected.


Not on the specific topic, but it is worth noting that mutable structs are viewed by many as evil. Others don't quite hold the same view, or at least not as religiously. (However, it is also worth noting that had Address been a class, then address1 and address2 would hold the same value (the reference to the Address object), and modification to the state of address1 would be visible via address2 as long as neither address1 or address2 are themselves reassigned.)

If this is an actual representation of your code, it would be worth doing some research on mutable structs so you at least have a full understanding of various pitfalls you may encounter.

like image 38
Anthony Pegram Avatar answered Oct 08 '22 19:10

Anthony Pegram