程式設計,不止有程式碼,還有藝術

華為雲開發者社群發表於2022-05-30
摘要:在程式碼的世界中,是存在很多藝術般的寫法,這可能也是部分程式設計師追求程式設計這項事業的內在動力。

本文分享自華為雲社群《【雲駐共創】用4種程式碼中的藝術試圖喚回你對程式設計的興趣》,作者: breakDawn。

也許對於部分人來說,喚起他們程式設計興趣的起點可能是一些能快速實現某功能的python小指令碼。但作為一個多年的java開發,更多是在接觸工作中的業務程式碼,CURD寫久了,總會偶爾感到一絲絲的疲勞。

回望窗外,思索著在程式碼的世界中,是存在很多藝術般的寫法,這可能也是部分程式設計師追求程式設計這項事業的內在動力。

這裡將為你呈現4種程式碼中的藝術,試圖喚回你對程式碼最初的興趣。

設計模式的藝術:用狀態模式告別if-else

面對判斷分支異常多、狀態變化異常複雜的業務邏輯程式碼,在大量if-else中遨遊往往會犯惡心,甚至懷疑起了人生。

例如“手寫一個判斷函式,確認字串是否是一個合法的科學數字表示式”這種常見的業務邏輯問題。如果用if-else寫,就會變成如下醜陋的程式碼:

程式設計,不止有程式碼,還有藝術

每次維護這種程式碼,總是都要從頭閱讀一遍,確認自己要在哪裡修改,彷彿在修補一個破舊的大棉襖。

但我們如果使用了設計模式中的狀態機模式來進行重構,整塊程式碼就會非常精妙。首先要畫出一副如下所示的狀態演變圖:

程式設計,不止有程式碼,還有藝術

