簡單聊聊各種語言的函數(shù)擴展

背景

最近有同事反應峰档,我們運營后臺下載的 CSV 文件出現(xiàn)錯亂的情況。問題的原因是原始數(shù)據(jù)中有 CSV 中非法的字符早像,比如說姓名字段,因為是用戶填寫的肖爵,內容有可能包含了 ,卢鹦、" 等字符,會導致 CSV 文件內容錯亂劝堪。

于是我就想用一個簡單的方式來解決這個問題冀自。一個簡單粗暴的解決方案就是導出時對字符串進行處理,將一些特殊字符替換掉秒啦,或者前后用"包起來熬粗。但是這樣的話,需要所有下載 CSV 的地方都要改寫余境,會比較麻煩驻呐。如果我們可以簡單的給 String 增加一個方法(如 String.csv())直接就把字符串處理成 CSV 兼容的格式,就會方便很多葛超。我們的運營后臺是使用 Scala 語言開發(fā)的暴氏,所幸的是,Scala 里提供了一個非常強大的功能绣张,可以滿足我們的需求答渔,那就是隱式轉換。

Scala 的隱式轉換

在 Scala 里可以通過 implicit 隱式轉換來實現(xiàn)函數(shù)擴展侥涵。

編譯器在碰到類型不匹配或是調用一個不存在的方法的時候沼撕,會去搜索符合條件的隱式類型轉換,如果找不到合適的隱式轉換方法則會報錯芜飘。

下面是處理 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 中有沒有隱式轉換的方法嗦明,發(fā)現(xiàn)在其父類 CsvHelper 中有方法把 String 轉換成 CsvString笼沥,而 CsvString 中實現(xiàn)了 csv 方法。所以編譯器最終會調用到 CsvString.csv 這個方法。

隱式轉換是一個很強大奔浅,但是也很容易誤用的功能馆纳。Scala 里隱式轉換有一些基本規(guī)則:

  • 優(yōu)先規(guī)則:如果存在兩個或者多個符合條件的隱式轉換,如果編譯器不能選擇一條最優(yōu)的隱式轉換汹桦,則提示錯誤鲁驶。具體的規(guī)則是:當前類中的隱式轉換優(yōu)先級大于父類中的隱式轉換;多個隱式轉換返回的類型有父子關系的時候舞骆,子類優(yōu)先級大于父類钥弯。
  • 隱式轉換只會隱式的調用一次,編譯器不會調用多個隱式方法督禽,不會產生調用鏈脆霎。
  • 如果當期代碼已經是合法的,不需要隱式轉換則不會使用隱式轉換狈惫。

Java 的動態(tài)擴展

我們再來看看我們熟悉的 Java 語言绪穆。Java 是一門靜態(tài)語言,本身沒有直接提供動態(tài)擴展的方法虱岂,但是我們可以通過 AOP 動態(tài)代理的方式來修改一個方法玖院,從而間接的實現(xiàn)方法的動態(tài)擴展。

下面就是一個我們就用 AspectJ 來實現(xiàn)一個動態(tài)擴展第岖,用于分頁查詢后獲取數(shù)據(jù)的總條數(shù)难菌。

@Aspect
@Component
public class PaginationAspect {
    @AfterReturning(
        pointcut = "execution(* com.xingren..*.*ByPage(..))",
        returning = "result"
    )
    public void afterByPage(JoinPoint joinPoint, Object result) {
        //根據(jù)result獲取sql信息,再查詢總條數(shù)封裝到result中蔑滓。
    }
}

其中 AfterReturning 注解表明在被注解方法返回后的一些后續(xù)動作郊酒。pointcut 定義切點的表達式,可以用通配符 * 表示键袱;returning 指定返回的參數(shù)名燎窘。然后就可以對返回的結果進行處理。這樣就可以達到動態(tài)的修改原始函數(shù)功能蹄咖。

當然除了 AspectJ 也可以使用 CGLib 來代理來實現(xiàn)簡單的 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;
}

創(chuàng)建一個對象 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;
    }
}

