钧梓昊逑

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

函数式接口 VS 委托

在C中,可以使用函数指针来存储函数的入口,从而使得函数可以像变量一样赋值、传递和存储,使得函数的调用变得十分灵活,是实现函数回调的基础。然而函数指针不存在函数的签名信息,甚至可以指向任何地址,使用上有诸多不安全因素,因此在很多现代语言中不存在函数指针这种类型。

在Java中,包装一个方法的调用,需要创建一个接口类型和相应的实现类型,在实现中调用需要包装的方法,如果需要调用的是实例方法,还需要将实例的引用传递进接口实现的实例中(后面再比较闭包)。这种实现方式的好处是不需要引入更多的语法概念,可以保持语言的精简,学习曲线平缓。缺点是代码量多,不能清晰的区分和表达“这是一个包装了方法的对象”这种概念,同时接口的定义多样,实例的使用者需要了解接口的细节才能很好地使用。

public interface Func<T, TResult> {

    TResult invoke(T arg);

}

 

public static void M(){

    Func<String, String> func = new Func<String, String>(){

        public String invoke(String arg){

            return method(arg);

        }

    };

    String result = func.invoke("world");

}

   

public static String method(String arg){

    return "hello " + arg;

}

 

在C#(1+)中,有一种特殊的引用类型叫做委托(Delegate),专门用于表达方法的引用。所有的委托类型都继承于System.Delegate,同时,拥有一组特殊的语法,使得委托的定义、实例化、调用十分简单和极具语义。委托的调用可以像调用普通方法一样,使用者不需要了解委托的内在实现。

delegate TResult Func<T,TResult>(T arg); //定义委托

 

public static void M(string arg){

    Func<string, string> func = Method;//指向静态方法

    Program p = new Program();

    func = p.InstanceMethod;//指向实例方法

    string result = func("world");//调用方法

}

 

public static string Method(string arg){

    return "hello " + arg;

}

 

public string InstanceMethod(string arg){

    return "Woo " + arg; ;

}

在.NET中,委托的内部实际上是封装了一个函数指针。

 

Java8中新加的函数式接口,采用了类似于C#的委托的语法,使得一个方法定义可以直接赋值给一个“函数式接口”,所谓函数式接口就是只包含一个方法定义的接口,可以使用@FunctionalInterface加以约束。

 

@FunctionalInterface

public interface Func<T, TResult> {

    TResult invoke(T arg);

}

 

public static void M(){

    Func<String, String> func = Programe::method;

    Programe p = new Programe();

    func = p::instanceMethod;

    String result = func.invoke("world");

}

   

public static String method(String arg){

    return "hello " + arg;

}

 

public String instanceMethod(String arg){

    return "Woo " + arg;

}

可以看到,无论是指向静态方法还是实例方法,Java8的语法都和C#十分接近,他们的内在实现也是类似的,.NET使用ldftn指令获取方法地址,而Java则使用invokedynamic。在C#,获取方法引用和调用方法的语法完全一致,而Java,静态方法必须使用类名::方法名的写法,和调用时可以只需方法名的写法有区别,同时多了一个::符号,增加了语法的复杂度,不明白这样设计的原因是什么,难道仅仅是为了区别C#。

 

更多

.NET的Delegate基类提供了委托的链表连接以实现多播,是事件机制实现的基础;提供了运行于线程池的异步调用方法,使用起来也十分方便。

 

匿名方法和闭包

当一个方法的定义仅仅是为了被引用到一个变量中,往往会使用内联方法定义,也就是匿名方法。

Java:

Func<String, String> func = new Func<String, String>(){

    public String invoke(String arg){

        return "hello " + arg;

    }

};

String result = func.invoke("world");

C#

Func<string, string> func = delegate(string arg){

    return "hello " + arg;

};

string result = func("world");

类似的语法,使得一个方法出现在了另一个方法体内,在编译之后,都生成了两个独立的方法。一般说来两个方法之间的临时变量有各自的栈范围,是不可以共享的,也就是说内部方法里不能访问外部方法的变量。然而从代码结构上,似乎又应该允许这样的访问,的确,如果内部方法如果可以访问外部方法的临时变量,将会带来很多便利,代码逻辑也更直观。Java和C#都实现了这种变量穿越的能力,就是闭包。

Java

public static void M(int arg){

    final int v = arg;

    Func<String, String> func = new Func<String, String>(){

        public String invoke(String arg){

            return "hello " + arg + v;

        }

    };

}

C#

public static void M(){

    int v=1;

    Func<string, string> func = delegate(string arg)    {

        return "hello " + arg + v;

    };

    string result = func("world");

}

 

然而他们的实现机制则有一些的区别。

Java的实现,在“初始化接口”时将内部需要的变量作为自动生成的匿名类型的构造方法的参数传递进实例内部并使用字段存储,内部方法调用外部变量实质上是调用实例的字段。这样一来,内部访问的变量和外部的变量在XXX实例化的那一刻开始,就独立变化了,表现得不像是同一个变量,因此在Java中闭包变量必须加上final修饰,以解决这一问题。

在C#中,内部方法的访问实现和Java类似,同时添加了对闭包字段访问的属性,在外部方法访问变量时,同样访问的是闭包对象的属性,也就是和内部访问的相同,从而实现了内部方法和外部方法共享一个“变量”。

两者的实现都将方法内的变量提升到了对象的成员,都将延长对象生命周期,需要注意。

 

泛型与类型推断

