news 2026/8/6 15:32:43

Unity脚本生命周期全解析:从Awake到OnDestroy的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity脚本生命周期全解析:从Awake到OnDestroy的实战指南

1. 项目概述:为什么脚本生命周期是Unity开发的基石

如果你在Unity里写过脚本,肯定用过Start()Update()。但你是否曾好奇,为什么Awake()总在Start()之前执行?为什么物理计算要放在FixedUpdate()里?为什么有时对象销毁了,但它的引用还在?这些看似零散的问题,其答案都指向一个核心概念——Unity的脚本生命周期。理解它,不是让你死记硬背几个函数的执行顺序,而是让你真正掌握Unity引擎驱动游戏世界的底层脉搏。这就像开车,新手只关心油门和刹车,而老司机懂得发动机的转速、变速箱的换挡逻辑,从而能开得更稳、更省油,甚至在出问题时能快速定位是哪里“掉了链子”。

脚本生命周期定义了从游戏对象被创建(或激活)到最终销毁的整个过程中,Unity引擎按何种顺序、在何时调用我们编写的那些回调函数。它不是一个可选的“高级话题”,而是高效、稳定编写Unity代码的必备常识。混乱的生命周期管理,是导致对象引用丢失、物理计算抖动、协程行为诡异、乃至内存泄漏等“玄学”Bug的罪魁祸首。我见过太多项目,因为开发者对OnEnableAwake的调用时机模糊不清,导致复杂的初始化逻辑像抽奖一样时灵时不灵。

因此,今天我们不满足于官方手册那张流程图,而是要把它掰开揉碎,结合我踩过的无数个坑,从初始化、运行到销毁,为你构建一个立体、透彻且能直接指导编码的认知体系。无论你是刚入门的新手,还是想梳理知识的中级开发者,这篇文章都将是你Unity工具箱里最坚实的一块拼图。

2. 核心需求解析:我们到底想解决什么问题?

在深入细节之前,我们先明确理解脚本生命周期要解决的核心痛点。这绝不是为了学术研究,而是为了解决实际开发中那些令人头疼的问题。

2.1 确保可靠的初始化顺序

想象一个场景:你有一个Player脚本(控制玩家)和一个UIManager脚本(管理UI)。UIManager需要在游戏一开始就获取Player的引用,以便显示血条。如果你把获取引用的代码写在UIManagerStart()里,把Player的初始化写在它的Start()里,那么谁先执行?答案是:不确定。Unity不保证不同游戏对象上Start()的执行顺序。

这会导致UIManagerStart()可能先执行,此时它去查找Player实例,要么找不到,要么找到的是一个尚未完全初始化的Player对象,血条显示为0或者直接报空引用异常。这种Bug在编辑器里可能因为加载顺序偶然正常,但打包后就会随机出现,极难排查。

核心需求:我们需要一个确定性的、早于Start()的时机,来完成跨脚本的依赖注入和核心数据准备。这就是Awake()OnEnable()存在的首要意义。

2.2 协调不同频率的逻辑更新

游戏运行时,各种逻辑更新的频率需求是不同的:

  • 玩家输入、游戏状态判断:需要每帧都检查,频率与画面刷新率一致(Update)。
  • 物理模拟(如刚体运动、碰撞检测):需要一个固定的时间步长,以保证模拟的稳定性和可重复性,不受帧率波动影响(FixedUpdate)。
  • 摄像机跟随:必须在所有物体位置在本帧更新确定之后再进行,确保摄像机看到的是最终稳定的画面(LateUpdate)。

如果把物理计算塞进Update,帧率高时物体“飘”,帧率低时物体“穿模”。如果把摄像机逻辑放在Update,可能会看到角色抖动。核心需求:我们需要不同的“钩子”函数,将不同性质的逻辑分配到引擎最合适的处理阶段。

2.3 管理对象状态与资源生命周期

对象不是突然出现或消失的。它可能被禁用(SetActive(false)),然后又启用;可能被实例化(Instantiate),也可能被销毁(Destroy)。在这些状态变化的临界点,我们需要执行特定的操作:

  • 对象被激活时:可能需要注册到全局管理器、开始播放音效。
  • 对象被禁用时:需要从管理器中注销、停止所有协程、释放临时资源。
  • 对象被销毁时:必须释放持有的非托管资源(如网络连接、文件句柄)、通知其他系统。

