特斯拉與阿里及租車公司合作 方便中國民眾購買電動車

電動汽車大廠特斯拉(Tesla)日前分別與阿里巴巴旗下支付寶及一嗨租車合作,中國大陸客戶不僅可用支付寶訂車付款,還可以租用特斯拉的電動車。首批兩輛Model S電動車已經正式交付,下一批將會在5月份到達。

除了阿里巴巴與一嗨租車搶著與特斯拉合作,電信巨頭之一的中國聯通日前也傳出與特斯拉中國合作的消息,將由聯通為特斯拉的電動車提供車載資訊服務。

其實電動車在大陸已經銷售多年,長城、比亞迪等大陸本土車廠都有製造電動汽車,但整體銷量始終未見起色。特斯拉卻在大陸獲得了超高人氣。

根據中國汽車工業協會的統計,2013年全國的純電動車銷量只有14,604輛,僅占全年汽車銷量的不到1%。此外,在大陸的電動車銷售業績中,大多屬於商用載客或公務車輛,民眾主動購買的純電動車數量相當稀少。

特斯拉去年全年銷售了23,500輛電動汽車。特斯拉創辦人Elon Musk日前表示,預估今年公司在全球的銷量將超過30,000輛,其中有5,000輛會來自大陸市場。該公司更計畫在大陸設立超級充電站與電動車產線。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

面試官:換人!他連哈希扣的都不懂

前言

相信你面試的時候,肯定被問過 hashCode 和 equals 相關的問題 。如:

  • hashCode 是什麼?它是怎麼得來的?有什麼用?
  • 經典題,equals 和 == 有什麼區別?
  • 為什麼要重寫 equals 和 hashCode ?
  • 重寫了 equals ,就必須要重寫 hashCode 嗎?為什麼?
  • hashCode 相等時,equals 一定相等嗎?反過來呢?

好的,上面就是靈魂拷問環節。其實,這些問題仔細想一下也不難,主要是平時我們很少去思考它。

正文

下面就按照上邊的問題順序,一個一個剖析它。扒開 hashCode 的神秘面紗。

什麼是 hashCode?

我們通常說的 hashCode 其實就是一個經過哈希運算之後的整型值。而這個哈希運算的算法,在 Object 類中就是通過一個本地方法 hashCode() 來實現的(HashMap 中還會有一些其它的運算)。

public native int hashCode();

可以看到它是一個本地方法。那麼,想要了解這個方法到底是用來幹嘛的,最直接有效的方法就是,去看它的源碼註釋。

下邊我就用我蹩腳的英文翻譯一下它的意思。。。

返回當前對象的一個哈希值。這個方法用於支持一些哈希表,例如 HashMap 。

通常來講,它有如下一些約定:

  • 若對象的信息沒有被修改,那麼,在一個程序的執行期間,對於相同的對象,不管調用多少次 hashCode 方法,都應該返回相同的值。當然,在相同程序的不同執行期間,不需要保持結果一致。
  • 若兩個對象的 equals 方法返回值相同,那麼,調用它們各自的 hashCode 方法時,也必須返回相同的結果。(ps: 這句話解答了上邊的一些問題,後面會用例子來證明這一點)
  • 當兩個對象的 equals 方法返回值不同時,那麼它們的 hashCode 方法不用保證必須返回不同的值。但是,我們應該知道,在這種情況下,我們最好也設計成 hashCode 返回不同的值。因為,這樣做有助於提高哈希表的性能。

在實際情況下,Object 類的 hashCode 方法在不同的對象中確實返回了不同的哈希值。這通常是通過把對象的內部地址轉換為一個整數來實現的。

ps: 這裏說的內部地址就是指物理地址,也就是內存地址。需要注意的是,雖然 hashCode 值是依據它的內存地址而得來的。但是,不能說 hashCode 就代表對象的內存地址,實際上,hashCode 地址是存放在哈希表中的。

上邊的源碼註釋真可謂是句句珠璣,把 hashCode 方法解釋的淋漓盡致。一會兒我通過一個案例說明,就能明白我為什麼這樣說了。

什麼是哈希表?

上文中提到了哈希表。什麼是哈希表呢?我們直接看百度百科的解釋。

用一張圖來表示它們的關係。

左邊一列就是一些關鍵碼(key),通過哈希函數,它們都會得到一個固定的值,分別對應右邊一列的某個值。右邊的這一列就可以認為是一張哈希表。

而且,我們會發現,有可能有些 key 不同,但是它們對應的哈希值卻是一樣的,例如 aa,bb 都指向 1001 。但是,一定不會出現同一個 key 指向不同的值。

這也非常好理解,因為哈希表就是用來查找 key 的哈希地址的。在 key 確定的情況下,通過哈希函數計算出來的 哈希地址,一定也是確定的。如圖中的 dd 已經確定在 1002 位置了,那麼就不可能再佔據 1003 位置。

思考一下,如果有另外一個元素 ee 來了,它的哈希地址也落在 1002 位置,怎麼辦呢?

hashCode 有什麼用?

其實,上圖就已經可以說明一些問題了。我們通過一個 key 計算出它的 hashCode 值,就可以唯一確定它在哈希表中的位置。這樣,在查詢時,就可以直接定位到當前元素,提高查詢效率。

現在我們假設有這樣一個場景。我們需要在內存中的一塊兒區域存放 10000 個不同的元素(以aa,bb,cc,dd 等為例)。那怎麼實現不同的元素插入,相同的元素覆蓋呢?

我們最容易想到的方法就是,每當存一個新元素時,就遍歷一遍已經存在的元素,看有沒有相同的。這樣雖然也是可以實現的,但是,如果已經存在了 9000 個元素,你就需要去遍歷一下這 9000 個元素。很明顯,這樣的效率是非常低下的。

我們轉換一種思路,還是以上圖為例。若來了一個新元素 ff,首先去計算它的 hashCode 值,得出為 1003 。發現此處還沒有元素,則直接把這個新元素 ff 放到此位置。

然後,ee 來了,通過計算哈希值得到 1002 。此時,發現 1002 位置已經存在一個元素了。那麼,通過 equals 方法比較它們是否相等,發現只有一個 dd 元素,很明顯和 ee 不相等。那麼,就把 ee 元素放到 dd 元素的後邊(可以用鏈表形式存放)。

我們會發現,當有新元素來的時候,先去計算它們的哈希值,再去確定存放的位置,這樣就可以減少比較的次數。如 ff 不需要比較, ee 只需要和 dd 比較一次。

當元素越來越多的時候,新元素也只需要和當前哈希值相同的位置上,已經存在的元素進行比較。而不需要和其他哈希值不同的位置上的元素進行比較。這樣就大大減少了元素的比較次數。

圖中為了方便,畫的哈希表比較小。現在假設,這個哈希表非常的大,例如有這麼非常多個位置,從 1001 ~ 9999。那麼,新元素插入的時候,有很大概率會插入到一個還沒有元素存在的位置上,這樣就不需要比較了,效率非常高。但是,我們會發現這樣也有一個弊端,就是哈希表所佔的內存空間就會變大。因此,這是一個權衡的過程。

有心的同學可能已經發現了。我去,上邊的這個做法好熟悉啊。沒錯,它就是大名鼎鼎的 HashMap 底層實現的思想。對 HashMap 還不了解的,趕緊看這篇文章理一下思路。HashMap 底層實現原理及源碼分析

所以,hashCode 有什麼用。很明顯,提高了查詢,插入元素的效率呀。

equals 和 == 有什麼區別?

這是萬年不變,經久不衰的經典面試題了。讓我油然想起,當初為了面試,背誦過的面經了,簡直是一把心酸一把淚。現在還能記得這道題的標準答案:equals 比較的是內容, == 比較的是地址。

當時,真的就只是背答案,知其然而不知其所以然。再往下問,為什麼要重寫 equals ,就懵逼了。

首先,我們應該知道 equals 是定義在所有類的父類 Object 中的。

 public boolean equals(Object obj) {
     return (this == obj);
 }

