feat(functional): 添加函数式编程扩展和Option类型支持

- 在FunctionExtensions中新增Map扩展方法用于对象映射
- 在PipeExtensions中新增On扩展方法用于值到函数的应用
- 移除Tap方法及相关测试以优化管道操作
- 新增NullableExtensions实现可空类型到Option的转换
- 新增Option结构体提供安全的可选值处理
- 新增OptionExtensions提供Map、Bind、Filter、Match等函数式操作
- 新增OptionValueExtensions提供GetOrElse和OrElse值提取方法
- 调整全局引用添加Concurrent集合支持
- 扩展IResetApplyAbleSettings接口添加Data属性
- 更新Godot设置类实现Data属性返回设置数据模型
This commit is contained in:
GeWuYou 2026-01-31 21:25:27 +08:00
parent c93d32c495
commit fd3a9ae9e0
13 changed files with 327 additions and 81 deletions

View File

@ -10,8 +10,6 @@ namespace GFramework.Core.Tests.functional.pipe;
[TestFixture]
public class PipeExtensionsTests
{
#region Pipe Tests
/// <summary>
/// 测试Pipe方法 - 验证值能够正确传递给函数并返回结果
/// </summary>
@ -20,18 +18,14 @@ public class PipeExtensionsTests
{
// Arrange
var value = 5;
// Act
var result = value.Pipe(x => x * 2);
// Assert
Assert.That(result, Is.EqualTo(10));
}
#endregion
#region Then Tests
/// <summary>
/// 测试Then方法 - 验证两个函数能够正确组合执行
/// </summary>
@ -44,7 +38,7 @@ public class PipeExtensionsTests
// Act
var composed = addTwo.Then((Func<int, int>)MultiplyByThree);
var result = composed(5);
// Assert
Assert.That(result, Is.EqualTo(21)); // (5+2)*3 = 21
return;
@ -52,10 +46,6 @@ public class PipeExtensionsTests
int MultiplyByThree(int x) => x * 3;
}
#endregion
#region After Tests
/// <summary>
/// 测试After方法 - 验证反向函数组合的正确性
/// </summary>
@ -68,7 +58,7 @@ public class PipeExtensionsTests
// Act
var composed = multiplyByThree.After((Func<int, int>)AddTwo);
var result = composed(5);
// Assert
Assert.That(result, Is.EqualTo(21)); // (5+2)*3 = 21
return;
@ -76,32 +66,6 @@ public class PipeExtensionsTests
int AddTwo(int x) => x + 2;
}
#endregion
#region Tap Tests
/// <summary>
/// 测试Tap方法 - 验证副作用操作执行后返回原值
/// </summary>
[Test]
public void Tap_Should_Execute_Action_And_Return_Original_Value()
{
// Arrange
var value = 42;
var capturedValue = 0;
// Act
var result = value.Tap(x => capturedValue = x);
// Assert
Assert.That(result, Is.EqualTo(42));
Assert.That(capturedValue, Is.EqualTo(42));
}
#endregion
#region Apply Tests
/// <summary>
/// 测试Apply方法 - 验证函数能够正确应用到参数上
/// </summary>
@ -110,18 +74,14 @@ public class PipeExtensionsTests
{
// Arrange
Func<int, int> multiplyByTwo = x => x * 2;
// Act
var result = multiplyByTwo.Apply(5);
// Assert
Assert.That(result, Is.EqualTo(10));
}
#endregion
#region Also Tests
/// <summary>
/// 测试Also方法 - 验证执行操作后返回原值功能
/// </summary>
@ -131,19 +91,15 @@ public class PipeExtensionsTests
// Arrange
var value = 42;
var capturedValue = 0;
// Act
var result = value.Also(x => capturedValue = x);
// Assert
Assert.That(result, Is.EqualTo(42));
Assert.That(capturedValue, Is.EqualTo(42));
}
#endregion
#region Let Tests
/// <summary>
/// 测试Let方法 - 验证值转换功能
/// </summary>
@ -152,13 +108,11 @@ public class PipeExtensionsTests
{
// Arrange
var value = 5;
// Act
var result = value.Let(x => x * 2);
// Assert
Assert.That(result, Is.EqualTo(10));
}
#endregion
}

