mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-04-02 11:44:28 +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"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="local-plan\todos\"/>
|
||||
<Folder Include="local-plan\评估\"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user