Lambda in Java VS in C#



核心+變化



“凡是錢能解決的問題,就不是大問題。有很多問題是錢無法解決的,比如生老病死,比如不再相愛。”,看過《蝸居》的朋友一眼就能認出來。雖然這部電視劇講的是chugui,但是毫無違和感,我當時都看出來真感情了。

海藻和宋思明雖然是因借錢開始的,但是后面的發展卻遠遠超出了它。這里面錢是問題的核心,后面發生的事情都是圍繞着核心的變化。

社會是一張龐大而復雜的網,有節點和連線組成。節點就是人,連線就是人際關系。這里面人是核心,人際關系是圍繞着核心的變化。

那到底是核心影響變化呢,還是變化影響核心呢,還是二者皆而有之呢?不管怎樣,請記住都是:核心+變化。

如果把核心看作是數據,那變化就是行為。如果把核心看作是字段,那變化就是方法。

哎呀,終於回到了編程上,差點沒繞回來。


那些年,我的專業課老師


“好了,我們這本書已經講完了”。老師,明明后面還有一半呢,怎么就講完了呢?“后面那是指針,給你們講了你們也不懂”。

都看看,這可是我計算機生涯的第一門語言,C語言呀,碰上這樣的老師,“不僅侮辱了我們的人格,還侮辱了我們的智商”。所以我今天取得的“成就”都是我自己努力得來的。

哈哈,開個玩笑,不管怎樣,還是要感謝老師帶我“上道”的。其它老師的“名言”,后續再分享。

有句話怎么講,“明知山有虎、偏向虎山行”,就是不信這個邪了,我倒要看看指針有多難。

事實證明,很多事情因人而異。好多人都說指針很難,但是我從一開始學習指針,直到現在,從來沒覺得它難。

指針只不過是在變量的基礎上又往前走了一步。變量對應的是數據本身,指針對應的是數據的地址。所以從指針獲取數據需要執行一步解引用,即星號(*)操作符。

不過變量類型很多,所以指針的類型也很多,還有指向指針的指針,因此指針在寫法上比較繁瑣,不容易記住,但並不難理解。


C語言中的函數指針


人們對帶“美”字的東西都比較感興趣。如美景、美食、美酒、美元、美女等。當然也有討厭的,如美國。

來個美食吧。魚類絕對算一個,特別是深海魚。無污染,低脂肪,高蛋白,維生素,不飽和脂肪酸,而且肉質嫩滑,味道鮮美。關鍵還符合我國的傳統文化,年年有魚啊。

島國喜歡做成生魚片或壽司。我國的花樣就多了,紅燒魚,清蒸魚,水煮魚,麻辣魚,剁椒魚,酸菜魚,蕃茄魚,烤魚等等。

那么問題來了,如何用C語言實現這么多的做魚呢?

首先從生活入手,廚房 + 廚師 + 生魚 = 熟魚。假設每個廚師只會做一種魚。用偽代碼表示:


CookedFish kitchen(UncookedFish, CookWay) {
    if (CookWay == "生魚片") {
        //廚師A做生魚片
    } else if (CookWay == "紅燒魚") {
        //廚師B做紅燒魚
    } else if (CookWay == "酸菜魚") {
        //廚師C做酸菜魚
    }...
}


這個方法看起來很臃腫,因為它把所有的做魚方法都放進來了。

就像所有的廚師都在廚房里候着,然后進來一條生魚,並告知要做成什么樣的,對應的廚師起身去做魚,剩余的廚師仍繼續候着。

現實中是廚師都在自己的崗位上,而非廚房里,需要做魚的時候,廚師和生魚進廚房即可。

就像這樣,廚房(廚師,生魚)= 熟魚,用偽代碼表示:


CookedFish kitchen(Cooker*, UncookedFish) {
    //廚師做魚
    Cooker(UncookedFish);
}


這里已經不需要CookWay了,因為一個廚師只會做一種魚,從廚師就可以知道魚的做法了。

這里面的核心是魚,圍繞核心的變化是廚師。如果魚是字段,那廚師就是方法了。

第一種方法之所以繁瑣,是因為我們只把字段傳進來了,方法全部在“廚房”里。其實即使廚房里有再多的方法,一次也只能用一個。

為了簡潔和更加符合實際,我們除了把字段傳進來之外,也把方法傳進來了。即,既把魚傳進來,也把廚師傳經來。這樣廚房里只有一個廚師在工作,就清爽多了。

我們可以看到,除了數據(魚)可以當作參數傳遞外,行為(廚師)也可以當作參數傳遞。

C語言不是OO的,沒有對象的概念,也沒有方法的概念,只有函數,一段可執行的代碼就是函數。

為了傳遞函數,需要用到函數指針,Cooker*就是函數指針,它指向的就是一個代碼片段。

函數指針代表的是函數簽名,即某一類函數。

如void (*fp)();這里的fp就是函數指針,它表示所有沒有入參也沒有返回值的這類函數。

如下:


//函數
void foo(){...};
void bar(){...};
//把函數賦給函數指針
fp = &foo;
fp = &bar;
//通過指針調用函數
(*fp)();


