C#的属性(Property)与字段(Field)有啥区别

463 阅读8分钟

C#的属性与字段有啥区别

本文转载自What Is the Difference Between Properties and Fields in C# - Code Maze (code-maze.com),相关源码可到作者仓库下载
这篇文章我们将学习C#的属性与字段的区别及怎样去使用它们。

什么是字段

我们称C#中直接定义在类或者结构体中的变量为字段。字段可以是任何类型并可以被public,private,protected,internal,protected internal或者private protected修饰。这些访问修饰符和字段一起定义它们的访问级别:

private int _age;

这里的_age是一个int类型的字段并且被标记为private,这意味着这个字段只能在类内部进行访问。

字段的通常用作backing store或者backing field。也就是说我们申明私有字段用来存储可供公共属性访问的数据。

感觉上面的backing store和backing field用中文有点不好表达,我的理解就是字段存储数据但不能被直接访问,想要获取字段的数据就需要通过属性将其暴露出来,所以对于对象来说字段就是背后的、幕后的

让我们申明一个Age属性并且使用_age字段作为它的幕后字段(上文提到的backing field):

public int Age
{
    get{ return _age;}
    set{ _age = value;}
}

当我们实例化一个对象,编译器会在调用对象构造函数之前初始化字段。然而,我们可以在字段申明的时候覆写任意字段拥有的值。

让我们看看创建一个Person类的时候的动作:

public class Person
{
    private string _name = "John Doe";
    
    public Person()
    {
        Console.WriteLine(_name);
        _name = "Jane Doe";
    }
    
    public void UpdateName(string name)
    {
        Console.WriteLine(_name);
        _name = name;
        Console.WriteLine(_name);
    }
}

这里我们在申明的时候给_name字段设置了一个值。然后,我们在构造函数中更新了这个值,最后,我们额外提供了UpdateName()方法用来更新这个值。

现在,如果我们实例化一个类的对象然后再调用UpdateName()方法:

var person = new Person();
person.UpdateName("Sam Doe");

我们在控制台的输出中看到这个字段值按我们更新这个值的顺序被更新了三次:

John Doe
Jane Doe
Sam Doe

字段的类型

我们可以申明字段为只读(readonly),这意味着我们只能在申明的时候或者使用构造函数给这个字段分配值。使_name字段只读将会导致UpdateName()方法中出现编译错误。

另一个我们可以和字段一起使用的关键字是static

一旦我们声明一个字段为static,字段将会与类本身关联而不是实例对象。 这意味着我们在同一个应用程序域中所有的该类的实例都只有一个静态字段的实例。静态字段可以被访问即便没有实例,有点类似于全局变量。

本质上,静态字段在程序上下文中扮演者单例的角色以确保类的所有实例能分享到同一个值。

让我们添加一个静态的Age字段

public class Person
{
    public static int Age;
    private string _name="Jone Doe";
}

我们现在可以访问Age字段即便没有实例化一个Person对象:

Person.Age = 19;

字段可以同时是staticreadonlystatic readonly字段与常量类似。然而,与常量不同的是它们的值是运行时决定的而不是编译时决定的。

我们可以申明一个字段为required,这要求我们在创建一个对象时对required字段进行初始化。让我们在Person类中申明一个required HasSuperPowers字段:

public required bool HasSuperPowers;

这将会触发编译器错误"Required member 'Person.HasSuperPower' must be set in the object initializer or attribute constructor.CS9035 Person.Person()"。要修复这个错误,我们需要添加一个对象初始化器:

var person =new Person{ HasSuperPowers = true};

C#12开始,我们有了主构造函数可以替换字段。主构造函数的参数可以初始化属性或者用于方法或者局部函数中的变量的字段。此外,我们可以把它传递给基础构造函数。

现在我们了解了C#中的字段了,让我们来看看C#中的属性。

什么是属性

C#中,属性是对封装私有字段的一种方法并通过getter和setter方法提供对它们可控的访问权限。 我们使用get访问器获取属性的值、set访问器指定属性的值。set访问器有被称为value的、与它的属性类型匹配的隐式参数。

创建一个我们前面看到过的带有幕后字段的属性Age:

public int Age
{
    get {return _age;}
    set {_age = value;}
}

创建自动实现属性:

public string Name {get;set;}

这些属性自动生成私有幕后字段并为get和set访问器提供默认实现。

属性的类型

上面两种属性都是"read-write"的。我们也能创建仅有get访问器的"read-only"属性或者只有set访问器的"write-only"属性。让我们创建一个Configuration类:

public class Configuration
{
    private string _secretKey = string.Empty;
    
    public string SceretKey
    {
        set
        {
            _secretKey = $"**{value}**";
        }
    }
    
    public string MaskedSecretKey
    {
        get {return _secretKey;}
    }
}

