-
封装:访问修饰符与属性(Property)
封装:访问修饰符与属性(Property)
-
封装的核心概念
封装是面向对象编程(OOP)三大特性之一,核心是隐藏类的内部实vb.net教程C#教程python教程SQL教程access 2010教程现细节,仅对外暴露可控的访问接口。通过封装,我们可以保护类的内部数据不被非法修改,同时降低代码的耦合度,提高可维护性。
封装的实现依赖两个关键工具:访问修饰符(控制成员可见性)和属性(封装字段的访问)。 -
访问修饰符:控制成员的可见性
访问修饰符用于定义类或类成员(字段、属性、方法等)的可访问范围,是封装的基础。C#中常用的访问修饰符有以下6种:
修饰符 可见范围
public 所有类均可访问(对外完全暴露)
private 仅当前类内部可访问(完全隐藏,默认修饰符)
protected 当前类及子类可访问
internal 同一程序集(Assembly)内的类可访问
protected internal 同一程序集内的类或当前类的子类可访问(组合protected和internal)
private protected 同一程序集内的当前类的子类可访问(C#7.2+新增,更严格的protected)
实例:访问修饰符的使用
csharp
public class Person
{
// private字段:仅类内部可访问
private string _name;
private int _age;
// protected方法:当前类及子类可访问
protected void ValidateAge(int age)
{
if (age < 0 || age > 120)
throw new ArgumentOutOfRangeException(nameof(age), "年龄必须在0-120之间");
}
// public方法:所有类可访问
public void SetInfo(string name, int age)
{
_name = name;
ValidateAge(age); // 调用protected方法
_age = age;
}
// internal属性:同一程序集内可访问
internal string GetName()
{
return _name;
}
}
逐行讲解:
private string _name:_name是私有字段,外部无法直接修改,只能通过类内部的方法(如SetInfo)间接访问。
protected void ValidateAge:ValidateAge是保护方法,只能在Person类或其子类中调用,用于验证年龄合法性。
public void SetInfo:SetInfo是公共方法,对外暴露的接口,外部通过它设置_name和_age,同时内部会调用ValidateAge确保数据合法。
internal string GetName:GetName是内部方法,同一程序集内的其他类可以调用它获取_name的值。
3. 属性(Property):封装字段的可控接口
属性是封装字段的常用方式,它通过get(读取)和set(写入)方法控制对字段的访问,相比直接使用public字段,属性可以添加验证逻辑、日志记录等额外操作,确保数据安全。
3.1 字段 vs 属性:为什么不用public字段?
直接使用public字段会导致外部可以随意修改数据,无法保证合法性。例如:
csharp
// 错误示例:public字段无保护
public class Person
{
public int Age; // 外部可直接赋值Age=-10,导致数据非法
}
而属性通过set方法可以添加验证逻辑,避免非法数据:
csharp
// 正确示例:用属性封装字段
public class Person
{
private int _age; // 私有字段,隐藏内部存储
// 公共属性:对外暴露可控访问
public int Age
{
get { return _age; } // 读取字段值
set
{
// 添加验证逻辑
if (value >=0 && value <=120)
_age = value;
else
throw new ArgumentOutOfRangeException(nameof(value), "年龄必须在0-120之间");
}
}
}
3.2 属性的语法与类型
属性的基本语法由get和set访问器组成,根据访问器的有无,可分为以下类型:
(1)读写属性(Full Property)
同时包含get和set访问器,允许读写操作:
csharp
public class Person
{
private string _name;
public string Name
{
get { return _name; } // 读取
set { _name = value; } // 写入(value是关键字,代表外部传入的值)
}
}
(2)只读属性
仅包含get访问器,不允许外部写入:
csharp
public class Person
{
private DateTime _birthDate;
// 只读属性:计算年龄(无法直接修改)
public int Age
{
get
{
return DateTime.Now.Year - _birthDate.Year;
}
}
// 构造函数:初始化生日
public Person(DateTime birthDate)
{
_birthDate = birthDate;
}
}
(3)只写属性
仅包含set访问器(很少使用,通常用于敏感数据的写入):
csharp
public class Person
{
private string _password;
// 只写属性:仅允许设置密码,不允许读取
public string Password
{
set { _password = HashPassword(value); } // 写入时加密
}
private string HashPassword(string password)
{
// 模拟加密逻辑
return password.GetHashCode().ToString();
}
}
(4)自动实现的属性(Auto-Implemented Property)
对于简单的属性(无额外逻辑),可以省略显式字段,由编译器自动生成隐藏字段(称为“后备字段”):
csharp
public class Person
{
// 自动属性:编译器自动生成private string _name;
public string Name { get; set; }
// 只读自动属性(C#6+):仅允许构造函数或初始化器赋值
public DateTime BirthDate { get; }
// 构造函数初始化只读属性
public Person(DateTime birthDate)
{
BirthDate = birthDate;
}
}
(5)Init-Only属性(C#9+)
init访问器允许在对象初始化时赋值,之后无法修改(用于不可变对象):
csharp
public class Person
{
// Init-only属性:仅在初始化时赋值
public string Name { get; init; }
public int Age { get; init; }
}
// 使用:初始化器赋值
var person = new Person { Name = "张三", Age = 25 };
// person.Name = "李四"; // 编译错误:init属性无法后续修改
-
封装的实践:完整类实例
以下是一个结合访问修饰符和属性的完整实例,展示封装的最佳实践:
csharp
using System;
public class Person
{
// 私有字段:隐藏内部数据
private string _name;
private int _age;
private string _email;
// 静态私有字段:类级别的常量(所有实例共享)
private static readonly string _defaultEmailSuffix = "@example.com";
// 构造函数:初始化必填字段
public Person(string name, int age)
{
_name = name;
Age = age; // 调用属性的set方法,验证年龄
_email = $"{name.ToLower()}{_defaultEmailSuffix}"; // 自动生成邮箱
}
// 公共属性:封装_name(读写)
public string Name
{
get { return _name; }
set
{
if (!string.IsNullOrEmpty(value))
_name = value;
else
throw new ArgumentNullException(nameof(value), "姓名不能为空");
}
}
// 公共属性:封装_age(读写+验证)
public int Age
{
get { return _age; }
set
{
if (value >=0 && value <=120)
_age = value;
else
throw new ArgumentOutOfRangeException(nameof(value), "年龄必须在0-120之间");
}
}
// 只读属性:封装_email(仅读取)
public string Email
{
get { return _email; }
}
// 公共方法:对外暴露的行为
public void Introduce()
{
Console.WriteLine($"我是{_name},今年{_age}岁,邮箱是{_email}");
}
}
// 使用示例
class Program
{
static void Main()
{
try
{
var person = new Person("张三", 25);
person.Introduce(); // 输出:我是张三,今年25岁,邮箱是zhangsan@example.com
person.Age = 30; // 修改年龄(合法)
person.Name = "李四"; // 修改姓名(合法)
// person.Email = "lisi@example.com"; // 编译错误:只读属性无法修改
person.Age = 150; // 抛出异常:年龄超出范围
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
逐行讲解:
private string _name:私有字段,隐藏内部存储,外部无法直接访问。
static readonly string _defaultEmailSuffix:静态只读字段,类级别的常量,所有实例共享。
public Person(string name, int age):构造函数,初始化必填字段,调用Age属性验证年龄。
public string Name:公共属性,通过set方法验证姓名非空。
public int Age:公共属性,通过set方法验证年龄范围。
public string Email:只读属性,仅允许读取自动生成的邮箱地址。
public void Introduce:公共方法,对外暴露的行为,内部使用私有字段。
5. 基础知识拓展
5.1 封装的好处
数据安全:通过属性验证逻辑,避免非法数据输入。
代码可维护性:修改内部实现(如字段名)时,只需调整属性的get/set方法,外部代码无需改动。
降低耦合度:外部仅依赖属性接口,不依赖内部实现细节。
5.2 自动属性的背后原理
自动实现的属性(如public string Name { get; set; })看似没有字段,实则编译器会自动生成一个后备字段(Backing Field),例如:
csharp
// 编译器自动生成的后备字段
private string <Name>k__BackingField;
// 自动属性的实际实现
public string Name
{
get { return <Name>k__BackingField; }
set { <Name>k__BackingField = value; }
}
5.3 接口中的属性
接口中可以定义属性,但只能声明访问器,不能包含实现:
csharp
public interface IPerson
{
string Name { get; set; } // 接口属性:仅声明
int Age { get; } // 只读接口属性
}
// 实现接口
public class Student : IPerson
{
public string Name { get; set; }
public int Age { get; set; }
}
-
总结
封装是OOP的核心特性,通过访问修饰符控制成员可见性,通过属性封装字段的访问,实现了“隐藏细节、暴露接口”的目标。合理使用封装可以让代码更健壮、易维护,是编写高质量C#程序的基础。
关键要点:
优先使用private字段隐藏内部数据,通过public属性对外暴露可控访问。
属性的set方法应添加验证逻辑,确保数据合法性。
避免使用public字段,除非是常量(const或readonly)。