可以看到,它的默認實現,就是 == ,這是用來比較內存地址的。所以,如果一個對象的 equals 不重寫的話,和 == 的效果是一樣的。

我們知道,當創建兩個普通對象時,一般情況下,它們所對應的內存地址是不一樣的。例如,我定義一個 User 類。

public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public User() {

    }
}

public class TestHashCode {
    public static void main(String[] args) {
        User user1 = new User("zhangsan", 20);
        User user2 = new User("lisi", 18); 

        System.out.println(user1 == user2);
        System.out.println(user1.equals(user2));
    }
}
// 結果: false	false

很明顯,zhangsan 和 lisi 是兩個人,兩個不同的對象。因此,它們所對應的內存地址不同,而且內容也不相等。

注意,這裏我還沒有對 User 重寫 equals,實際此時 equals 使用的是父類 Object 的方法,返回的肯定是不相等的。因此,為了更好地說明問題,我僅把第二行代碼修改如下:

//User user2 = new User("lisi", 18);
User user2 = new User("zhangsan", 20);

讓 user1 和 user2 的內容相同,都是 zhangsan,20歲。按我們的理解,這雖然是兩個對象,但是應該是指的同一個人,都是張三。但是,打印結果,如下:

這有悖於我們的認知,明明是同一個人,為什麼 equals 返回的卻不相等呢。因此,此時我們就需要把 User 類中的 equals 方法重寫,以達到我們的目的。在 User 中添加如下代碼(使用 idea 自動生成代碼):

public class User {
    ... //省略已知代碼
        
    @Override
    public boolean equals(Object o) {
        //若兩個對象的內存地址相同,則說明指向的是同一個對象,故內容一定相同。
        if (this == o) return true;
        //類都不是同一個,更別談相等了
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        //比較兩個對象中的所有屬性,即name和age都必須相同,才可認為兩個對象相等
        return age == user.age &&
                Objects.equals(name, user.name);
    }
   
}
//打印結果:  false 	true

再次執行程序,我們會發現此時 equals 返回 true ,這才是我們想要的。

因此,當我們使用自定義對象時。如果需要讓兩個對象的內容相同時,equals 返回 true,則需要重寫 equals 方法。

為什麼要重寫 equals 和 hashCode ?

在上邊的案例中,其實我們已經說明了為什麼要去重寫 equals 。因為,在對象內容相同的情況下,我們需要讓對象相等。因此,不能用 Object 類的默認實現,只去比較內存地址,這樣是不合理的。

那 hashCode 為什麼要重寫呢? 這就涉及到集合,如 Map 和 Set (底層其實也是 Map)了。

我們以 HashMap JDK1.8的源碼來看,如 put 方法。

我們會發現,代碼中會多次進行 hash 值的比較,只有當哈希值相等時,才會去比較 equals 方法。當 hashCode 和 equals 都相同時,才會覆蓋元素。get 方法也是如此(先比較哈希值,再比較equals),

只有 hashCode 和 equals 都相等時,才認為是同一個元素,找到並返回此元素,否則返回 null。

這也對應 “hashCode 有什麼用?”這一小節。 重寫 equals 和 hashCode 的目的,就是為了方便哈希表這樣的結構快速的查詢和插入。如果不重寫,則無法比較元素,甚至造成元素位置錯亂。

重寫了 equals ,就必須要重寫 hashCode 嗎?

答案是肯定的。首先,在上邊的 JDK 源碼註釋中第第二點,我們就會發現這句說明。其次,我們嘗試重寫 equals ,而不重寫 hashCode 看會發生什麼現象。

public class TestHashCode {
    public static void main(String[] args) {
        User user1 = new User("zhangsan", 20);
        User user2 = new User("zhangsan", 20);

        HashMap<User, Integer> map = new HashMap<>();
        map.put(user1,90);
        System.out.println(map.get(user2));
    }
}
// 打印結果: null

對於代碼中的 user1 和 user2 兩個對象來說,我們認為他是同一個人張三。定義一個 map ,key 存儲 User 對象, value 存儲他的學習成績。

當把 user1 對象作為 key ,成績 90 作為 value 存儲到 map 中時,我們肯定希望,用 key 為 user2 來取值時,得到的結果是 90 。但是,結果卻大失所望,得到了 null 。

這是因為,我們自定義的 User 類,雖然重寫了 equals ,但是沒有重寫 hashCode 。當 user1 放到 map 中時,計算出來的哈希值和用 user2 去取值時計算的哈希值不相等。因此,equals 方法都沒有比較的機會。認為他們是不同的元素。然而,其實,我們應該認為 user1 和 user2 是相同的元素的。

用圖來說明就是,user1 和 user2 存放在了 HashMap 中不同的桶裡邊,導致查詢不到目標元素。

因此,當我們用自定義類來作為 HashMap 的 key 時,必須要重寫 hashCode 和 equals 。否則,會得到我們不想要的結果。

這也是為什麼,我們平時都喜歡用 String 字符串來作為 key 的原因。 因為, String 類默認就幫我們實現了 equals 和 hashCode 方法的重寫。如下,

// String.java
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        //從前向後依次比較字符串中的每個字符
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
		//把字符串中的每個字符都取出來,參与運算
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        //把計算出來的最終值,存放在hash變量中。
        hash = h;
    }
    return h;
}

重寫 equals 時,可以使用 idea 提供的自動代碼,也可以自己手動實現。

public class User {
    ... //省略已知代碼
        
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
   
}
//此時,map.get(user2) 可以得到 90 的正確值

在重寫了 hashCode 后,使用自定義對象作為 key 時,還需要注意一點,不要在使用過程中,改變對象的內容,這樣會導致 hashCode 值發生改變,同樣得不到正確的結果。如下,

public class TestHashCode {
    public static void main(String[] args) {
        User user = new User("zhangsan", 20);

        HashMap<User, Integer> map = new HashMap<>();
        map.put(user,90);
        System.out.println(map.get(user));
        user.setAge(18); //把對象的年齡修改為18
        System.out.println(map.get(user));
    }
}
// 打印結果:
// 90
// null

會發現,修改后,拿到的值是 null 。這也是,hashCode 源碼註釋中的第一點說明的,hashCode 值不變的前提是,對象的信息沒有被修改。若被修改,則有可能導致 hashCode 值改變。

此時,有沒有聯想到其他一些問題。比如,為什麼 String 類要設計成不可以變的呢?這裏用 String 作為 HashMap 的 key 時,可以算作一個原因。你肯定不希望,放進去的時候還好好的,取出來的時候,卻找不到元素了吧。

String 類內部會有一個變量(hash)來緩存字符串的 hashCode 值。只有字符串不可變,才可以保證哈希值不變。

hashCode 相等時,equals 一定相等嗎?

很顯然不是的。在 HashMap 的源碼中,我們就能看到,當 hashCode 相等時(產生哈希碰撞),還需要比較它們的 equals ,才可以確定是否是同一個對象。因此,hashCode 相等時, equals 不一定相等 。

反過來,equals 相等的話, hashCode 一定相等嗎? 那必須的。equals 都相等了,那說明在 HashMap 中認為它們是同一個元素,所以 hashCode 值必須也要保證相等。

結論:

  • hashCode 相等,equals 不一定相等。
  • hashCode 不等,equals 一定不等。
  • equals 相等, hashCode 一定相等。
  • equals 不等, hashCode 不一定不等。

關於最後這一點,就是 hashCode 源碼註釋中提到的第三點。當 equals 不等時,不用必須保證它們的 hashCode 也不相等。但是為了提高哈希表的效率,最好設計成不等。

因為,我們既然知道它們不相等了,那麼當 hashCode 設計成不等時。只要比較 hashCode 不相等,我們就可以直接返回 null,而不必再去比較 equals 了。這樣,就減少了比較的次數,無疑提高了效率。

結尾

以上就是 hashCode 和 equals 相關的一些問題。相信已經可以解答你心中的疑惑了,也可以和面試官侃侃而談。再也不用擔心,面試官說換人了。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

