[杂谈]在C#中应用的函数式编程概念

发布于:2019-10-16

杂谈

原文-Functional Programming Concepts Applied Using C#

近年来函数式编程得到了很大的发展。读者也许对函数式编程很感兴趣,但是依然无法在日常工作中应用函数式编程。本篇文章旨在能为读者提供一个将函数式编程理念和技术应用到.NET Framework的指南。

箭头符号

在函数式语言中箭头符号主要是作为函数签名语法的首选语法,并且箭头函数具备右结合联属性(right associative)。如下面的例子

(int) -> ((int) -> (int)) EQUALS int -> int -> int

这可以被理解为,该函数使用两个整形数值作为参数并返回整形数值作为结果。在C#中,有3种方式可以创建该函数

Delegates = private delegate int ExampleDelegate(int left, int right) 其中left和right是函数签名中的前两个整形值,返回类型是函数签名中的第三个值。

using System;
 
internal class TestDelegates
{
    // int -> int -> int
    private delegate int MyFirstSumDelegate(int x, int y);
 
    // int -> int -> int -> ()
    private delegate void MySecondSumDelegate(int x, int y, int z);
 
    internal static void Run()
    {
        MyFirstSumDelegate myFirstSumDelegate = SumTwo;
 
        var delegateResult = myFirstSumDelegate(1, 2);
 
        Console.WriteLine(delegateResult);
 
        MySecondSumDelegate mySecondSumDelegate = SumThreeWithLog;
        mySecondSumDelegate(1, 2, 3);
    }
 
    private static int SumTwo(int a, int b) => a + b;
 
    private static void SumThreeWithLog(int a, int b, int c)
    {
        var result = a + b + c;
        Console.WriteLine(result);
    }
}

Funcs = private Func <int, int, int> ExampleFunc 在这个例子中,前两个整形是参数,最后一个整形值是返回类型。

using System;
 
internal class TestFuncs
{
    // int -> int -> int
    private delegate int MySumDelegate(int x, int y);
 
    internal static void Run()
    {
        MySumDelegate mySumDelegate = Sum;
        // int -> int -> int
        Func<int, int, int> mySumFunc = Sum;
 
        var delegateResult = mySumDelegate(1, 2);
        var funcResult = mySumFunc(1, 2);
 
        Console.WriteLine(delegateResult);
        Console.WriteLine(funcResult);
    }
 
    private static int Sum(int a, int b) => a + b
}

Actions = private Action<int, int, int> ExampleAction 这是一个非常特殊的情况,它们被认为是函数, 但是这个函数的返回值永远都是void,在这个函数中它接受3个整形参数并且返回void。

using System;
 
internal class TestActions
{
    // int -> int -> int -> ()
    private delegate void MySumDelegate(int x, int y, int z);
 
    internal static void Run()
    {
        MySumDelegate mySumDelegate = SumWithLog;
        // int -> int -> int -> ()
        Action<int, int, int> mySumAction = SumWithLog;
 
        mySumDelegate(1, 2, 3);
        mySumAction(1, 2, 3);
    }
 
    private static void SumWithLog(int a, int b, int c)
    {
        var result = a + b + c;
        Console.WriteLine(result);
    }
}

函数作为一等公民

当函数成为语言中的一等公民的时候,这就意味这函数具备下面的特性

  • 可以作为其它函数的参数值
  • 可以作为一个函数的返回值
  • 可以被赋值给变量
  • 可以存储到集合中
using System;
using System.Collections.Generic;
using static System.Console;
// you could use an alias to simplify types
// int -> int -> int
using Adder = System.Func<int, int, int>;
 
internal class TestFirstClassFunctions
{
    // Assigned to a variable
    private readonly static Adder adder = (x, y) => x + y;
 
    internal static void Run()
    {
        // Stored in a collection
        var functions = new Dictionary<string, Adder>
        {
            { "One", adder },
            { "Two", Generator(adder) },
        };
 
        var index = 1;
 
        foreach (var f in functions)
            WriteLine($"{f.Key} => {f.Value.Invoke(index, ++index)}");
    }
 
    // As return type and as parameter
    private static Adder Generator(Adder adder) => adder;
}

高阶函数

