- 添加完整的错误处理最佳实践指南,涵盖Result<T>和Option<T>使用方式 - 补充移动端性能优化策略,包括纹理压缩、对象池、内存管理和电池优化 - 更新单元测试教程中的相关文档链接 - 完善错误处理层次结构和测试示例代码 - 增加移动端UI优化和平台适配最佳实践
45 KiB
错误处理最佳实践
本指南介绍 GFramework 中的错误处理模式和最佳实践,帮助你构建健壮、可维护的游戏应用。
📋 目录
概述
为什么错误处理很重要
良好的错误处理是构建健壮应用的基础:
- 提高稳定性:优雅地处理异常情况,避免崩溃
- 改善用户体验:提供清晰的错误提示和恢复选项
- 简化调试:记录详细的错误信息,快速定位问题
- 增强可维护性:使用类型安全的错误处理模式
GFramework 的错误处理策略
GFramework 提供多种错误处理机制:
- Result<T> 模式:函数式错误处理,类型安全
- Option<T> 模式:处理可空值,避免 null 引用
- 异常处理:处理不可恢复的错误
- 日志系统:记录错误信息和调试数据
核心概念
错误类型
根据错误的性质和处理方式,可以分为以下几类:
1. 可预期的错误
这类错误是业务逻辑的一部分,应该使用 Result 或 Option 模式处理:
// 示例:用户输入验证
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. 不可预期的错误
这类错误通常是程序错误或系统故障,应该使用异常处理:
// 示例:文件系统错误
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 模式处理可能不存在的值:
// 示例:查找玩家
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 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("添加物品失败");
}
}
}
错误恢复
设计错误恢复策略,提供降级方案:
// 示例:多级降级策略
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> 是一个函数式类型,表示操作可能成功(包含值)或失败(包含异常):
// 创建成功结果
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 支持函数式编程的链式操作:
// 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:用户注册
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:物品交易
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 支持异步操作:
// 异步 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
// 组合多个结果,全部成功才返回成功
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 引用异常:
// 创建 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("值不存在");
}
常用操作
// 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:查找玩家
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:配置管理
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:物品查找
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 的转换
// 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;
异常处理
何时使用异常
异常应该用于处理不可预期的错误和不可恢复的错误:
// ✅ 适合使用异常的场景
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;
}
}
自定义异常
创建有意义的异常类型:
// 基础游戏异常
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. 捕获具体的异常类型
// ✅ 好的做法
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. 不要吞掉异常
// ❌ 避免:吞掉异常
try
{
RiskyOperation();
}
catch (Exception ex)
{
// 什么都不做,异常被吞掉了
}
// ✅ 好的做法:记录并重新抛出或处理
try
{
RiskyOperation();
}
catch (Exception ex)
{
_logger.Error("操作失败", ex);
throw; // 重新抛出原始异常
}
3. 使用 finally 清理资源
// ✅ 好的做法
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 提供多个日志级别,根据错误严重程度选择合适的级别:
// Trace:详细的调试信息
_logger.Trace("进入 ProcessInput 方法");
// Debug:调试信息
_logger.Debug($"玩家位置: {player.Position}");
// Info:一般信息
_logger.Info("游戏启动成功");
// Warning:警告信息
_logger.Warn("配置文件不存在,使用默认配置");
// Error:错误信息
_logger.Error("保存游戏失败", exception);
// Fatal:致命错误
_logger.Fatal("无法初始化渲染系统", exception);
结构化日志
使用结构化日志记录上下文信息:
// 记录带上下文的日志
_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. 记录足够的上下文
// ❌ 避免:信息不足
_logger.Error("操作失败");
// ✅ 好的做法:包含上下文
_logger.Error(
$"保存玩家数据失败: PlayerId={playerId}, Reason={ex.Message}",
ex
);
2. 不要记录敏感信息
// ❌ 避免:记录密码等敏感信息
_logger.Info($"用户登录: {username}, 密码: {password}");
// ✅ 好的做法:不记录敏感信息
_logger.Info($"用户登录: {username}");
3. 使用合适的日志级别
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);
}
}
}
用户反馈
友好的错误提示
将技术错误转换为用户友好的消息:
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
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[] { "确定" }
});
}
}
错误恢复
重试机制
实现自动重试策略:
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
);
}
}
降级策略
提供多级降级方案:
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
};
}
}
断路器模式
防止级联失败:
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. 错误处理的层次结构
// 底层:使用 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. 避免过度使用异常
// ❌ 避免:用异常处理正常流程
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. 提供有意义的错误消息
// ❌ 避免:模糊的错误消息
return Result<Item>.Failure("错误");
return Result<Item>.Failure("操作失败");
// ✅ 好的做法:具体的错误消息
return Result<Item>.Failure("物品不存在");
return Result<Item>.Failure($"背包空间不足,需要 {required} 格,当前 {available} 格");
return Result<Item>.Failure($"物品 {itemName} 不可交易");
5. 记录错误上下文
// ❌ 避免:缺少上下文
_logger.Error("保存失败", ex);
// ✅ 好的做法:包含完整上下文
_logger.Log(
LogLevel.Error,
"保存玩家数据失败",
ex,
("playerId", playerId),
("saveSlot", saveSlot),
("dataSize", data.Length),
("timestamp", DateTime.UtcNow)
);
6. 优雅降级
// ✅ 好的做法:提供降级方案
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. 测试错误处理
[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> 当值可能不存在,但不需要错误信息时
// 使用 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 的异步扩展方法:
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:
// 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 的链式操作:
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 扁平化嵌套:
// ❌ 避免:嵌套过深
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: 将错误转换为用户友好的消息:
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> 处理可能不存在的值
- ✅ 使用 异常 处理不可预期的系统错误
- ✅ 记录详细的 日志 信息
- ✅ 提供友好的 用户反馈
- ✅ 实现 错误恢复 和降级策略
- ✅ 编写 测试 验证错误处理逻辑
通过这些实践,你将构建出更加稳定、可维护、用户友好的游戏应用。
相关文档:
文档版本: 1.0.0 更新日期: 2026-03-07