25

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?

enzom83
  • 8,080
  • 10
  • 68
  • 114

2 Answers2

28

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?

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Yes, this example helped me to better understand the difference between reference and value types! – enzom83 Feb 09 '12 at 01:08
19

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.

Community
  • 1
  • 1
Anthony Pegram
  • 123,721
  • 27
  • 225
  • 246
  • I understand the reason: in other words, if the IPAddress had its fields public, I could reproduce the same result. – enzom83 Feb 09 '12 at 00:41
  • 1
    If you could modify the state of the object referenced by the string variable (or field) without actually overwriting the reference, you could observe changes in both structs. But you didn't modify the state (and you can't, at least not in safe code), you overwrote the reference entirely via reassignment. In the second, you modified the state of the object that was referenced by both struct instances. The value of IP, *the reference to the object*, did not change, so you observed the results in both. – Anthony Pegram Feb 09 '12 at 00:45
  • 1
    I'm not sure your statement is true, "It has nothing to do with the fact that string is a reference type or any special rules or any suggestion of immutability." If a string were a conventional reference type, then changing the value via one reference would change it for all references. But string is an immutable reference type, which means it behaves like a value type. http://stackoverflow.com/questions/636932/in-c-why-is-string-a-reference-type-that-behaves-like-a-value-type. Your statement is true if you assume "=" always means assign reference, but to some it means copy value. – BlueMonkMN Oct 10 '12 at 16:36
  • @BlueMonkMN, a string *is* a conventional reference type. Don't mistake easy equality checking and literal assignment support to mean it doesn't also behave like other reference types (or any type) as far as reassignment goes. "=" means assign value, whether you are talking about a value type or a reference type. The difference is that for reference types, *the value is the reference.* But whether you are talking int, string, or your own custom mutable class Foo, when you use "=" to assign to the variable, you are overwriting that variable with a new value. – Anthony Pegram Oct 10 '12 at 17:49
  • "If a string were a conventional reference type, then changing the value via one reference would change it for all references." Again, let's be clear. If you have `Foo a = new Foo(); a.Bar = 1; Foo b = a; a = new Foo(); a.Bar = 2;`, the reassignment and mutation of `a` has *nothing at all to do with `b`*. "Changing the value of a reference type" is not to be confused with "changing the state of the *object* referenced by a variable." A string behaves like any other value or reference type here. Attributions to "immutablity" only serve to cloud the incredibly simple issue of reassignment. – Anthony Pegram Oct 10 '12 at 17:54
  • @AnthonyPegram Everything you say is correct. And my comment may be superfluous, but my only point was that string objects, unlike most reference types, do not allow you to change their value. You're right that "=" behaves the same for strings and all other reference types. But there is no simple way (that I can think of off the top of my head) to *demonstrate* the fact that a string is a reference type by changing its value. In that sense it is an unconventional reference type. Sorry for confusing my comment by referring to "=". I brought that up as a way one might *try* to do so, but fail. – BlueMonkMN Oct 16 '12 at 13:31