1. 程式人生 > 實用技巧 >C#高階函式介紹

C#高階函式介紹

導語

一般常用的高階函式函式有Map,Filter,Fold,Flatten,FlatMap。C#的函數語言程式設計一般用它自帶的LINQ,LINQ我猜想它是從資料庫SQL語言的角度出發的。所以命名有些不一樣。

  • Map,對應C#的Select
  • Filter,對應C#的Where
  • Fold,對應C#的Aggregate

個人來講還是比較喜歡Map,Filter,Fold原來的這些名字,用過Lisp,Scala,Haskell的人一看就明白這些是什麼意思了。但是既然C#自帶提供了,那我們就直接使用吧

Select(Map)

先看一個例子,既然C#取名Select,那我們先舉一個類似資料庫的例子把。

struct People 
{
    public string Name { get; set; }
    public int Age { get; set; }
}
static void test1() 
{
    People[] PeopleList = 
    {                   
        new People { Name="A", Age = 1}, 
        new People { Name="B", Age = 2}, 
        new People { Name="C", Age = 3},       
    }
; PeopleList.Select(it => it.Name).ToList().ForEach(it => { Console.WriteLine(string.Format("NAME:{0}", it)); }); PeopleList.Select(it => it.Age).ToList().ForEach(it => { Console.WriteLine(string.Format("AGE:{0}", it)); }); } =====執行結果===== NAME:A NAME
:B NAME:C AGE:1 AGE:2 AGE:3 ==================

以上例子可以看出Select函式把People裡面屬性提取出來,但是Select的功能,遠大於此。其他很多語言其實叫Map,其實我更喜歡Map這個名字。Map更能貼切的形容這個功能,我們應該 把它理解為數學概念上的集合上對映。

對映

請看下面的例子

static void test2() 
{
    int[] ilist = { 1, 2, 3, 4, 5 };
    ilist.Select(it => it * 2).Select(it => it.ToString()).ToList().ForEach(it => 
    {
        Console.WriteLine(it);
    });
}
=====執行結果=====
2
4
6
8
10
==================

這個例子可以看出,原來的list的元素全部對映為原來的兩倍。其實我們可以用Select做更復雜的對映。我們先定義一個新的型別Student。

struct Student
{
    public string Name { set; get; }
    public int Age { set; get; }
}
static void test3() 
{
    People[] PeopleList = 
    {                   
        new People { Name="A", Age = 1}, 
        new People { Name="B", Age = 2}, 
        new People { Name="C", Age = 3},       
    };
    PeopleList.Select(it => new Student
    {
        Name = "Student" + it.Name,
        Age = it.Age,
    }).ToList().ForEach(it => 
    {
        Console.WriteLine(string.Format("NAME:{0} AGE:{1}",it.Name,it.Age));
    });
}
=====執行結果=====
NAME:StudentA AGE:1
NAME:StudentB AGE:2
NAME:StudentC AGE:3
==================

如上面的例子我們把原來為People的資料型別對映為Student了。

Where(Filter)

Where像是集合的減法,過濾掉不符合條件的資料。
假設我們接到一個需求,把大於等於5歲小於15歲的人抓起來送去學校當學生。可以像以下這麼寫。

static void test4() 
{
    People[] PeopleList = 
    {                   
        new People { Name="A", Age = 1}, 
        new People { Name="B", Age = 2}, 
        new People { Name="C", Age = 5},       
        new People { Name="D", Age = 6},  
        new People { Name="E", Age = 7},  
        new People { Name="F", Age = 10},
        new People { Name="G", Age = 20},
        new People { Name="H", Age = 21},
    };
    PeopleList.Where(it => it.Age >= 5 && it.Age < 15).Select(it => new Student
    {
        Name = it.Name,
        Age = it.Age
    }).ToList().ForEach(it => 
    {
        Console.WriteLine(string.Format("NAME:{0} AGE:{1}", it.Name, it.Age));
    });
}
=====執行結果=====
NAME:C AGE:5
NAME:D AGE:6
NAME:E AGE:7
NAME:F AGE:10
==================

Fold

下面開始介紹Fold,C#裡面有一個函式Aggregate,和此功能類似。但是我實在是受不了這個名字,我自己寫了一個,如下。按照C#的擴充套件方法來寫的,這樣的話我就可以直接在原來是資料型別中使用了。

public static R FoldL<T, R>(this IEnumerable<T> list, Func<R, T, R> accmulator, R startValue)
{
    R v = startValue;
    foreach (T item in list)
    {
        v = accmulator(v, item);
    }
    return v;
}  

FoldL是左摺疊的意思,把一串資料,從左邊開始累加在一起,至於用什麼方式累加那就看accmulator函數了。startValue是初始值。有左摺疊,當然就有右摺疊,但是右摺疊我一般不會用到。請看下面的例子。

static void test5()
{
    int[] ilist = { 1,2,3,4,5,6,7,8,9,10};
    Console.WriteLine(ilist.FoldL((acc, it) => acc + it, 0));
    Console.WriteLine(ilist.FoldL((acc, it) => acc + it + ",", "").TrimEnd(','));
}  
=====執行結果=====
55
1,2,3,4,5,6,7,8,9,10
==================

這個例子用了兩種累加的方法,第一種是初始值是0,然後從左邊直接相加。
第二種是初始值是""空字串,從左邊開始,先把資料轉化為字串,在累加之前的字串且在後面加上逗號。最終的結果會多出一個逗號,所以我在最後加了TrimEnd(',')去掉最後的逗號,讓資料更好看點。

