当我们将属性置为只读后,就可以认为调用者对该属性的值不可以进行更改了吗?答案是否定的。对于值类型来说,将其置为只读,确实可以让调用者不能够对其进行修改;但是对于引用类型来说,调用者还是可以调用引用对象的公有成员,包括那些可以修改属性状态的成员。
我们来看下面的代码。
public struct Employee
{
private string m_strName;
public string Name
{
get { return m_strName; }
set { m_strName = value; }
}
private int m_nAge;
public int Age
{
get { return m_nAge; }
set { m_nAge = value; }
}
public Employee(string name, int age)
{
m_strName = name;
m_nAge = age;
}
public override string ToString()
{
return string.Format("Name : {0}, Age : {1}.", m_strName, m_nAge);
}
}
下面是测试代码。
private static void Test()
{
Employee emp = new Employee("Wing", 24);
Console.WriteLine(emp.ToString());
ChangeName(emp.Name);
ChangeAge(emp.Age);
Console.WriteLine(emp.ToString());
ChangeEmp(emp);
Console.WriteLine(emp.ToString());
}
private static int ChangeAge(int age)
{
age += 1;
return age;
}
private static string ChangeName(string name)
{
name = "UnKnown";
return name;
}
private static void ChangeEmp(Employee emp)
{
emp.Name = "Unkown";
emp.Age += 1;
}
下面是执行结果。
通过上述代码,我们可以看到,对于值类型来说,在方法传递的过程中,是将对应的值进行复制后传递的;但是对于引用类型来说,是直接传递的引用对象的值。因此,通过调用方法对值类型的改变并不会对原有值类型的值,但是对于引用类型来说,就直接改变了原有引用对象的值。
我们可以通过以下四种方式来防止类型内部的数据结构遭到无意的改变:
- 值类型,就像上面代码中的Age,属于int类型。
- 常量类型,就像上面代码中的Name,属于string类型,对它的任何改变,都会直接创建一个新的string对象。
- 接口,在方法传递的过程中,不直接传递类的类型,而改用类实现的接口的类型,这样可以降低调用方可以调用的方法,它只能调用接口中定义的成员。
- 包装器,我们可以新建一个类,用于返回指定类型的数据,在返回的逻辑中,可以添加逻辑,对返回值是否只读进行限制。
上述四种方式中,前三种方式都已经在示例代码中进行说明了,我们主要看第四种方式,来看下面的代码。
public class MyBusinessObject
{
// Read Only property providing access to a
// private data member:
private DataSet _ds;
public IList this[ string tableName ]
{
get
{
DataView view =
_ds.DefaultViewManager.CreateDataView
( _ds.Tables[ tableName ] );
view.AllowNew = false;
view.AllowDelete = false;
view.AllowEdit = false;
return view;
}
}
}
// Access the dataset:
IList dv = bizOjb[ "customers" ];
foreach ( DataRowView r in dv )
Console.WriteLine( r[ "name" ] );
上述代码中,结合了第三种方式和第四种方式,首先属性的返回值由DateView变成了IList,这样调用方只能够调用IList类型中定义的成员;其次,我们在返回IList之前,将DataView的AllowNew、AllowDelete和AllowEdit属性都设置为false,这样也能够阻止调用方对其进行改动。
总结:将引用类型通过公共接口暴露给外界,将使得类型的用户不用通过我们定义的方法和属性,就能够更改对象的内部结构,这违反了我们通常的直觉,会导致常见的错误。如果我们到处的是引用而非值,那就需要改变类型的接口,如果只是简单的返回内部数据,那么我们实际上就给外界赋予了访问内部成员的权限。客户代码可以调用成员中任何可用的方法。通过使用接口或者包装器对象向外界提供内部的私有数据,我们可以限制外界对它们的访问能力。当希望客户代码更改内部数据元素时,我们应该实现Observer模式,以使对象可以对更改进行校验或响应。