为什么在更改引用时,可变StringBuilder的行为类似于不可变字符串?

时间:2013-04-05 19:14:55

标签: c#-4.0 immutability stringbuilder reference-type

Albaharis的第4版“C#4.0 IN A NUTSHELL”第249页指出: “...调用object.ReferenceEquals保证了正常的引用相等性。”

所以,我决定对此进行测试。

首先我尝试了这样的值类型。

 int aa1 = 5;
 int aa2 = aa1;
MessageBox.Show("object.ReferenceEquals(aa1,aa2) is: " + object.ReferenceEquals(aa1, aa2)); 

就像我预期的那样,结果是假的: object.ReferenceEquals(aa1,aa2)是: False

生活很美好。 然后我尝试了这样一个可变的引用类型。

System.Text.StringBuilder sblr1 = new System.Text.StringBuilder();
sblr1.Append("aaa");
System.Text.StringBuilder sblr2 = sblr1;          
MessageBox.Show("object.ReferenceEquals(sblr1,sblr2) is: " + object.ReferenceEquals(sblr1, sblr2)); 

就像我预期的那样,结果是真的 object.ReferenceEquals(sblr1,sblr2)是: True

生活还是不错的。 然后我想,因为它是一个可变的引用类型,那么如果我将一个变量更改为null,那么两者都应该为null。 所以我尝试了以下内容。

System.Text.StringBuilder sblr1 = new System.Text.StringBuilder();
sblr1.Append("aaa");
System.Text.StringBuilder sblr2 = sblr1;
sblr1 = null;
MessageBox.Show("object.ReferenceEquals(sblr1,sblr2) is: " + object.ReferenceEquals(sblr1, sblr2)); 

我希望它们都是空的。 但我得到的结果是假的: object.ReferenceEquals(sblr1,sblr2)是: False

现在生活并不那么好。 我认为如果它覆盖了sblr1的内存位置,那么它也会覆盖sblr2的内存位置。

然后我想也许他们指的是两个不同的空值,所以我尝试了这个:

System.Text.StringBuilder sblr1 = new System.Text.StringBuilder();
sblr1.Append("aaa");
System.Text.StringBuilder sblr2 = sblr1;
sblr2 = null;
MessageBox.Show("sblr1 == " + sblr1 + " and sblr2 == " + sblr2);

但是在这里,只有一个指向这样的null。 sblr1 == aaa和sblr2 ==

只有一个是空的。

它显示了我期望从像字符串对象这样的不可变引用类型的行为。 使用字符串对象,我可以这样做:

string aa1 = "aaX";
string aa2 = "aaX";
MessageBox.Show("object.ReferenceEquals(aa1,aa2) is: " + object.ReferenceEquals(aa1, aa2)); 

他们都会像这样引用相同的东西。 object.ReferenceEquals(aa1,aa2)是: True 因为“aaX”只会被写入程序集一次。

但如果我这样做:

string aa1 = "aaX";
string aa2 = "aaX"; 
aa1 = null; 
MessageBox.Show("After aa1 is null(" + aa1 + "), then aa2 is: " + aa2);

然后他们指出不同的事情: 在aa1为null()之后,则aa2为:aaX

那是因为字符串对象是不可变的。内存位置不会被覆盖。相反,变量指向堆内存中存在新值的不同位置。 在上面的示例中将aa1更改为null意味着aa1将指向堆内存中的其他位置。

为什么可变引用类型的行为与不可变引用类型的行为相同?


编辑下午4:03和4:08

我最近尝试过这个:

System.Text.StringBuilder sblr1 = new System.Text.StringBuilder();
sblr1.Append("aaa");

// sblr1 and sblr2 should now both point to the same location on the Heap that has "aaa".
System.Text.StringBuilder sblr2 = sblr1;

System.Text.StringBuilder sblr3 = new System.Text.StringBuilder();
sblr3.Append("bbb");

sblr1 = sblr3;
MessageBox.Show("sblr1 == " + sblr1 + " and sblr2 == " + sblr2 + " and sblr3 == " + sblr3);

