前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >C# 中的函数编程:实用部分

C# 中的函数编程:实用部分

作者头像
郑子铭
发布2025-01-16 21:43:31
发布2025-01-16 21:43:31
8300
代码可运行
举报
运行总次数:0
代码可运行

函数式编程模式常常给人一种学术性和抽象的感觉。"单子"(monads)和"函子"(functors)这样的术语往往会吓退许多开发者。但在这些令人生畏的术语背后,其实隐藏着一些能让代码更安全、更易维护的实用模式。

C#多年来已经采纳了许多函数式编程特性:

  • Records用于实现不可变性
  • LINQ用于函数式转换
  • Lambda表达式实现一等函数

这些特性不仅仅是语法糖 — 它们能帮助预防bug,使代码更容易理解。

让我们来看看今天就能在C#项目中使用的五种实用模式。

高阶函数 高阶函数可以接收其他函数作为参数或将函数作为结果返回。它们让你能够编写更灵活和可组合的代码,因为你可以像传递数据一样传递行为。

高阶函数的常见例子是LINQ中的Where和Select,它们接收用于转换数据的函数。

让我们用高阶函数重构这个验证示例:

代码语言:javascript
代码运行次数:0
复制
public classOrderValidator
{
    publicboolValidateOrder(Order order)
    {
        if(order.Items.Count ==)returnfalse;
        if(order.TotalAmount <=)returnfalse;
        if(order.ShippingAddress ==null)returnfalse;
        returntrue;
    }
}

// What if we need:
// - different validation rules for different countries?
// - to reuse some validations but not others?
// - to combine validations differently?

这里展示了如何使用高阶函数使其更灵活:

代码语言:javascript
代码运行次数:0
复制
public staticclassOrderValidation
{
    publicstaticFunc<Order, bool>CreateValidator(string countryCode,decimal minimumOrderValue)
    {
        var baseValidations =CombineValidations(
            o => o.Items.Count >,
            o => o.TotalAmount >= minimumOrderValue,
            o => o.ShippingAddress !=null
        );

        return countryCode switch
        {
            "US"=>CombineValidations(
                baseValidations,
                order =>IsValidUSAddress(order.ShippingAddress)),
            "EU"=>CombineValidations(
                baseValidations,
                order =>IsValidVATNumber(order.VatNumber)),
            _ => baseValidations
        };
    }

    privatestaticFunc<Order, bool>CombineValidations(paramsFunc<Order, bool>[] validations)=>
        order => validations.All(v =>v(order));
}

// Usage
var usValidator = OrderValidation.CreateValidator("US",minimumOrderValue:25.0m);
var euValidator = OrderValidation.CreateValidator("EU",minimumOrderValue:30.0m);

高阶函数方法使验证器变得可组合、可测试且易于扩展。每个验证规则都是一个简单的函数,我们可以将它们组合起来。

将错误作为值 C#中的错误处理通常是这样的:

代码语言:javascript
代码运行次数:0
复制
public classUserService
{
    publicUserCreateUser(string email,string password)
    {
        if(string.IsNullOrEmpty(email))
        {
            thrownewArgumentException("Email is required");
        }

        if(password.Length <)
        {
            thrownewArgumentException("Password too short");
        }

        if(_userRepository.EmailExists(email))
        {
            thrownewDuplicateEmailException(email);
        }

        // Create user...
    }
}

存在的问题?

  • 异常处理代价高昂
  • 调用者常常忘记处理异常
  • 方法签名具有欺骗性 — 它声称返回User但可能会抛出异常

我们可以使用OneOf库使错误变得明确。它为C#提供了判别联合,使用自定义类型OneOf<T0, ... Tn>。

代码语言:javascript
代码运行次数:0
复制
public classUserService
{
    publicOneOf<User, ValidationError, DuplicateEmailError>CreateUser(string email,string password)
    {
        if(string.IsNullOrEmpty(email))
        {
            returnnewValidationError("Email is required");
        }

        if(password.Length <)
        {
            returnnewValidationError("Password too short");
        }

        if(_userRepository.EmailExists(email))
        {
            returnnewDuplicateEmailError(email);
        }

        returnnewUser(email, password);
    }
}

通过使错误明确化:

  • 方法签名完全说明了真相
  • 调用者必须处理所有可能的结果
  • 没有异常带来的性能开销
  • 流程更容易理解

