学习 C# (第 7 天):C# 中的属性 (实用方法)






4.92/5 (40投票s)
面向对象编程:C# 中的属性 (实用方法)。
目录
- 目录:1
- 引言
- 属性 (定义)
- 路线图
- 属性 (解释)
- Get 访问器
- Set 访问器
- 只读
- 只写
- C# 中属性的洞察
- 静态属性
- 属性的返回类型
- Value 关键字
- 抽象属性
- 继承中的属性
- 摘要
- 结论
引言
我“深入面向对象编程”系列中的这篇文章将解释关于属性、其用途和 C# 中的索引器。我们将遵循相同的学习方式,即理论少,实践多。我将尝试深入解释概念。
属性 (定义)
让我们从 MSDN 摘录的定义开始:MSDN
“属性是一个成员,它提供了一种灵活的机制来读取、写入或计算私有字段的值。属性可以像公共数据成员一样使用,但实际上它们是称为访问器的特殊方法。这使得数据易于访问,同时仍然促进了方法的安全性和灵活性。”
路线图
让我们回顾一下我们的路线图,
- 深入 OOP(第一天):多态与继承(早期绑定/编译时多态)
- 深入OOP(第2天):多态性和继承(继承)
- 深入OOP(第3天):多态性和继承(动态绑定/运行时多态)
- 深入 OOP (第四天):多态与继承 (关于 C# 中的抽象类)
- 深入OOP(第5天):C#中访问修饰符的一切(Public/Private/Protected/Internal/Sealed/Constants/Readonly字段)
- 学习 C# (第 6 天):理解 C# 中的 Enum (实用方法)
- 学习 C# (第 7 天):C# 中的属性 (实用方法)
- 学习 C# (第 8 天):C# 中的索引器 (实用方法)
- 学习 C#(第 9 天):理解 C# 中的事件(深入探讨)
- 学习C#(第10天):C#中的委托(一种实用方法)
- 学习C#(第11天):C#中的事件(一种实用方法)
属性 (解释)
作为一名 C# 程序员,我必须说,属性是 C# 程序员在代码中受益匪浅的东西。如果我们分析属性的内部机制,它们与普通变量有很大不同;属性在内部是方法,不像变量那样拥有自己的内存。每当我们访问属性时,都可以利用属性编写自定义代码。我们可以在调用/执行属性时或在声明时访问代码,但变量无法做到这一点。通俗地说,属性是一个类成员,它被封装并抽象起来,与访问属性的最终开发者隔离开。属性可以包含很多最终用户不知道的代码。最终用户只关心像使用变量一样使用该属性。
现在让我们开始写一些代码。
注意:本文中的每一个代码片段都经过了尝试和测试。
实验1
创建一个简单的控制台应用程序,并将其命名为“Properties”。在其中添加一个名为“Properties”的类。您可以根据自己的意愿选择项目和类的名称。然后尝试在该类中创建一个属性,如下所示,
Properties.cs
namespace Properties
{
public class Properties
{
public string Name{}
}
}
尝试运行/构建应用程序,我们会得到什么?
输出
错误 'Properties.Properties.Name':属性或索引器必须至少有一个访问器
在上面的例子中,我们创建了一个名为 Name 的字符串类型属性。我们得到的错误非常自明意;它说属性必须有一个访问器,即 get 或 set。这意味着我们需要在属性中有一个可以被访问的东西,无论是设置属性值还是获取属性值,与变量不同,属性在没有访问器的情况下不能被声明。
实验2
让我们为我们的属性分配一个 get 访问器,并在 program.cs 文件的 Main 方法中尝试访问该属性。
Properties.cs
Properties.cs:
namespace Properties
{
public class Properties
{
public string Name
{
get
{
return "I am a Name property";
}
}
}
}
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Properties
{
class Program
{
static void Main(string[] args)
{
Properties properties=new Properties();
Console.WriteLine(properties.Name);
Console.ReadLine();
}
}
}
尝试运行/构建应用程序,我们会得到什么?
输出
它显示“我是一个 Name 属性”。这表明当我们尝试访问属性或尝试获取属性值时,我们的 get 访问器被调用了。
Get 访问器
现在在 Properties.cs 类中声明另一个名为 Age 的属性,它返回一个整数。它通过计算出生日期与当前日期之间的差来计算一个人的年龄,
Properties.cs
using System;
namespace Properties
{
public class Properties
{
public string Name
{
get
{
return "I am a Name property";
}
}
public int Age
{
get
{
DateTime dateOfBirth=new DateTime(1984,01,20);
DateTime currentDate = DateTime.Now;
int age = currentDate.Year - dateOfBirth.Year;
return age;
}
}
}
}
以与 Name 属性相同的方式调用 "Age
" 属性。
Program.cs
using System;
namespace Properties
{
class Program
{
static void Main(string[] args)
{
Properties properties=new Properties();
Console.WriteLine(properties.Name);
Console.WriteLine("My age is " + properties.Age);
Console.ReadLine();
}
}
}
尝试运行/构建应用程序,我们会得到什么?
输出
它返回相对于提供的出生日期的正确年龄。您没有注意到什么吗?我们的属性包含一些代码和逻辑来计算年龄,但调用者(即 Program.cs)不知道该逻辑,它只关心使用该属性。因此,我们看到属性封装并抽象了它的功能,与最终用户(在本例中是开发者)隔离开来。
要点
Get 访问器仅用于读取属性值。只有一个 "get" 的属性不能从调用者那里设置任何值。
这意味着调用者/最终用户只能以只读模式访问该属性。
Set 访问器
让我们从一个简单的例子开始。
实验1
Properties.cs
using System;
namespace Properties
{
public class Properties
{
public string Name
{
get { return "I am a Name property"; }
}
public int Age
{
get
{
DateTime dateOfBirth = new DateTime(1984, 01, 20);
DateTime currentDate = DateTime.Now;
int age = currentDate.Year - dateOfBirth.Year;
Console.WriteLine("Get Age called");
return age;
}
set
{
Console.WriteLine("Set Age called " + value);
}
}
}
}
以与 Name 属性相同的方式调用 "Age
" 属性。
Program.cs
using System;
namespace Properties
{
class Program
{
static void Main(string[] args)
{
Properties properties=new Properties();
Console.WriteLine(properties.Name);
properties.Age = 40;
Console.WriteLine("My age is " + properties.Age);
Console.ReadLine();
}
}
}
运行应用程序,
输出
在上面给出的例子中,我对 get 访问器做了一些小的改动,即只打印“控制在 'Get 访问器' 中”并在 Age 属性中引入了 "Set
"。其他一切都保持不变。现在当我调用 Name 属性时,它的工作方式与之前一样。由于我们使用了 "Set",所以现在我们允许设置属性的值。当我执行 properties.Age = 40;
时,意味着我正在将值 40 设置给该属性。我们可以说属性也可以用来赋值。在这种情况下,一旦我们为属性赋值,Set 访问器就会被调用。之后,当我们再次访问同一个属性时,我们的 get 访问器会被调用,它会返回一个带有自定义逻辑的值。这里有一个缺点,正如我们所见,每当我们调用 get 时,我们都会得到相同的值,而不是我们赋给该属性的值,这是因为 get 有其自定义的固定逻辑。让我们尝试克服这种情况。
实验2
我将要解释的例子使用了私有变量。但是您也可以使用自动属性来实现相同的功能。我将故意使用变量以便更好地理解。
Properties.cs
using System;
namespace Properties
{
public class Properties
{
private string name;
private int age;
public string Name
{
get { return name; }
set
{
Console.WriteLine("Set Name called ");
name = value;
}
}
public int Age
{
get { return age; }
set
{
Console.WriteLine("Set Age called ");
age = value;
}
}
}
}
Program.cs
using System;
namespace Properties
{
class Program
{
static void Main(string[] args)
{
Properties properties=new Properties();
properties.Name = "Akhil";
Console.WriteLine(properties.Name);
properties.Age = 40;
Console.WriteLine("My age is " + properties.Age);
Console.ReadLine();
}
}
}
运行应用程序,
输出
现在您可以看到,我们得到了与 Name 和 Age 属性相同的值。当我们访问这些属性时,会调用 get 访问器,它返回我们为其设置的相同值。这里属性内部使用局部变量来保存和维持值。
在日常编程中,我们通常会创建一个可以在类外部访问的公共属性。但是它内部使用的变量可以是私有的。
要点
用于属性的变量应与属性的数据类型相同。
在我们的例子中,我们使用了变量 name 和 age,它们与各自的属性共享相同的数据类型。我们不使用变量,因为可能存在我们无法控制这些变量的情况,最终用户可以在代码的任何点更改它们,而无需维护更改堆栈。此外,属性的一个主要用途是用户可以在变量发生变化时关联一些逻辑或操作,因此当我们使用属性时,我们可以轻松地跟踪变量值的变化。
使用自动属性时,它们会在内部完成这些操作,即我们不必定义额外的变量来执行此操作,如下所示,
实验3
Properties.cs
using System;
namespace Properties
{
public class Properties
{
private string name;
private int age;
public string Name { get; set; }
public int Age { get; set; }
}
}
Program.cs
using System;
namespace Properties
{
class Program
{
static void Main(string[] args)
{
Properties properties=new Properties();
properties.Name = "Akhil";
Console.WriteLine(properties.Name);
properties.Age = 40;
Console.WriteLine("My age is " + properties.Age);
Console.ReadLine();
}
}
}
运行应用程序,
输出
这里
public string Name { get; set; }
public int Age { get; set; }
是自动属性。
希望您现在知道如何定义属性及其用法。
只读
通过仅提供 get 访问器,可以使属性变为只读。如果我们不希望属性在类范围之外初始化或设置,则不提供 set 访问器。
Properties.cs
using System;
namespace Properties
{
public class Properties
{
private string name="Akhil";
private int age=32;
public string Name
{
get { return name; }
}
public int Age { get { return age; } }
}
}
Program.cs
using System;
namespace Properties
{
class Program
{
static void Main(string[] args)
{
Properties properties=new Properties();
properties.Name = "Akhil";
Console.WriteLine(properties.Name);
properties.Age = 40;
Console.WriteLine("My age is " + properties.Age);
Console.ReadLine();
}
}
}
构建应用程序,我们得到以下输出
错误 'Properties.Properties.Age':属性或索引器不能被赋值——它是只读的
错误 'Properties.Properties.Name':属性或索引器不能被赋值——它是只读的
在 Program 类的 Main 方法中,我们尝试通过以下方式设置 Age
和 Name
属性的值:
properties.Name = "Akhil";
properties.Age = 40;
但是由于它们被标记为只读,即只有 get 访问器,我们遇到了编译时错误。
只写
属性也可以是只写的,即与只读相反。在这种情况下,您只能设置属性的值,但无法访问它,因为我们没有 get 访问器。
Properties.cs
using System;
namespace Properties
{
public class Properties
{
private string name;
private int age;
public string Name
{
set { name=value; }
}
public int Age { set { age = value; } }
}
}
Program.cs
using System;
namespace Properties
{
class Program
{
static void Main(string[] args)
{
Properties properties=new Properties();
properties.Name = "Akhil";
Console.WriteLine(properties.Name);
properties.Age = 40;
Console.WriteLine("My age is " + properties.Age);
Console.ReadLine();
}
}
}
构建应用程序,我们得到以下输出
错误 'Properties.Properties.Age':由于缺少 get 访问器,属性或索引器不能在此上下文中使用
错误 'Properties.Properties.Name':由于缺少 get 访问器,属性或索引器不能在此上下文中使用
在上面提到的例子中,我们的属性只用 set 访问器标记,但我们在主程序中尝试通过以下方式访问这些属性:
Console.WriteLine(properties.Name);
Console.WriteLine("My age is " + properties.Age);
这意味着我们试图调用未定义的属性的 get 访问器,所以我们再次遇到了编译时错误。
C# 中属性的洞察
实验1
我们可以将属性定义为两组不同的部分吗?答案是 NO。
Properties.cs
using System;
namespace Properties
{
public class Properties
{
private string name;
public string Name
{
set { name=value; }
}
public string Name
{
get { return name; }
}
}
}
构建项目,我们得到编译时错误:
错误 'Properties.Properties':类型已包含 'Name' 的定义
在这里,我试图创建一个属性,该属性被分割成两个不同的访问器。编译器将属性名称视为一个独立的属性,所以我们不能定义一个具有两个名称且访问器不同的属性。
实验2
我们可以定义一个属性与一个已定义的变量相同吗?答案是 NO。
Properties.cs
using System;
namespace Properties
{
public class Properties
{
private string name;
public string name
{
set { name=value; }
get { return name; }
}
}
}
构建项目;我们得到编译时错误:
错误 'Properties.Properties':类型已包含 'name' 的定义
同样,我们不能有一个变量和一个同名的属性。它们可能在区分大小写方面有所不同,但它们不能共享相同的常用名称,因为在访问它们时,编译器可能会混淆您是要访问属性还是变量。
属性 vs 变量
一种普遍的看法是变量的执行速度比属性快。我并不否认这一点,但这可能并非在所有情况下都如此,并且可能因情况而异。如我所解释的,属性在内部执行一个函数/方法,而变量在使用时使用/初始化内存。有时属性并不比变量慢,因为属性代码在内部被重写为内存访问。
总结一下,MSDN 解释这个理论比我更好:
差异点 | 变量 | 属性 |
声明 | 单行声明语句 | 代码块中的一系列语句 |
实现 | 单个存储位置 | 可执行代码 (属性过程) |
存储 | 直接与变量的值关联 | 通常具有类或模块中无法从外部访问的内部存储。属性的值可能存在也可能不存在作为存储元素1 |
可执行代码 | 无 | 必须至少有一个过程 |
读写访问 | 读/写或只读 | 读/写、只读或只写 |
自定义操作 (除了接受或返回值) | 不可能 | 可在设置或检索属性值时执行 |
表格参考:https://msdn.microsoft.com/en-us/library/sk5e8eth(VS.80).aspx
静态属性
像变量和方法一样,属性也可以被标记为静态,
Properties.cs
using System;
namespace Properties
{
public class Properties
{
public static int Age
{
set
{
Console.WriteLine("In set static property; value is " + value);
}
get
{
Console.WriteLine("In get static property");
return 10;
}
}
}
}
Program.cs
using System;
namespace Properties
{
class Program
{
static void Main(string[] args)
{
Properties.Age = 40;
Console.WriteLine(Properties.Age);
Console.ReadLine();
}
}
}
输出
在上面的例子中,我创建了一个静态 Age
属性。当我尝试访问它时,您可以看到它是通过类名访问的,就像所有静态成员一样。因此,属性也像所有 C# 成员一样继承静态功能,无论是变量还是方法。它们只能通过类名访问。
属性的返回类型
实验1
Properties.cs
using System;
namespace Properties
{
public class Properties
{
public void AbsProperty
{
get
{
Console.WriteLine("Get called");
}
}
}
}
编译程序。
输出是编译时错误:
错误 'AbsProperty':属性或索引器不能为 void 类型
要点
属性不能有 void 返回类型。
实验2
尝试从 "set" 访问器返回一个值,
Properties.cs
using System;
namespace Properties
{
public class Properties
{
public int Age
{
set { return 5; }
}
}
}
编译程序,
错误 由于 'Properties.Properties.Age.set' 返回 void,因此 return 关键字后面不能跟对象表达式
这里编译器将 "set" 访问器理解为返回 void 并接受一个参数来初始化值的函数。所以 set 不能期望返回一个值。
如果我们只留下 return 语句为空,并删除 5,我们不会得到任何错误,代码可以编译,
using System;
namespace Properties
{
public class Properties
{
public int Age
{
set { return ; }
}
}
}
Value 关键字
我们有一个保留关键字 value。
using System;
namespace Properties
{
public class Properties
{
public string Name
{
set { string value; }
}
}
}
只需编译上面给出的代码,我们就会得到如下编译时错误:
错误 在此范围内不能声明名为 'value' 的局部变量,因为它会赋予 'value' 不同的含义,而 'value' 在父范围或当前范围中已用于表示其他内容
这表明 "value" 在这里是一个保留关键字。所以不能在 "set" 访问器中声明一个名为 value 的变量,因为它可能会赋予已保留关键字 value 不同的含义。
抽象属性
实验1
是的,我们也可以有抽象属性;让我们看看它是如何工作的,
Properties.cs
using System;
namespace Properties
{
public abstract class BaseClass
{
public abstract int AbsProperty { get; set; }
}
public class Properties : BaseClass
{
public override int AbsProperty
{
get
{
Console.WriteLine("Get called");
return 10;
}
set { Console.WriteLine("set called,value is " + value); }
}
}
}
Program.cs
using System;
namespace Properties
{
class Program
{
static void Main(string[] args)
{
Properties prop=new Properties();
prop.AbsProperty = 40;
Console.WriteLine(prop.AbsProperty);
Console.ReadLine();
}
}
}
输出
在上面的例子中,我创建了一个名为 "BaseClass" 的基类,并定义了一个名为 Absproperty
的抽象属性。由于属性是抽象的,它也遵循抽象的规则。我继承了我的 "Properties
" 类,并为该抽象属性提供了主体。由于属性是抽象的,我必须在派生类中重写它来为其添加功能。所以我使用了派生类中的 override 关键字。
在基类中,抽象属性没有任何主体,既没有 "get" 也没有 "set",所以我们必须在派生类中实现这两个访问器,就像在 "Properties
" 类中那样。
要点
如果不在派生类中定义的属性标记为 override,它将被默认视为 new。
有关更多理解,请参考关于 new 和 override 的文章。
实验2
Properties.cs
using System;
namespace Properties
{
public abstract class BaseClass
{
public abstract int AbsProperty { get; }
}
public class Properties : BaseClass
{
public override int AbsProperty
{
get
{
Console.WriteLine("Get called");
return 10;
}
set { Console.WriteLine("set called,value is " + value); }
}
}
}
Program.cs
using System;
namespace Properties
{
class Program
{
static void Main(string[] args)
{
Properties prop=new Properties();
prop.AbsProperty = 40;
Console.WriteLine(prop.AbsProperty);
Console.ReadLine();
}
}
}
输出
编译时错误,
错误 'Properties.Properties.AbsProperty.set':由于 'Properties.BaseClass.AbsProperty' 没有可重写的 set 访问器,因此无法重写
在上面的实验例子中,我只是从 Base 类中的 AbsProperty
中删除了 "set"。所有代码保持不变。现在我们在这里试图重写派生类中的 set 访问器,而该访问器在基类中不存在,因此编译器不允许您重写未在基类中声明的访问器,因此导致了编译时错误。
要点
您不能重写基类抽象属性中未定义的访问器。
继承中的属性
只需遵循给出的代码,
Properties.cs
using System;
namespace Properties
{
public class PropertiesBaseClass
{
public int Age
{
set {}
}
}
public class PropertiesDerivedClass:PropertiesBaseClass
{
public int Age
{
get { return 32; }
}
}
}
Program.cs
namespace Properties
{
class Program
{
static void Main(string[] args)
{
PropertiesBaseClass pBaseClass=new PropertiesBaseClass();
pBaseClass.Age = 10;
PropertiesDerivedClass pDerivedClass=new PropertiesDerivedClass();
((PropertiesBaseClass) pDerivedClass).Age = 15;
pDerivedClass.Age = 10;
}
}
}
正如您在上面给出的代码中所见,在 Properties.cs 文件中,我创建了两个类,一个是 Base,即 PropertiesBaseClass
,第二个是 Derived,即 PropertiesDerivedClass
。我故意为相同的属性名 Age 在 Base 类中声明了 set 访问器,在 Derived 类中声明了 get 访问器。现在这种情况可能让您觉得,当编译时,我们属性 Age
的代码将合二为一,即它将从 Base 类中获取 set,从 Derived 类中获取 get,并将它们合并成一个 Age 属性实体。但实际上并非如此。编译器将这两个属性视为不同的,并且不认为它们是相同的。在这种情况下,派生类中的属性实际上隐藏了基类中的属性,它们不是相同的,而是独立的属性。方法隐藏的相同概念也适用于此。您可以阅读关于隐藏的文章。
要从派生类对象使用基类属性,您需要将其转换为基类然后使用。
当您编译上面的代码时,您会收到如下编译时错误:
错误 'Properties.PropertiesDerivedClass.Age':属性或索引器不能被赋值——它是只读的
即我们可以执行 ((PropertiesBaseClass) pDerivedClass).Age = 15;
但我们不能执行 pDerivedClass.Age = 10;
因为派生类属性没有 "set" 访问器。
摘要
让我们回顾一下我们必须记住的所有要点,
- 用于属性的变量应与属性的数据类型相同。
- 属性不能有 void 返回类型。
- 如果不在派生类中定义的属性标记为 override,它将被默认视为 new。
- 您不能重写基类抽象属性中未定义的访问器。
- Get 访问器仅用于读取属性值。只有 get 的属性不能从调用者那里设置任何值。
结论
在本文中,我们学习了很多关于 C# 中的属性的知识。希望您现在对属性有了很好的理解。在我的下一篇文章中,我将解释 C# 中的所有索引器。
继续编码,享受阅读
如果您觉得我的文章有帮助,请不要忘记给文章评分/评论/点赞,这有助于我获得动力,并鼓励我写得更多。
我的其他系列文章
MVC: https://codeproject.org.cn/Articles/620195/Learning-MVC-Part-Introduction-to-MVC-Architectu
RESTful WebAPIs: https://codeproject.org.cn/Articles/990492/RESTful-Day-sharp-Enterprise-Level-Application
祝您编码愉快!