
1. 项目概述为什么这三个关键字是Unity开发者的基本功在Unity项目里写C#脚本无论是刚入门的新手还是有一定经验的开发者都绕不开abstract、virtual和override这三个关键字。我见过太多项目代码里充斥着混乱的继承关系该用抽象方法的地方用了虚方法该重写的地方没调用base导致功能缺失或者干脆因为不理解而避免使用继承最后写出一堆重复且难以维护的代码。这不仅仅是语法问题它直接关系到你代码的架构是否清晰、功能扩展是否灵活、以及团队协作是否顺畅。简单来说这三个关键字共同构成了C#面向对象编程中“多态”这一核心特性的基石。在Unity的语境下多态意味着你可以定义一个通用的“敌人”基类然后让“骷髅兵”、“兽人”、“巨龙”等具体类型各自实现不同的移动或攻击逻辑同时它们都能被同一个“敌人管理器”以统一的方式处理。abstract用来画蓝图、定规矩virtual提供一份默认的、可被修改的实施方案override则是子类对父类方案的个性化“改造”。搞不清它们你的游戏对象就像一群不听话的士兵各有各的步调指挥官你的游戏逻辑根本无法有效调度。这篇文章的目的就是帮你把这三个经常让人“傻傻分不清”的概念彻底掰开揉碎。我不会只停留在枯燥的教科书定义上而是会结合Unity开发中最常见的场景——比如设计技能系统、管理UI控件、处理游戏实体状态——用大量你能立刻在项目中复用的代码示例讲清楚每一个关键字应该在什么时机、以什么方式使用。同时我也会分享一些从实际项目踩坑中总结出来的经验比如如何避免过度设计、何时该用组合代替继承这些都是在官方文档里不会明说但却能极大提升你代码质量的实战技巧。2. 核心概念深度解析从设计意图理解差异很多教程一上来就对比语法但在我看来先理解每个关键字背后的“设计意图”才能从根本上区分它们。你可以把它们想象成建筑行业的不同指令。2.1 Abstract抽象的强制性的蓝图与契约abstract关键字的核心意图是强制和规范。当一个类被声明为抽象类abstract class或者一个方法被声明为抽象方法abstract method时它就是在对继承者说“我这儿有个重要的功能概念但具体怎么实现我不知道也不关心。你必须Must按照我给的签名方法名、参数和返回类型来提供一个具体的实现。”关键特性与使用场景无法实例化你不能用new关键字直接创建一个抽象类的对象。这很好理解一个只有蓝图没有具体施工方案的建筑当然没法直接入住。在Unity中这意味着你不会在场景中直接挂载一个抽象MonoBehaviour脚本。可包含抽象成员抽象类可以拥有抽象方法、属性、事件或索引器。这些成员只有声明没有实现体没有{}花括号包裹的代码。可包含具体实现与接口不同抽象类可以同时拥有已经实现的具体方法、字段和属性。这让你能在提供一部分通用功能的同时要求子类完成特定部分。Unity中的典型应用假设我们在做一个RPG游戏需要设计多种类型的“技能”。所有技能都有一些共同行为比如需要消耗法力、有冷却时间但释放效果千差万别。// 抽象类定义技能的通用框架和必须实现的契约 public abstract class SkillBase : MonoBehaviour { public float manaCost; public float cooldownTime; private float currentCooldown; // 具体方法所有技能共享的冷却逻辑 public bool IsReady() { return currentCooldown 0f; } void Update() { if (currentCooldown 0f) currentCooldown - Time.deltaTime; } // 抽象方法子类必须实现具体的释放逻辑 public abstract void Cast(GameObject target); // 受保护的具体方法提供释放技能的通用流程 protected void TriggerCast() { if (!IsReady()) return; currentCooldown cooldownTime; // 这里可以触发全局音效、动画事件等 Debug.Log(Skill triggered with cooldown: cooldownTime); } }在这个例子中SkillBase提供了一个完整的技能框架冷却计算、就绪判断但将最核心的Cast行为定义为抽象的。这意味着任何继承自SkillBase的类必须自己写一个Cast方法否则代码编译都无法通过。这就是abstract的强制性。实操心得当你发现多个类有大量共享代码但其中某个关键操作的行为又完全不同时抽象类就是最佳选择。它避免了代码重复同时通过强制实现保证了设计的一致性。在Unity中常用于MonoBehaviour的基类设计如BaseEnemy、BaseUIWindow等。2.2 Virtual虚拟的可选的默认实现与扩展点virtual关键字的核心意图是提供可选和可扩展的默认行为。它像是在说“我这儿有一个现成的、能工作的方案。你可以直接用它但如果你有更好的、更特定的想法也欢迎你来修改Override它。”关键特性与使用场景必须包含实现虚方法必须有方法体即它是一段可以执行的完整代码。允许被重写子类可以使用override关键字来提供该方法的新版本。非强制性子类可以选择不重写虚方法此时它将直接继承并使用父类的默认实现。Unity中的典型应用继续上面的技能系统假设我们有一类“引导类技能”如持续治疗、蓄力攻击它们除了有通用的释放逻辑在“持续引导”这个行为上既有共性比如都需要每帧检查引导是否被打断又有个性治疗是回血蓄力是加伤害。public class ChannelingSkill : SkillBase { public float channelDuration; protected bool isChanneling false; // 重写基类的抽象方法提供具体释放逻辑 public override void Cast(GameObject target) { if (!IsReady()) return; TriggerCast(); StartChanneling(target); } // 虚方法提供一个默认的引导开始逻辑子类可以扩展 protected virtual void StartChanneling(GameObject target) { isChanneling true; Debug.Log(开始引导技能持续 channelDuration 秒); // 默认行为在指定时间后自动结束引导 Invoke(nameof(EndChanneling), channelDuration); } // 虚方法提供一个默认的引导结束逻辑 protected virtual void EndChanneling() { isChanneling false; Debug.Log(引导结束); } void Update() { base.Update(); // 调用基类的Update以更新冷却 if (isChanneling) { // 默认的每帧引导逻辑例如检查施法者是否移动导致打断 OnChannelingUpdate(); } } // 虚方法引导期间每帧调用子类可重写以添加效果 protected virtual void OnChannelingUpdate() { // 默认可能什么都不做或只打印日志 // Debug.Log(Channeling...); } }这里StartChanneling、EndChanneling和OnChannelingUpdate都被声明为virtual。这意味着如果我创建一个HealingChannelSkill治疗引导技能我可以选择只重写OnChannelingUpdate在其中实现每帧为目标回复生命值而开始和结束引导的通用逻辑如设置标志、调用Invoke则直接复用父类的。注意事项过度使用virtual会让类的行为变得难以预测因为任何子类都可能改变其默认行为。一个好的经验法则是只有当你有确切的理由认为这个方法的实现很可能需要在子类中被改变时才将其设为虚方法。对于那些应该保持稳定、作为类核心契约的方法则不应设为virtual。2.3 Override重写执行子类的个性化方案override关键字本身没有独立的“设计意图”它是virtual和abstract的“响应”。它的作用就是明确告诉编译器“父类提供的这个方案无论是抽象的蓝图还是虚拟的默认方案我要在这里用自己的版本替换掉。”关键规则只能重写可重写成员即用virtual、abstract或override重写链中的上一级声明的方法或属性。签名必须完全一致方法名、返回类型、参数列表参数类型、数量和顺序必须与父类中的声明完全相同。访问修饰符不能更严格重写方法的访问级别如public、protected不能低于被重写的方法。通常保持一致。与new关键字的区别重要这是另一个容易混淆的点。override实现了真正的“多态”而new只是“隐藏”。override当你通过父类类型引用调用一个被重写的方法时实际执行的是子类的版本。这是多态的核心。new子类定义了一个与父类同名的方法但并未重写它。当通过父类类型引用调用时仍然执行父类的方法只有通过子类类型引用调用时才执行子类的这个新方法。这通常意味着设计上有问题应尽量避免。public class HealingChannelSkill : ChannelingSkill { public float healPerSecond; // 正确使用 override 重写虚方法 protected override void OnChannelingUpdate() { base.OnChannelingUpdate(); // 可以选择调用基类逻辑 // 添加治疗逻辑 if (currentTarget ! null) { Health health currentTarget.GetComponentHealth(); if (health ! null) { health.Restore(healPerSecond * Time.deltaTime); } } } // 危险使用 new 隐藏基类方法通常不建议 public new void SomeMethod() { // 这不会重写基类的虚方法只会隐藏它 } }调用基类实现base.关键字在重写方法中你可以使用base.MethodName()来调用父类该方法的原始实现。这是一个非常重要的技巧对于abstract方法的实现没有base可调因为抽象方法没有实现体。对于virtual方法的重写你可以选择是否调用base。如果你希望扩展父类的行为在父类逻辑前后添加新逻辑就应该调用base。如果你希望完全替换父类的行为则可以不调用。在之前Unity论坛的示例中提问者最初想避免在override Consume()中调用base.Consume()后来通过“模板方法模式”解决了——将核心逻辑_consumed true放在基类的Consume()具体方法中并调用一个抽象的ConsumeBehaviour()方法。这样子类只需重写行为部分无需关心状态设置。3. 在Unity中的实战应用与模式理解了基本概念后我们来看看在Unity游戏开发中如何运用这些概念来设计更优雅、更易维护的系统。Unity的MonoBehaviour生命周期和组件模型与面向对象继承结合能产生强大的效果。3.1 案例一构建灵活的游戏实体Enemy/Item系统这是最经典的应用。假设我们有一类“可交互物体”Interactable包括宝箱、NPC、门、可拾取物品等。它们都有“被交互”这个动作但效果不同。方案A使用抽象类public abstract class Interactable : MonoBehaviour { public string interactionPrompt 按E交互; // 所有可交互物都有提示文字 public abstract void Interact(Player player); // 但交互行为必须由子类定义 // 一个具体方法处理通用的高亮显示 public void ShowHighlight(bool isOn) { // ... 高亮渲染逻辑 } } public class TreasureChest : Interactable { public Item[] loot; private bool isOpened false; public override void Interact(Player player) { if (isOpened) { Debug.Log(宝箱已经是空的); return; } foreach (var item in loot) { player.inventory.Add(item); } isOpened true; // 可以播放开箱动画、音效 GetComponentAnimator().SetTrigger(Open); } } public class Door : Interactable { public bool isLocked false; public Item requiredKey; public override void Interact(Player player) { if (isLocked) { if (player.inventory.Contains(requiredKey)) { Unlock(); Open(); } else { Debug.Log(门被锁住了需要钥匙 requiredKey.itemName); } } else { Open(); } } private void Unlock() { /* ... */ } private void Open() { /* ... */ } }在这个设计中Player脚本只需要持有Interactable类型的引用调用Interact(player)即可无需关心具体是宝箱还是门。系统要新增一个“可阅读的告示牌”只需新建一个ReadableSign类继承Interactable并实现Interact方法即可玩家交互代码一行都不用改。方案B使用虚方法提供默认交互如果大多数可交互物体只是播放一个通用动画和音效只有少数需要特殊逻辑那么用虚方法更合适。public class Interactable : MonoBehaviour { public string interactionPrompt 按E交互; public AudioClip interactionSound; public string interactionAnimationTrigger; // 虚方法提供默认的交互行为播放音效和动画 public virtual void Interact(Player player) { if (interactionSound ! null) AudioSource.PlayClipAtPoint(interactionSound, transform.position); if (!string.IsNullOrEmpty(interactionAnimationTrigger)) { Animator anim GetComponentAnimator(); if (anim ! null) anim.SetTrigger(interactionAnimationTrigger); } Debug.Log(与 gameObject.name 交互了。); } } // 大多数普通物品直接使用默认交互 public class GenericItem : Interactable { // 不需要重写Interact直接使用父类的播放音效/动画的逻辑 } // 只有需要特殊逻辑的才重写 public class ComplexPuzzlePiece : Interactable { public PuzzleManager manager; public override void Interact(Player player) { // 先执行默认的交互反馈音效、动画 base.Interact(player); // 再执行特殊的解谜逻辑 manager.PieceActivated(this); } }模式选择心得选择抽象类还是虚方法关键在于子类行为的“差异性”和“强制性”。如果所有子类行为都必须不同且无合理默认值如各种技能的释放用abstract。如果大多数子类行为相似只有少数需要特例或者你希望提供一个“足够好”的默认实现那么用virtual。在Unity中由于MonoBehaviour本身是类且我们经常通过添加组件来组合功能因此使用虚方法的情况可能更频繁一些因为它提供了更大的灵活性。3.2 案例二实现可扩展的UI控件UI系统是另一个继承大显身手的地方。例如我们需要一个通用的“弹窗”系统。// 抽象基类定义弹窗的生命周期 public abstract class UIPopupBase : MonoBehaviour { public CanvasGroup canvasGroup; public float fadeDuration 0.3f; // 抽象方法子类必须定义弹窗显示时的具体内容设置如填充文本、图片 protected abstract void OnPopupShow(); // 抽象方法子类必须定义弹窗关闭时的清理工作 protected abstract void OnPopupClose(); // 具体方法统一的显示流程淡入 public void Show() { gameObject.SetActive(true); OnPopupShow(); // 调用子类具体设置 StartCoroutine(FadeIn()); } // 具体方法统一的关闭流程淡出 public void Close() { StartCoroutine(FadeOut()); } private IEnumerator FadeIn() { // ... 淡入动画逻辑 yield return null; } private IEnumerator FadeOut() { // ... 淡出动画逻辑 yield return null; OnPopupClose(); // 调用子类清理 gameObject.SetActive(false); } // 虚方法提供一个可被重写的确认按钮点击处理 public virtual void OnConfirmButtonClicked() { Debug.Log(默认确认操作); Close(); } // 虚方法提供一个可被重写的取消按钮点击处理 public virtual void OnCancelButtonClicked() { Debug.Log(默认取消操作); Close(); } } // 具体实现消息提示弹窗 public class MessagePopup : UIPopupBase { public Text messageText; private string _messageToShow; public void SetMessage(string msg) { _messageToShow msg; } protected override void OnPopupShow() { messageText.text _messageToShow; } protected override void OnPopupClose() { _messageToShow null; // 清理引用 } // 重写确认按钮行为可能不需要关闭 public override void OnConfirmButtonClicked() { Debug.Log(消息已读); // 不调用 base.Close()因为可能只是确认阅读弹窗不关闭 // 或者执行其他逻辑后再关闭 SendAnalyticsEvent(MessageConfirmed); base.OnConfirmButtonClicked(); // 最终还是会调用基类的Close } } // 具体实现物品获取弹窗 public class ItemObtainedPopup : UIPopupBase { public Image itemIcon; public Text itemNameText; private Item _obtainedItem; public void SetItem(Item item) { _obtainedItem item; } protected override void OnPopupShow() { itemIcon.sprite _obtainedItem.icon; itemNameText.text _obtainedItem.itemName; } protected override void OnPopupClose() { _obtainedItem null; } }这个设计的好处在于所有弹窗的显示/关闭动画、激活状态管理都被封装在基类中。当你需要一个新的弹窗类型比如“系统设置弹窗”你只需要继承UIPopupBase实现OnPopupShow和OnPopupClose来管理自己的UI元素按钮的默认行为都已经有了。UI管理器只需要调用Show()和Close()完全不用关心具体是哪种弹窗。3.3 案例三管理复杂的状态与行为State Pattern在角色状态机如玩家状态闲置、奔跑、跳跃、攻击中结合abstract和virtual可以写出非常清晰的代码。// 抽象状态基类 public abstract class PlayerState { protected PlayerController player; public PlayerState(PlayerController player) { this.player player; } // 抽象方法进入状态时必须执行的操作 public abstract void Enter(); // 抽象方法状态每帧更新逻辑 public abstract void Update(); // 抽象方法离开状态时的清理操作 public abstract void Exit(); // 虚方法处理输入子类可以选择重写 public virtual void HandleInput(InputData input) { // 默认可能处理一些所有状态都有的通用输入比如打开菜单 if (input.menuPressed) player.OpenMenu(); } // 虚方法处理物理更新子类可以选择重写 public virtual void FixedUpdate() { // 默认空实现 } } // 具体状态闲置状态 public class IdleState : PlayerState { public IdleState(PlayerController player) : base(player) { } public override void Enter() { player.animator.Play(Idle); player.ResetVelocityX(); // 确保停止水平移动 } public override void Update() { // 检查是否应该切换到其他状态 if (player.input.horizontal ! 0) { player.ChangeState(new RunningState(player)); } else if (player.input.jumpPressed player.IsGrounded()) { player.ChangeState(new JumpingState(player)); } } public override void Exit() { // 闲置状态离开时通常不需要特殊清理 } // 重写输入处理添加状态特定输入 public override void HandleInput(InputData input) { base.HandleInput(input); // 先处理通用输入如打开菜单 if (input.attackPressed) { player.ChangeState(new AttackingState(player)); } } } // 具体状态奔跑状态 public class RunningState : PlayerState { public RunningState(PlayerController player) : base(player) { } public override void Enter() { player.animator.Play(Run); } public override void Update() { // 应用水平移动 player.ApplyHorizontalMovement(player.input.horizontal); // 状态转移判断 if (player.input.horizontal 0) { player.ChangeState(new IdleState(player)); } else if (player.input.jumpPressed player.IsGrounded()) { player.ChangeState(new JumpingState(player)); } else if (!player.IsGrounded()) { player.ChangeState(new FallingState(player)); } } public override void Exit() { } // 重写FixedUpdate处理物理 public override void FixedUpdate() { player.ApplyGroundFriction(); } }PlayerController类持有一个当前状态的引用在Update中调用currentState.Update()和currentState.HandleInput()。这种设计将每个状态的行为封装在独立的类中避免了在PlayerController中使用巨大的switch-case或if-else链使得增加新状态如“滑铲”、“攀爬”变得非常容易也符合“开闭原则”。4. 高级话题、常见陷阱与性能考量掌握了基础用法和常见模式后我们还需要深入一些高级主题和实践中容易踩的坑。4.1 构造函数、Awake()、Start()的调用顺序在Unity中当使用继承的MonoBehaviour时生命周期方法的调用顺序至关重要。构造函数在脚本被加载时调用编辑模式或进入运行模式时早于任何Unity生命周期方法。不建议在MonoBehaviour的构造函数中执行依赖Unity引擎如访问GameObject、其他组件的逻辑因为此时这些环境可能尚未准备好。Awake()当脚本实例被创建时调用无论GameObject是否激活。调用顺序是从父类到子类。即先执行基类的Awake()再执行子类的Awake()。Start()在第一次Update()之前仅当脚本启用时调用。调用顺序也是从父类到子类。public class BaseClass : MonoBehaviour { protected int value; void Awake() { Debug.Log(BaseClass.Awake); value 10; // 假设这里进行了一些重要的初始化 } void Start() { Debug.Log(BaseClass.Start, value value); } } public class DerivedClass : BaseClass { void Awake() { // 此时基类的Awake已经执行完毕value已被初始化为10 Debug.Log(DerivedClass.Awake, base.value base.value); value 20; // 可以修改基类初始化的值 } void Start() { // 此时基类的Start也已执行 Debug.Log(DerivedClass.Start, value value); // 输出 20 } }重要陷阱如果你在子类的Awake或Start中依赖基类在这些方法中初始化的某些资源或状态这是安全的因为基类的方法会先执行。但反过来则不行。同时要小心在基类的Awake中调用虚方法因为此时子类的Awake尚未执行子类可能尚未完成自身的初始化。4.2 密封类sealed与密封方法sealed override有时你希望阻止一个类被进一步继承或者阻止一个虚方法在更下层的子类中被重写。这时就需要sealed关键字。密封类public sealed class FinalClass : BaseClass { ... }。FinalClass不能再被其他类继承。这通常用于表示一个概念的最终实现或者出于安全/性能考虑防止恶意子类化。密封方法在重写一个虚方法时你可以使用sealed override来声明这是该方法的最终版本派生类不能再重写它。public class GrandParent { public virtual void Method() { } } public class Parent : GrandParent { public sealed override void Method() { } // 这是Method的最终实现 } public class Child : Parent { // 编译错误不能重写被密封的方法 // public override void Method() { } }在Unity中如果你设计了一个非常稳定、核心的类比如一个高度优化的特定渲染器组件并且确定其行为不应被修改可以考虑将其密封。这能给编译器提供更多优化机会。4.3 性能影响与最佳实践虚方法调用Virtual Call的开销调用一个虚方法或抽象方法比调用一个非虚non-virtual的实例方法稍微慢一点。因为运行时需要查找对象的实际类型并跳转到正确的方法实现vtable查找。对于绝大多数游戏逻辑来说这个开销微乎其微不应成为你拒绝使用多态的理由。代码的清晰度、可维护性和架构的优雅性带来的好处远大于这点性能损失。只有在性能分析Profiling明确显示某处虚方法调用成为热点hotspot时才需要考虑优化例如通过内联、缓存或设计模式调整。避免过深的继承层次“金字塔”式的深层继承如GameEntity - MovingEntity - Character - Enemy - FlyingEnemy - Dragon会使代码难以理解和维护。建议继承层次不要超过3-4层。过深的继承会导致脆弱的基类问题修改基类可能意外破坏所有派生类。理解困难需要追溯很长的链才能理解一个类的全部行为。不灵活的代码例如Dragon继承自FlyingEnemy但如果后来想让它同时具备Boss的特性单继承就受限了。优先使用组合而非继承Composition over Inheritance这是面向对象设计的一条黄金法则。不要为了复用代码而滥用继承。问问自己“B 是一个 A 吗”“Is-a”关系。如果答案是模糊的或者只是为了复用A的几个方法那么组合即B类内部持有一个A类的实例通常是更好的选择。回顾本文开头引用的Unity论坛讨论用户GroZZleR提出的使用ListConsumableEffect的方案就是组合模式的经典应用。它比创建HealthPotion、ManaPotion、RejuvenationPotion等一堆继承类要灵活得多。为继承而设计否则就禁止它如果你创建一个类时并没有打算让它被继承那么应该考虑将其标记为sealed密封类或者至少不要将任何方法设为virtual。这可以防止其他开发者误用你的类也可以让编译器进行更多优化。4.4 常见编译错误与排查“不包含……的定义并且找不到可接受类型的……扩展方法”检查方法签名名称、参数类型、返回类型是否与父类中的abstract或virtual方法完全一致。大小写、参数顺序都不能错。“无法重写继承成员……因为它没有 virtual, abstract 或 override 修饰符”你试图用override重写一个在父类中既不是virtual也不是abstract的普通方法。如果你想“隐藏”它可以使用new关键字但请三思或者修改父类设计将方法改为virtual。“抽象类中的抽象成员……必须被实现”继承自抽象类的非抽象具体类没有实现基类中所有的abstract方法。你必须实现所有抽象成员或者将这个类也声明为abstract。“静态成员不能被标记为 override、virtual 或 abstract”static方法属于类本身而非实例因此不支持多态。你需要重新考虑设计或许应该使用实例方法。5. 总结与个人实践建议经过上面长篇累牍的讨论我们可以清晰地看到abstract、virtual和override绝不是三个孤立的语法糖而是构建灵活、可扩展C#代码尤其是在Unity中的一套组合工具。abstract用于搭建必须完成的框架virtual用于提供可定制的默认选项而override则是实现这种定制的具体手段。在我自己的项目实践中我遵循这样几个原则第一明确继承的“Is-a”关系。在决定让类B继承类A之前我会反复确认“B是一个A”这句话是否在业务逻辑上成立。例如“法师是一个角色”成立“火球术是一个技能”也成立。但“背包系统是一个角色”就不成立这时就应该用组合角色拥有一个背包组件而不是继承。第二抽象类用于定义“模板”接口用于定义“能力”。当一系列类有共同的代码骨架和状态字段但部分行为必须由子类决定时用抽象类。当只是需要一组不相关的类承诺实现某些方法如IDamageable可受伤、IInteractable可交互而它们之间没有共享代码时用接口。在Unity中由于C#不支持多继承但支持多接口实现接口的使用非常广泛。第三谨慎使用virtual优先提供“完整”的默认实现。不要随意地把方法标记为virtual。只有当你有明确的预见性知道这个方法在未来很可能需要被以不同方式实现时才这样做。一个设计良好的虚方法其默认实现应该是“完整”且有用的子类重写它通常是为了“增强”或“微调”而非“推翻重来”。第四在重写方法时想清楚是否要调用base。这是一个简单的规则但却能避免很多bug如果你重写的方法是为了扩展基类行为比如在播放音效前后做点别的事那么通常应该调用base.Method()。如果你是为了完全替换基类行为比如实现一种全新的移动算法那么就不调用。如果不确定调用base通常是更安全的选择除非你有充分理由不这样做。最后记住所有这些技术都是为了服务于更好的代码设计。不要为了用而用。当你发现代码中开始出现重复或者添加新功能时需要修改很多地方时就是考虑引入抽象、利用多态的好时机。从一个小模块开始尝试比如从设计一套简单的技能或状态开始慢慢体会它们带来的结构清晰和扩展便利你自然会越来越得心应手。