用廢棄物鋪路 越南首條回收塑膠道路啟用

摘錄自2019年10月3日中央社報導

越南第一條用廢棄塑膠鋪成的200公尺長道路日前在海防市啟用。這項以廢棄塑膠鋪路的提案由DOW CHEMICAL VIETNAM公司、海防市(Hai Phong)廷武(Dinh Vu)工業區(Deep C)開發商與越南航海大學等單位合作展開,其中第一階段在這個工業區內完成興建以廢棄塑膠鋪成的200公尺長道路。

越南媒體報導,大量回收塑膠廢棄物由當地環保單位提供,經清洗、烘乾與壓碎後,在攝氏150至180度的溫度下與瀝青混合,隨後用來鋪路;這個塑膠和瀝青混合的材料,有利提升道路耐用度。

越南商工總會(VCCI)表示,塑膠廢棄物處理的新出路符合於越南政府所提出經濟永續發展政策以及「全國反對塑膠垃圾行動」,這個模式應在各其他地方擴大實施。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

全球60城市氣候抗議行動開跑 紐澳數十人被捕 阿姆斯特丹數百人阻交通

摘錄自2019年10月7日聯合報報導

「反抗滅絕」在全球發起公民不服從運動7日展開,在澳洲和紐西蘭各地有數十名示威者遭到逮捕。這場運動將為期2週,目的為了要求各國政府採取緊急行動對抗氣候變遷。

數百人在繁忙的雪梨市中心道路上靜坐封路,最後警察拖走示威者終止這場活動,隨後有30人遭到起訴。

在布里斯本,一小群運動人士把自己鎖在一座橋上。警方表示,他們在現場逮捕7人,並全數加以起訴。

同一時間在紐西蘭首都威靈頓,示威者用鎖鏈把自己與一輛閃亮的粉紅色車輛綁住,造成市區部分交通癱瘓。警方表示,今天稍晚有30人遭到逮捕,但無人被起訴。

荷蘭阿姆斯特丹市7日出現數百名氣候變遷倡議人士,他們無視警方的禁令,高喊「反抗」,阻礙市中心交通。警方已警告在先,將逮捕違令人士,防止他們擾亂通勤民眾。

荷蘭警方已核准倡議團體「反抗滅絕」(Extinction Rebellion, XR)的抗議計劃,前提是不能阻礙交通。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

MongoDB via Dotnet Core數據映射詳解

用好數據映射,MongoDB via Dotnet Core開發變會成一件超級快樂的事。

一、前言

MongoDB這幾年已經成為NoSQL的頭部數據庫。

由於MongoDB free schema的特性,使得它在互聯網應用方面優於常規數據庫,成為了相當一部分大廠的主數據選擇;而它的快速布署和開發簡單的特點,也吸引着大量小開發團隊的支持。

關於MongoDB快速布署,我在15分鐘從零開始搭建支持10w+用戶的生產環境(二)里有寫,需要了可以去看看。

作為一個數據庫,基本的操作就是CRUD。MongoDB的CRUD,不使用SQL來寫,而是提供了更簡單的方式。

方式一、BsonDocument方式

BsonDocument方式,適合能熟練使用MongoDB Shell的開發者。MongoDB Driver提供了完全覆蓋Shell命令的各種方式,來處理用戶的CRUD操作。

這種方法自由度很高,可以在不需要知道完整數據集結構的情況下,完成數據庫的CRUD操作。

方式二、數據映射方式

數據映射是最常用的一種方式。準備好需要處理的數據類,直接把數據類映射到MongoDB,並對數據集進行CRUD操作。

下面,對數據映射的各個部分,我會逐個說明。

    為了防止不提供原網址的轉載,特在這裏加上原文鏈接:https://www.cnblogs.com/tiger-wang/p/13185605.html

二、開發環境&基礎工程

這個Demo的開發環境是:Mac + VS Code + Dotnet Core 3.1.2。

建立工程:

% dotnet new sln -o demo
The template "Solution File" was created successfully.
cd demo 
% dotnet new console -o demo
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on demo/demo.csproj...
  Determining projects to restore...
  Restored demo/demo/demo.csproj (in 162 ms).

Restore succeeded.
% dotnet sln add demo/demo.csproj 
Project `demo/demo.csproj` added to the solution.

建立工程完成。

下面,增加包mongodb.driver到工程:

cd demo
% dotnet add package mongodb.driver
  Determining projects to restore...
info : Adding PackageReference for package 'mongodb.driver' into project 'demo/demo/demo.csproj'.
info : Committing restore...
info : Writing assets file to disk. Path: demo/demo/obj/project.assets.json
log  : Restored /demo/demo/demo.csproj (in 6.01 sec).

項目準備完成。

看一下目錄結構:

% tree .
.
├── demo
│   ├── Program.cs
│   ├── demo.csproj
│   └── obj
│       ├── demo.csproj.nuget.dgspec.json
│       ├── demo.csproj.nuget.g.props
│       ├── demo.csproj.nuget.g.targets
│       ├── project.assets.json
│       └── project.nuget.cache
└── demo.sln

mongodb.driver是MongoDB官方的數據庫SDK,從Nuget上安裝即可。

三、Demo準備工作

創建數據映射的模型類CollectionModel.cs,現在是個空類,後面所有的數據映射相關內容會在這個類進行說明:

public class CollectionModel
{

}

並修改Program.cs,準備Demo方法,以及連接數據庫:

class Program
{

    private const string MongoDBConnection = "mongodb://localhost:27031/admin";

    private static IMongoClient _client = new MongoClient(MongoDBConnection);
    private static IMongoDatabase _database = _client.GetDatabase("Test");
    private static IMongoCollection<CollectionModel> _collection = _database.GetCollection<CollectionModel>("TestCollection");

    static async Task Main(string[] args)
    
{
        await Demo();
        Console.ReadKey();
    }

    private static async Task Demo()
    
{
    }
}

四、字段映射

從上面的代碼中,我們看到,在生成Collection對象時,用到了CollectionModel

IMongoDatabase _database = _client.GetDatabase("Test");
IMongoCollection<CollectionModel> _collection = _database.GetCollection<CollectionModel>("TestCollection");

這兩行,其實就完成了一個映射的工作:把MongoDB中,Test數據庫下,TestCollection數據集(就是SQL中的數據表),映射到CollectionModel這個數據類中。換句話說,就是用CollectionModel這個類,來完成對數據集TestCollection的所有操作。

保持CollectionModel為空,我們往數據庫寫入一行數據:

private static async Task Demo()
{
    CollectionModel new_item = new CollectionModel();
    await _collection.InsertOneAsync(new_item);
}

執行,看一下寫入的數據:


    "_id" : ObjectId("5ef1d8325327fd4340425ac9")
}

OK,我們已經寫進去一條數據了。因為映射類是空的,所以寫入的數據,也只有_id一行內容。

但是,為什麼會有一個_id呢?

1. ID字段

MongoDB數據集中存放的數據,稱之為文檔(Document)。每個文檔在存放時,都需要有一個ID,而這個ID的名稱,固定叫_id

當我們建立映射時,如果給出_id字段,則MongoDB會採用這個ID做為這個文檔的ID,如果不給出,MongoDB會自動添加一個_id字段。

例如:

public class CollectionModel
{

    public ObjectId _id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
}

public class CollectionModel
{

    public string title { get; set; }
    public string content { get; set; }
}

在使用上是完全一樣的。唯一的區別是,如果映射類中不寫_id,則MongoDB自動添加_id時,會用ObjectId作為這個字段的數據類型。

ObjectId是一個全局唯一的數據。

當然,MongoDB允許使用其它類型的數據作為ID,例如:stringintlongGUID等,但這就需要你自己去保證這些數據不超限並且唯一。

例如,我們可以寫成:

public class CollectionModel
{

    public long _id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
}

我們也可以在類中修改_id名稱為別的內容,但需要加一個描述屬性BsonId

public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
}

