1、永远使用属性替代可访问的数据成员

 

第一章 C#语言元素

 

如果你今天所做的工作地很好,那么为什么要改变呢?答案是你能做的更好!你改变工具或者语言是因为你能获得更好的生产力(效率)。但是你不改变你的习惯的话,恐怕不能获得预期的收获。尤其面对C#这样的与C++Java同族的语言,很容易让我们保持旧有的习惯。大多旧有的习惯是对的,C#设计者希望你能平衡这些知识,但是他们也增加和改变了一些语法元素来提供与通用语言运行时(CLR)的更好的整合,同时,为面向组件开发提供更好的支持。本章讨论这些你需要改变的习惯以及应该替代的习惯

 1、永远使用属性替代可访问的数据成员

 C#语音把属性从一个特别的习惯提升为首要的语言特征。如果你还是创建公有变量,现在可以停止了;如果你仍旧手工创建getset方法,也停止吧。属性让你将数据成员作为接口的一部分暴露出来的同时依然保持面向对象环境中的封装特性。属性作为语言元素就好像数据成员,但是他们却是通过方法来实现的。

 类型的一些成员确实最好用数据来表示:客户姓名、点的xy坐标或者去年的税收等等。属性可以使你创建一个类似数据存取的接口而且兼有函数的所有好处,客户代码访问属性和访问公共变量一样,但却是通过方法来实现的,通过属性访问器来实现这一点。

 .NET 框架假设你是使用属性而不是使用公有数据成员。事实上,.NET框架中的数据绑定支持属性而不是公有数据成员。无论是Web控件还是Winform控件,数据绑定都是通过对象的属性来和用户界面维系在一起的。数据绑定机制通过反射来查找类型的命名属性。

 textBoxCity.DataBindings.Add("Text", address, "City");

 上述代码将address对象的City属性同textBoxCity控件的Text属性绑定在一起(详情参见第38项)。如果City是公有数据成员的话就不能工作。框架设计者不支持那样的实践。共有数据成员是个糟糕的实践,所以没有对其支持。他们的决定也给你指出了如何正确理解面向对象技术。让我给C++Java程序员一点提示:数据绑定代码也不是setget函数,你应该使用属性来替换在那些语言中使用get_set_函数的习惯。

 是的,数据绑定只适用于显示用户界面逻辑的那些类,但是并不意味着属性也是只适用于UI逻辑的场合,你还可以在其他类和结构中使用属性。当需求和行为变化时,属性更容易应对这种变化。你也许很快就觉得你的customer类型永远也不应该有一个空白的name,如果name是公共属性,那么只在一处修改就可以了:

 

public class Customer

{

    
private string _name;

    
public string Name

    
{

        
get

        
{

            
return _name;

        }


        
set

        
{

            
if((value == null|| (value.Length == 0))

            
{

                
throw new ArguemntException("Name cannot be blank""Name");

            }


            _name 
= value;

        }


    }


 

    
//

}


如果你使用的是公共数据成员,那么你必须修改每一处设置customername的编码来修复这一点,这将花费多得多的时间。

因为属性是用方法来实现的,所以增加多线程支持会很容易。简单的增强getset方法就可以提供数据的同步访问:

 

    public string Name

    
{

        
get

        
{

            
lock (this)

            
{

                
return _name;

            }


        }


        
set

        
{

            
lock (this)

            
{

                _name 
= value;

            }


        }


    }


 属性有方法的所有语言特性。属性可以是虚拟的(virtual)。

 

public class Customer

{

    
private string _name;

 

    
public virtual string Name

    
{

        
get

        
{

            
return _name;

        }


        
set

        
{

            _name 
= value;

        }


    }


 

    
// 忽略其他实现

}


 很容易把属性扩展成抽象的或者作为接口定义的一部分:

 

public interface INameValuePair

{

    
object Name

    
{

        
get;

    }


 

    
object Value

    
{

        
get;

        
set;

    }


}


 最后,当然不止这些,你可以使用接口来创建接口的常量或者非常量版:

 

public interface IConstNameValuePair

{

    
object Name

    
{

        
get;

    }


 

    
object Value

    
{

        
get;

    }


}


 

public interface INameValuePair

{

    
object Value

    
{

        
get;

        
set;

    }


}


 

//用法

public class Stuff : IConstNameValuePair, INameValuePair

{

    
private string _name;

    
private object _value;

 

    
IConstNameValuePair

 

    
INameValuePair Members

}


 属性是非常完善的、首要的语言元素,它扩充了访问或修改内部数据的方法,你使用成员函数能做到的,用属性也一样可以做到。

属性访问器是利用两个分离的方法来作为类型的编译期实现的。在C#2.0中,你可以为getset访问器指定不同的访问修饰符。这使得你可以有更强的访问控制能力。

 

// C# 2.0 中的合法代码

public class Customer

{

    
private string _name;

    
public virtual string Name

    
{

        
get

        
{

            
return _name;

        }


        
protected set

        
{

            _name 
= value;

        }


    }


 

    
// 其他忽略的实现代码

}


 属性语法扩展超越了简单的数据域(fields)。如果你的数据类型的接口部分包含索引项,你可以使用索引器。这对于返回一个队列中的数据项非常有用:

 

    public int this[int index]

    
{

        
get

        
{

            
return _theValues[index];

        }


        
set

        
{

            _theValues[index] 
= value;

        }


    }


 

    
// 访问索引器

    
int val = MyObject[i];

 索引器作为单一的属性有所有的相同的语言支持。因为是利用你写的方法实现的,所以你可以在索引器中进行校验、计算等;索引器可以是虚拟的或者抽象的;可以被声明在接口中;可以是只读的,也可以是可读写的。带数字参数的一维索引器可以精确地进行数据绑定,其他非整形参数实现的索引器可以定义映射和字典:

 