static void test6()
{
    Student[] StudentList = 
    {
        new Student { Name="A",Age=10},
        new Student { Name="B",Age=11},
        new Student { Name="C",Age=10},
        new Student { Name="D",Age=13},
    };
    Console.WriteLine(StudentList.FoldL((acc, it) => acc + it.Age, 0) / StudentList.Length);
}  
=====執行結果=====
11
==================

這個例子可以求出所有學生的平均年齡,雖然C#有自帶的SUM函式,但是我在這裡還是用自己的FoldL函式來實現。

Flatten

Flatten函式也是很常用的,目的是把IEnumerable<IEnumerable<T>>兩層的資料變成一層IEnumerable<T>資料。這個對巢狀結構型別的資料處理非常有用。在C#我好像沒有找到類似的,所以我自己寫了一個。Flatten英文意思是變平,我們能形象地理解這個函式的意思是把兩層IEnumerable變成一層IEnumerable,當然它可以把三層變成兩層。Flatten的意思是把資料變平一點點,它的引數至少是接受兩層的資料。

public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> list)
{
    foreach (IEnumerable<T> item in list)
    {
        foreach (T it in item)
        {
            yield return it;
        }
    }
}  

如下面的例子,我們把Student這個型別新增一個課程的屬性,一個學生可能選擇多門課程。假設我的接到的需求是把學生選的課程和學生的名字打出一張表出來。請看下面的例子。

struct Student
{
    public string Name { set; get; }
    public int Age { set; get; }
    //課程
    public List<string> Courses { get; set; }
}  
static void test7()
{
    Student[] StudentList =
    {
        new Student { Name="A",Age=10,Courses=new List<string>{ "數學","英語"}},
        new Student { Name="B",Age=11,Courses=new List<string>{ "語文"}},
        new Student { Name="C",Age=10,Courses=new List<string>{ "數學","生物"}},
        new Student { Name="D",Age=13,Courses=new List<string>{ "物理","化學"}},
    };
    StudentList.Select(it => it.Courses.Select(course => new
    {
        Name=it.Name,
        Course=course
    })).Flatten().ToList().ForEach(it=> 
    {
        Console.WriteLine(string.Format("Name:{0} Course:{1}",it.Name,it.Course));
    });
}  
=====執行結果=====
Name:A Course:數學
Name:A Course:英語
Name:B Course:語文
Name:C Course:數學
Name:C Course:生物
Name:D Course:物理
Name:D Course:化學
==================

這個例子中,我們還用到了C#的匿名型別。

FlatMap

這個函式其實可以理解為把資料先做Flatten再做一次Map。例子就不寫了,程式碼貼這裡

public static IEnumerable<R> FlatMap<T, R>(this IEnumerable<IEnumerable<T>> list, Func<T, R> convert)
{
    foreach (IEnumerable<T> item in list)
    {
        foreach (T it in item)
        {
            yield return convert(it);
        }
    }
}  

ForEach

C#只提供了List型別的ForEach函式,但是沒有提供IEnumerable型別的ForEach,我們自己寫一個。程式碼如下

public static void ForEach<T>(this IEnumerable<T> list, Action<T> action)
{
    foreach (T item in list)
    {
        action(item);
    }
}  

GroupBy

最後介紹下C#的這個GroupBy函式,非常有用。如下面例子。

struct Student
{
    public string Name { set; get; }
    public int Age { set; get; }
    //課程
    public List<string> Courses { get; set; }
    public int Sex { get; set; }
    public int Class { get; set; }
}  

我們在Student型別裡面新增多兩個屬性。性別Sex屬性(男0,女1),班級屬性Class。
需求1:把女生全部找出來
需求2:把一班的所有女生找出來

static void test8()
{
    Student[] StudentList =
    {
        new Student { Name="A",Age=10,Sex=1,Class=1,Courses=new List<string>{ "數學","英語"}},
        new Student { Name="B",Age=11,Sex=0,Class=2,Courses=new List<string>{ "語文"}},
        new Student { Name="C",Age=10,Sex=1,Class=1,Courses=new List<string>{ "數學","生物"}},
        new Student { Name="D",Age=13,Sex=1,Class=2,Courses=new List<string>{ "物理","化學"}},
    };
    StudentList.GroupBy(it => it.Sex).Where(it=>it.Key==1).ForEach(it => 
    {
        it.ForEach(student => 
        {
            Console.WriteLine("NAME:" + student.Name);
        });
    });
    StudentList.GroupBy(it => new { SEX = it.Sex, CLASS = it.Class })
        .Where(it => it.Key.SEX == 1 && it.Key.CLASS == 1).Flatten().ForEach(it=> 
        {
            Console.WriteLine("一班女生名字:"+it.Name);
        });
}  
=====執行結果=====
NAME:A
NAME:C
NAME:D
一班女生名字:A
一班女生名字:C
==================

上面的這個例子我直接把ForEach用上了,而且在需求2中我把Flatten用上了,這樣我就不需要用兩次ForEach了。

其他

我們用了這些高階函式之後,基本上一個For迴圈都不用再寫了。需要注意的是C#的這些函式都是惰性呼叫的,它們是用IEnumerable的特性來實現惰性呼叫的。這些高階函式求出來的值,在需要的時候才會真正的執行迴圈體的呼叫。有很多細節需要理解,需要注意,後續我會詳細的舉例子來說明這些細節。

************轉摘:https://www.jianshu.com/p/8a8f3743969b