這兒特別要注意:BsonId屬性會告訴映射,topic_id就是這個文檔數據的ID。MongoDB在保存時,會將這個topic_id轉成_id保存到數據集中。

在MongoDB數據集中,ID字段的名稱固定叫_id。為了代碼的閱讀方便,可以在類中改為別的名稱,但這不會影響MongoDB中存放的ID名稱。

修改Demo代碼:

private static async Task Demo()
{
    CollectionModel new_item = new CollectionModel()
    {
        title = "Demo",
        content = "Demo content",
    };
    await _collection.InsertOneAsync(new_item);
}

跑一下Demo,看看保存的結果:


    "_id" : ObjectId("5ef1e1b1bc1e18086afe3183"), 
    "title" : "Demo"
    "content" : "Demo content"
}

2. 簡單字段

就是常規的數據字段,直接寫就成。

public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
    public int favor { get; set; }
}

保存后的數據:


    "_id" : ObjectId("5ef1e9caa9d16208de2962bb"), 
    "title" : "Demo"
    "content" : "Demo content"
    "favor" : NumberInt(100)
}

3. 一個的特殊的類型 – Decimal

說Decimal特殊,是因為MongoDB在早期,是不支持Decimal的。直到MongoDB v3.4開始,數據庫才正式支持Decimal。

所以,如果使用的是v3.4以後的版本,可以直接使用,而如果是以前的版本,需要用以下的方式:

[BsonRepresentation(BsonType.Double, AllowTruncation = true)]
public decimal price { get; set; }

其實就是把Decimal通過映射,轉為Double存儲。

4. 類字段

把類作為一個數據集的一個字段。這是MongoDB作為文檔NoSQL數據庫的特色。這樣可以很方便的把相關的數據組織到一條記錄中,方便展示時的查詢。

我們在項目中添加兩個類ContactAuthor

public class Contact
{

    public string mobile { get; set; }
}
public class Author
{

    public string name { get; set; }
    public List<Contact> contacts { get; set; }
}

然後,把Author加到CollectionModel中:

public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
    public int favor { get; set; }
    public Author author { get; set; }
}

嗯,開始變得有點複雜了。

完善Demo代碼:

private static async Task Demo()
{
    CollectionModel new_item = new CollectionModel()
    {
        title = "Demo",
        content = "Demo content",
        favor = 100,
        author = new Author
        {
            name = "WangPlus",
            contacts = new List<Contact>(),
        }
    };

    Contact contact_item1 = new Contact()
    {
        mobile = "13800000000",
    };
    Contact contact_item2 = new Contact()
    {
        mobile = "13811111111",
    };
    new_item.author.contacts.Add(contact_item1);
    new_item.author.contacts.Add(contact_item2);

    await _collection.InsertOneAsync(new_item);
}

保存的數據是這樣的:


    "_id" : ObjectId("5ef1e635ce129908a22dfb5e"), 
    "title" : "Demo"
    "content" : "Demo content"
    "favor" : NumberInt(100),
    "author" : {
        "name" : "WangPlus"
        "contacts" : [
            {
                "mobile" : "13800000000"
            }, 
            {
                "mobile" : "13811111111"
            }
        ]
    }
}

這樣的數據結構,用着不要太爽!

5. 枚舉字段

枚舉字段在使用時,跟類字段相似。

創建一個枚舉TagEnumeration

public enum TagEnumeration
{
    CSharp = 1,
    Python = 2,
}

加到CollectionModel中:

public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
    public int favor { get; set; }
    public Author author { get; set; }
    public TagEnumeration tag { get; set; }
}

修改Demo代碼:

private static async Task Demo()
{
    CollectionModel new_item = new CollectionModel()
    {
        title = "Demo",
        content = "Demo content",
        favor = 100,
        author = new Author
        {
            name = "WangPlus",
            contacts = new List<Contact>(),
        },
        tag = TagEnumeration.CSharp,
    };
    /* 後邊代碼略過 */
}

運行后看數據:


    "_id" : ObjectId("5ef1eb87cbb6b109031fcc31"), 
    "title" : "Demo"
    "content" : "Demo content"
    "favor" : NumberInt(100), 
    "author" : {
        "name" : "WangPlus"
        "contacts" : [
            {
                "mobile" : "13800000000"
            }, 
            {
                "mobile" : "13811111111"
            }
        ]
    }, 
    "tag" : NumberInt(1)
}

在這裏,tag保存了枚舉的值。

我們也可以保存枚舉的字符串。只要在CollectionModel中,tag聲明上加個屬性:

public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
    public int favor { get; set; }
    public Author author { get; set; }
    [BsonRepresentation(BsonType.String)]
    public TagEnumeration tag { get; set; }
}

數據會變成:


    "_id" : ObjectId("5ef1ec448f1d540919d15904"), 
    "title" : "Demo"
    "content" : "Demo content"
    "favor" : NumberInt(100), 
    "author" : {
        "name" : "WangPlus"
        "contacts" : [
            {
                "mobile" : "13800000000"
            }, 
            {
                "mobile" : "13811111111"
            }
        ]
    }, 
    "tag" : "CSharp"
}

6. 日期字段

日期字段會稍微有點坑。

這個坑其實並不源於MongoDB,而是源於C#的DateTime類。我們知道,時間根據時區不同,時間也不同。而DateTime並不準確描述時區的時間。

我們先在CollectionModel中增加一個時間字段:

public class CollectionModel
{

    [BsonId]
    public ObjectId topic_id { get; set; }
    public string title { get; set; }
    public string content { get; set; }
    public int favor { get; set; }
    public Author author { get; set; }
    [BsonRepresentation(BsonType.String)]
    public TagEnumeration tag { get; set; }
    public DateTime post_time { get; set; }
}

修改Demo:

private static async Task Demo()
{
    CollectionModel new_item = new CollectionModel()
    {
        /* 前邊代碼略過 */
        post_time = DateTime.Now, /* 2020-06-23T20:12:40.463+0000 */
    };
    /* 後邊代碼略過 */
}

運行看數據:


    "_id" : ObjectId("5ef1f1b9a75023095e995d9f"), 
    "title" : "Demo"
    "content" : "Demo content"
    "favor" : NumberInt(100), 
    "author" : {
        "name" : "WangPlus"
        "contacts" : [
            {
                "mobile" : "13800000000"
            }, 
            {
                "mobile" : "13811111111"
            }
        ]
    }, 
    "tag" : "CSharp"
    "post_time" : ISODate("2020-06-23T12:12:40.463+0000")
}

對比代碼時間和數據時間,會發現這兩個時間差了8小時 – 正好的中國的時區時間。

MongoDB規定,在數據集中存儲時間時,只會保存UTC時間。

如果只是保存(像上邊這樣),或者查詢時使用時間作為條件(例如查詢post_time < DateTime.Now的數據)時,是可以使用的,不會出現問題。

但是,如果是查詢結果中有時間字段,那這個字段,會被DateTime默認設置為DateTimeKind.Unspecified類型。而這個類型,是無時區信息的,輸出显示時,會造成混亂。

為了避免這種情況,在進行時間字段的映射時,需要加上屬性:

[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime post_time { get; set; }

這樣做,會強制DateTime類型的字段為DateTimeKind.Local類型。這時候,從显示到使用就正確了。

但是,別高興的太早,這兒還有一個但是。

這個但是是這樣的:數據集中存放的是UTC時間,跟我們正常的時間有8小時時差,如果我們需要按日統計,比方每天的銷售額/點擊量,怎麼搞?上面的方式,解決不了。

當然,基於MongoDB自由的字段處理,可以把需要統計的字段,按年月日時分秒拆開存放,像下面這樣的:

class Post_Time
{

    public int year { get; set; }
    public int month { get; set; }
    public int day { get; set; }
    public int hour { get; set; }
    public int minute { get; set; }
    public int second { get; set; }
}

能解決,但是Low哭了有沒有?

下面,終極方案來了。它就是:改寫MongoDB中對於DateTime字段的序列化類。噹噹當~~~

先創建一個類MyDateTimeSerializer

public class MyDateTimeSerializer : DateTimeSerializer
{
    public override DateTime Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    
{
        var obj = base.Deserialize(context, args);
        return new DateTime(obj.Ticks, DateTimeKind.Unspecified);
    }
    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DateTime value)
    
{
        var utcValue = new DateTime(value.Ticks, DateTimeKind.Utc);
        base.Serialize(context, args, utcValue);
    }
}

