简单聊聊各种语言的函数扩展

背景

最近有同事反应,我们运营后台下载的 CSV 文件出现错乱的情况。问题的原因是原始数据中有 CSV 中非法的字符,比如说姓名字段,因为是用户填写的,内容有可能包含了 ," 等字符,会导致 CSV 文件内容错乱。

于是我就想用一个简单的方式来解决这个问题。一个简单粗暴的解决方案就是导出时对字符串进行处理,将一些特殊字符替换掉,或者前后用"包起来。但是这样的话,需要所有下载 CSV 的地方都要改写,会比较麻烦。如果我们可以简单的给 String 增加一个方法(如 String.csv())直接就把字符串处理成 CSV 兼容的格式,就会方便很多。我们的运营后台是使用 Scala 语言开发的,所幸的是,Scala 里提供了一个非常强大的功能,可以满足我们的需求,那就是隐式转换。

Scala 的隐式转换

在 Scala 里可以通过 implicit 隐式转换来实现函数扩展。

编译器在碰到类型不匹配或是调用一个不存在的方法的时候,会去搜索符合条件的隐式类型转换,如果找不到合适的隐式转换方法则会报错。

下面是处理 CSV 下载字符串的代码:

trait CsvHelper {
  implicit def stringToCsvString(s: String) = new CsvString(s)
}
class CsvString(val s: String){
  def csv = s"""${s.replaceAll(",", " ").replaceAll("\"", "'")}"""
}

class Controller extends CsvHelper {
    def dowload(){
        ...
        ",foo,".csv //foo
    }
}

Controller 中我调用 String.csv 方法,但是 String 没有 csv 方法。这时候编译器就会去找 Controller 中有没有隐式转换的方法,发现在其父类 CsvHelper 中有方法把 String 转换成 CsvString,而 CsvString 中实现了 csv 方法。所以编译器最终会调用到 CsvString.csv 这个方法。

隐式转换是一个很强大,但是也很容易误用的功能。Scala 里隐式转换有一些基本规则:

  • 优先规则:如果存在两个或者多个符合条件的隐式转换,如果编译器不能选择一条最优的隐式转换,则提示错误。具体的规则是:当前类中的隐式转换优先级大于父类中的隐式转换;多个隐式转换返回的类型有父子关系的时候,子类优先级大于父类。
  • 隐式转换只会隐式的调用一次,编译器不会调用多个隐式方法,不会产生调用链。
  • 如果当期代码已经是合法的,不需要隐式转换则不会使用隐式转换。

Java 的动态扩展

我们再来看看我们熟悉的 Java 语言。Java 是一门静态语言,本身没有直接提供动态扩展的方法,但是我们可以通过 AOP 动态代理的方式来修改一个方法,从而间接的实现方法的动态扩展。

下面就是一个我们就用 AspectJ 来实现一个动态扩展,用于分页查询后获取数据的总条数。

@Aspect
@Component
public class PaginationAspect {
    @AfterReturning(
        pointcut = "execution(* com.xingren..*.*ByPage(..))",
        returning = "result"
    )
    public void afterByPage(JoinPoint joinPoint, Object result) {
        //根据result获取sql信息,再查询总条数封装到result中。
    }
}

其中 AfterReturning 注解表明在被注解方法返回后的一些后续动作。pointcut 定义切点的表达式,可以用通配符 * 表示;returning 指定返回的参数名。然后就可以对返回的结果进行处理。这样就可以达到动态的修改原始函数功能。

当然除了 AspectJ 也可以使用 CGLib 来代理来实现简单的 AOP。

public class FooService {
    public Page findByPage(){
        return new Page();
    }
    public Page findPage(){
        return new Page();
    }
}
@Data
public class Page {
    private String sql = "";
    private List<Object> content = new ArrayList();
    private Integer size = 0;
    private Integer page = 0;
    private Integer total = 0;
}

创建一个对象 FooService 用来模拟查询分页方法。

public class CGLibProxyFactory implements MethodInterceptor {

    private Object object;