View File

@ -10,6 +10,7 @@
// 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.functions;
/// <summary>
@ -54,7 +55,7 @@ public static class FunctionExtensions
this Func<T1, T2, TResult> func,
T1 firstArg)
=> x => func(firstArg, x);
/// <summary>
/// Repeat重复执行函数n次
/// </summary>
@ -74,6 +75,7 @@ public static class FunctionExtensions
{
result = func(result);
}
return result;
}
@ -122,4 +124,17 @@ public static class FunctionExtensions
return result;
};
}
}
/// <summary>
/// Map对单个对象应用函数
/// </summary>
/// <typeparam name="TSource">源对象类型</typeparam>
/// <typeparam name="TResult">映射后的类型</typeparam>
/// <param name="source">要映射的源对象</param>
/// <param name="selector">转换函数</param>
/// <returns>映射后的对象</returns>
public static TResult Map<TSource, TResult>(
this TSource source,
Func<TSource, TResult> selector)
=> selector(source);
}

View File

@ -10,6 +10,7 @@
// 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.pipe;
/// <summary>
@ -43,7 +44,7 @@ public static class PipeExtensions
this Func<TSource, TMiddle> first,
Func<TMiddle, TResult> second)
=> x => second(first(x));
/// <summary>
/// Compose反向组合f2.After(f1)
/// </summary>
@ -57,21 +58,6 @@ public static class PipeExtensions
this Func<TMiddle, TResult> second,
Func<TSource, TMiddle> first)
=> x => second(first(x));
/// <summary>
/// Tap执行副作用操作但返回原值用于调试、日志等
/// </summary>
/// <typeparam name="TSource">输入值的类型</typeparam>
/// <param name="value">要执行操作的输入值</param>
/// <param name="action">要执行的副作用操作</param>
/// <returns>原始输入值</returns>
public static TSource Tap<TSource>(
this TSource value,
Action<TSource> action)
{
action(value);
return value;
}
/// <summary>
/// Apply将函数应用于值柯里化辅助
@ -85,7 +71,20 @@ public static class PipeExtensions
this Func<TSource, TResult> func,
TSource value)
=> func(value);
/// <summary>
/// On将值应用于函数与Apply功能相同但参数顺序相反
/// </summary>
/// <typeparam name="TSource">输入值的类型</typeparam>
/// <typeparam name="TResult">函数返回结果的类型</typeparam>
/// <param name="value">要传递给函数的输入值</param>
/// <param name="func">要应用的函数</param>
/// <returns>函数执行后的结果</returns>
public static TResult On<TSource, TResult>(
this TSource value,
Func<TSource, TResult> func)
=> func(value);
/// <summary>
/// Also执行操作并返回原值
/// </summary>
@ -113,6 +112,4 @@ public static class PipeExtensions
this TSource value,
Func<TSource, TResult> transform)
=> transform(value);
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) 2026 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.types;
/// <summary>
/// 提供Nullable类型转换为Option类型的扩展方法
/// </summary>
public static class NullableExtensions
{
/// <summary>
/// 将可空引用类型转换为Option类型
/// </summary>
/// <typeparam name="T">引用类型</typeparam>
/// <param name="value">可空的引用类型值</param>
/// <returns>如果值为null则返回None否则返回包含该值的Some</returns>
public static Option<T> ToOption<T>(this T? value)
where T : class
=> value is null
? Option<T>.None()
: Option<T>.Some(value);
/// <summary>
/// 将可空值类型转换为Option类型
/// </summary>
/// <typeparam name="T">值类型</typeparam>
/// <param name="value">可空的值类型值</param>
/// <returns>如果值有值则返回包含该值的Some否则返回None</returns>
public static Option<T> ToOption<T>(this T? value)
where T : struct
=> value.HasValue
? Option<T>.Some(value.Value)
: Option<T>.None();
}