代碼簡單,一看就懂。

注意,使用這個方法,上邊那個對於時間加的屬性[BsonDateTimeOptions(Kind = DateTimeKind.Local)]一定不要添加,要不然就等着哭吧:P

創建完了,怎麼用?

如果你只想對某個特定映射的特定字段使用,比方只對CollectionModelpost_time字段來使用,可以這麼寫:

[BsonSerializer(typeof(MyDateTimeSerializer))]
public DateTime post_time { get; set; }

或者全局使用:

BsonSerializer.RegisterSerializer(typeof(DateTime), new MongoDBDateTimeSerializer());

BsonSerializer是MongoDB.Driver的全局對象。所以這個代碼,可以放到使用數據庫前的任何地方。例如在Demo中,我放在Main里了:

static async Task Main(string[] args)
{
    BsonSerializer.RegisterSerializer(typeof(DateTime), new MyDateTimeSerializer());

    await Demo();
    Console.ReadKey();
}

這回看數據,數據集中的post_time跟當前時間显示完全一樣了,你統計,你分組,可以隨便霍霍了。

7. Dictionary字段

這個需求很奇怪。我們希望在一個Key-Value的文檔中,保存一個Key-Value的數據。但這個需求又是真實存在的,比方保存一個用戶的標籤和標籤對應的命中次數。

數據聲明很簡單:

public Dictionary<stringint> extra_info { get; set; }

MongoDB定義了三種保存屬性:DocumentArrayOfDocumentsArrayOfArrays,默認是Document

屬性寫法是這樣的:

[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)]
public Dictionary<stringint> extra_info { get; set; }

這三種屬性下,保存在數據集中的數據結構有區別。

DictionaryRepresentation.Document


    "extra_info" : {
        "type" : NumberInt(1), 
        "mode" : NumberInt(2)
    }
}

DictionaryRepresentation.ArrayOfDocuments


    "extra_info" : [
        {
            "k" : "type"
            "v" : NumberInt(1)
        }, 
        {
            "k" : "mode"
            "v" : NumberInt(2)
        }
    ]
}

DictionaryRepresentation.ArrayOfArrays


    "extra_info" : [
        [
            "type"
            NumberInt(1)
        ], 
        [
            "mode"
            NumberInt(2)
        ]
    ]
}

這三種方式,從數據保存上並沒有什麼區別,但從查詢來講,如果這個字段需要進行查詢,那三種方式區別很大。

如果採用BsonDocument方式查詢,DictionaryRepresentation.Document無疑是寫着最方便的。

如果用Builder方式查詢,DictionaryRepresentation.ArrayOfDocuments是最容易寫的。

DictionaryRepresentation.ArrayOfArrays就算了。數組套數組,查詢條件寫死人。

我自己在使用時,多數情況用DictionaryRepresentation.ArrayOfDocuments

五、其它映射屬性

上一章介紹了數據映射的完整內容。除了這些內容,MongoDB還給出了一些映射屬性,供大家看心情使用。

1. BsonElement屬性

這個屬性是用來改數據集中的字段名稱用的。

看代碼:

[BsonElement("pt")]
public DateTime post_time { get; set; }

在不加BsonElement的情況下,通過數據映射寫到數據集中的文檔,字段名就是變量名,上面這個例子,字段名就是post_time

加上BsonElement后,數據集中的字段名會變為pt

2. BsonDefaultValue屬性

看名稱就知道,這是用來設置字段的默認值的。

看代碼:

[BsonDefaultValue("This is a default title")]
public string title { get; set; }

當寫入的時候,如果映射中不傳入值,則數據庫會把這個默認值存到數據集中。

3. BsonRepresentation屬性

這個屬性是用來在映射類中的數據類型和數據集中的數據類型做轉換的。

看代碼:

[BsonRepresentation(BsonType.String)]
public int favor { get; set; }

這段代表表示,在映射類中,favor字段是int類型的,而存到數據集中,會保存為string類型。

前邊Decimal轉換和枚舉轉換,就是用的這個屬性。

4. BsonIgnore屬性

這個屬性用來忽略某些字段。忽略的意思是:映射類中某些字段,不希望被保存到數據集中。

看代碼:

[BsonIgnore]
public string ignore_string { get; set; }

這樣,在保存數據時,字段ignore_string就不會被保存到數據集中。

六、總結

數據映射本身沒什麼新鮮的內容,但在MongoDB中,如果用好了映射,開發過程從效率到爽的程度,都不是SQL可以相比的。正所謂:

一入Mongo深似海,從此SQL是路人。

謝謝大家!

(全文完)

本文的配套代碼在https://github.com/humornif/Demo-Code/tree/master/0015/demo

 

 

微信公眾號:老王Plus

掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送

本文版權歸作者所有,轉載請保留此聲明和原文鏈接

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

Tesla Model 3 有對手了!LG Chem 研發長程電動車電池

Tesla 又有新對手了,南韓 LG Chem 目前也在開發最新電池技術,且無論航程或售價均足以與特斯拉研發中的 Model 3 匹敵。   Model S 是目前市面上唯一航程可以達 200 英哩的電動車,但即使是最低售價也高達 7.1 萬美元。特斯拉計畫在 2017 年推出 Model 3,售價將壓低至每輛 3.5 萬美元,但售價仍然不敵 LG Chem 正將推出的電動車。   LG Chem 位於美國的研究單位主管 Prabhakar Patil 證實,正在開發續航力 200 英哩的電池,目前已有幾家汽車業者表示對該項技術有興趣,產品最快於 2017 年就可上市,售價將介於 3 至 3.5 萬美元間。   Patil 雖然沒有明講,但外界猜想對該項技術有興趣的汽車業者,一定包含通用汽車。LG Chem 目前已是通用油電混合動力車 Volt 的電池供應商,且通用之前表示,將推出續航力 200 英哩的電動車,起價最低 3 萬美元,符合 Patil 設定的條件。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

跟着whatwg看一遍事件循環

前言

對於單線程來說,事件循環可以說是重中之重了,它為任務分配不同的優先級,井然有序的調度。讓js解析,用戶交互,頁面渲染等互不衝突,各司其職。

我們書寫的代碼無時無刻都在和事件循環打交道,要想寫出更流暢,我們就必須深入了解事件循環,下面我們將從規範中翻譯和解讀整個流程。

以下內容來自whatwg文檔,均為個人理解,若有不對,煩請指出,我會第一時間修改,避免誤導他人!

正文

為了協調用戶操作,js執行,頁面渲染,網絡請求等事件,每個宿主中,存在事件循環這樣的角色,並且該角色在當前宿主中是唯一的。

簡單解釋一下宿主:宿主是一個ECMAScript執行上下文,一般包含執行上下文棧,運行時執行環境,宿主記錄和一個執行線程,除了這個執行線程外,其他的專屬於當前宿主。例如,某些瀏覽器在不同的tabs使用同一個執行線程。

不僅如此,事件循環又存於在各個不同場景,有瀏覽器環境下的,worker環境下的和Worklet環境下的。

Worklet是一個輕量級的web worker,可以讓開發者訪問更底層的渲染工作線,也就是說你可以通過Worklet去干預瀏覽器的渲染環境。

