属性公开字段。 字段应该(几乎总是)保持私有,通过get和set属性访问。 属性提供了一定程度的抽象,允许您更改字段,而不影响使用您的类的东西所访问的外部方式。
public class MyClass
{
// this is a field. It is private to your class and stores the actual data.
private string _myField;
// this is a property. When accessed it uses the underlying field,
// but only exposes the contract, which will not be affected by the underlying field
public string MyProperty
{
get
{
return _myField;
}
set
{
_myField = value;
}
}
// This is an AutoProperty (C# 3.0 and higher) - which is a shorthand syntax
// used to generate a private field for you
public int AnotherProperty { get; set; }
}
@Kent指出,属性不需要仅用于封装字段,它们也可以在其他字段上进行计算或发挥其他作用。
@GSS指出,当访问属性时,您还可以执行其他逻辑,例如验证,这是另一个有用的功能。
string
类型,我的合约是:分配任何长度不超过20亿个字符的字符。如果属性是 DateTime
类型,我的合约是:分配在 DateTime 限制范围内的任何数字,我可以查找这些限制范围。如果创建者对 setter 添加了限制,则这些限制未得到传达。但是,如果创建者将类型从 string
更改为 Surname
,那么他们的新 Surname
类将传达限制,并且属性 public Surname LastName
没有 setter 验证。同时,Surname
是可重用的。 - Suamere面向对象编程原则认为,类的内部工作应该对外部世界进行隐藏。如果您公开一个字段,实际上就是暴露了类的内部实现。因此,我们使用属性(或 Java 中的方法)来封装字段,以便能够更改实现而不会破坏依赖于我们的代码。由于我们可以在属性中放置逻辑,因此如果需要,我们还可以执行验证逻辑等。
C# 3 具有可能令人困惑的自动属性概念。这允许我们简单地定义属性,C# 3 编译器将为我们生成私有字段。
public class Person
{
private string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
public int Age{get;set;} //AutoProperty generates private field for us
}
public int myVar { get; set; }
真正代表仍然会相当困难(我认为这也是这个问题获得至少50%点击率的原因)。 - Priidu Neemrevirtual
本身就是面向对象编程的一部分。 - Gobe一个重要的区别是接口可以有属性,但不能有字段。这使我认为应该使用属性来定义类的公共接口,而字段应该用于类的私有内部工作。作为一条规则,我很少创建公共字段,同样地,我也很少创建非公共属性。
我将给你几个使用属性的示例,以便引发你的思考:
使用 Properties,当属性的值被更改时(即 PropertyChangedEvent),或在值更改之前支持取消操作,您可以引发事件。
这是使用字段(直接访问)不可能做到的。
public class Person {
private string _name;
public event EventHandler NameChanging;
public event EventHandler NameChanged;
public string Name{
get
{
return _name;
}
set
{
OnNameChanging();
_name = value;
OnNameChanged();
}
}
private void OnNameChanging(){
NameChanging?.Invoke(this,EventArgs.Empty);
}
private void OnNameChanged(){
NameChanged?.Invoke(this,EventArgs.Empty);
}
}
由于其中许多人已经解释了Properties
和Field
的技术优缺点,现在是时候进入实时示例了。
1. 属性允许您设置只读访问级别
考虑dataTable.Rows.Count
和dataTable.Columns [i] .Caption
的情况。 它们来自DataTable
类,并且都对我们公开。 它们之间访问级别的差异在于,我们无法为dataTable.Rows.Count
设置值,但我们可以读取和写入dataTable.Columns [i] .Caption
。 这是否可以通过Field
完成? 不!这只能通过Properties
完成。
public class DataTable
{
public class Rows
{
private string _count;
// This Count will be accessable to us but have used only "get" ie, readonly
public int Count
{
get
{
return _count;
}
}
}
public class Columns
{
private string _caption;
// Used both "get" and "set" ie, readable and writable
public string Caption
{
get
{
return _caption;
}
set
{
_caption = value;
}
}
}
}
2. PropertyGrid中的属性
您可能已经在Visual Studio中使用了Button
。它的属性以Text
、Name
等形式显示在PropertyGrid
中。当我们拖放一个按钮时,当我们单击属性时,它将自动找到类Button
并过滤Properties
,并在PropertyGrid
中显示它们(PropertyGrid
不会显示Field
,即使它们是公共的)。
public class Button
{
private string _text;
private string _name;
private string _someProperty;
public string Text
{
get
{
return _text;
}
set
{
_text = value;
}
}
public string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
[Browsable(false)]
public string SomeProperty
{
get
{
return _someProperty;
}
set
{
_someProperty= value;
}
}
在PropertyGrid
中,会显示属性Name
和Text
,但不会显示SomeProperty
。为什么?因为属性可以接受特性。如果[Browsable(false)]
为false,则不会显示它。
3. 可以在属性内执行语句public class Rows
{
private string _count;
public int Count
{
get
{
return CalculateNoOfRows();
}
}
public int CalculateNoOfRows()
{
// Calculation here and finally set the value to _count
return _count;
}
}
4. 只有属性可以用作绑定源
绑定源能帮助我们减少代码行数。但是字段
不能被BindingSource
所接受,我们应该使用属性
。
5. 调试模式
假设我们使用字段
来存储一个值。在某个时刻,我们需要调试并检查该字段的值是否为 null。当代码行数超过 1000 行时,这将变得非常困难。在这种情况下,我们可以使用属性
并在其内部设置调试模式。
public string Name
{
// Can set debug mode inside get or set
get
{
return _name;
}
set
{
_name = value;
}
}
字段是在类或结构体中直接声明的变量。一个类或结构体可以有实例字段、静态字段或两者都有。通常情况下,你应该仅将字段用于具有私有或受保护访问级别的变量。通过使用这些结构来间接访问内部字段,可以防止无效输入值。数据应该通过方法、属性和索引器提供给客户端代码。
属性是一种成员,它提供了一个灵活的机制来读取、写入或计算私有字段的值。属性可以像公共数据成员一样使用,但它们实际上是称为访问器的特殊方法。这使得可以轻松地访问数据并仍然有助于推动方法的安全性和灵活性。 属性使一个类能够公开获取和设置值的公共方式,同时隐藏实现或验证代码。使用 get 属性访问器返回属性值,而使用 set 属性访问器分配新值。
字段是类级别存储数据的唯一机制。 字段在类范围内概念上是变量。如果要将某些数据存储到类的实例(对象)中,您需要使用字段。没有其他选择。属性不能存储任何数据,即使可能看起来它们能够这样做。请参见下文。
另一方面,属性从不存储数据。 它们只是可以在语法上以类似于字段的方式调用的方法对(get 和 set),在大多数情况下,它们访问(读取或写入)字段,这是一些混淆的来源。但由于属性方法是正常的 C# 方法(带有一些限制,例如固定原型),因此它们可以执行任何常规方法可以执行的操作。这意味着它们可以有 1000 行代码,可以抛出异常,调用其他方法,甚至可以是虚拟、抽象或重写的。使属性特殊的是 C# 编译器将一些额外的元数据存储到程序集中,这些元数据可用于搜索特定属性-广泛使用的功能。
获取和设置属性方法具有以下原型。
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
关键字,但这样做,您只能在构造函数中初始化只读字段。更重要的是,没有任何东西可以阻止您将无效值存储到字段中。
name.FullName=null;
name.YearOfBirth=2200;
name.Age=-140;
代码是有效的,所有分配都将被执行,尽管它们是不合逻辑的。 年龄
有一个负值,出生年份
很遥远,与 年龄
不对应,全名
是空的。使用字段无法防止 类 Name
的用户犯下这样的错误。
这里是使用属性修复这些问题的代码。
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
的无效值。Age
不能被写入。它是由YearOfBirth
和当前年份计算得出的。FullNameInUppercase
,将FullName
转换为大写字母。这是一个有点牵强的属性用法示例,属性通常用于以更适合用户的格式呈现字段值 - 例如,在特定数字或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;
}
}
尽管class AutoProps
仅包含属性(或看起来像),但它可以存储2个值,并且该类对象的大小等于sizeof(Value1)+sizeof(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;
}
}
}
因此,正如您所看到的,编译器仍然使用字段来存储值-因为字段是将值存储到对象中的唯一方法。
因此,尽管属性和字段具有类似的用法语法,但它们是非常不同的概念。即使您使用自动属性或事件-编译器也会生成隐藏字段,其中存储实际数据。
如果您需要使字段值对外部世界(您的类的用户)可访问,请勿使用公共或受保护的字段。字段始终应标记为私有。属性允许您进行值检查、格式化、转换等操作,通常可使代码更安全、更易读,并为将来的修改提供更多方便。
属性的主要优点是允许您更改对象上的数据访问方式,而不会破坏其公共接口。例如,如果您需要添加额外的验证或将存储字段更改为计算字段,则如果最初将字段公开为属性,则可以轻松完成此操作。如果您直接公开了一个字段,则必须更改类的公共接口才能添加新功能。该更改会破坏现有客户端,需要在使用您代码的新版本之前重新编译它们。
如果您编写的是用于广泛使用的类库(例如 .NET Framework,被数百万人使用),那么这可能是个问题。但是,如果您正在编写一个仅在小型代码库内部使用的类(例如 <= 50K行),那么这并不是什么大问题,因为没有人会受到您的更改的不利影响。在这种情况下,它只是个人偏好的问题。
属性支持不对称访问,即您可以拥有getter和setter中的一个或两个。同样,属性支持getter/setter的单独可访问性。字段则总是对称的,即您总是可以获取和设置值。只有readonly字段是例外,显然在初始化后无法设置。
属性可能运行很长时间,具有副作用,甚至可能引发异常。字段较快,没有副作用,永远不会抛出异常。由于副作用,属性可能每次调用返回不同的值(如DateTime.Now),即DateTime.Now并非始终相等。字段总是返回相同的值。
字段可用于out/ref参数,而属性则不能。属性支持附加逻辑 - 这可以用于实现惰性加载等功能。
属性通过封装获取/设置值的任何含义来支持一定程度的抽象化。
在大多数/所有情况下使用属性,但尝试避免副作用。