mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-04-03 04:14:30 +08:00
feat(config): 添加配置管理系统核心组件
- 实现了 ConfigRegistry 配置注册表,支持按名称注册和类型安全查询 - 创建了 InMemoryConfigTable 内存配置表,提供基于字典的只读配置存储 - 定义了 IConfigLoader、IConfigRegistry 和 IConfigTable 接口契约 - 添加了完整的单元测试验证配置表的注册、查询和类型检查功能 - 在项目文件中添加了新的代码文件夹结构 - 实现了配置表的覆盖策略以支持开发期热重载需求
This commit is contained in:
parent
a628ade28e
commit
c0aa8ba70e
19
GFramework.Game.Abstractions/Config/IConfigLoader.cs
Normal file
19
GFramework.Game.Abstractions/Config/IConfigLoader.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using GFramework.Core.Abstractions.Utility;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Abstractions.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义配置加载器契约。
|
||||||
|
/// 具体实现负责从文件系统、资源包或其他配置源加载文本配置,并将解析结果注册到配置注册表。
|
||||||
|
/// </summary>
|
||||||
|
public interface IConfigLoader : IUtility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 执行配置加载并将结果写入注册表。
|
||||||
|
/// 实现应在同一次加载过程中保证注册结果的一致性,避免只加载部分配置后就暴露给运行时消费。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="registry">用于接收配置表的注册表。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>表示异步加载流程的任务。</returns>
|
||||||
|
Task LoadAsync(IConfigRegistry registry, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
75
GFramework.Game.Abstractions/Config/IConfigRegistry.cs
Normal file
75
GFramework.Game.Abstractions/Config/IConfigRegistry.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
using GFramework.Core.Abstractions.Utility;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Abstractions.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义配置注册表契约,用于统一保存和解析按名称注册的配置表。
|
||||||
|
/// 注册表是运行时配置系统的入口,负责在加载阶段收集配置表,并在消费阶段提供类型安全查询。
|
||||||
|
/// </summary>
|
||||||
|
public interface IConfigRegistry : IUtility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前已注册配置表数量。
|
||||||
|
/// </summary>
|
||||||
|
int Count { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有已注册配置表名称。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>配置表名称集合。</returns>
|
||||||
|
IReadOnlyCollection<string> GetTableNames();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册指定名称的配置表。
|
||||||
|
/// 若名称已存在,则替换旧表,以便开发期热重载使用同一入口刷新配置。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">配置表主键类型。</typeparam>
|
||||||
|
/// <typeparam name="TValue">配置项值类型。</typeparam>
|
||||||
|
/// <param name="name">配置表名称。</param>
|
||||||
|
/// <param name="table">要注册的配置表实例。</param>
|
||||||
|
void RegisterTable<TKey, TValue>(string name, IConfigTable<TKey, TValue> table)
|
||||||
|
where TKey : notnull;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定名称的配置表。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">配置表主键类型。</typeparam>
|
||||||
|
/// <typeparam name="TValue">配置项值类型。</typeparam>
|
||||||
|
/// <param name="name">配置表名称。</param>
|
||||||
|
/// <returns>匹配的强类型配置表实例。</returns>
|
||||||
|
/// <exception cref="KeyNotFoundException">当配置表名称不存在时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当请求类型与已注册配置表类型不匹配时抛出。</exception>
|
||||||
|
IConfigTable<TKey, TValue> GetTable<TKey, TValue>(string name)
|
||||||
|
where TKey : notnull;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试获取指定名称的配置表。
|
||||||
|
/// 当名称存在但类型不匹配时返回 <c>false</c>,避免消费端将类型错误误判为加载成功。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">配置表主键类型。</typeparam>
|
||||||
|
/// <typeparam name="TValue">配置项值类型。</typeparam>
|
||||||
|
/// <param name="name">配置表名称。</param>
|
||||||
|
/// <param name="table">匹配的强类型配置表;未找到或类型不匹配时返回空。</param>
|
||||||
|
/// <returns>找到且类型匹配时返回 <c>true</c>,否则返回 <c>false</c>。</returns>
|
||||||
|
bool TryGetTable<TKey, TValue>(string name, out IConfigTable<TKey, TValue>? table)
|
||||||
|
where TKey : notnull;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查指定名称的配置表是否存在。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">配置表名称。</param>
|
||||||
|
/// <returns>存在时返回 <c>true</c>,否则返回 <c>false</c>。</returns>
|
||||||
|
bool HasTable(string name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除指定名称的配置表。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">配置表名称。</param>
|
||||||
|
/// <returns>移除成功时返回 <c>true</c>,否则返回 <c>false</c>。</returns>
|
||||||
|
bool RemoveTable(string name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空所有已注册配置表。
|
||||||
|
/// </summary>
|
||||||
|
void Clear();
|
||||||
|
}
|
||||||
65
GFramework.Game.Abstractions/Config/IConfigTable.cs
Normal file
65
GFramework.Game.Abstractions/Config/IConfigTable.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using GFramework.Core.Abstractions.Utility;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Abstractions.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义配置表的非泛型公共契约,用于在注册表中保存异构配置表实例。
|
||||||
|
/// 该接口只暴露运行时发现和诊断所需的元数据,不提供具体类型访问能力。
|
||||||
|
/// </summary>
|
||||||
|
public interface IConfigTable : IUtility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取配置表主键类型。
|
||||||
|
/// </summary>
|
||||||
|
Type KeyType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取配置项值类型。
|
||||||
|
/// </summary>
|
||||||
|
Type ValueType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前配置表中的条目数量。
|
||||||
|
/// </summary>
|
||||||
|
int Count { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义强类型只读配置表契约。
|
||||||
|
/// 运行时配置表应通过主键执行只读查询,而不是暴露可变集合接口,
|
||||||
|
/// 以保持配置数据在加载完成后的稳定性和可预测性。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">配置表主键类型。</typeparam>
|
||||||
|
/// <typeparam name="TValue">配置项值类型。</typeparam>
|
||||||
|
public interface IConfigTable<TKey, TValue> : IConfigTable
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定主键的配置项。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">配置项主键。</param>
|
||||||
|
/// <returns>找到的配置项。</returns>
|
||||||
|
/// <exception cref="KeyNotFoundException">当主键不存在时抛出。</exception>
|
||||||
|
TValue Get(TKey key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试获取指定主键的配置项。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">配置项主键。</param>
|
||||||
|
/// <param name="value">找到的配置项;未找到时返回默认值。</param>
|
||||||
|
/// <returns>找到配置项时返回 <c>true</c>,否则返回 <c>false</c>。</returns>
|
||||||
|
bool TryGet(TKey key, out TValue? value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查指定主键是否存在。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">配置项主键。</param>
|
||||||
|
/// <returns>主键存在时返回 <c>true</c>,否则返回 <c>false</c>。</returns>
|
||||||
|
bool ContainsKey(TKey key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取配置表中的所有配置项快照。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>只读配置项集合。</returns>
|
||||||
|
IReadOnlyCollection<TValue> All();
|
||||||
|
}
|
||||||
130
GFramework.Game.Tests/Config/ConfigRegistryTests.cs
Normal file
130
GFramework.Game.Tests/Config/ConfigRegistryTests.cs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
using GFramework.Game.Abstractions.Config;
|
||||||
|
using GFramework.Game.Config;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Tests.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证配置注册表的注册、覆盖和类型检查行为。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class ConfigRegistryTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证注册后的配置表可以按名称和类型成功解析。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void RegisterTable_Then_GetTable_Should_Return_Registered_Instance()
|
||||||
|
{
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
var table = CreateMonsterTable();
|
||||||
|
|
||||||
|
registry.RegisterTable("monster", table);
|
||||||
|
|
||||||
|
var resolved = registry.GetTable<int, MonsterConfigStub>("monster");
|
||||||
|
|
||||||
|
Assert.That(resolved, Is.SameAs(table));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证同名注册会覆盖旧表,用于后续热重载场景。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void RegisterTable_Should_Replace_Previous_Table_With_Same_Name()
|
||||||
|
{
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
var oldTable = CreateMonsterTable();
|
||||||
|
var newTable = new InMemoryConfigTable<int, MonsterConfigStub>(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new MonsterConfigStub(3, "Orc")
|
||||||
|
},
|
||||||
|
static config => config.Id);
|
||||||
|
|
||||||
|
registry.RegisterTable("monster", oldTable);
|
||||||
|
registry.RegisterTable("monster", newTable);
|
||||||
|
|
||||||
|
var resolved = registry.GetTable<int, MonsterConfigStub>("monster");
|
||||||
|
|
||||||
|
Assert.That(resolved, Is.SameAs(newTable));
|
||||||
|
Assert.That(resolved.Count, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证请求类型与实际注册类型不匹配时会抛出异常,避免消费端默默读取错误表。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void GetTable_Should_Throw_When_Requested_Type_Does_Not_Match_Registered_Table()
|
||||||
|
{
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
registry.RegisterTable("monster", CreateMonsterTable());
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => registry.GetTable<string, MonsterConfigStub>("monster"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证移除和清空操作会更新注册表状态。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void RemoveTable_And_Clear_Should_Update_Registry_State()
|
||||||
|
{
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
registry.RegisterTable("monster", CreateMonsterTable());
|
||||||
|
registry.RegisterTable("npc", CreateNpcTable());
|
||||||
|
|
||||||
|
var removed = registry.RemoveTable("monster");
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(removed, Is.True);
|
||||||
|
Assert.That(registry.HasTable("monster"), Is.False);
|
||||||
|
Assert.That(registry.Count, Is.EqualTo(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.Clear();
|
||||||
|
|
||||||
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建怪物配置表测试实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>怪物配置表。</returns>
|
||||||
|
private static IConfigTable<int, MonsterConfigStub> CreateMonsterTable()
|
||||||
|
{
|
||||||
|
return new InMemoryConfigTable<int, MonsterConfigStub>(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new MonsterConfigStub(1, "Slime"),
|
||||||
|
new MonsterConfigStub(2, "Goblin")
|
||||||
|
},
|
||||||
|
static config => config.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建 NPC 配置表测试实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>NPC 配置表。</returns>
|
||||||
|
private static IConfigTable<Guid, NpcConfigStub> CreateNpcTable()
|
||||||
|
{
|
||||||
|
return new InMemoryConfigTable<Guid, NpcConfigStub>(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new NpcConfigStub(Guid.NewGuid(), "Guide")
|
||||||
|
},
|
||||||
|
static config => config.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于怪物配置表测试的最小配置类型。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">配置主键。</param>
|
||||||
|
/// <param name="Name">配置名称。</param>
|
||||||
|
private sealed record MonsterConfigStub(int Id, string Name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于 NPC 配置表测试的最小配置类型。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">配置主键。</param>
|
||||||
|
/// <param name="Name">配置名称。</param>
|
||||||
|
private sealed record NpcConfigStub(Guid Id, string Name);
|
||||||
|
}
|
||||||
72
GFramework.Game.Tests/Config/InMemoryConfigTableTests.cs
Normal file
72
GFramework.Game.Tests/Config/InMemoryConfigTableTests.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using GFramework.Game.Config;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Tests.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证内存配置表的基础只读查询行为。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class InMemoryConfigTableTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证已存在主键可以被正确查询。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Get_Should_Return_Config_When_Key_Exists()
|
||||||
|
{
|
||||||
|
var table = new InMemoryConfigTable<int, MonsterConfigStub>(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new MonsterConfigStub(1, "Slime"),
|
||||||
|
new MonsterConfigStub(2, "Goblin")
|
||||||
|
},
|
||||||
|
static config => config.Id);
|
||||||
|
|
||||||
|
var result = table.Get(2);
|
||||||
|
|
||||||
|
Assert.That(result.Name, Is.EqualTo("Goblin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证重复主键会在加载期被拒绝,避免运行期覆盖旧值。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Constructor_Should_Throw_When_Duplicate_Key_Is_Detected()
|
||||||
|
{
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
new InMemoryConfigTable<int, MonsterConfigStub>(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new MonsterConfigStub(1, "Slime"),
|
||||||
|
new MonsterConfigStub(1, "Goblin")
|
||||||
|
},
|
||||||
|
static config => config.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 All 返回的集合包含完整快照。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void All_Should_Return_All_Configs()
|
||||||
|
{
|
||||||
|
var table = new InMemoryConfigTable<int, MonsterConfigStub>(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new MonsterConfigStub(1, "Slime"),
|
||||||
|
new MonsterConfigStub(2, "Goblin")
|
||||||
|
},
|
||||||
|
static config => config.Id);
|
||||||
|
|
||||||
|
var all = table.All();
|
||||||
|
|
||||||
|
Assert.That(all, Has.Count.EqualTo(2));
|
||||||
|
Assert.That(all.Select(static config => config.Name), Is.EquivalentTo(new[] { "Slime", "Goblin" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于配置表测试的最小配置类型。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">配置主键。</param>
|
||||||
|
/// <param name="Name">配置名称。</param>
|
||||||
|
private sealed record MonsterConfigStub(int Id, string Name);
|
||||||
|
}
|
||||||
162
GFramework.Game/Config/ConfigRegistry.cs
Normal file
162
GFramework.Game/Config/ConfigRegistry.cs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
using GFramework.Game.Abstractions.Config;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认配置注册表实现。
|
||||||
|
/// 该类型负责统一管理按名称注册的配置表,并在消费端提供类型安全的解析入口。
|
||||||
|
/// 为了支持开发期热重载,注册行为采用覆盖策略而不是拒绝重复名称。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ConfigRegistry : IConfigRegistry
|
||||||
|
{
|
||||||
|
private const string NameCannotBeNullOrWhiteSpaceMessage = "Table name cannot be null or whitespace.";
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, IConfigTable> _tables = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取已注册的配置表数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Count => _tables.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有已注册配置表的名称集合,按字典序排序。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>返回只读的配置表名称集合。</returns>
|
||||||
|
public IReadOnlyCollection<string> GetTableNames()
|
||||||
|
{
|
||||||
|
return _tables.Keys.OrderBy(static key => key, StringComparer.Ordinal).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个配置表到注册表中。
|
||||||
|
/// 如果同名的配置表已存在,则会覆盖原有注册以支持热重载。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">配置表主键的类型,必须为非空类型。</typeparam>
|
||||||
|
/// <typeparam name="TValue">配置表值的类型。</typeparam>
|
||||||
|
/// <param name="name">配置表的注册名称,用于后续查找。</param>
|
||||||
|
/// <param name="table">要注册的配置表实例。</param>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="name" /> 为 null、空或仅包含空白字符时抛出。</exception>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="table" /> 为 null 时抛出。</exception>
|
||||||
|
public void RegisterTable<TKey, TValue>(string name, IConfigTable<TKey, TValue> table)
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(NameCannotBeNullOrWhiteSpaceMessage, nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(table);
|
||||||
|
|
||||||
|
_tables[name] = table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据名称获取已注册的配置表,并进行类型验证。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">期望的主键类型,必须为非空类型。</typeparam>
|
||||||
|
/// <typeparam name="TValue">期望的值类型。</typeparam>
|
||||||
|
/// <param name="name">要查找的配置表名称。</param>
|
||||||
|
/// <returns>返回类型匹配的配置表实例。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="name" /> 为 null、空或仅包含空白字符时抛出。</exception>
|
||||||
|
/// <exception cref="KeyNotFoundException">当指定名称的配置表不存在时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// 当找到的配置表类型与请求的类型不匹配时抛出。
|
||||||
|
/// </exception>
|
||||||
|
public IConfigTable<TKey, TValue> GetTable<TKey, TValue>(string name)
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(NameCannotBeNullOrWhiteSpaceMessage, nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_tables.TryGetValue(name, out var table))
|
||||||
|
{
|
||||||
|
throw new KeyNotFoundException($"Config table '{name}' was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table is IConfigTable<TKey, TValue> typedTable)
|
||||||
|
{
|
||||||
|
return typedTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Config table '{name}' was registered as '{table.KeyType.Name} -> {table.ValueType.Name}', " +
|
||||||
|
$"but the caller requested '{typeof(TKey).Name} -> {typeof(TValue).Name}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试根据名称获取配置表,操作失败时不会抛出异常。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">期望的主键类型,必须为非空类型。</typeparam>
|
||||||
|
/// <typeparam name="TValue">期望的值类型。</typeparam>
|
||||||
|
/// <param name="name">要查找的配置表名称。</param>
|
||||||
|
/// <param name="table">
|
||||||
|
/// 输出参数,如果查找成功则返回类型匹配的配置表实例,否则为 null。
|
||||||
|
/// </param>
|
||||||
|
/// <returns>如果找到指定名称且类型匹配的配置表则返回 true,否则返回 false。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="name" /> 为 null、空或仅包含空白字符时抛出。</exception>
|
||||||
|
public bool TryGetTable<TKey, TValue>(string name, out IConfigTable<TKey, TValue>? table)
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
table = default;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(NameCannotBeNullOrWhiteSpaceMessage, nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_tables.TryGetValue(name, out var rawTable))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawTable is not IConfigTable<TKey, TValue> typedTable)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
table = typedTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查指定名称的配置表是否已注册。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">要检查的配置表名称。</param>
|
||||||
|
/// <returns>如果配置表已注册则返回 true,否则返回 false。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="name" /> 为 null、空或仅包含空白字符时抛出。</exception>
|
||||||
|
public bool HasTable(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(NameCannotBeNullOrWhiteSpaceMessage, nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _tables.ContainsKey(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从注册表中移除指定名称的配置表。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">要移除的配置表名称。</param>
|
||||||
|
/// <returns>如果配置表存在并被成功移除则返回 true,否则返回 false。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="name" /> 为 null、空或仅包含空白字符时抛出。</exception>
|
||||||
|
public bool RemoveTable(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(NameCannotBeNullOrWhiteSpaceMessage, nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _tables.TryRemove(name, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空注册表中的所有配置表。
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_tables.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
118
GFramework.Game/Config/InMemoryConfigTable.cs
Normal file
118
GFramework.Game/Config/InMemoryConfigTable.cs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using GFramework.Game.Abstractions.Config;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于内存字典的只读配置表实现。
|
||||||
|
/// 该实现用于 Runtime MVP 阶段,为加载器和注册表提供稳定的只读查询对象。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">配置表主键类型。</typeparam>
|
||||||
|
/// <typeparam name="TValue">配置项值类型。</typeparam>
|
||||||
|
public sealed class InMemoryConfigTable<TKey, TValue> : IConfigTable<TKey, TValue>
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyCollection<TValue> _allValues;
|
||||||
|
private readonly IReadOnlyDictionary<TKey, TValue> _entries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用配置项序列和主键选择器创建内存配置表。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">配置项序列。</param>
|
||||||
|
/// <param name="keySelector">用于提取主键的委托。</param>
|
||||||
|
/// <param name="comparer">可选的主键比较器。</param>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="values" /> 或 <paramref name="keySelector" /> 为空时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当配置项主键重复时抛出。</exception>
|
||||||
|
public InMemoryConfigTable(
|
||||||
|
IEnumerable<TValue> values,
|
||||||
|
Func<TValue, TKey> keySelector,
|
||||||
|
IEqualityComparer<TKey>? comparer = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
|
ArgumentNullException.ThrowIfNull(keySelector);
|
||||||
|
|
||||||
|
var dictionary = new Dictionary<TKey, TValue>(comparer);
|
||||||
|
var allValues = new List<TValue>();
|
||||||
|
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
var key = keySelector(value);
|
||||||
|
|
||||||
|
// 配置表必须在加载期拒绝重复主键,否则运行期查询结果将不可预测。
|
||||||
|
if (!dictionary.TryAdd(key, value))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Duplicate config key '{key}' was detected for table value type '{typeof(TValue).Name}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
allValues.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_entries = new ReadOnlyDictionary<TKey, TValue>(dictionary);
|
||||||
|
_allValues = new ReadOnlyCollection<TValue>(allValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取配置表的主键类型。
|
||||||
|
/// </summary>
|
||||||
|
public Type KeyType => typeof(TKey);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取配置表的值类型。
|
||||||
|
/// </summary>
|
||||||
|
public Type ValueType => typeof(TValue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取配置表中配置项的数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Count => _entries.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据主键获取配置项的值。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">要查找的配置项主键。</param>
|
||||||
|
/// <returns>返回对应主键的配置项值。</returns>
|
||||||
|
/// <exception cref="KeyNotFoundException">当指定主键的配置项不存在时抛出。</exception>
|
||||||
|
public TValue Get(TKey key)
|
||||||
|
{
|
||||||
|
if (!_entries.TryGetValue(key, out var value))
|
||||||
|
{
|
||||||
|
throw new KeyNotFoundException(
|
||||||
|
$"Config key '{key}' was not found in table '{typeof(TValue).Name}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试根据主键获取配置项的值,操作失败时不会抛出异常。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">要查找的配置项主键。</param>
|
||||||
|
/// <param name="value">
|
||||||
|
/// 输出参数,如果查找成功则返回对应的配置项值,否则为默认值。
|
||||||
|
/// </param>
|
||||||
|
/// <returns>如果找到指定主键的配置项则返回 true,否则返回 false。</returns>
|
||||||
|
public bool TryGet(TKey key, out TValue? value)
|
||||||
|
{
|
||||||
|
return _entries.TryGetValue(key, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查指定主键的配置项是否存在于配置表中。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">要检查的配置项主键。</param>
|
||||||
|
/// <returns>如果配置项已存在则返回 true,否则返回 false。</returns>
|
||||||
|
public bool ContainsKey(TKey key)
|
||||||
|
{
|
||||||
|
return _entries.ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取配置表中所有配置项的集合。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>返回所有配置项值的只读集合。</returns>
|
||||||
|
public IReadOnlyCollection<TValue> All()
|
||||||
|
{
|
||||||
|
return _allValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -134,6 +134,7 @@
|
|||||||
<AdditionalFiles Remove="AnalyzerReleases.Unshipped.md"/>
|
<AdditionalFiles Remove="AnalyzerReleases.Unshipped.md"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="local-plan\todos\"/>
|
||||||
<Folder Include="local-plan\评估\"/>
|
<Folder Include="local-plan\评估\"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user