核心 + 变化 "凡是钱能解决的问题, 就不是大问题. 有很多问题是钱无法解决的, 比如生老病死, 比如不再相爱.", 看过《蜗居》的朋友一眼就能认出来. 虽然这部电视剧讲的是 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)(int, int);
- // 加
- 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)(int, int), int a, int b) {
- return fp2(a, b);
- }
- // 调用, 把函数当作参数传入
- op2(add, 1, 2) == 3;
- op2(sub, 2, 1) == 1;
- op2(mul, 1, 2) == 2;
- op2(div, 4, 2) == 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 选择收费和有闭源的趋势, 微软却选择免费, 开源和跨平台, 上帝才知道这是怎么回事.
编程新说
用独特的视角说技术
来源: https://www.cnblogs.com/lixinjie/p/lambda-in-java-vs-in-csharp.html