C#中引用类型搭配私有Setter与公共Getter的行为疑问
好问题!这可是C#里不少新手容易踩的坑,我给你把这个逻辑掰扯明白。
首先先确认你已经搞懂的部分:对于valueType这种值类型,{ get; private set; }确实能完美实现“仅Foo类内部可写”的需求。因为值类型的get返回的是原数据的副本,外部就算修改这个副本也影响不到Foo实例里的原数据,再加上set是私有,外部根本没法直接给valueType赋值,所以完全安全。
但轮到fooBar这种嵌套的引用类型,情况就不一样了——private set只限制了引用本身的赋值权限,而不是引用指向的对象的内部状态!
具体来说是这样的:
fooBar的private set意味着外部代码永远不能做这件事:
var foo = new Foo(); foo.fooBar = new Foo.Bar(); // 编译报错!因为set是私有,外部无法重新赋值这个引用
但如果你的Bar类里有公共的可写成员(比如公共属性、字段),外部通过get拿到fooBar的引用后,完全可以修改Bar对象的内部内容。举个实际的代码例子:
假设你的Foo和Bar是这样实现的:
public class Foo { public class Bar { // 公共可写属性 public string InnerText { get; set; } // 公共字段 public int Counter; } public Bar fooBar { get; private set; } public int valueType { get; private set; } public Foo() { // 内部初始化fooBar引用 fooBar = new Bar(); } }
那外部代码可以轻松修改Bar的内部:
var fooInstance = new Foo(); // 完全合法!修改Bar对象的内部属性 fooInstance.fooBar.InnerText = "我是外部修改的内容"; // 同样合法!修改Bar对象的公共字段 fooInstance.fooBar.Counter = 999;
为什么会这样?
这本质是C#中引用类型的特性:当你通过get获取fooBar时,拿到的是指向堆上Bar对象的引用地址,而不是对象本身的副本。private set只管你能不能换这个地址(也就是给fooBar重新赋值新的Bar对象),但管不了你通过这个地址去修改对象内部的东西——只要Bar的成员是外部可访问的,就能改。
怎么解决?
如果想让Bar对象的内部也不能被外部修改,你需要把Bar设计成不可变类型,或者限制它的成员访问权限:
- 把Bar的所有可写成员改成仅内部可写,或者只提供getter:
public class Bar { // 只读属性,只能在构造函数或内部赋值 public string InnerText { get; } // 私有字段,配合内部方法修改 private int _counter; // 只读的公共访问器 public int Counter => _counter; // 构造函数初始化 public Bar(string innerText) { InnerText = innerText; } // 只有Foo内部能调用的修改方法 internal void IncreaseCounter() { _counter++; } }
- 或者直接把Bar的成员都设为私有,只通过Foo类提供的方法来修改Bar的状态——相当于把Bar的修改权限完全收拢到Foo内部。
这样一来,外部拿到fooBar的引用后,只能读取内容,没法做任何修改,就和值类型的valueType一样安全了。
内容的提问来源于stack exchange,提问作者c-a

