diff --git a/.gitignore b/.gitignore
index cff1d66..a6b3851 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,5 @@ riderModule.iml
/_ReSharper.Caches/
GFramework.sln.DotSettings.user
.idea/
-.opencode.json
+opencode.json
.claude/settings.local.json
\ No newline at end of file
diff --git a/GFramework.Core.Tests/extensions/AsyncExtensionsTests.cs b/GFramework.Core.Tests/extensions/AsyncExtensionsTests.cs
new file mode 100644
index 0000000..c23843a
--- /dev/null
+++ b/GFramework.Core.Tests/extensions/AsyncExtensionsTests.cs
@@ -0,0 +1,258 @@
+using System.Diagnostics;
+using GFramework.Core.extensions;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.extensions;
+
+///
+/// 测试 AsyncExtensions 扩展方法的功能
+///
+[TestFixture]
+public class AsyncExtensionsTests
+{
+ [Test]
+ public async Task WithTimeout_Should_Return_Result_When_Task_Completes_Before_Timeout()
+ {
+ // Arrange
+ var task = Task.FromResult(42);
+
+ // Act
+ var result = await task.WithTimeout(TimeSpan.FromSeconds(1));
+
+ // Assert
+ Assert.That(result, Is.EqualTo(42));
+ }
+
+ [Test]
+ public void WithTimeout_Should_Throw_TimeoutException_When_Task_Exceeds_Timeout()
+ {
+ // Arrange
+ var task = Task.Delay(TimeSpan.FromSeconds(2)).ContinueWith(_ => 42);
+
+ // Act & Assert
+ Assert.ThrowsAsync(async () =>
+ await task.WithTimeout(TimeSpan.FromMilliseconds(100)));
+ }
+
+ [Test]
+ public void WithTimeout_Should_Throw_OperationCanceledException_When_Cancellation_Requested()
+ {
+ // Arrange
+ using var cts = new CancellationTokenSource();
+ var task = Task.Delay(TimeSpan.FromSeconds(2)).ContinueWith(_ => 42);
+ cts.Cancel();
+
+ // Act & Assert
+ Assert.ThrowsAsync(async () =>
+ await task.WithTimeout(TimeSpan.FromSeconds(1), cts.Token));
+ }
+
+ [Test]
+ public async Task WithTimeout_NoResult_Should_Complete_When_Task_Completes_Before_Timeout()
+ {
+ // Arrange
+ var task = Task.CompletedTask;
+ var stopwatch = Stopwatch.StartNew();
+
+ // Act
+ await task.WithTimeout(TimeSpan.FromSeconds(1));
+ stopwatch.Stop();
+
+ // Assert
+ Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(1000), "Task should complete before timeout");
+ Assert.Pass("Task completed successfully within timeout period");
+ }
+
+ [Test]
+ public void WithTimeout_NoResult_Should_Throw_TimeoutException_When_Task_Exceeds_Timeout()
+ {
+ // Arrange
+ var task = Task.Delay(TimeSpan.FromSeconds(2));
+
+ // Act & Assert
+ Assert.ThrowsAsync(async () =>
+ await task.WithTimeout(TimeSpan.FromMilliseconds(100)));
+ }
+
+ [Test]
+ public async Task WithRetry_Should_Return_Result_When_Task_Succeeds()
+ {
+ // Arrange
+ var attemptCount = 0;
+ Func> taskFactory = () =>
+ {
+ attemptCount++;
+ return Task.FromResult(42);
+ };
+
+ // Act
+ var result = await taskFactory.WithRetry(3, TimeSpan.FromMilliseconds(10));
+
+ // Assert
+ Assert.That(result, Is.EqualTo(42));
+ Assert.That(attemptCount, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task WithRetry_Should_Retry_On_Failure()
+ {
+ // Arrange
+ var attemptCount = 0;
+ Func> taskFactory = () =>
+ {
+ attemptCount++;
+ if (attemptCount < 3)
+ throw new InvalidOperationException("Temporary failure");
+ return Task.FromResult(42);
+ };
+
+ // Act
+ var result = await taskFactory.WithRetry(3, TimeSpan.FromMilliseconds(10));
+
+ // Assert
+ Assert.That(result, Is.EqualTo(42));
+ Assert.That(attemptCount, Is.EqualTo(3));
+ }
+
+ [Test]
+ public void WithRetry_Should_Throw_AggregateException_When_All_Retries_Fail()
+ {
+ // Arrange
+ var attemptCount = 0;
+ Func> taskFactory = () =>
+ {
+ attemptCount++;
+ throw new InvalidOperationException("Permanent failure");
+ };
+
+ // Act & Assert
+ Assert.ThrowsAsync(async () =>
+ await taskFactory.WithRetry(2, TimeSpan.FromMilliseconds(10)));
+ }
+
+ [Test]
+ public async Task WithRetry_Should_Respect_ShouldRetry_Predicate()
+ {
+ // Arrange
+ var attemptCount = 0;
+ Func> taskFactory = () =>
+ {
+ attemptCount++;
+ throw new ArgumentException("Should not retry");
+ };
+
+ // Act & Assert
+ Assert.ThrowsAsync(async () =>
+ await taskFactory.WithRetry(3, TimeSpan.FromMilliseconds(10),
+ ex => ex is not ArgumentException));
+
+ await Task.Delay(50); // 等待任务完成
+ Assert.That(attemptCount, Is.EqualTo(1)); // 不应该重试
+ }
+
+ [Test]
+ public async Task TryAsync_Should_Return_Success_When_Task_Succeeds()
+ {
+ // Arrange
+ Func> func = () => Task.FromResult(42);
+
+ // Act
+ var result = await func.TryAsync();
+
+ // Assert
+ Assert.That(result.IsSuccess, Is.True);
+ Assert.That(result.IfFail(0), Is.EqualTo(42));
+ }
+
+ [Test]
+ public async Task TryAsync_Should_Return_Failure_When_Task_Throws()
+ {
+ // Arrange
+ Func> func = () => throw new InvalidOperationException("Test error");
+
+ // Act
+ var result = await func.TryAsync();
+
+ // Assert
+ Assert.That(result.IsFaulted, Is.True);
+ }
+
+ [Test]
+ public async Task WhenAll_Should_Wait_For_All_Tasks()
+ {
+ // Arrange
+ var task1 = Task.Delay(10);
+ var task2 = Task.Delay(20);
+ var task3 = Task.Delay(30);
+ var tasks = new[] { task1, task2, task3 };
+
+ // Act
+ await tasks.WhenAll();
+
+ // Assert
+ Assert.That(task1.IsCompleted, Is.True);
+ Assert.That(task2.IsCompleted, Is.True);
+ Assert.That(task3.IsCompleted, Is.True);
+ }
+
+ [Test]
+ public async Task WhenAll_WithResults_Should_Return_All_Results()
+ {
+ // Arrange
+ var task1 = Task.FromResult(1);
+ var task2 = Task.FromResult(2);
+ var task3 = Task.FromResult(3);
+ var tasks = new[] { task1, task2, task3 };
+
+ // Act
+ var results = await tasks.WhenAll();
+
+ // Assert
+ Assert.That(results, Is.EqualTo(new[] { 1, 2, 3 }));
+ }
+
+ [Test]
+ public async Task WithFallback_Should_Return_Result_When_Task_Succeeds()
+ {
+ // Arrange
+ var task = Task.FromResult(42);
+
+ // Act
+ var result = await task.WithFallback(_ => -1);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(42));
+ }
+
+ [Test]
+ public async Task WithFallback_Should_Return_Fallback_Value_When_Task_Fails()
+ {
+ // Arrange
+ var task = Task.FromException(new InvalidOperationException("Test error"));
+
+ // Act
+ var result = await task.WithFallback(ex => -1);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(-1));
+ }
+
+ [Test]
+ public async Task WithFallback_Should_Pass_Exception_To_Fallback()
+ {
+ // Arrange
+ var expectedException = new InvalidOperationException("Test error");
+ var task = Task.FromException(expectedException);
+ Exception? capturedEx = null;
+
+ // Act
+ await task.WithFallback(ex =>
+ {
+ capturedEx = ex;
+ return -1;
+ });
+
+ // Assert
+ Assert.That(capturedEx, Is.SameAs(expectedException));
+ }
+}
diff --git a/GFramework.Core.Tests/extensions/CollectionExtensionsTests.cs b/GFramework.Core.Tests/extensions/CollectionExtensionsTests.cs
new file mode 100644
index 0000000..683b88b
--- /dev/null
+++ b/GFramework.Core.Tests/extensions/CollectionExtensionsTests.cs
@@ -0,0 +1,185 @@
+using GFramework.Core.extensions;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.extensions;
+
+///
+/// 测试 CollectionExtensions 扩展方法的功能
+///
+[TestFixture]
+public class CollectionExtensionsTests
+{
+ [Test]
+ public void ForEach_Should_Execute_Action_For_Each_Element()
+ {
+ // Arrange
+ var numbers = new[] { 1, 2, 3, 4, 5 };
+ var sum = 0;
+
+ // Act
+ numbers.ForEach(n => sum += n);
+
+ // Assert
+ Assert.That(sum, Is.EqualTo(15));
+ }
+
+ [Test]
+ public void ForEach_Should_Throw_ArgumentNullException_When_Source_Is_Null()
+ {
+ // Arrange
+ IEnumerable? numbers = null;
+
+ // Act & Assert
+ Assert.Throws(() => numbers!.ForEach(n => { }));
+ }
+
+ [Test]
+ public void ForEach_Should_Throw_ArgumentNullException_When_Action_Is_Null()
+ {
+ // Arrange
+ var numbers = new[] { 1, 2, 3 };
+
+ // Act & Assert
+ Assert.Throws(() => numbers.ForEach(null!));
+ }
+
+ [Test]
+ public void IsNullOrEmpty_Should_Return_True_When_Source_Is_Null()
+ {
+ // Arrange
+ IEnumerable? numbers = null;
+
+ // Act
+ var result = numbers.IsNullOrEmpty();
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void IsNullOrEmpty_Should_Return_True_When_Source_Is_Empty()
+ {
+ // Arrange
+ var numbers = Array.Empty();
+
+ // Act
+ var result = numbers.IsNullOrEmpty();
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void IsNullOrEmpty_Should_Return_False_When_Source_Has_Elements()
+ {
+ // Arrange
+ var numbers = new[] { 1, 2, 3 };
+
+ // Act
+ var result = numbers.IsNullOrEmpty();
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void WhereNotNull_Should_Filter_Out_Null_Elements()
+ {
+ // Arrange
+ var items = new string?[] { "a", null, "b", null, "c" };
+
+ // Act
+ var result = items.WhereNotNull().ToArray();
+
+ // Assert
+ Assert.That(result.Length, Is.EqualTo(3));
+ Assert.That(result, Is.EqualTo(new[] { "a", "b", "c" }));
+ }
+
+ [Test]
+ public void WhereNotNull_Should_Return_Empty_Collection_When_All_Elements_Are_Null()
+ {
+ // Arrange
+ var items = new string?[] { null, null, null };
+
+ // Act
+ var result = items.WhereNotNull().ToArray();
+
+ // Assert
+ Assert.That(result, Is.Empty);
+ }
+
+ [Test]
+ public void WhereNotNull_Should_Throw_ArgumentNullException_When_Source_Is_Null()
+ {
+ // Arrange
+ IEnumerable? items = null;
+
+ // Act & Assert
+ Assert.Throws(() => items!.WhereNotNull().ToArray());
+ }
+
+ [Test]
+ public void ToDictionarySafe_Should_Create_Dictionary()
+ {
+ // Arrange
+ var items = new[] { ("a", 1), ("b", 2), ("c", 3) };
+
+ // Act
+ var result = items.ToDictionarySafe(x => x.Item1, x => x.Item2);
+
+ // Assert
+ Assert.That(result.Count, Is.EqualTo(3));
+ Assert.That(result["a"], Is.EqualTo(1));
+ Assert.That(result["b"], Is.EqualTo(2));
+ Assert.That(result["c"], Is.EqualTo(3));
+ }
+
+ [Test]
+ public void ToDictionarySafe_Should_Overwrite_Duplicate_Keys()
+ {
+ // Arrange
+ var items = new[] { ("a", 1), ("b", 2), ("a", 3) };
+
+ // Act
+ var result = items.ToDictionarySafe(x => x.Item1, x => x.Item2);
+
+ // Assert
+ Assert.That(result.Count, Is.EqualTo(2));
+ Assert.That(result["a"], Is.EqualTo(3)); // 最后一个值
+ Assert.That(result["b"], Is.EqualTo(2));
+ }
+
+ [Test]
+ public void ToDictionarySafe_Should_Throw_ArgumentNullException_When_Source_Is_Null()
+ {
+ // Arrange
+ IEnumerable<(string, int)>? items = null;
+
+ // Act & Assert
+ Assert.Throws(() =>
+ items!.ToDictionarySafe(x => x.Item1, x => x.Item2));
+ }
+
+ [Test]
+ public void ToDictionarySafe_Should_Throw_ArgumentNullException_When_KeySelector_Is_Null()
+ {
+ // Arrange
+ var items = new[] { ("a", 1), ("b", 2) };
+
+ // Act & Assert
+ Assert.Throws(() =>
+ items.ToDictionarySafe<(string, int), string, int>(null!, x => x.Item2));
+ }
+
+ [Test]
+ public void ToDictionarySafe_Should_Throw_ArgumentNullException_When_ValueSelector_Is_Null()
+ {
+ // Arrange
+ var items = new[] { ("a", 1), ("b", 2) };
+
+ // Act & Assert
+ Assert.Throws(() =>
+ items.ToDictionarySafe<(string, int), string, int>(x => x.Item1, null!));
+ }
+}
diff --git a/GFramework.Core.Tests/extensions/GuardExtensionsTests.cs b/GFramework.Core.Tests/extensions/GuardExtensionsTests.cs
new file mode 100644
index 0000000..30aa325
--- /dev/null
+++ b/GFramework.Core.Tests/extensions/GuardExtensionsTests.cs
@@ -0,0 +1,133 @@
+using GFramework.Core.extensions;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.extensions;
+
+///
+/// 测试 GuardExtensions 扩展方法的功能
+///
+[TestFixture]
+public class GuardExtensionsTests
+{
+ [Test]
+ public void ThrowIfNull_Should_Return_Value_When_Value_Is_Not_Null()
+ {
+ // Arrange
+ var value = "test";
+
+ // Act
+ var result = value.ThrowIfNull();
+
+ // Assert
+ Assert.That(result, Is.EqualTo("test"));
+ }
+
+ [Test]
+ public void ThrowIfNull_Should_Throw_ArgumentNullException_When_Value_Is_Null()
+ {
+ // Arrange
+ string? value = null;
+
+ // Act & Assert
+ Assert.Throws(() => value.ThrowIfNull());
+ }
+
+ [Test]
+ public void ThrowIfNull_Should_Include_ParamName_In_Exception()
+ {
+ // Arrange
+ string? value = null;
+
+ // Act & Assert
+ var ex = Assert.Throws(() => value.ThrowIfNull("testParam"));
+ Assert.That(ex.ParamName, Is.EqualTo("testParam"));
+ }
+
+ [Test]
+ public void ThrowIfNullOrEmpty_Should_Return_Value_When_Value_Is_Not_Empty()
+ {
+ // Arrange
+ var value = "test";
+
+ // Act
+ var result = value.ThrowIfNullOrEmpty();
+
+ // Assert
+ Assert.That(result, Is.EqualTo("test"));
+ }
+
+ [Test]
+ public void ThrowIfNullOrEmpty_Should_Throw_ArgumentNullException_When_Value_Is_Null()
+ {
+ // Arrange
+ string? value = null;
+
+ // Act & Assert
+ Assert.Throws(() => value.ThrowIfNullOrEmpty());
+ }
+
+ [Test]
+ public void ThrowIfNullOrEmpty_Should_Throw_ArgumentException_When_Value_Is_Empty()
+ {
+ // Arrange
+ var value = string.Empty;
+
+ // Act & Assert
+ Assert.Throws(() => value.ThrowIfNullOrEmpty());
+ }
+
+ [Test]
+ public void ThrowIfNullOrEmpty_Should_Include_ParamName_In_Exception()
+ {
+ // Arrange
+ var value = string.Empty;
+
+ // Act & Assert
+ var ex = Assert.Throws(() => value.ThrowIfNullOrEmpty("testParam"));
+ Assert.That(ex.ParamName, Is.EqualTo("testParam"));
+ }
+
+ [Test]
+ public void ThrowIfEmpty_Should_Return_Collection_When_Collection_Has_Elements()
+ {
+ // Arrange
+ var collection = new[] { 1, 2, 3 };
+
+ // Act
+ var result = collection.ThrowIfEmpty();
+
+ // Assert
+ Assert.That(result, Is.EqualTo(collection));
+ }
+
+ [Test]
+ public void ThrowIfEmpty_Should_Throw_ArgumentNullException_When_Collection_Is_Null()
+ {
+ // Arrange
+ IEnumerable? collection = null;
+
+ // Act & Assert
+ Assert.Throws(() => collection.ThrowIfEmpty());
+ }
+
+ [Test]
+ public void ThrowIfEmpty_Should_Throw_ArgumentException_When_Collection_Is_Empty()
+ {
+ // Arrange
+ var collection = Array.Empty();
+
+ // Act & Assert
+ Assert.Throws(() => collection.ThrowIfEmpty());
+ }
+
+ [Test]
+ public void ThrowIfEmpty_Should_Include_ParamName_In_Exception()
+ {
+ // Arrange
+ var collection = Array.Empty();
+
+ // Act & Assert
+ var ex = Assert.Throws(() => collection.ThrowIfEmpty("testParam"));
+ Assert.That(ex.ParamName, Is.EqualTo("testParam"));
+ }
+}
diff --git a/GFramework.Core.Tests/extensions/NumericExtensionsTests.cs b/GFramework.Core.Tests/extensions/NumericExtensionsTests.cs
new file mode 100644
index 0000000..c85bf5c
--- /dev/null
+++ b/GFramework.Core.Tests/extensions/NumericExtensionsTests.cs
@@ -0,0 +1,215 @@
+using GFramework.Core.extensions;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.extensions;
+
+///
+/// 测试 NumericExtensions 扩展方法的功能
+///
+[TestFixture]
+public class NumericExtensionsTests
+{
+ [Test]
+ public void Clamp_Should_Return_Min_When_Value_Is_Less_Than_Min()
+ {
+ // Arrange
+ var value = -10;
+
+ // Act
+ var result = value.Clamp(0, 100);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void Clamp_Should_Return_Max_When_Value_Is_Greater_Than_Max()
+ {
+ // Arrange
+ var value = 150;
+
+ // Act
+ var result = value.Clamp(0, 100);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(100));
+ }
+
+ [Test]
+ public void Clamp_Should_Return_Value_When_Value_Is_Within_Range()
+ {
+ // Arrange
+ var value = 50;
+
+ // Act
+ var result = value.Clamp(0, 100);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(50));
+ }
+
+ [Test]
+ public void Clamp_Should_Throw_ArgumentException_When_Min_Is_Greater_Than_Max()
+ {
+ // Arrange
+ var value = 50;
+
+ // Act & Assert
+ Assert.Throws(() => value.Clamp(100, 0));
+ }
+
+ [Test]
+ public void Between_Should_Return_True_When_Value_Is_Within_Range()
+ {
+ // Arrange
+ var value = 50;
+
+ // Act
+ var result = value.Between(0, 100);
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void Between_Should_Return_True_When_Value_Equals_Min()
+ {
+ // Arrange
+ var value = 0;
+
+ // Act
+ var result = value.Between(0, 100);
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void Between_Should_Return_True_When_Value_Equals_Max()
+ {
+ // Arrange
+ var value = 100;
+
+ // Act
+ var result = value.Between(0, 100);
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void Between_Should_Return_False_When_Value_Is_Less_Than_Min()
+ {
+ // Arrange
+ var value = -10;
+
+ // Act
+ var result = value.Between(0, 100);
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void Between_Should_Return_False_When_Value_Is_Greater_Than_Max()
+ {
+ // Arrange
+ var value = 150;
+
+ // Act
+ var result = value.Between(0, 100);
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void Between_Should_Return_False_When_Value_Equals_Boundary_And_Not_Inclusive()
+ {
+ // Arrange
+ var value = 0;
+
+ // Act
+ var result = value.Between(0, 100, inclusive: false);
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void Between_Should_Throw_ArgumentException_When_Min_Is_Greater_Than_Max()
+ {
+ // Arrange
+ var value = 50;
+
+ // Act & Assert
+ Assert.Throws(() => value.Between(100, 0));
+ }
+
+ [Test]
+ public void Lerp_Should_Return_From_When_T_Is_Zero()
+ {
+ // Arrange & Act
+ var result = 0f.Lerp(100f, 0f);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(0f));
+ }
+
+ [Test]
+ public void Lerp_Should_Return_To_When_T_Is_One()
+ {
+ // Arrange & Act
+ var result = 0f.Lerp(100f, 1f);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(100f));
+ }
+
+ [Test]
+ public void Lerp_Should_Return_Midpoint_When_T_Is_Half()
+ {
+ // Arrange & Act
+ var result = 0f.Lerp(100f, 0.5f);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(50f));
+ }
+
+ [Test]
+ public void InverseLerp_Should_Return_Zero_When_Value_Equals_From()
+ {
+ // Arrange & Act
+ var result = 0f.InverseLerp(0f, 100f);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(0f));
+ }
+
+ [Test]
+ public void InverseLerp_Should_Return_One_When_Value_Equals_To()
+ {
+ // Arrange & Act
+ var result = 100f.InverseLerp(0f, 100f);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(1f));
+ }
+
+ [Test]
+ public void InverseLerp_Should_Return_Half_When_Value_Is_Midpoint()
+ {
+ // Arrange & Act
+ var result = 50f.InverseLerp(0f, 100f);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(0.5f));
+ }
+
+ [Test]
+ public void InverseLerp_Should_Throw_DivideByZeroException_When_From_Equals_To()
+ {
+ // Arrange & Act & Assert
+ Assert.Throws(() => 50f.InverseLerp(100f, 100f));
+ }
+}
diff --git a/GFramework.Core.Tests/extensions/StringExtensionsTests.cs b/GFramework.Core.Tests/extensions/StringExtensionsTests.cs
new file mode 100644
index 0000000..c650014
--- /dev/null
+++ b/GFramework.Core.Tests/extensions/StringExtensionsTests.cs
@@ -0,0 +1,233 @@
+using GFramework.Core.extensions;
+using NUnit.Framework;
+
+namespace GFramework.Core.Tests.extensions;
+
+///
+/// 测试 StringExtensions 扩展方法的功能
+///
+[TestFixture]
+public class StringExtensionsTests
+{
+ [Test]
+ public void IsNullOrEmpty_Should_Return_True_When_String_Is_Null()
+ {
+ // Arrange
+ string? text = null;
+
+ // Act
+ var result = text.IsNullOrEmpty();
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void IsNullOrEmpty_Should_Return_True_When_String_Is_Empty()
+ {
+ // Arrange
+ var text = string.Empty;
+
+ // Act
+ var result = text.IsNullOrEmpty();
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void IsNullOrEmpty_Should_Return_False_When_String_Has_Content()
+ {
+ // Arrange
+ var text = "Hello";
+
+ // Act
+ var result = text.IsNullOrEmpty();
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void IsNullOrWhiteSpace_Should_Return_True_When_String_Is_Null()
+ {
+ // Arrange
+ string? text = null;
+
+ // Act
+ var result = text.IsNullOrWhiteSpace();
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void IsNullOrWhiteSpace_Should_Return_True_When_String_Is_WhiteSpace()
+ {
+ // Arrange
+ var text = " ";
+
+ // Act
+ var result = text.IsNullOrWhiteSpace();
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void IsNullOrWhiteSpace_Should_Return_False_When_String_Has_Content()
+ {
+ // Arrange
+ var text = "Hello";
+
+ // Act
+ var result = text.IsNullOrWhiteSpace();
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void NullIfEmpty_Should_Return_Null_When_String_Is_Empty()
+ {
+ // Arrange
+ var text = string.Empty;
+
+ // Act
+ var result = text.NullIfEmpty();
+
+ // Assert
+ Assert.That(result, Is.Null);
+ }
+
+ [Test]
+ public void NullIfEmpty_Should_Return_Null_When_String_Is_Null()
+ {
+ // Arrange
+ string? text = null;
+
+ // Act
+ var result = text.NullIfEmpty();
+
+ // Assert
+ Assert.That(result, Is.Null);
+ }
+
+ [Test]
+ public void NullIfEmpty_Should_Return_String_When_String_Has_Content()
+ {
+ // Arrange
+ var text = "Hello";
+
+ // Act
+ var result = text.NullIfEmpty();
+
+ // Assert
+ Assert.That(result, Is.EqualTo("Hello"));
+ }
+
+ [Test]
+ public void Truncate_Should_Return_Original_String_When_Length_Is_Less_Than_MaxLength()
+ {
+ // Arrange
+ var text = "Hello";
+
+ // Act
+ var result = text.Truncate(10);
+
+ // Assert
+ Assert.That(result, Is.EqualTo("Hello"));
+ }
+
+ [Test]
+ public void Truncate_Should_Truncate_String_And_Add_Suffix()
+ {
+ // Arrange
+ var text = "Hello World";
+
+ // Act
+ var result = text.Truncate(8);
+
+ // Assert
+ Assert.That(result, Is.EqualTo("Hello..."));
+ }
+
+ [Test]
+ public void Truncate_Should_Use_Custom_Suffix()
+ {
+ // Arrange
+ var text = "Hello World";
+
+ // Act
+ var result = text.Truncate(8, "~");
+
+ // Assert
+ Assert.That(result, Is.EqualTo("Hello W~"));
+ }
+
+ [Test]
+ public void Truncate_Should_Throw_ArgumentNullException_When_String_Is_Null()
+ {
+ // Arrange
+ string? text = null;
+
+ // Act & Assert
+ Assert.Throws(() => text!.Truncate(10));
+ }
+
+ [Test]
+ public void Truncate_Should_Throw_ArgumentOutOfRangeException_When_MaxLength_Is_Less_Than_Suffix_Length()
+ {
+ // Arrange
+ var text = "Hello";
+
+ // Act & Assert
+ Assert.Throws(() => text.Truncate(2, "..."));
+ }
+
+ [Test]
+ public void Join_Should_Join_Strings_With_Separator()
+ {
+ // Arrange
+ var words = new[] { "Hello", "World" };
+
+ // Act
+ var result = words.Join(", ");
+
+ // Assert
+ Assert.That(result, Is.EqualTo("Hello, World"));
+ }
+
+ [Test]
+ public void Join_Should_Return_Empty_String_When_Collection_Is_Empty()
+ {
+ // Arrange
+ var words = Array.Empty();
+
+ // Act
+ var result = words.Join(", ");
+
+ // Assert
+ Assert.That(result, Is.EqualTo(string.Empty));
+ }
+
+ [Test]
+ public void Join_Should_Throw_ArgumentNullException_When_Collection_Is_Null()
+ {
+ // Arrange
+ IEnumerable? words = null;
+
+ // Act & Assert
+ Assert.Throws(() => words!.Join(", "));
+ }
+
+ [Test]
+ public void Join_Should_Throw_ArgumentNullException_When_Separator_Is_Null()
+ {
+ // Arrange
+ var words = new[] { "Hello", "World" };
+
+ // Act & Assert
+ Assert.Throws(() => words.Join(null!));
+ }
+}
diff --git a/GFramework.Core/extensions/AsyncExtensions.cs b/GFramework.Core/extensions/AsyncExtensions.cs
new file mode 100644
index 0000000..437752c
--- /dev/null
+++ b/GFramework.Core/extensions/AsyncExtensions.cs
@@ -0,0 +1,230 @@
+namespace GFramework.Core.extensions;
+
+///
+/// 异步扩展方法
+///
+public static class AsyncExtensions
+{
+ ///
+ /// 为任务添加超时限制
+ ///
+ /// 任务结果类型
+ /// 要执行的任务
+ /// 超时时间
+ /// 取消令牌
+ /// 任务结果
+ /// 当 task 为 null 时抛出
+ /// 当任务超时时抛出
+ /// 当操作被取消时抛出
+ ///
+ ///
+ /// var result = await SomeAsyncOperation().WithTimeout(TimeSpan.FromSeconds(5));
+ ///
+ ///
+ public static async Task WithTimeout(
+ this Task task,
+ TimeSpan timeout,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(task);
+
+ using var timeoutCts = new CancellationTokenSource();
+ using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
+
+ var delayTask = Task.Delay(timeout, linkedCts.Token);
+ var completedTask = await Task.WhenAny(task, delayTask);
+
+ if (completedTask == delayTask)
+ {
+ // 优先检查外部取消令牌,若已取消则抛出 OperationCanceledException
+ cancellationToken.ThrowIfCancellationRequested();
+ throw new TimeoutException($"操作在 {timeout.TotalSeconds} 秒后超时");
+ }
+
+ await timeoutCts.CancelAsync();
+ return await task;
+ }
+
+ ///
+ /// 为任务添加超时限制(无返回值版本)
+ ///
+ /// 要执行的任务
+ /// 超时时间
+ /// 取消令牌
+ /// 当 task 为 null 时抛出
+ /// 当任务超时时抛出
+ /// 当操作被取消时抛出
+ public static async Task WithTimeout(
+ this Task task,
+ TimeSpan timeout,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(task);
+
+ using var timeoutCts = new CancellationTokenSource();
+ using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
+
+ var delayTask = Task.Delay(timeout, linkedCts.Token);
+ var completedTask = await Task.WhenAny(task, delayTask);
+
+ if (completedTask == delayTask)
+ {
+ // 优先检查外部取消令牌,若已取消则抛出 OperationCanceledException
+ cancellationToken.ThrowIfCancellationRequested();
+ throw new TimeoutException($"操作在 {timeout.TotalSeconds} 秒后超时");
+ }
+
+ await timeoutCts.CancelAsync();
+ await task;
+ }
+
+ ///
+ /// 为任务工厂添加重试机制
+ ///
+ /// 任务结果类型
+ /// 任务工厂函数
+ /// 最大重试次数
+ /// 重试间隔
+ /// 判断是否应该重试的函数,默认对所有异常重试
+ /// 任务结果
+ /// 当 taskFactory 为 null 时抛出
+ /// 当 maxRetries 小于 0 时抛出
+ ///
+ ///
+ /// var result = await (() => UnreliableOperation())
+ /// .WithRetry(maxRetries: 3, delay: TimeSpan.FromSeconds(1));
+ ///
+ ///
+ public static async Task WithRetry(
+ this Func> taskFactory,
+ int maxRetries,
+ TimeSpan delay,
+ Func? shouldRetry = null)
+ {
+ ArgumentNullException.ThrowIfNull(taskFactory);
+
+ if (maxRetries < 0)
+ throw new ArgumentOutOfRangeException(nameof(maxRetries), "最大重试次数不能为负数");
+
+ shouldRetry ??= _ => true;
+
+ for (var attempt = 0; attempt <= maxRetries; attempt++)
+ {
+ try
+ {
+ return await taskFactory();
+ }
+ catch (Exception ex)
+ {
+ // 若还有重试机会且允许重试,则等待后继续;否则统一包装为 AggregateException 抛出
+ if (attempt < maxRetries && shouldRetry(ex))
+ {
+ await Task.Delay(delay);
+ }
+ else
+ {
+ throw new AggregateException($"操作在 {attempt} 次重试后仍然失败", ex);
+ }
+ }
+ }
+
+ // 理论上不可达,仅满足编译器要求
+ throw new AggregateException($"操作在 {maxRetries} 次重试后仍然失败");
+ }
+
+ ///
+ /// 安全执行异步操作,将异常包装为 Result 类型
+ ///
+ /// 任务结果类型
+ /// 要执行的异步函数
+ /// 包含结果或异常的 Result 对象
+ /// 当 func 为 null 时抛出
+ ///
+ ///
+ /// var result = await (() => RiskyOperation()).TryAsync();
+ /// result.Match(
+ /// value => Console.WriteLine($"成功: {value}"),
+ /// error => Console.WriteLine($"失败: {error.Message}")
+ /// );
+ ///
+ ///
+ public static async Task> TryAsync(this Func> func)
+ {
+ ArgumentNullException.ThrowIfNull(func);
+
+ try
+ {
+ var result = await func();
+ return new Result(result);
+ }
+ catch (Exception ex)
+ {
+ return new Result(ex);
+ }
+ }
+
+ ///
+ /// 等待所有任务完成
+ ///
+ /// 任务集合
+ /// 当 tasks 为 null 时抛出
+ ///
+ ///
+ /// var tasks = new[] { Task1(), Task2(), Task3() };
+ /// await tasks.WhenAll();
+ ///
+ ///
+ public static Task WhenAll(this IEnumerable tasks)
+ {
+ ArgumentNullException.ThrowIfNull(tasks);
+ return Task.WhenAll(tasks);
+ }
+
+ ///
+ /// 等待所有任务完成并返回结果数组
+ ///
+ /// 任务结果类型
+ /// 任务集合
+ /// 所有任务的结果数组
+ /// 当 tasks 为 null 时抛出
+ ///
+ ///
+ /// var tasks = new[] { GetValue1(), GetValue2(), GetValue3() };
+ /// var results = await tasks.WhenAll();
+ ///
+ ///
+ public static Task WhenAll(this IEnumerable> tasks)
+ {
+ ArgumentNullException.ThrowIfNull(tasks);
+ return Task.WhenAll(tasks);
+ }
+
+ ///
+ /// 为任务添加失败回退机制
+ ///
+ /// 任务结果类型
+ /// 要执行的任务
+ /// 失败时的回退函数
+ /// 任务结果或回退值
+ /// 当 task 或 fallback 为 null 时抛出
+ ///
+ ///
+ /// var result = await RiskyOperation()
+ /// .WithFallback(ex => DefaultValue);
+ ///
+ ///
+ public static async Task WithFallback(this Task task, Func fallback)
+ {
+ ArgumentNullException.ThrowIfNull(task);
+ ArgumentNullException.ThrowIfNull(fallback);
+
+ try
+ {
+ return await task;
+ }
+ catch (Exception ex)
+ {
+ return fallback(ex);
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/extensions/CollectionExtensions.cs b/GFramework.Core/extensions/CollectionExtensions.cs
new file mode 100644
index 0000000..74bec8c
--- /dev/null
+++ b/GFramework.Core/extensions/CollectionExtensions.cs
@@ -0,0 +1,104 @@
+namespace GFramework.Core.extensions;
+
+///
+/// 集合扩展方法
+///
+public static class CollectionExtensions
+{
+ ///
+ /// 对集合中的每个元素执行指定操作
+ ///
+ /// 集合元素类型
+ /// 源集合
+ /// 要对每个元素执行的操作
+ /// 当 source 或 action 为 null 时抛出
+ ///
+ ///
+ /// var numbers = new[] { 1, 2, 3 };
+ /// numbers.ForEach(n => Console.WriteLine(n));
+ ///
+ ///
+ public static void ForEach(this IEnumerable source, Action action)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(action);
+
+ foreach (var item in source) action(item);
+ }
+
+ ///
+ /// 检查集合是否为 null 或空
+ ///
+ /// 集合元素类型
+ /// 要检查的集合
+ /// 如果集合为 null 或不包含任何元素,则返回 true;否则返回 false
+ ///
+ ///
+ /// List<int>? numbers = null;
+ /// if (numbers.IsNullOrEmpty()) { /* ... */ }
+ ///
+ ///
+ public static bool IsNullOrEmpty(this IEnumerable? source)
+ {
+ return source is null || !source.Any();
+ }
+
+ ///
+ /// 过滤掉集合中的 null 元素
+ ///
+ /// 集合元素类型(引用类型)
+ /// 源集合
+ /// 不包含 null 元素的集合
+ /// 当 source 为 null 时抛出
+ ///
+ ///
+ /// var items = new string?[] { "a", null, "b", null, "c" };
+ /// var nonNull = items.WhereNotNull(); // ["a", "b", "c"]
+ ///
+ ///
+ public static IEnumerable WhereNotNull(this IEnumerable source) where T : class
+ {
+ ArgumentNullException.ThrowIfNull(source);
+
+ return source.Where(item => item is not null)!;
+ }
+
+ ///
+ /// 将集合转换为字典,如果存在重复键则使用最后一个值
+ ///
+ /// 源集合元素类型
+ /// 字典键类型
+ /// 字典值类型
+ /// 源集合
+ /// 键选择器函数
+ /// 值选择器函数
+ /// 转换后的字典
+ /// 当 source、keySelector 或 valueSelector 为 null 时抛出
+ ///
+ ///
+ /// var items = new[] { ("a", 1), ("b", 2), ("a", 3) };
+ /// var dict = items.ToDictionarySafe(x => x.Item1, x => x.Item2);
+ /// // dict["a"] == 3 (最后一个值)
+ ///
+ ///
+ public static Dictionary ToDictionarySafe(
+ this IEnumerable source,
+ Func keySelector,
+ Func valueSelector) where TKey : notnull
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(keySelector);
+ ArgumentNullException.ThrowIfNull(valueSelector);
+
+ var dictionary = new Dictionary();
+
+ foreach (var item in source)
+ {
+ var key = keySelector(item);
+ var value = valueSelector(item);
+ dictionary[key] = value; // 覆盖重复键
+ }
+
+ return dictionary;
+ }
+}
diff --git a/GFramework.Core/extensions/GuardExtensions.cs b/GFramework.Core/extensions/GuardExtensions.cs
new file mode 100644
index 0000000..35c20d8
--- /dev/null
+++ b/GFramework.Core/extensions/GuardExtensions.cs
@@ -0,0 +1,118 @@
+using System.Runtime.CompilerServices;
+
+namespace GFramework.Core.extensions;
+
+///
+/// 参数验证扩展方法(Guard 模式)
+///
+public static class GuardExtensions
+{
+ ///
+ /// 如果值为 null 则抛出 ArgumentNullException
+ ///
+ /// 引用类型
+ /// 要检查的值
+ /// 参数名称(自动捕获)
+ /// 非 null 的值
+ /// 当 value 为 null 时抛出
+ ///
+ ///
+ /// public void Process(string? input)
+ /// {
+ /// var safeInput = input.ThrowIfNull(); // 自动使用 "input" 作为参数名
+ /// }
+ ///
+ ///
+ public static T ThrowIfNull(
+ this T? value,
+ [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : class
+ {
+ ArgumentNullException.ThrowIfNull(value, paramName);
+ return value;
+ }
+
+ ///
+ /// 如果字符串为 null 或空则抛出 ArgumentException
+ ///
+ /// 要检查的字符串
+ /// 参数名称(自动捕获)
+ /// 非空字符串
+ /// 当 value 为 null 时抛出
+ /// 当 value 为空字符串时抛出
+ ///
+ ///
+ /// public void SetName(string? name)
+ /// {
+ /// var safeName = name.ThrowIfNullOrEmpty();
+ /// }
+ ///
+ ///
+ public static string ThrowIfNullOrEmpty(
+ this string? value,
+ [CallerArgumentExpression(nameof(value))] string? paramName = null)
+ {
+ ArgumentNullException.ThrowIfNull(value, paramName);
+
+ if (value.Length == 0)
+ throw new ArgumentException("字符串不能为空", paramName);
+
+ return value;
+ }
+
+ ///
+ /// 如果字符串为 null、空或仅包含空白字符则抛出 ArgumentException
+ ///
+ /// 要检查的字符串
+ /// 参数名称(自动捕获)
+ /// 非空白字符串
+ /// 当 value 为 null 时抛出
+ /// 当 value 为空或仅包含空白字符时抛出
+ ///
+ ///
+ /// public void SetDescription(string? description)
+ /// {
+ /// var safeDescription = description.ThrowIfNullOrWhiteSpace();
+ /// }
+ ///
+ ///
+ public static string ThrowIfNullOrWhiteSpace(
+ this string? value,
+ [CallerArgumentExpression(nameof(value))] string? paramName = null)
+ {
+ ArgumentNullException.ThrowIfNull(value, paramName);
+
+ if (string.IsNullOrWhiteSpace(value))
+ throw new ArgumentException("字符串不能为空或仅包含空白字符", paramName);
+
+ return value;
+ }
+
+ ///
+ /// 如果集合为空则抛出 ArgumentException
+ ///
+ /// 集合元素类型
+ /// 要检查的集合
+ /// 参数名称(自动捕获)
+ /// 非空集合
+ /// 当 source 为 null 时抛出
+ /// 当 source 为空集合时抛出
+ ///
+ ///
+ /// public void ProcessItems(IEnumerable<int>? items)
+ /// {
+ /// var safeItems = items.ThrowIfEmpty();
+ /// }
+ ///
+ ///
+ public static IEnumerable ThrowIfEmpty(
+ this IEnumerable? source,
+ [CallerArgumentExpression(nameof(source))] string? paramName = null)
+ {
+ ArgumentNullException.ThrowIfNull(source, paramName);
+
+ if (!source.Any())
+ throw new ArgumentException("集合不能为空", paramName);
+
+ return source;
+ }
+}
diff --git a/GFramework.Core/extensions/NumericExtensions.cs b/GFramework.Core/extensions/NumericExtensions.cs
new file mode 100644
index 0000000..544d6fe
--- /dev/null
+++ b/GFramework.Core/extensions/NumericExtensions.cs
@@ -0,0 +1,112 @@
+namespace GFramework.Core.extensions;
+
+///
+/// 数值扩展方法
+///
+public static class NumericExtensions
+{
+ ///
+ /// 将值限制在指定的范围内
+ ///
+ /// 实现 IComparable 的类型
+ /// 要限制的值
+ /// 最小值
+ /// 最大值
+ /// 限制后的值
+ /// 当 value、min 或 max 为 null 时抛出
+ /// 当 min 大于 max 时抛出
+ ///
+ ///
+ /// var value = 150;
+ /// var clamped = value.Clamp(0, 100); // 返回 100
+ ///
+ ///
+ public static T Clamp(this T value, T min, T max) where T : IComparable
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ ArgumentNullException.ThrowIfNull(min);
+ ArgumentNullException.ThrowIfNull(max);
+
+ if (min.CompareTo(max) > 0)
+ throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})");
+
+ if (value.CompareTo(min) < 0)
+ return min;
+
+ if (value.CompareTo(max) > 0)
+ return max;
+
+ return value;
+ }
+
+ ///
+ /// 检查值是否在指定范围内
+ ///
+ /// 实现 IComparable 的类型
+ /// 要检查的值
+ /// 最小值
+ /// 最大值
+ /// 是否包含边界值,默认为 true
+ /// 如果值在范围内则返回 true,否则返回 false
+ /// 当 value、min 或 max 为 null 时抛出
+ /// 当 min 大于 max 时抛出
+ ///
+ ///
+ /// var value = 50;
+ /// var inRange = value.Between(0, 100); // 返回 true
+ /// var inRangeExclusive = value.Between(50, 100, inclusive: false); // 返回 false
+ ///
+ ///
+ public static bool Between(this T value, T min, T max, bool inclusive = true) where T : IComparable
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ ArgumentNullException.ThrowIfNull(min);
+ ArgumentNullException.ThrowIfNull(max);
+
+ if (min.CompareTo(max) > 0)
+ throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})");
+
+ if (inclusive)
+ return value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0;
+
+ return value.CompareTo(min) > 0 && value.CompareTo(max) < 0;
+ }
+
+ ///
+ /// 在两个值之间进行线性插值
+ ///
+ /// 起始值
+ /// 目标值
+ /// 插值参数(0 到 1 之间)
+ /// 插值结果
+ ///
+ ///
+ /// var result = 0f.Lerp(100f, 0.5f); // 返回 50
+ ///
+ ///
+ public static float Lerp(this float from, float to, float t)
+ {
+ return from + (to - from) * t;
+ }
+
+ ///
+ /// 计算值在两个值之间的插值参数
+ ///
+ /// 当前值
+ /// 起始值
+ /// 目标值
+ /// 插值参数(通常在 0 到 1 之间)
+ /// 当 from 等于 to 时抛出
+ ///
+ ///
+ /// var t = 50f.InverseLerp(0f, 100f); // 返回 0.5
+ ///
+ ///
+ public static float InverseLerp(this float value, float from, float to)
+ {
+ if (Math.Abs(to - from) < float.Epsilon)
+ throw new DivideByZeroException("起始值和目标值不能相等");
+
+ return (value - from) / (to - from);
+ }
+}
diff --git a/GFramework.Core/extensions/StringExtensions.cs b/GFramework.Core/extensions/StringExtensions.cs
new file mode 100644
index 0000000..8d88428
--- /dev/null
+++ b/GFramework.Core/extensions/StringExtensions.cs
@@ -0,0 +1,106 @@
+namespace GFramework.Core.extensions;
+
+///
+/// 字符串扩展方法
+///
+public static class StringExtensions
+{
+ ///
+ /// 指示指定的字符串是 null 还是空字符串
+ ///
+ /// 要测试的字符串
+ /// 如果 str 参数为 null 或空字符串 (""),则为 true;否则为 false
+ ///
+ ///
+ /// string? text = null;
+ /// if (text.IsNullOrEmpty()) { /* ... */ }
+ ///
+ ///
+ public static bool IsNullOrEmpty(this string? str)
+ {
+ return string.IsNullOrEmpty(str);
+ }
+
+ ///
+ /// 指示指定的字符串是 null、空还是仅由空白字符组成
+ ///
+ /// 要测试的字符串
+ /// 如果 str 参数为 null、空字符串或仅包含空白字符,则为 true;否则为 false
+ ///
+ ///
+ /// string? text = " ";
+ /// if (text.IsNullOrWhiteSpace()) { /* ... */ }
+ ///
+ ///
+ public static bool IsNullOrWhiteSpace(this string? str)
+ {
+ return string.IsNullOrWhiteSpace(str);
+ }
+
+ ///
+ /// 如果字符串为空,则返回 null;否则返回原字符串
+ ///
+ /// 要检查的字符串
+ /// 如果字符串为空则返回 null,否则返回原字符串
+ ///
+ ///
+ /// string text = "";
+ /// var result = text.NullIfEmpty(); // 返回 null
+ ///
+ ///
+ public static string? NullIfEmpty(this string? str)
+ {
+ return string.IsNullOrEmpty(str) ? null : str;
+ }
+
+ ///
+ /// 截断字符串到指定的最大长度,并可选地添加后缀
+ ///
+ /// 要截断的字符串
+ /// 最大长度(包括后缀)
+ /// 截断时添加的后缀,默认为 "..."
+ /// 截断后的字符串
+ /// 当 str 为 null 时抛出
+ /// 当 maxLength 小于后缀长度时抛出
+ ///
+ ///
+ /// var text = "Hello World";
+ /// var truncated = text.Truncate(8); // "Hello..."
+ ///
+ ///
+ public static string Truncate(this string str, int maxLength, string suffix = "...")
+ {
+ ArgumentNullException.ThrowIfNull(str);
+ ArgumentNullException.ThrowIfNull(suffix);
+
+ if (maxLength < suffix.Length)
+ throw new ArgumentOutOfRangeException(nameof(maxLength),
+ $"最大长度必须至少为后缀长度 ({suffix.Length})");
+
+ if (str.Length <= maxLength)
+ return str;
+
+ return string.Concat(str.AsSpan(0, maxLength - suffix.Length), suffix);
+ }
+
+ ///
+ /// 使用指定的分隔符连接字符串集合
+ ///
+ /// 要连接的字符串集合
+ /// 分隔符
+ /// 连接后的字符串
+ /// 当 values 或 separator 为 null 时抛出
+ ///
+ ///
+ /// var words = new[] { "Hello", "World" };
+ /// var result = words.Join(", "); // "Hello, World"
+ ///
+ ///
+ public static string Join(this IEnumerable values, string separator)
+ {
+ ArgumentNullException.ThrowIfNull(values);
+ ArgumentNullException.ThrowIfNull(separator);
+
+ return string.Join(separator, values);
+ }
+}