关于c#:包含引用类型的Struct

Struct containing reference types

结构是值类型,因此如果我将一个结构赋给另一个结构,它的字段将复制到第二个结构中。但是,如果结构的某些字段是引用类型,会发生什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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);
}

输出是:

1
2
127.0.0.1:8080
255.255.255.255:9090

为什么address1IP(字符串,即引用类型)没有改变?如果我用IPAddress替换string来表示MyIPEndPoint中的IP,也会发生同样的行为:虽然IPAddress是一个类(即引用类型),但它不表现为引用类型。为什么?

事实上,如果我用一个新的简单类MyIP来包装表示IP的string,行为就会改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyIP
{
    public string IpAsString;

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

当然,您还应该按以下方式调整MyIPEndPoint结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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中,我只修改了一个声明:

1
2
3
4
5
6
7
8
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);

现在输出为:

1
2
255.255.255.255:8080
255.255.255.255:9090

在第一种情况下,我期待着这个输出。为什么在第一种情况下引用的行为不符合预期?


考虑你的第一个例子。你有两个抽屉,标有"地址一"和"地址二"。两个抽屉都是空的。

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

现在你拿到一张纸,在上面写上"127.0.0.18080",然后把它放在抽屉1里。

1
MyIPEndPoint address2 = address1;

现在你拿一台复印机,在"地址一"抽屉里复印一份文件,然后把复印件放在"地址二"抽屉里。

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

现在,你把地址2抽屉里的那张纸划掉,换成新的文本。

抽屉一的纸没变。它仍然和以前一样。

现在考虑第二个例子。现在你有两个空抽屉,和以前一样,还有一本空白纸。

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

你随机挑选一页书,并将其命名为"参考书"。在那一页上写着"127.0.0.1"。你拿一张空纸,在那张纸上写上"参考号8080",然后把它贴在标有"地址一"的抽屉里。

1
MyIPEndPoint address2 = address1;

你在"地址一"中复印一份文件,然后把复印件放在"地址二"中。

1
address2.IP.IpAsString ="255.255.255.255";

你打开抽屉"地址二",看到上面写着"参考一"。你翻阅这本书,直到找到一页写着"参考书"。你把那里的东西划掉,换成新的文字。

1
address2.Port = 9090;

你打开抽屉"地址二",划掉"8080",换成"9090"。你把参考号放在原处。

完成后,抽屉"地址1"包含"参考1,8080",抽屉"地址2"包含"参考1,9090",书上有一页写着"参考1:255.255.255"。

现在,您是否了解引用类型和值类型之间的区别?


您已经正确理解,对于结构,Address1和Address2不是同一对象。已复制值。但是,对于字段,这是一个简单的重新分配案例。它与字符串是引用类型、任何特殊规则或任何不变性建议无关。您只需使用另一个值重新分配属性或字段。

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

您已经覆盖了本例中的引用。在一小段时间内,两个结构的字段都包含相同的引用,这一事实并不重要。在第二个例子中,您做了一些非常不同的事情。

1
2
3
someStruct.IP.SomeString ="A";
anotherStruct = someStruct;
anotherStruct.IP.SomeString ="B";

在这种情况下,IP的值没有改变。IP的部分状态已更改。每个结构的字段仍然引用相同的IP。

简单地说

1
2
3
4
5
6
7
8
9
10
11
12
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)

变量和字段保存值。对于类,这些值是对对象的引用。两个变量或字段可以保持相同的引用,但这并不意味着这些变量本身是链接的。不管怎样,它们都没有连接,它们只是持有一个共同的价值。当替换一个变量或字段的值时,另一个变量不受影响。

不是在特定的主题上,但值得注意的是可变结构被许多人视为邪恶。其他人并不完全持有相同的观点,或者至少没有那么虔诚。(然而,也值得注意的是,如果地址是一个类,那么地址1和地址2将保持相同的值(对地址对象的引用),并且只要地址1或地址2本身都没有重新分配,就可以通过地址2看到对地址1状态的修改。)

如果这是代码的实际表示,那么就值得对可变结构进行一些研究,这样至少可以充分理解可能遇到的各种陷阱。