    public Address this[string name]

    
{

        
get

        
{

            
return _theValues[name];

        }


        
set

        
{

            _theValues[name] 
= value;

       }


    }


 为了和C#中的多维数组保持一致,你可以创建多维索引器,每一维可以使用类似或不同的类型:

 

    public int this[int x, int y]

    
{

        
get

        
{

            
return ComputeValue(x, y);

        }


    }


 

    
public int this[int x, string name]

    
{

        
get

        
{

            
return ComputeValue(x, name);

        }


    }


 注意所有的索引器都使用this关键字声明,你不能命名一个索引器。因此,参数相同的索引器只能有一个。

属性从功能上说非常好,是个不错的改进。但是你可能还是试图创建数据成员然后当你需要属性的这些好处的时候用属性来替换数据成员。这听起来是个理由充分的策略但是错了。考虑下面类定义的部分:

 

// using public data members, bad practice

public class Customer

{

    
public string Name;

 

    
// remaining implementation omitted

}


 这里描述了一个客户,客户有一名字。你可以用成员符号来获取和设置名字:

 

string name = customerOne.Name;

customerOne.Name 
= "This Company, Inc.";

 这很简单也很直接。你可能认为你随后可以用属性来替换name数据成员,并且代码不做任何改变就能工作。好的,它的确部分正确。

属性在访问时看上去像是数据成员,这也是新的语法所要达到的目标。但是属性不是数据,对属性的访问和数据成员的访问会产生不同的MSIL代码。对前面客户类型的Name字段产生如下的MSIL

 .field public string Name

 访问字段产生如下语句:

 ldloc.0

ldfld       string NameSpace.Customer::Name

stloc.1

存储字段的值产生如下语句:

ldloc.0

ldstr       "This Company, Inc."

stfld       stirng NameSpace.Customer::Name

别担心我们不打算整天阅读IL代码。但是在这里,重要的是我们打算看看属性和数据成员之间的变化如何破坏二进制兼容性。考虑如下版本的客户类型,它是使用属性来实现的:

public class Customer

{

    
private string _name;

    
public string Name

    
{

        
get

        
{

            
return _name;

        }


        
set

        
{

            _name 
= value;

        }


    }


 

    
// remaining implementation omitted

}



当你写C#代码时,你访问name属性使用极其类似的语法:

string name = customerOne.Name;
customerOne.Name 
= "This Company, Inc."

但是C#编译器对此产生完全不同的MSIL,客户类型如下:

.property instance string Name()

{

    .get instance string NameSpace.Customer::get_Name()

    .set instance void NameSpace.Customer::set_Name(string)

}// end of property Customer::Name

.method public hidebysig specialname instance string

        get_Name() cil managed

{

    // Code size        11(0xb)

    .maxstack   1

    .locals init ([0] stirng CS$$000003$00000000)

    IL_0000:    ldarg.0

    IL_0001:    ldfld   string NameSpace.Customer::_name

    IL_0006:    stloc.0

    IL_0007:    br.s    IL_0009

    IL_0009:    ldloc.0

    IL_000a:    ret

}// end of method Customer::get_Name

.method public hidebysig specialname instance void

        set_Name(string 'value') cil managed

{

    // Code size        8(0x8)

    .maxstack   2

    IL_0000:    ldarg.0

    IL_0001:    ldarg.1

    IL_0002:    stfld       string NameSpece.Customer::_name

    IL_0007:    ret

}// end of method Customer::set_Name

有两点必须理解,那就是属性定义如何被翻译成MSIL。首先,.property指示字定义类属性的类型,并且用函数来实现属性的getset存取器。两个函数被标注了hidebysigspecialname。对我们来说,那些指示意味着这些函数在C#源码中不能直接调用,它们不是正常的类型定义中要考虑的。相反,你应该通过属性来访问它们。

确实,你期待属性定义被产生不同的MSIL,更重要的是,MSILgetset访问属性的方法也发生了改变:

// get

lodloc.0

callvirt    instance stirng NameSpace.CUstomer::get_Name()

stloc.1 

//set

ldloc.0

ldstr       "This Company, Inc."

callvirt    instance void NameSpace.Customer::set_Name(string)

 访问客户姓名的同样的C#原代码被编译成不同的MSIL指令,这依赖于Name成员是属性还是数据成员。访问属性和数据成员使用同样的C#原代码,把属性和数据成员的从源码翻译成不同的ILC#编译器的工作。 

尽管属性和数据成员是源码兼容的,但是它们是二进制不兼容的。很明显,这意味着当你把公共数据成员改成等价的公共属性是,必须重新编译所有的用到公共数据成员的代码。第4章,“创建二进制组件”会详细讨论,现在需要记住吧数据成员改成属性将会破坏二进制兼容性这个事实。这使得升级已发布的独立程序集更加困难。

看到属性相关的IL是,你可能想知道属性和数据成员的性能问题。属性不会比访问数据成员快,但是也慢不到哪去。JIT会内嵌一些方法调用,包括属性访问器。当JIT编译器内嵌属性访问器时,数据成员和属性的性能是等同的。即使属性访问器没有被内嵌,一次函数调用的成本也可忽略不计。只有少数情况下可以测量到。

无论你的类型是用公共的或受保护的接口来暴露数据,都要使用属性。用索引器来访问队列或者字典。所有的数据成员无一例外的私有化。你马上就会得到数据绑定的支持,而且会获得很多方法才能拥有的好处。每天花费一两分钟时间来用属性封装变量,如果是以后在用属性来改进你的设计将花费数小时。现在花点时间,以后会节省你很多时间。

posted @ 2007-01-29 22:20  天狼  阅读(1150)  评论(1编辑  收藏  举报