news 2026/7/21 1:41:36

Unity C#面向对象设计:abstract、virtual、override核心策略与实战应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity C#面向对象设计:abstract、virtual、override核心策略与实战应用

1. 项目概述:从“能用”到“会设计”的关键一步

在Unity开发中,尤其是当项目规模从Demo走向产品,从个人开发转向团队协作时,代码的结构和可维护性就成了决定项目生死的关键。很多开发者,包括我自己在早期,都经历过这样的阶段:功能实现了,但代码像一团乱麻,新增一个特性要改十几个地方,牵一发而动全身。问题的根源,往往在于对面向对象编程(OOP)中几个核心概念——abstractvirtualoverride——的理解停留在表面,没有形成一套清晰的“选择策略”。

这三个关键字,是C#赋予我们构建灵活、可扩展代码架构的利器。它们不是语法糖,而是设计思想的具象化。abstract(抽象)定义了“必须做什么”的契约,virtual(虚方法)提供了“默认怎么做”的模板,而override(重写)则是实现“具体怎么做”的自由。理解它们,意味着你从“写代码实现功能”的码农,向“设计系统架构”的工程师迈进了一大步。

这篇文章,我将结合在Unity中开发游戏系统、UI框架和工具链的实战经验,抛开教科书式的定义,直接深入到应用场景和选择逻辑中。我们会探讨:在什么情况下应该定义一个抽象类而不是接口?什么时候用虚方法比用抽象方法更合适?重写时有哪些坑必须避开?最终,你会得到一套清晰的决策流程图和实战检查清单,让你在下次设计新系统时,能自信地做出最合适的选择。

2. 核心概念再认识:超越语法定义

在深入实战前,我们需要统一认知,避免因术语理解偏差导致的误用。这里我不会重复MSDN上的标准定义,而是用Unity开发者更熟悉的方式来解读。

2.1 Abstract:搭建不可动摇的规则框架

你可以把abstract修饰的类或方法,想象成游戏策划案中的核心规则。比如,策划案规定:“游戏里必须有一种叫‘敌人’的东西,每个敌人都必须有一个‘被攻击时会受伤’的行为。” 这个规定是铁律,不容置疑,但策划案本身不具体说“哥布林”是怎么受伤的,“巨龙”又是怎么受伤的。

  • 抽象类(Abstract Class): 就是一个“半成品”的蓝图。它定义了这类对象必须有的部分(字段、属性、方法),尤其是那些必须存在但实现方式未知的行为(抽象方法)。你不能直接new一个抽象类,就像你不能把“敌人策划案”直接扔进游戏里当怪物用。它的存在,是为了被继承,为了给一系列具体的类(如Goblin,Dragon)建立一个统一的、强制性的起点。
  • 抽象方法(Abstract Method): 是写在蓝图上的“待办事项”,只有方法签名(名称、参数、返回类型),没有方法体({}里的实现代码)。继承了这个蓝图的子类必须用自己的方式完成这个“待办事项”(即重写该方法)。这是一种最强的“契约”。

在Unity中的典型误用:试图将一个普通的MonoBehaviour脚本挂到抽象类上。Unity编辑器会报错,因为MonoBehaviour要求组件可以被实例化。解决方案通常是让抽象类也继承MonoBehaviour,但只为它添加抽象方法,而具体的游戏对象挂载的是继承自该抽象类的具体子类脚本。

2.2 Virtual:提供可修改的默认方案

如果说abstract是“必须做,但你自己想办法”,那么virtual就是“通常这么做,但你可以改”。它提供了默认的、可工作的实现。子类可以完全沿用这个默认方案,也可以根据需要进行部分或全部的修改。

  • 虚方法(Virtual Method): 在父类中有一个完整的实现。子类可以选择:
    1. 直接继承:不重写,完全使用父类的逻辑。
    2. 重写(Override):用自己全新的逻辑完全替换父类的实现。
    3. 扩展(通过base关键字调用):先执行父类的逻辑,再添加自己的额外逻辑。这是非常强大且常用的模式。

一个关键区别:抽象方法强制子类提供实现;虚方法允许子类修改实现。这是选择二者的最根本依据。

2.3 Override:履行契约或进行定制