这里我们因为安全原因采用"write-only"SecretKey掩饰了真实的值。之后,"read-only"MaskedSecretKey属性用来取回这个被掩饰的值。

初始化唯一属性

从C#9.0开始,有了init访问器。这允许我们在对象创建期间设置属性的初始化值并且避免了对属性造成更大的修改:

public class Rectangle
{
    public double Width {get; init;}
    public double Height {get; init;}
}

现在,我们可以创建一个Rectangle类的实例并设置它的属性了:

var newRectangle = new Rectangle{width = 10,Height = 5};

然而,我们不能在后续中继续修改这些属性:

newRectangle.Width=15.0;

这将会导致编译器错误。

静态属性

与字段类似,我们可以添加访问修饰符来控制它们的访问级别。我们也可以使用static关键字来申明静态属性。例如,我们可以像Rectangle类中添加一个静态ScalingFactor属性:

public static double ScalingFactor {get;set}=1.0;

因为静态属性被绑定到类自身而不是一个特定的实例,我们可以直接访问它们而不需要实例化这个类:

Rectangle.ScalingFactor=2.0;

现在,让我们创建一个CreateScaledRectangle()方法来应用缩放:

public Rectangle CreateScaledRectangle(){
{
    return new Rectangle(Width * ScalingFactor,Height * ScalingFactor);
}

最后,让我们创建另一个Rectangle并缩放它:

var newRectangle = new Rectangle{Width=10.5,Height=5.5};
var newRecTangleScaled = newRectangle.CreateScaledRectangle();
Console.WriteLine("Dimensions of the new rectangle after scaling:"+$"{newRectangleScaled.Width} X {newRectangleScaled.Height}");

我们观察到缩放因子之前被设置为2.0也应用到newRectangle对象中了,即便我们没有在构造函数中设置新的缩放因子:

Dimensions of the new rectangle after scaling: 21 X 11

这是因为在相同的程序域内所有该类的实例共享一个静态实例

虚拟和抽象属性

属性也可以通过使用virtual关键字标记访问器声明为虚拟的。虚拟属性允许使用override关键字继承类并重载。我们可以通过虚拟属性为类中的属性提供一个默认行为,也可以继承类定义特定行为。

我们也可以通过abstract关键字声明抽象属性,当然,这只能在抽象类中实现。抽象属性要求派生类提供该属性的实现而不是直接实现。

属性与字段的区别

C#中的属性与字段的主要区别在于它们的可访问性、封装性以及它们提供数据访问的级别控制。

可访问性和直接访问

字段提供了对数据的直接访问并且经常用private,public或者protected之类的关键字声名来指定它们的可见性。它们缺乏封装保护机制,这使得它们能直接被类外直接访问。

属性,另一方面,封装并通过访问器控制访问。这使得数据能有更受控的可见性和修改。

封装性

字段缺乏封装性,这意味着它们能暴露任何没有保护或有效的数据,例如带有public权限修饰符的字段允许类外直接访问。

属性封装数据,使得我们能控制怎么访问和修改数据。例如,我们可以通过使用有效逻辑确保我们只用了有效的且想要的数据。

Get和Set访问器的使用方法

我们直接访问字段不需要显示访问器。相反,属性使用getset访问器、允许访问和修改的额外逻辑。例如,我们为一个长方形的宽设置一个有效的宽:

public double Width
{
    get => _width;
    init
    {
        if(value < 0)
            throw new ArgumentException("A Rectangle can't have negative width",nameof(value));
        _width = value;
    }
}

这只是使用属性的一种可能情况。

何时使用属性和字段

属性和字段的主要区别中的一个包含数据封装性。另一个主要区别是关于当数据被读写时我们希望表现的计算和有效性。这两个都是我们该考虑的事情当选择我们的数据作为字段或者作为属性暴露。

关于属性和字段封装的区别

因为字段提供数据的直接访问,这对优先级高的简单数据是很合适的。字段在简单域模型和其它的我们不需要对数据提供额外的有效性验证的程序中是有效的。因为数据直接访问,类或者结构体能直接修改对象内部的状态。

另一方面,属性允许我们控制潜在的数据结构的访问并且提供提供了一种额外有效性或者计算的方法。封装允许我们确保我们的对象维护有效的内部状态。

关于属性和封装计算值的区别

尽管字段提供了对潜在数据的简单和直接访问,属性有使我们对未设置或未返回的数据执行计算的优势。

例如,我们也许有一个模拟温度的类。我们内部用开尔文温度存储数值。我们也可以提供属性访问器让调用者获取或设置温度用华氏温度或者摄氏温度或者一些直接访问字段不太可能得到的数据。

总结

我们通过这篇文章了解了C#的字段和属性。我们关注了如何及何时去使用它们。两者的选择取决于对类中某一特定数据成员的控制和封装期望级别。