feat(extensions): 添加多个扩展方法类和对应测试

- 新增 AsyncExtensions 提供异步任务超时、重试、安全执行等功能
- 新增 CollectionExtensions 提供集合遍历、空值检查、过滤等扩展功能
- 新增 GuardExtensions 提供参数验证的 Guard 模式扩展方法
- 新增 NumericExtensions 提供数值范围限制、插值计算等数学扩展功能
- 为所有扩展方法添加完整的单元测试覆盖正常和异常情况
- 包含详细的 XML 文档注释和使用示例代码
This commit is contained in:
GeWuYou 2026-02-25 13:09:47 +08:00 committed by gewuyou
parent 66da08e3e1
commit 475f301d9f
11 changed files with 1695 additions and 1 deletions

2
.gitignore vendored
View File

@ -5,5 +5,5 @@ riderModule.iml
/_ReSharper.Caches/
GFramework.sln.DotSettings.user
.idea/
.opencode.json
opencode.json
.claude/settings.local.json

View File

@ -0,0 +1,258 @@
using System.Diagnostics;
using GFramework.Core.extensions;
using NUnit.Framework;
namespace GFramework.Core.Tests.extensions;
/// <summary>
/// 测试 AsyncExtensions 扩展方法的功能
/// </summary>
[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<TimeoutException>(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<OperationCanceledException>(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<TimeoutException>(async () =>
await task.WithTimeout(TimeSpan.FromMilliseconds(100)));
}
[Test]
public async Task WithRetry_Should_Return_Result_When_Task_Succeeds()
{
// Arrange
var attemptCount = 0;
Func<Task<int>> 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<Task<int>> 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<Task<int>> taskFactory = () =>
{
attemptCount++;
throw new InvalidOperationException("Permanent failure");
};
// Act & Assert
Assert.ThrowsAsync<AggregateException>(async () =>
await taskFactory.WithRetry(2, TimeSpan.FromMilliseconds(10)));
}
[Test]
public async Task WithRetry_Should_Respect_ShouldRetry_Predicate()
{
// Arrange
var attemptCount = 0;
Func<Task<int>> taskFactory = () =>
{
attemptCount++;
throw new ArgumentException("Should not retry");
};
// Act & Assert
Assert.ThrowsAsync<AggregateException>(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<Task<int>> 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<Task<int>> 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<int>(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<int>(expectedException);
Exception? capturedEx = null;
// Act
await task.WithFallback(ex =>
{
capturedEx = ex;
return -1;
});
// Assert
Assert.That(capturedEx, Is.SameAs(expectedException));
}
}

View File

@ -0,0 +1,185 @@
using GFramework.Core.extensions;
using NUnit.Framework;
namespace GFramework.Core.Tests.extensions;
/// <summary>
/// 测试 CollectionExtensions 扩展方法的功能
/// </summary>
[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<int>? numbers = null;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => 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<ArgumentNullException>(() => numbers.ForEach(null!));
}
[Test]
public void IsNullOrEmpty_Should_Return_True_When_Source_Is_Null()
{
// Arrange
IEnumerable<int>? 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<int>();
// 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<string?>? items = null;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => 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<ArgumentNullException>(() =>
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<ArgumentNullException>(() =>
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<ArgumentNullException>(() =>
items.ToDictionarySafe<(string, int), string, int>(x => x.Item1, null!));
}
}

View File

@ -0,0 +1,133 @@
using GFramework.Core.extensions;
using NUnit.Framework;
namespace GFramework.Core.Tests.extensions;
/// <summary>
/// 测试 GuardExtensions 扩展方法的功能
/// </summary>
[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<ArgumentNullException>(() => value.ThrowIfNull());
}
[Test]
public void ThrowIfNull_Should_Include_ParamName_In_Exception()
{
// Arrange
string? value = null;
// Act & Assert
var ex = Assert.Throws<ArgumentNullException>(() => 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<ArgumentNullException>(() => value.ThrowIfNullOrEmpty());
}
[Test]
public void ThrowIfNullOrEmpty_Should_Throw_ArgumentException_When_Value_Is_Empty()
{
// Arrange
var value = string.Empty;
// Act & Assert
Assert.Throws<ArgumentException>(() => value.ThrowIfNullOrEmpty());
}
[Test]
public void ThrowIfNullOrEmpty_Should_Include_ParamName_In_Exception()
{
// Arrange
var value = string.Empty;
// Act & Assert
var ex = Assert.Throws<ArgumentException>(() => 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<int>? collection = null;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => collection.ThrowIfEmpty());
}
[Test]
public void ThrowIfEmpty_Should_Throw_ArgumentException_When_Collection_Is_Empty()
{
// Arrange
var collection = Array.Empty<int>();
// Act & Assert
Assert.Throws<ArgumentException>(() => collection.ThrowIfEmpty());
}
[Test]
public void ThrowIfEmpty_Should_Include_ParamName_In_Exception()
{
// Arrange
var collection = Array.Empty<int>();
// Act & Assert
var ex = Assert.Throws<ArgumentException>(() => collection.ThrowIfEmpty("testParam"));
Assert.That(ex.ParamName, Is.EqualTo("testParam"));
}
}

View File

@ -0,0 +1,215 @@
using GFramework.Core.extensions;
using NUnit.Framework;
namespace GFramework.Core.Tests.extensions;
/// <summary>
/// 测试 NumericExtensions 扩展方法的功能
/// </summary>
[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<ArgumentException>(() => 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<ArgumentException>(() => 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<DivideByZeroException>(() => 50f.InverseLerp(100f, 100f));
}
}

View File

@ -0,0 +1,233 @@
using GFramework.Core.extensions;
using NUnit.Framework;
namespace GFramework.Core.Tests.extensions;
/// <summary>
/// 测试 StringExtensions 扩展方法的功能
/// </summary>
[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<ArgumentNullException>(() => 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<ArgumentOutOfRangeException>(() => 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<string>();
// 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<string>? words = null;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => words!.Join(", "));
}
[Test]
public void Join_Should_Throw_ArgumentNullException_When_Separator_Is_Null()
{
// Arrange
var words = new[] { "Hello", "World" };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => words.Join(null!));
}
}

View File

@ -0,0 +1,230 @@
namespace GFramework.Core.extensions;
/// <summary>
/// 异步扩展方法
/// </summary>
public static class AsyncExtensions
{
/// <summary>
/// 为任务添加超时限制
/// </summary>
/// <typeparam name="T">任务结果类型</typeparam>
/// <param name="task">要执行的任务</param>
/// <param name="timeout">超时时间</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>任务结果</returns>
/// <exception cref="ArgumentNullException">当 task 为 null 时抛出</exception>
/// <exception cref="TimeoutException">当任务超时时抛出</exception>
/// <exception cref="OperationCanceledException">当操作被取消时抛出</exception>
/// <example>
/// <code>
/// var result = await SomeAsyncOperation().WithTimeout(TimeSpan.FromSeconds(5));
/// </code>
/// </example>
public static async Task<T> WithTimeout<T>(
this Task<T> 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;
}
/// <summary>
/// 为任务添加超时限制(无返回值版本)
/// </summary>
/// <param name="task">要执行的任务</param>
/// <param name="timeout">超时时间</param>
/// <param name="cancellationToken">取消令牌</param>
/// <exception cref="ArgumentNullException">当 task 为 null 时抛出</exception>
/// <exception cref="TimeoutException">当任务超时时抛出</exception>
/// <exception cref="OperationCanceledException">当操作被取消时抛出</exception>
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;
}
/// <summary>
/// 为任务工厂添加重试机制
/// </summary>
/// <typeparam name="T">任务结果类型</typeparam>
/// <param name="taskFactory">任务工厂函数</param>
/// <param name="maxRetries">最大重试次数</param>
/// <param name="delay">重试间隔</param>
/// <param name="shouldRetry">判断是否应该重试的函数,默认对所有异常重试</param>
/// <returns>任务结果</returns>
/// <exception cref="ArgumentNullException">当 taskFactory 为 null 时抛出</exception>
/// <exception cref="ArgumentOutOfRangeException">当 maxRetries 小于 0 时抛出</exception>
/// <example>
/// <code>
/// var result = await (() => UnreliableOperation())
/// .WithRetry(maxRetries: 3, delay: TimeSpan.FromSeconds(1));
/// </code>
/// </example>
public static async Task<T> WithRetry<T>(
this Func<Task<T>> taskFactory,
int maxRetries,
TimeSpan delay,
Func<Exception, bool>? 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} 次重试后仍然失败");
}
/// <summary>
/// 安全执行异步操作,将异常包装为 Result 类型
/// </summary>
/// <typeparam name="T">任务结果类型</typeparam>
/// <param name="func">要执行的异步函数</param>
/// <returns>包含结果或异常的 Result 对象</returns>
/// <exception cref="ArgumentNullException">当 func 为 null 时抛出</exception>
/// <example>
/// <code>
/// var result = await (() => RiskyOperation()).TryAsync();
/// result.Match(
/// value => Console.WriteLine($"成功: {value}"),
/// error => Console.WriteLine($"失败: {error.Message}")
/// );
/// </code>
/// </example>
public static async Task<Result<T>> TryAsync<T>(this Func<Task<T>> func)
{
ArgumentNullException.ThrowIfNull(func);
try
{
var result = await func();
return new Result<T>(result);
}
catch (Exception ex)
{
return new Result<T>(ex);
}
}
/// <summary>
/// 等待所有任务完成
/// </summary>
/// <param name="tasks">任务集合</param>
/// <exception cref="ArgumentNullException">当 tasks 为 null 时抛出</exception>
/// <example>
/// <code>
/// var tasks = new[] { Task1(), Task2(), Task3() };
/// await tasks.WhenAll();
/// </code>
/// </example>
public static Task WhenAll(this IEnumerable<Task> tasks)
{
ArgumentNullException.ThrowIfNull(tasks);
return Task.WhenAll(tasks);
}
/// <summary>
/// 等待所有任务完成并返回结果数组
/// </summary>
/// <typeparam name="T">任务结果类型</typeparam>
/// <param name="tasks">任务集合</param>
/// <returns>所有任务的结果数组</returns>
/// <exception cref="ArgumentNullException">当 tasks 为 null 时抛出</exception>
/// <example>
/// <code>
/// var tasks = new[] { GetValue1(), GetValue2(), GetValue3() };
/// var results = await tasks.WhenAll();
/// </code>
/// </example>
public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> tasks)
{
ArgumentNullException.ThrowIfNull(tasks);
return Task.WhenAll(tasks);
}
/// <summary>
/// 为任务添加失败回退机制
/// </summary>
/// <typeparam name="T">任务结果类型</typeparam>
/// <param name="task">要执行的任务</param>
/// <param name="fallback">失败时的回退函数</param>
/// <returns>任务结果或回退值</returns>
/// <exception cref="ArgumentNullException">当 task 或 fallback 为 null 时抛出</exception>
/// <example>
/// <code>
/// var result = await RiskyOperation()
/// .WithFallback(ex => DefaultValue);
/// </code>
/// </example>
public static async Task<T> WithFallback<T>(this Task<T> task, Func<Exception, T> fallback)
{
ArgumentNullException.ThrowIfNull(task);
ArgumentNullException.ThrowIfNull(fallback);
try
{
return await task;
}
catch (Exception ex)
{
return fallback(ex);
}
}
}

View File

@ -0,0 +1,104 @@
namespace GFramework.Core.extensions;
/// <summary>
/// 集合扩展方法
/// </summary>
public static class CollectionExtensions
{
/// <summary>
/// 对集合中的每个元素执行指定操作
/// </summary>
/// <typeparam name="T">集合元素类型</typeparam>
/// <param name="source">源集合</param>
/// <param name="action">要对每个元素执行的操作</param>
/// <exception cref="ArgumentNullException">当 source 或 action 为 null 时抛出</exception>
/// <example>
/// <code>
/// var numbers = new[] { 1, 2, 3 };
/// numbers.ForEach(n => Console.WriteLine(n));
/// </code>
/// </example>
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(action);
foreach (var item in source) action(item);
}
/// <summary>
/// 检查集合是否为 null 或空
/// </summary>
/// <typeparam name="T">集合元素类型</typeparam>
/// <param name="source">要检查的集合</param>
/// <returns>如果集合为 null 或不包含任何元素,则返回 true否则返回 false</returns>
/// <example>
/// <code>
/// List&lt;int&gt;? numbers = null;
/// if (numbers.IsNullOrEmpty()) { /* ... */ }
/// </code>
/// </example>
public static bool IsNullOrEmpty<T>(this IEnumerable<T>? source)
{
return source is null || !source.Any();
}
/// <summary>
/// 过滤掉集合中的 null 元素
/// </summary>
/// <typeparam name="T">集合元素类型(引用类型)</typeparam>
/// <param name="source">源集合</param>
/// <returns>不包含 null 元素的集合</returns>
/// <exception cref="ArgumentNullException">当 source 为 null 时抛出</exception>
/// <example>
/// <code>
/// var items = new string?[] { "a", null, "b", null, "c" };
/// var nonNull = items.WhereNotNull(); // ["a", "b", "c"]
/// </code>
/// </example>
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class
{
ArgumentNullException.ThrowIfNull(source);
return source.Where(item => item is not null)!;
}
/// <summary>
/// 将集合转换为字典,如果存在重复键则使用最后一个值
/// </summary>
/// <typeparam name="T">源集合元素类型</typeparam>
/// <typeparam name="TKey">字典键类型</typeparam>
/// <typeparam name="TValue">字典值类型</typeparam>
/// <param name="source">源集合</param>
/// <param name="keySelector">键选择器函数</param>
/// <param name="valueSelector">值选择器函数</param>
/// <returns>转换后的字典</returns>
/// <exception cref="ArgumentNullException">当 source、keySelector 或 valueSelector 为 null 时抛出</exception>
/// <example>
/// <code>
/// var items = new[] { ("a", 1), ("b", 2), ("a", 3) };
/// var dict = items.ToDictionarySafe(x => x.Item1, x => x.Item2);
/// // dict["a"] == 3 (最后一个值)
/// </code>
/// </example>
public static Dictionary<TKey, TValue> ToDictionarySafe<T, TKey, TValue>(
this IEnumerable<T> source,
Func<T, TKey> keySelector,
Func<T, TValue> valueSelector) where TKey : notnull
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(keySelector);
ArgumentNullException.ThrowIfNull(valueSelector);
var dictionary = new Dictionary<TKey, TValue>();
foreach (var item in source)
{
var key = keySelector(item);
var value = valueSelector(item);
dictionary[key] = value; // 覆盖重复键
}
return dictionary;
}
}