View File

@ -0,0 +1,78 @@
// Copyright (c) 2026 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.types;
/// <summary>
/// 表示一个可能存在也可能不存在的值
/// </summary>
public readonly struct Option<T>
{
private readonly T _value;
/// <summary>
/// 获取当前Option是否包含值
/// </summary>
public bool IsSome { get; }
/// <summary>
/// 获取当前Option是否为空值
/// </summary>
public bool IsNone => !IsSome;
/// <summary>
/// 使用指定值创建Option实例
/// </summary>
/// <param name="value">要包装的值</param>
private Option(T value)
{
_value = value;
IsSome = true;
}
/// <summary>
/// 创建空的Option实例
/// </summary>
/// <param name="_">占位参数,用于区分构造函数重载</param>
private Option(bool _)
{
_value = default!;
IsSome = false;
}
/// <summary>
/// 创建包含指定值的Option实例
/// </summary>
/// <param name="value">要包装的值不能为null</param>
/// <returns>包含指定值的Option实例</returns>
/// <exception cref="ArgumentNullException">当value为null时抛出</exception>
public static Option<T> Some(T value)
{
return value is null ? throw new ArgumentNullException(nameof(value)) : new Option<T>(value);
}
/// <summary>
/// 创建空的Option实例
/// </summary>
/// <returns>空的Option实例</returns>
public static Option<T> None() => new(false);
/// <summary>
/// 获取Option中包含的值
/// </summary>
/// <exception cref="InvalidOperationException">当Option为空时抛出</exception>
public T Value =>
IsSome
? _value
: throw new InvalidOperationException("Option has no value");
}

View File