哪位给了我: sblr1 == bbb和sblr2 == aaa和sblr3 == bbb

这更像是我期待的结果。 我现在看到了,感谢这些评论,我绝对想要将null视为内存位置。

3 个答案:

答案 0 :(得分:1)

  

我认为如果它覆盖了sblr1的内存位置,那么它也会覆盖sblr2的内存位置。

这是你的误解。

当你这样写:

System.Text.StringBuilder sblr2 = sblr1;

您要将sblr2变量指定为与StringBuilder指向的同一sblr1实例的引用。这两个变量现在指向相同的参考。

然后写下:

sblr1 = null;

这会将sblr1变量更改为现在为空引用。您根本没有更改内存中的实例。

这与引用是否是可变类型无关。您正在更改变量,而不是它们引用的实例。

至于你的字符串示例:

  

那是因为字符串对象是不可变的。内存位置不会被覆盖

实际上并非如此。您将一个字符串变量设置为null的事实与字符串不可变实际上没有任何关系。这是一个单独的问题。

  

为什么可变引用类型的行为与不可变引用类型的行为相同?

您所看到的行为与可变性无关。它是所有引用类型的标准行为(无论是不可变的还是可变的)。可变性是一个不同的问题。

可变性的主要问题是:

假设你有一个类,如下:

class Foo
{
    public int Bar { get; set; }
}

如果你这样写:

Foo a = new Foo();
a.Bar = 42;
Foo b = a;
b.Bar = 54;
Console.WriteLine(a.Bar); // Will print 54, since you've changed the same mutable object

对于不可变类型,这不可能发生,因为你不能改变Bar - 相反,如果你创建了一个不可变类:

class Baz
{
    public Baz(int bar) { this.Bar = bar; }

    public int Bar { get; private set; }
}

你需要写:

Baz a = new Baz(42);
Baz b = a;

// This isn't legal now:
// b.Bar = 54;
// So you'd write:
b = new Baz(54); // Creates a new reference

或者,您可以让类返回“更改”操作的新引用,即:

class Baz
{
    public Baz(int bar) { this.Bar = bar; }

    public int Bar { get; private set; }

    public Baz Alter(int newValue) { return new Baz(newValue); } // May copy other data from "this"
}

然后当你写:

Baz a = new Baz(42);
Baz b = a.Alter(54); // b is now a new instance

这是string发生的事情 - 所有方法都返回一个新实例,因为字符串是不可变的,所以你永远不能“改变”现有的副本。

答案 1 :(得分:0)

这与可变性有关。此处涉及的规则与所有引用类型相同。引用类型中的非refout变量(或集合中的成员或插槽)是引用(duh)。这意味着它指的是一些对象。它引用另一个引用,或引用另一个引用的位置(例如,变量)。分配给变量(或集合中的成员或插槽)时,您可以更改该位置的引用;你不会覆盖任何对象的任何部分(当然,你指定的成员除外,如果它是成员)。

在您的代码中,有两个变量srbl1srbl2,每个变量都存储对同一字符串构建器对象的引用。分配给任一更改会覆盖其中一个引用(例如,使用null,或引用另一个对象)。

答案 2 :(得分:0)

更改引用只是改变某些内容所指的内容。它不会改变对象本身。

查看它的一种方法是想象一个整数数组:

int[] foo = new int[] {0, 1, 2, 3, 4, 5};

您可以创建两个引用数组中项目的索引:

int ix = 1;
int iy = ix;

然后foo[ix] == foo[iy]

如果您随后写foo[ix] = 42,那么foo[ix] == foo[iy]仍然是真的,因为您更改了索引所引用的值。

但如果您更改索引以便ix = 3,则ixiy指的是不同的内容。

参考类型的工作方式完全相同。

当您编写sblr1 = new StringBuilder()时,您已创建了一个新的StringBuilder对象实例,并使sblr1指向它。如果你然后写sblr2 = sblr1,你只是让sblr2指向同一个东西。然后sblr1 = null只是说,“sblr1不再指向任何东西了。”它实际上并不影响它引用的项目,就像将索引更改为数组不会影响被索引项目的值一样。