Effective C# Item23:避免返回内部类对象的引用
当我们将属性置为只读后,就可以认为调用者对该属性的值不可以进行更改了吗?答案是否定的。对于值类型来说,将其置为只读,确实可以让调用者不能够对其进行修改;但是对于引用类型来说,调用者还是可以调用引用对象的公有成员,包括那些可以修改属性状态的成员。
我们来看下面的代码。

1 public struct Employee
2 {
3 private string m_strName;
4 public string Name
5 {
6 get { return m_strName; }
7 set { m_strName = value; }
8 }
9
10 private int m_nAge;
11 public int Age
12 {
13 get { return m_nAge; }
14 set { m_nAge = value; }
15 }
16
17 public Employee(string name, int age)
18 {
19 m_strName = name;
20 m_nAge = age;
21 }
22
23 public override string ToString()
24 {
25 return string.Format("Name : {0}, Age : {1}.", m_strName, m_nAge);
26 }
27 }

1 private static void Test()
2 {
3 Employee emp = new Employee("Wing", 24);
4 Console.WriteLine(emp.ToString());
5 ChangeName(emp.Name);
6 ChangeAge(emp.Age);
7 Console.WriteLine(emp.ToString());
8 ChangeEmp(emp);
9 Console.WriteLine(emp.ToString());
10
11 }
12
13 private static int ChangeAge(int age)
14 {
15 age += 1;
16 return age;
17 }
18
19 private static string ChangeName(string name)
20 {
21 name = "UnKnown";
22 return name;
23 }
24
25 private static void ChangeEmp(Employee emp)
26 {
27 emp.Name = "Unkown";
28 emp.Age += 1;
29 }
如果将上面代码中Employee的类型由struct变为class,其他代码不变,执行结果如下。
通过上述代码,我们可以看到,对于值类型来说,在方法传递的过程中,是将对应的值进行复制后传递的;但是对于引用类型来说,是直接传递的引用对象的值。因此,通过调用方法对值类型的改变并不会对原有值类型的值,但是对于引用类型来说,就直接改变了原有引用对象的值。
我们可以通过以下四种方式来防止类型内部的数据结构遭到无意的改变:
- 值类型,就像上面代码中的Age,属于int类型。
- 常量类型,就像上面代码中的Name,属于string类型,对它的任何改变,都会直接创建一个新的string对象。
- 接口,在方法传递的过程中,不直接传递类的类型,而改用类实现的接口的类型,这样可以降低调用方可以调用的方法,它只能调用接口中定义的成员。
- 包装器,我们可以新建一个类,用于返回指定类型的数据,在返回的逻辑中,可以添加逻辑,对返回值是否只读进行限制。
上述四种方式中,前三种方式都已经在示例代码中进行说明了,我们主要看第四种方式,来看下面的代码。

1 public class MyBusinessObject
2 {
3 // Read Only property providing access to a
4 // private data member:
5 private DataSet _ds;
6 public IList this[ string tableName ]
7 {
8 get
9 {
10 DataView view =
11 _ds.DefaultViewManager.CreateDataView
12 ( _ds.Tables[ tableName ] );
13 view.AllowNew = false;
14 view.AllowDelete = false;
15 view.AllowEdit = false;
16 return view;
17 }
18 }
19 }
20
21
22 // Access the dataset:
23 IList dv = bizOjb[ "customers" ];
24 foreach ( DataRowView r in dv )
25 Console.WriteLine( r[ "name" ] );
总结:将引用类型通过公共接口暴露给外界,将使得类型的用户不用通过我们定义的方法和属性,就能够更改对象的内部结构,这违反了我们通常的直觉,会导致常见的错误。如果我们到处的是引用而非值,那就需要改变类型的接口,如果只是简单的返回内部数据,那么我们实际上就给外界赋予了访问内部成员的权限。客户代码可以调用成员中任何可用的方法。通过使用接口或者包装器对象向外界提供内部的私有数据,我们可以限制外界对它们的访问能力。当希望客户代码更改内部数据元素时,我们应该实现Observer模式,以使对象可以对更改进行校验或响应。
作者:李潘
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通