如果搞混了OnDisableOnDestroy,可能会导致对象禁用时错误地释放了共享资源,或者对象销毁后仍有其他系统持有其引用,造成内存泄漏。核心需求:我们需要清晰、成对的生命周期回调,来安全地管理对象的“生老病死”。

2.4 理解渲染与逻辑的交互时机

对于需要自定义渲染或后处理的效果,你必须知道Unity在什么时候准备渲染数据、什么时候实际绘制。例如,你想在每帧渲染前动态修改物体的材质属性,或者自己用GL库画线。如果你在Update里修改,但Unity的渲染管线已经在更早的阶段缓存了数据,你的修改可能不生效。核心需求:我们需要了解渲染管线中的关键回调点(如OnWillRenderObject,OnRenderImage),以便在正确的时机“介入”渲染流程。

3. 生命周期全流程深度拆解

现在,我们进入核心部分,按照一个脚本从无到有,再到消亡的完整过程,逐一拆解每个阶段。

3.1 初始化阶段:诞生与唤醒

这个阶段发生在游戏对象首次变得“可用”之前,是搭建脚本骨架的关键时期。

3.1.1Awake():最早的构造者

  • 调用时机:当脚本实例被创建时,无论游戏对象是否激活(Active),都会立即调用。对于场景中已放置的对象,发生在场景加载时;对于通过Instantiate动态创建的对象,发生在实例化那一刻。
  • 核心特性
    1. 仅调用一次:在脚本实例的整个生命周期中,Awake只执行一次。
    2. 早于所有Start:这是Unity保证的绝对顺序。所有对象的Awake都将在任何对象的Start之前执行完毕。
    3. 对象未激活也会调用:这是与OnEnableStart最本质的区别。即使GameObjectactiveInHierarchyfalse,其上的Awake也会被调用。
  • 典型用途
    • 初始化内部私有变量
    • 获取并缓存组件引用(如GetComponent)。这是最佳实践,避免在每次Update中都去查找组件。
    • 建立脚本间的静态引用或向管理器注册。因为此时其他脚本的Awake也可能正在执行,你可以安全地寻找它们。
  • 实战心得

    我习惯把Awake看作脚本的“构造函数”。在这里完成所有不依赖于其他对象是否“准备就绪”的初始化工作。特别是获取组件引用,一定要在Awake中完成,这是一个性能优化点。记住,Awake里不要假设其他游戏对象已经激活,因为你可能正在初始化一个预设体(Prefab)中尚未激活的部分。

3.1.2OnEnable():激活的信号

  • 调用时机仅在脚本所属的游戏对象变为激活状态(Active)时调用。这包括:
    • 场景加载时,对象初始即为激活状态(在Awake之后,Start之前调用)。
    • 通过SetActive(true)激活一个之前被禁用的对象。
    • 创建(Instantiate)一个激活状态的对象(在Awake之后,Start之前调用)。
  • 核心特性
    1. 可多次调用:只要对象在激活与非激活状态间切换,OnEnable就会随之调用。
    2. 在对象可交互前执行:这是对象进入游戏世界前的最后准备。
  • 典型用途
    • 订阅事件:例如InputSystem的输入事件、自定义的消息事件。确保对象激活时能接收事件。
    • 开始播放循环音效或粒子效果
    • 重置一些运行时状态,例如将血量回满,准备开始新一轮游戏。
  • Awake的抉择
    • 如果一段逻辑在对象整个生命周期中只需执行一次(如获取组件引用),放在Awake
    • 如果一段逻辑需要在对象每次被激活时都执行(如注册事件、重置状态),放在OnEnable
    • 重要原则:在OnEnable中订阅的事件,必须在对应的OnDisable中取消订阅,否则会导致内存泄漏(即使对象被禁用,事件持有其引用,垃圾回收器也无法回收)。

