1 = 1 是真理还是谬误?——谈Property
Property,这个面向时代诞生的宠儿,似乎总是被放在一个被追捧的角色。这个看似数据,实为代码的家伙,为从外部访问Class内部的私有成员提供了一个灵活的方法。
MS一声号召:用property吧!于是乎,原来在C++里面由于要提供外部访问而不得不声明为public的成员变量,在C#里面都安安全全地变回private field,然后再通过一个property来作为“代理”进行访问。这个代理的好处就是,可以对访问进行一定的控制。首先是可以允许只读或者只写;其次是,你可以在读写前后执行一些额外的操作,例如validation(验证赋予的值是否正确,是否在范围内等等),formatting(如按照指定的格式输出结果)等等。
于是,property被用得满天飞,多么简单的一个field都要用一个property来包装,哪怕只有一条a = value和return a。
的确,property是有很多优点;但是凡事都是双刃剑,property,尤其是其get、set的代码较为复杂的情况下,很容易引起各种意想不到的问题。究其原因,其中很重要的一个就是:很多人虽然心里知道property只不过是2个method而已,仍然在使用时看作一个普通的field、变量来使用。
我们假设有这样一个工厂,其关于机器的一些.Net程序中,有如下一个类:
这个类描述了一种机器,每台机器都可以有一个名字。对于这个名字对应的property的设计思路是,对于set,不允许设置为null或者为空;对于get则是返回机器的名字;当机器没有名字的时候,简单地返回默认的“DefaultMachineName”即可。
事实上,如果我们认真考虑的话,这个类的设计可能有若干不是很完善的地方;但是看看各种应用程序,包括MSDN上面的范例,很多都是这样的。
使用这个类的程序中,有这样一段:
这段程序的作用,显然是检查一台机器是否已经命名;如果还没有名字就让用户输入一个。这个类和这段程序在其原有的系统上运行正常;但是,当我们对其进行升级的时候,遇到了问题。升级时,我们希望生成一个同名的机器。于是我们像这样修改了程序:
Code
SciFiMachine m1 = new SciFiMachine();
SciFiMachine m2 = new SciFiMachine();
m2.Name = m1.Name;
if (! m1.HasName())
{
m1.Name = GetNameFromUserInput();
}
if (! m2.HasName())
{
m2.Name = GetNameFromUserInput();
}
上述的逻辑,如果是在C++世界里将不会有任何问题;可是在.Net的世界里,这却是一段会出错的程序。
比如,如果执行到m2.Name = m1.Name的时候,m1的f_name还没有被赋值的话,m2的f_name就会等于"DefaultMachineName";这样在下面验证是否有名字的时候,m1就会被GetNameFromUserInput()重新赋值,而m2则一直保持着"DefaultMachineName",根本不符合设计者的初衷。
造成这个问题的原因,当然有类的设计的问题、类的使用的问题;可是正如前面所说,像前者那样设计的类,很难说他设计错了;像后者那样使用,也不能说他完全用错了。这样,出现问题了也不大容易发现,因为我们已经习惯了在C++等语言中,使用a=b赋值之后a和b一定相等的常识了。此外,在类的内部我们直接操作field,而在外部则通过property进行操作;虽然看起来语法相同,但是如果不分清是field还是property就乱用一气,是很容易造成错误的。
因此,在property的世界里,由于property的本质是一个函数而不是一块数据,因此property的世界里没有真正的赋值操作。形式上是A = B的操作,实际上是Set_A(Get_B())的操作;A = B未必一定能把B的值真正赋给A,使用property的时候必须牢记这一规律,谨慎使用。
Code
SciFiMachine m1 = new SciFiMachine();
if (! m1.HasName())
{
m1.Name = GetNameFromUserInput();
}
Code