提到了worklet,那就順便看一個例子(需開啟服務,不要以file協議運行),通過這個例子,可以看到事件循環不同階段觸發了什麼鈎子函數:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <style>
            .fancy {
                background-image: paint(headerHighlight);
                display: layout(sample-layout);
                background-color: green;
            }
        </style>
    </head>
    <body>
        <h1 class="fancy">My Cool Header</h1>
        <script>
            console.log('開始');
            CSS.paintWorklet.addModule('./paint.js');
            CSS.layoutWorklet.addModule('./layout.js');

            requestAnimationFrame(() => {
                console.log('requestAnimationFrame');
            });
            Promise.resolve().then(() => {
                console.log('微任務');
            });
            setTimeout(function () {
                document.querySelector('.fancy').style.height = '150px';
                ('translateZ(0)');

                Promise.resolve().then(() => {
                    console.log('新一輪的微任務');
                });
                requestAnimationFrame(() => {
                    console.log('新一輪的requestAnimationFrame');
                });
            }, 2000);
            console.log(2);
        </script>
    </body>
</html>

// paint.js
registerPaint(
    'headerHighlight',
    class {
        static get contextOptions() {
            console.log('contextOptions');
            return {alpha: true};
        }

        paint(ctx) {
            console.log('paint函數');
        }
    }
);

// ==========================分割線

// layout.js
registerLayout(
    'sample-layout',
    class {
        async intrinsicSizes(children, edges, styleMap) {}

        async layout(children, edges, constraints, styleMap, breakToken) {
            console.log('layout階段');
        }
    }
);

事件循環有一個或多個Task隊列,每個Task隊列都是Task的一個集合。其中Task不是指我們的某個函數,而是一個上下文環境,結構如下:

  • step:一系列任務將要執行的步驟
  • source:任務來源,常用來對相關任務進行分組和系列化
  • document:與當前任務相關的document對象,如果是非window環境則為null
  • 環境配置對象:在任務期間追蹤記錄任務狀態

這裏的Task隊列不是Task,是一個集合,因為取出一個Task隊列中的Task是選擇一個可執行的Task,而不是出隊操作。

微任務隊列是一個入對出對的隊列。

這裏說明一下,Task隊列為什麼有多個,因為不同的Task隊列有不同的優先級,進而進行次序排列和調用,有沒有感覺react的fiber和這個有點類似?

舉個例子,Task隊列可以是專門負責鼠標和鍵盤事件的,並且賦予鼠標鍵盤隊列較高的優先級,以便及時響應用戶操作。另一個Task隊列負責其他任務源。不過也不要餓死任何一個task,這個後續處理模型中會介紹。

Task封裝了負責以下任務的算法:

  • Events: 由專門的Task在特定的EventTarget(一個具有監聽訂閱模式列表的對象)上分發事件對象
  • Parsing: html解析器標記一個或多個字節,並處理所有生成的結果token
  • Callbacks: 由專門的Task觸發回調函數
  • Using a resource: 當該算法獲取資源的時候,如果該階段是以非阻塞方式發生,那麼一旦部分或者全部資源可用,則由Task進行後續處理
  • Reacting to DOM manipulation: 通過dom操作觸發的任務,例如插入一個節點到document

事件循環有一個當前運行中的Task,可以為null,如果是null的話,代表着可以接受一個新的Task(新一輪的步驟)。

事件循環有微任務隊列,默認為空,其中的任務由微任務排隊算法創建。

事件循環有一個執行微任務檢查點,默認為false,用來防止微任務死循環。

微任務排隊算法:

  1. 如果未提供event loop,設置一個隱式event loop。
  2. 如果未提供document,設置一個隱式document.
  3. 創建一個Task作為新的微任務
  4. 設置setp、source、document到新的Task上
  5. 設置Task的環境配置對象為空集
  6. 添加到event loop的微任務隊列中

微任務檢查算法:

  1. 如果微任務檢查標誌為true,直接return
  2. 設置微任務檢查標誌為true
  3. 如果微任務隊里不為空(也就是說微任務添加的微任務也會在這個循環中出現,直到微任務隊列為空):
    1. 從微任務隊列中找出最老的任務(防餓死)
    2. 設置當前執行任務為這個最老的任務
    3. 執行
    4. 重置當前執行任務為null
  4. 通知環境配置對象的promise進行reject操作
  5. 清理indexdb事務(不太明白這一步,如果有讀者了解,煩請點撥一下)
  6. 設置微任務檢查標誌為false

處理模型

event loop會按照下面這些步驟進行調度:

  1. 找到一個可執行的Task隊列,如果沒有則跳轉到下面的微任務步驟
  2. 讓最老的Task作為Task隊列中第一個可執行的Task,並將其移除
  3. 將最老的Task作為event loop的可執行Task
  4. 記錄任務開始時間點
  5. 執行Task中的setp對應的步驟(上文中Task結構中的step)
  6. 設置event loop的可執行任務為null
  7. 執行微任務檢查算法
  8. 設置hasARenderingOpportunity(是否可以渲染的flag)為false
  9. 記住當前時間點
  10. 通過下面步驟記錄任務持續時間
    1. 設置頂層瀏覽器環境為空
    2. 對於每個最老Task的腳本執行環境配置對象,設置當前的頂級瀏覽器上下文到其上
    3. 報告消耗過長的任務,並附帶開始時間,結束時間,頂級瀏覽器上下文和當前Task
  11. 如果在window環境下,會根據硬件條件決定是否渲染,比如刷新率,頁面性能,頁面是否在後台,不過渲染會定期出現,避免頁面卡頓。值得注意的是,正常的刷新率為60hz,大概是每秒60幀,大約16.7ms每幀,如果當前瀏覽器環境不支持這個刷新率的話,會自動降為30hz,而不是丟幀。而李蘭其在後台的時候,聰明的瀏覽器會將這個渲染時機降為每秒4幀甚至更低,事件循環也會減少(這就是為什麼我們可以用setInterval來判斷時候能打開其他app的判斷依據的原因)。如果能渲染的話會設置hasARenderingOpportunity為true。

除此之外,還會在觸發resize、scroll、建立媒體查詢、運行css動畫等,也就是說瀏覽器幾乎大部分用戶操作都發生在事件循環中,更具體點是事件循環中的ui render部分。之後會進行requestAnimationFrame和IntersectionObserver的觸發,再之後是ui渲染

  1. 如果下麵條件都成立,那麼執行空閑階段算法,對於開發者來說就是調用window.requestIdleCallback方法
    1. 在window環境下
    2. event loop中沒有活躍的Task
    3. 微任務隊列為空
    4. hasARenderingOpportunity為false

借鑒網上的一張圖來粗略表示下整個流程

小結

上面就是整個事件循環的流程,瀏覽器就是按照這個規則一遍遍的執行,而我們要做的就是了解並適應這個規則,讓瀏覽器渲染出性能更高的頁面。

比如:

  1. 非首屏相關性能打點可以放到idle callback中執行,減少對頁面性能的損耗
  2. 微任務中遞歸添加微任務會導致頁面卡死,而不是隨着事件循環一輪輪的執行
  3. 更新元素布局的最好時機是在requestAnimateFrame中
  4. 盡量避免頻繁獲取元素布局信息,因為這會觸發強制layout(哪些屬性會導致強制layout?),影響頁面性能
  5. 事件循環有多個任務隊列,他們互不衝突,但是用戶交互相關的優先級更高
  6. resize、scroll等會伴隨事件循環中ui渲染觸發,而不是根據我們的滾動觸發,換句話說,這些操作自帶節流
  7. 等等,歡迎補充

最後感謝大家閱讀,歡迎一起探討!

提前祝大家端午節nb

參考

composite

深入探究 eventloop 與瀏覽器渲染的時序問題

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

用Map+函數式接口來實現策略模式

用Map+函數式接口來實現策略模式

目前在魔都,貝殼找房是我的僱主,平時關注一些 java 領域相關的技術,希望你們能在這篇文章中找到些有用的東西。個人水平有限,如果文章有錯誤還請指出,在留言區一起交流。

本文已投稿至公眾號 Hollis 原文鏈接:https://mp.weixin.qq.com/s/hkypvNBkRjPM6HM51_jW9g

我想大家肯定都或多或少的看過各種“策略模式”的講解、佈道等等,這篇文章就是來好好“澄清”一下策略模式,並嘗試回答以下的問題:

  1. 策略模式是如何優化業務邏輯代碼結構的?
  2. 殺雞焉用宰牛刀?就是幾個if else場景我需要用到策略模式?!
  3. 有沒有什麼更好的代碼結構來實現策略模式的嗎?