@ -0,0 +1,89 @@
// Copyright (c) 2026 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.types;
/// <summary>
/// 提供Option类型的功能扩展方法支持映射、绑定、过滤和匹配操作
/// </summary>
public static class OptionExtensions
{
/// <summary>
/// 将Option中的值通过指定的映射函数转换为另一种类型的Option
/// </summary>
/// <typeparam name="TSource">源Option中值的类型</typeparam>
/// <typeparam name="TResult">映射后结果的类型</typeparam>
/// <param name="option">要进行映射操作的Option实例</param>
/// <param name="mapper">用于将源值转换为目标值的映射函数</param>
/// <returns>包含映射后值的新Option实例如果原Option为空则返回None</returns>
public static Option<TResult> Map<TSource, TResult>(
this Option<TSource> option,
Func<TSource, TResult> mapper)
{
return option.IsSome
? Option<TResult>.Some(mapper(option.Value))
: Option<TResult>.None();
}
/// <summary>
/// 将Option中的值通过指定的绑定函数转换为另一个Option实现Option的扁平化映射
/// </summary>
/// <typeparam name="TSource">源Option中值的类型</typeparam>
/// <typeparam name="TResult">绑定后Option中值的类型</typeparam>
/// <param name="option">要进行绑定操作的Option实例</param>
/// <param name="binder">用于将源值转换为新Option的绑定函数</param>
/// <returns>绑定函数返回的Option实例如果原Option为空则返回None</returns>
public static Option<TResult> Bind<TSource, TResult>(
this Option<TSource> option,
Func<TSource, Option<TResult>> binder)
{
return option.IsSome
? binder(option.Value)
: Option<TResult>.None();
}
/// <summary>
/// 根据指定的谓词函数过滤Option中的值
/// </summary>
/// <typeparam name="TSource">Option中值的类型</typeparam>
/// <param name="option">要进行过滤操作的Option实例</param>
/// <param name="predicate">用于判断值是否满足条件的谓词函数</param>
/// <returns>如果Option有值且满足谓词条件则返回原Option否则返回None</returns>
public static Option<TSource> Filter<TSource>(
this Option<TSource> option,
Func<TSource, bool> predicate)
{
return option.IsSome && predicate(option.Value)
? option
: Option<TSource>.None();
}
/// <summary>
/// 对Option进行模式匹配根据Option的状态执行不同的函数
/// </summary>
/// <typeparam name="TSource">Option中值的类型</typeparam>
/// <typeparam name="TResult">匹配结果的类型</typeparam>
/// <param name="option">要进行匹配操作的Option实例</param>
/// <param name="some">当Option包含值时执行的函数</param>
/// <param name="none">当Option为空时执行的函数</param>
/// <returns>根据Option状态执行相应函数后的结果</returns>
public static TResult Match<TSource, TResult>(
this Option<TSource> option,
Func<TSource, TResult> some,
Func<TResult> none)
{
return option.IsSome
? some(option.Value)
: none();
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) 2026 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.types;
/// <summary>
/// 提供Option类型值的操作扩展方法
/// </summary>
public static class OptionValueExtensions
{
/// <summary>
/// 获取Option中的值如果Option为空则返回默认值
/// </summary>
/// <typeparam name="T">Option中存储的值的类型</typeparam>
/// <param name="option">要获取值的Option对象</param>
/// <param name="defaultValue">当Option为空时返回的默认值</param>
/// <returns>Option中的值或默认值</returns>
public static T GetOrElse<T>(
this Option<T> option,
T defaultValue)
=> option.IsSome ? option.Value : defaultValue;
/// <summary>
/// 获取Option中的值如果Option为空则通过工厂函数生成默认值
/// </summary>
/// <typeparam name="T">Option中存储的值的类型</typeparam>
/// <param name="option">要获取值的Option对象</param>
/// <param name="defaultFactory">当Option为空时用于生成默认值的工厂函数</param>
/// <returns>Option中的值或通过工厂函数生成的值</returns>
public static T GetOrElse<T>(
this Option<T> option,
Func<T> defaultFactory)
=> option.IsSome ? option.Value : defaultFactory();
/// <summary>
/// 获取当前Option如果当前Option为空则返回备用Option
/// </summary>
/// <typeparam name="T">Option中存储的值的类型</typeparam>
/// <param name="option">当前Option对象</param>
/// <param name="fallback">当当前Option为空时返回的备用Option</param>
/// <returns>当前Option或备用Option</returns>
public static Option<T> OrElse<T>(
this Option<T> option,
Option<T> fallback)
=> option.IsSome ? option : fallback;
}

View File

@ -17,4 +17,11 @@ namespace GFramework.Game.Abstractions.setting;
/// 定义一个可重置且可应用设置的接口
/// 该接口继承自IResettable和IApplyAbleSettings接口组合了重置功能和应用设置功能
/// </summary>
public interface IResetApplyAbleSettings : IResettable, IApplyAbleSettings;
public interface IResetApplyAbleSettings : IResettable, IApplyAbleSettings
{
/// <summary>
/// 获取设置数据对象
/// </summary>
/// <returns>ISettingsData类型的设置数据</returns>
ISettingsData Data { get; }
}

View File

@ -13,6 +13,7 @@
global using System;
global using System.Collections.Generic;
global using System.Collections.Concurrent;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;

View File

@ -1,5 +1,4 @@
using System.Collections.Concurrent;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.extensions;
using GFramework.Core.logging;
using GFramework.Core.model;

View File

@ -34,6 +34,8 @@ public class GodotAudioSettings(ISettingsModel model, AudioBusMap audioBusMap)
model.GetData<AudioSettings>().Reset();
}
public ISettingsData Data { get; } = model.GetData<AudioSettings>();
/// <summary>
/// 设置指定音频总线的音量
/// </summary>

View File

@ -50,4 +50,6 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin
{
model.GetData<GraphicsSettings>().Reset();
}
public ISettingsData Data { get; } = model.GetData<GraphicsSettings>();
}

View File

@ -47,4 +47,6 @@ public class GodotLocalizationSettings(ISettingsModel model, LocalizationMap loc
{
model.GetData<LocalizationSettings>().Reset();
}
public ISettingsData Data { get; } = model.GetData<LocalizationSettings>();
}