mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-24 04:06:48 +08:00
- 添加完整的错误处理最佳实践指南,涵盖Result<T>和Option<T>使用方式 - 补充移动端性能优化策略,包括纹理压缩、对象池、内存管理和电池优化 - 更新单元测试教程中的相关文档链接 - 完善错误处理层次结构和测试示例代码 - 增加移动端UI优化和平台适配最佳实践
1686 lines
45 KiB
Markdown
1686 lines
45 KiB
Markdown
# 错误处理最佳实践
|
||
|
||
> 本指南介绍 GFramework 中的错误处理模式和最佳实践,帮助你构建健壮、可维护的游戏应用。
|
||
|
||
## 📋 目录
|
||
|
||
- [概述](#概述)
|
||
- [核心概念](#核心概念)
|
||
- [Result 模式](#result-模式)
|
||
- [Option 模式](#option-模式)
|
||
- [异常处理](#异常处理)
|
||
- [日志记录](#日志记录)
|
||
- [用户反馈](#用户反馈)
|
||
- [错误恢复](#错误恢复)
|
||
- [最佳实践](#最佳实践)
|
||
- [常见问题](#常见问题)
|
||
|
||
## 概述
|
||
|
||
### 为什么错误处理很重要
|
||
|
||
良好的错误处理是构建健壮应用的基础:
|
||
|
||
- **提高稳定性**:优雅地处理异常情况,避免崩溃
|
||
- **改善用户体验**:提供清晰的错误提示和恢复选项
|
||
- **简化调试**:记录详细的错误信息,快速定位问题
|
||
- **增强可维护性**:使用类型安全的错误处理模式
|
||
|
||
### GFramework 的错误处理策略
|
||
|
||
GFramework 提供多种错误处理机制:
|
||
|
||
1. **Result<T> 模式**:函数式错误处理,类型安全
|
||
2. **Option<T> 模式**:处理可空值,避免 null 引用
|
||
3. **异常处理**:处理不可恢复的错误
|
||
4. **日志系统**:记录错误信息和调试数据
|
||
|
||
## 核心概念
|
||
|
||
### 错误类型
|
||
|
||
根据错误的性质和处理方式,可以分为以下几类:
|
||
|
||
**1. 可预期的错误**
|
||
|
||
这类错误是业务逻辑的一部分,应该使用 Result 或 Option 模式处理:
|
||
|
||
```csharp
|
||
// 示例:用户输入验证
|
||
public Result<PlayerData> ValidatePlayerName(string name)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(name))
|
||
return Result<PlayerData>.Failure("玩家名称不能为空");
|
||
|
||
if (name.Length < 3)
|
||
return Result<PlayerData>.Failure("玩家名称至少需要 3 个字符");
|
||
|
||
if (name.Length > 20)
|
||
return Result<PlayerData>.Failure("玩家名称不能超过 20 个字符");
|
||
|
||
return Result<PlayerData>.Success(new PlayerData { Name = name });
|
||
}
|
||
```
|
||
|
||
**2. 不可预期的错误**
|
||
|
||
这类错误通常是程序错误或系统故障,应该使用异常处理:
|
||
|
||
```csharp
|
||
// 示例:文件系统错误
|
||
public async Task<SaveData> LoadSaveFileAsync(string path)
|
||
{
|
||
try
|
||
{
|
||
var json = await File.ReadAllTextAsync(path);
|
||
return JsonSerializer.Deserialize<SaveData>(json);
|
||
}
|
||
catch (FileNotFoundException ex)
|
||
{
|
||
_logger.Error($"存档文件不存在: {path}", ex);
|
||
throw new SaveSystemException("无法加载存档文件", ex);
|
||
}
|
||
catch (JsonException ex)
|
||
{
|
||
_logger.Error($"存档文件格式错误: {path}", ex);
|
||
throw new SaveSystemException("存档文件已损坏", ex);
|
||
}
|
||
}
|
||
```
|
||
|
||
**3. 可空值**
|
||
|
||
使用 Option 模式处理可能不存在的值:
|
||
|
||
```csharp
|
||
// 示例:查找玩家
|
||
public Option<Player> FindPlayerById(string playerId)
|
||
{
|
||
var player = _players.FirstOrDefault(p => p.Id == playerId);
|
||
return player != null ? Option<Player>.Some(player) : Option<Player>.None;
|
||
}
|
||
```
|
||
|
||
### 错误传播
|
||
|
||
错误应该在合适的层级处理,不要过早捕获:
|
||
|
||
```csharp
|
||
// ✅ 好的做法:让错误向上传播
|
||
public class InventorySystem : AbstractSystem
|
||
{
|
||
public Result<Item> AddItem(string itemId, int quantity)
|
||
{
|
||
// 验证参数
|
||
var validationResult = ValidateAddItem(itemId, quantity);
|
||
if (validationResult.IsFaulted)
|
||
return validationResult;
|
||
|
||
// 检查空间
|
||
var spaceResult = CheckInventorySpace(quantity);
|
||
if (spaceResult.IsFaulted)
|
||
return Result<Item>.Failure(spaceResult.Exception);
|
||
|
||
// 添加物品
|
||
var item = CreateItem(itemId, quantity);
|
||
_items.Add(item);
|
||
|
||
return Result<Item>.Success(item);
|
||
}
|
||
}
|
||
|
||
// ❌ 避免:过早捕获错误
|
||
public class InventorySystem : AbstractSystem
|
||
{
|
||
public Result<Item> AddItem(string itemId, int quantity)
|
||
{
|
||
try
|
||
{
|
||
// 所有操作都在 try 块中
|
||
ValidateAddItem(itemId, quantity);
|
||
CheckInventorySpace(quantity);
|
||
var item = CreateItem(itemId, quantity);
|
||
_items.Add(item);
|
||
return Result<Item>.Success(item);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 捕获所有异常,丢失了错误的具体信息
|
||
return Result<Item>.Failure("添加物品失败");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 错误恢复
|
||
|
||
设计错误恢复策略,提供降级方案:
|
||
|
||
```csharp
|
||
// 示例:多级降级策略
|
||
public async Task<GameConfig> LoadConfigAsync()
|
||
{
|
||
// 1. 尝试从云端加载
|
||
var cloudResult = await TryLoadFromCloudAsync();
|
||
if (cloudResult.IsSuccess)
|
||
{
|
||
_logger.Info("从云端加载配置成功");
|
||
return cloudResult.Match(succ: c => c, fail: _ => null);
|
||
}
|
||
|
||
// 2. 尝试从本地缓存加载
|
||
var cacheResult = await TryLoadFromCacheAsync();
|
||
if (cacheResult.IsSuccess)
|
||
{
|
||
_logger.Warn("云端加载失败,使用本地缓存");
|
||
return cacheResult.Match(succ: c => c, fail: _ => null);
|
||
}
|
||
|
||
// 3. 使用默认配置
|
||
_logger.Error("所有配置源加载失败,使用默认配置");
|
||
return GetDefaultConfig();
|
||
}
|
||
```
|
||
|
||
## Result 模式
|
||
|
||
### 基本用法
|
||
|
||
Result<T> 是一个函数式类型,表示操作可能成功(包含值)或失败(包含异常):
|
||
|
||
```csharp
|
||
// 创建成功结果
|
||
var successResult = Result<int>.Success(42);
|
||
|
||
// 创建失败结果
|
||
var failureResult = Result<int>.Failure(new Exception("操作失败"));
|
||
var failureResult2 = Result<int>.Failure("操作失败"); // 使用字符串
|
||
|
||
// 检查状态
|
||
if (result.IsSuccess)
|
||
{
|
||
var value = result.Match(succ: v => v, fail: _ => 0);
|
||
}
|
||
|
||
if (result.IsFaulted)
|
||
{
|
||
var error = result.Exception;
|
||
}
|
||
```
|
||
|
||
### 链式操作
|
||
|
||
Result 支持函数式编程的链式操作:
|
||
|
||
```csharp
|
||
// Map:转换成功值
|
||
var result = Result<int>.Success(42)
|
||
.Map(x => x * 2) // Result<int>(84)
|
||
.Map(x => x.ToString()); // Result<string>("84")
|
||
|
||
// Bind:链式转换,可能失败
|
||
var result = Result<string>.Success("42")
|
||
.Bind(s => int.TryParse(s, out var i)
|
||
? Result<int>.Success(i)
|
||
: Result<int>.Failure("解析失败"))
|
||
.Map(i => i * 2);
|
||
|
||
// Ensure:验证条件
|
||
var result = Result<int>.Success(42)
|
||
.Ensure(x => x > 0, "值必须为正数")
|
||
.Ensure(x => x < 100, "值必须小于 100");
|
||
|
||
// OnSuccess/OnFailure:执行副作用
|
||
result
|
||
.OnSuccess(value => _logger.Info($"成功: {value}"))
|
||
.OnFailure(ex => _logger.Error($"失败: {ex.Message}"));
|
||
```
|
||
|
||
### 实际应用示例
|
||
|
||
**示例 1:用户注册**
|
||
|
||
```csharp
|
||
public class UserRegistrationSystem : AbstractSystem
|
||
{
|
||
public Result<User> RegisterUser(string username, string email, string password)
|
||
{
|
||
return ValidateUsername(username)
|
||
.Bind(_ => ValidateEmail(email))
|
||
.Bind(_ => ValidatePassword(password))
|
||
.Bind(_ => CheckUsernameAvailability(username))
|
||
.Bind(_ => CheckEmailAvailability(email))
|
||
.Map(_ => CreateUser(username, email, password))
|
||
.OnSuccess(user => {
|
||
_logger.Info($"用户注册成功: {username}");
|
||
this.SendEvent(new UserRegisteredEvent { User = user });
|
||
})
|
||
.OnFailure(ex => _logger.Warn($"用户注册失败: {ex.Message}"));
|
||
}
|
||
|
||
private Result<string> ValidateUsername(string username)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(username))
|
||
return Result<string>.Failure("用户名不能为空");
|
||
|
||
if (username.Length < 3 || username.Length > 20)
|
||
return Result<string>.Failure("用户名长度必须在 3-20 个字符之间");
|
||
|
||
if (!Regex.IsMatch(username, @"^[a-zA-Z0-9_]+$"))
|
||
return Result<string>.Failure("用户名只能包含字母、数字和下划线");
|
||
|
||
return Result<string>.Success(username);
|
||
}
|
||
|
||
private Result<string> ValidateEmail(string email)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(email))
|
||
return Result<string>.Failure("邮箱不能为空");
|
||
|
||
if (!Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$"))
|
||
return Result<string>.Failure("邮箱格式不正确");
|
||
|
||
return Result<string>.Success(email);
|
||
}
|
||
|
||
private Result<string> ValidatePassword(string password)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(password))
|
||
return Result<string>.Failure("密码不能为空");
|
||
|
||
if (password.Length < 8)
|
||
return Result<string>.Failure("密码长度至少为 8 个字符");
|
||
|
||
if (!passwordchar.IsUpper))
|
||
return Result<string>.Failure("密码必须包含至少一个大写字母");
|
||
|
||
if (!password.Any(char.IsDigit))
|
||
return Result<string>.Failure("密码必须包含至少一个数字");
|
||
|
||
return Result<string>.Success(password);
|
||
}
|
||
|
||
private Result<bool> CheckUsernameAvailability(string username)
|
||
{
|
||
var exists = _userRepository.ExistsByUsername(username);
|
||
return exists
|
||
? Result<bool>.Failure("用户名已被使用")
|
||
: Result<bool>.Success(true);
|
||
}
|
||
|
||
private Result<bool> CheckEmailAvailability(string email)
|
||
{
|
||
var exists = _userRepository.ExistsByEmail(email);
|
||
return exists
|
||
? Result<bool>.Failure("邮箱已被注册")
|
||
: Result<bool>.Success(true);
|
||
}
|
||
|
||
private User CreateUser(string username, string email, string password)
|
||
{
|
||
var user = new User
|
||
{
|
||
Id = Guid.NewGuid().ToString(),
|
||
Username = username,
|
||
Email = email,
|
||
PasswordHash = HashPassword(password),
|
||
CreatedAt = DateTime.UtcNow
|
||
};
|
||
|
||
_userRepository.Add(user);
|
||
return user;
|
||
}
|
||
}
|
||
```
|
||
|
||
**示例 2:物品交易**
|
||
|
||
```csharp
|
||
public class TradingSystem : AbstractSystem
|
||
{
|
||
public Result<Trade> ExecuteTrade(string sellerId, string buyerId, string itemId, int price)
|
||
{
|
||
return ValidateTradeParticipants(sellerId, buyerId)
|
||
.Bind(_ => ValidateItem(sellerId, itemId))
|
||
.Bind(_ => ValidateBuyerFunds(buyerId, price))
|
||
.Bind(_ => TransferItem(sellerId, buyerId, itemId))
|
||
.Bind(_ =&gerCurrency(buyerId, sellerId, price))
|
||
.Map(_ => CreateTradeRecord(sellerId, buyerId, itemId, price))
|
||
.OnSuccess(trade => {
|
||
_logger.Info($"交易完成: {trade.Id}");
|
||
this.SendEvent(new TradeCompletedEvent { Trade = trade });
|
||
})
|
||
.OnFailure(ex => {
|
||
_logger.Error($"交易失败: {ex.Message}");
|
||
RollbackTrade(sellerId, buyerId, itemId, price);
|
||
});
|
||
}
|
||
|
||
private Result<bool> ValidateTradeParticipants(string sellerId, string buyerId)
|
||
{
|
||
if (sellerId == buyerId)
|
||
return Result<bool>.Failure("不能与自己交易");
|
||
|
||
var seller = _playerRepository.GetById(sellerId);
|
||
if (seller == null)
|
||
return Result<bool>.Failure("卖家不存在");
|
||
|
||
var buyer = _playerRepository.GetById(buyerId);
|
||
if (buyer == null)
|
||
return Result<bool>.Failure("买家不存在");
|
||
|
||
return Result<bool>.Success(true);
|
||
}
|
||
|
||
private Result<bool> ValidateItem(string sellerId, string itemId)
|
||
{
|
||
var inventory = _inventoryRepository.GetByPlayerId(sellerId);
|
||
var item = inventory.Items.FirstOrDefault(i => i.Id == itemId);
|
||
|
||
if (item == null)
|
||
return Result<bool>.Failure("物品不存在");
|
||
|
||
if (!item.IsTradeable)
|
||
return Result<bool>.Failure("该物品不可交易");
|
||
|
||
return Result<bool>.Success(true);
|
||
}
|
||
|
||
private Result<bool> ValidateBuyerFunds(string buyerId, int price)
|
||
{
|
||
var buyer = _playerRepository.GetById(buyerId);
|
||
if (buyer.Currency < price)
|
||
return Result<bool>.Failure($"金币不足,需要 {price},当前 {buyer.Currency}");
|
||
|
||
return Result<bool>.Success(true);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 异步操作
|
||
|
||
Result 支持异步操作:
|
||
|
||
```csharp
|
||
// 异步 Map
|
||
var result = await Result<int>.Success(42)
|
||
.MapAsync(async x => await GetUserDataAsync(x));
|
||
|
||
// 异步 Bind
|
||
var result = await Result<string>.Success("user123")
|
||
.BindAsync(async userId => await LoadUserAsync(userId));
|
||
|
||
// TryAsync:安全执行异步操作
|
||
var result = await ResultExtensions.TryAsync(async () =>
|
||
{
|
||
var data = await LoadDataAsync();
|
||
return ProcessData(data);
|
||
});
|
||
```
|
||
|
||
### 组合多个 Result
|
||
|
||
```csharp
|
||
// 组合多个结果,全部成功才返回成功
|
||
var results = new[]
|
||
{
|
||
ValidateUsername("player1"),
|
||
ValidateEmail("player1@example.com"),
|
||
ValidatePassword("Password123")
|
||
};
|
||
|
||
var combinedResult = results.Combine(); // Result<List<string>>
|
||
|
||
if (combinedResult.IsSuccess)
|
||
{
|
||
_logger.Info("所有验证通过");
|
||
}
|
||
else
|
||
{
|
||
_logger.Error($"验证失败: {combinedResult.Exception.Message}");
|
||
}
|
||
```
|
||
|
||
## Option 模式
|
||
|
||
### 基本用法
|
||
|
||
Option<T> 用于表示可能存在或不存在的值,避免 null 引用异常:
|
||
|
||
```csharp
|
||
// 创建 Some(有值)
|
||
var someOption = Option<int>.Some(42);
|
||
|
||
// 创建 None(无值)
|
||
var noneOption = Option<int>.None;
|
||
|
||
// 检查状态
|
||
if (option.IsSome)
|
||
{
|
||
var value = option.GetOrElse(0);
|
||
}
|
||
|
||
if (option.IsNone)
|
||
{
|
||
_logger.Warn("值不存在");
|
||
}
|
||
```
|
||
|
||
### 常用操作
|
||
|
||
```csharp
|
||
// GetOrElse:获取值或返回默认值
|
||
var value = option.GetOrElse(0);
|
||
var value2 = option.GetOrElse(() => GetDefaultValue());
|
||
|
||
// Map:转换值
|
||
var mapped = option.Map(x => x * 2);
|
||
|
||
// Bind:链式转换
|
||
var bound = option.Bind(x => x > 0
|
||
? Option<string>.Some(x.ToString())
|
||
: Option<string>.None);
|
||
|
||
// Filter:过滤值
|
||
var filtered = option.Filter(x => x > 0);
|
||
|
||
// Match:模式匹配
|
||
var result = option.Match(
|
||
some: value => $"值: {value}",
|
||
none: () => "无值"
|
||
);
|
||
```
|
||
|
||
### 实际应用示例
|
||
|
||
**示例 1:查找玩家**
|
||
|
||
```csharp
|
||
public class PlayerSystem : AbstractSystem
|
||
{
|
||
public Option<Player> FindPlayerById(string playerId)
|
||
{
|
||
var player = _players.FirstOrDefault(p => p.Id == playerId);
|
||
return player != null ? Option<Player>.Some(player) : Option<Player>.None;
|
||
}
|
||
|
||
public Option<Player> FindPlayerByName(string playerName)
|
||
{
|
||
var player = _players.FirstOrDefault(p => p.Name == playerName);
|
||
return player != null ? Option<Player>.Some(player) : Option<Player>.None;
|
||
}
|
||
|
||
public void UpdatePlayerScore(string playerId, int score)
|
||
{
|
||
FindPlayerById(playerId).Match(
|
||
some: player => {
|
||
player.Score += score;
|
||
_logger.Info($"玩家 {player.Name} 得分更新: {player.Score}");
|
||
this.SendEvent(new PlayerScoreUpdatedEvent { Player = player });
|
||
},
|
||
none: () => _logger.Warn($"玩家不存在: {playerId}")
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
**示例 2:配置管理**
|
||
|
||
```csharp
|
||
public class ConfigurationSystem : AbstractSystem
|
||
{
|
||
private readonly Dictionary<string, string> _config = new();
|
||
|
||
public Option<string> GetConfig(string key)
|
||
{
|
||
return _config.TryGetValue(key, out var value)
|
||
? Option<string>.Some(value)
|
||
: Option<string>.None;
|
||
}
|
||
|
||
public Option<int> GetIntConfig(string key)
|
||
{
|
||
return GetConfig(key)
|
||
.Bind(value => int.TryParse(value, out var result)
|
||
? Option<int>.Some(result)
|
||
: Option<int>.None);
|
||
}
|
||
|
||
public Option<bool> GetBoolConfig(string key)
|
||
{
|
||
return GetConfig(key)
|
||
.Bind(value => bool.TryParse(value, out var result)
|
||
? Option<bool>.Some(result)
|
||
: Option<bool>.None);
|
||
}
|
||
|
||
public void ApplyGraphicsSettings()
|
||
{
|
||
var width = GetIntConfig("screen_width").GetOrElse(1920);
|
||
var height = GetIntConfig("screen_height").GetOrElse(1080);
|
||
var fullscreen = GetBoolConfig("fullscreen").GetOrElse(false);
|
||
var vsync = GetBoolConfig("vsync").GetOrElse(true);
|
||
|
||
ApplyScreenSettings(width, height, fullscreen, vsync);
|
||
}
|
||
}
|
||
```
|
||
|
||
**示例 3:物品查找**
|
||
|
||
```csharp
|
||
public class InventorySystem : AbstractSystem
|
||
{
|
||
public Option<Item> FindItemById(string itemId)
|
||
{
|
||
var item = _items.FirstOrDefault(i => i.Id == itemId);
|
||
return item != null ? Option<Item>.Some(item) : Option<Item>.None;
|
||
}
|
||
|
||
public Option<Item> FindItemByType(ItemType type)
|
||
{
|
||
var item = _items.FirstOrDefault(i => i.Type == type);
|
||
return item != null ? Option<Item>.Some(item) : Option<Item>.None;
|
||
}
|
||
|
||
public Result<Item> UseItem(string itemId)
|
||
{
|
||
return FindItemById(itemId)
|
||
.ToResult("物品不存在")
|
||
.Bind(item => item.IsUsable
|
||
? Result<Item>.Success(item)
|
||
: Result<Item>.Failure("物品不可使用"))
|
||
.OnSuccess(item => {
|
||
item.Use();
|
||
_logger.Info($"使用物品: {item.Name}");
|
||
this.SendEvent(new ItemUsedEvent { Item = item });
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
### Option 与 Result 的转换
|
||
|
||
```csharp
|
||
// Option 转 Result
|
||
var option = Option<int>.Some(42);
|
||
var result = option.ToResult("值不存在"); // Result<int>
|
||
|
||
// Result 转 Option(需要自定义扩展)
|
||
var result = Result<int>.Success(42);
|
||
var option = result.IsSuccess
|
||
? Option<int>.Some(result.Match(succ: v => v, fail: _ => 0))
|
||
: Option<int>.None;
|
||
```
|
||
|
||
## 异常处理
|
||
|
||
### 何时使用异常
|
||
|
||
异常应该用于处理**不可预期的错误**和**不可恢复的错误**:
|
||
|
||
```csharp
|
||
// ✅ 适合使用异常的场景
|
||
public class SaveSystem : AbstractSystem
|
||
{
|
||
public async Task SaveGameAsync(SaveData data)
|
||
{
|
||
try
|
||
{
|
||
var json = JsonSerializer.Serialize(data);
|
||
await File.WriteAllTextAsync(_savePath, json);
|
||
}
|
||
catch (UnauthorizedAccessException ex)
|
||
{
|
||
_logger.Fatal("没有写入权限", ex);
|
||
throw new SaveSystemException("无法保存游戏:权限不足", ex);
|
||
}
|
||
catch (IOException ex)
|
||
{
|
||
_logger.Error("IO 错误", ex);
|
||
throw new SaveSystemException("无法保存游戏:磁盘错误", ex);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ❌ 不适合使用异常的场景
|
||
public class PlayerSystem : AbstractSystem
|
||
{
|
||
// 不要用异常处理业务逻辑
|
||
public void UpdatePlayerHealth(string playerId, int damage)
|
||
{
|
||
var player = FindPlayerById(playerId);
|
||
if (player == null)
|
||
throw new PlayerNotFoundException(playerId); // ❌ 应该用 Option 或 Result
|
||
|
||
player.Health -= damage;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 自定义异常
|
||
|
||
创建有意义的异常类型:
|
||
|
||
```csharp
|
||
// 基础游戏异常
|
||
public class GameException : Exception
|
||
{
|
||
public string ErrorCode { get; }
|
||
public Dictionary<string, object> Context { get; }
|
||
|
||
public GameException(
|
||
string message,
|
||
string errorCode,
|
||
Dictionary<string, object> context = null,
|
||
Exception innerException = null)
|
||
: base(message, innerException)
|
||
{
|
||
ErrorCode = errorCode;
|
||
Context = context ?? new Dictionary<string, object>();
|
||
}
|
||
}
|
||
|
||
// 存档系统异常
|
||
public class SaveSystemException : GameException
|
||
{
|
||
public SaveSystemException(
|
||
string message,
|
||
Exception innerException = null)
|
||
: base(message, "SAVE_ERROR", null, innerException)
|
||
{
|
||
}
|
||
}
|
||
|
||
// 网络异常
|
||
public class NetworkException : GameException
|
||
{
|
||
public int StatusCode { get; }
|
||
|
||
public NetworkException(
|
||
string message,
|
||
int statusCode,
|
||
Exception innerException = null)
|
||
: base(
|
||
message,
|
||
"NETWORK_ERROR",
|
||
new Dictionary<string, object> { ["statusCode"] = statusCode },
|
||
innerException)
|
||
{
|
||
StatusCode = statusCode;
|
||
}
|
||
}
|
||
|
||
// 使用示例
|
||
public class NetworkSystem : AbstractSystem
|
||
{
|
||
public async Task<PlayerData> FetchPlayerDataAsync(string playerId)
|
||
{
|
||
try
|
||
{
|
||
var response = await _httpClient.GetAsync($"/api/players/{playerId}");
|
||
|
||
if (!response.IsSuccessStatusCode)
|
||
{
|
||
throw new NetworkException(
|
||
$"获取玩家数据失败: {playerId}",
|
||
(int)response.StatusCode
|
||
);
|
||
}
|
||
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
return JsonSerializer.Deserialize<PlayerData>(json);
|
||
}
|
||
catch (HttpRequestException ex)
|
||
{
|
||
_logger.Error($"网络请求失败: {playerId}", ex);
|
||
throw new NetworkException("网络连接失败", 0, ex);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 异常处理最佳实践
|
||
|
||
**1. 捕获具体的异常类型**
|
||
|
||
```csharp
|
||
// ✅ 好的做法
|
||
try
|
||
{
|
||
var data = await LoadDataAsync();
|
||
}
|
||
catch (FileNotFoundException ex)
|
||
{
|
||
_logger.Warn("文件不存在", ex);
|
||
return GetDefaultData();
|
||
}
|
||
catch (JsonException ex)
|
||
{
|
||
_logger.Error("JSON 解析失败", ex);
|
||
throw new DataException("数据格式错误", ex);
|
||
}
|
||
catch (IOException ex)
|
||
{
|
||
_logger.Error("IO 错误", ex);
|
||
throw new DataException("读取数据失败", ex);
|
||
}
|
||
|
||
// ❌ 避免:捕获所有异常
|
||
try
|
||
{
|
||
var data = await LoadDataAsync();
|
||
}
|
||
catch (Exception ex) // 太宽泛
|
||
{
|
||
_logger.Error("加载失败", ex);
|
||
return null;
|
||
}
|
||
```
|
||
|
||
**2. 不要吞掉异常**
|
||
|
||
```csharp
|
||
// ❌ 避免:吞掉异常
|
||
try
|
||
{
|
||
RiskyOperation();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 什么都不做,异常被吞掉了
|
||
}
|
||
|
||
// ✅ 好的做法:记录并重新抛出或处理
|
||
try
|
||
{
|
||
RiskyOperation();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Error("操作失败", ex);
|
||
throw; // 重新抛出原始异常
|
||
}
|
||
```
|
||
|
||
**3. 使用 finally 清理资源**
|
||
|
||
```csharp
|
||
// ✅ 好的做法
|
||
FileStream stream = null;
|
||
try
|
||
{
|
||
stream = File.OpenRead(path);
|
||
// 处理文件
|
||
}
|
||
catch (IOException ex)
|
||
{
|
||
_logger.Error("文件读取失败", ex);
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
stream?.Dispose(); // 确保资源被释放
|
||
}
|
||
|
||
// 更好的做法:使用 using 语句
|
||
using (var stream = File.OpenRead(path))
|
||
{
|
||
// 处理文件
|
||
} // 自动释放资源
|
||
```
|
||
|
||
## 日志记录
|
||
|
||
### 日志级别
|
||
|
||
GFramework 提供多个日志级别,根据错误严重程度选择合适的级别:
|
||
|
||
```csharp
|
||
// Trace:详细的调试信息
|
||
_logger.Trace("进入 ProcessInput 方法");
|
||
|
||
// Debug:调试信息
|
||
_logger.Debug($"玩家位置: {player.Position}");
|
||
|
||
// Info:一般信息
|
||
_logger.Info("游戏启动成功");
|
||
|
||
// Warning:警告信息
|
||
_logger.Warn("配置文件不存在,使用默认配置");
|
||
|
||
// Error:错误信息
|
||
_logger.Error("保存游戏失败", exception);
|
||
|
||
// Fatal:致命错误
|
||
_logger.Fatal("无法初始化渲染系统", exception);
|
||
```
|
||
|
||
### 结构化日志
|
||
|
||
使用结构化日志记录上下文信息:
|
||
|
||
```csharp
|
||
// 记录带上下文的日志
|
||
_logger.Log(
|
||
LogLevel.Error,
|
||
"玩家操作失败",
|
||
("playerId", playerId),
|
||
("action", "attack"),
|
||
("targetId", targetId),
|
||
("timestamp", DateTime.UtcNow)
|
||
);
|
||
|
||
// 记录带异常的结构化日志
|
||
_logger.Log(
|
||
LogLevel.Error,
|
||
"数据库操作失败",
|
||
exception,
|
||
("operation", "insert"),
|
||
("table", "players"),
|
||
("retryCount", retryCount)
|
||
);
|
||
```
|
||
|
||
### 日志最佳实践
|
||
|
||
**1. 记录足够的上下文**
|
||
|
||
```csharp
|
||
// ❌ 避免:信息不足
|
||
_logger.Error("操作失败");
|
||
|
||
// ✅ 好的做法:包含上下文
|
||
_logger.Error(
|
||
$"保存玩家数据失败: PlayerId={playerId}, Reason={ex.Message}",
|
||
ex
|
||
);
|
||
```
|
||
|
||
**2. 不要记录敏感信息**
|
||
|
||
```csharp
|
||
// ❌ 避免:记录密码等敏感信息
|
||
_logger.Info($"用户登录: {username}, 密码: {password}");
|
||
|
||
// ✅ 好的做法:不记录敏感信息
|
||
_logger.Info($"用户登录: {username}");
|
||
```
|
||
|
||
**3. 使用合适的日志级别**
|
||
|
||
```csharp
|
||
public class SaveSystem : AbstractSystem
|
||
{
|
||
public async Task<Result<SaveData>> LoadSaveAsync(string saveId)
|
||
{
|
||
_logger.Debug($"开始加载存档: {saveId}");
|
||
|
||
try
|
||
{
|
||
var data = await _storage.LoadAsync<SaveData>(saveId);
|
||
|
||
if (data == null)
|
||
{
|
||
_logger.Warn($"存档不存在: {saveId}");
|
||
return Result<SaveData>.Failure("存档不存在");
|
||
}
|
||
|
||
_logger.Info($"存档加载成功: {saveId}");
|
||
return Result<SaveData>.Success(data);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Error($"加载存档失败: {saveId}", ex);
|
||
return Result<SaveData>.Failure(ex);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 用户反馈
|
||
|
||
### 友好的错误提示
|
||
|
||
将技术错误转换为用户友好的消息:
|
||
|
||
```csharp
|
||
public class ErrorMessageService
|
||
{
|
||
private readonly Dictionary<string, string> _errorMessages = new()
|
||
{
|
||
["NETWORK_ERROR"] = "网络连接失败,请检查网络设置",
|
||
["SAVE_ERROR"] = "保存失败,请确保有足够的磁盘空间",
|
||
["INVALID_INPUT"] = "输入无效,请检查后重试",
|
||
["PERMISSION_DENIED"] = "权限不足,无法执行此操作",
|
||
["RESOURCE_NOT_FOUND"] = "资源不存在",
|
||
["TIMEOUT"] = "操作超时,请稍后重试"
|
||
};
|
||
|
||
public string GetUserFriendlyMessage(string errorCode)
|
||
{
|
||
return _errorMessages.TryGetValue(errorCode, out var message)
|
||
? message
|
||
: "发生未知错误,请联系技术支持";
|
||
}
|
||
|
||
public string GetUserFriendlyMessage(Exception ex)
|
||
{
|
||
return ex switch
|
||
{
|
||
GameException gameEx => GetUserFriendlyMessage(gameEx.ErrorCode),
|
||
FileNotFoundException => "文件不存在",
|
||
UnauthorizedAccessException => "权限不足",
|
||
TimeoutException => "操作超时",
|
||
_ => "发生未知错误"
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
### 错误提示 UI
|
||
|
||
```csharp
|
||
public class ErrorNotificationSystem : AbstractSystem
|
||
{
|
||
private readonly ErrorMessageService _messageService;
|
||
|
||
protected override void OnInit()
|
||
{
|
||
this.RegisterEvent<ErrorOccurredEvent>(OnErrorOccurred);
|
||
}
|
||
|
||
private void OnErrorOccurred(ErrorOccurredEvent e)
|
||
{
|
||
var userMessage = _messageService.GetUserFriendlyMessage(e.Exception);
|
||
|
||
// 根据错误严重程度显示不同的 UI
|
||
if (e.Exception is GameException gameEx)
|
||
{
|
||
switch (gameEx.ErrorCode)
|
||
{
|
||
case "NETWORK_ERROR":
|
||
ShowNetworkErrorDialog(userMessage);
|
||
break;
|
||
|
||
case "SAVE_ERROR":
|
||
ShowSaveErrorDialog(userMessage);
|
||
break;
|
||
|
||
default:
|
||
ShowGenericErrorToast(userMessage);
|
||
break;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ShowGenericErrorDialog(userMessage);
|
||
}
|
||
}
|
||
|
||
private void ShowNetworkErrorDialog(string message)
|
||
{
|
||
this.SendEvent(new ShowDialogEvent
|
||
{
|
||
Title = "网络错误",
|
||
Message = message,
|
||
Buttons = new[] { "重试", "取消" },
|
||
OnButtonClicked = buttonIndex =>
|
||
{
|
||
if (buttonIndex == 0) // 重试
|
||
{
|
||
this.SendEvent(new RetryNetworkOperationEvent());
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
private void ShowSaveErrorDialog(string message)
|
||
{
|
||
this.SendEvent(new ShowDialogEvent
|
||
{
|
||
Title = "保存失败",
|
||
Message = message,
|
||
Buttons = new[] { "确定" }
|
||
});
|
||
}
|
||
|
||
private void ShowGenericErrorToast(string message)
|
||
{
|
||
this.SendEvent(new ShowToastEvent
|
||
{
|
||
Message = message,
|
||
Duration = 3000
|
||
});
|
||
}
|
||
|
||
private void ShowGenericErrorDialog(string message)
|
||
{
|
||
this.SendEvent(new ShowDialogEvent
|
||
{
|
||
Title = "错误",
|
||
Message = message,
|
||
Buttons = new[] { "确定" }
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
## 错误恢复
|
||
|
||
### 重试机制
|
||
|
||
实现自动重试策略:
|
||
|
||
```csharp
|
||
public class RetryPolicy
|
||
{
|
||
public int MaxRetries { get; set; } = 3;
|
||
public TimeSpan InitialDelay { get; set; } = TimeSpan.FromSeconds(1);
|
||
public double BackoffMultiplier { get; set; } = 2.0;
|
||
|
||
public async Task<Result<T>> ExecuteAsync<T>(
|
||
Func<Task<T>> operation,
|
||
Func<Exception, bool> shouldRetry = null)
|
||
{
|
||
var delay = InitialDelay;
|
||
Exception lastException = null;
|
||
|
||
for (int attempt = 0; attempt <= MaxRetries; attempt++)
|
||
{
|
||
try
|
||
{
|
||
var result = await operation();
|
||
return Result<T>.Success(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
lastException = ex;
|
||
|
||
// 检查是否应该重试
|
||
if (shouldRetry != null && !shouldRetry(ex))
|
||
{
|
||
break;
|
||
}
|
||
|
||
// 最后一次尝试失败
|
||
if (attempt == MaxRetries)
|
||
{
|
||
break;
|
||
}
|
||
|
||
// 等待后重试
|
||
await Task.Delay(delay);
|
||
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * BackoffMultiplier);
|
||
}
|
||
}
|
||
|
||
return Result<T>.Failure(lastException);
|
||
}
|
||
}
|
||
|
||
// 使用示例
|
||
public class NetworkSystem : AbstractSystem
|
||
{
|
||
private readonly RetryPolicy _retryPolicy = new()
|
||
{
|
||
MaxRetries = 3,
|
||
InitialDelay = TimeSpan.FromSeconds(1),
|
||
BackoffMultiplier = 2.0
|
||
};
|
||
|
||
public async Task<Result<PlayerData>> FetchPlayerDataAsync(string playerId)
|
||
{
|
||
return await _retryPolicy.ExecuteAsync(
|
||
operation: async () =>
|
||
{
|
||
var response = await _httpClient.GetAsync($"/api/players/{playerId}");
|
||
response.EnsureSuccessStatusCode();
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
return JsonSerializer.Deserialize<PlayerData>(json);
|
||
},
|
||
shouldRetry: ex => ex is HttpRequestException || ex is TimeoutException
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 降级策略
|
||
|
||
提供多级降级方案:
|
||
|
||
```csharp
|
||
public class ConfigurationSystem : AbstractSystem
|
||
{
|
||
public async Task<GameConfig> LoadConfigAsync()
|
||
{
|
||
// 1. 尝试从远程服务器加载
|
||
var remoteResult = await TryLoadFromRemoteAsync();
|
||
if (remoteResult.IsSuccess)
|
||
{
|
||
_logger.Info("从远程服务器加载配置成功");
|
||
await CacheConfigAsync(remoteResult.Match(succ: c => c, fail: _ => null));
|
||
return remoteResult.Match(succ: c => c, fail: _ => null);
|
||
}
|
||
|
||
_logger.Warn($"远程加载失败: {remoteResult.Exception.Message}");
|
||
|
||
// 2. 尝试从本地缓存加载
|
||
var cacheResult = await TryLoadFromCacheAsync();
|
||
if (cacheResult.IsSuccess)
|
||
{
|
||
_logger.Info("从本地缓存加载配置成功");
|
||
return cacheResult.Match(succ: c => c, fail: _ => null);
|
||
}
|
||
|
||
_logger.Warn($"缓存加载失败: {cacheResult.Exception.Message}");
|
||
|
||
// 3. 使用内置默认配置
|
||
_logger.Error("所有配置源加载失败,使用默认配置");
|
||
return GetDefaultConfig();
|
||
}
|
||
|
||
private async Task<Result<GameConfig>> TryLoadFromRemoteAsync()
|
||
{
|
||
try
|
||
{
|
||
var response = await _httpClient.GetAsync("/api/config");
|
||
response.EnsureSuccessStatusCode();
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
var config = JsonSerializer.Deserialize<GameConfig>(json);
|
||
return Result<GameConfig>.Success(config);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return Result<GameConfig>.Failure(ex);
|
||
}
|
||
}
|
||
|
||
private async Task<Result<GameConfig>> TryLoadFromCacheAsync()
|
||
{
|
||
try
|
||
{
|
||
var json = await File.ReadAllTextAsync(_cachePath);
|
||
var config = JsonSerializer.Deserialize<GameConfig>(json);
|
||
return Result<GameConfig>.Success(config);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return Result<GameConfig>.Failure(ex);
|
||
}
|
||
}
|
||
|
||
private async Task CacheConfigAsync(GameConfig config)
|
||
{
|
||
try
|
||
{
|
||
var json = JsonSerializer.Serialize(config);
|
||
await File.WriteAllTextAsync(_cachePath, json);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Warn("缓存配置失败", ex);
|
||
}
|
||
}
|
||
|
||
private GameConfig GetDefaultConfig()
|
||
{
|
||
return new GameConfig
|
||
{
|
||
ServerUrl = "https://default.example.com",
|
||
Timeout = 30,
|
||
MaxRetries = 3
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
### 断路器模式
|
||
|
||
防止级联失败:
|
||
|
||
```csharp
|
||
public class CircuitBreaker
|
||
{
|
||
private int _failureCount = 0;
|
||
private DateTime _lastFailureTime = DateTime.MinValue;
|
||
private CircuitState _state = CircuitState.Closed;
|
||
|
||
public int FailureThreshold { get; set; } = 5;
|
||
public TimeSpan OpenDuration { get; set; } = TimeSpan.FromMinutes(1);
|
||
|
||
public async Task<Result<T>> ExecuteAsync<T>(Func<Task<T>> operation)
|
||
{
|
||
// 检查断路器状态
|
||
if (_state == CircuitState.Open)
|
||
{
|
||
if (DateTime.UtcNow - _lastFailureTime > OpenDuration)
|
||
{
|
||
_state = CircuitState.HalfOpen;
|
||
}
|
||
else
|
||
{
|
||
return Result<T>.Failure("断路器已打开,操作被拒绝");
|
||
}
|
||
}
|
||
|
||
try
|
||
{
|
||
var result = await operation();
|
||
|
||
// 成功,重置计数器
|
||
if (_state == CircuitState.HalfOpen)
|
||
{
|
||
_state = CircuitState.Closed;
|
||
}
|
||
_failureCount = 0;
|
||
|
||
return Result<T>.Success(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_failureCount++;
|
||
_lastFailureTime = DateTime.UtcNow;
|
||
|
||
// 达到阈值,打开断路器
|
||
if (_failureCount >= FailureThreshold)
|
||
{
|
||
_state = CircuitState.Open;
|
||
}
|
||
|
||
return Result<T>.Failure(ex);
|
||
}
|
||
}
|
||
|
||
private enum CircuitState
|
||
{
|
||
Closed, // 正常状态
|
||
Open, // 断路器打开,拒绝请求
|
||
HalfOpen // 半开状态,尝试恢复
|
||
}
|
||
}
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
### 1. 选择合适的错误处理方式
|
||
|
||
根据错误类型选择处理方式:
|
||
|
||
| 错误类型 | 处理方式 | 示例 |
|
||
|-----------|-----------------|-------------|
|
||
| 可预期的业务错误 | Result<T> | 用户输入验证、权限检查 |
|
||
| 可能不存在的值 | Option<T> | 查找操作、配置读取 |
|
||
| 不可预期的系统错误 | Exception | 文件 IO、网络错误 |
|
||
| 不可恢复的错误 | Exception + 日志 | 初始化失败、资源耗尽 |
|
||
|
||
### 2. 错误处理的层次结构
|
||
|
||
```csharp
|
||
// 底层:使用 Result 处理业务错误
|
||
public class InventoryRepository
|
||
{
|
||
public Result<Item> GetItem(string itemId)
|
||
{
|
||
var item = _items.FirstOrDefault(i => i.Id == itemId);
|
||
return item != null
|
||
? Result<Item>.Success(item)
|
||
: Result<Item>.Failure("物品不存在");
|
||
}
|
||
}
|
||
|
||
// 中层:组合多个操作
|
||
public class InventorySystem : AbstractSystem
|
||
{
|
||
public Result<Item> UseItem(string itemId)
|
||
{
|
||
return _repository.GetItem(itemId)
|
||
.Bind(item => ValidateItemUsage(item))
|
||
.Bind(item => ConsumeItem(item))
|
||
.OnSuccess(item => this.SendEvent(new ItemUsedEvent { Item = item }))
|
||
.OnFailure(ex => _logger.Warn($"使用物品失败: {ex.Message}"));
|
||
}
|
||
}
|
||
|
||
// 上层:处理用户交互
|
||
public class InventoryController : IController
|
||
{
|
||
public void OnUseItemButtonClicked(string itemId)
|
||
{
|
||
var result = _inventorySystem.UseItem(itemId);
|
||
|
||
result.Match(
|
||
succ: item => ShowSuccessMessage($"使用了 {item.Name}"),
|
||
fail: ex => ShowErrorMessage(ex.Message)
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 避免过度使用异常
|
||
|
||
```csharp
|
||
// ❌ 避免:用异常处理正常流程
|
||
public Player GetPlayerById(string playerId)
|
||
{
|
||
var player = _players.FirstOrDefault(p => p.Id == playerId);
|
||
if (player == null)
|
||
throw new PlayerNotFoundException(playerId); // 不应该用异常
|
||
|
||
return player;
|
||
}
|
||
|
||
// ✅ 好的做法:使用 Option
|
||
public Option<Player> GetPlayerById(string playerId)
|
||
{
|
||
var player = _players.FirstOrDefault(p => p.Id == playerId);
|
||
return player != null
|
||
? Option<Player>.Some(player)
|
||
: Option<Player>.None;
|
||
}
|
||
```
|
||
|
||
### 4. 提供有意义的错误消息
|
||
|
||
```csharp
|
||
// ❌ 避免:模糊的错误消息
|
||
return Result<Item>.Failure("错误");
|
||
return Result<Item>.Failure("操作失败");
|
||
|
||
// ✅ 好的做法:具体的错误消息
|
||
return Result<Item>.Failure("物品不存在");
|
||
return Result<Item>.Failure($"背包空间不足,需要 {required} 格,当前 {available} 格");
|
||
return Result<Item>.Failure($"物品 {itemName} 不可交易");
|
||
```
|
||
|
||
### 5. 记录错误上下文
|
||
|
||
```csharp
|
||
// ❌ 避免:缺少上下文
|
||
_logger.Error("保存失败", ex);
|
||
|
||
// ✅ 好的做法:包含完整上下文
|
||
_logger.Log(
|
||
LogLevel.Error,
|
||
"保存玩家数据失败",
|
||
ex,
|
||
("playerId", playerId),
|
||
("saveSlot", saveSlot),
|
||
("dataSize", data.Length),
|
||
("timestamp", DateTime.UtcNow)
|
||
);
|
||
```
|
||
|
||
### 6. 优雅降级
|
||
|
||
```csharp
|
||
// ✅ 好的做法:提供降级方案
|
||
public async Task<Texture> LoadTextureAsync(string path)
|
||
{
|
||
// 1. 尝试加载指定纹理
|
||
var result = await TryLoadTextureAsync(path);
|
||
if (result.IsSuccess)
|
||
return result.Match(succ: t => t, fail: _ => null);
|
||
|
||
// 2. 尝试加载备用纹理
|
||
_logger.Warn($"加载纹理失败: {path},使用备用纹理");
|
||
var fallbackResult = await TryLoadTextureAsync(_fallbackTexturePath);
|
||
if (fallbackResult.IsSuccess)
|
||
return fallbackResult.Match(succ: t => t, fail: _ => null);
|
||
|
||
// 3. 使用默认纹理
|
||
_logger.Error("所有纹理加载失败,使用默认纹理");
|
||
return _defaultTexture;
|
||
}
|
||
```
|
||
|
||
### 7. 测试错误处理
|
||
|
||
```csharp
|
||
[TestFixture]
|
||
public class InventorySystemTests
|
||
{
|
||
[Test]
|
||
public void UseItem_ItemNotFound_ShouldReturnFailure()
|
||
{
|
||
// Arrange
|
||
var system = new InventorySystem();
|
||
var invalidItemId = "invalid_item";
|
||
|
||
// Act
|
||
var result = system.UseItem(invalidItemId);
|
||
|
||
// Assert
|
||
Assert.That(result.IsFaulted, Is.True);
|
||
Assert.That(result.Exception.Message, Contains.Substring("物品不存在"));
|
||
}
|
||
|
||
[Test]
|
||
public void UseItem_ItemNotUsable_ShouldReturnFailure()
|
||
{
|
||
// Arrange
|
||
var system = new InventorySystem();
|
||
var item = new Item { Id = "item1", IsUsable = false };
|
||
system.AddItem(item);
|
||
|
||
// Act
|
||
var result = system.UseItem("item1");
|
||
|
||
// Assert
|
||
Assert.That(result.IsFaulted, Is.True);
|
||
Assert.That(result.Exception.Message, Contains.Substring("不可使用"));
|
||
}
|
||
|
||
[Test]
|
||
public void UseItem_ValidItem_ShouldReturnSuccess()
|
||
{
|
||
// Arrange
|
||
var system = new InventorySystem();
|
||
var item = new Item { Id = "item1", IsUsable = true };
|
||
system.AddItem(item);
|
||
|
||
// Act
|
||
var result = system.UseItem("item1");
|
||
|
||
// Assert
|
||
Assert.That(result.IsSuccess, Is.True);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 常见问题
|
||
|
||
### Q1: 何时使用 Result,何时使用 Option?
|
||
|
||
**A:**
|
||
|
||
- 使用 **Result<T>** 当操作可能失败,需要返回错误信息时
|
||
- 使用 **Option<T>** 当值可能不存在,但不需要错误信息时
|
||
|
||
```csharp
|
||
// 使用 Result:需要知道为什么失败
|
||
public Result<User> RegisterUser(string username, string password)
|
||
{
|
||
if (string.IsNullOrEmpty(username))
|
||
return Result<User>.Failure("用户名不能为空");
|
||
|
||
if (password.Length < 8)
|
||
return Result<User>.Failure("密码长度至少为 8 个字符");
|
||
|
||
// ...
|
||
}
|
||
|
||
// 使用 Option:只需要知道是否存在
|
||
public Option<User> FindUserById(string userId)
|
||
{
|
||
var user = _users.FirstOrDefault(u => u.Id == userId);
|
||
return user != null ? Option<User>.Some(user) : Option<User>.None;
|
||
}
|
||
```
|
||
|
||
### Q2: 如何处理异步操作中的错误?
|
||
|
||
**A:** 使用 Result 的异步扩展方法:
|
||
|
||
```csharp
|
||
public async Task<Result<PlayerData>> LoadPlayerDataAsync(string playerId)
|
||
{
|
||
return await ResultExtensions.TryAsync(async () =>
|
||
{
|
||
var data = await _httpClient.GetStringAsync($"/api/players/{playerId}");
|
||
return JsonSerializer.Deserialize<PlayerData>(data);
|
||
});
|
||
}
|
||
|
||
// 链式异步操作
|
||
public async Task<Result<Player>> LoadAndValidatePlayerAsync(string playerId)
|
||
{
|
||
var result = await LoadPlayerDataAsync(playerId);
|
||
return await result.BindAsync(async data =>
|
||
{
|
||
var isValid = await ValidatePlayerDataAsync(data);
|
||
return isValid
|
||
? Result<Player>.Success(CreatePlayer(data))
|
||
: Result<Player>.Failure("玩家数据验证失败");
|
||
});
|
||
}
|
||
```
|
||
|
||
### Q3: 如何在 Command 和 Query 中处理错误?
|
||
|
||
**A:** Command 和 Query 可以返回 Result:
|
||
|
||
```csharp
|
||
// Command 返回 Result
|
||
public class SaveGameCommand : AbstractCommand<Result<SaveData>>
|
||
{
|
||
protected override Result<SaveData> OnDo()
|
||
{
|
||
try
|
||
{
|
||
var data = CollectSaveData();
|
||
var saveSystem = this.GetSystem<SaveSystem>();
|
||
return saveSystem.SaveGame(data);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
this.GetUtility<ILogger>().Error("保存游戏失败", ex);
|
||
return Result<SaveData>.Failure(ex);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Query 返回 Option
|
||
public class GetPlayerQuery : AbstractQuery<Option<Player>>
|
||
{
|
||
public string PlayerId { get; set; }
|
||
|
||
protected override Option<Player> OnDo()
|
||
{
|
||
var playerSystem = this.GetSystem<PlayerSystem>();
|
||
return playerSystem.FindPlayerById(PlayerId);
|
||
}
|
||
}
|
||
```
|
||
|
||
### Q4: 如何处理多个可能失败的操作?
|
||
|
||
**A:** 使用 Result 的链式操作:
|
||
|
||
```csharp
|
||
public Result<Trade> ExecuteTrade(string sellerId, string buyerId, string itemId, int price)
|
||
{
|
||
return ValidateSeller(sellerId)
|
||
.Bind(_ => ValidateBuyer(buyerId))
|
||
.Bind(_ => ValidateItem(itemId))
|
||
.Bind(_ => ValidatePrice(price))
|
||
.Bind(_ => TransferItem(sellerId, buyerId, itemId))
|
||
.Bind(_ => TransferCurrency(buyerId, sellerId, price))
|
||
.Map(_ => CreateTradeRecord(sellerId, buyerId, itemId, price));
|
||
}
|
||
```
|
||
|
||
### Q5: 如何避免 Result 嵌套过深?
|
||
|
||
**A:** 使用 Bind 扁平化嵌套:
|
||
|
||
```csharp
|
||
// ❌ 避免:嵌套过深
|
||
public Result<string> ProcessData(string input)
|
||
{
|
||
var result1 = Step1(input);
|
||
if (result1.IsSuccess)
|
||
{
|
||
var result2 = Step2(result1.Match(succ: v => v, fail: _ => ""));
|
||
if (result2.IsSuccess)
|
||
{
|
||
var result3 = Step3(result2.Match(succ: v => v, fail: _ => ""));
|
||
return result3;
|
||
}
|
||
return Result<string>.Failure(result2.Exception);
|
||
}
|
||
return Result<string>.Failure(result1.Exception);
|
||
}
|
||
|
||
// ✅ 好的做法:使用 Bind 扁平化
|
||
public Result<string> ProcessData(string input)
|
||
{
|
||
return Step1(input)
|
||
.Bind(Step2)
|
||
.Bind(Step3);
|
||
}
|
||
```
|
||
|
||
### Q6: 如何在 UI 层处理错误?
|
||
|
||
**A:** 将错误转换为用户友好的消息:
|
||
|
||
```csharp
|
||
public class UIController : IController
|
||
{
|
||
private readonly ErrorMessageService _errorMessageService;
|
||
|
||
public void OnSaveButtonClicked()
|
||
{
|
||
var result = this.SendCommand(new SaveGameCommand());
|
||
|
||
result.Match(
|
||
succ: data => {
|
||
ShowSuccessToast("游戏保存成功");
|
||
},
|
||
fail: ex => {
|
||
var userMessage = _errorMessageService.GetUserFriendlyMessage(ex);
|
||
ShowErrorDialog("保存失败", userMessage);
|
||
}
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
良好的错误处理是构建健壮应用的基础。遵循本指南的最佳实践:
|
||
|
||
- ✅ 使用 **Result<T>** 处理可预期的业务错误
|
||
- ✅ 使用 **Option<T>** 处理可能不存在的值
|
||
- ✅ 使用 **异常** 处理不可预期的系统错误
|
||
- ✅ 记录详细的 **日志** 信息
|
||
- ✅ 提供友好的 **用户反馈**
|
||
- ✅ 实现 **错误恢复** 和降级策略
|
||
- ✅ 编写 **测试** 验证错误处理逻辑
|
||
|
||
通过这些实践,你将构建出更加稳定、可维护、用户友好的游戏应用。
|
||
|
||
---
|
||
|
||
**相关文档**:
|
||
|
||
- [架构模式最佳实践](./architecture-patterns.md)
|
||
- [扩展方法使用指南](../core/extensions.md)
|
||
- [日志系统](../core/logging.md)
|
||
|
||
**文档版本**: 1.0.0
|
||
**更新日期**: 2026-03-07
|