3.1.3Start():就绪,开始!

  • 调用时机:在对象首次激活后的第一帧更新之前,并且在所有Awake函数执行完毕之后。仅调用一次
  • 核心特性
    1. 依赖已就绪:此时,你可以确信所有对象的Awake都已执行完毕。这是进行依赖于其他脚本初始化的操作的安全时机
    2. 对象必定处于激活状态:能执行到Start,意味着OnEnable已被调用,对象是活跃的。
  • 典型用途
    • 执行依赖于其他游戏对象或脚本已完成初始化的逻辑。例如,UIManagerStart中查找并绑定Player的引用,此时可以确信PlayerAwake(其中可能进行了关键初始化)已经完成。
    • 开始一些只需要执行一次的运行时逻辑。
  • 常见误区

    很多新手会把所有初始化代码都塞进Start,这可能导致跨脚本依赖问题。正确的做法是:将构建自身的代码放在Awake(如获取组件),将建立外部联系的代码放在Start。如果逻辑与激活状态强相关且可能多次执行,则应考虑OnEnable

3.2 运行阶段:心跳与律动

对象激活后,便进入了游戏循环。这个阶段由一系列按固定顺序和频率调用的函数主导。

3.2.1FixedUpdate():物理世界的节拍器

  • 调用时机:以固定的时间间隔调用,默认每秒50次(间隔0.02秒)。可以在Edit -> Project Settings -> Time中修改Fixed Timestep
  • 设计初衷:为物理模拟(PhysX引擎)提供一个稳定、可预测的时间步长。物理计算对稳定性要求极高,使用可变帧率的Update会导致模拟结果不一致(即“不同配置电脑上物理效果不同”)。
  • 核心规则
    • 如果游戏帧率很高,可能在一帧内调用多次FixedUpdate
    • 如果游戏帧率很低,可能多帧才调用一次FixedUpdate,引擎会通过“追赶”机制补足计算,但这可能导致“卡顿”感。
    • FixedUpdate中处理刚体运动(如Rigidbody.AddForce)时,无需乘以Time.deltaTime,因为其调用间隔本身就是固定的。
  • 典型用途
    • 所有与Rigidbody相关的操作。
    • 需要严格定时且与渲染帧率无关的逻辑(如某些游戏逻辑的定时器)。

3.2.2Update():逻辑更新的主循环

  • 调用时机每帧调用一次,调用频率与游戏帧率相同。
  • 核心特性:这是游戏逻辑的“大本营”,处理玩家输入、非物理的游戏状态更新、动画状态机驱动等。
  • 注意事项
    • Time.deltaTime是你的好朋友。任何与帧率相关的运动或变化(如移动非物理对象、插值、计时),都必须乘以Time.deltaTime来保证在不同帧率下速度一致。
    • Update的执行顺序默认是不确定的。两个不同对象上的Update,谁先谁后没有保证。如果需要明确顺序,必须使用Script Execution Order设置。