override是子类对父类中abstractvirtual方法的响应动作。它是实现多态(Polymorphism)的基石。多态允许我们写这样的代码:Enemy currentEnemy = GetCurrentEnemy(); currentEnemy.TakeDamage(10);而不用关心currentEnemy具体是哥布林还是巨龙,它们会各自执行自己被重写的TakeDamage方法。

新手常踩的坑

  1. 签名不一致:重写方法时,方法名、参数类型和数量、返回类型必须与父类方法完全一致,否则编译器会认为这是一个新的方法,而非重写。
  2. 可访问性降低:重写方法不能比父类原方法的访问权限更严格(例如,父类是protected virtual,子类不能重写为private)。
  3. 忘记调用base.Method():当父类的虚方法包含一些重要的初始化或清理逻辑时,在子类重写中忘记通过base关键字调用父类方法,可能导致难以察觉的Bug。例如,一个虚方法OnEnable()里注册了事件,子类重写时若没调用base.OnEnable(),事件就注册不上。

3. 实战应用场景深度解析

理论说再多,不如看实战。下面我们通过几个在Unity开发中高频出现的场景,来具体感受这三个关键字如何塑造我们的代码。

3.1 场景一:构建可扩展的游戏角色状态机

状态机(State Machine)是游戏角色行为控制的灵魂。一个典型的状态包括:进入状态(Enter)、状态更新(Update)、退出状态(Exit)。我们用抽象类和虚方法来设计一个优雅的解决方案。

// 1. 定义抽象状态基类 public abstract class CharacterStateBase { // 抽象方法:进入状态时必须执行的操作,子类必须实现 public abstract void OnStateEnter(); // 虚方法:状态每帧更新,子类可以重写以定制逻辑 public virtual void OnStateUpdate() { // 这里可以放一些所有状态都可能需要的通用更新逻辑 // 例如:检查是否处于无敌时间、更新状态计时器等 // Debug.Log(“State is updating...”); } // 虚方法:退出状态时必须执行的操作,提供默认实现(通常是清理工作) public virtual void OnStateExit() { // 默认可能只是重置一些标志位 // 子类可以选择重写以进行更复杂的清理 } // 普通方法:所有状态共享的辅助方法 protected void PlayAnimation(string animName) { // 调用动画系统播放指定动画 } } // 2. 实现具体状态类 public class IdleState : CharacterStateBase { private float idleTimer; // 必须实现抽象方法 public override void OnStateEnter() { PlayAnimation(“Idle”); idleTimer = 0f; Debug.Log(“进入待机状态”); } // 重写虚方法,添加特定逻辑 public override void OnStateUpdate() { // 首先调用父类的通用更新逻辑(如果有的话) // base.OnStateUpdate(); idleTimer += Time.deltaTime; if (idleTimer > 5f) { // 触发切换到其他状态(如巡逻状态) } } // 可以选择不重写 OnStateExit,使用父类的默认清理 } public class AttackState : CharacterStateBase { public override void OnStateEnter() { PlayAnimation(“Attack”); // 播放攻击音效 // 生成攻击判定框 } public override void OnStateUpdate() { // 检查动画是否播放完毕,完毕则切换回待机或移动状态 } public override void OnStateExit() { // 必须重写,因为需要销毁攻击判定框,这是攻击状态特有的清理工作 // Destroy(attackCollider); // 同时也可以调用父类的清理逻辑(如果父类有) base.OnStateExit(); Debug.Log(“退出攻击状态,清理特效”); } }

设计思路解析

  • 为什么用abstract修饰OnStateEnter因为每个状态“进入”时的行为差异极大(播放的动画、初始化的变量、触发的特效都不同),且这个行为是必需的,没有合理的默认实现。这强制每个状态设计者都必须思考“进入这个状态要做什么”。
  • 为什么用virtual修饰OnStateUpdateOnStateExit因为“更新”和“退出”可能有通用模式。例如,所有状态可能都需要在Update里检查是否满足退出条件;所有状态在Exit时可能都需要重置某个标志。提供虚方法默认实现(哪怕是空的),给了子类一个“安全网”,也给了扩展的入口。AttackStateOnStateExit就展示了先做自己特有的清理,再调用父类通用清理的最佳实践。
  • 多态的威力:状态管理器只需要持有CharacterStateBase currentState;,然后调用currentState.OnStateUpdate()。它完全不用关心当前是哪种具体状态,代码简洁而强大。

