在C#中,是什么使字段与属性不同?何时应该使用字段而不是属性?
当前回答
虽然字段和财产看起来很相似,但它们是两个完全不同的语言元素。
字段是在类级别上存储数据的唯一机制。字段在概念上是类范围内的变量。如果要将一些数据存储到类(对象)的实例中,则需要使用字段。别无选择。虽然财产无法存储任何数据,但看起来它们可以这样做。请参阅下文。另一方面,财产从不存储数据。它们只是一对方法(get和set),在语法上可以以与字段类似的方式调用,在大多数情况下,它们访问(用于读取或写入)字段,这是一些混淆的根源。但是,由于属性方法是(有一些限制,如固定原型)常规C#方法,它们可以做常规方法所能做的任何事情。这意味着它们可以有1000行代码,它们可以抛出异常,调用其他方法,甚至可以是虚拟的、抽象的或重写的。财产之所以特别,是因为C#编译器将一些额外的元数据存储到程序集中,这些程序集可用于搜索特定的财产,而这些属性是广泛使用的功能。
Get和set属性方法具有以下原型。
PROPERTY_TYPE get();
void set(PROPERTY_TYPE value);
因此,这意味着可以通过定义一个字段和两个相应的方法来“模拟”财产。
class PropertyEmulation
{
private string MSomeValue;
public string GetSomeValue()
{
return(MSomeValue);
}
public void SetSomeValue(string value)
{
MSomeValue=value;
}
}
这种属性模拟对于不支持财产的编程语言来说是很典型的,比如标准C++。在C#中,您应该始终首选财产作为访问字段的方式。
因为只有字段可以存储数据,这意味着类包含的字段越多,此类的内存对象就会消耗越多。另一方面,向类中添加新的财产不会使此类对象变大。这是一个例子。
class OneHundredFields
{
public int Field1;
public int Field2;
...
public int Field100;
}
OneHundredFields Instance=new OneHundredFields() // Variable 'Instance' consumes 100*sizeof(int) bytes of memory.
class OneHundredProperties
{
public int Property1
{
get
{
return(1000);
}
set
{
// Empty.
}
}
public int Property2
{
get
{
return(1000);
}
set
{
// Empty.
}
}
...
public int Property100
{
get
{
return(1000);
}
set
{
// Empty.
}
}
}
OneHundredProperties Instance=new OneHundredProperties() // !!!!! Variable 'Instance' consumes 0 bytes of memory. (In fact a some bytes are consumed becasue every object contais some auxiliarity data, but size doesn't depend on number of properties).
尽管属性方法可以做任何事情,但在大多数情况下,它们充当了访问对象字段的一种方式。如果你想让一个字段可以被其他类访问,你可以通过两种方式来实现。
将字段设置为公共字段-不可取。使用财产。
这是一个使用公共字段的类。
class Name
{
public string FullName;
public int YearOfBirth;
public int Age;
}
Name name=new Name();
name.FullName="Tim Anderson";
name.YearOfBirth=1979;
name.Age=40;
虽然从设计角度来看,代码是完全有效的,但它有几个缺点。因为字段既可以读也可以写,所以不能阻止用户写入字段。您可以应用readonly关键字,但通过这种方式,您只能在构造函数中初始化readonly字段。此外,没有什么可以阻止您将无效值存储到字段中。
name.FullName=null;
name.YearOfBirth=2200;
name.Age=-140;
代码有效,所有赋值都将执行,尽管它们不合逻辑。Age为负值,YearOfBirth是遥远的未来,与Age不对应,FullName为空。对于字段,您无法阻止类名的用户犯这样的错误。
下面是一个具有财产的代码,用于修复这些问题。
class Name
{
private string MFullName="";
private int MYearOfBirth;
public string FullName
{
get
{
return(MFullName);
}
set
{
if (value==null)
{
throw(new InvalidOperationException("Error !"));
}
MFullName=value;
}
}
public int YearOfBirth
{
get
{
return(MYearOfBirth);
}
set
{
if (MYearOfBirth<1900 || MYearOfBirth>DateTime.Now.Year)
{
throw(new InvalidOperationException("Error !"));
}
MYearOfBirth=value;
}
}
public int Age
{
get
{
return(DateTime.Now.Year-MYearOfBirth);
}
}
public string FullNameInUppercase
{
get
{
return(MFullName.ToUpper());
}
}
}
类的更新版本具有以下优点。
检查FullName和YearOfBirth是否存在无效值。年龄不可写。它是根据出生年份和当前年份计算的。新属性FullNameInUppercase将FullName转换为UPPER CASE。这是一个有点做作的属性用法示例,其中财产通常用于以更适合用户的格式显示字段值,例如在DateTime格式的特定数字上使用当前区域设置。
除此之外,可以将财产定义为虚拟或重写,因为它们是常规的.NET方法。与常规方法相同的规则适用于此类属性方法。
C#还支持索引器,索引器是在属性方法中具有索引参数的财产。这是一个例子。
class MyList
{
private string[] MBuffer;
public MyList()
{
MBuffer=new string[100];
}
public string this[int Index]
{
get
{
return(MBuffer[Index]);
}
set
{
MBuffer[Index]=value;
}
}
}
MyList List=new MyList();
List[10]="ABC";
Console.WriteLine(List[10]);
因为C#3.0允许您定义自动财产。这是一个例子。
class AutoProps
{
public int Value1
{
get;
set;
}
public int Value2
{
get;
set;
}
}
尽管类AutoProps只包含财产(或看起来像),但它可以存储2个值,并且该类的对象大小等于sizeof(Value1)+size of(Value2)=4+4=8个字节。
原因很简单。当您定义自动属性时,C#编译器会生成包含隐藏字段和属性的自动代码,属性方法访问此隐藏字段。这是编译器生成的代码。
下面是ILSpy从编译的程序集生成的代码。类包含生成的隐藏字段和财产。
internal class AutoProps
{
[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private int <Value1>k__BackingField;
[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private int <Value2>k__BackingField;
public int Value1
{
[CompilerGenerated]
get
{
return <Value1>k__BackingField;
}
[CompilerGenerated]
set
{
<Value1>k__BackingField = value;
}
}
public int Value2
{
[CompilerGenerated]
get
{
return <Value2>k__BackingField;
}
[CompilerGenerated]
set
{
<Value2>k__BackingField = value;
}
}
}
因此,正如您所看到的,编译器仍然使用字段来存储值,因为字段是将值存储到对象中的唯一方法。
正如你所见,虽然财产和字段有相似的用法语法,但它们是非常不同的概念。即使您使用自动财产或事件,隐藏字段也由编译器生成,真实数据存储在这里。
如果需要使外部世界(类的用户)可以访问字段值,请不要使用公共或受保护的字段。字段始终应标记为私有。财产允许您进行值检查、格式化、转换等,通常会使代码更安全、更可读、更可扩展,以便将来进行修改。
其他回答
这里的第二个问题,“什么时候应该使用字段而不是属性?”,在另一个答案中只简短地提到了这个问题,也有点像这个问题,但没有太多细节。
总的来说,所有其他答案都是关于良好设计的一针见血的:比起暴露字段,更喜欢暴露财产。虽然你可能不会经常说“哇,想象一下,如果我把这里变成了一块地而不是一块地,情况会变得多么糟糕”,但想到你会说“哇!谢天谢地,我在这里用了一块土地而不是一处地”的情况,那就难得多了
但与财产相比,字段有一个优点,那就是它们可以用作“ref”/“out”参数。假设您有一个具有以下签名的方法:
public void TransformPoint(ref double x, ref double y);
假设您希望使用该方法来转换如下创建的数组:
System.Windows.Point[] points = new Point[1000000];
Initialize(points);
因为X和Y是财产,所以我认为这是最快的方法:
for (int i = 0; i < points.Length; i++)
{
double x = points[i].X;
double y = points[i].Y;
TransformPoint(ref x, ref y);
points[i].X = x;
points[i].Y = y;
}
这会很好的!除非你的测量结果证明了这一点,否则没有理由大惊小怪。但我认为,从技术上来说,它并不能保证像这样快:
internal struct MyPoint
{
internal double X;
internal double Y;
}
// ...
MyPoint[] points = new MyPoint[1000000];
Initialize(points);
// ...
for (int i = 0; i < points.Length; i++)
{
TransformPoint(ref points[i].X, ref points[i].Y);
}
我自己进行了一些测量,带字段的版本与带财产的版本(.NET 4.6、Windows 7、x64、发布模式、未附加调试程序)相比,大约需要61%的时间。TransformPoint方法的成本越高,差异就越不明显。若要自己重复此操作,请将第一行注释掉,并将其未注释掉。
即使上述方法没有性能优势,但在其他地方使用ref和out参数可能会有好处,例如在调用Interlocked或Volatile方法家族时。注意:如果这对您来说是新的,Volatile基本上是一种获得Volatile关键字提供的相同行为的方法。因此,就像volatile一样,它并没有像名字所暗示的那样神奇地解决所有线程安全问题。
我当然不想让人觉得我在提倡你说“哦,我应该开始公开字段,而不是财产。”关键是,如果你需要在调用中定期使用这些成员来接受“ref”或“out”参数,尤其是在可能是简单值类型的东西上,它不太可能需要财产的任何增值元素,那么可以进行一个参数。
财产用于显示字段。它们使用访问器(set、get),通过这些访问器可以读取、写入或操作私有字段的值。
财产不指定存储位置。相反,它们具有读取、写入或计算其值的访问器。
使用财产,我们可以对字段上设置的数据类型进行验证。
例如,我们有一个私有整数字段age,我们应该允许正值,因为age不能为负值。
我们可以使用getter和setter以及使用property两种方式来实现这一点。
Using Getter and Setter
// field
private int _age;
// setter
public void set(int age){
if (age <=0)
throw new Exception();
this._age = age;
}
// getter
public int get (){
return this._age;
}
Now using property we can do the same thing. In the value is a key word
private int _age;
public int Age{
get{
return this._age;
}
set{
if (value <= 0)
throw new Exception()
}
}
如果我们在get和set访问器中没有逻辑,我们可以使用自动实现的属性。
当使用自动实现的属性编译时,将创建一个只能通过get和set访问器访问的私有匿名字段。
public int Age{get;set;}
抽象财产抽象类可以具有抽象属性,该属性应在派生类中实现
public abstract class Person
{
public abstract string Name
{
get;
set;
}
public abstract int Age
{
get;
set;
}
}
// overriden something like this
// Declare a Name property of type string:
public override string Name
{
get
{
return name;
}
set
{
name = value;
}
}
我们可以私下设置房产在这种情况下,我们可以私下设置auto属性(在类中设置)
public int MyProperty
{
get; private set;
}
您可以使用此代码实现相同的效果。在该属性集中,由于我们必须直接将值设置为字段,因此功能不可用。
private int myProperty;
public int MyProperty
{
get { return myProperty; }
}
IMO,财产只是我们以前使用的“SetXXX()”“GetXX()”函数/方法/接口对,但它们更简洁优雅。
基本区别和一般区别是:
领域
始终给予get和set访问权限不会造成副作用(引发异常、调用方法、更改字段(获取/设置的字段除外)等)
财产
并非总是同时提供get和set访问权限CAN导致副作用
字段是类的普通成员变量或成员实例。财产是获取和设置其值的抽象。财产也称为访问器,因为如果您将类中的字段公开为私有字段,则它们提供了更改和检索字段的方法。通常,您应该将成员变量声明为私有的,然后为它们声明或定义财产。
class SomeClass
{
int numbera; //Field
//Property
public static int numbera { get; set;}
}