3.2.3LateUpdate():收尾与跟随

  • 调用时机:在同一帧中,所有Update函数执行完毕后调用。
  • 设计初衷:解决某些逻辑需要在所有其他逻辑完成之后才能执行的问题。
  • 最经典用例——第三人称摄像机跟随
    void Update () { // 玩家角色移动和旋转的逻辑 HandlePlayerMovement(); } void LateUpdate () { // 摄像机跟随:基于玩家当前(已在本帧Update中更新过的)位置进行计算 cameraTransform.position = playerTransform.position + offset; }
    • 如果摄像机跟随放在Update里,且摄像机的Update在玩家Update之前执行,那么摄像机就会基于玩家上一帧的位置进行跟随,导致画面抖动。LateUpdate保证了计算基于本帧最终结果。
  • 其他用途:UI界面的最终更新、一些需要在所有对象位置确定后才进行的计算。

3.2.4 渲染回调:与图形管线的握手

这一组函数让你有机会在Unity渲染管线的特定阶段插入代码。注意:以下回调主要在Built-in Render Pipeline中有效,在URP/HDRP中部分被新的渲染图(Render Graph)和Renderer Features等机制替代。

  • OnWillRenderObject():如果对象对任何摄像机可见,则为每个摄像机调用一次。可用于为每个摄像机动态修改材质属性。
  • OnPreRender(),OnPostRender():在特定摄像机开始渲染和结束渲染时调用。通常用于摄像机特效。
  • OnRenderImage(RenderTexture src, RenderTexture dest):在所有渲染完成后、最终图像显示到屏幕前调用。用于实现全屏后处理效果(如模糊、色调调整)。这是实现自定义后处理Shader的传统入口。
  • OnGUI():用于渲染IMGUI(Immediate Mode GUI)。注意OnGUI每帧可能被调用多次以处理布局和事件。它性能较低,仅适用于编辑器工具或简单调试界面,生产环境UI应使用UGUI或UI Toolkit。

3.2.5 协程(Coroutines):打破帧的束缚

协程不是生命周期函数,但它与Update循环紧密交互,是管理跨帧、延时行为的核心工具。

  • 本质:它是一个能在yield语句处暂停执行,并在下一帧或指定条件满足后从暂停处继续执行的函数。
  • 与生命周期的联动
    • yield return null;:在下一帧所有Update执行之后恢复。
    • yield return new WaitForFixedUpdate();:在下一帧所有FixedUpdate执行之后恢复。
    • yield return new WaitForSeconds(t);:在指定秒数(真实时间)后,于某一帧的Update之后恢复。
    • yield return StartCoroutine(OtherCoroutine());:等待另一个协程完全结束。
  • 关键陷阱

    协程的停止。如果你在OnDisableOnDestroy中不手动停止(StopCoroutine)或停止所有协程(StopAllCoroutines),那么即使对象被禁用或销毁,协程中引用的对象可能无法被垃圾回收,导致内存泄漏。更安全的方式是使用一个bool标志在OnDisable中控制协程逻辑退出。

3.3 终结阶段:禁用与销毁

对象生命周期的终点,是资源清理和状态重置的最后机会。

3.3.1OnDisable():停用的通知

  • 调用时机:当脚本所属的游戏对象变为非激活状态时调用。包括:
    • 调用SetActive(false)
    • 销毁对象(Destroy)时,在OnDestroy之前调用。
    • 包含该对象的场景被卸载时。
  • 核心职责清理在OnEnable中进行的操作。这是最重要的编程实践之一。
  • 必须在此执行的操作
    1. 取消所有事件订阅
    2. 停止所有协程
    3. 从全局管理器或列表中注销自身
    4. 停止音效、粒子等可能独立于对象状态继续播放的内容。
  • 心得

    OnDisable看作OnEnable的镜像。养成“在哪儿订阅,就在哪儿取消”的肌肉记忆。这是避免幽灵对象和内存泄漏的最有效手段。

3.3.2OnDestroy():最后的告别

  • 调用时机:在对象被销毁的当前帧的末尾,在所有帧更新函数执行完毕后调用。无论是通过Destroy立即销毁,还是因为场景切换而销毁,都会调用。
  • 核心职责:释放脚本持有的非托管资源或需要手动管理的资源。
    • 非托管资源:例如通过System.IO创建的文件流、网络连接、原生插件分配的内存等。.NET的垃圾回收器不管理这些,必须手动释放。
    • 托管资源:如对其他Unity对象(GameObject,Component)的引用,通常无需在此处理,因为引用断开后GC会处理。但有时为了加速回收或打破循环引用,可以在此将引用置为null
  • 注意:在OnDestroy中,你不能再创建新的Unity对象(如Instantiate)或调用Destroy其他对象,因为对象销毁流程已不可逆。

3.3.3OnApplicationQuit():应用的终点

  • 调用时机:在用户退出应用程序(包括在编辑器中停止播放)之前,在所有活动的游戏对象上调用。
  • 用途:执行全局的清理工作,如保存游戏数据到磁盘、向服务器发送退出信号、释放应用级别的单例资源。
  • 重要提示:在编辑器中,切换播放模式也会触发此回调。区分编辑器状态和真机状态时需要注意。

4. 高级主题与执行顺序控制

理解了基本流程后,我们来看看如何驾驭这个流程,解决复杂场景下的问题。

4.1 动画系统与状态机回调

当使用Animator组件时,Unity提供了另一组与动画状态机相关的回调,它们被插入到主更新循环的特定阶段。

  • OnStateMachineEnter/Exit:进入或退出一个动画状态机层时调用。
  • OnStateEnter/Update/Exit:进入、处于或离开某个具体动画状态时调用。
  • OnAnimatorIK:用于设置逆向动力学(IK),在动画处理之后、写入骨骼变换之前调用,可以覆盖动画结果。
  • OnAnimatorMove:用于处理根运动(Root Motion),允许脚本完全控制由动画驱动的角色位移。

这些回调的执行顺序被严格定义在生命周期流程图中(位于UpdateLateUpdate之间),使得动画与游戏逻辑可以精确同步。

4.2 掌控全局:Script Execution Order

默认情况下,不同游戏对象上相同生命周期函数的调用顺序是未定义的。这有时会导致问题。Unity提供了Script Execution Order(脚本执行顺序)来解决。

  • 位置Edit -> Project Settings -> Script Execution Order
  • 作用:你可以在这里拖拽脚本类型,为它们设置一个优先级数值。数值小的脚本(默认是0)先执行,数值大的后执行。
  • 典型应用场景
    1. 管理器模式:确保GameManagerInputManager等全局管理器的AwakeStart在所有其他业务脚本之前执行,以便完成全局初始化。
    2. 依赖关系:确保PhysicsSystemMovementSystem之前更新,因为移动系统需要最新的物理碰撞信息。
  • 使用建议

    不要滥用这个功能。过度依赖执行顺序会使代码耦合度变高,难以理解和维护。优先考虑通过事件(Event)、观察者模式或依赖注入来解耦脚本。仅在确有必要(如底层框架)时使用。

4.3 编辑器模式下的特殊回调

Reset()OnValidate()是两个仅在Unity编辑器中有用的回调。

  • Reset():当脚本首次被添加到游戏对象,或在Inspector面板中点击Reset菜单项时调用。常用于设置脚本的默认值。
  • OnValidate():当脚本的值在Inspector中被修改(包括反序列化,如加载场景、修改Prefab)时调用。常用于在编辑时验证输入、更新关联的组件或执行一些预览计算。

    警告OnValidate在编辑模式下频繁调用,切勿在其中执行耗时操作或产生副作用的逻辑(如实例化对象)。它主要用于数据验证和编辑器可视化。

5. 实战避坑指南与性能考量

理论结合实践,下面是我总结的几个关键陷阱和优化建议。

陷阱一:在Awake/OnEnable中访问其他未初始化的对象

  • 问题:在Awake中试图通过FindGetComponent查找一个可能尚未执行Awake的对象,或者该对象还未被实例化。
  • 解决方案
    1. 使用依赖注入:通过Inspector面板拖拽赋值,这是最可靠的方式。
    2. 使用单例或服务定位器模式,在Awake中将自己注册到全局可访问的地方,让其他对象在Start中按需获取。
    3. 如果必须动态查找,考虑将逻辑移到Start中,或者使用协程等待一帧(yield return null)。

陷阱二:Update中的性能黑洞

  • 问题:在Update中每帧进行昂贵的查找(如Find,GetComponent)、复杂的物理射线检测(Raycast)或字符串操作。
  • 解决方案
    1. 缓存:所有GetComponentFind的结果都应在Awake中缓存到私有变量中。
    2. 分帧处理:对于非紧急的批量操作(如更新大量NPC的状态),使用协程或自定义计时器分摊到多帧完成。
    3. 使用合适的更新频率:不是所有逻辑都需要每帧运行。对于AI决策、寻路更新等,可以使用InvokeRepeating或基于时间的自定义计时器。

陷阱三:协程与对象生命周期的不同步

  • 问题:协程中引用了外部对象,但该对象在协程执行过程中被销毁了,导致空引用异常。
  • 解决方案
    private IEnumerator MyCoroutine() { // 在关键操作前检查对象是否已被销毁 while (someCondition && this != null) { // 使用前再次检查关键依赖对象 if (targetObject == null) yield break; // 安全退出 // ... 你的逻辑 ... yield return new WaitForSeconds(1f); } } void OnDisable() { // 安全地停止协程 StopAllCoroutines(); }

陷阱四:不理解FixedUpdate与Update的混合使用

  • 问题:在Update中读取Rigidbody.velocity或施加力,结果不稳定。
  • 黄金法则
    • 读物理数据(位置、速度等):在UpdateLateUpdate中读,因为物理引擎在FixedUpdate后更新这些数据,在Update中读到的就是最新结果。
    • 写物理指令(加力、设置速度):在FixedUpdate中写,以确保指令在下一个物理步长中被处理。
    • 永远不要在Update中直接修改Transform的位置来移动物理对象,这会导致物理引擎和变换系统冲突。应通过Rigidbody来移动。

关于性能的思考:生命周期函数本身是引擎的调用开销。一个空的Update函数每帧也会产生微小的开销。对于大量存在的、不需要每帧更新的对象(如远处的装饰物),可以考虑使用按需更新模式:在Update中检查距离或状态,只有满足条件时才执行昂贵逻辑,或者完全禁用该脚本组件,在需要时再启用。

6. 调试与可视化技巧

理解生命周期最直观的方式就是看。这里有几个调试技巧:

  1. 打印日志法:在每个关键生命周期函数中打印带时间戳的日志,观察控制台的输出顺序。

    void Awake() { Debug.Log($"{Time.frameCount}: {gameObject.name} - Awake"); } void OnEnable() { Debug.Log($"{Time.frameCount}: {gameObject.name} - OnEnable"); } void Start() { Debug.Log($"{Time.frameCount}: {gameObject.name} - Start"); } void Update() { Debug.Log($"{Time.frameCount}: {gameObject.name} - Update"); } // ... 其他函数同理
  2. 使用Unity Profiler:在Profiler窗口的CPU使用率模块中,你可以清晰地看到每一帧中FixedUpdateUpdateLateUpdateCoroutines等所占用的时间和调用次数。这是定位性能问题的利器。

  3. 自定义编辑器可视化:对于复杂的对象状态机,可以在OnDrawGizmos中绘制图标和连线,直观显示对象在生命周期各阶段的状态(例如,用不同颜色表示Awake完成、Start完成等)。

掌握Unity脚本生命周期,本质上是掌握了与引擎对话的节奏。它让你从被动的代码执行者,变为主动的游戏世界架构师。当你清楚地知道每一行代码将在何时、以何种频率被调用时,你就能写出更健壮、更高效、更易于维护的代码。下次当你面对一个诡异的Bug时,不妨先问自己:我的代码,正处在生命周期的哪个阶段?

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

Kubernetes全栈编排与云原生架构实践指南

1. 云原生架构与K8s全栈编排的核心价值 云原生架构已经成为现代应用开发的黄金标准,而Kubernetes(K8s)作为这一领域的核心编排平台,其重要性不言而喻。在实际企业环境中,单纯部署K8s集群远远不够,真正的挑战…

作者头像 李华
网站建设 2026/8/6 15:28:03

KVM更改虚拟机默认存储路径

Virt默认的虚拟机存储路径是/var/lib/libvirt/images,如下图所示接下来我们创建一个新的存储池,用来存储新建的虚拟机。存储池的名称为vm, 路径为/home/kvm/ (/home目录磁盘空间最大)注意:是新建存储池,不是存储卷新的…

作者头像 李华
网站建设 2026/8/6 15:26:15

Transformer走到尽头?新一代Mobius架构,彻底突破三大瓶颈

文章目录前言1. Transformer的天花板,好像真摸到了1.1 大佬唱衰不是随口吐槽2. Transformer的三个老毛病,一直没根治2.1 脑子里有货,掏出来特别慢2.2 想学新知识,成本高得离谱2.3 没见过的题,很难举一反三3. 病根出在架…

作者头像 李华
网站建设 2026/8/6 15:26:13

MySQL binlog日志管理:安全删除与最佳实践

1. MySQL binlog日志文件管理基础MySQL的二进制日志(binlog)是数据库系统中至关重要的组成部分,它记录了所有修改数据的SQL语句(如INSERT、UPDATE、DELETE等),以二进制的形式保存在磁盘上。对于DBA和开发人…

作者头像 李华
网站建设 2026/8/6 15:23:48

如何用Sticky便签工具打造你的Linux桌面数字工作台

如何用Sticky便签工具打造你的Linux桌面数字工作台 【免费下载链接】sticky A sticky notes app for the linux desktop 项目地址: https://gitcode.com/gh_mirrors/stic/sticky 你是否经常在Linux桌面上找不到重要信息?会议记录、代码片段、待办事项散落在各…

作者头像 李华
网站建设 2026/8/6 15:20:17

C# 学习 (1.安装.net 10)

链接 https://dotnet.microsoft.com/zh-cn/download/dotnet/10.0 选择SDK-WINDOWS-安全程序-x64 下载并安装完成后,打开CMD 执行 dotnet --version ,返回10.XXXX 则安装成功

作者头像 李华