3.2 场景二:设计UI控件的通用交互模板

UI系统是另一个重灾区,按钮、滑块、开关等控件都有共同的交互生命周期:指针进入、按下、抬起、退出。我们可以创建一个基础的交互类。

using UnityEngine; using UnityEngine.EventSystems; public abstract class UIInteractableBase : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler { [SerializeField] protected bool isInteractive = true; // 是否可交互 protected bool isHovered = false; protected bool isPressed = false; // 虚方法:提供默认的视觉反馈(如颜色变化) protected virtual void OnNormal() { // Debug.Log(“恢复正常状态”); } protected virtual void OnHover() { // Debug.Log(“鼠标悬停”); } protected virtual void OnPressed() { // Debug.Log(“鼠标按下”); } // 抽象方法:子类必须定义具体的交互行为(如点击后打开哪个面板) protected abstract void OnClick(); // 接口实现 - 这些方法通常是固定的模板,适合用虚方法提供默认骨架 public virtual void OnPointerEnter(PointerEventData eventData) { if (!isInteractive) return; isHovered = true; if (!isPressed) { OnHover(); } } public virtual void OnPointerExit(PointerEventData eventData) { if (!isInteractive) return; isHovered = false; if (!isPressed) { OnNormal(); } } public virtual void OnPointerDown(PointerEventData eventData) { if (!isInteractive) return; isPressed = true; OnPressed(); } public virtual void OnPointerUp(PointerEventData eventData) { if (!isInteractive) return; bool wasPressed = isPressed; isPressed = false; if (wasPressed && isHovered) { OnClick(); // 触发抽象方法,执行具体逻辑 OnNormal(); } else if (isHovered) { OnHover(); } else { OnNormal(); } } // 一个可能有用的虚方法:用于外部控制交互性 public virtual void SetInteractive(bool interactive) { if (isInteractive != interactive) { isInteractive = interactive; if (!interactive) { // 强制恢复到普通状态 isHovered = false; isPressed = false; OnNormal(); } } } }

设计思路解析

  • 接口与抽象类的结合:这里我们继承了MonoBehaviour并实现了Unity的UI事件接口(IPointerEnterHandler等)。接口保证了我们必须有这些方法,而抽象类UIInteractableBase则提供了这些接口方法的默认实现逻辑(状态管理、条件判断)。这是一种非常实用的模式。
  • virtual用于流程控制OnPointerDown等接口方法是固定的流程(按下->设置状态->调用反馈),所以用virtual。子类(如一个特殊按钮)如果需要在按下时播放特殊音效,可以重写OnPointerDown,并在其中先调用base.OnPointerDown(eventData)确保基础流程执行,再添加自己的音效逻辑。
  • abstract用于核心行为OnClick是每个UI控件唯一且必须不同的核心行为(普通按钮加载场景,商店按钮打开商店面板),所以定义为抽象方法。这强制每个具体的按钮类都必须明确“点击后到底要干嘛”。
  • protected virtual用于可选扩展OnNormal,OnHover,OnPressed是视觉/听觉反馈。我们提供了空实现(virtual),子类可以重写它们来改变颜色、缩放、播放动画等。如果某个控件不需要悬停效果,它完全可以不重写OnHover

3.3 场景三:实现可配置的数据管理器基类

在游戏开发中,我们经常需要管理各种配置表(如物品表、关卡表)。这些管理器的共同点是:都需要加载数据、提供根据ID查询数据的方法。但数据来源可能不同(Resources加载、Addressables、网络下载)。

using System.Collections.Generic; using UnityEngine; public abstract class DataManagerBase<TData, TKey> : MonoBehaviour where TData : class, new() { protected Dictionary<TKey, TData> dataDictionary = new Dictionary<TKey, TData>(); // 抽象方法:子类必须定义如何从原始数据(如TextAsset)解析成TData对象 protected abstract TData ParseDataRow(string rowData); // 抽象方法:子类必须定义如何从TData对象中提取出Key protected abstract TKey GetKeyFromData(TData data); // 虚方法:加载数据的总流程。这是一个“模板方法”,定义了步骤骨架。 public virtual bool LoadData(TextAsset dataFile) { if (dataFile == null) { Debug.LogError(“数据文件为空!”); return false; } dataDictionary.Clear(); string[] lines = dataFile.text.Split(‘\n’); bool success = true; // 步骤1: 遍历每一行(通常跳过表头) for (int i = 1; i < lines.Length; i++) { string line = lines[i].Trim(); if (string.IsNullOrEmpty(line)) continue; // 步骤2: 调用抽象方法解析单行数据 TData data = ParseDataRow(line); if (data == null) { Debug.LogWarning($“解析第{i}行数据失败: {line}”); success = false; continue; } // 步骤3: 调用抽象方法获取Key TKey key = GetKeyFromData(data); if (key == null) { Debug.LogWarning($“从数据中获取Key失败: {line}”); success = false; continue; } // 步骤4: 存入字典 if (!dataDictionary.ContainsKey(key)) { dataDictionary.Add(key, data); } else { Debug.LogWarning($“重复的Key: {key}, 第{i}行数据将被忽略。”); success = false; } } OnDataLoaded(success); // 步骤5: 加载完成后的回调 return success; } // 虚方法:数据加载完成后的回调,子类可重写以进行初始化 protected virtual void OnDataLoaded(bool loadSuccess) { if (loadSuccess) { Debug.Log($“{GetType().Name} 数据加载完成,共加载 {dataDictionary.Count} 条记录。”); } } // 具体方法:提供查询服务。基于抽象方法构建的稳定功能。 public TData GetData(TKey key) { if (dataDictionary.TryGetValue(key, out TData data)) { return data; } Debug.LogWarning($“未找到Key为 {key} 的数据。”); return null; } public bool HasData(TKey key) => dataDictionary.ContainsKey(key); }

设计思路解析

  • “模板方法”模式LoadData是一个经典的虚方法模板。它定义了加载数据的固定流程(清空字典、按行读取、解析、存储、回调),但将其中会变化的部分——如何解析一行数据(ParseDataRow)和如何获取Key(GetKeyFromData)——推迟到子类,通过抽象方法来实现。这样,所有数据管理器的加载逻辑都是一致且健壮的,我们只需要关心具体的解析规则。
  • 泛型的应用:使用泛型TDataTKey,使得这个基类可以用于管理任何类型的数据(ItemData,LevelData)和任何类型的Key(int,string)。这极大地提高了代码的复用性。
  • virtual用于提供扩展点OnDataLoaded是一个虚方法。基类提供了一个简单的日志实现。子类可以重写它,在数据加载完成后进行更复杂的操作,比如初始化其他依赖此数据的系统,或者对数据进行预处理建立索引。如果子类不需要,则什么都不用做。
  • 稳定与变化的分离GetDataHasData这些对外提供服务的具体方法是稳定的,它们建立在抽象的解析方法之上。无论子类如何解析数据,查询接口永远不变。这是面向对象设计“开闭原则”(对扩展开放,对修改关闭)的完美体现。

4. 核心选择策略与决策流程图

经过上面的场景分析,我们可以提炼出一套选择abstractvirtual还是普通方法的标准策略。这不仅仅是语法选择,更是设计意图的传达。

4.1 何时使用 Abstract(抽象方法与抽象类)

使用abstract的核心信号是:“我不知道你会怎么做,但你必须做这个。”这是一种强制性的设计约束。

选择抽象类(而非接口)当:

  1. 你需要为一系列相关的类提供共同的基类实现(字段、属性、非抽象方法)。例如,所有“敌人”都有血量(字段)和移动速度(属性),都有“播放受伤动画”(具体方法)和“计算掉落物”(抽象方法)的行为。抽象类可以包含这些具体成员,而接口不能。
  2. 你希望控制继承链的构造函数行为。抽象类可以有构造函数,用于初始化公共字段,确保子类在创建时处于一致的状态。
  3. 你预计未来可能会在基类中添加新的带有默认实现的方法(虚方法),而不想破坏所有现有的实现类。如果使用接口,添加新方法会强制所有实现类都去实现它,破坏性很大。

选择抽象方法当:

  1. 该行为在概念上是这个类族不可或缺的核心功能,但每个子类的实现逻辑完全不同,无法给出一个有意义的默认实现。
  2. 你希望强制每个子类的设计者都认真思考并实现这个行为,避免他们因为忘记实现而导致运行时错误(编译器会报错)。

注意:一个类只要包含至少一个抽象方法,这个类本身就必须声明为abstract。抽象类中可以同时包含抽象方法和具体方法(包括虚方法)。

4.2 何时使用 Virtual(虚方法)

使用virtual的核心信号是:“我提供了一个不错的默认实现,但如果你有更好的主意,欢迎你来改。”这是一种提供便利和允许扩展的设计。

选择虚方法当:

  1. 该行为有一个合理的、对大多数子类都适用的默认实现。例如,MonoBehaviour中的Start()Update()虽然是空方法,但它们被定义为虚方法,为你提供了在特定生命周期注入代码的入口。
  2. 你预见到部分子类可能需要定制或扩展该行为。例如,一个Vehicle类的Move()方法,默认可能是轮式移动,但Airplane子类就需要重写为飞行逻辑。
  3. 你正在实现**“模板方法”模式**。就像上面数据管理器的例子,在父类的虚方法中定义算法骨架,将某些步骤延迟到子类中实现。
  4. 你希望子类能够通过base.Method()调用父类的实现,在其基础上进行增强,而不是完全替换。

一个重要的权衡:过度使用虚方法会带来微小的性能开销(因为涉及虚方法表查找),但在现代游戏开发中,这点开销在绝大多数情况下可以忽略不计。设计的清晰度和灵活性远比这点性能重要。除非你在编写极度性能敏感的代码(如每帧调用数千次的底层循环),否则应优先考虑良好的设计。

4.3 何时使用普通方法(非 virtual)

当一个方法在父类中拥有完整、稳定且不希望被子类改变的实现时,就使用普通方法。这代表了“这就是最终方案,不要动它”。

选择普通方法当:

  1. 该方法是类的核心辅助功能或工具方法,其逻辑是确定且封闭的。例如,一个数学计算工具类中的方法。
  2. 该方法的实现涉及到类的内部状态或私有字段的完整性,如果被子类修改可能导致对象状态不一致。将其设为非虚方法可以保护类的内部不变性(Invariant)。
  3. 出于性能考虑,在极少数需要避免虚方法调用开销的场景下。
  4. 明确禁止子类以任何方式改变此行为。

4.4 决策流程图与检查清单

为了更直观地做出选择,你可以遵循下面的决策流程:

开始设计一个类的方法时,问自己: 1. 这个方法是否是此类族(继承体系)中“必须存在”的行为? ├── 是 → 2. └── 否 → 考虑它是否应该是这个类的方法?或许应该放到别处。 2. 我能否为所有子类提供一个有意义的默认实现? ├── 能 → 使用 Virtual 方法。(提供默认实现,允许子类修改) └── 不能 → 使用 Abstract 方法。(强制子类实现) 3. 对于非必须存在的方法,我是否预见到子类可能需要改变或扩展它? ├── 是 → 使用 Virtual 方法。 └── 否 → 使用普通方法。

实战检查清单(在代码审查或自己回顾时使用):

  • [ ]抽象方法检查:我定义的每个抽象方法,是否真的在所有子类中都有截然不同的实现?有没有可能提取一些公共逻辑到基类,将抽象方法改为虚方法?
  • [ ]虚方法检查:我提供的虚方法默认实现是否安全、无副作用?子类在重写时,是否清楚他们应该/可以调用base.Method()
  • [ ]重写检查:我重写的方法签名是否完全正确?访问修饰符是否没有变得更严格?我是否考虑了调用父类实现(如果需要)?
  • [ ]多态使用检查:我是否在尽可能使用基类类型(CharacterStateBase,UIInteractableBase)来引用对象,以利用多态性,而不是到处使用具体类型(IdleState,SpecialButton)?

5. 高级技巧、常见陷阱与性能考量

掌握了基础策略,我们再来看看一些能让你代码更上一层楼的技巧,以及必须绕开的深坑。

5.1new关键字与“隐藏”方法——一个危险的“特性”

有时你会看到这样的代码:

public class ParentClass { public void DoSomething() { Debug.Log(“Parent”); } } public class ChildClass : ParentClass { public new void DoSomething() { Debug.Log(“Child”); } // 使用 new 关键字 }

new关键字在这里的作用是“隐藏”从父类继承来的同名方法,而不是重写它。这是一个非常容易导致混淆和Bug的特性。

陷阱演示:

ParentClass obj = new ChildClass(); obj.DoSomething(); // 输出什么?

答案是输出“Parent”。因为obj的编译时类型是ParentClass,而DoSomething不是虚方法,没有多态性。编译器直接绑定了ParentClass.DoSomething

对比override

public class ParentClass { public virtual void DoSomething() { Debug.Log(“Parent”); } } public class ChildClass : ParentClass { public override void DoSomething() { Debug.Log(“Child”); } } ParentClass obj = new ChildClass(); obj.DoSomething(); // 输出 “Child”

结论与建议尽量避免使用new来隐藏方法。它的存在主要是为了处理版本兼容性问题(比如你引用的一个库更新了,其基类添加了一个和你子类同名的方法)。在你自己可控的代码中,如果子类需要提供与父类同名但行为不同的方法,首先考虑是否应该将父类方法设计为virtual然后进行override。如果父类方法确实不应该被重写(非virtual),那么考虑给子类方法换一个更准确的名字,以避免混淆。

5.2 构造函数、字段初始化与虚方法的调用顺序

这是一个经典的陷阱,在Unity中由于MonoBehaviour的生命周期而变得更加复杂。

public class BaseClass { protected string name = “Base”; public BaseClass() { Debug.Log(“Base Constructor: “ + name); Initialize(); } protected virtual void Initialize() { name += “_Initialized”; } } public class DerivedClass : BaseClass { public DerivedClass() { Debug.Log(“Derived Constructor: “ + name); } protected override void Initialize() { base.Initialize(); name = “Derived”; } } // 调用 new DerivedClass(); 输出顺序是?

输出顺序是:

  1. Base Constructor: Base(基类字段初始化)
  2. DerivedInitialize()被调用(因为多态,即使是在基类构造函数中),将name改为 “Derived”。
  3. Derived Constructor: Derived

陷阱:在基类构造函数中调用虚方法,此时子类的构造函数尚未执行,子类对象可能处于一个“未完全初始化”的状态。如果重写的虚方法依赖于子类构造函数中初始化的字段,就会导致空引用或默认值错误。

Unity 中的特定情况Awake()Start()MonoBehaviour的虚方法。如果一个基类MonoBehaviourAwake()中调用了一个虚方法进行初始化,而子类重写了这个方法并访问了在Start()或甚至Awake()后半部分才初始化的字段,就会出问题。

最佳实践

  1. 避免在构造函数中调用虚方法
  2. 在Unity中,如果需要在初始化时调用可定制的逻辑,可以考虑使用一个显式的Init()方法,并在所有相关字段都确保初始化后(例如在Start()中)手动调用它。
  3. 或者,使用一个bool isInitialized标志位,确保初始化逻辑只执行一次。

5.3 性能考量:虚方法调用与内联优化

虚方法调用比非虚方法调用稍慢,因为运行时需要通过虚方法表(vtable)进行间接查找,而不是直接跳转到固定的函数地址。这在绝大多数游戏逻辑中(每秒调用几千几万次)的影响微乎其微,完全不需要担心。

需要关注性能的场景

  • Update()中每帧调用数千次的、非常小的工具方法。如果这些方法是虚方法,且被证明是性能热点(通过Profiler检测),可以考虑将其改为非虚方法,或者使用其他设计模式(如策略模式通过委托注入)。
  • 极度关键的渲染循环或物理计算代码

不要进行不成熟的优化:永远先追求清晰、灵活、可维护的设计。在性能问题被实际测量和证实之前,不要因为担心虚调用开销而放弃良好的面向对象设计。使用virtualoverride带来的架构收益,远大于那纳秒级的性能成本。

5.4 抽象类 vs 接口:如何选择?

这是一个永恒的话题。在C#中,一个类只能继承一个基类(单继承),但可以实现多个接口。这是最根本的区别。

选择接口(Interface)当:

  • 你定义的是一个能力契约,而不是一个具体的实现。例如,“可以攻击”(IAttackable)、“可以销毁”(IDestroyable)。任何类,无论它继承自谁,都可以拥有这些能力。
  • 你需要让一个类拥有多种不同的、可能不相关的“角色”或“能力”
  • 你正在设计小而专一的契约,不包含任何实现细节。

选择抽象类(Abstract Class)当:

  • 你需要在多个紧密相关的类之间共享代码(字段、属性、具体方法实现)。
  • 你希望为继承者提供一些公共的状态或行为,并控制其构造过程。
  • 你预计基类在未来会添加新的带有默认实现的方法,并且不希望破坏所有现有子类(接口添加方法会破坏所有实现者)。

在Unity中的常见模式两者结合使用。例如,UIInteractableBase是一个抽象类,它提供了UI交互的通用状态管理和流程,并实现了IPointerClickHandler等接口。具体的按钮类继承这个抽象类,就自动获得了接口能力以及丰富的默认实现。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/21 1:41:16

苏州高端住宅容积率1.01的设计策略与价值分析

1. 项目背景解析&#xff1a;容积率背后的居住革命容积率1.01在苏州高端住宅市场堪称"奢侈指标"。这个数字意味着在虎丘江南里的地块上&#xff0c;每平方米土地仅建造约1.01平方米的建筑面积。相比苏州工业园区普遍2.0以上的容积率&#xff0c;这个数值直接反映了开…

作者头像 李华
网站建设 2026/7/21 1:40:45

LangGraph多智能体架构分水岭:Network+Supervisor双层设计

1. 项目概述&#xff1a;为什么“网络型监督型”多智能体架构正在成为LangGraph落地的分水岭最近三个月&#xff0c;我帮六家不同行业的客户做LangGraph项目咨询&#xff0c;从电商客服知识库增强&#xff0c;到金融风控规则引擎重构&#xff0c;再到生物医药文献摘要协同生成—…

作者头像 李华
网站建设 2026/7/21 1:31:10

STM32 GPIO工作原理与配置实战指南

1. GPIO基础概念与STM32特性GPIO(General Purpose Input/Output)是嵌入式系统中最基础也最重要的外设之一。在STM32微控制器中&#xff0c;GPIO引脚就像是我们与外部世界交互的"手脚"——通过它们可以读取传感器数据、控制LED、驱动电机&#xff0c;实现各种输入输出…

作者头像 李华
网站建设 2026/7/21 1:31:06

市场热门的谷歌SEO优化服务机构,究竟有何独特之处?

在当今数字化时代&#xff0c;谷歌SEO优化对于企业拓展海外市场至关重要。市场上热门的谷歌SEO优化服务机构众多&#xff0c;它们各有特色。以凰启出海为例&#xff0c;它作为外贸整合营销资深服务商&#xff0c;展现出了许多独特之处。专业团队与丰富经验凰启出海拥有一支由50…

作者头像 李华
网站建设 2026/7/21 1:27:57

WebGPU与WebCodecs实现浏览器4K视频剪辑技术解析

1. 项目概述&#xff1a;浏览器端的4K视频剪辑革命去年帮朋友处理一段活动视频时&#xff0c;我带着16寸MacBook Pro跑到咖啡馆&#xff0c;刚打开Final Cut Pro就引来了周围人异样的目光——专业视频编辑软件对硬件的要求&#xff0c;已经让移动办公成了伪命题。而OpenReel Vi…

作者头像 李华
网站建设 2026/7/21 1:25:44

机器学习工程师能力四层诊断图谱:从数据感知到业务归因

1. 这不是面试题集&#xff0c;而是一张机器学习能力诊断图谱“16 Interview Questions Every Machine Learning Enthusiast Should Know”——看到这个标题&#xff0c;我第一反应不是去背答案&#xff0c;而是把它当成一张X光片&#xff1a;它不考你记住了多少公式&#xff0…

作者头像 李华