再看一個例子:


//函數指針
int (*fp2)(intint);
//加
int add(int a, int b{
    return a + b;
}
//減
int sub(int a, int b{
    return a + b;
}
//乘
int mul(int a, int b{
    return a + b;
}
//除
int div(int a, int b{
    return a + b;
}
//函數
int op2(int (*fp2)(intint), int a, int b) {
    return fp2(a, b);
}
//調用,把函數當作參數傳入
op2(add12) == 3;
op2(sub, 21) == 1;
op2(mul, 12) == 2;
op2(div, 42) == 2;


看不懂函數指針沒關系,后面還有C#和Java代碼。


C#中的Lambda表達式


初次接觸lambda表達式就是在C#中,已是很多年前的事了。C#確實從C和C++中繼承了很多特性。

在C#中應該也支持指針,但是不推薦使用。為了完成C語言中函數指針這種功能,C#提供了類型安全的“函數指針”,就是委托。

委托的關鍵字是delegate,它的用法如下:


public delegate void FB(int a);


委托表示的是方法簽名,即一類方法。此處定義的委托類型是FB,它表示所有入參為一個整型且沒有返回值的方法。

所以可以把方法賦值給委托,自然可以調用委托,如下:


//一個整型入參,無返回值
public void Foo(int a)
{
    Console.WriteLine("Foo: " + a);
}

//一個整型入參,無返回值
public void Bar(int a)
{
    Console.WriteLine("Bar: " + a);
}

//委托的賦值與調用
public void TestDelegate()
{
    //把方法賦給委托
    FB fb = this.Foo;
    //調用委托
    fb(1);
    //把方法賦給委托
    fb = this.Bar;
    //調用委托
    fb(2);
}


委托還可以用作方法的參數,此時就可以把一個方法當作參數傳給其它方法。


//第一個參數FB就是委托
public void TestDelegate(FB fb, int a)
{
    fb(a);
}

//下面是對這個方法的調用
Program p = new Program();

//p.Foo是一個方法,可以作為參數傳入
p.TestDelegate(p.Foo, 1);

//p.Bar是一個方法,可以作為參數傳入
p.TestDelegate(p.Bar, 2);


lambda表達式本質上就是一段可執行的代碼,但是不同語言對它的實現是不同的,在C#中就實現為委托。


public void TestLambda()
{
    //這就是lambda表達式的寫法,它被賦值給了委托
    FB fb = (int a) => Console.WriteLine("lambda 1: " + a);
    fb(1);
    //lambda表達式
    fb = (a) => 
    {
      Console.WriteLine("lambda 2: " + a); 
    };
    fb(2);
}


由於編譯器會進行類型推斷,所以可以省略參數類型。如果只有一個入參的話,可以省略那個小括號。如果只有一個語句的話,可以不用要大括號。

lambda表達式作為參數傳遞:


public void TestLambda(FB fb, int a)
{
    fb(a);
}

//lambda表達式直接作為參數
p.TestLambda((int a) => Console.WriteLine("lambda 1: " + a), 1);

//lambda表達式直接作為參數
p.TestLambda((a) => { Console.WriteLine("lambda 2: " + a); }, 2);


C#中的匿名方法,如下:


public void TestAnonymousMethod()
{
    //匿名方法可以賦值給委托,用關鍵字delegate替代方法名
    FB fb = delegate(int a)
    {
      Console.WriteLine("anonymous method: " + a);
    };

    fb(1);
    fb(2);
}


匿名方法作為參數傳遞:


public void TestAnonymousMethod(FB fb, int a)
{
    fb(a);
}

//匿名方法直接作為方法參數
p.TestAnonymousMethod(delegate(int a) { Console.WriteLine("anonymous method: " + a); }, 1);

//匿名方法直接作為方法參數
p.TestAnonymousMethod(delegate(int a) { Console.WriteLine("anonymous method: " + a); }, 2);


總之,C#中的普通方法,lambda表達式,匿名方法,最后都可以賦值給委托進行傳遞和調用。

看不懂C#沒關系,后面還有Java代碼。



Java中的Lambda表達式


自從Java換了爸爸后,簡直像坐上了火箭。各種其它語言的特性都陸續加進來了。從Java 8開始也可以使用lambda表達式了。

不過說來慚愧,我用的不多,因為它在我的編程生涯中已經不再“稀奇”了,因為之前已經在C#中體驗過了。

Java也是從C和C++發展過來的,繼承了一些特性,但拋棄的更多。類似函數指針的功能,就被優化沒了。

因此在Java語言中,對於行為(可執行代碼片段)的傳遞,無法做到方法級別,只能再往上走一步,做到接口級別或類級別。

也就是說,你想傳遞一個方法時,必須要傳遞一個類或對象作為方法的載體才行。這種使用方式其實一直都存在着的。

Java 8中引入一個新的概念叫做函數式接口。它規定這樣的接口只能包含一個抽象方法,但可以包含其它帶默認實現的任意方法。

函數式接口也可以像普通接口那樣使用。只不過會有一些特定功能,畢竟是為了配合Java 8支持函數式編程而特意起的這個名字。

有一個函數式接口叫做Consumer<T>,它只有一個抽象方法是void accept(T t);這個方法接收一個入參但沒有返回值。

所以這個函數式接口就代表了這樣一類方法,即有一個入參但沒有返回值的所有方法。所以函數式接口大致上也可以看作是一類方法的“方法簽名”。

定義一個函數式接口變量,表示只有一個入參但沒有返回值的這類方法。


//函數式接口變量
public Consumer<Integer> fb;


可以把方法賦值給函數式接口,然后進行調用,如下:


//只有一個入參且沒有返回值的方法
public void foo(int a) {
    System.out.println("foo: " + a);
}

//只有一個入參且沒有返回值的方法
public void bar(int a) {
    System.out.println("bar: " + a);
}

//函數式接口的賦值與調用
public void testFunctionalInterface() {
    //把方法賦值給函數式接口變量
    fb = this::foo;
    //調用函數式接口,就是調用賦給它的方法
    fb.accept(1);
    //把方法賦給函數式接口變量
    fb = this::bar;
    //調用
    fb.accept(2);
}


注意引用方法時使用了::操作符,這個應該是C++中的寫法吧。

函數式接口作為方法參數:


public void testFunctionalInterface(Consumer<Integer> fb, int a) {
    fb.accept(a);
}

Program p = new Program();

//把方法作為參數傳給其它方法
p.testFunctionalInterface(p::foo, 1);

//把方法作為參數傳給其它方法
p.testFunctionalInterface(p::bar, 2);


毫無懸念,lambda表達式可以賦給函數式接口變量。


public void testLambda() {
    //lambda表達式的寫法
    fb = (Integer a) -> System.out.println("lambda 1: " + a);
    fb.accept(1);
    //lambda表達式
    fb = (a) -> {
        System.out.println("lambda 2: " + a); 
    };
    fb.accept(2);
}


很顯然,沒有太多的驚喜。

lambda表達式作為參數傳遞:


public void testLambda(Consumer<Integer> fb, int a{
    fb.accept(a);
}
//lambda表達式直接作為參數傳遞
p.testLambda((Integer a) -> System.out.println("lambda 1: " + a), 1);

//lambda表達式直接作為參數傳遞
p.testLambda((a) -> { System.out.println("lambda 2: " + a); }, 2);


Java中的匿名類:


public void testAnonymousClass() {
    //匿名類,函數式接口作為普通接口使用
    fb = new Consumer<Integer>() {
            @Override
            public void accept(Integer a
{
                System.out.println("anonymous class: " + a);
            }
        };

    fb.accept(1);
    fb.accept(2);
}


匿名類作為參數傳遞:


public void testAnonymousClass(Consumer<Integer> fb, int a{
    fb.accept(a);
}

//匿名類直接做參數
p.testAnonymousClass(new Consumer<Integer>() {
        @Override
        public void accept(Integer a
{
            System.out.println("anonymous class: " + a);
        }
    }, 1);

//匿名類直接做參數
p.testAnonymousClass(new Consumer<Integer>() {
        @Override
        public void accept(Integer a
{
            System.out.println("anonymous class: " + a);
        }
    }, 2);


總之,在Java中無論是普通方法,還是lambda表達式,或是匿名類,其實它們最后都變成了一個類,且都實現了這個函數式接口。

只不過匿名類是我們自己定義的。lambda表達式最后對應的類是編譯器造出來的,所以它是“人造”的,但不是匿名的。

看不懂Java沒關系,只要明白了原理就算是知道了精髓。



in C# VS in Java


為了“模擬”函數指針的功能,在C#中使用委托,在Java中使用函數式接口。

函數式接口其實就是一個接口,它看起來具有表示“方法簽名”的功能,但是很丑陋。

委托看起來更像“方法簽名”,也很優雅,但不要被迷惑,其實它也是一個類,沒有什么高級東西。

對於引用方法的寫法不同,C#中使用this.Foo,this.Bar,Java中使用this::foo,this::bar。

lambda表達式的寫法略微不同,C#中是() => {},Java中是() -> {}。

關於匿名,C#中雖然叫匿名方法,其實最后還是一個類,而且是委托類型的。直接用delegate關鍵字定義匿名方法。

Java中就叫匿名類,名副其實,最后就是一個類。只不過需要先定義一個接口,然后直接使用接口實現匿名類。

我們發現C#和Java對lambda表達式的支持其實差不多。不同的是C#更優雅一些,Java更丑陋一些。



思想提升



一定要明白不僅普通數據可以當作參數傳遞,代碼片段(就是邏輯)也可以當作參數傳遞。

這個思想就是核心,至於寫法和用法就是圍繞着核心的變化而已。


PS:最近幾年很少看到語言之爭啊,莫非大家都覺得PHP是世界上最好的語言啦😁。Oracle選擇收費和有閉源的趨勢,微軟卻選擇免費、開源和跨平台,上帝才知道這是怎么回事。


編程新說


用獨特的視角說技術


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM