切换导航
简历
工具
演示
查询
导航菜单
服务端
30
前端
114
数据库
82
进阶开发
8
桌面程序
2
QA
15
软件使用
36
规范协议
5
操作系统
11
AI
0
复用资源
6
硬件
0
标签
C# 基础
Mybatis
ADO.NET
ASP.NET
Python 基础
Linq
Web Forms
.NET
Spring
SpringBoot
SpringCloud
.NET Core 中间件
Maven
.NET 类库
ASP.NET MVC
EF
NHibernate
WebApi
Java
外部链接
.NET Core SDK和运行时安装包
淘宝 NPM 镜像
查询网
Tables Generator线上表格转换工具
引用cdn在线脚本和样式
首页
服务端
C# 类、结构和记录中的属性介绍
[TOC] ## 1. 属性概述 属性(Properties)是一种特殊的成员,它提供灵活的机制来读取、写入或计算私有字段的值。 属性可用作公共数据成员,但它们是称为“访问器”的特殊方法。 此功能使得可以轻松访问数据,还有助于提高方法的安全性和灵活性。 1. 允许类公开地获取和设置对象的值,但隐藏了实际的实现细节。这与公共字段(Public Fields)不同,因为公共字段直接暴露了存储的值。使用属性可以提供更好的封装,使得代码更安全、更易于维护 - **🧪示例** 下面是一个简单的C#类,其中包含一个属性: ```csharp public class Person { // 这是一个属性,它允许我们获取和设置 Name 属性的值。 public string Name { get; set; } } ``` 在上面的代码中,`Name` 是一个属性,它提供了对 `Name` 字段的访问。使用 `get` 和 `set` 访问器,你可以像这样读取和设置 `Name` 属性的值: ```csharp Person person = new Person(); person.Name = "Alice"; // 设置 Name 属性的值 string name = person.Name; // 获取 Name 属性的值 ``` > 属性是C#中实现封装的一种强大工具,它们允许你控制对对象内部数据的访问,同时隐藏实现细节。通过使用属性,你可以编写更安全、更易于维护的代码。 2. get 属性访问器用于返回属性值,而 set 属性访问器用于分配新值。 init 属性访问器仅用于在对象构造过程中分配新值。 这些访问器可以具有不同的访问级别。 有关详细信息,请参阅本文的第4点【限制访问器可访问性】。 - **🧪示例** 下面是一个简单的三类访问器属性代码: ```csharp public class Person { public string Name1 { get { return "曹操" ;} set { } } // 上面的语句无论调用方如何设置属性值,返回的都将是”曹操“ public string Name2 { get {return "曹操";}} // 上面的语句是只读属性,给这类属性赋值将会提示语法相关的错误,可通过retrun来确定属性的值 public string Name3 { set;} // 上面的语句是只写属性,一般情况不会编写这类属性 public string Name4 { init;} // 上面的语句也是只写属性,与set的区别就是init只能在构造对象时赋值一次,之后再赋值就是语法错误了 } ``` > init 是C#9 才有的语法 3. value 关键字用于定义由 set 或 init 访问器分配的值。 - **🧪示例** 下面是一个简单的value关键字的使用: ```csharp private string name; public string Name { get { return name; } set { name = value; } } // 上面代码通过value关键字将属性值赋值给字段name,然后get返回name作为属性的值 ``` > value 关键字的使用,通常都需要搭配一个字段成员,将字段的值赋值给新的字段成员,借助这个字段可以通过一些验证规则,重新给属性赋值 4. 属性可以是读-写属性(既有 get 访问器又有 set 访问器)、只读属性(有 get 访问器,但没有 set 访问器)或只写访问器(有 set 访问器,但没有 get 访问器)。 只写属性很少出现,常用于限制对敏感数据的访问。 - **🧪示例**:沿用第3点的示例即可 5. 不需要自定义访问器代码的简单属性,可以作为【表达式主体定义】或【自动实现的属性】来实现。 - **🧪示例**:编程中的实现方式,你可以不写访问器 get和set,init的主体代码,直接给属性进行初始化赋值 ```csharp public string PerInfo { get; init; } = "人员信息"; ``` > 示例中使用了init访问器,那么字段也只能在创建对象时修改字段初始化的值 ### 1.1. 表达式主体定义 属性访问器通常由单行语句组成,这些语句只分配或只返回表达式的结果。 可以将这些属性作为 expression-bodied 成员来实现。 => 符号后跟用于为属性赋值或从属性中检索值的表达式,即组成了表达式主体定义。 只读属性可以将 get 访问器作为 expression-bodied 成员实现。 在这种情况下,既不使用 get 访问器关键字,也不使用 return 关键字。 下面的示例将只读 Name 属性作为 expression-bodied 成员实现。 ```csharp internal class Per { private string name; public string Name { get { return name; } set { name = value; } } public string Sex { get; set; } public int Age { get; init; } public string Country { get { return "中国"; } } public string PerInfo { get; init; } = "人员信息"; public string AllInfo => $"{Country},{Name},{Age},{Sex},{PerInfo}"; // 这是一个只读的属性 } ``` get 和 set 访问器都可以作为 expression-bodied 成员实现。 在这种情况下,必须使用 get 和 set 关键字。 下面的示例阐释如何为这两个访问器使用表达式主体定义。 return 关键字不与 get 访问器一起使用。 ```csharp internal class Per { private string name; public string Name { get => name; set => name = value; } } ``` ### 1.2. 自动实现的属性 在某些情况下,属性 get 和 set 访问器仅向支持`字段`赋值或仅从其中检索值,而不包括任何附加逻辑。 通过使用自动实现的属性,既能简化代码,还能让 C# 编译器透明地提供支持字段。 如果属性具有 get 和 set(或 get 和 init)访问器,则必须自动实现这两个访问器。 自动实现的属性通过以下方式定义:使用 get 和 set 关键字,但不提供任何实现。 下面的示例 Sex 和 Age 是自动实现的属性。 ```csharp internal class Per { public string Sex { get; set; } public int Age { get; init; } } ``` > 自动实现的属性是编译器会自动生成一个私有的后备字段来存储属性值 不能在接口中声明自动实现的属性。 自动实现的属性声明一个`私有实例支持字段`,在接口中不能这样做,因为接口不包含任何实例字段或方法的实现。接口只是定义了一组合同或契约,要求实现它的类或结构体提供这些成员的实现。 如果您想在接口中声明一个属性,您需要显式地声明访问器(get 和 set) ```csharp public interface IPer { int Level { get; set; } } internal class Per : IPer { public int Level { get; set; } = 0; } ``` **示例:**创建一个仅用于封装一组自动实现的属性的不可变轻型类,要实现不可变的轻量类,就需要保证类中的属性再被创建后不会被修改,下面有三种方式可保证属性除了创建时可赋值外,其它时刻调用者都不可对属性进行修改 + 仅声明 get 访问器,使属性除了能在该类型的构造函数中可变,在其他任何位置都不可变。 + 声明 init 访问器而不是 set 访问器,这使属性只能在构造函数中进行设置,或者通过使用对象初始值设定项设置。 + 将 set 访问器声明为专用。 属性可在该类型中设置,但它对于使用者是不可变的。 > 可以将 required 修饰符添加到属性声明中,以强制调用方将属性设置为初始化新对象的一部分 下面代码分别使用了三种方法来声明不可变属性 ```csharp public class Per { public Per(string name, int age, string country) { Name = name; Age = age; Country = country; } public string Name { get; init; } public int Age { get; private set; } public string Country { get; } } // 调用 static void Main(string[] args) { Per p = new Per("曹孟德", 56, "魏国"); p.Name = "刘备"; // 提示错误 : CS8852: 只能在对象初始值设定项中或在实例构造函数或init”访问器中的"this”或"base"上分配 init-only 属性或索引器“Per.Name" p.Age = 60; // 提示错误 : CS0272: 届性或索引器”Per.Age"不能用在此上下文中,因为 set 访问器不可访问 p.Country = "蜀国"; // 提示错误 : CS0200: 无法为属性或索引器”Per.Country”赋值 - 它是只读的 } ``` ### 1.3. 必需的属性 从 C# 11 开始,可以添加 required 成员以强制客户端代码初始化任何属性或字段 ```csharp public class SaleItem { public required string Name { get; set; } public required decimal Price { get; set; } } // 若要创建 SaleItem,必须使用对象初始值设定项设置 Name 和 Price 属性 var item = new SaleItem { Name = "Shoes", Price = 19.95m }; Console.WriteLine($"{item.Name}: sells for {item.Price:C2}"); ``` ## 2. 使用属性 属性的使用对于使用者来说和字段一样,但对于属性的实现者来将,需要实现`get`和`set`访问器代码,只有`get`访问器的属性为只读属性,只有`set`访问器的属性为只写属性,读写属性需要同时具备`get`和`set`,在C#9以上的版本中可以使用`init`来代替`set`访问器,它也是只读属性 > 与字段不同,属性不会被归类为变量。 因此,不能将属性作为 ref 或 out 参数传递 **属性具有许多用途:**它们可以先验证数据,再允许进行更改;可以在类上透明地公开数据,其中数据是从某个其他源(如数据库)检索到的;可以在数据发生更改时采取措施,例如引发事件或更改其他字段的值 **属性声明方式:**通过依次指定字段的访问级别、属性类型、属性名、声明 get 访问器和/或 set 访问器的代码块,在类块中声明属性 ```csharp public class Date { private int _month = 7; // 使用私有字段跟踪实际值(属性使用专用字段,被唤作”后备存储“,通常都是私有访问限制,确保只能通过属性对其进行操作) public int Month { get => _month; set // set 访问器可确保 Month 值设置在 1 至 12 之间 { if ((value > 0) && (value < 13)) { _month = value; } } } } ``` ### 2.1. get 访问器 get 访问器的正文类似于方法。 它必须返回属性类型的值,在C#中,属性的get访问器用于获取属性的值。如果编译器可以确定get访问器的实现并且优化开启,编译器有时会将get访问器的调用内联,这意味着它会直接在代码中使用属性值而不是调用方法,从而避免方法调用开销。但是,如果get访问器是虚拟的,编译器就无法进行内联,因为编译器在编译时不知道运行时代替哪个方法会被调用。 **示例:**通过示例理解get访问器的调用 ```csharp public class Person { private string name; // Name 属性的 get 访问器返回 name 私有变量的值。假设我们启用优化(例如使用 /O2 编译器选项),并且编译器能够确定 get 访问器的实现,编译器可能会内联对 get 访问器的调用。这意味着在代码中直接使用 name 变量而不是通过 Name 属性来获取它的值 public string Name { get { return name; } set { name = value; } } } ``` > /O2 编译器:指定的是用最快的速度 ```csharp // 给Name属性设置为一个虚拟属性,那 Name 属性就有一个额外的动态分派层。因为编译器不知道运行时代替哪个具体方法被调用(除非实际执行运行时类型检查),所以它不能安全地将对 get 访问器的调用内联。这意味着每次访问 Name 属性时,都会通过运行时分派机制来调用正确的 get 访问器实现,这增加了额外的开销 public virtual string Name { get { return name; } set { name = value; } } ``` #### 2.1.1. get 访问器使用 + 当属性是一个箭头函数时,通常这个属性是一个只读属性 ```csharp private string _name; public string Name => _name; ``` + 获取属性值的过程就是调用get访问器 ```csharp new Person().Name; //.Name 此处调用get访问器 ``` + get访问器可以是一个模式表达式 ```csharp private string _name; public string Name => _name != null ? _name : "NAN"; // 一个三元表达式 ``` ### 2.2. set 访问器 set 访问器类似于返回类型为 void 的方法。 它使用名为 value 的隐式参数,该参数的类型为属性的类型。 在下面的示例中,将 set 访问器添加到 Name 属性: ```csharp class Student { private string _name; public string Name { get => _name; set => _name = value; } } ``` 向属性赋值时,通过使用提供新值的自变量调用 set 访问器。 例如: ```csharp var student = new Student(); student.Name = "Joe"; // 此处调用set访问器 ``` ### 2.3. init 访问器 用于创建 init 访问器的代码与用于创建 set 访问器的代码相同,只不过前者使用的关键字是 init 而不是 set。 不同之处在于,init 访问器只能在构造函数中使用,或通过对象初始值设定项使用。 ### 2.4. 属性的扩展使用 属性的一些常用使用方式 #### 2.4.1. 属性访问修饰符 可以将属性标记为 public、private、protected、internal、protected internal 或 private protected。 这些访问修饰符定义该类的用户访问该属性的方式。 相同属性的 get 和 set 访问器可以具有不同的访问修饰符。 例如,get 可能为 public允许从类型外部进行只读访问;而 set 可能为 private 或 protected #### 2.4.2. 静态属性 可以通过使用 static 关键字将属性声明为静态属性。 静态属性可供调用方在任何时候使用,即使不存在类的任何实例 ```csharp public class StudyBaseObject{ // 声明了一个只读的静态属性 public static string Info { get { return "StudyBaseObject类静态属性Info"; } } } // 调用 StudyBaseObject.Info ``` #### 2.4.3. 虚拟属性 可以通过使用 virtual 关键字将属性标记为虚拟属性。 虚拟属性可使派生类使用 override 关键字重写属性行为。 重写虚拟属性的属性也可以是 sealed,指定对于派生类,它不再是虚拟的 ```csharp public class StudyObject { public virtual double Height { get => 122; set { } } } public class Study : StudyObject { // 重写了基类虚拟属性,并声明为封闭属性 public sealed override double Height { get => 185; set { } } } public class Study2 : Study { // 红线提示:CS0239:“Study2.Height”: 继承成员"Study.Height"是密封的,无法进行重写 public override double Height { get => 185; set { } } } // 调用 public static string Example() { StudyObject obj = new StudyObject(); msg += $"StudyObject虚拟属性:{ obj.Height};"; HiddrenObjctProperty obj2 = new HiddrenObjctProperty(); msg += $"HiddrenObjctProperty重写虚拟属性:{obj2.Height}"; return msg; // 输出:StudyObject虚拟属性:122;HiddrenObjctProperty重写虚拟属性:185 } ``` #### 2.4.4. 抽象属性 可以将属性声明为 abstract。 抽象属性不定义类中的实现,派生类必须写入自己的实现,实现抽象属性需要使用 override 关键字详细效果看下面的示例 ```csharp public abstract class PerInfo { public string Name { get; set; } public abstract double Weight { get; set; } // 声明一个抽象属性 } public class China : PerInfo { public China(double weight) => this.weight = weight; private double weight; public override double Weight { get=> weight * 1.2; set => value = weight; } // 实现抽象属性 } public class USA : PerInfo { public USA(double weight) => this.weight = weight; private double weight; public override double Weight { get => weight * 1.6; set => weight = value; } // 实现抽象属性 } public static string Example2() { China c = new China(100); msg += $"China:{c.Weight}\n"; USA u = new USA(100); msg += $"USA:{u.Weight}"; return msg; } // 输出:China:120 // USA:160 ``` #### 2.4.5. Hidden 属性 派生类中声明和基类同名的属性,从而隐藏的基类中的属性,派生类和基类属性使用相同命名时,需要使用 new 关键字,详细效果看下面的示例 ```csharp public class StudyObject { public string Name { get; set; } } public class HiddrenObjctProperty : StudyObject { public new string Name { get; set; } // 使用了 new 关键字,隐藏基类的 Name 属性 } public static class ExampleClass { static string msg = string.Empty; public static string Example1() { HiddrenObjctProperty hop = new HiddrenObjctProperty(); hop.Name = "曹操"; ((StudyObject)hop).Name = "刘备"; // 基类属性被隐藏后,需要转换成基类类型后才能能有效赋值 msg = $"HiddrenObjctProperty.Name = {hop.Name},StudyObject.Name = {((StudyObject)hop).Name}"; // 获取基类被隐藏的字段时,也需要转换成基类类型 return msg; // 输出结果:HiddrenObjctProperty.Name = 曹操,StudyObject.Name = 刘备 } } ``` ## 3. 接口属性 接口属性即在接口中声明的属性,在接口中的属性没有实现主体,接口属性可以通过访问器声明属性是否可读、可写,在接口属性中不会自动实现属性,接口属性的实现基本与抽象类的抽线属性实现一致 ```csharp public interface IStudyBaseObject { int Level { get; set; } } internal class StudyBaseObject : IStudyBaseObject { private int level; public int Level { get => level > 1 ? 1 : 0; set => level = value; } // 实现了IStudyBaseObject接口的Level属性 } ``` 需要注意的是如果要实现类同时实现两个接口,而这两个接口又同时存在相同签名的接口属性,那么实现类在实现属性时需要使用显示的接口属性名称,了解详细看下面的示例 ```csharp internal class StudyBaseObject : IStudyBaseObject, IStudyBaseObject2 { public StudyBaseObject(int l) { this.level = l; } public int level; // 显示的实现接口IStudyBaseObject2的Level int IStudyBaseObject2.Level { get => level > 1 ? 999999 : 0; set => level = value; } // 显示的实现接口IStudyBaseObject的Level int IStudyBaseObject.Level { get => level > 1 ? 1 : 0; set => level = value; } } public interface IStudyBaseObject { int Level { get => 1; set { } } } public interface IStudyBaseObject2 { int Level { get => 2; set { } } } // 调用 public static string Example3() { IStudyBaseObject c = new StudyBaseObject(2); msg += $"Example3:{c.Level},"; IStudyBaseObject2 c2 = new StudyBaseObject(2); msg += $"Example3:{c2.Level}\n"; return msg; // 输出:Example3:1,Example3:999999 } ``` ## 4. 限制访问器可访问性 属性或索引器的 get 和 set 部分称为访问器。 默认情况下,这些访问器具有与其所属属性或索引器相同的可见性或访问级别,不过,有时限制对其中某个访问器的访问是有益的。 例如,限制 set 访问器的可访问性,同时保持 get 访问器可公开访问 ```csharp public class Per{ private string _name = "Hello"; public string Name { get // get 访问器接收该属性本身的可访问性级别 public { return _name; } protected set //set 访问器,则通过对该访问器本身应用 protected 访问修饰符来进行显式限制 { _name = value; } } } public class Main{ Per p = new Per(); p.Name = "曹操"; // 错误提示: CS0272: 届性或索引器”Name.Age"不能用在此上下文中,因为 set 访问器不可访问 } ``` ### 4.1. 对访问器的访问修饰符的限制 对属性或索引器使用访问修饰符受以下条件的制约: + 不能对`接口`或`显式接口成员实现`使用访问器修饰符。 ```csharp public interface IStudyBaseObject { // public int Level { private get; set; } // IStudyBaseObject.Level.getCS0442:“IStudyBaseObject.Level.get”: 抽属性不能具有专用访问器 public int Level { get; set; } } public class StudyBaseObject:IStudyBaseObject { int IStudyBaseObject.Level { private get => level > 1 ? 999999 : 0; set => level = value; } // CS0106: 修饰符”private"对该项无效 } ``` + 仅当属性或索引器同时具有 set 和 get 访问器时,才能使用访问器修饰符。 这种情况下,只允许对其中一个访问器使用修饰符 ```csharp public int Age { private get; } // CS0276:“StudyObject.Ages": 仅当性或索引器同时具有 get 访问和 set 访问器时,才能对访问器使用可访问性修饰符 ``` + 如果属性或索引器具有 override 修饰符,则访问器修饰符必须与被重写的访问器的访问器(如有)相同 ```csharp public virtual double Height { get => 122; set { } } public override double Height { private get => 185; set { } } // CS0507:“Hiddren0bjctProperty,.Height.get” 当重写public继承成员"StudyObject,Height,get”时,无法更改访问修饰符 ``` + 访问器的可访问性级别必须比属性或索引器本身的可访问性级别具有更严格的限制 ```csharp private int Age { public get; set; } // CS0273:“StudyObject,Age.get”访问器的可访问性修饰符必须比性或索引器"StudyObject,Age"具有更强的限制 ``` ### 4.2. 重写访问器的访问修饰符 重写属性或索引器时,被重写的访问器对重写代码而言必须是可访问的。 此外,属性/索引器及其访问器的可访问性都必须与相应的被重写属性/索引器及其访问器匹配。 例如: ```csharp public class Parent { public virtual int TestProperty { protected set { } get { return 0; } } } public class Kid : Parent { public override int TestProperty { // 使用与重写访问器中相同的可访问性级别 protected set { } // 这里不能使用访问修饰符 get { return 0; } } } ``` ### 4.3. 实现接口 使用访问器实现接口时,访问器不一定有访问修饰符。 但如果使用一个访问器(如 get)实现接口,则另一个访问器可以具有访问修饰符,如下面的示例所示: ```csharp public interface ISomeInterface { int TestProperty { // 接口这里不允许使用访问修饰符 get; } } public class TestClass : ISomeInterface { public int TestProperty { // 实现接口属性,这里也不可以使用访问修饰符 get { return 10; } // 接口没有声明set访问器,这里可以使用修饰符 protected set { } } } ``` ### 4.4. 访问器可访问性域 + 如果对访问器使用访问修饰符,则访问器的可访问性域由该修饰符确定。 + 如果未对访问器使用访问修饰符,则访问器的可访问性域由属性或索引器的可访问性级别确定。 ## 5. 声明和使用读写属性 通过控制属性的 set 和 get 访问器来声明字段的可写,可读性,set 访问器可分配数据成员,get 访问器检索数据成员值 ```csharp public string Name { get; set; } // 可读可写 public int Age { set; } // 只写 public string Country { get; } // 只读 public string PerInfo { get; private set } // 只读(通过控制set访问器的访问限制来使属性变成只读【set访问器也可以换成inti访问器】) ``` ## 6. 扩展内容 ### 6.1. 属性与字段的区别 |对比项|说明| |--|--| |封装|属性提供了封装,隐藏了内部实现细节。而字段则直接暴露了存储的值。| |可读性|属性使代码更具可读性,因为它们在语法上看起来像是公共字段,但实际上可能有额外的逻辑(如验证、触发事件等)。| |扩展性|你可以在不修改使用属性的代码的情况下,通过修改属性的访问器来添加额外的逻辑(例如在设置值时添加验证)。| |可控性|通过只提供get或set访问器,你可以控制对属性或字段的访问权限。例如,你可能只希望提供一个只读属性(只有get没有set)。| [=========] ## 参考资料 + [《属性》](https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/properties) + [《使用属性》](https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/using-properties) + [《如何声明和使用读/写属性》](https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/how-to-declare-and-use-read-write-properties) + [《自动实现的属性》](https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties) + [《限制访问器可访问性》](https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/restricting-accessor-accessibility) + [《接口属性》](https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/interface-properties) + [《如何声明和使用读/写属性》](https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/how-to-declare-and-use-read-write-properties) + [《如何使用自动实现的属性实现轻量类》](https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/how-to-implement-a-lightweight-class-with-auto-implemented-properties) + [《/O1、/O2(最小化大小、最大化速度)》](https://learn.microsoft.com/zh-cn/cpp/build/reference/o1-o2-minimize-size-maximize-speed?view=msvc-170)
扫一扫手机看
点击排行
windows 文件资源管理器使用
Layui 弹窗open使用说明
SQL Server 中字符串函数使用
C# 使用QQ邮箱代理做邮件接收功能
SQL Server关于apply的两种形式cross apply 和 outer apply
ASP.NET MVC 模型的自定义验证
nvm安装及常用命令
SQL Server 中数据类型转换
SQL Server 中WITH AS短语的使用
IE浏览器下AJAX缓存问题导致数据不更新的解决办法
订阅号