一个字段和一个属性有什么区别?

1526
在 C# 中,属性和字段的区别在于什么?何时应该使用字段而不是属性?

44
微软在其成员设计指南中直接回答了这个问题(适用于所有.NET语言)。具体细节请参阅文章属性设计字段设计。请注意,实例成员和静态成员之间有区别。 - DavidRR
33个回答

1322

属性公开字段。 字段应该(几乎总是)保持私有,通过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指出,当访问属性时,您还可以执行其他逻辑,例如验证,这是另一个有用的功能。


16
如果我理解得不正确,请原谅,但是如果字段似乎可以处理访问控制,为什么还需要在属性前面添加访问修饰符呢?也就是说,为什么不把属性设为public呢?但是要确保不影响使用你的类的组件对这些属性的外部访问方式。 - Chucky
36
在编辑之前,你的回答是正确的,但是奇怪地获得了错误的点赞评论。一个属性应该始终封装一个或多个字段,并且不应该执行任何重量级的操作或验证。如果您需要为 UserName 或 Password 这样的属性进行验证,请将它们的类型从字符串更改为 值对象。类创建者和使用者之间存在一种默契合同。字段保存状态,属性使用一个或多个字段公开状态,方法改变状态(重量级操作),而函数执行查询(重量级操作)。这并不是铁规,只是松散的期望。 - Suamere
6
如果我是一个类的用户,我会遵循该类创建者设定的合约。如果属性是 string 类型,我的合约是:分配任何长度不超过20亿个字符的字符。如果属性是 DateTime 类型,我的合约是:分配在 DateTime 限制范围内的任何数字,我可以查找这些限制范围。如果创建者对 setter 添加了限制,则这些限制未得到传达。但是,如果创建者将类型从 string 更改为 Surname,那么他们的新 Surname 类将传达限制,并且属性 public Surname LastName 没有 setter 验证。同时,Surname 是可重用的。 - Suamere
4
在我的例子中,“Surname”是可重复使用的,因此您无需担心将来将这些验证内容复制/粘贴到代码中其他位置的属性设置器中。也不必担心如果您对“Surname”的业务规则进行更改,它是否存在于多个位置的验证。请查看我发布的有关值对象的链接。 - Suamere
4
这并不回答问题。问题是“有什么区别...”,而这篇文章说“我并没有告诉你有什么区别,而是告诉你应该如何工作”。 - stenci
显示剩余8条评论

326

面向对象编程原则认为,类的内部工作应该对外部世界进行隐藏。如果您公开一个字段,实际上就是暴露了类的内部实现。因此,我们使用属性(或 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
}

130
+1 提到自动属性 - 我认为这是许多回答(和其他地方)遗漏的内容。没有这个解释,理解 public int myVar { get; set; } 真正代表仍然会相当困难(我认为这也是这个问题获得至少50%点击率的原因)。 - Priidu Neemre
10
也要感谢提到自动属性,并介绍其工作原理(“AutoProperty为我们生成了私有字段”)。这是我一直在寻找答案的问题。在研究过程中,我没有看到MSDN页面上有任何关于它们是否创建了私有字段并引起了困惑的指示。我猜这就是意思吗?“自动实现属性允许使用属性,但显然不能用于后备字段,因为这些字段无法从源代码访问。如果必须在属性的后备字段上使用属性,请创建一个普通属性。” 但不确定。 - Nyra
10
请注意,给出的示例根本没有封装任何内容。这个属性可以完全访问私有字段,因此这根本不是面向对象的。在这种情况下,你可能也会有一个公共字段。虽然它可以帮助稍微重构未来的代码,但任何值得一试的IDE都可以用几个按键将字段转换为属性。答案在技术上对属性的工作原理可能是正确的,但它并没有给出有关它们用途的良好的“面向对象”的解释。 - sara
2
@kai 我同意答案过于简化,没有展示自动属性的全部功能,但我不同意这不是面向对象的。你可能需要查看字段和属性之间的区别。字段不能是虚拟的,而virtual本身就是面向对象编程的一部分。 - Gobe
5
@sara是对的。这里使用属性和自动属性的方式对于OOP原则没有任何益处。你可以使用字段,然后在需要添加特殊功能来获取和设置时将其更改为属性。从一开始就让所有东西都有未使用的getter和setter是C#世界中流行的做法,但它违反了反过度工程化的原则,并掩盖了封装性的破坏。流行并不总是正确的。 - OCDev
显示剩余4条评论

211

一个重要的区别是接口可以有属性,但不能有字段。这使我认为应该使用属性来定义类的公共接口,而字段应该用于类的私有内部工作。作为一条规则,我很少创建公共字段,同样地,我也很少创建非公共属性。


3
好奇那些罕见的情况可能是什么! - Eagle_Eye
6
这个回答和下面的回答都是正确的。但是得票最多的两个帖子并没有回答问题。问题是“有什么区别...”,但得票最高的答案回应道:“我不是告诉你有什么区别,而是告诉你如何工作”。 - stenci

118

我将给你几个使用属性的示例,以便引发你的思考:


1
一个关于脏数据追踪的问题:如果我可以直接更改字段,那该怎么办——我不知道是否可以这样做,我可以说:“如果对象的任何一个字段都没有更改,则无需保存对象”,因此脏数据追踪就不会有差异了,我有什么遗漏吗? - sites
3
相对于脏数据追踪而言,属性的优势在于,如果属性设定器设置了“脏”标志,则在该标志未设置的情况下,代码无需检查任何属性的值以查看它们是否已更改。相比之下,如果一个对象将其属性公开为字段,则必须将所有字段的内容与先前的值进行比较(这不仅会增加比较时间,还意味着代码必须具有先前的值)。 - supercat
这些都是好的。它还允许您触发方法(作为事件),或在设置或读取值时记录日志。 - coloboxp