高阶函数就是可以被认为是一个函数接受一个参数做参数或返回一个函数做结果,或者同时接受一个函数做参数且返回一个函数做结果

using System;
using static System.Console;
using Subtractor = System.Func<int, int, int>;
 
internal class TestHigherOrderFunctions
{
    private readonly static Subtractor subtractor = (x, y) => x - y;
 
    internal static void Run() =>
        WriteLine(Generator(subtractor)(44, 2));
 
    // This is a HOF
    // takes a functions as argument
    // and returns a function
    private static Subtractor Generator(Subtractor subtractor)
        => subtractor;
}

纯函数

如果一个函数的返回结果,完全依赖于传入的参数,就可以认为这个函数是一个纯函数。因为不产生任何副作用, 因此使用纯函数可以在下面几点上得到提高:

  • 易于测试
  • 正确性
  • 可读性

因为不存在副作用,所以就不会产生意外的情况,因此我们编写的代码就如我们所预期的那样工作。同时,我们的代码 不会带来过多的认知负担,因此这些代码将简单易懂,易于维护。

using static System.Console;
 
internal class TestPureFunctions
{
    private const string A = "a";
    private const string B = "b";
    private const string AB = "ab";
    private const string ABAB = "ab";
 
    internal static void Run()
    {
        var ab = Join(A, B);
        // We can expect the same result always
        // if we pass the same parameters
        WriteLine(ab == AB);
        WriteLine(Join(A, B) == AB);
 
        var abab = Join(AB, AB);
        // This also has an impact
        // on Referential Transparency
        WriteLine(abab == ABAB);
        WriteLine(Join(AB, AB) == ABAB);
    }
 
    private static string Join(string lhs, string rhs)
        => $"{lhs}{rhs}";
}

副作用

为了能让副作用这个定义更加清晰,就需要明确的定义什么是副作用,如果一个函数产生了副作用,那么它的行为将会符合下面任意一项:

  • 更改了全局的状态,在这里“全局”代表任何该函数作用域以外的状态。例如一个实例的私有数据域就被认为是全局的,因为它可以被该实例类的所有函数进行修改。
  • 更改了传入参数,这经常发生在参数为对象的时候,函数修改了对象中的数据域
  • 抛出了异常
  • 进行了IO操作,包括任何应用和外部世界进行交互的行为,对console,文件系统或者数据库的读写操作,以及和其它进程进行交互。
using static System.Console;
 
internal class TestSideEffects
{
    internal static void Run()
    {
        var ab = JoinWithLogs("a", "b");
        var abab = JoinWithLogs(ab, ab);
 
        WriteLine(ab);
        WriteLine(abab);
    }
 
    private static string JoinWithLogs(string lhs, string rhs)
    {
        // Here we have the side effect of writing to console
        // but it could be any I/O operation.
        WriteLine($"lhs before: {lhs}");
        WriteLine($"rhs before: {rhs}");
 
        var result = $"{lhs}{rhs}";
 
        WriteLine($"result: {result}");
        return result;
    }
}

不可变性

在OOP或者FP中,不可以变对象(不改变的对象)是一种从对象被创建后就不会再发生改变的一种对象。 在C#中,我们可以使用Nuget中的包去强化这个特性。并且我们可以使用下面这些做法来完成不可变。

using System.Collections.Generic;
using System.Collections.Immutable; // Nuget package
using System.Linq;
using static System.Console;
 
internal static class TestImmutability
{
    // readonly fields are immutable
    // once they are initialized. at run-time
    private static readonly string dashes = new string('-', 50);
    private static readonly IEnumerable<int> numbers = Enumerable.Range(1, int.MaxValue);
 
    // const fields are immutable as well. at compile-time
    private const int Ten = 10;
    private const int Six = 6;
    private const int Three = 3;
 
    internal static void Run()
    {
        List();
        Stack();
        Queue();
    }
 
    private static void List()
    {
        Separator("Immutable List");
 
        var list = ImmutableList<int>.Empty;
        Action<int> listAdder = number => list = list.Add(number * Ten);
 
        numbers
            .Skip(100)
            .Take(10)
            .AddTo(listAdder);
 
        list.PrintToConsole();
    }
 
