Santé

为明天干杯!
posts - 47, comments - 320, trackbacks - 9, articles - 0
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

公告

1 = 1 是真理还是谬误?——谈Property

Posted on 2006-01-05 18:49 smalldust 阅读(...) 评论(...) 编辑 收藏

Property,这个面向时代诞生的宠儿,似乎总是被放在一个被追捧的角色。这个看似数据,实为代码的家伙,为从外部访问Class内部的私有成员提供了一个灵活的方法。

 

MS一声号召:用property吧!于是乎,原来在C++里面由于要提供外部访问而不得不声明为public的成员变量,在C#里面都安安全全地变回private field,然后再通过一个property来作为“代理”进行访问。这个代理的好处就是,可以对访问进行一定的控制。首先是可以允许只读或者只写;其次是,你可以在读写前后执行一些额外的操作,例如validation(验证赋予的值是否正确,是否在范围内等等),formatting(如按照指定的格式输出结果)等等。

 

于是,property被用得满天飞,多么简单的一个field都要用一个property来包装,哪怕只有一条a = valuereturn a

 

的确,property是有很多优点;但是凡事都是双刃剑,property,尤其是其getset的代码较为复杂的情况下,很容易引起各种意想不到的问题。究其原因,其中很重要的一个就是:很多人虽然心里知道property只不过是2method而已,仍然在使用时看作一个普通的field、变量来使用。

 

我们假设有这样一个工厂,其关于机器的一些.Net程序中,有如下一个类:

    class SciFiMachine
    {
        
private string f_name = null;

        
public string Name
        {
            
set
            {
                
if (value == null)
                {
                    
throw new NullReferenceException("Name cannot be null.");
                }
                
if (value == String.Empty)
                {
                    
throw new ArgumentException("Name cannot be empty.");
                }
                f_name 
= value;
            }
            
get
            {
                
if (f_name == null || f_name == String.Empty)
                {
                    
return "DefaultMachineName";
                }
                
else
                {
                    
return f_name;
                }
            }
        }

        
public SciFiMachine()
        {
            
// Some initializations
        }

        
public bool HasName()
        {
            
if (f_name == null)
            {
                
return false;
            }
            
if (f_name == string.Empty)
            {
                
return false;
            }
            
return true;
        }
        
//...
    }

 

这个类描述了一种机器,每台机器都可以有一个名字。对于这个名字对应的property的设计思路是,对于set,不允许设置为null或者为空;对于get则是返回机器的名字;当机器没有名字的时候,简单地返回默认的“DefaultMachineName”即可。

事实上,如果我们认真考虑的话,这个类的设计可能有若干不是很完善的地方;但是看看各种应用程序,包括MSDN上面的范例,很多都是这样的。
使用这个类的程序中,有这样一段:

 

 

            SciFiMachine m1 = new SciFiMachine();
            
//...
            if (! m1.HasName())
            {
                
m1.Name = GetNameFromUserInput();
            }
            
//...

 

 

这段程序的作用,显然是检查一台机器是否已经命名;如果还没有名字就让用户输入一个。这个类和这段程序在其原有的系统上运行正常;但是,当我们对其进行升级的时候,遇到了问题。

 

升级时,我们希望生成一个同名的机器。于是我们像这样修改了程序:

 

 

            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的时候,m1f_name还没有被赋值的话,m2f_name就会等于"DefaultMachineName";这样在下面验证是否有名字的时候,m1就会被GetNameFromUserInput()重新赋值,而m2则一直保持着"DefaultMachineName",根本不符合设计者的初衷。

 

造成这个问题的原因,当然有类的设计的问题、类的使用的问题;可是正如前面所说,像前者那样设计的类,很难说他设计错了;像后者那样使用,也不能说他完全用错了。这样,出现问题了也不大容易发现,因为我们已经习惯了在C++等语言中,使用a=b赋值之后ab一定相等的常识了。此外,在类的内部我们直接操作field,而在外部则通过property进行操作;虽然看起来语法相同,但是如果不分清是field还是property就乱用一气,是很容易造成错误的。

 

因此,在property的世界里,由于property的本质是一个函数而不是一块数据,因此property的世界里没有真正的赋值操作。形式上是A = B的操作,实际上是Set_A(Get_B())的操作;A = B未必一定能把B的值真正赋给A,使用property的时候必须牢记这一规律,谨慎使用。