策略模式是如何優化業務邏輯代碼結構的?

要回答這個問題,我們還得先扒一扒策略模式的定義,從定義着手來理解它:

策略模式的教科書定義

它的定義很精簡:一個類的行為或其算法可以在運行時更改。我們把它降維到代碼層面,用人話翻譯一下就是,運行時我給你這個類的方法傳不同的“key”,你這個方法會執行不同的業務邏輯。細品一下,這不就是 if else 乾的事嗎?

策略模式優化了什麼?

其實策略模式的核心思想和 if else如出一轍,根據不同的key動態的找到不同的業務邏輯,那它就只是如此嗎?

實際上,我們口中的策略模式其實就是在代碼結構上調整,用接口+實現類+分派邏輯來使代碼結構可維護性好點。

一般教科書上講解到接口與實現類就結束了,其他博客上會帶上提及分派邏輯。這裏就不啰嗦了。

小結一下,即使用了策略模式,你該寫的業務邏輯照常寫,到邏輯分派的時候,還是變相的if else。而它的優化點是抽象了出了接口,將業務邏輯封裝成一個一個的實現類,任意地替換。在複雜場景(業務邏輯較多)時比直接 if else 來的好維護些。

殺雞焉用宰牛刀?就是幾個if else場景我需要用到策略模式?!

我想小夥伴們經常有這樣的不滿,我的業務邏輯就3 4 行,你給我整一大堆類定義?有必要這麼麻煩嗎?我看具體的業務邏輯還需要去不同的類中,簡單點行不行。

其實我們所不滿的就是策略模式帶來的缺點:1、策略類會增多 2、業務邏輯分散到各個實現類中,而且沒有一個地方可以俯視整個業務邏輯

針對傳統策略模式的缺點,在這分享一個實現思路,這個思路已經幫我們團隊解決了多個複雜if else的業務場景,理解上比較容易,代碼上需要用到Java8的特性——利用Map與函數式接口來實現。

直接show代碼結構:為了簡單演示一個思路,代碼用String 類型來模擬一個業務BO

其中:

  1. getCheckResult() 為傳統的做法
  2. getCheckResultSuper()則事先在Map中定義好了“判斷條件”與“業務邏輯”的映射關係,具體講解請看代碼註釋
/**
 * 某個業務服務類
 */
@Service
public class BizService {

    /**
     * 傳統的 if else 解決方法
     * 當每個業務邏輯有 3 4 行時,用傳統的策略模式不值得,直接的if else又顯得不易讀
     */
    public String getCheckResult(String order) {
        if ("校驗1".equals(order)) {
            return "執行業務邏輯1";
        } else if ("校驗2".equals(order)) {
            return "執行業務邏輯2";
        }else if ("校驗3".equals(order)) {
            return "執行業務邏輯3";
        }else if ("校驗4".equals(order)) {
            return "執行業務邏輯4";
        }else if ("校驗5".equals(order)) {
            return "執行業務邏輯5";
        }else if ("校驗6".equals(order)) {
            return "執行業務邏輯6";
        }else if ("校驗7".equals(order)) {
            return "執行業務邏輯7";
        }else if ("校驗8".equals(order)) {
            return "執行業務邏輯8";
        }else if ("校驗9".equals(order)) {
            return "執行業務邏輯9";
        }
        return "不在處理的邏輯中返回業務錯誤";
    }

    /**
     * 業務邏輯分派Map
     * Function為函數式接口,下面代碼中 Function<String, String> 的含義是接收一個Stirng類型的變量,返回一個String類型的結果
     */
    private Map<String, Function<String, String>> checkResultDispatcher = new HashMap<>();

    /**
     * 初始化 業務邏輯分派Map 其中value 存放的是 lambda表達式
     */
    @PostConstruct
    public void checkResultDispatcherInit() {
        checkResultDispatcher.put("校驗1", order -> String.format("對%s執行業務邏輯1", order));
        checkResultDispatcher.put("校驗2", order -> String.format("對%s執行業務邏輯2", order));
        checkResultDispatcher.put("校驗3", order -> String.format("對%s執行業務邏輯3", order));
        checkResultDispatcher.put("校驗4", order -> String.format("對%s執行業務邏輯4", order));
        checkResultDispatcher.put("校驗5", order -> String.format("對%s執行業務邏輯5", order));
        checkResultDispatcher.put("校驗6", order -> String.format("對%s執行業務邏輯6", order));
        checkResultDispatcher.put("校驗7", order -> String.format("對%s執行業務邏輯7", order));
        checkResultDispatcher.put("校驗8", order -> String.format("對%s執行業務邏輯8", order));
        checkResultDispatcher.put("校驗9", order -> String.format("對%s執行業務邏輯9", order));
    }

    public String getCheckResultSuper(String order) {
        //從邏輯分派Dispatcher中獲得業務邏輯代碼,result變量是一段lambda表達式
        Function<String, String> result = checkResultDispatcher.get(order);
        if (result != null) {
            //執行這段表達式獲得String類型的結果
            return result.apply(order);
        }
        return "不在處理的邏輯中返回業務錯誤";
    }
}

通過http調用一下看看效果:

/**
 * 模擬一次http調用
 */
@RestController
public class BizController {

    @Autowired
    private BizService bizService;

    @PostMapping("/v1/biz/testSuper")
    public String test2(String order) {
        return bizService.getCheckResultSuper(order);
    }
}

這是個簡單的demo演示,之後會舉一些複雜的場景案例。

魯迅曾說過,“每解決一個問題,就會引出更多的問題”。我們一起來看看這樣的實現有什麼好處,會帶來什麼問題。

好處很直觀:

  1. 在一段代碼里直觀的看到”判斷條件”與業務邏輯的映射關係
  2. 不需要單獨定義接口與實現類,直接使用現有的函數式接口(什麼?不知道函數式接口?快去了解),而實現類直接就是業務代碼本身。

不好的點:

  1. 需要團隊成員對lambda表達式有所了解(什麼?Java14都出來了還有沒用上Java8新特性的小夥伴?)

接下來我舉幾個在業務中經常遇到的if else場景,並用Map+函數式接口的方式來解決它

在真實業務場景問題的解決思路

有的小夥伴會說,我的判斷條件有多個啊,而且很複雜,你之前舉個例子只有單個判斷邏輯,而我有多個判斷邏輯該怎麼辦呢?

很好解決:寫一個判斷邏輯的方法,Map的key由方法計算出

/**
 * 某個業務服務類
 */
@Service
public class BizService {

    private Map<String, Function<String, String>> checkResultDispatcherMuti = new HashMap<>();

    /**
     * 初始化 業務邏輯分派Map 其中value 存放的是 lambda表達式
     */
    @PostConstruct
    public void checkResultDispatcherMuitInit() {
        checkResultDispatcherMuti.put("key_訂單1", order -> String.format("對%s執行業務邏輯1", order));
        checkResultDispatcherMuti.put("key_訂單1_訂單2", order -> String.format("對%s執行業務邏輯2", order));
        checkResultDispatcherMuti.put("key_訂單1_訂單2_訂單3", order -> String.format("對%s執行業務邏輯3", order));
    }

    public String getCheckResultMuti(String order, int level) {
        //寫一段生成key的邏輯:
        String ley = getDispatcherKey(order, level);

        Function<String, String> result = checkResultDispatcherMuti.get(ley);
        if (result != null) {
            //執行這段表達式獲得String類型的結果
            return result.apply(order);
        }
        return "不在處理的邏輯中返回業務錯誤";
    }

    /**
     * 判斷條件方法
     */
    private String getDispatcherKey(String order, int level) {
        StringBuilder key = new StringBuilder("key");
        for (int i = 1; i <= level; i++) {
            key.append("_" + order + i);
        }
        return key.toString();
    }
}

用http模擬一下:

/**
 * 模擬一次http調用
 */