    private static void Stack()
    {
        Separator("Immutable Stack");
 
        var stack = ImmutableStack<int>.Empty;
        Action<int> stackAdder = number => stack = stack.Push(number * Six);
 
        numbers
            .SkipWhile(n => n < 200)
            .TakeWhile(n => n < 220)
            .Where(n => n % 3 == 0)
            .AddTo(stackAdder);
 
        stack.PrintToConsole();
    }
 
    private static void Queue()
    {
        Separator("Immutable Queue");
 
        var queue = ImmutableQueue<int>.Empty;
        Action<int> queueAdder = number => queue = queue.Enqueue(number * Three);
 
        numbers
            .SkipWhile(n => n < 1000)
            .TakeWhile(n => n < 1011)
            .Where(n => n % 2 == 0)
            .AddTo(queueAdder);
 
        queue.PrintToConsole();
    }
 
    private static void AddTo<T>(this IEnumerable<T> items, Action<T> addToOtherCollection)
    {
        foreach (var item in items)
            addToOtherCollection(item);
    }
 
    private static void PrintToConsole<T>(this IEnumerable<T> items)
    {
        foreach (var item in items)
            WriteLine(item);
    }
 
    private static void Separator(this string text)
    {
        WriteLine(dashes);
        WriteLine(text);
        WriteLine(dashes);
    }
}

函数的元

从逻辑,数学和计算机科学的角度来说,函数的元就是函数的可以接受的参数的个数。

  • Nullary 不使用参数
  • Unary 使用单参数
  • Binary 使用两个参数
  • Ternary 使用三个参数
  • n-ary 使用n个参数

using System.Linq;
using static System.Console;
 
internal class TestArity
{
    internal static void Run()
    {
        WriteLine(Nullary());
        WriteLine(Unary(1));
        WriteLine(Binary(1, 2));
        WriteLine(Ternary(1, 2, 3));
        WriteLine(N_Ary(1, 2, 3, 4));
        WriteLine(N_Ary(Enumerable.Range(1, 16).ToArray()));
        WriteLine(N_Ary(Enumerable.Range(1, 100).ToArray()));
    }
 
    private static string Nullary()
        => "No arguments";
 
    private static string Unary(int one)
        => $"One argument: {one}";
 
    private static string Binary(int one, int two)
        => $"Two arguments: {one} and {two}";
 
    private static string Ternary(int one, int two, int three)
        => $"Three arguments: {one}, {two} and {three}";
 
    private static string N_Ary(params int[] arguments)
        => $"N arguments: {arguments.Length}";
}

引用透明

在函数编程语言中,引用透明一般定义为程序中的一个表达式,可以被它的值(或任何具备相同值的表达式或者值)取代,而不改变程序的结果。 这就代表着,程序中的方法,在给定的参数下,不会产生额外的作用,总会返回相同的值。 这些函数式编程的理念,同样可以应用到过程式编程语言中,这些理念会使代码更加的清晰。

using static System.Console;
 
internal class TestReferentialTransparency
{
    private const int Two = 2;
    private const int Nine = 9;
    private const int Sixteen = 16;
    private const int Eighteen = 18;
 
    internal static void Run()
    {
        OperationsWithReferentialTransparency();
        OperationsWithoutReferentialTransparency();
    }
 
    private static void OperationsWithReferentialTransparency()
    {
        // YES - Sum(16, 2) == 18 and Mul(9, 2) == 18
        // and the result of the program will be the same
        var sum = Sum(Sixteen, Two) == Eighteen;
        var mul = Mul(Nine, Two) == Eighteen;
    }
 
    private static void OperationsWithoutReferentialTransparency()
    {
        // NO - Sum(16, 2) == 18 and Mul(9, 2) == 18
        // but WE ARE NOT logging to the console the result
        var sum = SumLog(Sixteen, Two) == Eighteen;
        var mul = MulLog(Nine, Two) == Eighteen;
    }
 
    private static int Sum(int a, int b) => a + b;
 
    private static int Mul(int a, int b) => a * b;
 
    private static int SumLog(int a, int b)
    {
        var result = a + b;
        WriteLine($"Returning {result}");
        return result;
    }
 
    private static int MulLog(int a, int b)
    {
        var result = a * b;
        WriteLine($"Returning {result}");
        return result;
    }
}

Delegates

Delegates是C#中最常见的第一等公民,非常常见,也非常常用。