View File

@ -0,0 +1,118 @@
using System.Runtime.CompilerServices;
namespace GFramework.Core.extensions;
/// <summary>
/// 参数验证扩展方法Guard 模式)
/// </summary>
public static class GuardExtensions
{
/// <summary>
/// 如果值为 null 则抛出 ArgumentNullException
/// </summary>
/// <typeparam name="T">引用类型</typeparam>
/// <param name="value">要检查的值</param>
/// <param name="paramName">参数名称(自动捕获)</param>
/// <returns>非 null 的值</returns>
/// <exception cref="ArgumentNullException">当 value 为 null 时抛出</exception>
/// <example>
/// <code>
/// public void Process(string? input)
/// {
/// var safeInput = input.ThrowIfNull(); // 自动使用 "input" 作为参数名
/// }
/// </code>
/// </example>
public static T ThrowIfNull<T>(
this T? value,
[CallerArgumentExpression(nameof(value))] string? paramName = null) where T : class
{
ArgumentNullException.ThrowIfNull(value, paramName);
return value;
}
/// <summary>
/// 如果字符串为 null 或空则抛出 ArgumentException
/// </summary>
/// <param name="value">要检查的字符串</param>
/// <param name="paramName">参数名称(自动捕获)</param>
/// <returns>非空字符串</returns>
/// <exception cref="ArgumentNullException">当 value 为 null 时抛出</exception>
/// <exception cref="ArgumentException">当 value 为空字符串时抛出</exception>
/// <example>
/// <code>
/// public void SetName(string? name)
/// {
/// var safeName = name.ThrowIfNullOrEmpty();
/// }
/// </code>
/// </example>
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;
}
/// <summary>
/// 如果字符串为 null、空或仅包含空白字符则抛出 ArgumentException
/// </summary>
/// <param name="value">要检查的字符串</param>
/// <param name="paramName">参数名称(自动捕获)</param>
/// <returns>非空白字符串</returns>
/// <exception cref="ArgumentNullException">当 value 为 null 时抛出</exception>
/// <exception cref="ArgumentException">当 value 为空或仅包含空白字符时抛出</exception>
/// <example>
/// <code>
/// public void SetDescription(string? description)
/// {
/// var safeDescription = description.ThrowIfNullOrWhiteSpace();
/// }
/// </code>
/// </example>
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;
}
/// <summary>
/// 如果集合为空则抛出 ArgumentException
/// </summary>
/// <typeparam name="T">集合元素类型</typeparam>
/// <param name="source">要检查的集合</param>
/// <param name="paramName">参数名称(自动捕获)</param>
/// <returns>非空集合</returns>
/// <exception cref="ArgumentNullException">当 source 为 null 时抛出</exception>
/// <exception cref="ArgumentException">当 source 为空集合时抛出</exception>
/// <example>
/// <code>
/// public void ProcessItems(IEnumerable&lt;int&gt;? items)
/// {
/// var safeItems = items.ThrowIfEmpty();
/// }
/// </code>
/// </example>
public static IEnumerable<T> ThrowIfEmpty<T>(
this IEnumerable<T>? source,
[CallerArgumentExpression(nameof(source))] string? paramName = null)
{
ArgumentNullException.ThrowIfNull(source, paramName);
if (!source.Any())
throw new ArgumentException("集合不能为空", paramName);
return source;
}
}

