diff --git a/GFramework.Core/extensions/AsyncExtensions.cs b/GFramework.Core/extensions/AsyncExtensions.cs index 0b4b890..2b640dd 100644 --- a/GFramework.Core/extensions/AsyncExtensions.cs +++ b/GFramework.Core/extensions/AsyncExtensions.cs @@ -1,5 +1,3 @@ -using GFramework.Core.Functional; - namespace GFramework.Core.extensions; /// @@ -99,132 +97,6 @@ public static class AsyncExtensions } } - /// - /// 为任务工厂添加重试机制 - /// - /// 任务结果类型 - /// 任务工厂函数 - /// 最大重试次数 - /// 重试间隔 - /// 判断是否应该重试的函数,默认对所有异常重试 - /// 当为 true 时直接抛出原始异常,否则包装为 AggregateException - /// 任务结果 - /// 当 taskFactory 为 null 时抛出 - /// 当 maxRetries 小于 0 时抛出 - /// - /// - /// var result = await (() => UnreliableOperation()) - /// .WithRetry(maxRetries: 3, delay: TimeSpan.FromSeconds(1)); - /// - /// - public static async Task WithRetry( - this Func> taskFactory, - int maxRetries, - TimeSpan delay, - Func? shouldRetry = null, - bool throwOriginal = false) - { - 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 - { - if (throwOriginal) - throw; - - throw new AggregateException($"操作在 {attempt} 次重试后仍然失败", ex); - } - } - } - - // 理论上不可达,仅满足编译器要求 - throw new AggregateException($"操作在 {maxRetries} 次重试后仍然失败"); - } - - /// - /// 安全执行异步操作,将异常包装为 Result 类型 - /// - /// 任务结果类型 - /// 要执行的异步函数 - /// 包含结果或异常的 Result 对象 - /// 当 func 为 null 时抛出 - /// - /// - /// var result = await (() => RiskyOperation()).TryAsync(); - /// result.Match( - /// value => Console.WriteLine($"成功: {value}"), - /// error => Console.WriteLine($"失败: {error.Message}") - /// ); - /// - /// - public static async Task> TryAsync(this Func> func) - { - ArgumentNullException.ThrowIfNull(func); - - try - { - var result = await func(); - return new Result(result); - } - catch (Exception ex) - { - return new Result(ex); - } - } - - /// - /// 等待所有任务完成 - /// - /// 任务集合 - /// 当 tasks 为 null 时抛出 - /// - /// - /// var tasks = new[] { Task1(), Task2(), Task3() }; - /// await tasks.WhenAll(); - /// - /// - public static Task WhenAll(this IEnumerable tasks) - { - ArgumentNullException.ThrowIfNull(tasks); - return Task.WhenAll(tasks); - } - - /// - /// 等待所有任务完成并返回结果数组 - /// - /// 任务结果类型 - /// 任务集合 - /// 所有任务的结果数组 - /// 当 tasks 为 null 时抛出 - /// - /// - /// var tasks = new[] { GetValue1(), GetValue2(), GetValue3() }; - /// var results = await tasks.WhenAll(); - /// - /// - public static Task WhenAll(this IEnumerable> tasks) - { - ArgumentNullException.ThrowIfNull(tasks); - return Task.WhenAll(tasks); - } - /// /// 为任务添加失败回退机制 /// diff --git a/GFramework.Core/extensions/NumericExtensions.cs b/GFramework.Core/extensions/NumericExtensions.cs index f2080db..d268806 100644 --- a/GFramework.Core/extensions/NumericExtensions.cs +++ b/GFramework.Core/extensions/NumericExtensions.cs @@ -5,36 +5,6 @@ namespace GFramework.Core.extensions; /// public static class NumericExtensions { - /// - /// 将值限制在指定的范围内 - /// - /// 实现 IComparable 的类型 - /// 要限制的值 - /// 最小值 - /// 最大值 - /// 限制后的值 - /// 当 value、min 或 max 为 null 时抛出 - /// 当 min 大于 max 时抛出 - /// - /// - /// var value = 150; - /// var clamped = value.Clamp(0, 100); // 返回 100 - /// - /// - public static T Clamp(this T value, T min, T max) where T : IComparable - { - 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; - } - /// /// 检查值是否在指定范围内 /// diff --git a/GFramework.Core/extensions/ObjectExtensions.cs b/GFramework.Core/extensions/ObjectExtensions.cs index be3f672..9e2ee24 100644 --- a/GFramework.Core/extensions/ObjectExtensions.cs +++ b/GFramework.Core/extensions/ObjectExtensions.cs @@ -136,27 +136,6 @@ public static class ObjectExtensions return obj as T; } - /// - /// 对对象执行指定操作后返回对象本身, - /// 用于构建流式(链式)调用。 - /// - /// 对象类型 - /// 源对象 - /// 要执行的操作 - /// 原对象 - /// - /// - /// obj.As<MyRule>() - /// ?.Do(r => r.Initialize()) - /// ?.Do(r => r.Execute()); - /// - /// - public static T Do(this T obj, Action action) - { - action(obj); - return obj; - } - /// /// 根据对象的运行时类型,依次匹配并执行对应的处理逻辑, /// 只会执行第一个匹配成功的处理器。 diff --git a/GFramework.Core/extensions/StringExtensions.cs b/GFramework.Core/extensions/StringExtensions.cs index 8d88428..a3beb4b 100644 --- a/GFramework.Core/extensions/StringExtensions.cs +++ b/GFramework.Core/extensions/StringExtensions.cs @@ -5,38 +5,6 @@ namespace GFramework.Core.extensions; /// public static class StringExtensions { - /// - /// 指示指定的字符串是 null 还是空字符串 - /// - /// 要测试的字符串 - /// 如果 str 参数为 null 或空字符串 (""),则为 true;否则为 false - /// - /// - /// string? text = null; - /// if (text.IsNullOrEmpty()) { /* ... */ } - /// - /// - public static bool IsNullOrEmpty(this string? str) - { - return string.IsNullOrEmpty(str); - } - - /// - /// 指示指定的字符串是 null、空还是仅由空白字符组成 - /// - /// 要测试的字符串 - /// 如果 str 参数为 null、空字符串或仅包含空白字符,则为 true;否则为 false - /// - /// - /// string? text = " "; - /// if (text.IsNullOrWhiteSpace()) { /* ... */ } - /// - /// - public static bool IsNullOrWhiteSpace(this string? str) - { - return string.IsNullOrWhiteSpace(str); - } - /// /// 如果字符串为空,则返回 null;否则返回原字符串 /// @@ -103,4 +71,4 @@ public static class StringExtensions return string.Join(separator, values); } -} +} \ No newline at end of file diff --git a/GFramework.Core/functional/Option.cs b/GFramework.Core/functional/Option.cs new file mode 100644 index 0000000..709c343 --- /dev/null +++ b/GFramework.Core/functional/Option.cs @@ -0,0 +1,316 @@ +// Copyright (c) 2025 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace GFramework.Core.Functional; + +/// +/// 表示可能存在或不存在的值,用于替代 null 引用的函数式编程类型 +/// +/// 值的类型 +public readonly struct Option +{ + private readonly T _value; + private readonly bool _isSome; + + /// + /// 私有构造函数,用于创建 Some 状态 + /// + private Option(T value) + { + _value = value; + _isSome = true; + } + + /// + /// 判断是否有值 + /// + public bool IsSome => _isSome; + + /// + /// 判断是否无值 + /// + public bool IsNone => !_isSome; + + #region 工厂方法 + + /// + /// 创建包含值的 Option + /// + /// 要包装的值 + /// 包含值的 Option + /// 当 value 为 null 时抛出 + /// + /// + /// var option = Option<int>.Some(42); + /// + /// + public static Option Some(T value) + { + ArgumentNullException.ThrowIfNull(value); + return new Option(value); + } + + /// + /// 表示无值的 Option + /// + public static Option None => default; + + #endregion + + #region 取值 + + /// + /// 获取值,如果无值则返回默认值 + /// + /// 无值时返回的默认值 + /// Option 中的值或默认值 + /// + /// + /// var value = option.GetOrElse(0); // 如果无值则返回 0 + /// + /// + public T GetOrElse(T defaultValue) => _isSome ? _value : defaultValue; + + /// + /// 获取值,如果无值则通过工厂函数生成默认值 + /// + /// 生成默认值的工厂函数 + /// Option 中的值或工厂函数生成的值 + /// 当 factory 为 null 时抛出 + /// + /// + /// var value = option.GetOrElse(() => ExpensiveDefault()); + /// + /// + public T GetOrElse(Func factory) + { + ArgumentNullException.ThrowIfNull(factory); + return _isSome ? _value : factory(); + } + + #endregion + + #region 变换 + + /// + /// 映射值到新类型,如果无值则返回 None + /// + /// 映射后的类型 + /// 映射函数 + /// 映射后的 Option + /// 当 mapper 为 null 时抛出 + /// + /// + /// var option = Option<int>.Some(42); + /// var mapped = option.Map(x => x.ToString()); // Option<string>.Some("42") + /// + /// + public Option Map(Func mapper) + { + ArgumentNullException.ThrowIfNull(mapper); + return _isSome ? Option.Some(mapper(_value)) : Option.None; + } + + /// + /// 单子绑定操作,将 Option 链式转换 + /// + /// 绑定后的类型 + /// 绑定函数 + /// 绑定后的 Option + /// 当 binder 为 null 时抛出 + /// + /// + /// var option = Option<string>.Some("42"); + /// var bound = option.Bind(s => int.TryParse(s, out var i) + /// ? Option<int>.Some(i) + /// : Option<int>.None); + /// + /// + public Option Bind(Func> binder) + { + ArgumentNullException.ThrowIfNull(binder); + return _isSome ? binder(_value) : Option.None; + } + + #endregion + + #region 过滤 + + /// + /// 根据条件过滤值,不满足条件则返回 None + /// + /// 过滤条件 + /// 满足条件的 Option 或 None + /// 当 predicate 为 null 时抛出 + /// + /// + /// var option = Option<int>.Some(42); + /// var filtered = option.Filter(x => x > 0); // Option<int>.Some(42) + /// var filtered2 = option.Filter(x => x < 0); // Option<int>.None + /// + /// + public Option Filter(Func predicate) + { + ArgumentNullException.ThrowIfNull(predicate); + return _isSome && predicate(_value) ? this : None; + } + + #endregion + + #region 模式匹配 + + /// + /// 模式匹配,根据是否有值执行不同的函数 + /// + /// 返回值类型 + /// 有值时执行的函数 + /// 无值时执行的函数 + /// 匹配结果 + /// 当 some 或 none 为 null 时抛出 + /// + /// + /// var result = option.Match( + /// some: value => $"Value: {value}", + /// none: () => "No value" + /// ); + /// + /// + public TResult Match(Func some, Func none) + { + ArgumentNullException.ThrowIfNull(some); + ArgumentNullException.ThrowIfNull(none); + return _isSome ? some(_value) : none(); + } + + /// + /// 模式匹配(副作用版本),根据是否有值执行不同的操作 + /// + /// 有值时执行的操作 + /// 无值时执行的操作 + /// 当 some 或 none 为 null 时抛出 + /// + /// + /// option.Match( + /// some: value => Console.WriteLine($"Value: {value}"), + /// none: () => Console.WriteLine("No value") + /// ); + /// + /// + public void Match(Action some, Action none) + { + ArgumentNullException.ThrowIfNull(some); + ArgumentNullException.ThrowIfNull(none); + if (_isSome) + some(_value); + else + none(); + } + + #endregion + + #region 转换 + + /// + /// 转换为 Result 类型 + /// + /// 无值时的错误消息 + /// Result 类型 + /// + /// + /// var result = option.ToResult("Value not found"); + /// + /// + public Result ToResult(string errorMessage = "Value is None") + { + ArgumentException.ThrowIfNullOrWhiteSpace(errorMessage); + return _isSome + ? Result.Succeed(_value) + : Result.Fail(new InvalidOperationException(errorMessage)); + } + + /// + /// 转换为可枚举集合(有值时包含一个元素,无值时为空集合) + /// + /// 可枚举集合 + /// + /// + /// var items = option.ToEnumerable(); // 有值时: [value], 无值时: [] + /// + /// + public IEnumerable ToEnumerable() + { + if (_isSome) + yield return _value; + } + + #endregion + + #region 隐式转换 + + /// + /// 从值隐式转换为 Option + /// + public static implicit operator Option(T value) => + value is not null ? Some(value) : None; + + #endregion + + #region 相等性和比较 + + /// + /// 判断两个 Option 是否相等 + /// + public bool Equals(Option other) + { + if (_isSome != other._isSome) + return false; + + return !_isSome || EqualityComparer.Default.Equals(_value, other._value); + } + + /// + /// 判断与对象是否相等 + /// + public override bool Equals(object? obj) => + obj is Option other && Equals(other); + + /// + /// 获取哈希码 + /// + public override int GetHashCode() => + _isSome ? EqualityComparer.Default.GetHashCode(_value!) : 0; + + /// + /// 相等运算符 + /// + public static bool operator ==(Option left, Option right) => + left.Equals(right); + + /// + /// 不等运算符 + /// + public static bool operator !=(Option left, Option right) => + !left.Equals(right); + + #endregion + + #region 字符串表示 + + /// + /// 获取字符串表示 + /// + public override string ToString() => + _isSome ? $"Some({_value})" : "None"; + + #endregion +} \ No newline at end of file diff --git a/GFramework.Core/functional/async/AsyncFunctionalExtensions.cs b/GFramework.Core/functional/async/AsyncFunctionalExtensions.cs new file mode 100644 index 0000000..dc722b3 --- /dev/null +++ b/GFramework.Core/functional/async/AsyncFunctionalExtensions.cs @@ -0,0 +1,110 @@ +// Copyright (c) 2025 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace GFramework.Core.Functional.Async; + +/// +/// 异步函数式编程扩展方法 +/// +public static class AsyncFunctionalExtensions +{ + /// + /// 为任务工厂添加重试机制 + /// + /// 任务结果类型 + /// 任务工厂函数 + /// 最大重试次数 + /// 重试间隔 + /// 判断是否应该重试的函数,默认对所有异常重试 + /// 当为 true 时直接抛出原始异常,否则包装为 AggregateException + /// 任务结果 + /// 当 taskFactory 为 null 时抛出 + /// 当 maxRetries 小于 0 时抛出 + /// + /// + /// var result = await (() => UnreliableOperation()) + /// .WithRetry(maxRetries: 3, delay: TimeSpan.FromSeconds(1)); + /// + /// + public static async Task WithRetry( + this Func> taskFactory, + int maxRetries, + TimeSpan delay, + Func? shouldRetry = null, + bool throwOriginal = false) + { + 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 + { + if (throwOriginal) + throw; + + throw new AggregateException($"操作在 {attempt} 次重试后仍然失败", ex); + } + } + } + + // 理论上不可达,仅满足编译器要求 + throw new AggregateException($"操作在 {maxRetries} 次重试后仍然失败"); + } + + /// + /// 安全执行异步操作,将异常包装为 Result 类型 + /// + /// 任务结果类型 + /// 要执行的异步函数 + /// 包含结果或异常的 Result 对象 + /// 当 func 为 null 时抛出 + /// + /// + /// var result = await (() => RiskyOperation()).TryAsync(); + /// result.Match( + /// value => Console.WriteLine($"成功: {value}"), + /// error => Console.WriteLine($"失败: {error.Message}") + /// ); + /// + /// + public static async Task> TryAsync(this Func> func) + { + ArgumentNullException.ThrowIfNull(func); + + try + { + var result = await func(); + return new Result(result); + } + catch (Exception ex) + { + return new Result(ex); + } + } +} \ No newline at end of file diff --git a/GFramework.Core/extensions/ResultExtensions.cs b/GFramework.Core/functional/result/ResultExtensions.cs similarity index 95% rename from GFramework.Core/extensions/ResultExtensions.cs rename to GFramework.Core/functional/result/ResultExtensions.cs index 0565a96..ffc588b 100644 --- a/GFramework.Core/extensions/ResultExtensions.cs +++ b/GFramework.Core/functional/result/ResultExtensions.cs @@ -2,18 +2,19 @@ // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + using GFramework.Core.Functional; -namespace GFramework.Core.Extensions; +namespace GFramework.Core.functional.result; /// /// Result 类型的扩展方法,提供函数式编程支持 @@ -46,11 +47,8 @@ public static class ResultExtensions if (result.IsFaulted) return Result>.Fail(result.Exception); - if (result.IsBottom) - return Result>.Fail(new InvalidOperationException("Cannot combine Bottom results")); - if (result.IsSuccess) - result.IfSucc(values.Add); + values.Add(result.Match(succ: v => v, fail: _ => throw new InvalidOperationException())); } return Result>.Succeed(values); @@ -265,7 +263,7 @@ public static class ResultExtensions string errorMessage = "Value is null") where T : class => value is not null ? Result.Succeed(value) - : Result.Fail(new ArgumentNullException(nameof(value), errorMessage)); + : Result.Fail(new ArgumentNullException(errorMessage)); /// /// 将可空值类型转换为 Result @@ -275,7 +273,7 @@ public static class ResultExtensions string errorMessage = "Value is null") where T : struct => value.HasValue ? Result.Succeed(value.Value) - : Result.Fail(new ArgumentNullException(nameof(value), errorMessage)); + : Result.Fail(new ArgumentNullException(errorMessage)); #endregion } \ No newline at end of file