using System;
using System.Diagnostics;

internal static class TestDelegates
{
    // string -> ()
    private delegate void LoggerDelegate(string message);
    // string -> string
    private delegate string LoggerWithResponseDelegate(string message);

    internal static void Run()
    {
        RunVoidDelegates();
        RunResponseDelegates();
    }

    private static void RunVoidDelegates()
    {
        LoggerDelegate consoleLoggerHandler = ConsoleLogger;
        LoggerDelegate debugLoggerHandler = DebugLogger;
        LoggerDelegate allConsoleHandlers = consoleLoggerHandler + debugLoggerHandler;

        consoleLoggerHandler("This goes to the console");
        debugLoggerHandler("This goes to the debug");
        allConsoleHandlers("this goes to all");
    }

    private static void RunResponseDelegates()
    {
        LoggerWithResponseDelegate consoleLoggerHandler = ConsoleLoggerWithResponse;
        LoggerWithResponseDelegate debugLoggerHandler = DebugLoggerWithResponse;
        LoggerWithResponseDelegate allConsoleHandlers = consoleLoggerHandler + debugLoggerHandler;

        var consoleResponse = consoleLoggerHandler("This goes to the console");
        var debugResponse = debugLoggerHandler("This goes to the debug");
        var lastResponse = allConsoleHandlers("this goes to all");
    }

    private static void ConsoleLogger(string message) => Console.WriteLine(message);

    private static void DebugLogger(string message) => Debug.WriteLine(message);

    private static string ConsoleLoggerWithResponse(string message)
    {
        Console.WriteLine(message);
        return "Logged to console";
    }

    private static string DebugLoggerWithResponse(string message)
    {
        Debug.WriteLine(message);
        return "Logged to debug";
    }
}

Actions

Actions是.NET Framework 为我们提供创建delegates的语法糖。Actions从来都不会返回值,因为Actions总是返回void,并且Actions最多可以接受16个值作为参数。 需要注意以下两点

  • 在FP中void返回类型被称为unit
  • 在箭头符号中unit被解释为()
using System;
using System.Diagnostics;

internal static class TestActions
{
    internal static void Run()
    {
        // string -> ()
        Action<string> consoleLoggerHandler = ConsoleLogger;
        Action<string> debugLoggerHandler = DebugLogger;
        Action<string> allConsoleHandlers = consoleLoggerHandler + debugLoggerHandler;

        consoleLoggerHandler("This goes to the console");
        debugLoggerHandler("This goes to the debug");
        allConsoleHandlers("this goes to all");
    }

    private static void ConsoleLogger(string message) => Console.WriteLine(message);

    private static void DebugLogger(string message) => Debug.WriteLine(message);
}

Funcs

Funcs是.NET Framework 为我们提供创建delegates的另一个语法糖。但是Funcs并没有固定返回类型,同样Funcs也可以接受16个值作为参数。 Funcs和Actions之间最大的不同,Funcs的最后一个参数代表这函数的返回类型。

using System;
using System.Diagnostics;

internal static class TestFuncs
{
    internal static void Run()
    {
        // string -> string
        Func<string, string> consoleLoggerHandler = ConsoleLogger;
        Func<string, string> debugLoggerHandler = DebugLogger;
        Func<string, string> allConsoleHandlers = consoleLoggerHandler + debugLoggerHandler;

        var consoleResponse = consoleLoggerHandler("This goes to the console");
        var debugResponse = debugLoggerHandler("This goes to the debug");
        var lastResponse = allConsoleHandlers("this goes to all");
    }

    private static string ConsoleLogger(string message)
    {
        Console.WriteLine(message);
        return "Logged to console";
    }

    private static string DebugLogger(string message)
    {
        Debug.WriteLine(message);
        return "Logged to debug";
    }
}

科里化函数

科里化函数是以伟大数学家Haskell Curry命名的。科里化过程,是将一个接收t1,t2,……,为参数的n元函数f转化成一个接收t1作为参数并返回一个接收t2作为参数的1元函数,一直到n-1个参数为止。 只要所有的参数都被传入,最终返回的结果和函数f的结果是相同的。换句化说,一个具有下面签名的n元函数

(T1, T2, …, Tn) → R