View File

@ -0,0 +1,112 @@
namespace GFramework.Core.extensions;
/// <summary>
/// 数值扩展方法
/// </summary>
public static class NumericExtensions
{
/// <summary>
/// 将值限制在指定的范围内
/// </summary>
/// <typeparam name="T">实现 IComparable 的类型</typeparam>
/// <param name="value">要限制的值</param>
/// <param name="min">最小值</param>
/// <param name="max">最大值</param>
/// <returns>限制后的值</returns>
/// <exception cref="ArgumentNullException">当 value、min 或 max 为 null 时抛出</exception>
/// <exception cref="ArgumentException">当 min 大于 max 时抛出</exception>
/// <example>
/// <code>
/// var value = 150;
/// var clamped = value.Clamp(0, 100); // 返回 100
/// </code>
/// </example>
public static T Clamp<T>(this T value, T min, T max) where T : IComparable<T>
{
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;
}
/// <summary>
/// 检查值是否在指定范围内
/// </summary>
/// <typeparam name="T">实现 IComparable 的类型</typeparam>
/// <param name="value">要检查的值</param>
/// <param name="min">最小值</param>
/// <param name="max">最大值</param>
/// <param name="inclusive">是否包含边界值,默认为 true</param>
/// <returns>如果值在范围内则返回 true否则返回 false</returns>
/// <exception cref="ArgumentNullException">当 value、min 或 max 为 null 时抛出</exception>
/// <exception cref="ArgumentException">当 min 大于 max 时抛出</exception>
/// <example>
/// <code>
/// var value = 50;
/// var inRange = value.Between(0, 100); // 返回 true
/// var inRangeExclusive = value.Between(50, 100, inclusive: false); // 返回 false
/// </code>
/// </example>
public static bool Between<T>(this T value, T min, T max, bool inclusive = true) where T : IComparable<T>
{
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;
}
/// <summary>
/// 在两个值之间进行线性插值
/// </summary>
/// <param name="from">起始值</param>
/// <param name="to">目标值</param>
/// <param name="t">插值参数0 到 1 之间)</param>
/// <returns>插值结果</returns>
/// <example>
/// <code>
/// var result = 0f.Lerp(100f, 0.5f); // 返回 50
/// </code>
/// </example>
public static float Lerp(this float from, float to, float t)
{
return from + (to - from) * t;
}
/// <summary>
/// 计算值在两个值之间的插值参数
/// </summary>
/// <param name="value">当前值</param>
/// <param name="from">起始值</param>
/// <param name="to">目标值</param>
/// <returns>插值参数(通常在 0 到 1 之间)</returns>
/// <exception cref="DivideByZeroException">当 from 等于 to 时抛出</exception>
/// <example>
/// <code>
/// var t = 50f.InverseLerp(0f, 100f); // 返回 0.5
/// </code>
/// </example>
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);
}
}