創(chuàng)建一個代理類實現(xiàn) MethodInterceptor 接口蚜迅,手動調用 invoke 方法,用來動態(tài)的修改被代理的實現(xiàn)方法俊抵∷唬可以在執(zhí)行之前做一些參數(shù)校驗,或者一些參數(shù)的預處理徽诲。也可以獲取修改執(zhí)行的結果刹帕,或者干脆不調用 invoke 方法吵血,自定義實現(xiàn)。也可以在調用后做一些后續(xù)動作偷溺。

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`來創(chuàng)建被代理類的代理實例
            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 動態(tài)代理践瓷,同樣的邏輯也可以達到動態(tài)的修改原始方法的目的,從而間接的實現(xiàn)函數(shù)擴展亡蓉。不過 Proxy 動態(tài)代理是基于接口的代理。

其它語言的函數(shù)擴展

其實除了 Scala 的隱式轉換和 Java 的動態(tài)代理喷舀,其他很多語言也能支持各種不同的函數(shù)擴展砍濒。

Swift

在 Swift 中可以通過關鍵詞 extension 對已有的類進行擴展,可以擴展方法硫麻、屬性爸邢、下標、構造器等等拿愧。

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

比如說我給 Int 增加一個 times 方法杠河。即執(zhí)行任務的次數(shù)。就可以如下使用:

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

上面的代碼會執(zhí)行 2 次打印方法浇辜。

Go

在 Go 中可以通過在方法名前面加上一個變量券敌,這個附加的參數(shù)會將該函數(shù)附加到這種類型上。即給一個方法加上接收器柳洋。

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

"aaaaa".toUpper //輸出 AAAAA

Kotlin

Kotlin 的函數(shù)擴展非常簡單待诅,就是定義的時候,函數(shù)名寫成 接收器 + . + 方法名 就行了熊镣。

class C {

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

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

注意當給一個類擴展已有的方法的時候卑雁,默認使用的是類自帶的成員函數(shù)。如下:

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

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

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

可以通過函數(shù)重載的方式區(qū)分成員函數(shù)(fun C.foo(i:Int) { println("extension") })绪囱,在調用的地方顯示的區(qū)分测蹲。

JavaScript

在 JavaScript 中也可以很方便的給一個對象擴展函數(shù)。寫法就是 對象 + . + 函數(shù)名鬼吵。

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"

總結

其實了解不同語言對于函數(shù)擴展的實現(xiàn)挺有意思的扣甲,本文只是粗略的介紹了一下。合理的使用這些語言的擴展齿椅,可以幫助我們提高代碼質量和工作效率文捶。我們還可以通過函數(shù)擴展來對第三方類庫進行修改或者擴展,從而更靈活的調用第三方類庫媒咳。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末粹排,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子涩澡,更是在濱河造成了極大的恐慌顽耳,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異射富,居然都是意外死亡膝迎,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門胰耗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來限次,“玉大人,你說我怎么就攤上這事柴灯÷袈” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵赠群,是天一觀的道長羊始。 經常有香客問我,道長查描,這世上最難降的妖魔是什么突委? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮冬三,結果婚禮上匀油,老公的妹妹穿的比我還像新娘。我一直安慰自己勾笆,他們只是感情好钧唐,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著匠襟,像睡著了一般钝侠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酸舍,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天帅韧,我揣著相機與錄音,去河邊找鬼啃勉。 笑死忽舟,一個胖子當著我的面吹牛,可吹牛的內容都是我干的淮阐。 我是一名探鬼主播叮阅,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼泣特!你這毒婦竟也來了浩姥?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤状您,失蹤者是張志新(化名)和其女友劉穎勒叠,沒想到半個月后兜挨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡眯分,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年拌汇,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弊决。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡噪舀,死狀恐怖,靈堂內的尸體忽然破棺而出飘诗,到底是詐尸還是另有隱情与倡,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布疚察,位于F島的核電站,受9級特大地震影響仇奶,放射性物質發(fā)生泄漏貌嫡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一该溯、第九天 我趴在偏房一處隱蔽的房頂上張望岛抄。 院中可真熱鬧,春花似錦狈茉、人聲如沸夫椭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹭秋。三九已至,卻和暖如春堤撵,著一層夾襖步出監(jiān)牢的瞬間仁讨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工实昨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留洞豁,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓荒给,卻偏偏與公主長得像丈挟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子志电,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內容