1668 字
8 分钟
为 Godot 学 C# 03:数组、字符串、空值与异常
教程二 · 第 3 章:数组、字符串、可空性、异常
3.1 数组:定长的一串
int[] scores = new int[4]; // 4 个 int,默认全 0scores[0] = 90;GD.Print(scores[0]); // 90,下标从 0 开始GD.Print(scores.Length); // 4
// 声明时直接填Vector2[] spawnPoints = { new(0, 0), new(100, 0), new(200, 0) };GD.Print(spawnPoints[2]); // (200, 0)spawnPoints[2] = new Vector2(300, 50);- 长度创建后不可变
- 越界访问(
scores[9])抛IndexOutOfRangeException - Godot 里数组出现频率不高,多数时候用 List(第 6 章),但
GetChildren()、碰撞列表、方法返回的T[]你要会读
遍历:
foreach (Vector2 p in spawnPoints) GD.Print(p);
for (int i = 0; i < spawnPoints.Length; i++) GD.Print($"{i}: {spawnPoints[i]}");3.2 字符串
创建与拼接
string name = "Arale";int hp = 80;
string s1 = "玩家 " + name + " HP=" + hp; // 加号拼接(少量使用没问题)string s2 = string.Format("玩家 {0} HP={1}", name, hp); // 占位符(老式)string s3 = $"玩家 {name} HP={hp}"; // 字符串插值,最常用$"..." 中花括号里可以放任何表达式:
GD.Print($"位置 {GlobalPosition:N1}");Label.Text = $"{_hp}/{MaxHp}";GD.Print($"暴击?{_hp < 30}"); // 直接放 bool常用操作
string line = " Godot C# ";line.Trim(); // "Godot C#",去两端空白"hello".ToUpper(); // HELLO"player.tscn".EndsWith(".tscn"); // true"res://scenes/main.tscn".Contains("main"); // true"a,b,c".Split(','); // ["a","b","c"]string.Join("-", 1, 2, 3); // "1-2-3""HP:80".Replace("80", "90");int.Parse("42"); // 字符串 → 数字,格式错会抛异常int.TryParse("42", out int v); // 安全版:失败返回 false,不炸string 是值类型脾气的引用类型:字符串不可修改,所有”修改”方法都返回新字符串:
string s = "abc";s.ToUpper();GD.Print(s); // 还是 abc!要写 s = s.ToUpper();字符与编码(够用版)
char c = 'A'; // 单引号是 char,双引号是 stringint code = (int)'A'; // 65char digit = "Level3"[5]; // 下标取字符读外部文件、做存档格式时才会深入编码,日常中文直接用就行(Godot 内部全 Unicode)。
3.3 null:空,以及怎么和它共处
引用类型变量可以指向”什么都没有”:
Player target = null; // 现在没有目标if (target != null) target.TakeDamage(10); // 不判空直接调用 → NullReferenceException 崩溃Godot 节点还有第二种”空”:C# 对象还在,但引擎里的节点已经被 QueueFree 销毁。判空要用 Godot 的方式:
if (Godot.GodotObject.IsInstanceValid(_target)){ // 节点真的存在于引擎中}// Godot 重载了 == null,被释放的节点 == null 也为 true,所以这样也行:if (_target != null && _target.IsInsideTree()) { }记住两个场景的区别:
target = null:你自己的引用还没赋值target.QueueFree()之后:引擎对象死了,普通 C# 判空有时不敏感 → 用IsInstanceValid
Nullable:现代 C# 的防空体系
在 .csproj 开 <Nullable>enable</Nullable> 后:
string a = null; // 警告:非空类型不该是 nullstring? b = null; // OK:? 声明"它可能为空"int? maybeLevel = null; // 值类型加 ? 也可空(Nullable<int>)int level = maybeLevel ?? 1; // ?? 空合并:为空就用右边maybeLevel ??= 1; // 为空才赋值b!.Length; // ! 告诉编译器"我确定不为空,别警告"(有风险才用)Godot 脚本的标准写法:
private Label _scoreLabel = null!; // 声明时还没有节点,承诺 _Ready 里赋值public override void _Ready(){ _scoreLabel = GetNode<Label>("ScoreLabel");}null! 是”null 包容”:值确实是 null,但我知道它会在进树时被赋值,让编译器闭嘴。别到处乱用 !,它是在告诉编译器别管你——只在你比编译器更清楚生命周期时用(Godot 节点字段正是这种情况)。
安全导航运算符 ?.
_target?.TakeDamage(10); // target 为 null 就什么都不做,等价于 if 判空var len = _player?.Name.Length; // 可能为 null 的表达式类型变成 string?对 Godot 已释放对象,?. 只防 C# null 不防”已释放”,那种情况仍用 IsInstanceValid。
3.4 异常:出错的正规通道
try{ var text = File.ReadAllText("user://save.json"); var data = Json.ParseString(text);}catch (Exception e) // 接住错误,程序不崩{ GD.PushError($"读档失败:{e.Message}");}finally{ GD.Print("无论成败都执行(清理资源用)");}- 异常是抛出来的:程序遇到非法操作(文件不存在、JSON 格式错、数组越界、强转失败)→ 抛对应异常 → 沿调用链向上找 catch,没人接 → 游戏崩/红字
Exception是所有异常的基类;可以只接特定类型:catch (System.IO.FileNotFoundException) { /* 存档丢了,重建默认档 */ }catch (System.Text.Json.JsonException) { /* 档坏了 */ }
异常使用原则(游戏开发实战版):
- 不要用异常处理”正常会发生的事”(玩家没跳过时别抛异常),它是为意外设计的,且有性能开销
- 边界处接住:读写文件、网络、玩家自定义内容——崩在存档损坏上是最差体验,这里要 try-catch 兜底并回退默认值
- 自己内部逻辑错误(拼写错误、状态不对)让它尽早炸,别用空 catch 吞掉:
catch { } // 反模式:错误被静默,以后 debug 地狱
Godot 全局兜底:
// Autoload 的 _Ready 中GD.UnhandledException += e => GD.PushError($"未处理异常:{e}");3.5 转换:类型之间
// 字符串 ↔ 数字int hp = int.Parse("100");int.TryParse("100", out hp);"100".ToInt(); // Godot 扩展,和 Parse 类似100.ToString();((int)3.9).ToString();
// 强转:数值之间、有继承关系的类型之间double d = 1.5;float f = (float)d;
Node node = GetNode("Player");Player p = (Player)node; // 直接强转,类型不对会抛 InvalidCastExceptionPlayer? safe = node as Player; // as:转不成给 null,不抛异常Godot 里最常用的模式就是第 2 章的 is:
if (node is Player player) { /* 安全拿到 player */ }选择口诀:确定类型用 (T) 直接转(错了早暴露);不确定先 is T t 或 as T 判空。
3.6 综合例子:读存档的完整防御写法
public SaveData LoadOrCreate(){ string path = "user://save.tres"; try { if (ResourceLoader.Exists(path)) { var data = ResourceLoader.Load<SaveData>(path); if (data != null) return data; } } catch (Exception e) { GD.PushError($"存档损坏,使用新档:{e.Message}"); // 也可把坏档改名备份:save.tres.bak } return new SaveData();}这里把第 6 章教程一的内容和本章的 null、异常、防御式写法串起来了。
3.7 作业
- 用插值字符串打印:玩家名、血量百分比(
(float)hp/maxHp * 100,想想为什么要 float),保留一位小数(:N1格式)。 - 给平台游戏的敌人目标字段写一段安全逻辑:每帧先检查
IsInstanceValid(_target),有效才追击;无效就GetFirstNodeInGroup("player")重新找。 int.TryParse接住玩家在 LineEdit 里输入的等级,非法输入时 Label 提示”请输入数字”。- 故意写:
string s = null; GD.Print(s.Length);,Build 能过吗?开了 Nullable 有什么提示?运行结果是什么?然后分别用?.和 try-catch 各修一次。
第 4 章进入面向对象:类与对象——理解 Godot 脚本到底是什么。
《Godot C# 零基础》系列导航
⬅️ 上一篇:为 Godot 学 C# 02:运算符、流程控制与方法 📑 返回系列总目录(第 01 篇) ➡️ 下一篇:为 Godot 学 C# 04:类与对象
为 Godot 学 C# 03:数组、字符串、空值与异常
https://021028.xyz/posts/default/172/