View File

@ -0,0 +1,106 @@
namespace GFramework.Core.extensions;
/// <summary>
/// 字符串扩展方法
/// </summary>
public static class StringExtensions
{
/// <summary>
/// 指示指定的字符串是 null 还是空字符串
/// </summary>
/// <param name="str">要测试的字符串</param>
/// <returns>如果 str 参数为 null 或空字符串 (""),则为 true否则为 false</returns>
/// <example>
/// <code>
/// string? text = null;
/// if (text.IsNullOrEmpty()) { /* ... */ }
/// </code>
/// </example>
public static bool IsNullOrEmpty(this string? str)
{
return string.IsNullOrEmpty(str);
}
/// <summary>
/// 指示指定的字符串是 null、空还是仅由空白字符组成
/// </summary>
/// <param name="str">要测试的字符串</param>
/// <returns>如果 str 参数为 null、空字符串或仅包含空白字符则为 true否则为 false</returns>
/// <example>
/// <code>
/// string? text = " ";
/// if (text.IsNullOrWhiteSpace()) { /* ... */ }
/// </code>
/// </example>
public static bool IsNullOrWhiteSpace(this string? str)
{
return string.IsNullOrWhiteSpace(str);
}
/// <summary>
/// 如果字符串为空,则返回 null否则返回原字符串
/// </summary>
/// <param name="str">要检查的字符串</param>
/// <returns>如果字符串为空则返回 null否则返回原字符串</returns>
/// <example>
/// <code>
/// string text = "";
/// var result = text.NullIfEmpty(); // 返回 null
/// </code>
/// </example>
public static string? NullIfEmpty(this string? str)
{
return string.IsNullOrEmpty(str) ? null : str;
}
/// <summary>
/// 截断字符串到指定的最大长度,并可选地添加后缀
/// </summary>
/// <param name="str">要截断的字符串</param>
/// <param name="maxLength">最大长度(包括后缀)</param>
/// <param name="suffix">截断时添加的后缀,默认为 "..."</param>
/// <returns>截断后的字符串</returns>
/// <exception cref="ArgumentNullException">当 str 为 null 时抛出</exception>
/// <exception cref="ArgumentOutOfRangeException">当 maxLength 小于后缀长度时抛出</exception>
/// <example>
/// <code>
/// var text = "Hello World";
/// var truncated = text.Truncate(8); // "Hello..."
/// </code>
/// </example>
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);
}
/// <summary>
/// 使用指定的分隔符连接字符串集合
/// </summary>
/// <param name="values">要连接的字符串集合</param>
/// <param name="separator">分隔符</param>
/// <returns>连接后的字符串</returns>
/// <exception cref="ArgumentNullException">当 values 或 separator 为 null 时抛出</exception>
/// <example>
/// <code>
/// var words = new[] { "Hello", "World" };
/// var result = words.Join(", "); // "Hello, World"
/// </code>
/// </example>
public static string Join(this IEnumerable<string> values, string separator)
{
ArgumentNullException.ThrowIfNull(values);
ArgumentNullException.ThrowIfNull(separator);
return string.Join(separator, values);
}
}