@RestController
public class BizController {

    @Autowired
    private BizService bizService;

    @PostMapping("/v1/biz/testMuti")
    public String test1(String order, Integer level) {
        return bizService.getCheckResultMuti(order, level);
    }
}

只要設計好你的key的生成規則就好。

還有小夥伴會問:我的業務邏輯有很多很多行,在checkResultDispatcherMuitInit()方法的Map中直接寫不會很長嗎?

直接寫當然長了,我們可以抽象出一個service服務專門放業務邏輯,然後在定義中調用它就好了:

提供一個業務邏輯單元:

/**
 * 提供業務邏輯單元
 */
@Service
public class BizUnitService {

    public String bizOne(String order) {
        return order + "各種花式操作1";
    }
    public String bizTwo(String order) {
        return order + "各種花式操作2";
    }
    public String bizThree(String order) {
        return order + "各種花式操作3";
    }
}
/**
 * 某個業務服務類
 */
@Service
public class BizService {
    @Autowired
    private BizUnitService bizUnitService;

    private Map<String, Function<String, String>> checkResultDispatcherComX = new HashMap<>();

    /**
     * 初始化 業務邏輯分派Map 其中value 存放的是 lambda表達式
     */
    @PostConstruct
    public void checkResultDispatcherComXInit() {
        checkResultDispatcherComX.put("key_訂單1", order -> bizUnitService.bizOne(order));
        checkResultDispatcherComX.put("key_訂單1_訂單2", order -> bizUnitService.bizTwo(order));
        checkResultDispatcherComX.put("key_訂單1_訂單2_訂單3", order -> bizUnitService.bizThree(order));
    }

    public String getCheckResultComX(String order, int level) {
        //寫一段生成key的邏輯:
        String ley = getDispatcherComXKey(order, level);

        Function<String, String> result = checkResultDispatcherComX.get(ley);
        if (result != null) {
            //執行這段表達式獲得String類型的結果
            return result.apply(order);
        }
        return "不在處理的邏輯中返回業務錯誤";
    }

    /**
     * 判斷條件方法
     */
    private String getDispatcherComXKey(String order, int level) {
        StringBuilder key = new StringBuilder("key");
        for (int i = 1; i <= level; i++) {
            key.append("_" + order + i);
        }
        return key.toString();
    }
}

調用結果:

總結

最後,我們一起嘗試回答以下幾個問題:

  1. 策略模式是如何優化業務邏輯代碼結構的?
    —— 抽象了出了接口,將業務邏輯封裝成一個一個的實現類,任意地替換。在複雜場景(業務邏輯較多)時比直接 if else 來的好維護些。
  2. 殺雞焉用宰牛刀?就是幾個if else場景我需要用到策略模式?!——我們所不滿的其實就是傳統接口實現的缺點:1、策略類會很多。 2、業務邏輯分散到各個實現類中,而且沒有一個地方可以俯覽整個業務邏輯
  3. 有沒有什麼更好的代碼結構來實現策略模式的嗎?——針對傳統策略模式的缺點,分享了利用Map與函數式接口來實現的思路。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

算法崗面試題:模型的bias和variance是什麼?用隨機森林舉例

校招在即,準備準備一些面試可能會用到的東西吧。希望這次面試不會被掛。

基本概念

說到機器學習模型的誤差,主要就是bias和variance。

  • Bias:如果一個模型的訓練錯誤大,然後驗證錯誤和訓練錯誤都很大,那麼這個模型就是高bias。可能是因為欠擬合,也可能是因為模型是弱分類器。

  • Variance:模型的訓練錯誤小,但是驗證錯誤遠大於訓練錯誤,那麼這個模型就是高Variance,或者說它是過擬合。

這個圖中,左上角是低偏差低方差的,可以看到所有的預測值,都會落在靶心,完美模型;

右上角是高偏差,可以看到,雖然整體數據預測的好像都在中心,但是波動很大。

【高偏差vs高方差】
在機器學習中,因為偏差和方差不能兼顧,所以我們一般會選擇高偏差、低方差的左下角的模型。穩定性是最重要的,寧可所有的樣本都80%正確率,也不要部分樣本100%、部分50%的正確率。個人感覺,穩定性是學習到東西的體現,高方差模型與隨機蒙的有什麼區別?

隨機森林為例

上面的可能有些抽象,這裏用RandomForest(RF)來作為例子:
隨機森林是bagging的集成模型,這裏:
\(RF(x)=\frac{1}{B}\sum^B_{i=1}{T_{i,z_i}(x)}\)

  • RF(x)表示隨機森林對樣本x的預測值;
  • B表示總共有B棵樹;
  • \(z_i\)表示第i棵樹所使用的訓練集,是使用bagging的方法,從所有訓練集中進行行採樣和列採樣得到的子數據集。

這裏所有的\(z\),都是從所有數據集中隨機採樣的,所以可以理解為都是服從相同分佈的。所以不斷增加B的數量,增加隨機森林中樹的數量,是不會減小模型的偏差的。
【個人感覺,是因為不管訓練再多的樹,其實就那麼多數據,怎麼訓練都不會減少,這一點比較好理解】

【RF是如何降低偏差的?】
直觀上,使用多棵樹和bagging,是可以增加模型的穩定性的。怎麼證明的?

我們需要計算\(Var(T(x))\)
假設不同樹的\(z_i\)之間的相關係數為\(\rho\),然後每棵樹的方差都是\(\sigma^2\).

先複習一下兩個隨機變量相加的方差如何表示:
\(Var(aX+bY)=a^2 Var(X)+b^2 Var(Y) + 2ab cov(X,Y)\)

  • Cov(X,Y)表示X和Y的協方差。協方差和相關係數不一樣哦,要除以X和Y的標準差:
    \(\rho=\frac{cov(X,Y)}{\sigma_X \sigma_Y}\)

下面轉成B個相關變量的方差計算,是矩陣的形式:

很好推導的,可以試一試。

這樣可以看出來了,RF的樹的數量越多,RF方差的第二項會不斷減小,但是第一項不變。也就是說,第一項就是RF模型偏差的下極限了。

【總結】

  • 增加決策樹的數量B,偏差不變;方差減小;
  • 增加決策樹深度,偏差減小;\(\rho\)減小,\(\sigma^2\)增加;
  • 增加bagging採樣比例,偏差減小;\(\rho\)增加,\(\sigma^2\)增加;

【bagging vs boost】
之前也提到過了boost算法:
一文讀懂:GBDT梯度提升
GBDT中,在某種情況下,是不斷訓練之前模型的殘差,來達到降低bias的效果。雖然也是集成模型,但是可以想到,每一個GBDT中的樹,所學習的數據的分佈都是不同的,這意味着在GBDT模型的方差會隨着決策樹的數量增多,不斷地增加。

  • bagging的目的:降低方差;
  • boost的目的:降低偏差

喜歡的話請關注我們的微信公眾號~【你好世界煉丹師】。

  • 公眾號主要講統計學,數據科學,機器學習,深度學習,以及一些參加Kaggle競賽的經驗。
  • 公眾號內容建議作為課後的一些相關知識的補充,飯後甜點。
  • 此外,為了不過多打擾,公眾號每周推送一次,每次4~6篇精選文章。

微信搜索公眾號:你好世界煉丹師。期待您的關注。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

F-貿聯營收隨 Tesla 出貨倍增 2015 年可望翻倍成長

從 2005 年開始就與美國電動車大廠 Tesla 合作,而貿聯的營收比重也隨著 Tesla 成長,從 2013 年的 3~5%,到 2014 年倍增到 8% 至 10%,有機會晉升貿聯的前五大客戶。   據了解,2013 年同期貿聯打入 Tesla 的零件料號總數約 45 個,目前已倍增到 100 個,法人推估,平均貿聯交貨每台電動車用線的成本可達 300~400 美元,以 2015 年 Tesla 出貨量上看 10  萬輛來看,2015 年對貿聯的營收貢獻可望維持近倍成長,營收占比拉高至 15~20%。  

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!