mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-24 12:33:30 +08:00
- 将所有小写的命名空间导入更正为首字母大写格式 - 统一 GFramework 框架的命名空间引用规范 - 修复 core、ecs、godot 等模块的命名空间导入错误 - 标准化文档示例代码中的 using 语句格式 - 确保所有文档中的命名空间引用保持一致性 - 更新 global using 语句以匹配正确的命名空间格式
736 lines
19 KiB
Markdown
736 lines
19 KiB
Markdown
---
|
||
title: 存储系统详解
|
||
description: 存储系统提供了灵活的文件存储和作用域隔离功能,支持跨平台数据持久化。
|
||
---
|
||
|
||
# 存储系统详解
|
||
|
||
## 概述
|
||
|
||
存储系统是 GFramework.Game 中用于管理文件存储的核心组件。它提供了统一的存储接口,支持键值对存储、作用域隔离、目录操作等功能,让你可以轻松实现游戏数据的持久化。
|
||
|
||
存储系统采用装饰器模式设计,通过 `IStorage` 接口定义统一的存储操作,`FileStorage` 提供基于文件系统的实现,`ScopedStorage`
|
||
提供作用域隔离功能。
|
||
|
||
**主要特性**:
|
||
|
||
- 统一的键值对存储接口
|
||
- 基于文件系统的持久化
|
||
- 作用域隔离和命名空间管理
|
||
- 线程安全的并发访问
|
||
- 支持同步和异步操作
|
||
- 目录和文件列举功能
|
||
- 路径安全防护
|
||
- 跨平台支持(包括 Godot)
|
||
|
||
## 核心概念
|
||
|
||
### 存储接口
|
||
|
||
`IStorage` 定义了统一的存储操作:
|
||
|
||
```csharp
|
||
public interface IStorage : IUtility
|
||
{
|
||
// 检查键是否存在
|
||
bool Exists(string key);
|
||
Task<bool> ExistsAsync(string key);
|
||
|
||
// 读取数据
|
||
T Read<T>(string key);
|
||
T Read<T>(string key, T defaultValue);
|
||
Task<T> ReadAsync<T>(string key);
|
||
|
||
// 写入数据
|
||
void Write<T>(string key, T value);
|
||
Task WriteAsync<T>(string key, T value);
|
||
|
||
// 删除数据
|
||
void Delete(string key);
|
||
Task DeleteAsync(string key);
|
||
|
||
// 目录操作
|
||
Task<IReadOnlyList<string>> ListDirectoriesAsync(string path = "");
|
||
Task<IReadOnlyList<string>> ListFilesAsync(string path = "");
|
||
Task<bool> DirectoryExistsAsync(string path);
|
||
Task CreateDirectoryAsync(string path);
|
||
}
|
||
```
|
||
|
||
### 文件存储
|
||
|
||
`FileStorage` 是基于文件系统的存储实现:
|
||
|
||
- 将数据序列化后保存为文件
|
||
- 支持自定义文件扩展名(默认 `.dat`)
|
||
- 使用细粒度锁保证线程安全
|
||
- 自动创建目录结构
|
||
- 防止路径遍历攻击
|
||
|
||
### 作用域存储
|
||
|
||
`ScopedStorage` 提供命名空间隔离:
|
||
|
||
- 为所有键添加前缀
|
||
- 支持嵌套作用域
|
||
- 透明包装底层存储
|
||
- 实现逻辑分组
|
||
|
||
### 存储类型
|
||
|
||
`StorageKinds` 枚举定义了不同的存储方式:
|
||
|
||
```csharp
|
||
[Flags]
|
||
public enum StorageKinds
|
||
{
|
||
None = 0,
|
||
Local = 1 << 0, // 本地文件系统
|
||
Memory = 1 << 1, // 内存存储
|
||
Remote = 1 << 2, // 远程存储
|
||
Database = 1 << 3 // 数据库存储
|
||
}
|
||
```
|
||
|
||
## 基本用法
|
||
|
||
### 创建文件存储
|
||
|
||
```csharp
|
||
using GFramework.Game.Storage;
|
||
using GFramework.Game.Serializer;
|
||
|
||
// 创建序列化器
|
||
var serializer = new JsonSerializer();
|
||
|
||
// 创Windows 示例)
|
||
var storage = new FileStorage(@"C:\MyGame\Data", serializer);
|
||
|
||
// 或使用自定义扩展名
|
||
var storage = new FileStorage(@"C:\MyGame\Data", serializer, ".json");
|
||
```
|
||
|
||
### 写入和读取数据
|
||
|
||
```csharp
|
||
// 写入简单类型
|
||
storage.Write("player_score", 1000);
|
||
storage.Write("player_name", "Alice");
|
||
|
||
// 写入复杂对象
|
||
var settings = new GameSettings
|
||
{
|
||
Volume = 0.8f,
|
||
Difficulty = "Hard",
|
||
Language = "zh-CN"
|
||
};
|
||
storage.Write("settings", settings);
|
||
|
||
// 读取数据
|
||
int score = storage.Read<int>("player_score");
|
||
string name = storage.Read<string>("player_name");
|
||
var loadedSettings = storage.Read<GameSettings>("settings");
|
||
|
||
// 读取数据(带默认值)
|
||
int highScore = storage.Read("high_score", 0);
|
||
```
|
||
|
||
### 异步操作
|
||
|
||
```csharp
|
||
// 异步写入
|
||
await storage.WriteAsync("player_level", 10);
|
||
|
||
// 异步读取
|
||
int level = await storage.ReadAsync<int>("player_level");
|
||
|
||
// 异步检查存在
|
||
bool exists = await storage.ExistsAsync("player_level");
|
||
|
||
// 异步删除
|
||
await storage.DeleteAsync("player_level");
|
||
```
|
||
|
||
### 检查和删除
|
||
|
||
```csharp
|
||
// 检查键是否存在
|
||
if (storage.Exists("player_score"))
|
||
{
|
||
Console.WriteLine("存档存在");
|
||
}
|
||
|
||
// 删除数据
|
||
storage.Delete("player_score");
|
||
|
||
// 异步检查
|
||
bool exists = await storage.ExistsAsync("player_score");
|
||
```
|
||
|
||
### 使用层级键
|
||
|
||
```csharp
|
||
// 使用 / 分隔符创建层级结构
|
||
storage.Write("player/profile/name", "Alice");
|
||
storage.Write("player/profile/level", 10);
|
||
storage.Write("player/inventory/gold", 1000);
|
||
|
||
// 文件结构:
|
||
// Data/
|
||
// player/
|
||
// profile/
|
||
// name.dat
|
||
// level.dat
|
||
// inventory/
|
||
// gold.dat
|
||
|
||
// 读取层级数据
|
||
string name = storage.Read<string>("player/profile/name");
|
||
int gold = storage.Read<int>("player/inventory/gold");
|
||
```
|
||
|
||
## 作用域存储
|
||
|
||
### 创建作用域存储
|
||
|
||
```csharp
|
||
using GFramework.Game.Storage;
|
||
|
||
// 基于文件存储创建作用域存储
|
||
var baseStorage = new FileStorage(@"C:\MyGame\Data", serializer);
|
||
var playerStorage = new ScopedStorage(baseStorage, "player");
|
||
|
||
// 所有操作都会添加 "player/" 前缀
|
||
playerStorage.Write("name", "Alice"); // 实际存储为 "player/name.dat"
|
||
playerStorage.Write("level", 10); // 实际存储为 "player/level.dat"
|
||
|
||
// 读取时也使用相同的前缀
|
||
string name = playerStorage.Read<string>("name"); // 从 "player/name.dat" 读取
|
||
```
|
||
|
||
### 嵌套作用域
|
||
|
||
```csharp
|
||
// 创建嵌套作用域
|
||
var settingsStorage = new ScopedStorage(baseStorage, "settings");
|
||
var graphicsStorage = new ScopedStorage(settingsStorage, "graphics");
|
||
|
||
// 前缀变为 "settings/graphics/"
|
||
graphicsStorage.Write("resolution", "1920x1080");
|
||
// 实际存储为 "settings/graphics/resolution.dat"
|
||
|
||
// 或使用 Scope 方法
|
||
var audioStorage = settingsStorage.Scope("audio");
|
||
audioStorage.Write("volume", 0.8f);
|
||
// 实际存储为 "settings/audio/volume.dat"
|
||
```
|
||
|
||
### 多作用域隔离
|
||
|
||
```csharp
|
||
// 创建不同作用域的存储
|
||
var playerStorage = new ScopedStorage(baseStorage, "player");
|
||
var gameStorage = new ScopedStorage(baseStorage, "game");
|
||
var settingsStorage = new ScopedStorage(baseStorage, "settings");
|
||
|
||
// 在不同作用域中使用相同的键不会冲突
|
||
playerStorage.Write("level", 5); // player/level.dat
|
||
gameStorage.Write("level", "forest_area_1"); // game/level.dat
|
||
settingsStorage.Write("level", "high"); // settings/level.dat
|
||
|
||
// 读取时各自独立
|
||
int playerLevel = playerStorage.Read<int>("level"); // 5
|
||
string gameLevel = gameStorage.Read<string>("level"); // "forest_area_1"
|
||
string settingsLevel = settingsStorage.Read<string>("level"); // "high"
|
||
```
|
||
|
||
## 高级用法
|
||
|
||
### 目录操作
|
||
|
||
```csharp
|
||
// 列举子目录
|
||
var directories = await storage.ListDirectoriesAsync("player");
|
||
foreach (var dir in directories)
|
||
{
|
||
Console.WriteLine($"目录: {dir}");
|
||
}
|
||
|
||
// 列举文件
|
||
var files = await storage.ListFilesAsync("player/inventory");
|
||
foreach (var file in files)
|
||
{
|
||
Console.WriteLine($"文件: {file}");
|
||
}
|
||
|
||
// 检查目录是否存在
|
||
bool exists = await storage.DirectoryExistsAsync("player/quests");
|
||
|
||
// 创建目录
|
||
await storage.CreateDirectoryAsync("player/achievements");
|
||
```
|
||
|
||
### 批量操作
|
||
|
||
```csharp
|
||
public async Task SaveAllPlayerData(PlayerData player)
|
||
{
|
||
var playerStorage = new ScopedStorage(baseStorage, $"player_{player.Id}");
|
||
|
||
// 批量写入
|
||
var tasks = new List<Task>
|
||
{
|
||
playerStorage.WriteAsync("profile", player.Profile),
|
||
playerStorage.WriteAsync("inventory", player.Inventory),
|
||
playerStorage.WriteAsync("quests", player.Quests),
|
||
playerStorage.WriteAsync("achievements", player.Achievements)
|
||
};
|
||
|
||
await Task.WhenAll(tasks);
|
||
Console.WriteLine("所有玩家数据已保存");
|
||
}
|
||
|
||
public async Task<PlayerData> LoadAllPlayerData(int playerId)
|
||
{
|
||
var playerStorage = new ScopedStorage(baseStorage, $"player_{playerId}");
|
||
|
||
// 批量读取
|
||
var tasks = new[]
|
||
{
|
||
playerStorage.ReadAsync<Profile>("profile"),
|
||
playerStorage.ReadAsync<Inventory>("inventory"),
|
||
playerStorage.ReadAsync<QuestData>("quests"),
|
||
playerStorage.ReadAsync<Achievements>("achievements")
|
||
};
|
||
|
||
await Task.WhenAll(tasks);
|
||
|
||
return new PlayerData
|
||
{
|
||
Id = playerId,
|
||
Profile = tasks[0].Result,
|
||
Inventory = tasks[1].Result,
|
||
Quests = tasks[2].Result,
|
||
Achievements = tasks[3].Result
|
||
};
|
||
}
|
||
```
|
||
|
||
### 存储迁移
|
||
|
||
```csharp
|
||
public async Task MigrateStorage(IStorage oldStorage, IStorage newStorage, string path = "")
|
||
{
|
||
// 列举所有文件
|
||
var files = await oldStorage.ListFilesAsync(path);
|
||
|
||
foreach (var file in files)
|
||
{
|
||
var key = string.IsNullOrEmpty(path) ? file : $"{path}/{file}";
|
||
|
||
// 读取旧数据
|
||
var data = await oldStorage.ReadAsync<object>(key);
|
||
|
||
// 写入新存储
|
||
await newStorage.WriteAsync(key, data);
|
||
|
||
Console.WriteLine($"已迁移: {key}");
|
||
}
|
||
|
||
// 递归处理子目录
|
||
var directories = await oldStorage.ListDirectoriesAsync(path);
|
||
foreach (var dir in directories)
|
||
{
|
||
var subPath = string.IsNullOrEmpty(path) ? dir : $"{path}/{dir}";
|
||
await MigrateStorage(oldStorage, newStorage, subPath);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 存储备份
|
||
|
||
```csharp
|
||
public class StorageBackupSystem
|
||
{
|
||
private readonly IStorage _storage;
|
||
private readonly string _backupPrefix = "backup";
|
||
|
||
public async Task CreateBackup(string sourcePath)
|
||
{
|
||
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||
var backupPath = $"{_backupPrefix}/{timestamp}";
|
||
|
||
await CopyDirectory(sourcePath, backupPath);
|
||
Console.WriteLine($"备份已创建: {backupPath}");
|
||
}
|
||
|
||
public async Task RestoreBackup(string backupName, string targetPath)
|
||
{
|
||
var backupPath = $"{_backupPrefix}/{backupName}";
|
||
|
||
if (!await _storage.DirectoryExistsAsync(backupPath))
|
||
{
|
||
throw new DirectoryNotFoundException($"备份不存在: {backupName}");
|
||
}
|
||
|
||
await CopyDirectory(backupPath, targetPath);
|
||
Console.WriteLine($"已从备份恢复: {backupName}");
|
||
}
|
||
|
||
private async Task CopyDirectory(string source, string target)
|
||
{
|
||
var files = await _storage.ListFilesAsync(source);
|
||
foreach (var file in files)
|
||
{
|
||
var sourceKey = $"{source}/{file}";
|
||
var targetKey = $"{target}/{file}";
|
||
var data = await _storage.ReadAsync<object>(sourceKey);
|
||
await _storage.WriteAsync(targetKey, data);
|
||
}
|
||
|
||
var directories = await _storage.ListDirectoriesAsync(source);
|
||
foreach (var dir in directories)
|
||
{
|
||
await CopyDirectory($"{source}/{dir}", $"{target}/{dir}");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 缓存层
|
||
|
||
```csharp
|
||
public class CachedStorage : IStorage
|
||
{
|
||
private readonly IStorage _innerStorage;
|
||
private readonly ConcurrentDictionary<string, object> _cache = new();
|
||
|
||
public CachedStorage(IStorage innerStorage)
|
||
{
|
||
_innerStorage = innerStorage;
|
||
}
|
||
|
||
public T Read<T>(string key)
|
||
{
|
||
// 先从缓存读取
|
||
if (_cache.TryGetValue(key, out var cached))
|
||
{
|
||
return (T)cached;
|
||
}
|
||
|
||
// 从存储读取并缓存
|
||
var value = _innerStorage.Read<T>(key);
|
||
_cache[key] = value;
|
||
return value;
|
||
}
|
||
|
||
public void Write<T>(string key, T value)
|
||
{
|
||
// 写入存储
|
||
_innerStorage.Write(key, value);
|
||
|
||
// 更新缓存
|
||
_cache[key] = value;
|
||
}
|
||
|
||
public void Delete(string key)
|
||
{
|
||
_innerStorage.Delete(key);
|
||
_cache.TryRemove(key, out _);
|
||
}
|
||
|
||
public void ClearCache()
|
||
{
|
||
_cache.Clear();
|
||
}
|
||
}
|
||
```
|
||
|
||
## Godot 集成
|
||
|
||
### 使用 Godot 文件存储
|
||
|
||
```csharp
|
||
using GFramework.Godot.Storage;
|
||
|
||
// 创建 Godot 文件存储
|
||
var storage = new GodotFileStorage(serializer);
|
||
|
||
// 使用 user:// 路径(用户数据目录)
|
||
storage.Write("user://saves/slot1.dat", saveData);
|
||
var data = storage.Read<SaveData>("user://saves/slot1.dat");
|
||
|
||
// 使用 res:// 路径(资源目录,只读)
|
||
var config = storage.Read<Config>("res://config/default.json");
|
||
|
||
// 普通文件路径也支持
|
||
storage.Write("/tmp/temp_data.dat", tempData);
|
||
```
|
||
|
||
### Godot 路径说明
|
||
|
||
```csharp
|
||
// user:// - 用户数据目录
|
||
// Windows: %APPDATA%/Godot/app_userdata/[project_name]
|
||
// Linux: ~/.local/share/godot/app_userdata/[project_name]
|
||
// macOS: ~/Library/Application Support/Godot/app_userdata/[project_name]
|
||
storage.Write("user://save.dat", data);
|
||
|
||
// res:// - 项目资源目录(只读)
|
||
var config = storage.Read<Config>("res://data/config.json");
|
||
|
||
// 绝对路径
|
||
storage.Write("/home/user/game/data.dat", data);
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
1. **使用作用域隔离不同类型的数据**
|
||
```csharp
|
||
✓ var playerStorage = new ScopedStorage(baseStorage, "player");
|
||
✓ var settingsStorage = new ScopedStorage(baseStorage, "settings");
|
||
✗ storage.Write("player_name", name); // 不使用作用域
|
||
```
|
||
|
||
2. **使用异步操作避免阻塞**
|
||
```csharp
|
||
✓ await storage.WriteAsync("data", value);
|
||
✗ storage.Write("data", value); // 在 UI 线程中同步操作
|
||
```
|
||
|
||
3. **读取时提供默认值**
|
||
```csharp
|
||
✓ int score = storage.Read("score", 0);
|
||
✗ int score = storage.Read<int>("score"); // 键不存在时抛异常
|
||
```
|
||
|
||
4. **使用层级键组织数据**
|
||
```csharp
|
||
✓ storage.Write("player/inventory/gold", 1000);
|
||
✗ storage.Write("player_inventory_gold", 1000);
|
||
```
|
||
|
||
5. **处理存储异常**
|
||
```csharp
|
||
try
|
||
{
|
||
await storage.WriteAsync("data", value);
|
||
}
|
||
catch (IOException ex)
|
||
{
|
||
Logger.Error($"存储失败: {ex.Message}");
|
||
ShowErrorMessage("保存失败,请检查磁盘空间");
|
||
}
|
||
```
|
||
|
||
6. **定期清理过期数据**
|
||
```csharp
|
||
public async Task CleanupOldData(TimeSpan maxAge)
|
||
{
|
||
var files = await storage.ListFilesAsync("temp");
|
||
foreach (var file in files)
|
||
{
|
||
var data = await storage.ReadAsync<TimestampedData>($"temp/{file}");
|
||
if (DateTime.Now - data.Timestamp > maxAge)
|
||
{
|
||
await storage.DeleteAsync($"temp/{file}");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
7. **使用合适的序列化器**
|
||
```csharp
|
||
// JSON - 可读性好,适合配置文件
|
||
var jsonStorage = new FileStorage(path, new JsonSerializer(), ".json");
|
||
|
||
// 二进制 - 性能好,适合大量数据
|
||
var binaryStorage = new FileStorage(path, new BinarySerializer(), ".dat");
|
||
```
|
||
|
||
## 常见问题
|
||
|
||
### 问题:如何实现跨平台存储路径?
|
||
|
||
**解答**:
|
||
使用 `Environment.GetFolderPath` 获取平台特定路径:
|
||
|
||
```csharp
|
||
public static string GetStoragePath()
|
||
{
|
||
var appData = Environment.GetFolderPath(
|
||
Environment.SpecialFolder.ApplicationData);
|
||
return Path.Combine(appData, "MyGame", "Data");
|
||
}
|
||
|
||
var storage = new FileStorage(GetStoragePath(), serializer);
|
||
```
|
||
|
||
### 问题:存储系统是否线程安全?
|
||
|
||
**解答**:
|
||
是的,`FileStorage` 使用细粒度锁机制保证线程安全:
|
||
|
||
```csharp
|
||
// 不同键的操作可以并发执行
|
||
Task.Run(() => storage.Write("key1", value1));
|
||
Task.Run(() => storage.Write("key2", value2));
|
||
|
||
// 相同键的操作会串行化
|
||
Task.Run(() => storage.Write("key", value1));
|
||
Task.Run(() => storage.Write("key", value2)); // 等待第一个完成
|
||
```
|
||
|
||
### 问题:如何实现存储加密?
|
||
|
||
**解答**:
|
||
创建加密存储包装器:
|
||
|
||
```csharp
|
||
public class EncryptedStorage : IStorage
|
||
{
|
||
private readonly IStorage _innerStorage;
|
||
private readonly IEncryption _encryption;
|
||
|
||
public void Write<T>(string key, T value)
|
||
{
|
||
var json = JsonSerializer.Serialize(value);
|
||
var encrypted = _encryption.Encrypt(json);
|
||
_innerStorage.Write(key, encrypted);
|
||
}
|
||
|
||
public T Read<T>(string key)
|
||
{
|
||
var encrypted = _innerStorage.Read<byte[]>(key);
|
||
var json = _encryption.Decrypt(encrypted);
|
||
return JsonSerializer.Deserialize<T>(json);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 问题:如何限制存储大小?
|
||
|
||
**解答**:
|
||
实现配额管理:
|
||
|
||
```csharp
|
||
public class QuotaStorage : IStorage
|
||
{
|
||
private readonly IStorage _innerStorage;
|
||
private readonly long _maxSize;
|
||
private long _currentSize;
|
||
|
||
public void Write<T>(string key, T value)
|
||
{
|
||
var data = Serialize(value);
|
||
var size = data.Length;
|
||
|
||
if (_currentSize + size > _maxSize)
|
||
{
|
||
throw new InvalidOperationException("存储配额已满");
|
||
}
|
||
|
||
_innerStorage.Write(key, value);
|
||
_currentSize += size;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 问题:如何实现存储压缩?
|
||
|
||
**解答**:
|
||
使用压缩序列化器:
|
||
|
||
```csharp
|
||
public class CompressedSerializer : ISerializer
|
||
{
|
||
private readonly ISerializer _innerSerializer;
|
||
|
||
public string Serialize<T>(T value)
|
||
{
|
||
var json = _innerSerializer.Serialize(value);
|
||
var bytes = Encoding.UTF8.GetBytes(json);
|
||
var compressed = Compress(bytes);
|
||
return Convert.ToBase64String(compressed);
|
||
}
|
||
|
||
public T Deserialize<T>(string data)
|
||
{
|
||
var compressed = Convert.FromBase64String(data);
|
||
var bytes = Decompress(compressed);
|
||
var json = Encoding.UTF8.GetString(bytes);
|
||
return _innerSerializer.Deserialize<T>(json);
|
||
}
|
||
|
||
private byte[] Compress(byte[] data)
|
||
{
|
||
using var output = new MemoryStream();
|
||
using (var gzip = new GZipStream(output, CompressionMode.Compress))
|
||
{
|
||
gzip.Write(data, 0, data.Length);
|
||
}
|
||
return output.ToArray();
|
||
}
|
||
|
||
private byte[] Decompress(byte[] data)
|
||
{
|
||
using var input = new MemoryStream(data);
|
||
using var gzip = new GZipStream(input, CompressionMode.Decompress);
|
||
using var output = new MemoryStream();
|
||
gzip.CopyTo(output);
|
||
return output.ToArray();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 问题:如何监控存储操作?
|
||
|
||
**解答**:
|
||
实现日志存储包装器:
|
||
|
||
```csharp
|
||
public class LoggingStorage : IStorage
|
||
{
|
||
private readonly IStorage _innerStorage;
|
||
private readonly ILogger _logger;
|
||
|
||
public void Write<T>(string key, T value)
|
||
{
|
||
var stopwatch = Stopwatch.StartNew();
|
||
try
|
||
{
|
||
_innerStorage.Write(key, value);
|
||
_logger.Info($"写入成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Error($"写入失败: {key}, 错误: {ex.Message}");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
public T Read<T>(string key)
|
||
{
|
||
var stopwatch = Stopwatch.StartNew();
|
||
try
|
||
{
|
||
var value = _innerStorage.Read<T>(key);
|
||
_logger.Info($"读取成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms");
|
||
return value;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Error($"读取失败: {key}, 错误: {ex.Message}");
|
||
throw;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 相关文档
|
||
|
||
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
|
||
- [序列化系统](/zh-CN/game/serialization) - 数据序列化
|
||
- [Godot 集成](/zh-CN/godot/index) - Godot 中的存储
|
||
- [存档系统教程](/zh-CN/tutorials/save-system) - 完整示例
|