进行科里化之后,可以得到下面的函数签名

(T1) (T2) … (Tn) → R

就这个技术自身而言,并没有什么实际的意义,但是科里化是偏函数应用的基础。

using System;
using static System.Console;

internal class TestCurriedFunctions
{
    internal static void Run()
    {
        WriteLine(Greeter("Hello")("World"));
        WriteLine(Sum(5)(18));
    }

    private static Func<string, Func<string, string>> Greeter =>
        (firstName) => (lastName) => $"{firstName} {lastName}";

    private static Func<int, Func<int, int>> Sum =>
        (lhs) => (rhs) => lhs + rhs;
}

偏函数应用

为函数提供少于它所需要的参数的数量时,我们称之为偏函数应用。为了能让函数更加抽象从而能更好的重用,就需要一种机制去给函数预置一些参数。 偏函数应用,就是一种高元函数(一个接收多个参数多函数)转化成多个接收少量参数的函数的方法。

using System;
using static System.Console;

internal class TestPartialApplication
{
    internal static void Run()
    {
        WriteLine(MaleFormalGreeter("World"));
        WriteLine(FemaleFormalGreeter("World"));
        WriteLine(PlusFive(5));
        WriteLine(PlusFive(18));
    }

    private static Func<string, Func<string, string>> Greeter =>
        (firstName) => (lastName) => $"{firstName} {lastName}";

    private static Func<string, string> MaleFormalGreeter =>
        (lastName) => Greeter("Mr.")(lastName);

    private static Func<string, string> FemaleFormalGreeter =>
        (lastName) => Greeter("Ms.")(lastName);

    private static Func<int, Func<int, int>> Sum =>
        (lhs) => (rhs) => lhs + rhs;

    private static Func<int, int> PlusFive =>
        (factor) => Sum(factor)(5);
}

惰性计算/延迟执行

在计算过程中惰性,代表一个表达式会被延迟到需要它的值的时候才会被进行求职。 它的好处就是,如果一个表达式的代价很大,并且它并不是每次都必须计算的,按需计算就会产生更小的代价。

using System;
using System.Collections.Generic;
using System.Linq;
using static System.Console;

internal class TestLazyEvaluation
{
    private static IEnumerable<int> LazyNumbers
    {
        get
        {
            yield return 1;
            yield return 2;
            yield return 3;
            yield return 4;
            yield return 5;
            yield return 6;
            yield return 7;
            yield return 8;
            yield return 9;
            yield return 10;
        }
    }

    internal static void Run()
    {
        LazinessWithFunctions();
        LazinessWithEnumerators();
    }

    private static void LazinessWithFunctions()
    {
        var random = new Random();
        Func<bool> isLessThanZeroPointFive = () => random.NextDouble() < 0.5;
        Func<int> left = () => 23;
        Func<int> right = () => 42;
        Func<int> lazyExecuted = () => isLessThanZeroPointFive()
            ? left()
            : right();

        // all the previous code is evaluated
        // until we call lazyExecuted function
        WriteLine(lazyExecuted());
    }

    private static void LazinessWithEnumerators()
    {
        var skipFour = LazyNumbers.Skip(4);
        var takeFour = skipFour.Take(4);

        // all the previous code is evaluated
        // until we enumerate the collection in a foreach
        foreach (var number in takeFour)
            WriteLine(number);
    }
}

Extension Methods

扩展方法,可以让我们用新方法来扩展原有类型,但是无需派生新的类型,重新编译或者修改原有类型。

  • 它们只是静态类中的静态方法
  • 使用开放封闭原则
  • 可以使用方法链
using System;
using static System.Console;

internal class TestExtensionMethods
{
    internal static void Run()
    {
        var person = new Person("George", 30);
        WriteLine(person);
        WriteLine($"Days lived from person object {person.DaysLived()}");
        WriteLine($"Days lived from Age property {person.Age.DaysLived()}");
    }
}

internal class Person
{
    internal string Name { get; }
    internal int Age { get; }

    internal Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public override string ToString() => $"My name is {Name} and I am {Age} years old";
}

internal static class PersonExtensions
{
    internal static int DaysLived(this Person person) =>
        person.Age * 365;

    internal static int DaysLived(this int age) => age * 365;
}

智能构造函数

