mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
feat(extensions): 添加多个扩展方法类和对应测试
- 新增 AsyncExtensions 提供异步任务超时、重试、安全执行等功能 - 新增 CollectionExtensions 提供集合遍历、空值检查、过滤等扩展功能 - 新增 GuardExtensions 提供参数验证的 Guard 模式扩展方法 - 新增 NumericExtensions 提供数值范围限制、插值计算等数学扩展功能 - 为所有扩展方法添加完整的单元测试覆盖正常和异常情况 - 包含详细的 XML 文档注释和使用示例代码
This commit is contained in:
parent
66da08e3e1
commit
475f301d9f
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,5 +5,5 @@ riderModule.iml
|
|||||||
/_ReSharper.Caches/
|
/_ReSharper.Caches/
|
||||||
GFramework.sln.DotSettings.user
|
GFramework.sln.DotSettings.user
|
||||||
.idea/
|
.idea/
|
||||||
.opencode.json
|
opencode.json
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
258
GFramework.Core.Tests/extensions/AsyncExtensionsTests.cs
Normal file
258
GFramework.Core.Tests/extensions/AsyncExtensionsTests.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
185
GFramework.Core.Tests/extensions/CollectionExtensionsTests.cs
Normal file
185
GFramework.Core.Tests/extensions/CollectionExtensionsTests.cs
Normal 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!));
|
||||||
|
}
|
||||||
|
}
|
||||||
133
GFramework.Core.Tests/extensions/GuardExtensionsTests.cs
Normal file
133
GFramework.Core.Tests/extensions/GuardExtensionsTests.cs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
215
GFramework.Core.Tests/extensions/NumericExtensionsTests.cs
Normal file
215
GFramework.Core.Tests/extensions/NumericExtensionsTests.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
233
GFramework.Core.Tests/extensions/StringExtensionsTests.cs
Normal file
233
GFramework.Core.Tests/extensions/StringExtensionsTests.cs
Normal 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!));
|
||||||
|
}
|
||||||
|
}
|
||||||
230
GFramework.Core/extensions/AsyncExtensions.cs
Normal file
230
GFramework.Core/extensions/AsyncExtensions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
104
GFramework.Core/extensions/CollectionExtensions.cs
Normal file
104
GFramework.Core/extensions/CollectionExtensions.cs
Normal 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<int>? 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
118
GFramework.Core/extensions/GuardExtensions.cs
Normal file
118
GFramework.Core/extensions/GuardExtensions.cs
Normal 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<int>? 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
112
GFramework.Core/extensions/NumericExtensions.cs
Normal file
112
GFramework.Core/extensions/NumericExtensions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
106
GFramework.Core/extensions/StringExtensions.cs
Normal file
106
GFramework.Core/extensions/StringExtensions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user