diff --git a/GFramework.Core.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs b/GFramework.Core.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs index 57c49d08..dc6be9b0 100644 --- a/GFramework.Core.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs +++ b/GFramework.Core.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs @@ -635,7 +635,7 @@ public sealed class ContextRegistrationAnalyzer : DiagnosticAnalyzer if (targetMethod.TypeArguments[0] is not INamedTypeSymbol namedType) return false; - if (targetMethod.Name == "RegisterModel" && + if (string.Equals(targetMethod.Name, "RegisterModel", StringComparison.Ordinal) && SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType)) { componentKind = ComponentKind.Model; @@ -643,7 +643,7 @@ public sealed class ContextRegistrationAnalyzer : DiagnosticAnalyzer return true; } - if (targetMethod.Name == "RegisterSystem" && + if (string.Equals(targetMethod.Name, "RegisterSystem", StringComparison.Ordinal) && SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType)) { componentKind = ComponentKind.System; @@ -651,7 +651,7 @@ public sealed class ContextRegistrationAnalyzer : DiagnosticAnalyzer return true; } - if (targetMethod.Name == "RegisterUtility" && + if (string.Equals(targetMethod.Name, "RegisterUtility", StringComparison.Ordinal) && SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType)) { componentKind = ComponentKind.Utility; diff --git a/GFramework.Core.SourceGenerators/Analyzers/PriorityUsageAnalyzer.cs b/GFramework.Core.SourceGenerators/Analyzers/PriorityUsageAnalyzer.cs index 520448a6..0761765e 100644 --- a/GFramework.Core.SourceGenerators/Analyzers/PriorityUsageAnalyzer.cs +++ b/GFramework.Core.SourceGenerators/Analyzers/PriorityUsageAnalyzer.cs @@ -62,7 +62,7 @@ public sealed class PriorityUsageAnalyzer : DiagnosticAnalyzer var method = invocation.TargetMethod; // 检查方法名是否为 GetAll - if (method.Name != "GetAll") + if (!string.Equals(method.Name, "GetAll", StringComparison.Ordinal)) return; // 检查是否为泛型方法 diff --git a/GFramework.Core.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs b/GFramework.Core.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs index 93771bc1..fe593909 100644 --- a/GFramework.Core.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs +++ b/GFramework.Core.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs @@ -179,12 +179,7 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator { var registrations = new List(); - foreach (var attribute in typeSymbol.GetAttributes() - // Roslyn 会把 partial 类型上的属性合并到同一个集合中。 - // 先按语法树标识排序,才能让每个文件内的 Span.Start 成为可比较的稳定顺序键。 - .OrderBy(GetAttributeSyntaxTreeOrderKey, StringComparer.Ordinal) - .ThenBy(GetAttributeOrder) - .ThenBy(GetAttributeTypeOrderKey, StringComparer.Ordinal)) + foreach (var attribute in GetOrderedRegistrationAttributes(typeSymbol)) { if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, registerModelAttribute)) { @@ -239,6 +234,16 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator return registrations; } + private static IOrderedEnumerable GetOrderedRegistrationAttributes(INamedTypeSymbol typeSymbol) + { + // Roslyn 会把 partial 类型上的属性合并到同一个集合中。 + // 先按语法树标识排序,才能让每个文件内的 Span.Start 成为可比较的稳定顺序键。 + return typeSymbol.GetAttributes() + .OrderBy(GetAttributeSyntaxTreeOrderKey, StringComparer.Ordinal) + .ThenBy(GetAttributeOrder) + .ThenBy(GetAttributeTypeOrderKey, StringComparer.Ordinal); + } + private static bool TryCreateRegistration( SourceProductionContext context, INamedTypeSymbol ownerType, @@ -323,7 +328,8 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator RegistrationKind.Model => "RegisterModel", RegistrationKind.System => "RegisterSystem", RegistrationKind.Utility => "RegisterUtility", - _ => throw new ArgumentOutOfRangeException(nameof(registration.Kind)) + _ => throw new InvalidOperationException( + $"Unsupported registration kind '{registration.Kind}'.") }); builder.Append("(new "); builder.Append(registration.ComponentTypeDisplayName); diff --git a/GFramework.Core.SourceGenerators/Diagnostics/LoggerDiagnostic.cs b/GFramework.Core.SourceGenerators/Diagnostics/LoggerDiagnostics.cs similarity index 100% rename from GFramework.Core.SourceGenerators/Diagnostics/LoggerDiagnostic.cs rename to GFramework.Core.SourceGenerators/Diagnostics/LoggerDiagnostics.cs diff --git a/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs b/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs index c89f18ca..ff1acccc 100644 --- a/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs +++ b/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs @@ -72,14 +72,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase ? null : symbol.ContainingNamespace.ToDisplayString(); - var generateIsMethods = GetNamedBooleanArgument( - attr, - nameof(GenerateEnumExtensionsAttribute.GenerateIsMethods), - true); - var generateIsInMethod = GetNamedBooleanArgument( - attr, - nameof(GenerateEnumExtensionsAttribute.GenerateIsInMethod), - true); + var generationOptions = GetGenerationOptions(attr); var enumName = symbol.Name; var fullEnumName = symbol.ToDisplayString(); var members = symbol.GetMembers() @@ -104,7 +97,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase // 两个生成开关是彼此独立的契约,需要分别控制输出,并保持空行布局稳定,便于快照精确回归。 var hasGeneratedMembers = false; - if (generateIsMethods) + if (generationOptions.GenerateIsMethods) { hasGeneratedMembers = AppendIsMethods( sb, @@ -112,7 +105,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase fullEnumName); } - if (generateIsInMethod) + if (generationOptions.GenerateIsInMethod) { if (hasGeneratedMembers) { @@ -130,6 +123,24 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase return sb.ToString(); } + /// + /// 读取枚举扩展生成选项,并在属性未显式指定时回退到契约默认值。 + /// + /// 待分析的特性数据。 + /// 包含各个生成开关的选项元组。 + private static (bool GenerateIsMethods, bool GenerateIsInMethod) GetGenerationOptions(AttributeData attribute) + { + return ( + GetNamedBooleanArgument( + attribute, + nameof(GenerateEnumExtensionsAttribute.GenerateIsMethods), + true), + GetNamedBooleanArgument( + attribute, + nameof(GenerateEnumExtensionsAttribute.GenerateIsInMethod), + true)); + } + /// /// 获取生成文件的提示名称 /// @@ -151,7 +162,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase { foreach (var namedArgument in attribute.NamedArguments) { - if (namedArgument.Key == argumentName && + if (string.Equals(namedArgument.Key, argumentName, StringComparison.Ordinal) && namedArgument.Value.Value is bool value) { return value; diff --git a/GFramework.Core.Tests/State/StateMachineSystemTests.cs b/GFramework.Core.Tests/State/StateMachineSystemTests.cs index 2c234238..9ee084ff 100644 --- a/GFramework.Core.Tests/State/StateMachineSystemTests.cs +++ b/GFramework.Core.Tests/State/StateMachineSystemTests.cs @@ -250,7 +250,7 @@ public class TestStateMachineSystemV5 : StateMachineSystem /// 获取状态机内部的状态字典 /// /// 类型到状态实例的映射字典 - public Dictionary GetStates() + public IDictionary GetStates() { return States; } diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index cf55b192..b9e2ecf6 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -288,7 +288,7 @@ public abstract class Architecture : IArchitecture { try { - await InitializeInternalAsync(true); + await InitializeInternalAsync(true).ConfigureAwait(false); } catch (Exception e) { @@ -304,7 +304,8 @@ public abstract class Architecture : IArchitecture /// 是否启用异步模式 private async Task InitializeInternalAsync(bool asyncMode) { - _context = await _bootstrapper.PrepareForInitializationAsync(_context, Configurator, asyncMode); + _context = await _bootstrapper.PrepareForInitializationAsync(_context, Configurator, asyncMode) + .ConfigureAwait(false); // === 用户 OnInitialize === _logger.Debug("Calling user OnInitialize()"); @@ -312,7 +313,7 @@ public abstract class Architecture : IArchitecture _logger.Debug("User OnInitialize() completed"); // === 组件初始化阶段 === - await _lifecycle.InitializeAllComponentsAsync(asyncMode); + await _lifecycle.InitializeAllComponentsAsync(asyncMode).ConfigureAwait(false); // === 初始化完成阶段 === _bootstrapper.CompleteInitialization(); @@ -337,7 +338,7 @@ public abstract class Architecture : IArchitecture /// public virtual async ValueTask DestroyAsync() { - await _lifecycle.DestroyAsync(); + await _lifecycle.DestroyAsync().ConfigureAwait(false); } /// diff --git a/GFramework.Core/Architectures/ArchitectureBootstrapper.cs b/GFramework.Core/Architectures/ArchitectureBootstrapper.cs index 343bcdd6..ae0bdd38 100644 --- a/GFramework.Core/Architectures/ArchitectureBootstrapper.cs +++ b/GFramework.Core/Architectures/ArchitectureBootstrapper.cs @@ -35,7 +35,7 @@ internal sealed class ArchitectureBootstrapper( var context = EnsureContext(existingContext); ConfigureServices(context, configurator); - await InitializeServiceModulesAsync(asyncMode); + await InitializeServiceModulesAsync(asyncMode).ConfigureAwait(false); return context; } @@ -117,6 +117,6 @@ internal sealed class ArchitectureBootstrapper( /// 是否允许异步初始化服务模块。 private async Task InitializeServiceModulesAsync(bool asyncMode) { - await services.ModuleManager.InitializeAllAsync(asyncMode); + await services.ModuleManager.InitializeAllAsync(asyncMode).ConfigureAwait(false); } } diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index e0ac2dd6..8a64e98e 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -97,7 +97,7 @@ public class ArchitectureContext : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - return await CqrsRuntime.SendAsync(this, request, cancellationToken); + return await CqrsRuntime.SendAsync(this, request, cancellationToken).ConfigureAwait(false); } /// @@ -124,7 +124,7 @@ public class ArchitectureContext : IArchitectureContext where TNotification : INotification { ArgumentNullException.ThrowIfNull(notification); - await CqrsRuntime.PublishAsync(this, notification, cancellationToken); + await CqrsRuntime.PublishAsync(this, notification, cancellationToken).ConfigureAwait(false); } /// @@ -151,7 +151,7 @@ public class ArchitectureContext : IArchitectureContext CancellationToken cancellationToken = default) where TCommand : IRequest { - await SendRequestAsync(command, cancellationToken); + await SendRequestAsync(command, cancellationToken).ConfigureAwait(false); } /// @@ -162,7 +162,7 @@ public class ArchitectureContext : IArchitectureContext IRequest command, CancellationToken cancellationToken = default) { - return await SendRequestAsync(command, cancellationToken); + return await SendRequestAsync(command, cancellationToken).ConfigureAwait(false); } #endregion @@ -205,7 +205,7 @@ public class ArchitectureContext : IArchitectureContext if (query == null) throw new ArgumentNullException(nameof(query)); var asyncQueryBus = GetOrCache(); if (asyncQueryBus == null) throw new InvalidOperationException("IAsyncQueryExecutor not registered"); - return await asyncQueryBus.SendAsync(query); + return await asyncQueryBus.SendAsync(query).ConfigureAwait(false); } /// @@ -219,7 +219,7 @@ public class ArchitectureContext : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(query); - return await SendRequestAsync(query, cancellationToken); + return await SendRequestAsync(query, cancellationToken).ConfigureAwait(false); } #endregion @@ -356,7 +356,7 @@ public class ArchitectureContext : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); - return await SendRequestAsync(command, cancellationToken); + return await SendRequestAsync(command, cancellationToken).ConfigureAwait(false); } /// @@ -368,7 +368,7 @@ public class ArchitectureContext : IArchitectureContext ArgumentNullException.ThrowIfNull(command); var commandBus = GetOrCache(); if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered"); - await commandBus.SendAsync(command); + await commandBus.SendAsync(command).ConfigureAwait(false); } /// @@ -382,7 +382,7 @@ public class ArchitectureContext : IArchitectureContext ArgumentNullException.ThrowIfNull(command); var commandBus = GetOrCache(); if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered"); - return await commandBus.SendAsync(command); + return await commandBus.SendAsync(command).ConfigureAwait(false); } /// diff --git a/GFramework.Core/Architectures/ArchitectureDisposer.cs b/GFramework.Core/Architectures/ArchitectureDisposer.cs index c2c7f553..e87d25a7 100644 --- a/GFramework.Core/Architectures/ArchitectureDisposer.cs +++ b/GFramework.Core/Architectures/ArchitectureDisposer.cs @@ -59,15 +59,15 @@ internal sealed class ArchitectureDisposer( if (currentPhase == ArchitecturePhase.None) { logger.Debug("Architecture destroy called but never initialized, cleaning up registered components"); - await CleanupComponentsAsync(); + await CleanupComponentsAsync().ConfigureAwait(false); return; } logger.Info("Starting architecture destruction"); enterPhase(ArchitecturePhase.Destroying); - await CleanupComponentsAsync(); - await services.ModuleManager.DestroyAllAsync(); + await CleanupComponentsAsync().ConfigureAwait(false); + await services.ModuleManager.DestroyAllAsync().ConfigureAwait(false); // Destroyed 广播依赖容器中的阶段监听器,必须在清空容器前完成。 enterPhase(ArchitecturePhase.Destroyed); @@ -93,7 +93,7 @@ internal sealed class ArchitectureDisposer( if (component is IAsyncDestroyable asyncDestroyable) { - await asyncDestroyable.DestroyAsync(); + await asyncDestroyable.DestroyAsync().ConfigureAwait(false); } else if (component is IDestroyable destroyable) { @@ -109,4 +109,4 @@ internal sealed class ArchitectureDisposer( _disposables.Clear(); _disposableSet.Clear(); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/ArchitectureLifecycle.cs b/GFramework.Core/Architectures/ArchitectureLifecycle.cs index 6cb8f3df..d503f68c 100644 --- a/GFramework.Core/Architectures/ArchitectureLifecycle.cs +++ b/GFramework.Core/Architectures/ArchitectureLifecycle.cs @@ -160,7 +160,7 @@ internal sealed class ArchitectureLifecycle( foreach (var utility in utilities) { logger.Debug($"Initializing utility: {utility.GetType().Name}"); - await InitializeComponentAsync(utility, asyncMode); + await InitializeComponentAsync(utility, asyncMode).ConfigureAwait(false); } logger.Info("All context utilities initialized"); @@ -178,7 +178,7 @@ internal sealed class ArchitectureLifecycle( foreach (var model in models) { logger.Debug($"Initializing model: {model.GetType().Name}"); - await InitializeComponentAsync(model, asyncMode); + await InitializeComponentAsync(model, asyncMode).ConfigureAwait(false); } logger.Info("All models initialized"); @@ -196,7 +196,7 @@ internal sealed class ArchitectureLifecycle( foreach (var system in systems) { logger.Debug($"Initializing system: {system.GetType().Name}"); - await InitializeComponentAsync(system, asyncMode); + await InitializeComponentAsync(system, asyncMode).ConfigureAwait(false); } logger.Info("All systems initialized"); @@ -218,7 +218,7 @@ internal sealed class ArchitectureLifecycle( private static async Task InitializeComponentAsync(IInitializable component, bool asyncMode) { if (asyncMode && component is IAsyncInitializable asyncInit) - await asyncInit.InitializeAsync(); + await asyncInit.InitializeAsync().ConfigureAwait(false); else component.Initialize(); } @@ -244,7 +244,7 @@ internal sealed class ArchitectureLifecycle( /// public async ValueTask DestroyAsync() { - await _disposer.DestroyAsync(CurrentPhase, EnterPhase); + await _disposer.DestroyAsync(CurrentPhase, EnterPhase).ConfigureAwait(false); } /// @@ -285,4 +285,4 @@ internal sealed class ArchitectureLifecycle( public Task WaitUntilReadyAsync() => _readyTcs.Task; #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractAsyncCommand.cs b/GFramework.Core/Command/AbstractAsyncCommand.cs index 779f3314..e0d2ca1a 100644 --- a/GFramework.Core/Command/AbstractAsyncCommand.cs +++ b/GFramework.Core/Command/AbstractAsyncCommand.cs @@ -16,7 +16,7 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand /// 表示异步操作的任务 async Task IAsyncCommand.ExecuteAsync() { - await OnExecuteAsync(); + await OnExecuteAsync().ConfigureAwait(false); } /// @@ -25,4 +25,4 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand /// /// 表示异步操作的任务 protected abstract Task OnExecuteAsync(); -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs b/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs index d97ab90b..00fcaaba 100644 --- a/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs +++ b/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs @@ -17,7 +17,7 @@ public abstract class AbstractAsyncCommand(TInput input) : ContextAwareB /// 表示异步操作的任务 async Task IAsyncCommand.ExecuteAsync() { - await OnExecuteAsync(input); + await OnExecuteAsync(input).ConfigureAwait(false); } /// diff --git a/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs b/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs index ae19458e..7d1f6a91 100644 --- a/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs +++ b/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs @@ -18,7 +18,7 @@ public abstract class AbstractAsyncCommand(TInput input) : Cont /// 表示异步操作且包含结果的任务 async Task IAsyncCommand.ExecuteAsync() { - return await OnExecuteAsync(input); + return await OnExecuteAsync(input).ConfigureAwait(false); } /// diff --git a/GFramework.Core/Concurrency/AsyncKeyLockManager.cs b/GFramework.Core/Concurrency/AsyncKeyLockManager.cs index 9ad2384e..f4c70595 100644 --- a/GFramework.Core/Concurrency/AsyncKeyLockManager.cs +++ b/GFramework.Core/Concurrency/AsyncKeyLockManager.cs @@ -22,7 +22,7 @@ namespace GFramework.Core.Concurrency; public sealed class AsyncKeyLockManager : IAsyncKeyLockManager { private readonly Timer _cleanupTimer; - private readonly ConcurrentDictionary _locks = new(); + private readonly ConcurrentDictionary _locks = new(StringComparer.Ordinal); private readonly long _lockTimeoutMs; private volatile bool _disposed; @@ -119,7 +119,8 @@ public sealed class AsyncKeyLockManager : IAsyncKeyLockManager LastAccessTicks = kvp.Value.LastAccessTicks, // CurrentCount == 0 表示锁被持有,可能有等待者(近似值) WaitingCount = kvp.Value.Semaphore.CurrentCount == 0 ? 1 : 0 - }); + }, + StringComparer.Ordinal); } /// @@ -187,4 +188,4 @@ public sealed class AsyncKeyLockManager : IAsyncKeyLockManager Semaphore.Dispose(); } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Configuration/ConfigurationManager.cs b/GFramework.Core/Configuration/ConfigurationManager.cs index 2340dcce..5eddcabb 100644 --- a/GFramework.Core/Configuration/ConfigurationManager.cs +++ b/GFramework.Core/Configuration/ConfigurationManager.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Globalization; using System.IO; using System.Text.Json; using GFramework.Core.Abstractions.Configuration; @@ -32,7 +33,7 @@ public class ConfigurationManager : IConfigurationManager /// /// 配置存储字典(线程安全) /// - private readonly ConcurrentDictionary _configs = new(); + private readonly ConcurrentDictionary _configs = new(StringComparer.Ordinal); private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ConfigurationManager)); @@ -45,7 +46,7 @@ public class ConfigurationManager : IConfigurationManager /// 配置监听器字典(线程安全) /// 键:配置键,值:监听器列表 /// - private readonly ConcurrentDictionary> _watchers = new(); + private readonly ConcurrentDictionary> _watchers = new(StringComparer.Ordinal); /// /// 获取配置数量 @@ -200,7 +201,7 @@ public class ConfigurationManager : IConfigurationManager /// public string SaveToJson() { - var dict = _configs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var dict = _configs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.Ordinal); return JsonSerializer.Serialize(dict, new JsonSerializerOptions { WriteIndented = true @@ -321,11 +322,11 @@ public class ConfigurationManager : IConfigurationManager // 尝试类型转换 try { - return (T)Convert.ChangeType(value, typeof(T)); + return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); } catch { return default!; } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/CoroutineHandle.cs b/GFramework.Core/Coroutine/CoroutineHandle.cs index e17c1dda..c595c852 100644 --- a/GFramework.Core/Coroutine/CoroutineHandle.cs +++ b/GFramework.Core/Coroutine/CoroutineHandle.cs @@ -85,6 +85,15 @@ public readonly struct CoroutineHandle : IEquatable return _id; } + /// + /// 返回协程句柄的稳定字符串表示,用于日志和诊断输出。 + /// + /// 包含内部标识符与键值的诊断字符串。 + public override string ToString() + { + return $"CoroutineHandle(Id={_id}, Key={Key})"; + } + /// /// 比较两个协程句柄是否相等 /// @@ -106,4 +115,4 @@ public readonly struct CoroutineHandle : IEquatable { return a._id != b._id; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/CoroutineScheduler.cs b/GFramework.Core/Coroutine/CoroutineScheduler.cs index 0a24ce6d..21f326ad 100644 --- a/GFramework.Core/Coroutine/CoroutineScheduler.cs +++ b/GFramework.Core/Coroutine/CoroutineScheduler.cs @@ -742,7 +742,7 @@ public sealed class CoroutineScheduler( var handler = OnCoroutineException; if (handler != null) { - Task.Run(() => + _ = Task.Run(() => { try { @@ -755,7 +755,7 @@ public sealed class CoroutineScheduler( }); } - _logger.Error($"[CoroutineScheduler] Coroutine {handle} failed with exception: {ex}"); + _logger.Error($"[CoroutineScheduler] Coroutine {handle.ToString()} failed with exception: {ex}"); FinalizeCoroutine(slotIndex, CoroutineCompletionStatus.Faulted, ex); } @@ -1003,4 +1003,4 @@ public sealed class CoroutineScheduler( } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/CoroutineStatistics.cs b/GFramework.Core/Coroutine/CoroutineStatistics.cs index 56321195..69463d72 100644 --- a/GFramework.Core/Coroutine/CoroutineStatistics.cs +++ b/GFramework.Core/Coroutine/CoroutineStatistics.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Globalization; using GFramework.Core.Abstractions.Coroutine; namespace GFramework.Core.Coroutine; @@ -10,7 +11,7 @@ namespace GFramework.Core.Coroutine; internal sealed class CoroutineStatistics : ICoroutineStatistics { private readonly Dictionary _countByPriority = new(); - private readonly Dictionary _countByTag = new(); + private readonly Dictionary _countByTag = new(StringComparer.Ordinal); private readonly object _lock = new(); private int _activeCount; private double _maxExecutionTimeMs; @@ -109,29 +110,31 @@ internal sealed class CoroutineStatistics : ICoroutineStatistics public string GenerateReport() { var sb = new StringBuilder(); - sb.AppendLine("=== 协程统计报告 ==="); - sb.AppendLine($"总启动数: {TotalStarted}"); - sb.AppendLine($"总完成数: {TotalCompleted}"); - sb.AppendLine($"总失败数: {TotalFailed}"); - sb.AppendLine($"当前活跃: {ActiveCount}"); - sb.AppendLine($"当前暂停: {PausedCount}"); - sb.AppendLine($"平均执行时间: {AverageExecutionTimeMs:F2} ms"); - sb.AppendLine($"最大执行时间: {MaxExecutionTimeMs:F2} ms"); + sb.AppendLine(FormattableString.Invariant($"=== 协程统计报告 ===")); + sb.AppendLine(FormattableString.Invariant($"总启动数: {TotalStarted}")); + sb.AppendLine(FormattableString.Invariant($"总完成数: {TotalCompleted}")); + sb.AppendLine(FormattableString.Invariant($"总失败数: {TotalFailed}")); + sb.AppendLine(FormattableString.Invariant($"当前活跃: {ActiveCount}")); + sb.AppendLine(FormattableString.Invariant($"当前暂停: {PausedCount}")); + sb.AppendLine(FormattableString.Invariant( + $"平均执行时间: {AverageExecutionTimeMs.ToString("F2", CultureInfo.InvariantCulture)} ms")); + sb.AppendLine(FormattableString.Invariant( + $"最大执行时间: {MaxExecutionTimeMs.ToString("F2", CultureInfo.InvariantCulture)} ms")); lock (_lock) { if (_countByPriority.Count > 0) { - sb.AppendLine("\n按优先级统计:"); + sb.AppendLine(FormattableString.Invariant($"\n按优先级统计:")); foreach (var kvp in _countByPriority.OrderByDescending(x => x.Key)) - sb.AppendLine($" {kvp.Key}: {kvp.Value}"); + sb.AppendLine(FormattableString.Invariant($" {kvp.Key}: {kvp.Value}")); } if (_countByTag.Count > 0) { - sb.AppendLine("\n按标签统计:"); + sb.AppendLine(FormattableString.Invariant($"\n按标签统计:")); foreach (var kvp in _countByTag.OrderByDescending(x => x.Value)) - sb.AppendLine($" {kvp.Key}: {kvp.Value}"); + sb.AppendLine(FormattableString.Invariant($" {kvp.Key}: {kvp.Value}")); } } @@ -212,4 +215,4 @@ internal sealed class CoroutineStatistics : ICoroutineStatistics } } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/Instructions/WaitForTask.T.cs b/GFramework.Core/Coroutine/Instructions/WaitForTask.T.cs index 2a613859..1e95b77b 100644 --- a/GFramework.Core/Coroutine/Instructions/WaitForTask.T.cs +++ b/GFramework.Core/Coroutine/Instructions/WaitForTask.T.cs @@ -24,7 +24,7 @@ public sealed class WaitForTask : IYieldInstruction _done = true; else // 注册完成回调 - _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously); + _ = _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously); } /// @@ -50,4 +50,4 @@ public sealed class WaitForTask : IYieldInstruction /// 获取等待是否已完成 /// public bool IsDone => _done; -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/Instructions/WaitForTask.cs b/GFramework.Core/Coroutine/Instructions/WaitForTask.cs index efc505fb..f8e09cf1 100644 --- a/GFramework.Core/Coroutine/Instructions/WaitForTask.cs +++ b/GFramework.Core/Coroutine/Instructions/WaitForTask.cs @@ -26,7 +26,7 @@ public sealed class WaitForTask : IYieldInstruction _done = true; else // 注册完成回调 - _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously); + _ = _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously); } /// @@ -47,4 +47,4 @@ public sealed class WaitForTask : IYieldInstruction /// 获取等待是否已完成 /// public bool IsDone => _done; -} \ No newline at end of file +} diff --git a/GFramework.Core/Environment/EnvironmentBase.cs b/GFramework.Core/Environment/EnvironmentBase.cs index 5c22dfd4..7895170e 100644 --- a/GFramework.Core/Environment/EnvironmentBase.cs +++ b/GFramework.Core/Environment/EnvironmentBase.cs @@ -11,7 +11,7 @@ public abstract class EnvironmentBase : ContextAwareBase, IEnvironment /// /// 存储环境值的字典,键为字符串,值为对象类型 /// - protected readonly Dictionary Values = new(); + protected readonly IDictionary Values = new Dictionary(StringComparer.Ordinal); /// /// 获取环境名称的抽象属性 @@ -84,4 +84,4 @@ public abstract class EnvironmentBase : ContextAwareBase, IEnvironment // 将键值对添加到Values字典中 Values[key] = value; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Events/EventStatistics.cs b/GFramework.Core/Events/EventStatistics.cs index ff79c392..22ae8b2f 100644 --- a/GFramework.Core/Events/EventStatistics.cs +++ b/GFramework.Core/Events/EventStatistics.cs @@ -9,9 +9,9 @@ namespace GFramework.Core.Events; /// public sealed class EventStatistics : IEventStatistics { - private readonly Dictionary _listenerCountByType = new(); + private readonly Dictionary _listenerCountByType = new(StringComparer.Ordinal); private readonly object _lock = new(); - private readonly Dictionary _publishCountByType = new(); + private readonly Dictionary _publishCountByType = new(StringComparer.Ordinal); private long _totalFailed; private long _totalHandled; private long _totalPublished; @@ -85,27 +85,27 @@ public sealed class EventStatistics : IEventStatistics public string GenerateReport() { var sb = new StringBuilder(); - sb.AppendLine("=== 事件统计报告 ==="); - sb.AppendLine($"总发布数: {TotalPublished}"); - sb.AppendLine($"总处理数: {TotalHandled}"); - sb.AppendLine($"总失败数: {TotalFailed}"); - sb.AppendLine($"活跃事件类型: {ActiveEventTypes}"); - sb.AppendLine($"活跃监听器: {ActiveListeners}"); + sb.AppendLine(FormattableString.Invariant($"=== 事件统计报告 ===")); + sb.AppendLine(FormattableString.Invariant($"总发布数: {TotalPublished}")); + sb.AppendLine(FormattableString.Invariant($"总处理数: {TotalHandled}")); + sb.AppendLine(FormattableString.Invariant($"总失败数: {TotalFailed}")); + sb.AppendLine(FormattableString.Invariant($"活跃事件类型: {ActiveEventTypes}")); + sb.AppendLine(FormattableString.Invariant($"活跃监听器: {ActiveListeners}")); lock (_lock) { if (_publishCountByType.Count > 0) { - sb.AppendLine("\n按事件类型统计(发布次数):"); + sb.AppendLine(FormattableString.Invariant($"\n按事件类型统计(发布次数):")); foreach (var kvp in _publishCountByType.OrderByDescending(x => x.Value)) - sb.AppendLine($" {kvp.Key}: {kvp.Value}"); + sb.AppendLine(FormattableString.Invariant($" {kvp.Key}: {kvp.Value}")); } if (_listenerCountByType.Count > 0) { - sb.AppendLine("\n按事件类型统计(监听器数量):"); + sb.AppendLine(FormattableString.Invariant($"\n按事件类型统计(监听器数量):")); foreach (var kvp in _listenerCountByType.OrderByDescending(x => x.Value)) - sb.AppendLine($" {kvp.Key}: {kvp.Value}"); + sb.AppendLine(FormattableString.Invariant($" {kvp.Key}: {kvp.Value}")); } } @@ -162,4 +162,4 @@ public sealed class EventStatistics : IEventStatistics } } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/AsyncExtensions.cs b/GFramework.Core/Extensions/AsyncExtensions.cs index db2a7ec0..47124086 100644 --- a/GFramework.Core/Extensions/AsyncExtensions.cs +++ b/GFramework.Core/Extensions/AsyncExtensions.cs @@ -118,11 +118,11 @@ public static class AsyncExtensions try { - return await task; + return await task.ConfigureAwait(false); } catch (Exception ex) { return fallback(ex); } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/ContextAwareCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareCommandExtensions.cs index 876c9776..49676e56 100644 --- a/GFramework.Core/Extensions/ContextAwareCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCommandExtensions.cs @@ -54,7 +54,7 @@ public static class ContextAwareCommandExtensions ArgumentNullException.ThrowIfNull(command); var context = contextAware.GetContext(); - await context.SendCommandAsync(command); + await context.SendCommandAsync(command).ConfigureAwait(false); } /// @@ -72,6 +72,6 @@ public static class ContextAwareCommandExtensions ArgumentNullException.ThrowIfNull(command); var context = contextAware.GetContext(); - return await context.SendCommandAsync(command); + return await context.SendCommandAsync(command).ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/ContextAwareQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareQueryExtensions.cs index ce12e22f..63ad260c 100644 --- a/GFramework.Core/Extensions/ContextAwareQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareQueryExtensions.cs @@ -41,6 +41,6 @@ public static class ContextAwareQueryExtensions ArgumentNullException.ThrowIfNull(query); var context = contextAware.GetContext(); - return await context.SendQueryAsync(query); + return await context.SendQueryAsync(query).ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/NumericExtensions.cs b/GFramework.Core/Extensions/NumericExtensions.cs index 448975b4..eaa079e8 100644 --- a/GFramework.Core/Extensions/NumericExtensions.cs +++ b/GFramework.Core/Extensions/NumericExtensions.cs @@ -26,7 +26,7 @@ public static class NumericExtensions public static bool Between(this T value, T min, T max, bool inclusive = true) where T : IComparable { if (min.CompareTo(max) > 0) - throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})"); + throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})", nameof(min)); if (inclusive) return value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0; @@ -71,4 +71,4 @@ public static class NumericExtensions return (value - from) / (to - from); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Functional/Async/AsyncFunctionalExtensions.cs b/GFramework.Core/Functional/Async/AsyncFunctionalExtensions.cs index 5ce9dbd5..cc20ff34 100644 --- a/GFramework.Core/Functional/Async/AsyncFunctionalExtensions.cs +++ b/GFramework.Core/Functional/Async/AsyncFunctionalExtensions.cs @@ -54,14 +54,14 @@ public static class AsyncFunctionalExtensions { try { - return await taskFactory(); + return await taskFactory().ConfigureAwait(false); } catch (Exception ex) { // 若还有重试机会且允许重试,则等待后继续;否则统一包装为 AggregateException 抛出 if (attempt < maxRetries && shouldRetry(ex)) { - await Task.Delay(delay); + await Task.Delay(delay).ConfigureAwait(false); } else { @@ -99,7 +99,7 @@ public static class AsyncFunctionalExtensions try { - var result = await func(); + var result = await func().ConfigureAwait(false); return new Result(result); } catch (Exception ex) @@ -107,4 +107,4 @@ public static class AsyncFunctionalExtensions return new Result(ex); } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Functional/Result.T.cs b/GFramework.Core/Functional/Result.T.cs index ad347cab..06e381c0 100644 --- a/GFramework.Core/Functional/Result.T.cs +++ b/GFramework.Core/Functional/Result.T.cs @@ -201,7 +201,7 @@ public readonly struct Result : IEquatable>, IComparable> try { - return new Result(await f(_value!)); + return new Result(await f(_value!).ConfigureAwait(false)); } catch (Exception ex) { @@ -307,7 +307,7 @@ public readonly struct Result : IEquatable>, IComparable> if (IsSuccess) return EqualityComparer.Default.Equals(_value, other._value); if (IsFaulted) return Exception.GetType() == other.Exception.GetType() - && Exception.Message == other.Exception.Message; + && string.Equals(Exception.Message, other.Exception.Message, StringComparison.Ordinal); return true; // both Bottom } @@ -418,4 +418,4 @@ public readonly struct Result : IEquatable>, IComparable> ResultState.Faulted => $"Fail({Exception.Message})", _ => "(Bottom)" }; -} \ No newline at end of file +} diff --git a/GFramework.Core/Functional/Result.cs b/GFramework.Core/Functional/Result.cs index 994df5f2..7701eda8 100644 --- a/GFramework.Core/Functional/Result.cs +++ b/GFramework.Core/Functional/Result.cs @@ -126,7 +126,7 @@ public readonly struct Result : IEquatable return true; return _exception!.GetType() == other._exception!.GetType() && - _exception.Message == other._exception.Message; + string.Equals(_exception.Message, other._exception.Message, StringComparison.Ordinal); } /// @@ -216,4 +216,4 @@ public readonly struct Result : IEquatable ArgumentNullException.ThrowIfNull(func); return IsSuccess ? func() : Result.Failure(_exception!); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Functional/ResultExtensions.cs b/GFramework.Core/Functional/ResultExtensions.cs index a5cab9cc..6be7870d 100644 --- a/GFramework.Core/Functional/ResultExtensions.cs +++ b/GFramework.Core/Functional/ResultExtensions.cs @@ -111,6 +111,7 @@ public static class ResultExtensions ArgumentNullException.ThrowIfNull(binder); return result.IsSuccess ? await binder(result.Match(succ: v => v, fail: _ => throw new InvalidOperationException())) + .ConfigureAwait(false) : Result.Fail(result.Exception); } @@ -176,7 +177,7 @@ public static class ResultExtensions return result.Match( succ: value => predicate(value) ? result - : Result.Fail(new ArgumentException(errorMessage)), + : Result.Fail(new ArgumentException(errorMessage, nameof(errorMessage))), fail: _ => result ); } @@ -233,7 +234,7 @@ public static class ResultExtensions ArgumentNullException.ThrowIfNull(func); try { - return Result.Succeed(await func()); + return Result.Succeed(await func().ConfigureAwait(false)); } catch (Exception ex) { @@ -261,7 +262,7 @@ public static class ResultExtensions string errorMessage = "Value is null") where T : class => value is not null ? Result.Succeed(value) - : Result.Fail(new ArgumentNullException(errorMessage)); + : Result.Fail(new ArgumentNullException(nameof(value), errorMessage)); /// /// 将可空值类型转换为 Result @@ -271,7 +272,7 @@ public static class ResultExtensions string errorMessage = "Value is null") where T : struct => value.HasValue ? Result.Succeed(value.Value) - : Result.Fail(new ArgumentNullException(errorMessage)); + : Result.Fail(new ArgumentNullException(nameof(value), errorMessage)); #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 9da7b6bc..4dd7034b 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -392,7 +392,10 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) var assemblyArray = assemblies.ToArray(); foreach (var assembly in assemblyArray) { - ArgumentNullException.ThrowIfNull(assembly); + if (assembly is null) + { + throw new ArgumentException("Assemblies collection cannot contain null items.", nameof(assemblies)); + } } _lock.EnterWriteLock(); diff --git a/GFramework.Core/Localization/Formatters/CompactNumberLocalizationFormatter.cs b/GFramework.Core/Localization/Formatters/CompactNumberLocalizationFormatter.cs index b37d892a..e91ee978 100644 --- a/GFramework.Core/Localization/Formatters/CompactNumberLocalizationFormatter.cs +++ b/GFramework.Core/Localization/Formatters/CompactNumberLocalizationFormatter.cs @@ -1,3 +1,4 @@ +using System.Globalization; using GFramework.Core.Abstractions.Localization; using GFramework.Core.Abstractions.Utility.Numeric; using GFramework.Core.Utility.Numeric; @@ -155,13 +156,14 @@ public sealed class CompactNumberLocalizationFormatter : ILocalizationFormatter ref bool trimTrailingZeros, ref bool useGroupingBelowThreshold) { + var formatProvider = CultureInfo.InvariantCulture; return key switch { - "maxDecimals" => int.TryParse(value, out maxDecimalPlaces), - "minDecimals" => int.TryParse(value, out minDecimalPlaces), + "maxDecimals" => int.TryParse(value, NumberStyles.Integer, formatProvider, out maxDecimalPlaces), + "minDecimals" => int.TryParse(value, NumberStyles.Integer, formatProvider, out minDecimalPlaces), "trimZeros" => bool.TryParse(value, out trimTrailingZeros), "grouping" => bool.TryParse(value, out useGroupingBelowThreshold), _ => true }; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Localization/LocalizationManager.cs b/GFramework.Core/Localization/LocalizationManager.cs index 9c38cf17..184a76b3 100644 --- a/GFramework.Core/Localization/LocalizationManager.cs +++ b/GFramework.Core/Localization/LocalizationManager.cs @@ -27,8 +27,8 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager public LocalizationManager(LocalizationConfig? config = null) { _config = config ?? new LocalizationConfig(); - _tables = new Dictionary>(); - _formatters = new Dictionary(); + _tables = new Dictionary>(StringComparer.Ordinal); + _formatters = new Dictionary(StringComparer.Ordinal); _languageChangeCallbacks = new List>(); _currentLanguage = _config.DefaultLanguage; _currentCulture = GetCultureInfo(_currentLanguage); @@ -53,7 +53,7 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager throw new ArgumentNullException(nameof(languageCode)); } - if (_currentLanguage == languageCode) + if (string.Equals(_currentLanguage, languageCode, StringComparison.Ordinal)) { return; } @@ -227,11 +227,11 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager return; // 已加载 } - var languageTables = new Dictionary(); + var languageTables = new Dictionary(StringComparer.Ordinal); // 加载回退语言(如果不是默认语言) Dictionary? fallbackTables = null; - if (languageCode != _config.FallbackLanguage) + if (!string.Equals(languageCode, _config.FallbackLanguage, StringComparison.Ordinal)) { LoadLanguage(_config.FallbackLanguage); _tables.TryGetValue(_config.FallbackLanguage, out fallbackTables); @@ -264,7 +264,7 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager { var json = File.ReadAllText(filePath); var data = JsonSerializer.Deserialize>(json); - return data ?? new Dictionary(); + return data ?? new Dictionary(StringComparer.Ordinal); } /// @@ -314,4 +314,4 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager } } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Localization/LocalizationString.cs b/GFramework.Core/Localization/LocalizationString.cs index 15832ab0..c925cf9f 100644 --- a/GFramework.Core/Localization/LocalizationString.cs +++ b/GFramework.Core/Localization/LocalizationString.cs @@ -18,7 +18,10 @@ public class LocalizationString : ILocalizationString /// 预编译的静态正则表达式,用于格式化字符串中的变量替换 /// private static readonly Regex FormatVariableRegex = - new(FormatVariablePattern, RegexOptions.Compiled | RegexOptions.CultureInvariant); + new( + FormatVariablePattern, + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, + TimeSpan.FromSeconds(1)); private readonly ILocalizationManager _manager; private readonly Dictionary _variables; @@ -35,7 +38,7 @@ public class LocalizationString : ILocalizationString _manager = manager ?? throw new ArgumentNullException(nameof(manager)); Table = table ?? throw new ArgumentNullException(nameof(table)); Key = key ?? throw new ArgumentNullException(nameof(key)); - _variables = new Dictionary(); + _variables = new Dictionary(StringComparer.Ordinal); } /// @@ -234,4 +237,4 @@ public class LocalizationString : ILocalizationString { return manager.GetFormatter(name); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Localization/LocalizationTable.cs b/GFramework.Core/Localization/LocalizationTable.cs index 51090892..58528775 100644 --- a/GFramework.Core/Localization/LocalizationTable.cs +++ b/GFramework.Core/Localization/LocalizationTable.cs @@ -32,8 +32,8 @@ public class LocalizationTable : ILocalizationTable { Name = name ?? throw new ArgumentNullException(nameof(name)); Language = language ?? throw new ArgumentNullException(nameof(language)); - _data = new Dictionary(data); - _overrides = new Dictionary(); + _data = new Dictionary(data, StringComparer.Ordinal); + _overrides = new Dictionary(StringComparer.Ordinal); Fallback = fallback; } @@ -105,7 +105,7 @@ public class LocalizationTable : ILocalizationTable /// 包含所有键的可枚举集合 public IEnumerable GetKeys() { - var keys = new HashSet(_data.Keys); + var keys = new HashSet(_data.Keys, StringComparer.Ordinal); keys.UnionWith(_overrides.Keys); if (Fallback != null) @@ -133,4 +133,4 @@ public class LocalizationTable : ILocalizationTable _overrides[key] = value; } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/AppenderConfiguration.cs b/GFramework.Core/Logging/AppenderConfiguration.cs new file mode 100644 index 00000000..5922ee8b --- /dev/null +++ b/GFramework.Core/Logging/AppenderConfiguration.cs @@ -0,0 +1,54 @@ +using GFramework.Core.Abstractions.Logging; + +namespace GFramework.Core.Logging; + +/// +/// Appender 配置。 +/// +public sealed class AppenderConfiguration +{ + /// + /// Appender 类型(Console, File, RollingFile, Async)。 + /// + public string Type { get; set; } = string.Empty; + + /// + /// 格式化器类型(Default, Json)。 + /// + public string Formatter { get; set; } = "Default"; + + /// + /// 文件路径(仅用于 File 和 RollingFile)。 + /// + public string? FilePath { get; set; } + + /// + /// 是否使用颜色(仅用于 Console)。 + /// + public bool UseColors { get; set; } = true; + + /// + /// 缓冲区大小(仅用于 Async)。 + /// + public int BufferSize { get; set; } = 10000; + + /// + /// 最大文件大小(仅用于 RollingFile,字节)。 + /// + public long MaxFileSize { get; set; } = 10 * 1024 * 1024; + + /// + /// 最大文件数量(仅用于 RollingFile)。 + /// + public int MaxFileCount { get; set; } = 5; + + /// + /// 过滤器配置。 + /// + public FilterConfiguration? Filter { get; set; } + + /// + /// 内部 Appender 配置(仅用于 Async)。 + /// + public AppenderConfiguration? InnerAppender { get; set; } +} diff --git a/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs b/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs index af70014f..0a906f46 100644 --- a/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs +++ b/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs @@ -162,7 +162,7 @@ public sealed class AsyncLogAppender : ILogAppender { try { - await foreach (var entry in _channel.Reader.ReadAllAsync(cancellationToken)) + await foreach (var entry in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { try { @@ -235,4 +235,4 @@ public sealed class AsyncLogAppender : ILogAppender // 错误观察者只用于诊断,绝不能反向影响日志处理线程的生命周期。 } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/Appenders/StatisticsAppender.cs b/GFramework.Core/Logging/Appenders/StatisticsAppender.cs index ec2cd008..48fb3f87 100644 --- a/GFramework.Core/Logging/Appenders/StatisticsAppender.cs +++ b/GFramework.Core/Logging/Appenders/StatisticsAppender.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Text; +using System.Globalization; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Time; using GFramework.Core.Time; @@ -13,7 +14,7 @@ namespace GFramework.Core.Logging.Appenders; public sealed class StatisticsAppender : ILogAppender { private readonly ConcurrentDictionary _levelCounts = new(); - private readonly ConcurrentDictionary _loggerCounts = new(); + private readonly ConcurrentDictionary _loggerCounts = new(StringComparer.Ordinal); private readonly ITimeProvider _timeProvider; private long _errorCount; private long _startTimeTicks; @@ -127,7 +128,7 @@ public sealed class StatisticsAppender : ILogAppender /// public IReadOnlyDictionary GetLoggerCounts() { - return new Dictionary(_loggerCounts); + return new Dictionary(_loggerCounts, StringComparer.Ordinal); } /// @@ -151,27 +152,28 @@ public sealed class StatisticsAppender : ILogAppender var startTime = StartTime; var now = _timeProvider.UtcNow; - report.AppendLine("=== 日志统计报告 ==="); - report.AppendLine($"统计时间: {startTime:yyyy-MM-dd HH:mm:ss} - {now:yyyy-MM-dd HH:mm:ss}"); - report.AppendLine($"运行时长: {Uptime}"); - report.AppendLine($"总日志数: {TotalCount}"); - report.AppendLine($"错误日志数: {ErrorCount}"); - report.AppendLine($"错误率: {ErrorRate:P2}"); + report.AppendLine(FormattableString.Invariant($"=== 日志统计报告 ===")); + report.AppendLine(FormattableString.Invariant( + $"统计时间: {startTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)} - {now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)}")); + report.AppendLine(FormattableString.Invariant($"运行时长: {Uptime}")); + report.AppendLine(FormattableString.Invariant($"总日志数: {TotalCount}")); + report.AppendLine(FormattableString.Invariant($"错误日志数: {ErrorCount}")); + report.AppendLine(FormattableString.Invariant($"错误率: {ErrorRate:P2}")); report.AppendLine(); - report.AppendLine("按级别统计:"); + report.AppendLine(FormattableString.Invariant($"按级别统计:")); foreach (var level in Enum.GetValues()) { var count = GetCountByLevel(level); if (count > 0) { var percentage = (double)count / TotalCount; - report.AppendLine($" {level}: {count} ({percentage:P2})"); + report.AppendLine(FormattableString.Invariant($" {level}: {count} ({percentage:P2})")); } } report.AppendLine(); - report.AppendLine("按日志记录器统计 (Top 10):"); + report.AppendLine(FormattableString.Invariant($"按日志记录器统计 (Top 10):")); var topLoggers = _loggerCounts .OrderByDescending(kvp => kvp.Value) .Take(10); @@ -179,9 +181,9 @@ public sealed class StatisticsAppender : ILogAppender foreach (var (logger, count) in topLoggers) { var percentage = (double)count / TotalCount; - report.AppendLine($" {logger}: {count} ({percentage:P2})"); + report.AppendLine(FormattableString.Invariant($" {logger}: {count} ({percentage:P2})")); } return report.ToString(); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/CachedLoggerFactory.cs b/GFramework.Core/Logging/CachedLoggerFactory.cs index 220fc2ad..edea5745 100644 --- a/GFramework.Core/Logging/CachedLoggerFactory.cs +++ b/GFramework.Core/Logging/CachedLoggerFactory.cs @@ -8,7 +8,7 @@ namespace GFramework.Core.Logging; /// public sealed class CachedLoggerFactory : ILoggerFactory { - private readonly ConcurrentDictionary _cache = new(); + private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); private readonly ILoggerFactory _innerFactory; /// @@ -31,4 +31,4 @@ public sealed class CachedLoggerFactory : ILoggerFactory var cacheKey = $"{name}:{minLevel}"; return _cache.GetOrAdd(cacheKey, _ => _innerFactory.GetLogger(name, minLevel)); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/CompositeLogger.cs b/GFramework.Core/Logging/CompositeLogger.cs index fe5dda40..c551f91e 100644 --- a/GFramework.Core/Logging/CompositeLogger.cs +++ b/GFramework.Core/Logging/CompositeLogger.cs @@ -74,7 +74,7 @@ public sealed class CompositeLogger : AbstractLogger, IDisposable if (!IsEnabled(level)) return; var propsDict = properties.Length > 0 - ? properties.ToDictionary(p => p.Key, p => p.Value) + ? properties.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal) : null; var entry = new LogEntry( @@ -104,7 +104,7 @@ public sealed class CompositeLogger : AbstractLogger, IDisposable if (!IsEnabled(level)) return; var propsDict = properties.Length > 0 - ? properties.ToDictionary(p => p.Key, p => p.Value) + ? properties.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal) : null; var entry = new LogEntry( @@ -131,4 +131,4 @@ public sealed class CompositeLogger : AbstractLogger, IDisposable appender.Flush(); } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs new file mode 100644 index 00000000..9bea072f --- /dev/null +++ b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs @@ -0,0 +1,78 @@ +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging.Appenders; + +namespace GFramework.Core.Logging; + +/// +/// 可配置的 Logger 工厂。 +/// +internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable +{ + private readonly ILogAppender[] _appenders; + private readonly LoggingConfiguration _config; + private bool _disposed; + + /// + /// 初始化一个基于日志配置创建输出管线的工厂实例。 + /// + /// 日志配置。 + public ConfigurableLoggerFactory(LoggingConfiguration config) + { + _config = config ?? throw new ArgumentNullException(nameof(config)); + _appenders = config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray(); + } + + /// + /// 释放内部 Appender 持有的资源。 + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + foreach (var appender in _appenders) + { + if (appender is IDisposable disposable) + { + disposable.Dispose(); + } + } + + _disposed = true; + } + + /// + /// 为指定名称创建日志记录器,并应用最匹配的命名空间级别配置。 + /// + /// 日志记录器名称。 + /// 调用方传入的默认最小级别。 + /// 可写入日志的记录器实例。 + public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info) + { + var effectiveLevel = _config.MinLevel; + + foreach (var kvp in _config.LoggerLevels) + { + if (string.Equals(name, kvp.Key, StringComparison.Ordinal) || + name.StartsWith(kvp.Key + ".", StringComparison.Ordinal)) + { + effectiveLevel = kvp.Value; + break; + } + } + + if (_appenders.Length == 0) + { + return new ConsoleLogger(name, effectiveLevel); + } + + if (_appenders.Length == 1 && _appenders[0] is ConsoleAppender) + { + return new ConsoleLogger(name, effectiveLevel); + } + + return new CompositeLogger(name, effectiveLevel, _appenders); + } +} diff --git a/GFramework.Core/Logging/ConsoleLogger.cs b/GFramework.Core/Logging/ConsoleLogger.cs index c852d785..e1f0f023 100644 --- a/GFramework.Core/Logging/ConsoleLogger.cs +++ b/GFramework.Core/Logging/ConsoleLogger.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using GFramework.Core.Abstractions.Logging; @@ -35,7 +36,7 @@ public sealed class ConsoleLogger( /// 异常信息,可为空 protected override void Write(LogLevel level, string message, Exception? exception) { - var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); var levelStr = LevelStrings[(int)level]; var log = $"[{timestamp}] {levelStr} [{Name()}] {message}"; @@ -81,4 +82,4 @@ public sealed class ConsoleLogger( } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/FilterConfiguration.cs b/GFramework.Core/Logging/FilterConfiguration.cs new file mode 100644 index 00000000..7e9d8db8 --- /dev/null +++ b/GFramework.Core/Logging/FilterConfiguration.cs @@ -0,0 +1,29 @@ +using GFramework.Core.Abstractions.Logging; + +namespace GFramework.Core.Logging; + +/// +/// 过滤器配置。 +/// +public sealed class FilterConfiguration +{ + /// + /// 过滤器类型(LogLevel, Namespace, Composite)。 + /// + public string Type { get; set; } = "LogLevel"; + + /// + /// 最小日志级别(用于 LogLevel 过滤器)。 + /// + public LogLevel? MinLevel { get; set; } + + /// + /// 命名空间前缀列表(用于 Namespace 过滤器)。 + /// + public List? Namespaces { get; set; } + + /// + /// 子过滤器列表(用于 Composite 过滤器)。 + /// + public List? Filters { get; set; } +} diff --git a/GFramework.Core/Logging/Filters/SamplingFilter.cs b/GFramework.Core/Logging/Filters/SamplingFilter.cs index 9bdf4a31..0d170ef3 100644 --- a/GFramework.Core/Logging/Filters/SamplingFilter.cs +++ b/GFramework.Core/Logging/Filters/SamplingFilter.cs @@ -14,7 +14,7 @@ public sealed class SamplingFilter : ILogFilter private const int DefaultMaxLoggers = 1000; private readonly int _maxLoggers; private readonly int _sampleRate; - private readonly ConcurrentDictionary _samplingStates = new(); + private readonly ConcurrentDictionary _samplingStates = new(StringComparer.Ordinal); private readonly ITimeProvider _timeProvider; private readonly TimeSpan _timeWindow; @@ -69,7 +69,7 @@ public sealed class SamplingFilter : ILogFilter foreach (var kvp in _samplingStates) { - if (kvp.Key == "*") continue; // 不清理共享状态 + if (string.Equals(kvp.Key, "*", StringComparison.Ordinal)) continue; // 不清理共享状态 if (kvp.Value.IsStale(now, staleThreshold)) { @@ -132,4 +132,4 @@ public sealed class SamplingFilter : ILogFilter return now - lastAccess > staleThreshold; } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/Formatters/DefaultLogFormatter.cs b/GFramework.Core/Logging/Formatters/DefaultLogFormatter.cs index 4c2dcef0..0d723343 100644 --- a/GFramework.Core/Logging/Formatters/DefaultLogFormatter.cs +++ b/GFramework.Core/Logging/Formatters/DefaultLogFormatter.cs @@ -1,5 +1,6 @@ using System; using System.Text; +using System.Globalization; using GFramework.Core.Abstractions.Logging; namespace GFramework.Core.Logging.Formatters; @@ -26,7 +27,7 @@ public sealed class DefaultLogFormatter : ILogFormatter /// 格式化后的日志字符串 public string Format(LogEntry entry) { - var timestamp = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var timestamp = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); var levelStr = LevelStrings[(int)entry.Level]; var sb = new StringBuilder(); @@ -54,4 +55,4 @@ public sealed class DefaultLogFormatter : ILogFormatter return sb.ToString(); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/Formatters/JsonLogFormatter.cs b/GFramework.Core/Logging/Formatters/JsonLogFormatter.cs index a48ce7e2..61ee1d41 100644 --- a/GFramework.Core/Logging/Formatters/JsonLogFormatter.cs +++ b/GFramework.Core/Logging/Formatters/JsonLogFormatter.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Globalization; using GFramework.Core.Abstractions.Logging; namespace GFramework.Core.Logging.Formatters; @@ -21,9 +22,9 @@ public sealed class JsonLogFormatter : ILogFormatter /// JSON 格式的日志字符串 public string Format(LogEntry entry) { - var logObject = new Dictionary + var logObject = new Dictionary(StringComparer.Ordinal) { - ["timestamp"] = entry.Timestamp.ToString("O"), // ISO 8601 格式 + ["timestamp"] = entry.Timestamp.ToString("O", CultureInfo.InvariantCulture), // ISO 8601 格式 ["level"] = entry.Level.ToString().ToUpperInvariant(), ["logger"] = entry.LoggerName, ["message"] = entry.Message @@ -49,4 +50,4 @@ public sealed class JsonLogFormatter : ILogFormatter return JsonSerializer.Serialize(logObject, JsonOptions); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/LoggingConfiguration.cs b/GFramework.Core/Logging/LoggingConfiguration.cs index 76681a96..c9cec380 100644 --- a/GFramework.Core/Logging/LoggingConfiguration.cs +++ b/GFramework.Core/Logging/LoggingConfiguration.cs @@ -20,82 +20,5 @@ public sealed class LoggingConfiguration /// /// 特定 Logger 的日志级别配置 /// - public Dictionary LoggerLevels { get; set; } = new(); + public Dictionary LoggerLevels { get; set; } = new(StringComparer.Ordinal); } - -/// -/// Appender 配置 -/// -public sealed class AppenderConfiguration -{ - /// - /// Appender 类型(Console, File, RollingFile, Async) - /// - public string Type { get; set; } = string.Empty; - - /// - /// 格式化器类型(Default, Json) - /// - public string Formatter { get; set; } = "Default"; - - /// - /// 文件路径(仅用于 File 和 RollingFile) - /// - public string? FilePath { get; set; } - - /// - /// 是否使用颜色(仅用于 Console) - /// - public bool UseColors { get; set; } = true; - - /// - /// 缓冲区大小(仅用于 Async) - /// - public int BufferSize { get; set; } = 10000; - - /// - /// 最大文件大小(仅用于 RollingFile,字节) - /// - public long MaxFileSize { get; set; } = 10 * 1024 * 1024; // 10MB - - /// - /// 最大文件数量(仅用于 RollingFile) - /// - public int MaxFileCount { get; set; } = 5; - - /// - /// 过滤器配置 - /// - public FilterConfiguration? Filter { get; set; } - - /// - /// 内部 Appender 配置(仅用于 Async) - /// - public AppenderConfiguration? InnerAppender { get; set; } -} - -/// -/// 过滤器配置 -/// -public sealed class FilterConfiguration -{ - /// - /// 过滤器类型(LogLevel, Namespace, Composite) - /// - public string Type { get; set; } = "LogLevel"; - - /// - /// 最小日志级别(用于 LogLevel 过滤器) - /// - public LogLevel? MinLevel { get; set; } - - /// - /// 命名空间前缀列表(用于 Namespace 过滤器) - /// - public List? Namespaces { get; set; } - - /// - /// 子过滤器列表(用于 Composite 过滤器) - /// - public List? Filters { get; set; } -} \ No newline at end of file diff --git a/GFramework.Core/Logging/LoggingConfigurationLoader.cs b/GFramework.Core/Logging/LoggingConfigurationLoader.cs index d0565b67..545787e2 100644 --- a/GFramework.Core/Logging/LoggingConfigurationLoader.cs +++ b/GFramework.Core/Logging/LoggingConfigurationLoader.cs @@ -127,66 +127,3 @@ public static class LoggingConfigurationLoader }; } } - -/// -/// 可配置的 Logger 工厂 -/// -internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable -{ - private readonly ILogAppender[] _appenders; - private readonly LoggingConfiguration _config; - private bool _disposed; - - public ConfigurableLoggerFactory(LoggingConfiguration config) - { - _config = config ?? throw new ArgumentNullException(nameof(config)); - _appenders = config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray(); - } - - public void Dispose() - { - if (_disposed) - return; - - foreach (var appender in _appenders) - { - if (appender is IDisposable disposable) - { - disposable.Dispose(); - } - } - - _disposed = true; - } - - public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info) - { - // 检查是否有特定 Logger 的级别配置(支持前缀匹配) - var effectiveLevel = _config.MinLevel; - - foreach (var kvp in _config.LoggerLevels) - { - // 精确匹配或前缀匹配(命名空间层级) - if (name == kvp.Key || name.StartsWith(kvp.Key + ".", StringComparison.Ordinal)) - { - effectiveLevel = kvp.Value; - break; - } - } - - // 如果没有 Appender,返回简单的 ConsoleLogger - if (_appenders.Length == 0) - { - return new ConsoleLogger(name, effectiveLevel); - } - - // 如果只有一个 Appender 且是 ConsoleAppender,优化为 ConsoleLogger - if (_appenders.Length == 1 && _appenders[0] is ConsoleAppender) - { - return new ConsoleLogger(name, effectiveLevel); - } - - // 返回 CompositeLogger - return new CompositeLogger(name, effectiveLevel, _appenders); - } -} \ No newline at end of file diff --git a/GFramework.Core/Pool/AbstractObjectPoolSystem.cs b/GFramework.Core/Pool/AbstractObjectPoolSystem.cs index 7afe321c..95c25b3c 100644 --- a/GFramework.Core/Pool/AbstractObjectPoolSystem.cs +++ b/GFramework.Core/Pool/AbstractObjectPoolSystem.cs @@ -15,7 +15,7 @@ public abstract class AbstractObjectPoolSystem /// /// 存储对象池的字典,键为池标识,值为池信息 /// - protected readonly Dictionary Pools = new(); + protected readonly IDictionary Pools = new Dictionary(); /// /// 获取对象池中的对象,如果池中没有可用对象则创建新的对象 @@ -254,4 +254,4 @@ public abstract class AbstractObjectPoolSystem /// public int ActiveCount { get; set; } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Resource/ResourceCache.cs b/GFramework.Core/Resource/ResourceCache.cs index 440e93b0..cffa2908 100644 --- a/GFramework.Core/Resource/ResourceCache.cs +++ b/GFramework.Core/Resource/ResourceCache.cs @@ -3,19 +3,8 @@ using System.Collections.Concurrent; namespace GFramework.Core.Resource; /// -/// 资源缓存条目 -/// -internal sealed class ResourceCacheEntry(object resource, Type resourceType) -{ - public object Resource { get; } = resource ?? throw new ArgumentNullException(nameof(resource)); - public Type ResourceType { get; } = resourceType ?? throw new ArgumentNullException(nameof(resourceType)); - public int ReferenceCount { get; set; } - public DateTime LastAccessTime { get; set; } = DateTime.UtcNow; -} - -/// -/// 资源缓存系统,管理已加载资源的缓存和引用计数 -/// 线程安全:所有公共方法都是线程安全的 +/// 资源缓存系统,管理已加载资源的缓存和引用计数。 +/// 线程安全:所有公共方法都是线程安全的。 /// internal sealed class ResourceCache { @@ -24,7 +13,7 @@ internal sealed class ResourceCache /// private const string PathCannotBeNullOrEmptyMessage = "Path cannot be null or whitespace."; - private readonly ConcurrentDictionary _cache = new(); + private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); private readonly object _lock = new(); /// @@ -208,4 +197,4 @@ internal sealed class ResourceCache return unreferencedPaths; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Resource/ResourceCacheEntry.cs b/GFramework.Core/Resource/ResourceCacheEntry.cs new file mode 100644 index 00000000..46536f59 --- /dev/null +++ b/GFramework.Core/Resource/ResourceCacheEntry.cs @@ -0,0 +1,27 @@ +namespace GFramework.Core.Resource; + +/// +/// 资源缓存条目。 +/// +internal sealed class ResourceCacheEntry(object resource, Type resourceType) +{ + /// + /// 获取缓存中的资源实例。 + /// + public object Resource { get; } = resource ?? throw new ArgumentNullException(nameof(resource)); + + /// + /// 获取资源的运行时类型。 + /// + public Type ResourceType { get; } = resourceType ?? throw new ArgumentNullException(nameof(resourceType)); + + /// + /// 获取或设置当前引用计数。 + /// + public int ReferenceCount { get; set; } + + /// + /// 获取或设置最近访问时间。 + /// + public DateTime LastAccessTime { get; set; } = DateTime.UtcNow; +} diff --git a/GFramework.Core/Resource/ResourceManager.cs b/GFramework.Core/Resource/ResourceManager.cs index 11c30a05..ebd30bc7 100644 --- a/GFramework.Core/Resource/ResourceManager.cs +++ b/GFramework.Core/Resource/ResourceManager.cs @@ -104,7 +104,7 @@ public class ResourceManager : IResourceManager try { - var resource = await loader.LoadAsync(path); + var resource = await loader.LoadAsync(path).ConfigureAwait(false); lock (_loadLock) { // 双重检查 @@ -231,7 +231,7 @@ public class ResourceManager : IResourceManager /// public async Task PreloadAsync(string path) where T : class { - await LoadAsync(path); + await LoadAsync(path).ConfigureAwait(false); } /// @@ -324,4 +324,4 @@ public class ResourceManager : IResourceManager } } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Services/ServiceModuleManager.cs b/GFramework.Core/Services/ServiceModuleManager.cs index a3965f4d..73e895c9 100644 --- a/GFramework.Core/Services/ServiceModuleManager.cs +++ b/GFramework.Core/Services/ServiceModuleManager.cs @@ -30,7 +30,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager return; } - if (_modules.Any(m => m.ModuleName == module.ModuleName)) + if (_modules.Any(m => string.Equals(m.ModuleName, module.ModuleName, StringComparison.Ordinal))) { _logger.Warn($"Module {module.ModuleName} already registered"); return; @@ -109,7 +109,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager if (asyncMode && module is IAsyncInitializable asyncInitializable) { - await asyncInitializable.InitializeAsync(); + await asyncInitializable.InitializeAsync().ConfigureAwait(false); } else { @@ -137,7 +137,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager try { _logger.Debug($"Destroying module: {module.ModuleName}"); - await module.DestroyAsync(); + await module.DestroyAsync().ConfigureAwait(false); } catch (Exception ex) { diff --git a/GFramework.Core/State/StateMachine.cs b/GFramework.Core/State/StateMachine.cs index 2d0c3f58..f0e8dfa1 100644 --- a/GFramework.Core/State/StateMachine.cs +++ b/GFramework.Core/State/StateMachine.cs @@ -17,7 +17,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine /// /// 存储所有已注册状态的字典,键为状态类型,值为状态实例 /// - protected readonly Dictionary States = new(); + protected readonly IDictionary States = new Dictionary(); /// /// 获取当前激活的状态 @@ -45,7 +45,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine /// 要注销的状态类型 public async Task UnregisterAsync() where T : IState { - await _transitionLock.WaitAsync(); + await _transitionLock.WaitAsync().ConfigureAwait(false); try { var stateToUnregister = PrepareUnregister(out var isCurrentState); @@ -53,7 +53,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (isCurrentState) { - await ExecuteExitAsync(Current!, null); + await ExecuteExitAsync(Current!, null).ConfigureAwait(false); Current = null; } @@ -73,7 +73,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine /// 如果可以切换则返回true,否则返回false public async Task CanChangeToAsync() where T : IState { - await _transitionLock.WaitAsync(); + await _transitionLock.WaitAsync().ConfigureAwait(false); try { if (!States.TryGetValue(typeof(T), out var target)) @@ -81,7 +81,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (Current == null) return true; - return await CanTransitionToAsync(Current, target); + return await CanTransitionToAsync(Current, target).ConfigureAwait(false); } finally { @@ -98,7 +98,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine /// 当目标状态未注册时抛出 public async Task ChangeToAsync() where T : IState { - await _transitionLock.WaitAsync(); + await _transitionLock.WaitAsync().ConfigureAwait(false); try { IState target; @@ -114,15 +114,15 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (currentSnapshot != null) { - var canTransition = await CanTransitionToAsync(currentSnapshot, target); + var canTransition = await CanTransitionToAsync(currentSnapshot, target).ConfigureAwait(false); if (!canTransition) { - await OnTransitionRejectedAsync(currentSnapshot, target); + await OnTransitionRejectedAsync(currentSnapshot, target).ConfigureAwait(false); return false; } } - await ChangeInternalAsync(target); + await ChangeInternalAsync(target).ConfigureAwait(false); return true; } finally @@ -190,13 +190,13 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine /// 如果成功回退则返回true,否则返回false public async Task GoBackAsync() { - await _transitionLock.WaitAsync(); + await _transitionLock.WaitAsync().ConfigureAwait(false); try { var previousState = FindValidPreviousState(); if (previousState == null) return false; - await ChangeInternalWithoutHistoryAsync(previousState); + await ChangeInternalWithoutHistoryAsync(previousState).ConfigureAwait(false); return true; } finally @@ -282,13 +282,13 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (Current == next) return; var old = Current; - await OnStateChangingAsync(old, next); + await OnStateChangingAsync(old, next).ConfigureAwait(false); - await ExecuteExitAsync(old, next); + await ExecuteExitAsync(old, next).ConfigureAwait(false); Current = next; - await ExecuteEnterAsync(Current, old); + await ExecuteEnterAsync(Current, old).ConfigureAwait(false); - await OnStateChangedAsync(old, Current); + await OnStateChangedAsync(old, Current).ConfigureAwait(false); } /// @@ -300,18 +300,18 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (Current == next) return; var old = Current; - await OnStateChangingAsync(old, next); + await OnStateChangingAsync(old, next).ConfigureAwait(false); - await ExecuteExitAsync(old, next); + await ExecuteExitAsync(old, next).ConfigureAwait(false); AddToHistory(old); Current = next; - await ExecuteEnterAsync(Current, old); + await ExecuteEnterAsync(Current, old).ConfigureAwait(false); - await OnStateChangedAsync(old, Current); + await OnStateChangedAsync(old, Current).ConfigureAwait(false); } /// @@ -344,7 +344,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (state == null) return; if (state is IAsyncState asyncState) - await asyncState.OnEnterAsync(from); + await asyncState.OnEnterAsync(from).ConfigureAwait(false); else state.OnEnter(from); } @@ -357,7 +357,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (state == null) return; if (state is IAsyncState asyncState) - await asyncState.OnExitAsync(to); + await asyncState.OnExitAsync(to).ConfigureAwait(false); else state.OnExit(to); } @@ -368,7 +368,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine private static async Task CanTransitionToAsync(IState current, IState target) { if (current is IAsyncState asyncState) - return await asyncState.CanTransitionToAsync(target); + return await asyncState.CanTransitionToAsync(target).ConfigureAwait(false); return current.CanTransitionTo(target); } @@ -432,4 +432,4 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine OnStateChanged(from, to); return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/GFramework.Core/State/StateMachineSystem.cs b/GFramework.Core/State/StateMachineSystem.cs index 773a13a2..5fe02103 100644 --- a/GFramework.Core/State/StateMachineSystem.cs +++ b/GFramework.Core/State/StateMachineSystem.cs @@ -72,7 +72,7 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem { if (Current is IAsyncState asyncState) { - await asyncState.OnExitAsync(null); // ✅ 正确等待异步清理 + await asyncState.OnExitAsync(null).ConfigureAwait(false); // ✅ 正确等待异步清理 } else { @@ -87,7 +87,7 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem { if (state is IAsyncDestroyable asyncDestroyable) { - await asyncDestroyable.DestroyAsync(); + await asyncDestroyable.DestroyAsync().ConfigureAwait(false); } else if (state is IDestroyable destroyable) { @@ -106,7 +106,7 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem protected override async Task ChangeInternalAsync(IState next) { var old = Current; - await base.ChangeInternalAsync(next); + await base.ChangeInternalAsync(next).ConfigureAwait(false); // 发送状态变更事件,通知监听者状态已发生改变 this.SendEvent(new StateChangedEvent @@ -115,4 +115,4 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem NewState = Current }); } -} \ No newline at end of file +} diff --git a/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs index 63896c92..7cc2b57b 100644 --- a/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs +++ b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs @@ -50,7 +50,7 @@ public sealed class LoggingBehavior : IPipelineBehavior : IPipelineBehavior try { - return await next(message, cancellationToken); + return await next(message, cancellationToken).ConfigureAwait(false); } finally { diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 5f7fa643..108f6c03 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -73,7 +73,7 @@ internal sealed class CqrsDispatcher( foreach (var handler in handlers) { PrepareHandler(handler, context); - await dispatchBinding.Invoker(handler, notification, cancellationToken); + await dispatchBinding.Invoker(handler, notification, cancellationToken).ConfigureAwait(false); } } @@ -106,9 +106,10 @@ internal sealed class CqrsDispatcher( PrepareHandler(behavior, context); if (behaviors.Count == 0) - return await dispatchBinding.RequestInvoker(handler, request, cancellationToken); + return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false); - return await dispatchBinding.PipelineInvoker(handler, behaviors, request, cancellationToken); + return await dispatchBinding.PipelineInvoker(handler, behaviors, request, cancellationToken) + .ConfigureAwait(false); } /// diff --git a/GFramework.Cqrs/Internal/WeakKeyCache.cs b/GFramework.Cqrs/Internal/WeakKeyCache.cs index dadf2db4..2d97bd3d 100644 --- a/GFramework.Cqrs/Internal/WeakKeyCache.cs +++ b/GFramework.Cqrs/Internal/WeakKeyCache.cs @@ -44,8 +44,8 @@ internal sealed class WeakKeyCache if (entries.TryGetValue(key, out cachedValue)) return cachedValue; - var createdValue = valueFactory(key); - ArgumentNullException.ThrowIfNull(createdValue); + var createdValue = valueFactory(key) ?? + throw new InvalidOperationException("The value factory returned null."); entries.Add(key, createdValue); return createdValue; } @@ -78,8 +78,8 @@ internal sealed class WeakKeyCache if (entries.TryGetValue(key, out cachedValue)) return cachedValue; - var createdValue = valueFactory(key, state); - ArgumentNullException.ThrowIfNull(createdValue); + var createdValue = valueFactory(key, state) ?? + throw new InvalidOperationException("The value factory returned null."); entries.Add(key, createdValue); return createdValue; } @@ -125,90 +125,3 @@ internal sealed class WeakKeyCache return TryGetValue(key, out var value) ? value : null; } } - -/// -/// 提供以两段 为键的弱引用缓存。 -/// 适用于请求/响应或流请求/响应这类组合类型元数据的复用场景。 -/// -/// 缓存值类型。 -/// -/// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用, -/// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。 -/// -internal sealed class WeakTypePairCache - where TValue : class -{ - private readonly WeakKeyCache> _entries = new(); - - /// - /// 获取指定类型对对应的缓存值;若未命中则创建并写入。 - /// - /// 第一段类型键。 - /// 第二段类型键。 - /// 创建缓存值的工厂方法。 - /// 已存在或新创建的缓存值。 - /// - /// 或 - /// 。 - /// - public TValue GetOrAdd(Type primaryType, Type secondaryType, Func valueFactory) - { - ArgumentNullException.ThrowIfNull(primaryType); - ArgumentNullException.ThrowIfNull(secondaryType); - ArgumentNullException.ThrowIfNull(valueFactory); - - var secondaryEntries = _entries.GetOrAdd(primaryType, static _ => new WeakKeyCache()); - return secondaryEntries.GetOrAdd( - secondaryType, - (PrimaryType: primaryType, Factory: valueFactory), - static (cachedSecondaryType, state) => state.Factory(state.PrimaryType, cachedSecondaryType)); - } - - /// - /// 尝试读取指定类型对的缓存值,而不触发新的创建逻辑。 - /// - /// 第一段类型键。 - /// 第二段类型键。 - /// 命中时返回的缓存值。 - /// 若命中当前缓存则为 ;否则为 - /// - /// 。 - /// - public bool TryGetValue(Type primaryType, Type secondaryType, out TValue? value) - { - ArgumentNullException.ThrowIfNull(primaryType); - ArgumentNullException.ThrowIfNull(secondaryType); - - if (_entries.TryGetValue(primaryType, out var secondaryEntries) && - secondaryEntries is not null) - return secondaryEntries.TryGetValue(secondaryType, out value); - - value = null; - return false; - } - - /// - /// 清空当前缓存实例。 - /// - /// - /// 该方法主要服务于测试,避免同一进程里的静态缓存污染后续断言。 - /// - public void Clear() - { - _entries.Clear(); - } - - /// - /// 返回指定类型对当前命中的缓存对象;若未命中则返回 。 - /// - /// 第一段类型键。 - /// 第二段类型键。 - /// 当前缓存对象,或 - /// - /// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。 - /// - public TValue? GetValueOrDefaultForTesting(Type primaryType, Type secondaryType) - { - return TryGetValue(primaryType, secondaryType, out var value) ? value : null; - } -} diff --git a/GFramework.Cqrs/Internal/WeakTypePairCache.cs b/GFramework.Cqrs/Internal/WeakTypePairCache.cs new file mode 100644 index 00000000..4742a908 --- /dev/null +++ b/GFramework.Cqrs/Internal/WeakTypePairCache.cs @@ -0,0 +1,88 @@ +namespace GFramework.Cqrs.Internal; + +/// +/// 提供以两段 为键的弱引用缓存。 +/// 适用于请求/响应或流请求/响应这类组合类型元数据的复用场景。 +/// +/// 缓存值类型。 +/// +/// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用, +/// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。 +/// +internal sealed class WeakTypePairCache + where TValue : class +{ + private readonly WeakKeyCache> _entries = new(); + + /// + /// 获取指定类型对对应的缓存值;若未命中则创建并写入。 + /// + /// 第一段类型键。 + /// 第二段类型键。 + /// 创建缓存值的工厂方法。 + /// 已存在或新创建的缓存值。 + /// + /// 或 + /// 。 + /// + public TValue GetOrAdd(Type primaryType, Type secondaryType, Func valueFactory) + { + ArgumentNullException.ThrowIfNull(primaryType); + ArgumentNullException.ThrowIfNull(secondaryType); + ArgumentNullException.ThrowIfNull(valueFactory); + + var secondaryEntries = _entries.GetOrAdd(primaryType, static _ => new WeakKeyCache()); + return secondaryEntries.GetOrAdd( + secondaryType, + (PrimaryType: primaryType, Factory: valueFactory), + static (cachedSecondaryType, state) => state.Factory(state.PrimaryType, cachedSecondaryType)); + } + + /// + /// 尝试读取指定类型对的缓存值,而不触发新的创建逻辑。 + /// + /// 第一段类型键。 + /// 第二段类型键。 + /// 命中时返回的缓存值。 + /// 若命中当前缓存则为 ;否则为 + /// + /// 。 + /// + public bool TryGetValue(Type primaryType, Type secondaryType, out TValue? value) + { + ArgumentNullException.ThrowIfNull(primaryType); + ArgumentNullException.ThrowIfNull(secondaryType); + + if (_entries.TryGetValue(primaryType, out var secondaryEntries) && + secondaryEntries is not null) + return secondaryEntries.TryGetValue(secondaryType, out value); + + value = null; + return false; + } + + /// + /// 清空当前缓存实例。 + /// + /// + /// 该方法主要服务于测试,避免同一进程里的静态缓存污染后续断言。 + /// + public void Clear() + { + _entries.Clear(); + } + + /// + /// 返回指定类型对当前命中的缓存对象;若未命中则返回 。 + /// + /// 第一段类型键。 + /// 第二段类型键。 + /// 当前缓存对象,或 + /// + /// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。 + /// + public TValue? GetValueOrDefaultForTesting(Type primaryType, Type secondaryType) + { + return TryGetValue(primaryType, secondaryType, out var value) ? value : null; + } +} diff --git a/GFramework.Godot/Architectures/AbstractArchitecture.cs b/GFramework.Godot/Architectures/AbstractArchitecture.cs index 45f20e4d..aebb2df5 100644 --- a/GFramework.Godot/Architectures/AbstractArchitecture.cs +++ b/GFramework.Godot/Architectures/AbstractArchitecture.cs @@ -85,7 +85,7 @@ public abstract class AbstractArchitecture( Name = _architectureAnchorName }; - _anchor.Bind(() => DestroyAsync().AsTask()); + _anchor.Bind(() => _ = DestroyAsync().AsTask()); tree.Root.CallDeferred(Node.MethodName.AddChild, _anchor); } @@ -106,7 +106,7 @@ public abstract class AbstractArchitecture( throw new InvalidOperationException("Anchor not initialized"); // 等待锚点准备就绪 - await _anchor.WaitUntilReadyAsync(); + await _anchor.WaitUntilReadyAsync().ConfigureAwait(false); // 延迟调用将扩展节点添加为锚点的子节点 _anchor.CallDeferred(Node.MethodName.AddChild, module.Node); @@ -136,6 +136,6 @@ public abstract class AbstractArchitecture( _extensions.Clear(); - await base.DestroyAsync(); + await base.DestroyAsync().ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Config/GodotYamlConfigDirectoryEntry.cs b/GFramework.Godot/Config/GodotYamlConfigDirectoryEntry.cs new file mode 100644 index 00000000..839b832b --- /dev/null +++ b/GFramework.Godot/Config/GodotYamlConfigDirectoryEntry.cs @@ -0,0 +1,33 @@ +namespace GFramework.Godot.Config; + +/// +/// 描述一次目录枚举返回的单个子项。 +/// +/// +/// 该结构只承载目录扫描阶段需要的最小信息。 +/// 必须是单个目录项名称,而不是包含父目录的完整路径; +/// 对于 Godot 路径和普通路径都遵循相同约定,便于加载器统一做后续拼接与过滤。 +/// +internal readonly record struct GodotYamlConfigDirectoryEntry +{ + /// + /// 初始化一个目录枚举结果项。 + /// + /// 当前目录项的名称,不包含父目录路径。 + /// 指示该目录项是否为子目录。 + public GodotYamlConfigDirectoryEntry(string name, bool isDirectory) + { + Name = name; + IsDirectory = isDirectory; + } + + /// + /// 获取当前目录项的名称,不包含父目录路径。 + /// + public string Name { get; } + + /// + /// 获取一个值,指示当前目录项是否为子目录。 + /// + public bool IsDirectory { get; } +} diff --git a/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs b/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs new file mode 100644 index 00000000..79a40390 --- /dev/null +++ b/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs @@ -0,0 +1,163 @@ +using System.IO; +using FileAccess = Godot.FileAccess; + +namespace GFramework.Godot.Config; + +/// +/// 抽象 与具体宿主环境之间的 Godot 路径和文件访问边界。 +/// +/// +/// 该抽象存在的原因,是编辑器态与导出态对 res://user:// 的访问方式不同: +/// 编辑器态通常可以把 Godot 特殊路径全局化后直接落到普通文件系统,而导出态往往只能通过 Godot API 读取原始文本资源, +/// 再把它们复制到运行时缓存目录。 在目录不存在或当前环境无法枚举时必须返回 +/// ,用来表达“不可访问”而不是抛出未找到异常; 则应保留底层读取失败异常, +/// 交由加载器包装成配置诊断。对于普通文件系统路径,应遵循 / 语义; +/// 对于 Godot 特殊路径,则应使用引擎提供的路径解析和读取能力。 +/// +internal sealed class GodotYamlConfigEnvironment +{ + /// + /// 初始化一个可替换的 Godot YAML 配置宿主环境抽象。 + /// + /// 返回当前进程是否处于 Godot 编辑器态的委托。 + /// + /// 把 Godot 特殊路径转换为普通绝对路径的委托。 + /// 当前加载器仅会在输入为 res://user:// 时调用它,返回值必须为非空绝对路径。 + /// + /// + /// 枚举指定目录直接子项的委托。 + /// 当目录不存在、无法访问或当前环境无法枚举该路径时,必须返回 。 + /// + /// + /// 检查指定路径上的文件是否存在的委托。 + /// 输入既可能是 Godot 特殊路径,也可能是普通绝对路径。 + /// + /// + /// 读取指定文件完整字节内容的委托。 + /// 当文件缺失或读取失败时,应抛出底层异常,由加载器统一包装为配置加载诊断。 + /// + /// 任一委托参数为 时抛出。 + public GodotYamlConfigEnvironment( + Func isEditor, + Func globalizePath, + Func?> enumerateDirectory, + Func fileExists, + Func readAllBytes) + { + IsEditor = isEditor ?? throw new ArgumentNullException(nameof(isEditor)); + GlobalizePath = globalizePath ?? throw new ArgumentNullException(nameof(globalizePath)); + EnumerateDirectory = enumerateDirectory ?? throw new ArgumentNullException(nameof(enumerateDirectory)); + FileExists = fileExists ?? throw new ArgumentNullException(nameof(fileExists)); + ReadAllBytes = readAllBytes ?? throw new ArgumentNullException(nameof(readAllBytes)); + } + + /// + /// 获取默认的 Godot 运行时环境实现。 + /// + /// + /// 默认实现使用 检测编辑器态, + /// 使用 处理 Godot 特殊路径, + /// 并在 Godot 路径与普通路径之间切换对应的枚举和读取 API。 + /// + public static GodotYamlConfigEnvironment Default { get; } = new( + static () => OS.HasFeature("editor"), + static path => ProjectSettings.GlobalizePath(path), + EnumerateDirectoryCore, + FileExistsCore, + ReadAllBytesCore); + + /// + /// 获取用于判断当前进程是否处于编辑器态的委托。 + /// + public Func IsEditor { get; } + + /// + /// 获取把 Godot 特殊路径转换为普通绝对路径的委托。 + /// + /// + /// 当前加载器只会对 res://user:// 路径调用该委托。 + /// 返回空字符串会被视为无效环境实现,并在后续路径解析阶段触发异常。 + /// + public Func GlobalizePath { get; } + + /// + /// 获取用于枚举目录直接子项的委托。 + /// + /// + /// 当目录不存在、无法访问,或当前环境无法枚举给定路径时,该委托必须返回 。 + /// 返回的集合只应包含当前目录下的直接子项,调用方会自行过滤隐藏项、子目录与非 YAML 文件。 + /// + public Func?> EnumerateDirectory { get; } + + /// + /// 获取用于检查文件是否存在的委托。 + /// + public Func FileExists { get; } + + /// + /// 获取用于读取文件完整字节内容的委托。 + /// + /// + /// 该委托在路径不存在、权限不足或 I/O 失败时应抛出底层异常,以便加载器保留失败原因并生成诊断信息。 + /// + public Func ReadAllBytes { get; } + + private static IReadOnlyList? EnumerateDirectoryCore(string path) + { + if (!path.IsGodotPath()) + { + if (!Directory.Exists(path)) + { + return null; + } + + return Directory + .EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly) + .Select(static entryPath => new GodotYamlConfigDirectoryEntry( + Path.GetFileName(entryPath), + Directory.Exists(entryPath))) + .ToArray(); + } + + using var directory = DirAccess.Open(path); + if (directory == null) + { + return null; + } + + var entries = new List(); + var listDirectoryError = directory.ListDirBegin(); + if (listDirectoryError != Error.Ok) + { + return null; + } + + while (true) + { + var name = directory.GetNext(); + if (string.IsNullOrEmpty(name)) + { + break; + } + + entries.Add(new GodotYamlConfigDirectoryEntry(name, directory.CurrentIsDir())); + } + + directory.ListDirEnd(); + return entries; + } + + private static bool FileExistsCore(string path) + { + return path.IsGodotPath() + ? FileAccess.FileExists(path) + : File.Exists(path); + } + + private static byte[] ReadAllBytesCore(string path) + { + return path.IsGodotPath() + ? FileAccess.GetFileAsBytes(path) + : File.ReadAllBytes(path); + } +} diff --git a/GFramework.Godot/Config/GodotYamlConfigLoader.cs b/GFramework.Godot/Config/GodotYamlConfigLoader.cs index 51742da9..56e4c415 100644 --- a/GFramework.Godot/Config/GodotYamlConfigLoader.cs +++ b/GFramework.Godot/Config/GodotYamlConfigLoader.cs @@ -153,7 +153,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader SynchronizeRuntimeCache(cancellationToken); } - await _loader.LoadAsync(registry, cancellationToken); + await _loader.LoadAsync(registry, cancellationToken).ConfigureAwait(false); } /// @@ -511,194 +511,3 @@ public sealed class GodotYamlConfigLoader : IConfigLoader innerException); } } - -/// -/// 抽象 与具体宿主环境之间的 Godot 路径和文件访问边界。 -/// -/// -/// 该抽象存在的原因,是编辑器态与导出态对 res://user:// 的访问方式不同: -/// 编辑器态通常可以把 Godot 特殊路径全局化后直接落到普通文件系统,而导出态往往只能通过 Godot API 读取原始文本资源, -/// 再把它们复制到运行时缓存目录。 在目录不存在或当前环境无法枚举时必须返回 -/// ,用来表达“不可访问”而不是抛出未找到异常; 则应保留底层读取失败异常, -/// 交由加载器包装成配置诊断。对于普通文件系统路径,应遵循 / 语义; -/// 对于 Godot 特殊路径,则应使用引擎提供的路径解析和读取能力。 -/// -internal sealed class GodotYamlConfigEnvironment -{ - /// - /// 初始化一个可替换的 Godot YAML 配置宿主环境抽象。 - /// - /// 返回当前进程是否处于 Godot 编辑器态的委托。 - /// - /// 把 Godot 特殊路径转换为普通绝对路径的委托。 - /// 当前加载器仅会在输入为 res://user:// 时调用它,返回值必须为非空绝对路径。 - /// - /// - /// 枚举指定目录直接子项的委托。 - /// 当目录不存在、无法访问或当前环境无法枚举该路径时,必须返回 。 - /// - /// - /// 检查指定路径上的文件是否存在的委托。 - /// 输入既可能是 Godot 特殊路径,也可能是普通绝对路径。 - /// - /// - /// 读取指定文件完整字节内容的委托。 - /// 当文件缺失或读取失败时,应抛出底层异常,由加载器统一包装为配置加载诊断。 - /// - /// 任一委托参数为 时抛出。 - public GodotYamlConfigEnvironment( - Func isEditor, - Func globalizePath, - Func?> enumerateDirectory, - Func fileExists, - Func readAllBytes) - { - IsEditor = isEditor ?? throw new ArgumentNullException(nameof(isEditor)); - GlobalizePath = globalizePath ?? throw new ArgumentNullException(nameof(globalizePath)); - EnumerateDirectory = enumerateDirectory ?? throw new ArgumentNullException(nameof(enumerateDirectory)); - FileExists = fileExists ?? throw new ArgumentNullException(nameof(fileExists)); - ReadAllBytes = readAllBytes ?? throw new ArgumentNullException(nameof(readAllBytes)); - } - - /// - /// 获取默认的 Godot 运行时环境实现。 - /// - /// - /// 默认实现使用 检测编辑器态, - /// 使用 处理 Godot 特殊路径, - /// 并在 Godot 路径与普通路径之间切换对应的枚举和读取 API。 - /// - public static GodotYamlConfigEnvironment Default { get; } = new( - static () => OS.HasFeature("editor"), - static path => ProjectSettings.GlobalizePath(path), - EnumerateDirectoryCore, - FileExistsCore, - ReadAllBytesCore); - - /// - /// 获取用于判断当前进程是否处于编辑器态的委托。 - /// - public Func IsEditor { get; } - - /// - /// 获取把 Godot 特殊路径转换为普通绝对路径的委托。 - /// - /// - /// 当前加载器只会对 res://user:// 路径调用该委托。 - /// 返回空字符串会被视为无效环境实现,并在后续路径解析阶段触发异常。 - /// - public Func GlobalizePath { get; } - - /// - /// 获取用于枚举目录直接子项的委托。 - /// - /// - /// 当目录不存在、无法访问,或当前环境无法枚举给定路径时,该委托必须返回 。 - /// 返回的集合只应包含当前目录下的直接子项,调用方会自行过滤隐藏项、子目录与非 YAML 文件。 - /// - public Func?> EnumerateDirectory { get; } - - /// - /// 获取用于检查文件是否存在的委托。 - /// - public Func FileExists { get; } - - /// - /// 获取用于读取文件完整字节内容的委托。 - /// - /// - /// 该委托在路径不存在、权限不足或 I/O 失败时应抛出底层异常,以便加载器保留失败原因并生成诊断信息。 - /// - public Func ReadAllBytes { get; } - - private static IReadOnlyList? EnumerateDirectoryCore(string path) - { - if (!path.IsGodotPath()) - { - if (!Directory.Exists(path)) - { - return null; - } - - return Directory - .EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly) - .Select(static entryPath => new GodotYamlConfigDirectoryEntry( - Path.GetFileName(entryPath), - Directory.Exists(entryPath))) - .ToArray(); - } - - using var directory = DirAccess.Open(path); - if (directory == null) - { - return null; - } - - var entries = new List(); - var listDirectoryError = directory.ListDirBegin(); - if (listDirectoryError != Error.Ok) - { - return null; - } - - while (true) - { - var name = directory.GetNext(); - if (string.IsNullOrEmpty(name)) - { - break; - } - - entries.Add(new GodotYamlConfigDirectoryEntry(name, directory.CurrentIsDir())); - } - - directory.ListDirEnd(); - return entries; - } - - private static bool FileExistsCore(string path) - { - return path.IsGodotPath() - ? FileAccess.FileExists(path) - : File.Exists(path); - } - - private static byte[] ReadAllBytesCore(string path) - { - return path.IsGodotPath() - ? FileAccess.GetFileAsBytes(path) - : File.ReadAllBytes(path); - } -} - -/// -/// 描述一次目录枚举返回的单个子项。 -/// -/// -/// 该结构只承载目录扫描阶段需要的最小信息。 -/// 必须是单个目录项名称,而不是包含父目录的完整路径; -/// 对于 Godot 路径和普通路径都遵循相同约定,便于加载器统一做后续拼接与过滤。 -/// -internal readonly record struct GodotYamlConfigDirectoryEntry -{ - /// - /// 初始化一个目录枚举结果项。 - /// - /// 当前目录项的名称,不包含父目录路径。 - /// 指示该目录项是否为子目录。 - public GodotYamlConfigDirectoryEntry(string name, bool isDirectory) - { - Name = name; - IsDirectory = isDirectory; - } - - /// - /// 获取当前目录项的名称,不包含父目录路径。 - /// - public string Name { get; } - - /// - /// 获取一个值,指示当前目录项是否为子目录。 - /// - public bool IsDirectory { get; } -} diff --git a/GFramework.Godot/Data/GodotResourceRepository.cs b/GFramework.Godot/Data/GodotResourceRepository.cs index 0035e7d4..eb8b819f 100644 --- a/GFramework.Godot/Data/GodotResourceRepository.cs +++ b/GFramework.Godot/Data/GodotResourceRepository.cs @@ -209,7 +209,8 @@ public class GodotResourceRepository } // 只处理.tres和.res扩展名的资源文件 - if (!entry.EndsWith(".tres") && !entry.EndsWith(".res")) + if (!entry.EndsWith(".tres", StringComparison.OrdinalIgnoreCase) && + !entry.EndsWith(".res", StringComparison.OrdinalIgnoreCase)) return; // 加载资源文件 @@ -224,4 +225,4 @@ public class GodotResourceRepository if (!_storage.TryAdd(resource.Key, resource)) Log.Warn($"Duplicate key detected: {resource.Key}"); } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Extensions/GodotPathExtensions.cs b/GFramework.Godot/Extensions/GodotPathExtensions.cs index 5469482d..c4cfda45 100644 --- a/GFramework.Godot/Extensions/GodotPathExtensions.cs +++ b/GFramework.Godot/Extensions/GodotPathExtensions.cs @@ -13,7 +13,7 @@ public static class GodotPathExtensions /// 如果路径以 "user://" 开头且不为空,则返回 true;否则返回 false。 public static bool IsUserPath(this string path) { - return !string.IsNullOrEmpty(path) && path.StartsWith("user://"); + return !string.IsNullOrEmpty(path) && path.StartsWith("user://", StringComparison.Ordinal); } /// @@ -23,7 +23,7 @@ public static class GodotPathExtensions /// 如果路径以 "res://" 开头且不为空,则返回 true;否则返回 false。 public static bool IsResPath(this string path) { - return !string.IsNullOrEmpty(path) && path.StartsWith("res://"); + return !string.IsNullOrEmpty(path) && path.StartsWith("res://", StringComparison.Ordinal); } /// @@ -35,4 +35,4 @@ public static class GodotPathExtensions { return path.IsUserPath() || path.IsResPath(); } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Extensions/NodeExtensions.cs b/GFramework.Godot/Extensions/NodeExtensions.cs index a0106418..b9aabb66 100644 --- a/GFramework.Godot/Extensions/NodeExtensions.cs +++ b/GFramework.Godot/Extensions/NodeExtensions.cs @@ -176,7 +176,7 @@ public static class NodeExtensions public static async Task AddChildXAsync(this Node parent, Node child) { parent.AddChild(child); - await child.WaitUntilReadyAsync(); + await child.WaitUntilReadyAsync().ConfigureAwait(false); } /// @@ -285,4 +285,4 @@ public static class NodeExtensions return t; throw new InvalidCastException($"Cannot cast {node} to {typeof(T)}"); } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Logging/GodotLogger.cs b/GFramework.Godot/Logging/GodotLogger.cs index 62529213..e0fc2948 100644 --- a/GFramework.Godot/Logging/GodotLogger.cs +++ b/GFramework.Godot/Logging/GodotLogger.cs @@ -1,4 +1,5 @@ -using GFramework.Core.Abstractions.Logging; +using System.Globalization; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; using Godot; @@ -36,7 +37,7 @@ public sealed class GodotLogger( protected override void Write(LogLevel level, string message, Exception? exception) { // 构造时间戳和日志前缀 - var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); var levelStr = LevelStrings[(int)level]; var logPrefix = $"[{timestamp}] {levelStr} [{Name()}]"; @@ -71,4 +72,4 @@ public sealed class GodotLogger( break; } } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Scene/SceneBehaviorBase.cs b/GFramework.Godot/Scene/SceneBehaviorBase.cs index d2c30da5..e3a0b083 100644 --- a/GFramework.Godot/Scene/SceneBehaviorBase.cs +++ b/GFramework.Godot/Scene/SceneBehaviorBase.cs @@ -114,7 +114,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior // 调用可选接口 if (_scene != null) - await _scene.OnLoadAsync(param); + await _scene.OnLoadAsync(param).ConfigureAwait(false); _isLoaded = true; _isTransitioning = false; @@ -130,7 +130,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior _isTransitioning = true; if (_scene != null) - await _scene.OnEnterAsync(); + await _scene.OnEnterAsync().ConfigureAwait(false); _isActive = true; _isTransitioning = false; @@ -144,7 +144,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior public virtual async ValueTask OnPauseAsync() { if (_scene != null) - await _scene.OnPauseAsync(); + await _scene.OnPauseAsync().ConfigureAwait(false); // 暂停处理 Owner.SetProcess(false); @@ -165,7 +165,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior return; if (_scene != null) - await _scene.OnResumeAsync(); + await _scene.OnResumeAsync().ConfigureAwait(false); // 恢复处理 Owner.SetProcess(true); @@ -185,7 +185,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior _isTransitioning = true; if (_scene != null) - await _scene.OnExitAsync(); + await _scene.OnExitAsync().ConfigureAwait(false); _isActive = false; } @@ -198,7 +198,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior public virtual async ValueTask OnUnloadAsync() { if (_scene != null) - await _scene.OnUnloadAsync(); + await _scene.OnUnloadAsync().ConfigureAwait(false); // 释放节点 Owner.QueueFreeX(); @@ -208,4 +208,4 @@ public abstract class SceneBehaviorBase : ISceneBehavior } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Godot/Setting/Data/LocalizationMap.cs b/GFramework.Godot/Setting/Data/LocalizationMap.cs index 1864f4ae..fffcd0f7 100644 --- a/GFramework.Godot/Setting/Data/LocalizationMap.cs +++ b/GFramework.Godot/Setting/Data/LocalizationMap.cs @@ -24,7 +24,7 @@ public class LocalizationMap /// /// 用户语言 -> Godot locale 映射表。 /// - public Dictionary LanguageMap { get; set; } = new() + public Dictionary LanguageMap { get; set; } = new(StringComparer.Ordinal) { { "简体中文", "zh_CN" }, { "English", "en" } @@ -33,7 +33,7 @@ public class LocalizationMap /// /// 用户语言 -> GFramework 本地化语言码映射表。 /// - public Dictionary FrameworkLanguageMap { get; set; } = new() + public Dictionary FrameworkLanguageMap { get; set; } = new(StringComparer.Ordinal) { { "简体中文", "zhs" }, { "English", "eng" } @@ -68,4 +68,4 @@ public class LocalizationMap return FrameworkLanguageMap.GetValueOrDefault(storedLanguage, DefaultFrameworkLanguage); } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Setting/GodotGraphicsSettings.cs b/GFramework.Godot/Setting/GodotGraphicsSettings.cs index e6870cef..6f2db267 100644 --- a/GFramework.Godot/Setting/GodotGraphicsSettings.cs +++ b/GFramework.Godot/Setting/GodotGraphicsSettings.cs @@ -14,7 +14,7 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin /// 应用图形设置到Godot引擎 /// /// 异步任务 - public async Task Apply() + public Task Apply() { var settings = model.GetData(); // 创建分辨率向量 @@ -40,7 +40,7 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin DisplayServer.WindowSetPosition(pos); } - await Task.CompletedTask; + return Task.CompletedTask; } /// @@ -62,4 +62,4 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin /// 该属性返回图形设置数据的具体类型信息。 /// public Type DataType { get; } = typeof(GraphicsSettings); -} \ No newline at end of file +} diff --git a/GFramework.Godot/Storage/GodotFileStorage.cs b/GFramework.Godot/Storage/GodotFileStorage.cs index 12565205..a835f64a 100644 --- a/GFramework.Godot/Storage/GodotFileStorage.cs +++ b/GFramework.Godot/Storage/GodotFileStorage.cs @@ -81,8 +81,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); + var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false); - await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) + await using (pathLock.ConfigureAwait(false)) { // 处理Godot文件系统路径的删除操作 if (path.IsGodotPath()) @@ -179,8 +180,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); + var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false); - await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) + await using (pathLock.ConfigureAwait(false)) { if (!path.IsGodotPath()) return File.Exists(path); using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); @@ -238,8 +240,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); + var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false); - await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) + await using (pathLock.ConfigureAwait(false)) { string content; @@ -290,7 +293,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable dir.ListDirEnd(); return (IReadOnlyList)result; - }); + }).ConfigureAwait(false); } /// @@ -319,7 +322,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable dir.ListDirEnd(); return (IReadOnlyList)result; - }); + }).ConfigureAwait(false); } /// @@ -345,7 +348,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable var fullPath = ToAbsolutePath(path); if (!DirAccess.DirExistsAbsolute(fullPath)) DirAccess.MakeDirRecursiveAbsolute(fullPath); - }); + }).ConfigureAwait(false); } #endregion @@ -378,8 +381,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); + var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false); - await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) + await using (pathLock.ConfigureAwait(false)) { var content = _serializer.Serialize(value); if (path.IsGodotPath()) @@ -397,4 +401,4 @@ public sealed class GodotFileStorage : IStorage, IDisposable } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Godot/UI/UiPageBehaviorFactory.cs b/GFramework.Godot/UI/UiPageBehaviorFactory.cs index 9f5042e6..8b5f8319 100644 --- a/GFramework.Godot/UI/UiPageBehaviorFactory.cs +++ b/GFramework.Godot/UI/UiPageBehaviorFactory.cs @@ -40,7 +40,7 @@ public static class UiPageBehaviorFactory UiLayer.Modal => new ModalLayerUiPageBehavior(owner, key), UiLayer.Toast => new ToastLayerUiPageBehavior(owner, key), UiLayer.Topmost => new TopmostLayerUiPageBehavior(owner, key), - _ => throw new ArgumentException($"Unsupported UI layer: {layer}") + _ => throw new ArgumentException($"Unsupported UI layer: {layer}", nameof(layer)) }; } -} \ No newline at end of file +}