那是2006年12月份的一個非常寒冷的夜晚,我寫下了《公式翻譯》這篇文章,並貼在自己在csdn.net的技術博客上,在當時,那文章對我而言是非常有成就感的,曾經以前自己遇到的一個難題,就這樣被自己用一些“無聊”的時間給攻克了。
光陰飛逝,N年過去,我沒想到當初寫的這么篇文章居然要在最近的一個項目里派上用場了,這種感覺就像達文西對賣豬肉阿柒說:“國家有任務要交給你了!”於是我花時間整理了一下。功能嘛,和以前差不多,就是根據一個公式字符串和提供的參數,計算出其數值。而這次不是用C++,而是更簡單的C#,跟以前用C++寫的相比,占用內存空間會多一些,但運算速度應該會有些提升,因為這次我把翻譯和計算分開了,不用每次計算的時候都得先翻譯,另外還可以使用不定參數的函數,提供了較為友好的出錯提示,代碼嘛,也非常OOP。原理圖如下:
從圖中可以看出,一條表達式(#n表示第n個參數,n從0開始),可以拆分成一棵樹,樹的葉子都是能夠直接提供值的標量或變量,而樹的其它節點,則表示一種運算(四則運算或函數)。計算的過程是非常典型的后序遍歷過程,先准備參數,后執行運算,圖中紅色的數字標明了它們計算的次序。執行效果:
誰能用計算器驗算下?
當你拿到一條公式之后,如何讓程序一步步去執行翻譯呢?規則很簡單:先拆加減,再拆乘除,(跳過括號)最后拆括號(函數也算作括號),拆到最后應該只剩下變量和標量,就可以計算了。
如上圖的例子,“#0*pow(2,sin(#1)+1)/(30+6/#2)”,先拆加減,有沒有加減?——沒有,那么乘除呢?有啊,於是拆成了“#0”、“pow(2,sin(#1)+1)”和“(30+6/#2)”三項,“#0”是第一項,標記為“first”,第二項“pow(2,sin(#1)+1)”和前一項的關系是“multiply”,第三項和前一項關系是“divide”。OK,項拆分好之后再次把各個項當作是單獨的表達式,執行前面提到的那個規則,如“#0”這項,先拆加減,沒有,再拆乘除,也沒有,括號和函數?沒有too,最后發現它是個變量,標記它的index為0,結束,別的項也是一樣的動作,依此類推。
C#和C++相比,多了一個非常非常逆天的功能——反射!這也是我認為的C#中最cool的功能,有了這個功能,我就能在不改變現有公式翻譯代碼的基礎上,輕易地添加新的函數,因為我可以根據函數名稱字符串,找到要調用的函數,生成其delegate,這次的公式翻譯Ⅱ就是這么干的。
另外,這次還加入了比較友好的出錯提示,告訴用戶哪里錯了,是括號缺失還是表達式本身就不對。