目录
C# 属性(Property)
属性是一种成员,它提供灵活的机制来读取、写入或计算私有字段的值。 属性可用作公共数据成员,但它们是称为“访问器”的特殊方法。 此功能使得可以轻松访问数据,还有助于提高方法的安全性和灵活性。
属性概述
属性是一种 C# 语言中用来公开获取和设置值的机制,它们实质上是对字段的封装,可以隐藏实现细节并加入验证逻辑。在 C# 中,属性通常由一对 get 和 set 方法组成,分别用于获取和设置属性的值。在较新版本的 C# 中,还引入了 init 访问器,用于在对象构造期间进行赋值操作。以下是属性的一些重要概念:
-
get 和 set 访问器:get 访问器用于返回属性的值,而 set 访问器用于分配新的值给属性。在 C# 9 及以上版本中,init 访问器则用于在对象构造过程中分配新值。
-
value 关键字:在 set 或 init 访问器中,可以使用 value 关键字来表示要设置的新值。
-
访问级别:get、set 和 init 访问器可以具有不同的访问级别,这样可以对属性的读写访问进行精细控制。
-
属性类型:属性可以是读写属性(既有 get 访问器又有 set 访问器)、只读属性(只有 get 访问器,没有 set 访问器)或者只写属性(只有 set 访问器,没有 get 访问器)。
-
自动实现的属性:对于简单的属性,不需要编写自定义的 get 和 set 方法,可以使用表达式主体定义或自动实现属性。
属性的设计使得类可以更好地封装数据,并提供更加灵活的访问控制和验证逻辑。值得注意的是,属性是面向对象编程中的重要概念,能够提高代码的可读性、可维护性和安全性。
具有支持字段的属性
在 C# 中,属性通常用于对字段进行封装,从而提供对字段的访问控制和验证逻辑。以下是一个示例,展示了如何定义一个具有支持字段的属性:
public class Person
{
private string _name; // 支持字段
public string Name // 属性
{
get { return _name; } // get 访问器
set { _name = value; } // set 访问器
}
}
在这个示例中,Person 类包含一个私有字段 _name 和一个公共属性 Name。属性 Name 的 get 访问器用于返回私有字段 _name 的值,而 set 访问器用于分配新值给私有字段。这种做法使得属性成为字段的外部接口,可以隐藏内部实现的细节。
使用支持字段的属性可以带来以下好处:
- 封装性:通过属性访问字段,可以将字段的实现细节隐藏起来,提高了类的封装性。
- 验证逻辑:在属性的 set 访问器中,可以加入验证逻辑,确保设置的值符合特定规则。
- 访问控制:通过属性可以灵活地控制字段的读写访问,例如只读、只写或读写等。
这种使用属性对字段进行封装的方式是面向对象编程中的一种良好实践,能够提高代码的可维护性和安全性。
表达式主体定义
在 C# 6 及更高版本中,引入了表达式主体定义(Expression-bodied definition)的语法,使属性的定义更加简洁。使用表达式主体定义,可以省略属性的 get 和 set 访问器的大括号,直接用一个表达式来表示其实现。
以下是一个使用表达式主体定义的属性示例:
public class Person
{
private string _name;
public string Name
{
get => _name; // 使用表达式主体定义的 get 访问器
set => _name = value; // 使用表达式主体定义的 set 访问器
}
}
在这个示例中,属性 Name 的 get 和 set 访问器使用了表达式主体定义,并通过箭头(=>)将表达式与访问器关联起来。在这种情况下,表达式主体定义等效于使用大括号包裹的单条语句。
使用表达式主体定义可以带来以下好处:
- 简洁性:表达式主体定义能够更简洁地表示属性的实现,减少了代码量。
- 可读性:通过使用表达式主体定义,可以更清晰地表达属性的意图,提高代码的可读性。
- 一致性:表达式主体定义可以与其他成员(如方法)的定义方式保持一致,统一了代码风格。
需要注意的是,表达式主体定义仅适用于简单的属性实现,如果需要包含更复杂的逻辑或多条语句,仍然需要使用传统的大括号包裹的方式定义访问器。
总之,表达式主体定义是一种简洁而清晰的属性定义方式,可以提高代码的可读性和一致性。
自动实现的属性
在 C# 中,自动实现属性(Auto-implemented properties)是一种简化属性定义的方式,它允许开发者在不显式定义支持字段的情况下,直接定义属性并让编译器自动生成支持字段。这种方式可以减少重复的代码,并且适用于属性的逻辑比较简单的情况。
以下是一个使用自动实现属性的示例:
public class Person
{
public string Name { get; set; } // 自动实现的属性
}
在这个示例中,属性 Name 被定义为自动实现属性。在这种情况下,编译器会自动创建一个支持字段来存储属性的值,开发者无需显式定义这个字段。同时,编译器也会自动实现属性的 get 和 set 访问器,以便对支持字段进行读取和赋值操作。
使用自动实现属性可以带来以下好处:
- 简洁性:自动实现属性消除了定义支持字段和访问器的需要,使属性定义更加简洁。
- 可读性:通过使用自动实现属性,可以更清晰地表达属性的意图,提高代码的可读性。
- 减少重复代码:避免了为每个属性显式编写相似的支持字段和访问器的重复工作。
需要注意的是,自动实现属性并不适用于所有情况。例如,如果属性需要包含特定的逻辑、验证或与其他字段交互,那么就需要使用传统的属性定义方式,以便在访问器中添加所需的逻辑。
总之,自动实现属性是一种方便且简洁的属性定义方式,适用于简单的属性场景,并能够提高代码的可读性和简洁性。
必需的属性
在 C# 11 中,我们可以使用 required 关键字来标记属性或字段,以确保客户端代码在实例化对象时必须初始化这些成员。这样可以提高代码的安全性和可读性。
public class SaleItem
{
public required string Name
{ get; set; }
public required decimal Price
{ get; set; }
}
在 SaleItem 类中,Name 和 Price 属性被标记为 required,这表示它们是必需的属性,并且在实例化对象时必须进行初始化。
通过使用对象初始值设定项,我们可以在实例化 SaleItem 对象时为这两个属性指定初始值,如下所示:
var item = new SaleItem { Name = "Shoes", Price = 19.95m };
Console.WriteLine($"{item.Name}: sells for {item.Price:C2}");
在这个示例中,我们使用对象初始化器为 Name 和 Price 属性设置初始值。这样,我们就遵循了要求,在实例化对象时必须为这两个属性提供初始值。
这种方式可以确保在编译时捕获未初始化属性的错误,并提醒我们提供必要的初始值。这有助于减少潜在的运行时错误,并提高代码的健壮性和可靠性。
使用属性
get 访问器
在 C# 中,属性可以包含一个可选的 get 访问器,用于获取属性的值。get 访问器定义了当访问属性时所执行的操作。当我们使用属性访问表达式来获取属性的值时,就会调用该属性的 get 访问器。
下面是一个简单的示例,展示了如何定义一个带有 get 访问器的属性:
public class Person
{
private string name;
public string Name
{
get
{
return name;
}
}
}
在这个示例中,属性 Name 包含了一个 get 访问器,它定义了获取属性值的逻辑。在这种情况下,当我们访问 Name 属性时,将返回私有字段 name 的值。
我们可以像访问字段一样使用属性来获取其值:
Person person = new Person();
string personName = person.Name; // 调用 Name 属性的 get 访问器来获取属性值
需要注意的是,如果属性只包含 get 访问器而没有 set 访问器,那么这个属性就是一个只读属性,只能在类的内部进行赋值。外部代码只能通过 get 访问器来获取属性的值,而无法对其进行赋值。
总之,get 访问器允许我们在属性被访问时执行特定的逻辑,例如计算属性的值、对属性进行验证等。这种方式使得属性在外部代码中的使用更加灵活和安全。
set 访问器
在 C# 中,属性可以包含一个可选的 set 访问器,用于设置属性的值。set 访问器定义了当对属性进行赋值时所执行的操作。当我们使用赋值语句给属性赋值时,就会调用该属性的 set 访问器。
下面是一个简单的示例,展示了如何定义一个带有 get 和 set 访问器的属性:
public class Person
{
private string name;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
}
在这个示例中,属性 Name 包含了一个 get 访问器和一个 set 访问器。get 访问器用于获取属性值,而 set 访问器用于设置属性值。在这种情况下,当我们为 Name 属性赋值时,将调用 set 访问器,并将赋予属性的新值作为参数传递给 set 访问器。
我们可以像操作字段一样使用属性来设置其值:
Person person = new Person();
person.Name = "Alice"; // 调用 Name 属性的 set 访问器来设置属性值
需要注意的是,如果属性只包含 get 访问器而没有 set 访问器,那么这个属性就是一个只读属性,只能在类的内部进行赋值。外部代码只能通过 get 访问器来获取属性的值,而无法对其进行赋值。
总之,set 访问器允许我们在属性被赋值时执行特定的逻辑,例如对新值进行验证、触发事件等。这种方式使得属性在被赋值时具有更多的灵活性和安全性。
init 访问器
init 访问器是用 init 关键字来声明的,用于限制属性只能在构造函数中或对象初始化器中进行初始化。不同之处在于,init 访问器只能在构造函数中使用,或通过对象初始值设定项使用。
下面是一个简单的示例,展示了如何创建带有 init 访问器的属性:
public class Person
{
public string Name { get; init; }
public int Age { get; init; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
在这个示例中,Name 和 Age 属性都使用了 init 访问器,这意味着它们只能在构造函数中进行初始化。因此,一旦对象被创建并初始化完成,就无法再修改它们的值。
除了构造函数外,我们还可以 通过对象初始值设定项 来设置具有 init 访问器的属性的初始值:
Person person = new Person
{
Name = "Alice",
Age = 25
};
在这种情况下,对象初始化器提供了另一种方式来初始化具有 init 访问器的属性。
需要注意的是,如果尝试在对象初始化完成后修改具有 init 访问器的属性的值,将会导致编译错误。
总而言之,init 访问器提供了一种在对象初始化期间设置属性初始值的机制,从而增强了对象的不变性和安全性。
备注
可以将属性标记为 public、private、protected、internal、protected internal 或 private protected。 这些访问修饰符定义该类的用户访问该属性的方式。 相同属性的 get 和 set 访问器可以具有不同的访问修饰符。 例如,get 可能为 public允许从类型外部进行只读访问;而 set 可能为 private 或 protected。 有关详细信息,请参阅访问修饰符。
可以通过使用 static 关键字将属性声明为静态属性。 静态属性可供调用方在任何时候使用,即使不存在类的任何实例。 有关详细信息,请参阅静态类和静态类成员。
可以通过使用 virtual 关键字将属性标记为虚拟属性。 虚拟属性可使派生类使用 override 关键字重写属性行为。 有关这些选项的详细信息,请参阅继承。
重写虚拟属性的属性也可以是 sealed,指定对于派生类,它不再是虚拟的。 最后,可以将属性声明为 abstract。 抽象属性不定义类中的实现,派生类必须写入自己的实现。 有关这些选项的详细信息,请参阅抽象类、密封类及类成员。
注意:在 static 属性的访问器上使用 virtual、abstract 或 override 修饰符是错误的。
接口属性
接口属性是指在接口中声明的属性。在 C# 中,接口可以包含属性的声明,但不能包含属性的实现。
注意,接口中的属性不包含实际的实现代码,只是定义了属性的特征和行为。实现接口的类需要提供属性的具体实现。
下面是一个代码示例,展示了接口属性的声明和实现,以及如何在实现类中使用这些属性:
using System;
public interface IShape
{
double Area { get; }
double Perimeter { get; }
}
public class Rectangle : IShape
{
private double _length;
private double _width;
public double Length
{
get => _length;
set => _length = value > 0 ? value : throw new ArgumentException("长度必须大于0。");
}
public double Width
{
get => _width;
set => _width = value > 0 ? value : throw new ArgumentException("宽度必须大于0。");
}
public double Area => _length * _width;
public double Perimeter => 2 * (_length + _width);
}
public class Program
{
public static void Main()
{
Rectangle rectangle = new Rectangle();
rectangle.Length = 5;
rectangle.Width = 3;
Console.WriteLine("矩形属性:");
Console.WriteLine("长度: " + rectangle.Length);
Console.WriteLine("宽度: " + rectangle.Width);
Console.WriteLine("面积: " + rectangle.Area);
Console.WriteLine("周长: " + rectangle.Perimeter);
}
}
在这个示例中,我们定义了一个名为 IShape 的接口,其中包含了两个只读属性 Area 和 Perimeter。
然后,我们定义了一个名为 Rectangle 的类,它实现了 IShape 接口,并提供了对应的属性实现。在 Rectangle 类中,我们使用私有字段 _length 和 _width 来存储矩形的长度和宽度。通过属性访问器,我们可以对这些字段进行读取和写入,并在设置属性值时进行有效性检查。
Area 属性返回矩形的面积,即长度乘以宽度;Perimeter 属性返回矩形的周长,即两倍的长度加上两倍的宽度。
在 Main 方法中,我们创建了 Rectangle 类的实例,并通过属性访问器给属性赋值。然后,我们打印出矩形的属性值,包括长度、宽度、面积和周长。
限制访问器可访问性
属性或索引器的 get 和 set 部分称为访问器。 默认情况下,这些访问器具有与其所属属性或索引器相同的可见性或访问级别。 有关详细信息,请参阅可访问性级别。 不过,有时限制对其中某个访问器的访问是有益的。 通常,限制 set 访问器的可访问性,同时保持 get 访问器可公开访问。
对访问器的访问修饰符的限制
在 C# 中,可以对属性和索引器的访问器使用不同的访问修饰符,但有几个限制条件需要注意:
- 访问修饰符必须同时应用于属性或索引器的 get 和 set 访问器。
- 访问修饰符的可访问性级别必须比属性或索引器本身的可访问性级别更严格。
这意味着,如果属性或索引器是公共的(public),则其访问器可以是公共的、受保护的(protected)、内部的(internal)或私有的(private)。但如果属性或索引器是受保护的、内部的或私有的,则其访问器必须具有相同或更严格的访问级别。
下面是一个示例,演示了如何使用访问修饰符来限制属性和索引器的访问性:
public class MyClass
{
private int _myProperty;
public int MyProperty
{
// 公共的 get 访问器
public get { return _myProperty; }
// 受保护的 set 访问器
protected set { _myProperty = value; }
}
private string[] _myArray = new string[5];
public string this[int index]
{
// 内部的 get 访问器
internal get { return _myArray[index]; }
// 私有的 set 访问器
private set { _myArray[index] = value; }
}
}
在这个示例中,MyProperty 属性具有公共的 get 访问器和受保护的 set 访问器。这意味着该属性可以从任何地方进行读取,但只能在派生类中进行写入。
this[int index] 索引器具有内部的 get 访问器和私有的 set 访问器。这意味着索引器可以从程序集内的任何位置进行读取,但只能在类的内部进行写入。
总结起来,属性和索引器的访问修饰符受到上述条件的限制:必须同时应用于 get 和 set 访问器,并且访问修饰符的可访问性级别必须比属性或索引器本身的级别更严格。
重写访问器的访问修饰符
在C#中,当你重写一个属性或索引器的访问器时,你可以选择修改访问修饰符。
当你在子类中重写一个属性或索引器的访问器时,你可以使用与父类中相同或更宽松的访问修饰符。这是因为子类可以具有比父类更广泛的访问权限。
例如,如果父类中的属性的访问修饰符是 protected,你可以在子类中将访问修饰符更改为 public。这样,子类就可以提供对该属性的公共访问。
另一方面,如果父类中的属性的访问修饰符是 public,你不可以在子类中将访问修饰符更改为 protected,因为这会降低访问权限。
下面是一个示例,演示了如何在子类中重写属性的访问器并修改访问修饰符:
public class BaseClass
{
protected int MyProperty { get; set; }
}
public class DerivedClass : BaseClass
{
// 重写父类属性的访问器,并将访问修饰符更改为 public
public override int MyProperty
{
get { return base.MyProperty; }
set { base.MyProperty = value; }
}
}
在这个示例中,BaseClass 定义了一个受保护的属性 MyProperty。在 DerivedClass 中,我们重写了 MyProperty 的访问器,并将访问修饰符更改为公共的。这样,DerivedClass 就可以提供对属性的公共访问。
需要注意的是,你不能将访问修饰符更改为比父类更严格的访问级别。例如,如果父类中的属性是公共的,你不能在子类中将其更改为受保护的或私有的。
总结起来,当你重写属性或索引器的访问器时,你可以使用与父类中相同或更宽松的访问修饰符。这允许你在子类中提供更广泛的访问权限。
实现接口
使用访问器实现接口时,访问器不一定有访问修饰符。 但当你使用一个访问器(如 get)实现接口时,另一个访问器(如 set)可以具有访问修饰符。但是,你需要确保其访问修饰符与接口定义中的相关访问修饰符一致或更宽松。
如果接口定义的访问修饰符是 public,则实现该接口的访问器必须至少是 public。这是因为接口定义要求实现成员具有与接口成员相同或更高的可见性。
下面是一个示例,演示了使用访问器实现接口并具有不同访问修饰符的情况:
public interface IMyInterface
{
int MyProperty { get; set; }
}
public class MyClass : IMyInterface
{
// 使用公共的 get 和 私有的 set 访问器实现接口
public int MyProperty
{
public get { return 0; }
private set { }
}
}
在这个示例中,IMyInterface 定义了一个具有公共的 get 和 set 访问器的属性 MyProperty。然后,MyClass 类实现了这个接口,并提供了具有公共的 get 和私有的 set 访问器的属性 MyProperty。
需要注意的是,尽管对于属性 MyProperty 的 set 访问器使用了私有的访问修饰符,但这不影响接口的实现。因为接口只要求访问修饰符与接口成员一致或更宽松。
总结起来,当你使用访问器实现接口时,访问修饰符的规则是:访问修饰符必须与接口定义中的相关访问修饰符一致或更宽松。这样可以确保接口的实现符合接口成员的可见性要求。
访问器可访问性域
如果对访问器使用访问修饰符,则访问器的可访问性域由该修饰符确定。
如果未对访问器使用访问修饰符,则访问器的可访问性域由属性或索引器的可访问性级别确定。
示例
下面是一个代码示例,演示了如何使用访问修饰符来限制访问器的可访问性:
using System;
public interface IMyInterface
{
int MyProperty { get; }
}
public class MyClass : IMyInterface
{
private int myPropertyValue;
// 实现接口要求的公共的 get 访问器
public int MyProperty
{
get
{
return myPropertyValue;
}
private set // 类中私有的 set 访问器
{
myPropertyValue = value;
}
}
}
public class Program
{
public static void Main()
{
MyClass myObject = new MyClass();
// 接口要求的属性可以通过公共的 get 访问器在外部访问
int value = myObject.MyProperty;
Console.WriteLine("MyProperty value: " + value);
// 类中私有的 set 访问器仅在类内部可访问
// 所以这里会导致编译错误
//myObject.MyProperty = 10;
}
}
在这个示例中,IMyInterface 接口定义了一个具有公共的 get 访问器的属性 MyProperty。在 MyClass 类中,我们使用了接口要求的get 访问器,并将 set 访问器限制为私有的。在 Main 方法中,我们演示了如何通过公共的 get 访问器在外部访问属性的值,以及尝试使用私有的 set 访问器设置属性的值,由于私有的 set 访问器只能在类内部访问,所以最后一行代码会导致编译错误。