C# 学习笔记 14:封装的艺术——属性(Properties)
开篇:皮皮的“裸奔”数据
皮皮(Pipi)最近发现一件很离谱的事: 他写的游戏里,外挂横行,怪物一个比一个离谱。
排查半天后,他终于找到了罪魁祸首——Monster 类是这么写的:
publicclassMonster{publicint Hp; // 任何人都能随便改!}
于是,黑客(或者写 Bug 的皮皮自己)出现了:
Monster boss = new Monster();boss.Hp = -1000; // 血量成了负数
游戏逻辑里只判断了:
if (hp == 0) 死亡
结果呢? 负血不死,锁血怪物横行。
瓜瓜(Guagua)看完代码,当场拍桌:
“皮皮!你的数据在裸奔! 谁都能上来踢一脚。 你得给它穿上盔甲,装个安检门!”
这,就是我们今天要学的主角:封装(Encapsulation)。
第一关:什么是封装?
封装的核心思想只有一句话:
隐藏内部细节,只暴露必要的接口。
在 C# 里,它通常体现在这两个角色上:
字段(Field)👉 存数据的地方 👉 应该是 private就像你钱包里的现金,只能你自己碰。
属性(Property)👉 对外提供访问的窗口 👉 应该是 public就像银行柜台,别人想存钱、取钱,必须走流程,不能直接伸手进金库。
一句话记忆版:
字段负责“藏”,属性负责“管”。

第二关:给数据装上安检门(完整属性)
我们先把 Hp 藏起来,再安排一个“保安”统一管理。
私有字段(Backing Field)
这是数据的真身,一般命名时加下划线:
privateint _hp;
公共属性(Property)
这是对外的“窗口”,包含两部分:
set :别人修改数据时执行 👉 重点:这里可以写逻辑!
publicclassMonster{// 真正的血量,藏在保险箱里privateint _hp;// 对外开放的窗口publicint Hp {get {return _hp; // 想看?可以 }set {// 👮♂️ 安检开始// value 是 C# 关键字,表示“外面传进来的新值”if (value < 0) { Console.WriteLine("警告:血量不能为负,已修正为 0"); _hp = 0; }elseif (value > 1000) { _hp = 1000; // 锁上限 }else { _hp = value; // 合法,放行 } } }}
现在,不管谁想改 Hp,都必须经过这道安检门。
第三关:懒人神器——自动属性
“我只是想存个值啊!”
皮皮很快又崩溃了。
怪物类里除了 Hp,还有:
这些字段暂时根本不需要校验逻辑,却要写一大堆样板代码:
publicclassMonster{privatestring _name;publicstring Name {get { return _name; }set { _name = value; } }}
皮皮(抱头):
“这也太多废话了吧? 为了封装,我手都要写断了!”
瓜瓜(微微一笑):
“放心,C# 编译器早就替你考虑到了。”
自动属性(Auto-Property)
如果你在 get / set 里不写任何逻辑,那这些代码可以统统省掉:
publicstring Name { get; set; }
这行代码 = 上面那一大坨
幕后发生了什么?
你得到的是:👉 简洁的写法👉 完整的封装
第四关:灵魂拷问——那我为什么不用字段?
皮皮突然发现一个“捷径”:
publicstring Name;
看起来更简单,用起来也一样。
“既然现在没逻辑,我为什么不直接用字段?”
瓜瓜立刻严肃起来:
“这是新手最容易踩的两个大坑。”
坑 1:接口不允许字段(Interface Contract)
还记得我们前面学的 接口 (Interface) 吗?它是 “能力的契约”。
C# 规定:接口只能定义“行为”(方法、属性),不能定义“存储”(字段)。
interfaceIUser{// ❌ 编译错误// int Id;// ✅ 正确int Id { get; set; }}
如果你希望类未来能被接口规范化管理,公开数据就必须是属性。
坑 2:后悔药无效(Binary Compatibility)
这是库作者必踩的天坑。
假设你发布了一个 DLL:
publicint Age;
后来你想在修改 Age 时加日志、加通知,于是改成:
publicint Age { get; set; }
结果:灾难发生。
所有引用你 DLL 的老程序必须重新编译否则直接运行崩溃
瓜瓜总结:
属性就像预留的插座。现在只是通电, 未来想加安检、加逻辑、加监控,都不用拆墙。
结论只有一句:
公开数据,永远用属性,不用字段。
第五关:只读属性 & 计算属性
只读属性(private set)
有些数据:
publicclassHero{publicint Level { get; privateset; }publicvoidLevelUp() { Level++; }}
Hero h = new Hero();Console.WriteLine(h.Level); // ✅ 可读// h.Level = 99; // ❌ 外部不可写
这是游戏、业务系统里最常见的设计。
计算属性(Computed Property)
属性不一定要存数据, 它也可以只是一个实时计算的结果:
publicclassMonster{publicint Hp { get; set; }publicbool IsDead {get { return Hp <= 0; } }// 简写:// public bool IsDead => Hp <= 0;}
总结
今天我们掌握了:
- 自动属性
{ get; set; } :不是偷懒,而是标准写法
课后思考
如果你在 get 里写了一个非常耗时的计算, 而别人频繁访问 obj.Value,会发生什么?
👉 提示:属性应当是轻量级的, 重逻辑请用 方法(Method)。
下期预告
皮皮发现一个新问题:
“不管我造多少只怪物, 想统计‘当前一共有多少怪物’都很麻烦。”
难道要用全局变量?
下一期:静态成员(Static)属于“模具本身”,而不是“每个对象”的数据。