泛型是C#2添加的新特性,给C#带来了十分灵动的类型定义方式。例如,不再需要为各种含有不同类型的函数签名的方法定义委托,只需要根据不同参数个数以及有无返回值定义一批泛型委托就可以表示绝大部分函数了。

    delegate TResult Func<TResult>();

    delegate TResult Func<T, TResult>(T arg);

    delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);

    ……

    delegate void Action();

    delegate void Action<T>(T arg);

delegate void Action<T1, T2>(T1 arg1, T2 arg2);

……

再例如,复杂的数据结构可以用嵌套的泛型定义:如Dictionary<Tuple<int, KeyValuePair<string, long>>, List<int>>。这样的数据结构定义比自定义类型更为直观和统一。但是代码则较为冗长,在new这样的实例时,需要重复写两遍。在C#3中,添加了对匿名类型的类型推导能力,自动根据等号右边的表达式推导左边变量的类型。

var data = new Dictionary<Tuple<int, KeyValuePair<string, long>>, List<int>>();

var data = new Func<int>(delegate()    {

        return 0;

    });

在C#中,各种开泛型和闭泛型在编译后都是区别存在的,Action<>,Action<int>,Action<string>都是不同的类型,从而真正实现了强类型约束以及运行时获取类型参数的能力,同时类型参数可以是值类型,提高了泛型集合的性能。

 

Lambda表达式

Lambda表达式是C#3添加的新特性。用于简化委托的定义。

对于以下匿名方法的定义:

Func<string, string> func = delegate(string arg)    {

    return "hello " + arg;

};

去掉delegate关键字,并用符号=>连接参数与方法体:

Func<string, string> func = (string arg) =>   {

        return "hello " + arg;

    };

通过类型推断,可以省去参数的类型:

Func<string, string> func = (arg) =>   {

        return "hello " + arg;

};

对于只有一个参数的方法,可以省去参数的括号:

Func<string, string> func = arg =>   {

        return "hello " + arg;

};

对于只有一行表达式的方法,可以省去方法体括号和return关键字:

Func<string, string> func = arg => "hello " + arg;

Java8中的lambda表达式几乎和C#完全一致,只不过连接符号使用->而不是=>。

Func<String, String> func = arg -> "hello " + arg;

 

显然,lambda表达式大大化简了匿名方法的定义,方法内联到任何需要匿名方法的地方。

如,定义一个可枚举类型,提供一个筛选元素的方法Where:

class Enumerable<T> : IEnumerable<T>

{

    public IEnumerable<T> Where(Func<T, bool> predicat)

    {

        foreach (var item in this)

        {

            if (predicat(item))

            {

                yield return item;

            }

        }

    }

    ……

}

当我需要对已存在的集合进行按条件筛选,则需要提供一个筛选条件的委托类型,这里可以使用lambda表达式来定义,就显得十分方便和直观。

Enumerable<int> collection;

var result = collection.Where(item => item > 100);

 

高阶函数

Lambda表达式除了简化匿名方法的定义以外,由于其强大的表达能力,赋于了语言更多的函数式表达能力。

将参数或者返回类型为函数的函数称为高阶函数。

如斐波那契数列函数定义:

f(0) = 1;

f(1) = 1;

f(n) = f(n-1) + f(n-2);

用C#可以写成:

Func<int, int> f = null;

f = x => x <= 1 ? 1 : f(x - 1) + f(x - 2);

int result = f(5);

 

 

Streams VS linq

在前面的例子中,定了一个Where方法用于对集合元素的筛选,事实上.NET4内置提供了多种类似的集合操作方法(这些方法都是通过扩展方法(C#3新特性)添加的,可以在不修改类定义的情况下为类添加类似于实例方法的效果)。利用这些方法和lambda表达式,可以为集合进行多种操作。

var result = Enumerable.Range(0, 100)//遍历1到100

    .Where(n => n % 2 == 0)//筛选能被2整除的数

    .Where(n => n % 3 == 0)//筛选能被2整除的数

    .Select(n => n.ToString())//转换成字符串

.Reverse();//反转顺序

通过这样的链式编程,可以表达各种数据集合操作,而这些都只是操作定义,真正的数据操作在使用时才进行,达到了延时操作效果。

更进一步,C#实现了linq,一种语言级查询语言,将查询简化到了极致,达到了类似于SQL的效果。

上面的查询可以写成:

var reuslt = from n in Enumerable.Range(0, 100)

                where n % 2 == 0 && n % 3 == 0

                select n.ToString();

用一行语句表达了纯粹的查询,不存在任何方法、委托的痕迹。

Java8中添加java.util.Stream包,用于实现类似于C#链式查询的效果。

List<String> stringCollection = null;

String[] result = stringCollection.stream()

.filter(s -> s.startsWith("a"))

.map(s -> s.toString())

.toArray();

 

表达式树

在C#中,lambda表达式可以被编译为一种数据结构,称为表达式树,而这个数据结构可以在运行时被分析、处理或者编译,做到很多灵活、有趣且高效的效果,其中最为瞩目的就是将linq作为ORM的查询语言,将数据库的查询融入到语言中,接受编译时强类型检查。

 

Annotation VS Attribute

注解(annotation)是Java6添加的新特性,在Java8中,可以对同一个元素添加重复的注解。

注解十分类似于.NET(1+)中的特性(attribute)。不同的是.NET Attribute是类而不是接口,可以带有方法逻辑而不仅仅是数据,从而可以利用各种设计模式,得到更多的设计可能性。

 

 

 

posted on 2014-03-23 00:26  钧梓昊逑  阅读(10002)  评论(1编辑  收藏  举报