使用方式如下:

代码语言:javascript
代码运行次数:0
复制
var result = userService.CreateUser(email, password);

result.Switch(
    user => SendWelcomeEmail(user),
    validationError => HandleError(validationError),
    duplicateError => HandleError(duplicateError)
);

单子绑定 单子是值的容器 — 像List、IEnumerable或Task。它的特别之处在于你可以对容器内的值进行链式操作,而无需直接处理容器。这种链式操作称为单子绑定。

你每天都在使用LINQ时都在使用单子绑定,只是可能不知道。它允许我们链式操作来转换数据。

Map (Select) 转换值:

代码语言:javascript
代码运行次数:0
复制
// Simple transformations with Select (Map)
var numbers = new[] { , , ,  };

var doubled = numbers.Select(x => x * );

Bind (SelectMany) 转换并展平:

代码语言:javascript
代码运行次数:0
复制
// Operations that return multiple values use SelectMany (Bind)
var folders = new[] { "docs", "photos" };

var files = folders.SelectMany(folder => Directory.GetFiles(folder));

在实践中应用单子的一个流行例子是Result模式,它提供了一种清晰的方式来链接可能失败的操作。

纯函数 纯函数是可预测的:它们只依赖于输入,不会改变系统中的任何东西。没有数据库调用,没有API请求,没有全局状态。这种约束使它们更容易理解、测试和调试。

代码语言:javascript
代码运行次数:0
复制
// Impure - relies on hidden state
publicclassPriceCalculator
{
    privatedecimal _taxRate;
    privateList<Discount> _activeDiscounts;

    publicdecimalCalculatePrice(Order order)
    {
        var price = order.Items.Sum(i => i.Price);

        foreach(var discount in _activeDiscounts)
        {
            price -= discount.Calculate(price);
        }

        return price *(+ _taxRate);
    }
}

这是同样的例子作为纯函数:

代码语言:javascript
代码运行次数:0
复制
// Pure - everything is explicit
publicstaticclassPriceCalculator
{
    publicstaticdecimalCalculatePrice(
        Order order,
        decimal taxRate,
        IReadOnlyList<Discount> discounts)
    {
        var basePrice = order.Items.Sum(i => i.Price);

        var afterDiscounts = discounts.Aggregate(
            basePrice,
            (price, discount)=> price - discount.Calculate(price));

        return afterDiscounts *(+ taxRate);
    }
}

纯函数是线程安全的,易于测试,并且易于理解,因为所有依赖都是显式的。

不可变性 不可变对象在创建后不能被更改。相反,它们为每个更改创建新的实例。这个简单的约束消除了整类bug:竞态条件、意外修改和不一致状态。

这是一个可变类型的例子:

代码语言:javascript
代码运行次数:0
复制
public classOrder
{
    publicList<OrderItem> Items {get;set;}
    publicdecimal Total {get;set;}
    publicOrderStatus Status {get;set;}

    publicvoidAddItem(OrderItem item)
    {
        Items.Add(item);
        Total += item.Price;
        // Bug: Thread safety issues
        // Bug: Can modify shipped orders
        // Bug: Total might not match Items
    }
}

让我们将其改造为不可变类型:

代码语言:javascript
代码运行次数:0
复制
public recordOrder
{
    publicImmutableList<OrderItem> Items {get;init;}
    publicOrderStatus Status {get;init;}
    publicdecimal Total => Items.Sum(x => x.Price);

    publicOrderAddItem(OrderItem item)
    {
        if(Status != OrderStatus.Created)
        {
            thrownewInvalidOperationException("Can't modify shipped orders");
        }

        returnthiswith
        {
            Items = Items.Add(item)
        };
    }
}

不可变版本的特点:

  • 默认线程安全
  • 使无效状态变得不可能
  • 保持数据和计算的一致性
  • 使更改明确且可追踪

函数式编程不仅仅是关于写"更干净"的代码。这些模式从根本上改变了你处理复杂性的方式:

  • 将错误推送到编译时 — 在运行代码之前捕获问题
  • 使无效状态变得不可能 — 不依赖文档或约定
  • 使正确路径明显 — 当一切都是显式的,流程就很清晰

你可以逐步采用这些模式。从一个类、一个模块、一个功能开始。目标不是写纯函数式代码。目标是写出更安全、更可预测、更易维护的代码。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-01-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档