    public CGLibProxyFactory(Object object){
        this.object = object;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("before method! do something...");

        Object result = methodProxy.invoke(object, objects);
        //进行方法判断,是否需要处理
        if (method.getName().contains("ByPage")) {
            if (result instanceof Page) {
                System.out.println("after method! do something...");
                ((Page) result).setTotal(100);
            }
        }
        return result;
    }
}

创建一个代理类实现 MethodInterceptor 接口,手动调用 invoke 方法,用来动态的修改被代理的实现方法。可以在执行之前做一些参数校验,或者一些参数的预处理。也可以获取修改执行的结果,或者干脆不调用 invoke 方法,自定义实现。也可以在调用后做一些后续动作。

public class ObjectFactoryUtils {
    public static <T> Optional<T> getProxyObject(Class<T> clazz) {
        try {
            T obj = clazz.newInstance();
            CGLibProxyFactory factory = new CGLibProxyFactory(obj);
            Enhancer enhancer=new Enhancer();//利用`Enhancer`来创建被代理类的代理实例
            enhancer.setSuperclass(clazz);//设置目标class
            enhancer.setCallback(factory);//设置回调代理类
            return Optional.of((T)enhancer.create());
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return Optional.empty();
    }
}

public static void main(String[] args) {
        Optional<FooService> proxyObject = ObjectFactoryUtils.getProxyObject(FooService.class);
        if(proxyObject.isPresent()) {
            FooService foo = proxyObject.get();
            System.out.println("findByPage:");
            System.out.println(foo.findByPage().getTotal());
            System.out.println("findPage:");
            System.out.println(foo.findPage().getTotal());
        }
}

最后打印的输出是:

findByPage:
before method! do something...
after method! do something...
100
findPage:
before method! do something...
0

当然除了 CGLIB 代理也可以使用 Proxy 动态代理,同样的逻辑也可以达到动态的修改原始方法的目的,从而间接的实现函数扩展。不过 Proxy 动态代理是基于接口的代理。

其它语言的函数扩展

其实除了 Scala 的隐式转换和 Java 的动态代理,其他很多语言也能支持各种不同的函数扩展。

Swift

在 Swift 中可以通过关键词 extension 对已有的类进行扩展,可以扩展方法、属性、下标、构造器等等。

extension Int {
    func times(task: () -> Void) {
        for _ in 0..<self {
            task()
        } 
    }
}

比如说我给 Int 增加一个 times 方法。即执行任务的次数。就可以如下使用:

2.times({
    print("Hello!")
})

上面的代码会执行 2 次打印方法。

Go

在 Go 中可以通过在方法名前面加上一个变量,这个附加的参数会将该函数附加到这种类型上。即给一个方法加上接收器。

func (s string) toUpper() string {
    return strings.ToUpper(s)
}

"aaaaa".toUpper //输出 AAAAA

Kotlin

Kotlin 的函数扩展非常简单,就是定义的时候,函数名写成 接收器 + . + 方法名 就行了。

class C {

}
fun C.foo() { println("extension") }

C().foo() //输出extension

注意当给一个类扩展已有的方法的时候,默认使用的是类自带的成员函数。如下:

class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

C().foo() //输出member

可以通过函数重载的方式区分成员函数(fun C.foo(i:Int) { println("extension") }),在调用的地方显示的区分。

JavaScript

在 JavaScript 中也可以很方便的给一个对象扩展函数。写法就是 对象 + . + 函数名

var date = new Date();
date.format = function() {
    return this.toISOString().slice(0, 10);
}
date.format(); //"2017-11-29"

也可以给一个 Object 进行扩展:

Date.prototype.format = function() {
     return this.toISOString().slice(0, 10);
}
new Date().format(); //"2017-11-29"

总结

其实了解不同语言对于函数扩展的实现挺有意思的,本文只是粗略的介绍了一下。合理的使用这些语言的扩展,可以帮助我们提高代码质量和工作效率。我们还可以通过函数扩展来对第三方类库进行修改或者扩展,从而更灵活的调用第三方类库。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容