1668 字
8 分钟
为 Godot 学 C# 03:数组、字符串、空值与异常

教程二 · 第 3 章:数组、字符串、可空性、异常#

3.1 数组:定长的一串#

int[] scores = new int[4]; // 4 个 int,默认全 0
scores[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,双引号是 string
int code = (int)'A'; // 65
char 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; // 警告:非空类型不该是 null
string? 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; // 直接强转,类型不对会抛 InvalidCastException
Player? 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 作业#

  1. 用插值字符串打印:玩家名、血量百分比((float)hp/maxHp * 100,想想为什么要 float),保留一位小数(:N1 格式)。
  2. 给平台游戏的敌人目标字段写一段安全逻辑:每帧先检查 IsInstanceValid(_target),有效才追击;无效就 GetFirstNodeInGroup("player") 重新找。
  3. int.TryParse 接住玩家在 LineEdit 里输入的等级,非法输入时 Label 提示”请输入数字”。
  4. 故意写: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/
作者
021028
发布于
2026-10-14
许可协议
CC BY-NC-SA 4.0