(圖源來自leetcode,連結見:https://leetcode.cn/problems/valid-number/solution/you-xiao-shu-zi-by-leetcode-solution-298l/)

狀態圖繪製完成之後,就可以根據狀態變化的合理性,確認狀態是否符合要求。

程式碼如下所示:

class Solution {
        public enum CharType {
            NUMBER,
            OP,
            POINT,
            E;
 
            public static CharType toCharType(Character c) {
                if (Character.isDigit(c)) {
                    return NUMBER;
                } else if (c == '+' || c == '-') {
                    return OP;
                } else if (c == '.') {
                    return POINT;
                } else if (c =='e' || c == 'E') {
                    return E;
                } else {
                    return null;
                }
            }
        }
        public enum State {
            INIT(false),
            OP1(false),
            // 在.前面的數字
            BEFORE_POINT_NUMBER(true),
            // 前面沒數字的點
            NO_BEFORE_NUMBER_POINT(false),
            // 前面有數字的點
            BEFORE_NUMBER_POINT(true),
            // 點後面的數字
            AFTER_POINT_NUMBER(true),
            // e/E
            OPE(false),
            // E後面的符號
            OP2(false),
            // e後面的數字
            AFTER_E_NUMBER(true);
 
            // 是否可在這個狀態結束
            private boolean canEnd;
 
            State(boolean canEnd) {
                this.canEnd = canEnd;
            }
 
            public boolean isCanEnd() {
                return canEnd;
            }
        }
 
        public Map<State, Map<CharType, State>> transferMap = new HashMap<>() {{
            Map<CharType, State> map = new HashMap<>() {{
                put(CharType.OP, State.OP1);
                put(CharType.NUMBER, State.BEFORE_POINT_NUMBER);
                put(CharType.POINT, State.NO_BEFORE_NUMBER_POINT);
            }};
            put(State.INIT, map);
 
            map = new HashMap<>() {{
                put(CharType.POINT, State.NO_BEFORE_NUMBER_POINT);
                put(CharType.NUMBER, State.BEFORE_POINT_NUMBER);
            }};
            put(State.OP1, map);
 
            map = new HashMap<>() {{
                put(CharType.POINT, State.BEFORE_NUMBER_POINT);
                put(CharType.NUMBER, State.BEFORE_POINT_NUMBER);
                put(CharType.E, State.OPE);
            }};
            put(State.BEFORE_POINT_NUMBER, map);
 
            map = new HashMap<>() {{
                put(CharType.NUMBER, State.AFTER_POINT_NUMBER);
            }};
            put(State.NO_BEFORE_NUMBER_POINT, map);
 
            map = new HashMap<>() {{
                put(CharType.NUMBER, State.AFTER_POINT_NUMBER);
                put(CharType.E, State.OPE);
            }};
            put(State.BEFORE_NUMBER_POINT, map);
 
            map = new HashMap<>() {{
                put(CharType.E, State.OPE);
                put(CharType.NUMBER, State.AFTER_POINT_NUMBER);
            }};
            put(State.AFTER_POINT_NUMBER, map);
            map = new HashMap<>() {{
                put(CharType.OP, State.OP2);
                put(CharType.NUMBER, State.AFTER_E_NUMBER);
            }};
            put(State.OPE, map);
            map = new HashMap<>() {{
                put(CharType.NUMBER, State.AFTER_E_NUMBER);
            }};
            put(State.OP2, map);
 
            map = new HashMap<>() {{
                put(CharType.NUMBER, State.AFTER_E_NUMBER);
            }};
            put(State.AFTER_E_NUMBER, map);
        }};
        public boolean isNumber(String s) {
            State state = State.INIT;
            for (char c : s.toCharArray()) {
                Map<CharType, State> transMap = transferMap.get(state);
                CharType charType = CharType.toCharType(c);
                if (charType == null) {
                    return false;
                }
                if (!transMap.containsKey(charType)) {
                    return false;
                }
                // 狀態變更
                state = transMap.get(charType);
            }
            return state.canEnd;
        }
    }

從下面的程式碼可以看到,未來只需要維護transferMap 即可,非常方便,程式碼的優秀設計模式是一門造福懶人程式設計師們的藝術,重構出一個易於維護的程式碼也是程式設計師的成就感來源之一。

併發程式設計的藝術:詭異的Java程式碼揭示了cpu快取的原理

著名的Java併發程式設計大師Doug lea在JDK 7的併發包裡新增一個佇列集合類Linked-TransferQueue,它在使用volatile變數時,用一種追加位元組的方式來優化佇列出隊和入隊的效能。LinkedTransferQueue的程式碼如下,著重關注p0~pe的定義:

/** 佇列中的頭部節點 */
private transient final PaddedAtomicReference<QNode> head;
/** 佇列中的尾部節點 */
private transient final PaddedAtomicReference<QNode> tail;
static final class PaddedAtomicReference <T> extends AtomicReference T> {
     // 使用很多4個位元組的引用追加到64個位元組
     Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
     PaddedAtomicReference(T r) {
        super(r);
     }
}
public class AtomicReference <V> implements java.io.Serializable {
     private volatile V value;
     // 省略其他程式碼

追加位元組能優化效能?這種方式看起來很神奇,但如果深入理解處理器架構就能理解其中的奧祕:(以下的解釋來自《Java併發程式設計的藝術一書》 )

“因為對於英特爾酷睿i7、酷睿、Atom和NetBurst,以及Core Solo和Pentium M處理器的L1、L2或L3快取的快取記憶體行是64個位元組寬,不支援部分填充快取行。

這意味著,如果佇列的頭節點和尾節點都不足64位元組的話,處理器會將它們都讀到同一個快取記憶體行中,在多處理器下每個處理器都會快取同樣的頭、尾節點。

當一個處理器試圖修改頭節點時,會將整個快取行鎖定,那麼在快取一致性機制的作用下,會導致其他處理器不能訪問自己快取記憶體中的尾節點,而佇列的入隊和出隊操作則需要不停修改頭節點和尾節點,所以在多處理器的情況下將會嚴重影響到佇列的入隊和出隊效率。

因此Doug lea使用追加到64位元組的方式來填滿高速緩衝區的快取行,避免頭節點和尾節點載入到同一個快取行,使頭、尾節點在修改時不會互相鎖定。

可以看到,在java的併發程式碼中能夠體現底層快取的設計。雖然這程式碼不太符合java希望遮蔽底層實現細節的設計理念,但是Doug lea大師對細節的考慮仍然讓人讚歎不已。

演算法的藝術:用搜尋解決迷宮問題

學習資料結構時,相信“深度優先搜尋”和“廣度優先搜尋”對初學者來說一度是一個噩夢,做練習題時也是用各種姿勢遍歷去二叉樹,無法感受到樂趣所在。

但是當你用搜尋來解決比較簡單的迷宮尋路問題時,便會感到演算法的魅力。

想起小時候玩一些RPG遊戲,往往會有各種迷宮,每次自己探索出口時,其實就是用的深度搜尋,找不到會回溯,然而這樣費時間也費腦子,當地圖過大,大腦的快取不足,或者思考深度不足時,解決起來就很困難。

程式設計,不止有程式碼,還有藝術

但如果有計算機的幫忙,對於每次的移動,給定地圖輸入,使用搜尋演算法、A*等演算法,便能夠快速找到迷宮的離開路線。

下面給出一個虛擬碼,來簡單解釋搜尋問題是怎麼解決問題的:

搜尋(當前地圖,當前點) {
  If (是否已經搜尋過這個場景) {
    Return;
    }

  If(是否到達邊界) {
      重新整理最新結果;
      return;
  }

  for(遍歷當前點的所有選擇) {
      if (是否是無效的選擇) {
          continue;
          }
 
      將當前選擇帶來的變化更新到地圖中
      進入後續的搜尋
      回退當前選擇帶來的變化
   }
}

所以當你學習完搜尋演算法,卻還對其應用感到困惑時,不妨來做一道迷宮尋路題.(例如

或者自己寫一個五子棋對戰程式與自己對戰。對戰程式除了搜尋演算法,還要考慮博弈論的思想,通過alpha-beta演算法來處理敵對雙方對結果的選擇,編寫評估函式來定義對局面好壞的判斷, 整個編寫過程會更加複雜而有趣,不妨作為自己對搜尋演算法更深層次的學習時嘗試一番。

二進位制的藝術:用數學節省了空間

“給定一個非空整數陣列,除了某個元素只出現一次以外,其餘每個元素均出現兩次。找出那個只出現了一次的元素,而且不能用額外空間”

甚至還有升級版:

“給你一個整數陣列 ,除某個元素僅出現一次外,其餘每個元素都恰出現 三次 。請你找出並返回那個只出現了一次的元素。”

第一想法肯定是維護一個雜湊表或者陣列。但是問題要求不能用額外空間,這一般都是為了在有成本限制的環境下考慮和設計的,例如記憶體有限的某些硬體裝置中。

因此在最佳解法中,選擇藉助了二進位制來解決這個問題。通過同位異或得0,不同位異或得1的特性,快速過濾掉相同的數字:

class Solution {
    public int singleNumber(int[] nums) {
        int result = 0;
        for(int num : nums) {
            result ^= num;
        }
        return result;
    }
}

是不是感覺非常巧妙有趣,利用數學的二進位制特性,簡單的異或就搞定了本來需要大量記憶體的問題,不禁令人拍案叫絕。

程式設計,不止有程式碼,還有藝術

本文參與華為雲社群【內容共創】活動第16期。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章