智能构造函数是只生成所需类型的值的函数。 但是,在构造值时,它会执行一些额外的检查。 在这种情况下,实用的方案是将引用类型的构造函数设置为私有化,同时公开一个实例化的函数。

using System;
using static System.Console;

internal class TestSmartConstructors
{
    internal static void Run()
    {
        HappyPath();
        InvalidName();
        InvalidAge();
    }

    private static void HappyPath()
    {
        var person = SmartPerson.Create("George", 30);
        WriteLine($"Valid person data so all goes well and we can print {person}");
    }

    private static void InvalidName()
    {
        try
        {
            var person = SmartPerson.Create(null, 30);
        }
        catch (ArgumentNullException ex)
        {
            WriteLine($"{ex.Message}");
        }
    }

    private static void InvalidAge()
    {
        try
        {
            var person = SmartPerson.Create("George", -1);
        }
        catch (ArgumentOutOfRangeException ex)
        {
            WriteLine($"{ex.Message}");
        }
    }
}

internal class SmartPerson
{
    internal string Name { get; }
    internal int Age { get; }

    private SmartPerson(string name, int age) =>
        (Name, Age) = (name, age);

    internal static SmartPerson Create(string name, int age)
    {
        GuardName(name);
        GuardAge(age);

        return new SmartPerson(name, age);
    }

    private static void GuardName(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentNullException(nameof(name), "Name can't be null, empty or white spaces");
    }

    private static void GuardAge(int age)
    {
        if (age < 0 || age > 120)
            throw new ArgumentOutOfRangeException(nameof(age), "Age is not in valid range");
    }

    public override string ToString() => $"My name is {Name} and I am {Age} years old";
}

避免过度使用原始数据类型

过度使用的原始数据类型包括:string,int,bool,double和datetime等来表示复杂的类型,例如一个人的年龄,姓名和邮件等。 为了避免这些,我们应该创建更小的对象去表示它们,从而我们可以得到下面这些好处。

  • 所有的验证都在一处,带有智能构造函数的小类
  • 不可变
  • 自我检查
  • 深度值比较
using System;
using System.Text.RegularExpressions;
using static System.Console;

internal static class TestAvoidPrimitiveObsession
{
    internal static void Run()
    {
        HappyPathEmail();
        InvalidEmail();
        NullEmail();

        HappyPathAge(); 
        InvalidAge();
    }

    internal static void HappyPathEmail()
    {
        var emailOne = Email.Create("[email protected]");
        var emailTwo = Email.Create("[email protected]");
        var emailThree = Email.Create("[email protected]");

        // Implicit conversion using operators
        string fromEmail = Email.Create("[email protected]");
        Email fromString = "[email protected]";

        WriteLine($"{emailOne} == {emailTwo} ? {(emailOne.Equals(emailTwo))}");
        WriteLine($"{emailOne} == {emailThree} ? {(emailOne.Equals(emailThree))}");
    }

    private static void InvalidEmail()
    {
        try
        {
            var email = Email.Create("[email protected]");
        }
        catch (ArgumentException ex)
        {
            WriteLine(ex.Message);
        }
    }

    private static void NullEmail()
    {
        try
        {
            var email = Email.Create(null);
        }
        catch (ArgumentNullException ex)
        {
            WriteLine(ex.Message);
        }
    }

    private static void HappyPathAge()
    {
        var ageOne = Age.Create(30);
        var ageTwo =  Age.Create(25);
        var ageThree = Age.Create(30);

        // Implicit conversion using operators
        int fromAge = Age.Create(20);
        Age fromInt = 30;

        WriteLine($"{ageOne} == {ageTwo} ? {(ageOne.Equals(ageTwo))}");
        WriteLine($"{ageOne} == {ageThree} ? {(ageOne.Equals(ageThree))}");
    }

    private static void InvalidAge()
    {
        try
        {
            var age = Age.Create(130);
        }
        catch (ArgumentOutOfRangeException ex)
        {
            WriteLine(ex.Message);
        }
    }

    private class Email
    {
        private readonly string value;

        private Email(string value) => this.value = value;

        internal static Email Create(string email)
        {
            GuardEmail(email);
            return new Email(email);
        }

