C# 属性访问器权限控制:从 `public get; private set;` 到更精细的封装策略

发布时间:2026/7/15 1:58:03
C# 属性访问器权限控制:从 `public get; private set;` 到更精细的封装策略 1. 为什么需要属性访问器权限控制在C#中属性Property是封装字段Field的重要机制。想象一下你有一个银行账户类里面存着用户的余额。如果直接把余额字段公开任何人都可以随意修改它这显然不安全。这时候就需要属性访问器来控制谁可以读取、谁可以修改这个值。最常见的场景就是public get; private set;这种组合。它允许外部代码读取属性值但只有类内部可以修改。比如public class BankAccount { public decimal Balance { get; private set; } public void Deposit(decimal amount) { if(amount 0) Balance amount; } }这样设计有几个明显好处安全性防止外部代码随意修改关键数据可控性修改数据时可以加入验证逻辑灵活性未来可以修改内部实现而不影响外部调用我见过不少项目因为没做好封装导致数据被意外修改的bug。有一次调试了半天最后发现是某个字段被外部直接修改了如果当初用了private set问题根本不会发生。2. 基础访问修饰符详解2.1 标准访问修饰符C#提供了几种基本的访问修饰符来控制属性访问器的可见性public完全公开任何代码都可以访问private仅限当前类内部访问protected当前类及其派生类可以访问internal同一程序集内的代码可以访问protected internal当前程序集或派生类可以访问这些修饰符可以自由组合在get和set访问器上。比如public class Example { // 外部可读仅派生类可写 public string Name { get; protected set; } // 同一程序集可读仅当前类可写 internal int Count { get; private set; } }2.2 实际应用场景不同场景适合不同的权限组合配置类通常使用public get; private set;允许读取但不允许随意修改public class AppSettings { public string ApiKey { get; private set; } }基类状态使用protected set让派生类可以修改状态public abstract class Vehicle { public int Speed { get; protected set; } }模块内部共享使用internal让同一程序集的类可以访问public class CacheManager { internal static Cache Instance { get; private set; } }3. 进阶封装策略3.1 程序集级别的封装当你的项目由多个程序集组成时internal修饰符就特别有用。比如你在开发一个SDK有些功能只希望在SDK内部使用不希望暴露给使用者public class SdkClient { // 外部可以调用 public void Connect() { /*...*/ } // 只有SDK内部可以使用 internal void ResetConnection() { /*...*/ } }protected internal则更进一步允许派生类访问即使它们在不同的程序集中public class BaseClass { protected internal string InternalData { get; set; } } // 在另一个程序集中 public class DerivedClass : BaseClass { public void UseData() { // 可以访问基类的protected internal成员 Console.WriteLine(InternalData); } }3.2 继承链上的精细控制在设计类层次结构时可以通过组合不同修饰符实现精细控制public abstract class Animal { // 派生类可以读取和修改 protected string GeneticCode { get; set; } // 派生类可以读取但不能修改 public virtual int LegCount { get; protected set; } 4; } public class Bird : Animal { public Bird() { LegCount 2; // 可以设置 GeneticCode AVES; // 可以设置 } // 进一步限制外部可读但不可写 public new int LegCount { get; private set; } }这种设计确保了基类对关键属性的控制权同时给予派生类适当的灵活性。4. 特殊场景与最佳实践4.1 只读属性的多种实现方式有时候我们需要创建真正的只读属性不仅在类外部只读在内部也是只读。有几种实现方式构造函数初始化public class ImmutablePoint { public int X { get; } public int Y { get; } public ImmutablePoint(int x, int y) { X x; Y y; } }init访问器C# 9.0public class Person { public string Name { get; init; } } // 使用 var person new Person { Name Alice }; // person.Name Bob; // 编译错误私有set 只读字段public class Logger { private readonly string _logFile; public string LogFile _logFile; public Logger(string logFile) { _logFile logFile; } }4.2 属性与字段的选择什么时候该用自动属性什么时候该用字段属性的组合根据我的经验使用自动属性当不需要额外逻辑只是简单封装字段时public string Name { get; set; } // 简洁明了使用显式字段当需要以下情况时添加验证逻辑实现延迟加载触发事件需要readonly字段private string _name; public string Name { get _name; set { if(string.IsNullOrWhiteSpace(value)) throw new ArgumentException(Name cannot be empty); _name value; OnNameChanged(); } }4.3 性能考量自动属性在大多数情况下性能足够好因为JIT编译器会进行优化。但在高性能场景下直接访问字段可能更快。这时候可以考虑public class HighPerfClass { private int _counter; // 外部访问用属性 public int Counter _counter; // 内部频繁访问用字段 internal void Increment() _counter; }5. 实际项目中的封装策略5.1 领域模型中的封装在领域驱动设计(DDD)中封装尤为重要。以订单系统为例public class Order { private readonly ListOrderItem _items new(); // 外部可以读取订单项但不能直接修改 public IReadOnlyCollectionOrderItem Items _items.AsReadOnly(); public decimal Total _items.Sum(i i.Subtotal); // 只有特定方法可以修改订单状态 public OrderStatus Status { get; private set; } public void AddItem(Product product, int quantity) { // 业务逻辑验证... _items.Add(new OrderItem(product, quantity)); } public void Cancel() { if(Status ! OrderStatus.Pending) throw new InvalidOperationException(只能取消待处理的订单); Status OrderStatus.Cancelled; } }这种封装确保了订单项不会被外部意外修改订单状态变更遵循业务规则总金额始终保持一致5.2 API设计中的封装设计公共API时良好的封装可以避免破坏性变更。比如public class ApiClient { // 内部实现可能变化但接口不变 private HttpClient _client; // 外部只能读取超时时间修改需要通过特定方法 public TimeSpan Timeout { get; private set; } public void SetTimeout(TimeSpan timeout) { if(timeout.TotalMilliseconds 0) throw new ArgumentException(超时时间必须大于0); Timeout timeout; _client.Timeout timeout; } }这样设计后即使未来需要修改HttpClient的实现方式也不会影响API的使用者。5.3 测试中的封装考虑适当的封装也能让单元测试更容易。比如public class PriceCalculator { // 内部算法可能很复杂但通过protected virtual允许测试派生类 protected virtual decimal CalculateDiscount(Order order) { // 复杂的折扣计算逻辑... } public decimal GetFinalPrice(Order order) { return order.Total - CalculateDiscount(order); } } // 测试专用派生类 public class TestablePriceCalculator : PriceCalculator { protected override decimal CalculateDiscount(Order order) { return 10m; // 固定折扣方便测试 } }这种设计既保持了生产代码的封装性又为测试提供了必要的灵活性。