在C#中,是什么使字段与属性不同?何时应该使用字段而不是属性?


当前回答

使用财产,您可以在属性值更改时(也称为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);
 }
}

其他回答

虽然字段和财产看起来很相似,但它们是两个完全不同的语言元素。

字段是在类级别上存储数据的唯一机制。字段在概念上是类范围内的变量。如果要将一些数据存储到类(对象)的实例中,则需要使用字段。别无选择。虽然财产无法存储任何数据,但看起来它们可以这样做。请参阅下文。另一方面,财产从不存储数据。它们只是一对方法(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;
        }
    }
}

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

正如你所见,虽然财产和字段有相似的用法语法,但它们是非常不同的概念。即使您使用自动财产或事件,隐藏字段也由编译器生成,真实数据存储在这里。

如果需要使外部世界(类的用户)可以访问字段值,请不要使用公共或受保护的字段。字段始终应标记为私有。财产允许您进行值检查、格式化、转换等,通常会使代码更安全、更可读、更可扩展,以便将来进行修改。

我将举几个使用财产的示例,这些属性可能会使齿轮转动:

惰性初始化:如果您有一个对象的属性,该属性的加载成本很高,但在正常运行代码时无法访问,则可以通过该属性延迟加载。这样,它就只是坐在那里,但当另一个模块第一次尝试调用该属性时,它会检查基础字段是否为空-如果为空,它会继续加载它,调用模块不知道。这可以大大加快对象初始化。肮脏追踪:这是我从StackOverflow上的问题中了解到的。当我有很多对象的值可能在运行期间发生了变化时,我可以使用该属性来跟踪它们是否需要保存回数据库。如果对象的任何一个属性都没有改变,IsDirty标志都不会被触发,因此保存功能在决定需要返回数据库时会跳过它。

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

其他信息:默认情况下,get和set访问器与属性本身一样可访问。通过对访问器应用更严格的访问修饰符,可以分别控制/限制访问器的可访问性(对于get和set)。

例子:

public string Name
{
    get
    {
        return name;
    }
    protected set
    {
        name = value;
    }
}

这里get仍然是公共访问的(因为属性是公共的),但set是受保护的(一个更受限制的访问说明符)。

在后台,属性被编译为方法。因此,Name属性被编译为get_Name()和set_Name(字符串值)。如果您研究编译的代码,您可以看到这一点。因此,在使用它们时会有(非常)小的性能开销。通常,如果向外部公开字段,则始终使用Property,如果需要验证值,则通常在内部使用Property。