        private static void GuardEmail(string email)
        {
            if (email is null)
                throw new ArgumentNullException(nameof(email), "Email address cannot be null");

            var match = Regex.Match(
                email,
                Constants.EmailPattern,
                RegexOptions.Compiled | RegexOptions.IgnoreCase);

            if (!match.Success)
                throw new ArgumentException("Invalid Email address format", nameof(email));
        }

        public static implicit operator string(Email email) => email.value;

        public static implicit operator Email(string email) => Create(email);

        public override bool Equals(object obj)
        {
            var email = obj as Email;

            if (ReferenceEquals(email, null))
                return false;

            return this.value == email.value;
        }

        public override int GetHashCode() => value.GetHashCode();

        public override string ToString() => value;
    }

    private class Age
    {
        private readonly int value;

        private Age(int value) => this.value = value;

        internal static Age Create(int age)
        {
            GuardAge(age);
            return new Age(age);
        }

        private static void GuardAge(int age)
        {
            if (age < 0 || age > 120)
                throw new ArgumentOutOfRangeException(nameof(age), "Age is not in valid range");
        }

        public static implicit operator int(Age age) => age.value;

        public static implicit operator Age(int age) => Create(age);

        public override bool Equals(object obj)
        {
            var age = obj as Age;

            if (ReferenceEquals(age, null))
                return false;

            return this.value == age.value;
        }

        public override int GetHashCode() => value.GetHashCode();

        public override string ToString() => $"{value}";
    }

    private static class Constants
    {
        internal static string EmailPattern => @"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$";
    }
}

范化

范型可以让我们使用占位符去定义一个类,这些占位符可以是类的域,方法参数等。 在编译的时候,范化会用特定的类型代替这些占位符。

  • 让代码重用度更高,具备类型安全和提升性能
  • 它通常被用来创建集合类型的类
  • 通过约束条件来限制类型的范围
using System;
using System.Collections.Generic;
using static System.Console;

internal static class TestGenerics
{
    internal static void Run()
    {
         CreateGenerics();
         CreateGenericsWithConstraints();
    }

    private static void CreateGenerics()
    {
        new List<object>
        {
            Generic<int>.Create(42),
            Generic<long>.Create(42L),
            Generic<float>.Create(42.5F),
            Generic<double>.Create(42D),
            Generic<decimal>.Create(42.5M),
            Generic<bool>.Create(false),
            Generic<DateTime>.Create(DateTime.Now),
            Generic<byte>.Create(0x0042),
            Generic<char>.Create('A'),
            Generic<string>.Create("George"),
            Generic<Func<int, int>>.Create(x => x + x),
            Generic<Action<int, int>>.Create((x, y) => WriteLine(x + y)),
            Generic<dynamic>.Create(7.5F + 35L),
            Generic<object>.Create(new { a = 42 })
        }
        .ForEach(WriteLine);
    }

    private static void CreateGenericsWithConstraints()
    {
        new List<object>
        {
            GenericWithConstraints<int, object>.Create(42, new { A = 42 }),
            GenericWithConstraints<long, object>.Create(42L, new { B = 42L }),
            GenericWithConstraints<float, object>.Create(42F, new { C = 42F }),
            GenericWithConstraints<double, object>.Create(42D, new { D = 42D }),
            GenericWithConstraints<decimal, object>.Create(42M, new { E = 42M }),
            GenericWithConstraints<bool, object>.Create(true, new { F = true }),
            GenericWithConstraints<DateTime, object>.Create(DateTime.Now, new { G = DateTime.Now }),
            GenericWithConstraints<byte, object>.Create(0x0042, new { H = 0x0042 }),
            GenericWithConstraints<char, object>.Create('J', new { I = 'J' })
        }
        .ForEach(WriteLine);
    }

    private class Generic<T>
    {
        private T GenericReadOnlyProperty { get; }

        private Generic(T genericArgument) => GenericReadOnlyProperty = genericArgument;

        internal static Generic<T> Create(T genericValue) => new Generic<T>(genericValue);

        public override string ToString() => $"{GenericReadOnlyProperty} - {typeof(T)}";
    }