76

使用 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);
 }
}

4
我找了很长时间才找到这个。这是一个 MVVM。谢谢! :) - user5039044

56

由于其中许多人已经解释了PropertiesField的技术优缺点,现在是时候进入实时示例了。

1. 属性允许您设置只读访问级别

考虑dataTable.Rows.CountdataTable.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。它的属性以TextName等形式显示在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中,会显示属性NameText,但不会显示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;
        }
   }

这些是有趣的事实,但你错过了字段和属性哲学的重点。 - David Ferenczy Rogožan
什么是“PHILOSOPHY”?@Dawid Ferenczy - Sarath Subramanian
请参考标记的答案。但是您注意到,您只是提供了一个使用示例,因为字段和属性之间的区别已经被描述了,所以请忘记我的评论 :) - David Ferenczy Rogožan
1
我已经阅读了它,但显然你没有读我的先前评论:“但你注意到了,你只是提供了一个用法示例,因为字段和属性之间的区别已经被描述了,所以请忘记我的评论:)”。 - David Ferenczy Rogožan

54

差异 - 用途(何时和为什么使用)

字段是在类或结构体中直接声明的变量。一个类或结构体可以有实例字段、静态字段或两者都有。通常情况下,你应该仅将字段用于具有私有或受保护访问级别的变量。通过使用这些结构来间接访问内部字段,可以防止无效输入值。数据应该通过方法、属性和索引器提供给客户端代码。

属性是一种成员,它提供了一个灵活的机制来读取、写入或计算私有字段的值。属性可以像公共数据成员一样使用,但它们实际上是称为访问器的特殊方法。这使得可以轻松地访问数据并仍然有助于推动方法的安全性和灵活性。 属性使一个类能够公开获取和设置值的公共方式,同时隐藏实现或验证代码。使用 get 属性访问器返回属性值,而使用 set 属性访问器分配新值。


这是一个很棒的答案,真的帮助我理解了这个问题。 - Steve Bauman
属性是一种成员,它提供了一种灵活的机制来读取、写入或计算私有字段的值。 - Gary

27
尽管字段和属性看起来相似,但它们是两个完全不同的语言元素。
  1. 字段是类级别存储数据的唯一机制。 字段在类范围内概念上是变量。如果要将某些数据存储到类的实例(对象)中,您需要使用字段。没有其他选择。属性不能存储任何数据,即使可能看起来它们能够这样做。请参见下文。

  2. 另一方面,属性从不存储数据。 它们只是可以在语法上以类似于字段的方式调用的方法对(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).

虽然属性方法可以做任何事情,但在大多数情况下,它们作为访问对象字段的方式。如果您想使某个字段对其他类可访问,可以通过以下两种方式实现。

  1. 将字段设为公共字段-不建议。
  2. 使用属性。

这里是一个使用公共字段的类。

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());
        }
    }
}
更新后的类具有以下优点:
  1. 检查了FullNameYearOfBirth的无效值。
  2. Age不能被写入。它是由YearOfBirth和当前年份计算得出的。
  3. 新增了一个属性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;
        }
    }
}

因此,正如您所看到的,编译器仍然使用字段来存储值-因为字段是将值存储到对象中的唯一方法。

因此,尽管属性和字段具有类似的用法语法,但它们是非常不同的概念。即使您使用自动属性或事件-编译器也会生成隐藏字段,其中存储实际数据。

如果您需要使字段值对外部世界(您的类的用户)可访问,请勿使用公共或受保护的字段。字段始终应标记为私有。属性允许您进行值检查、格式化、转换等操作,通常可使代码更安全、更易读,并为将来的修改提供更多方便。


18

属性的主要优点是允许您更改对象上的数据访问方式,而不会破坏其公共接口。例如,如果您需要添加额外的验证或将存储字段更改为计算字段,则如果最初将字段公开为属性,则可以轻松完成此操作。如果您直接公开了一个字段,则必须更改类的公共接口才能添加新功能。该更改会破坏现有客户端,需要在使用您代码的新版本之前重新编译它们。

如果您编写的是用于广泛使用的类库(例如 .NET Framework,被数百万人使用),那么这可能是个问题。但是,如果您正在编写一个仅在小型代码库内部使用的类(例如 <= 50K行),那么这并不是什么大问题,因为没有人会受到您的更改的不利影响。在这种情况下,它只是个人偏好的问题。


17

属性支持不对称访问,即您可以拥有getter和setter中的一个或两个。同样,属性支持getter/setter的单独可访问性。字段则总是对称的,即您总是可以获取和设置值。只有readonly字段是例外,显然在初始化后无法设置。

属性可能运行很长时间,具有副作用,甚至可能引发异常。字段较快,没有副作用,永远不会抛出异常。由于副作用,属性可能每次调用返回不同的值(如DateTime.Now),即DateTime.Now并非始终相等。字段总是返回相同的值。

字段可用于out/ref参数,而属性则不能。属性支持附加逻辑 - 这可以用于实现惰性加载等功能。

属性通过封装获取/设置值的任何含义来支持一定程度的抽象化。

在大多数/所有情况下使用属性,但尝试避免副作用。


当字段的数据类型是带有转换运算符重载的对象时,它们可能具有与属性相同的所有成本问题 - 这是一个微妙的陷阱。 - Andy Dent
2
属性永远不应该产生副作用。即使调试器也认为它们可以安全地求值。 - Craig Gidney
@Strilanc:我完全同意,但并非总是如此。至于调试器,如果你说的是FuncEval,那么它存在许多问题。 - Brian Rasmussen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,