C#语言已经将属性的地位从编程习惯提升为良好的语言特征。如果你仍在自己的类型中创建公共变量,建议不要这样做;也不要手工创建get 和set方法。属性暴露数据成员作为公共接口的一部分并提供面向对象环境中的封装功能。属性是一种语言元素,访问它们如同访问数据成员,但是它们由方法来实现。
类型的一些成员,数据成员是它们最好的表现形式:客户的名字,点的x,y坐标,去年的税收。属性允许你创建一个接口,用起来如同数据访问一样,但它仍保留了成员函数的优势。客户代码访问属性如同访问公共成员,但实际上是由你定义的属性访问器的行为实现的。
.NET框架假定你会为公共数据成员使用属性。实际上,.net框架中的数据绑定用属性而不是公共数据成员。数据绑定把对象的属性与用户接口型控件关联起来,或web控件或windows forms控件。数据绑定机制利用反射去类型中找到指定的属性:
数据绑定只应用在这些包含用于在用户接口逻辑中显示的元素的类中。但这并不意味着属性只应用于UI逻辑。你仍需在其他类和结构中使用属性。随着时间的过去,属性往往比较容易修改以适应新的需求。你可能决定你的客户类型中不允许存在一个空名。如果你用Name属性,只要修改一个地方:
因为属性由方法实现,所以让它支持多线程是比较简单的。简单的增强get和set方法的实现以提供数据的同步访问:
属性的访问器是两个分离的方法,编译后陷入你的类型中。在c#2.0中你可以在你的属性中对get和set访问器指定不同的访问权限。这种机制提供了更大的对你暴露的数据成员可见性的控制:
属性功能健全,但仍有人会先使用数据成员当需要用到属性的特性时再改成属性替换之,这似乎是有道理的,但这是不对的,看下面类定义中的片段:
属性访问起来象是数据成员。这就是新语法背后的目的。但属性不是数据成员,它们的MSIL是截然不同的。先前的Customer类型为数据成员Name产生下面的MSIL:
当然,你希望属性定义产生不同的MSIL代码,比较重要的一点,属性的get和set访问器产生不同的MSIL代码:
尽管属性和数据成员是代码兼容的,但并不是二进制兼容的。显而易见,这意味着当你把数据成员修改为属性后,你必须重新编译用到数据成员的所有代码。第四章,“创建二进制组件”,详细讨论二进制组件,我们必须记住当把数据成员简单的修改为属性时破坏了二进制兼容性。这使得升级单一的程序集变的更加困难。
当你查看属性的IL代码时,你可能想知道属性和数据成员执行的关系。属性不会比数据成员快但也不会慢多少。JIT编译器做了一些内联方法调用,包括属性访问器。当JIT编译器内联属性访问器时,数据成员和属性的执行是一样的。甚至当属性并没被内联处理时,实际的执行区别仅在于可以忽略的一个函数调用的开销。这仅在小数目的情况下测试通过。
无论你在接口中暴露公共或保护类型的数据,请使用属性。对序列或dictionay使用索引器。所有的数据成员必须声明为私有,没有例外。你立即得到数据绑定的支持,在将来某一时候改变方法的实现也变的容易。额外的就是你得花费一到二分钟的打字时间把变量包装成属性。迟些时候寻找需要使用属性的地方来正确的表达你的设计将花费点时间,现在就抽出一点时间吧,这将为你今后节省时间。
类型的一些成员,数据成员是它们最好的表现形式:客户的名字,点的x,y坐标,去年的税收。属性允许你创建一个接口,用起来如同数据访问一样,但它仍保留了成员函数的优势。客户代码访问属性如同访问公共成员,但实际上是由你定义的属性访问器的行为实现的。
.NET框架假定你会为公共数据成员使用属性。实际上,.net框架中的数据绑定用属性而不是公共数据成员。数据绑定把对象的属性与用户接口型控件关联起来,或web控件或windows forms控件。数据绑定机制利用反射去类型中找到指定的属性:
textBoxCity.DataBindings.Add( "Text",
address, "City" );
上面的代码把address对象的City属性绑定到textBoxCity控件的Text属性(更多请参见Item38)。如果使用名为City的公共数据成员,那么上面的功能将不能实现;因为.net框架类库设计者不允许此种方式。使用公共数据成员是坏习惯,所有类库设计者不提供对它们的支持。它们的建议是另一个让你去遵循正确的面向对象技术的原因。address, "City" );
数据绑定只应用在这些包含用于在用户接口逻辑中显示的元素的类中。但这并不意味着属性只应用于UI逻辑。你仍需在其他类和结构中使用属性。随着时间的过去,属性往往比较容易修改以适应新的需求。你可能决定你的客户类型中不允许存在一个空名。如果你用Name属性,只要修改一个地方:
public class Customer
{
private string _name;
public string Name
{
get
{
return _name;
}
set
{
if (( value == null ) ||
( value.Length == 0 ))
throw new ArgumentException( "Name cannot be blank",
"Name" );
_name = value;
}
}
//
}
如果你使用了公共数据成员,你将在代码的每个角落寻找并替换客户的名字,这将花费不少时间。{
private string _name;
public string Name
{
get
{
return _name;
}
set
{
if (( value == null ) ||
( value.Length == 0 ))
throw new ArgumentException( "Name cannot be blank",
"Name" );
_name = value;
}
}
//
}
因为属性由方法实现,所以让它支持多线程是比较简单的。简单的增强get和set方法的实现以提供数据的同步访问:
public string Name
{
get
{
lock( this )
{
return _name;
}
}
set
{
lock( this )
{
_name = value;
}
}
}
属性拥有方法所有的语言特征。属性可以是virtual的:{
get
{
lock( this )
{
return _name;
}
}
set
{
lock( this )
{
_name = value;
}
}
}
public class Customer
{
private string _name;
public virtual string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
// remaining implementation omitted
}
你可以将属性声明为abstract甚至是接口定义的一部分:{
private string _name;
public virtual string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
// remaining implementation omitted
}
public interface INameValuePair
{
object Name
{
get;
}
object Value
{
get;
set;
}
}
你也可以创建接口的const和nonconst版本:{
object Name
{
get;
}
object Value
{
get;
set;
}
}
public interface IConstNameValuePair
{
object Name
{
get;
}
object Value
{
get;
}
}
public interface INameValuePair
{
object Value
{
get;
set;
}
}
// Usage:
public class Stuff : IConstNameValuePair, INameValuePair
{
private string _name;
private object _value;
#region IConstNameValuePair Members
public object Name
{
get
{
return _name;
}
}
object IConstNameValuePair.Value
{
get
{
return _value;
}
}
#endregion
#region INameValuePair Members
public object Value
{
get
{
return _value;
}
set
{
_value = value;
}
}
#endregion
}
属性是健全的一流的语言元素,是方法的扩展用来访问或修改内部数据。任何可以用成员函数处理的,你也可以用属性处理之。{
object Name
{
get;
}
object Value
{
get;
}
}
public interface INameValuePair
{
object Value
{
get;
set;
}
}
// Usage:
public class Stuff : IConstNameValuePair, INameValuePair
{
private string _name;
private object _value;
#region IConstNameValuePair Members
public object Name
{
get
{
return _name;
}
}
object IConstNameValuePair.Value
{
get
{
return _value;
}
}
#endregion
#region INameValuePair Members
public object Value
{
get
{
return _value;
}
set
{
_value = value;
}
}
#endregion
}
属性的访问器是两个分离的方法,编译后陷入你的类型中。在c#2.0中你可以在你的属性中对get和set访问器指定不同的访问权限。这种机制提供了更大的对你暴露的数据成员可见性的控制:
// Legal C# 2.0:
public class Customer
{
private string _name;
public virtual string Name
{
get
{
return _name;
}
protected set
{
_name = value;
}
}
// remaining implementation omitted
}
属性远远扩展了简单的数据字段。如果你的类型需要包含索引item的功能作为接口的一部分,你可以使用索引器(带参数的属性)。 在序列中返回item是一种很好的创建属性的方法:public class Customer
{
private string _name;
public virtual string Name
{
get
{
return _name;
}
protected set
{
_name = value;
}
}
// remaining implementation omitted
}
public int this [ int index ]
{
get
{
return _theValues [ index ] ;
}
set
{
_theValues[ index ] = value;
}
}
// Accessing an indexer:
int val = MyObject[ i ];
索引器作为简单的属性拥有所有属性所拥有的语言特性:它们由你编写的方法实现,所以你可以在其中进行验证和计算。索引器可以是virtual,abstract,可以在接口中声明,只读或读写。一维数字型的索引器可以参与数据绑定。也可以用非整型的参数的索引器定义map和dictionary:{
get
{
return _theValues [ index ] ;
}
set
{
_theValues[ index ] = value;
}
}
// Accessing an indexer:
int val = MyObject[ i ];
public Address this [ string name ]
{
get
{
return _theValues[ name ] ;
}
set
{
_theValues[ name ] = value;
}
}
c#中为了保持与多维array一致,你可以创建多维索引器,在每个轴上用相似或不同的类型:{
get
{
return _theValues[ name ] ;
}
set
{
_theValues[ name ] = value;
}
}
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关键字声明,你无法命名一个索引器。所以在你的类型中带同样参数的索引器最多只有一个。{
get
{
return ComputeValue( x, y );
}
}
public int this[ int x, string name ]
{
get
{
return ComputeValue( x, name );
}
}
属性功能健全,但仍有人会先使用数据成员当需要用到属性的特性时再改成属性替换之,这似乎是有道理的,但这是不对的,看下面类定义中的片段:
// using public data members, bad practice:
public class Customer
{
public string Name;
// remaining implementation omitted
}
这描述了一个客户,带一个Name字段。你可以通过相似的成员符号获取或设置客户的名字:public class Customer
{
public string Name;
// remaining implementation omitted
}
string name = customerOne.Name;
customerOne.Name = "This Company, Inc.";
这简单而又直接,某一时间你又可以用属性代替Name数据成员,代码照常运行无需修改,但这只有几分正确。customerOne.Name = "This Company, Inc.";
属性访问起来象是数据成员。这就是新语法背后的目的。但属性不是数据成员,它们的MSIL是截然不同的。先前的Customer类型为数据成员Name产生下面的MSIL:
.field public string Name
访问这个字段时产生如下的描述:ldloc.0
ldfld string NameSpace.Customer::Name
stloc.1
对这个字段存储一个值时产生如下代码:ldfld string NameSpace.Customer::Name
stloc.1
ldloc.0
ldstr "This Company, Inc."
stfld string NameSpace.Customer::Name
别担心我们不用整天看IL代码,但在这里是重要的,因为我们需要了解数据成员与属性之间转变破坏二进制兼容性的区别。考虑Customer类型的这个版本:ldstr "This Company, Inc."
stfld string NameSpace.Customer::Name
public class Customer
{
private string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
// remaining implementation omitted
}
当你编写c#代码时,你用相同的语法访问Name属性:{
private string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
// remaining implementation omitted
}
string name = customerOne.Name;
customerOne.Name = "This Company, Inc.";
但c#编译器为这段代码产生完全不同的MSIL:customerOne.Name = "This Company, Inc.";
.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] string CS$00000003$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 NameSpace.Customer::_name
IL_0007: ret
} // end of method Customer::set_Name
两点比较重要的必须理解属性定义是怎样转变为MSIL代码的。首先,.property指示属性的类型,属性的get和set访问器的函数实现。两个函数都带有hidebysig和specialname标签。这样设计意味着c#代码中不能直接使用它们,它们也不是正式类型的一部分,必须通过属性访问他们。{
.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] string CS$00000003$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 NameSpace.Customer::_name
IL_0007: ret
} // end of method Customer::set_Name
当然,你希望属性定义产生不同的MSIL代码,比较重要的一点,属性的get和set访问器产生不同的MSIL代码:
// get
ldloc.0
callvirt instance string 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#代码,把源代码编译成不同的IL代码是编译器的工作。ldloc.0
callvirt instance string NameSpace.Customer::get_Name()
stloc.1
// set
ldloc.0
ldstr "This Company, Inc."
callvirt instance void NameSpace.Customer::set_Name(string)
尽管属性和数据成员是代码兼容的,但并不是二进制兼容的。显而易见,这意味着当你把数据成员修改为属性后,你必须重新编译用到数据成员的所有代码。第四章,“创建二进制组件”,详细讨论二进制组件,我们必须记住当把数据成员简单的修改为属性时破坏了二进制兼容性。这使得升级单一的程序集变的更加困难。
当你查看属性的IL代码时,你可能想知道属性和数据成员执行的关系。属性不会比数据成员快但也不会慢多少。JIT编译器做了一些内联方法调用,包括属性访问器。当JIT编译器内联属性访问器时,数据成员和属性的执行是一样的。甚至当属性并没被内联处理时,实际的执行区别仅在于可以忽略的一个函数调用的开销。这仅在小数目的情况下测试通过。
无论你在接口中暴露公共或保护类型的数据,请使用属性。对序列或dictionay使用索引器。所有的数据成员必须声明为私有,没有例外。你立即得到数据绑定的支持,在将来某一时候改变方法的实现也变的容易。额外的就是你得花费一到二分钟的打字时间把变量包装成属性。迟些时候寻找需要使用属性的地方来正确的表达你的设计将花费点时间,现在就抽出一点时间吧,这将为你今后节省时间。