    private class GenericWithConstraints<T, U>
        where T : struct
        where U : new()
    {
        private T @Struct { get; }

        private U @Object { get; }

        private GenericWithConstraints(T @struct, U @object) =>
            (@Struct, @Object) = (@struct, @object);

        internal static GenericWithConstraints<T, U> Create(T @struct, U @object) =>
            new GenericWithConstraints<T, U>(@struct, @object);

        public override string ToString() =>
            $"{@Struct} - {typeof(T)} && {@Object} - {typeof(U)} ";
    }
}

LINQ

LINQ代表着Language INtegrated Query。它是一个功能强大的函数库,提供了很多lists或者说更泛化“序列”类型的常见操作。 只要是IEnumerable类型的实例,它们都可以作为LINQ的使用对象。在LINQ中最常用的操作是mapping, sorting,和 filtering。

  • 使用高阶函数作为它的参数,让LINQ具备了更强的扩展性
  • LINQ中可以使用方法语法或者查询语法
using System;
using System.Collections.Generic;
using System.Linq;
using static System.Console;

internal static class TestLINQ
{
    private static readonly string Dashes = new string('-', 40);

    private static readonly IEnumerable<char> Alphabet = Enumerable
        .Range('a', 'z' - 'a' + 1)
        .Select(i => (Char)i);

    private static readonly IEnumerable<int> LazyNumbers = Enumerable.Range(0, 50);

    private static IEnumerable<IEnumerable<int>> NestedLazyNumbers
    {
        get
        {
            yield return LazyNumbers;
            yield return LazyNumbers;
        }
    }

    internal static void Run()
    {
        Map();
        Filter();
        Zip();
        Take();
        Skip();
        TakeWhile();
        SkipWhile();
        OrderBy();
        OrderByDescending();
        FoldRight();
        FoldLeft();
        Bind();
    }

    private static void Map()
    {
        LazyNumbers
            .Select(number => number * 10)
            .Print(nameof(Map));
    }

    private static void Filter()
    {
        LazyNumbers
            .Where(number => number > 5)
            .Print(nameof(Filter));
    }

    private static void Zip()
    {
        LazyNumbers
            .Zip(Alphabet, (number, letter) => $"{number} --> {letter}")
            .Print(nameof(Zip));
    }

    private static void Take()
    {
        LazyNumbers
            .Take(10)
            .Print(nameof(Take));
    }

    private static void Skip()
    {
        LazyNumbers
            .Skip(40)
            .Print(nameof(Skip));
    }

    private static void TakeWhile()
    {
        LazyNumbers
            .TakeWhile(number => number < 10)
            .Print(nameof(TakeWhile));
    }

    private static void SkipWhile()
    {
        LazyNumbers
            .SkipWhile(number => number < 40)
            .Print(nameof(SkipWhile));
    }

    private static void OrderBy()
    {
        LazyNumbers
            .OrderBy(number => number)
            .Print(nameof(OrderBy));
    }

    private static void OrderByDescending()
    {
        LazyNumbers
            .OrderByDescending(number => number)
            .Print(nameof(OrderByDescending));
    }

    private static void FoldRight()
    {
        LazyNumbers
            .Take(10)
            .Aggregate((accumulator, next) => accumulator + next)
            .ToEnumerable()
            .Print(nameof(FoldRight));
    }

    private static void FoldLeft()
    {
        LazyNumbers
            .Skip(40)
            .Reverse()
            .Aggregate((accumulator, next) => accumulator - next)
            .ToEnumerable()
            .Print(nameof(FoldLeft));
    }

    private static void Bind()
    {
        NestedLazyNumbers
            .SelectMany(innerEnumerable => innerEnumerable)
            .OrderBy(number => number)
            .Take(20)
            .Print(nameof(Bind));
    }

    private static void Print<TItem>(this IEnumerable<TItem> items, string message)
    {
        var spaces = new string(' ', 31 - message.Length);
        WriteLine($"{Dashes} {message}{spaces}{Dashes}");

        foreach (var item in items)
            WriteLine(item);
    }

    private static IEnumerable<TItem> ToEnumerable<TItem>(this TItem item)
    {
        yield return item;
    }
}

总结

我希望这片文章能帮助读者理解一些FP方面的知识,并可以将它们应用到自己的项目中。从而提高项目的可读性,可测试性和软件的稳定性。请记住FP和OOP从来都不是对立的,它们是正交的,我们可以同时使用它们来完善我们的项目。