特斯拉 Cybertruck 的 6 個設計原則與 5 個未公開的疑問

特斯拉上週發表了最新車種 Cybertruck,破格的外型與性能讓全球傻眼,本篇文章將告訴你 Cybertruck 設計成這樣的 6 個理由,以及 5 個它尚未說清楚的疑點。

老派科幻電影的線條、全車冷鑄不鏽鋼、跑得比保時捷還快、拉得比福特還給力,特斯拉 Cybertruck 一出場就奪走全世界目光,如同特斯拉執行長馬斯克(Elon Musk)說的,他要改變皮卡車一成不變的外觀。改變歸改變,改成這樣確實是有點誇張了,但 Cybertruck 在設計時,是遵循著以下 6 個原則,才會長成這樣。

  1. 原則一:強悍外表
  2. 原則二:將低風阻,提高效率
  3. 原則三:內在舒適大空間
  4. 原則四:適應不同路面駕駛
  5. 原則五:大載貨量與拉力
  6. 原則六:大電池容量

這些原則(或說是目標),部分來自於馬斯克個人的期待,一部分來自於市場調查的回饋,相信收到這個提案要求的專案,當下應該很想離職。但從發表會的成果來看,他們確實做到了。

為了看起來更強悍,Cybertruck 全車採用 301 不鏽鋼包覆。

皮卡車外表強悍,幾乎是一種固定形象,方方正正的大塊頭是百年傳統。但這樣的外表牴觸了原則二的低風阻要求,同時別忘了,低風阻的跑車,可不會配備原則三的車內大空間。

最終,結合這三項要求的最大公約數,原來就存在於 80 年代的科幻電影跟電玩中,當然這一切若沒有電動馬達是不可能達成的。燃油車的引擎通常配置於車頭,這是為了讓後方留出寬敞的乘坐空間,犧牲了車頭的流線設計,也在傳輸過程中浪費了部分動力。因此追求速度的跑車,通常將車頭壓低,引擎後置,這樣子減低了風阻,也讓動力傳導更直接,但是乘坐空間就非常狹窄,更別提燃油皮卡車後面的大型貨斗了。

Cybertruck 和目前越來越多的電動車一樣,將馬達、電池組整合在底盤,離輪胎更近,也讓上部空間更有彈性,Cybertruck 進一步運用了大眾對皮卡車車身較高的印象,讓它的底盤空間能塞進更多電池,還有其他黑科技,來滿足這些設計原則,像是適應性氣壓懸吊、龐大的電池容量以及可容納六人乘坐的車廂和一個可開閉的貨斗。

從試乘影片中,可以看到後座乘客(身高約 180 公分)在入座後,頭頂上仍然有兩個拳頭左右的空間,算是相當舒適,然而腿部空間似乎就比較侷促,後座寬度看起來和一般房車差不多,塞進兩位大漢剛剛好,整體來說,算是勉強達成了原則三的大空間要求。(官方數據,能容納 6 個成人)

但是,在完成這樣的設計之後,我們會發現,這種外觀線條看起來並不是那麼強悍,因為斜切的車頭讓人聯想到的是跑車,不是皮卡車或悍馬車這種硬漢風格。解決的辦法,就如同我們現在看到的,捨棄一般車輛的鈑金與烤漆,改用與姊妹公司 SpaceX 的太空船同樣材料的 301 不鏽鋼板,而且厚度達到 3 公厘,足以抵擋 9mm 口徑手槍的射擊。代價是,每一扇車門重達 27 公斤,萬一被車門夾到,後果可是不得了。

Cybertruck 內部空間比想像中寬闊,前方只有一塊 17 吋觸控螢幕作為主控台。

5 個等待揭開的疑問

發表會後隔天,馬斯克宣布訂單數量已經突破 14 萬,換句話說,這個發表會已經幫特斯拉賺進了 1,400 萬美元,然而這位愛做夢、愛說大話的老闆,心中也明白最後成交的數量可能不到一半,因為這台科幻裝甲車,仍然有幾個問題需要釐清。

問題一:後照鏡

許多人都發現了,發表會上的 Cybertruck 並沒有裝備車側後照鏡,官網上的影片與照片也都沒有看到,推測是使用攝影機來取代了傳統後照鏡,然而並非所有國家都開放電子後照鏡上路,包含美國跟台灣都還沒開放。在 Cybertruck 試乘的過程中,也沒看到螢幕上有播放左右後側的畫面,只有裝設數位正後方後照鏡,究竟 Cybertruck 怎麼解決車側後照鏡的問題,尚有待解答。

問題二:外觀塗裝

全不鏽鋼車身肯定會吸引到一部分的客群,但是正式販售時會不會提供不同款式的彩繪或是塗裝,並未說明,畢竟許多車主仍然希望自己的愛車能保有一些特殊性,至少花了 7 萬美元購買頂級款的車主,會希望自己的車看起來跟 4 萬美元的有些不同。

而提到外觀就不能不提那一整片乾乾淨淨的車頭,一般車輛會有車頭 Logo、進氣壩等,即使特斯拉其他車款,也都留有進氣孔,或許 Cybertruck 可以不用 Logo 來辨識,但是令人好奇的是它怎麼解決進氣的問題,又或者 Cybertruck 有了不同的方式來處理散熱和車內空氣流通問題

問題三:上下車

一般的皮卡車,車身都較高,對於家有老小的家庭來說,上下車會比較辛苦。從 Cybertruck 的照片中來看,底盤高度約在成年女子的膝蓋,算是有一點點難度。

在發表會上,Cybertruck 展示了它的適應性氣壓懸吊系統,在貨斗載重時,能夠自動調整車身高度;特斯拉官方也表示,當行駛於不同路面時,Cybertruck 會自動調整懸吊,在高速公路時會降低車身,在路面崎嶇時,會提高車身。這讓人不免想知道,當車主或乘客要上下車的時候,車身是否也會自動降低,方便進出呢?

問題四:貨斗操縱方式

市面上的皮卡車,後方貨斗大都是開放式的,也有車主會加裝棚罩,除了保護行李外,最重要是不讓路人亂丟垃圾。Cybertruck 的貨斗是機械式的,在發表會中,可以看到機車騎士走到車尾按了一些隱藏的開關,開啟貨斗,接著再用手拉開尾門,放下斜板。

特斯拉是否會將貨斗開閉設計得更容易使用呢?

對於一般卡車司機來說,這些動作稀鬆平常,但對於標榜高科技的 Cybertruck 來說,似乎不太「性感」,因此我們不免好奇,正式上市後,特斯拉是否會將操作貨斗開閉的功能,放到他們的手機 App 裡,讓車主可以更輕鬆地開關?

問題五:交車時間

最後的問題,也是特斯拉在過去幾年最為人詬病的問題,真的能夠如期交車嗎?特斯拉官網上寫得非常保守,「生產日期接近 2021 年底」,最高規格的三馬達版本,更是標明「預計在 2022 年底開始生產」。這等於是在說,我沒有說什麼時候交車,我只說了什麼時候開始製造,從生產到交車要多久,沒有人知道。

(合作媒體:。圖片來源:)

延伸閱讀:

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

【其他文章推薦】

※公開收購3c價格,不怕被賤賣!

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

自己動手實現分佈式任務調度框架(續)

  之前寫過一篇:本來是用來閑來分享一下自己的思維方式,時至今日發現居然有些人正在使用了,本着對代碼負責人的態度,對代碼部分已知bug進行了修改,並增加了若干功能,如立即啟動,實時停止等功能,新增加的功能會在這一篇做詳細的說明。

  提到分佈式任務調度,市面上本身已經有一些框架工具可以使用,但是個人覺得功能做的都太豐富,架構都過於複雜,所以才有了我重複造輪子。個人喜歡把複雜的問題簡單化,利用有限的資源實現竟可能多的功能。因為有幾個朋友問部署方式,這裏再次強調下:我的這個服務可以直接打成jar放在自己本地倉庫,然後依賴進去,或者直接copy代碼過去,當成自己項目的一部分就可以了。也就是說跟隨你們自己的項目啟動,所以我這裏也沒有寫界面。下面先談談怎麼基於上次的代碼實現任務立即啟動吧!

  調度和自己服務整合後部署圖抽象成如下:

  

 

 

   用戶在前端點擊立即請求按鈕,通過各種負載均衡軟件或者設備,到達某台機器的某個帶有本調度框架的服務,然後進行具體的執行,也就是說這個立即啟動就是一個最常見最簡單的請求,沒有過多複雜的問題(比如多節點會不會重複執行這些)。最簡單的辦法,當用戶請求過來直接用一個線程或者線程池執行用戶點的那個任務的邏輯代碼就行了,當然我這裏沒有那麼粗暴,現有的調度代碼資源如下:

package com.rdpaas.task.scheduler;

import com.rdpaas.task.common.Invocation;
import com.rdpaas.task.common.Node;
import com.rdpaas.task.common.NotifyCmd;
import com.rdpaas.task.common.Task;
import com.rdpaas.task.common.TaskDetail;
import com.rdpaas.task.common.TaskStatus;
import com.rdpaas.task.config.EasyJobConfig;
import com.rdpaas.task.repository.NodeRepository;
import com.rdpaas.task.repository.TaskRepository;
import com.rdpaas.task.strategy.Strategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 任務調度器
 * @author rongdi
 * @date 2019-03-13 21:15
 */
@Component
public class TaskExecutor {

    private static final Logger logger = LoggerFactory.getLogger(TaskExecutor.class);

    @Autowired
    private TaskRepository taskRepository;

    @Autowired
    private NodeRepository nodeRepository;

    @Autowired
    private EasyJobConfig config;

    /**
     * 創建任務到期延時隊列
      */
    private DelayQueue<DelayItem<Task>> taskQueue = new DelayQueue<>();

    /**
     * 可以明確知道最多只會運行2個線程,直接使用系統自帶工具就可以了
     */
    private ExecutorService bossPool = Executors.newFixedThreadPool(2);

    /**
     * 正在執行的任務的Future
     */
    private Map<Long,Future> doingFutures = new HashMap<>();

    /**
     * 聲明工作線程池
     */
    private ThreadPoolExecutor workerPool;
    
    /**
     * 獲取任務的策略
     */
    private Strategy strategy;


    @PostConstruct
    public void init() {
        /**
         * 根據配置選擇一個節點獲取任務的策略
         */
        strategy = Strategy.choose(config.getNodeStrategy());
        /**
         * 自定義線程池,初始線程數量corePoolSize,線程池等待隊列大小queueSize,當初始線程都有任務,並且等待隊列滿后
         * 線程數量會自動擴充最大線程數maxSize,當新擴充的線程空閑60s后自動回收.自定義線程池是因為Executors那幾個線程工具
         * 各有各的弊端,不適合生產使用
         */
        workerPool = new ThreadPoolExecutor(config.getCorePoolSize(), config.getMaxPoolSize(), 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(config.getQueueSize()));
        /**
         * 執行待處理任務加載線程
         */
        bossPool.execute(new Loader());
        /**
         * 執行任務調度線程
         */
        bossPool.execute(new Boss());
    
    }

    class Loader implements Runnable {

        @Override
        public void run() {
            for(;;) {
                try { 
                    /**
                     * 先獲取可用的節點列表
                     */
                    List<Node> nodes = nodeRepository.getEnableNodes(config.getHeartBeatSeconds() * 2);
                    if(nodes == null || nodes.isEmpty()) {
                        continue;
                    }
                    /**
                     * 查找還有指定時間(單位秒)才開始的主任務列表
                     */
                    List<Task> tasks = taskRepository.listNotStartedTasks(config.getFetchDuration());
                    if(tasks == null || tasks.isEmpty()) {
                        continue;
                    }
                    for(Task task:tasks) {
                        
                        boolean accept = strategy.accept(nodes, task, config.getNodeId());
                        /**
                         * 不該自己拿就不要搶
                         */
                        if(!accept) {
                            continue;
                        }
                        /**
                         * 先設置成待執行
                         */
                        task.setStatus(TaskStatus.PENDING);
                        task.setNodeId(config.getNodeId());
                        /**
                         * 使用樂觀鎖嘗試更新狀態,如果更新成功,其他節點就不會更新成功。如果其它節點也正在查詢未完成的
                         * 任務列表和當前這段時間有節點已經更新了這個任務,version必然和查出來時候的version不一樣了,這裏更新
                         * 必然會返回0了
                         */
                        int n = taskRepository.updateWithVersion(task);
                        Date nextStartTime = task.getNextStartTime();
                        if(n == 0 || nextStartTime == null) {
                            continue;
                        }
                        /**
                         * 封裝成延時對象放入延時隊列,這裏再查一次是因為上面樂觀鎖已經更新了版本,會導致後面結束任務更新不成功
                         */
                        task = taskRepository.get(task.getId());
                        DelayItem<Task> delayItem = new DelayItem<Task>(nextStartTime.getTime() - new Date().getTime(), task);
                        taskQueue.offer(delayItem);
                        
                    }
                    Thread.sleep(config.getFetchPeriod());
                } catch(Exception e) {
                    logger.error("fetch task list failed,cause by:{}", e);
                }
            }
        }
        
    }
    
    class Boss implements Runnable {
        @Override
        public void run() {
            for (;;) {
                try {
                     /**
                     * 時間到了就可以從延時隊列拿出任務對象,然後交給worker線程池去執行
                     */
                    DelayItem<Task> item = taskQueue.take();
                    if(item != null && item.getItem() != null) {
                        Task task = item.getItem();
                        /**
                         * 真正開始執行了設置成執行中
                         */
                        task.setStatus(TaskStatus.DOING);
                        /**
                         * loader線程中已經使用樂觀鎖控制了,這裏沒必要了
                         */
                        taskRepository.update(task);
                        /**
                         * 提交到線程池
                         */
                        Future future = workerPool.submit(new Worker(task));
                        /**
                         * 暫存在doingFutures
                         */
                        doingFutures.put(task.getId(),future);
                    }
                     
                } catch (Exception e) {
                    logger.error("fetch task failed,cause by:{}", e);
                }
            }
        }

    }

    class Worker implements Callable<String> {

        private Task task;

        public Worker(Task task) {
            this.task = task;
        }

        @Override
        public String call() {
            logger.info("Begin to execute task:{}",task.getId());
            TaskDetail detail = null;
            try {
                //開始任務
                detail = taskRepository.start(task);
                if(detail == null) return null;
                //執行任務
                task.getInvokor().invoke();
                //完成任務
                finish(task,detail);
                logger.info("finished execute task:{}",task.getId());
                /**
                 * 執行完后刪了
                 */
                doingFutures.remove(task.getId());
            } catch (Exception e) {
                logger.error("execute task:{} error,cause by:{}",task.getId(), e);
                try {
                    taskRepository.fail(task,detail,e.getCause().getMessage());
                } catch(Exception e1) {
                    logger.error("fail task:{} error,cause by:{}",task.getId(), e);
                }
            }
            return null;
        }

    }

    /**
     * 完成子任務,如果父任務失敗了,子任務不會執行
     * @param task
     * @param detail
     * @throws Exception
     */
    private void finish(Task task,TaskDetail detail) throws Exception {

        //查看是否有子類任務
        List<Task> childTasks = taskRepository.getChilds(task.getId());
        if(childTasks == null || childTasks.isEmpty()) {
            //當沒有子任務時完成父任務
            taskRepository.finish(task,detail);
            return;
        } else {
            for (Task childTask : childTasks) {
                //開始任務
                TaskDetail childDetail = null;
                try {
                    //將子任務狀態改成執行中
                    childTask.setStatus(TaskStatus.DOING);
                    childTask.setNodeId(config.getNodeId());
                    //開始子任務
                    childDetail = taskRepository.startChild(childTask,detail);
                    //使用樂觀鎖更新下狀態,不然這裏可能和恢複線程產生併發問題
                    int n = taskRepository.updateWithVersion(childTask);
                    if (n > 0) {
                        //再從數據庫取一下,避免上面update修改后version不同步
                        childTask = taskRepository.get(childTask.getId());
                        //執行子任務
                        childTask.getInvokor().invoke();
                        //完成子任務
                        finish(childTask, childDetail);
                    }
                } catch (Exception e) {
                    logger.error("execute child task error,cause by:{}", e);
                    try {
                        taskRepository.fail(childTask, childDetail, e.getCause().getMessage());
                    } catch (Exception e1) {
                        logger.error("fail child task error,cause by:{}", e);
                    }
                }
            }
            /**
             * 當有子任務時完成子任務后再完成父任務
             */
            taskRepository.finish(task,detail);

        }

    }

    /**
     * 添加任務
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addTask(String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        return taskRepository.insert(task);
    }

    /**
     * 添加子任務
     * @param pid
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addChildTask(Long pid,String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        task.setPid(pid);
        return taskRepository.insert(task);
    }

   
}

  上面主要就是三組線程,Loader負責加載將要執行的任務放入本地的任務隊列,Boss線程負責取出任務隊列的任務,然後分配Worker線程池的一個線程去執行。由上面的代碼可以看到如果要立即執行,其實只需要把一個延時為0的任務放入任務隊列,等着Boss線程去取然後分配給worker執行就可以實現了,代碼如下:

    /**
     * 立即執行任務,就是設置一下延時為0加入任務隊列就好了,這個可以外部直接調用
     * @param taskId
     * @return
     */
    public boolean startNow(Long taskId) {
        Task task = taskRepository.get(taskId);
        task.setStatus(TaskStatus.DOING);
        taskRepository.update(task);
        DelayItem<Task> delayItem = new DelayItem<Task>(0L, task);
        return taskQueue.offer(delayItem);
    }

  啟動不用再多說,下面介紹一下停止任務,根據面向對象的思維,用戶要想停止一個任務,最終執行停止任務的就是正在執行任務的那個節點。停止任務有兩種情況,第一種任務沒有正在運行如何停止,第二種是任務正在運行如何停止。第一種其實直接改變一下任務對象的狀態為停止就行了,不必多說。下面主要考慮如何停止正在運行的任務,細心的朋友可能已經發現上面代碼和之前那一篇代碼有點區別,之前用的Runnble作為線程實現接口,這個用了Callable,其實在java中停止線程池中正在運行的線程最常用的就是直接調用future的cancel方法了,要想獲取到這個future對象就需要將以前實現Runnbale改成實現Callable,然後提交到線程池由execute改成submit就可以了,然後每次提交到線程池得到的future對象使用taskId一起保存在一個map中,方便根據taskId隨時找到。當然任務執行完后要及時刪除這個map里的任務,以免常駐其中導致內存溢出。停止任務的請求流程如下

  

 

 

  圖還是原來的圖,但是這時候情況不一樣了,因為停止任務的時候假如當前正在執行這個任務的節點處於服務1,負載均衡是不知道要去把你引到服務1的,他可能會引入到服務2,那就悲劇了,所以通用的做法就是停止請求過來不管落到哪個節點上,那個節點就往一個公用的mq上發一個帶有停止任務業務含義的消息,各個節點訂閱這個消息,然後判斷都判斷任務在不在自己這裏執行,如果在就執行停止操作。但是這樣勢必讓我們的調度服務又要依賴一個外部的消息隊列服務,就算很方便的就可以引入一個外部的消息隊列,但是你真的可以駕馭的了嗎,消息丟了咋辦,重複發送了咋辦,消息服務掛了咋辦,網絡斷了咋辦,又引入了一大堆問題,那我是不是又要寫n篇文章來分別解決這些問題。往往現實卻是就是這麼殘酷,你解決了一個問題,引入了更多的問題,這就是為什麼bug永遠改不完的道理了。當然這不是我的風格,我的風格是利用有限的資源做盡可能多的事情(可能是由於我工作的企業都是那種資源貧瘠的,養成了我這種習慣,土豪公司的程序員請繞道,哈哈)。

  簡化一下問題:目前的問題就是如何讓正在執行任務的節點知道,然後停止正在執行的這個任務,其實就是這個停止通知如何實現。這不免讓我想起了12306網站上買票,其實我們作為老百姓多麼希望12306可以在有票的時候發個短信通知一下我們,然後我們上去搶,但是現實卻是,你要麼使用軟件一直刷,要麼是自己隔一段時間上去瞄一下有沒有票。如果把有票了給我們發短信通知定義為異步通知,那麼這種我們要隔一段時間自己去瞄一下的方式就是同步輪訓。這兩種方式都能達到告知的目的,關鍵的區別在於你到底有沒有時間去一直去瞄,不過相比於可以回家,這些時間都是值得的。個人認為軟件的設計其實就是一個權衡是否值得的過程。如果約定了不使用外部消息隊列這種異步通知的方式,那麼我們只能使用同步輪訓的方式了。不過正好我們的任務調度本身已經有一個心跳機制,沒隔一段時間就去更新一下節點狀態,如果我們把用戶的停止請求作為命令信息更新到每個節點的上,然後隨着心跳獲取到這個節點的信息,然後判斷這個命令,做相應的處理是不是就可以完美解決這個問題。值得嗎?很明顯是值得的,我們只是在心跳邏輯上加一個小小的副作用就實現了通知功能了。代碼如下

package com.rdpaas.task.common;

/**
 * @author rongdi
 * @date 2019/11/26
 */
public enum NotifyCmd {

    //沒有通知,默認狀態
    NO_NOTIFY(0),
    //開啟任務(Task)
    START_TASK(1),
    //修改任務(Task)
    EDIT_TASK(2),
    //停止任務(Task)
    STOP_TASK(3);

    int id;

    NotifyCmd(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public static NotifyCmd valueOf(int id) {
        switch (id) {
            case 1:
                return START_TASK;
            case 2:
                return EDIT_TASK;
            case 3:
                return STOP_TASK;
            default:
                return NO_NOTIFY;
        }
    }

}
package com.rdpaas.task.handles;

import com.rdpaas.task.common.NotifyCmd;
import com.rdpaas.task.utils.SpringContextUtil;

/**
 * @author: rongdi
 * @date:
 */
public interface NotifyHandler<T> {

    static NotifyHandler chooseHandler(NotifyCmd notifyCmd) {
        return SpringContextUtil.getByTypeAndName(NotifyHandler.class,notifyCmd.toString());
    }

    public void update(T t);

}
package com.rdpaas.task.handles;

import com.rdpaas.task.scheduler.TaskExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author: rongdi
 * @date:
 */
@Component("STOP_TASK")
public class StopTaskHandler implements NotifyHandler<Long> {

    @Autowired
    private TaskExecutor taskExecutor;

    @Override
    public void update(Long taskId) {
        taskExecutor.stop(taskId);
    }

}
class HeartBeat implements Runnable {
        @Override
        public void run() {
            for(;;) {
                try {
                    /**
                     * 時間到了就可以從延時隊列拿出節點對象,然後更新時間和序號,
                     * 最後再新建一個超時時間為心跳時間的節點對象放入延時隊列,形成循環的心跳
                     */
                    DelayItem<Node> item = heartBeatQueue.take();
                    if(item != null && item.getItem() != null) {
                        Node node = item.getItem();
                        handHeartBeat(node);
                    }
                    heartBeatQueue.offer(new DelayItem<>(config.getHeartBeatSeconds() * 1000,new Node(config.getNodeId())));
                } catch (Exception e) {
                    logger.error("task heart beat error,cause by:{} ",e);
                }
            }
        }
    }

    /**
     * 處理節點心跳
     * @param node
     */
    private void handHeartBeat(Node node) {
        if(node == null) {
            return;
        }
        /**
         * 先看看數據庫是否存在這個節點
         * 如果不存在:先查找下一個序號,然後設置到node對象中,最後插入
         * 如果存在:直接根據nodeId更新當前節點的序號和時間
         */
        Node currNode= nodeRepository.getByNodeId(node.getNodeId());
        if(currNode == null) {
            node.setRownum(nodeRepository.getNextRownum());
            nodeRepository.insert(node);
        } else  {
            nodeRepository.updateHeartBeat(node.getNodeId());
            NotifyCmd cmd = currNode.getNotifyCmd();
            String notifyValue = currNode.getNotifyValue();
            if(cmd != null && cmd != NotifyCmd.NO_NOTIFY) {
                /**
                 * 藉助心跳做一下通知的事情,比如及時停止正在執行的任務
                 * 根據指令名稱查找Handler
                 */
                NotifyHandler handler = NotifyHandler.chooseHandler(currNode.getNotifyCmd());
                if(handler == null || StringUtils.isEmpty(notifyValue)) {
                    return;
                }
                /**
                 * 執行操作
                 */
                handler.update(Long.valueOf(notifyValue));
            }
            
        }


    }

  最終的任務調度代碼如下:

package com.rdpaas.task.scheduler;

import com.rdpaas.task.common.Invocation;
import com.rdpaas.task.common.Node;
import com.rdpaas.task.common.NotifyCmd;
import com.rdpaas.task.common.Task;
import com.rdpaas.task.common.TaskDetail;
import com.rdpaas.task.common.TaskStatus;
import com.rdpaas.task.config.EasyJobConfig;
import com.rdpaas.task.repository.NodeRepository;
import com.rdpaas.task.repository.TaskRepository;
import com.rdpaas.task.strategy.Strategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 任務調度器
 * @author rongdi
 * @date 2019-03-13 21:15
 */
@Component
public class TaskExecutor {

    private static final Logger logger = LoggerFactory.getLogger(TaskExecutor.class);

    @Autowired
    private TaskRepository taskRepository;

    @Autowired
    private NodeRepository nodeRepository;

    @Autowired
    private EasyJobConfig config;

    /**
     * 創建任務到期延時隊列
      */
    private DelayQueue<DelayItem<Task>> taskQueue = new DelayQueue<>();

    /**
     * 可以明確知道最多只會運行2個線程,直接使用系統自帶工具就可以了
     */
    private ExecutorService bossPool = Executors.newFixedThreadPool(2);

    /**
     * 正在執行的任務的Future
     */
    private Map<Long,Future> doingFutures = new HashMap<>();

    /**
     * 聲明工作線程池
     */
    private ThreadPoolExecutor workerPool;
    
    /**
     * 獲取任務的策略
     */
    private Strategy strategy;


    @PostConstruct
    public void init() {
        /**
         * 根據配置選擇一個節點獲取任務的策略
         */
        strategy = Strategy.choose(config.getNodeStrategy());
        /**
         * 自定義線程池,初始線程數量corePoolSize,線程池等待隊列大小queueSize,當初始線程都有任務,並且等待隊列滿后
         * 線程數量會自動擴充最大線程數maxSize,當新擴充的線程空閑60s后自動回收.自定義線程池是因為Executors那幾個線程工具
         * 各有各的弊端,不適合生產使用
         */
        workerPool = new ThreadPoolExecutor(config.getCorePoolSize(), config.getMaxPoolSize(), 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(config.getQueueSize()));
        /**
         * 執行待處理任務加載線程
         */
        bossPool.execute(new Loader());
        /**
         * 執行任務調度線程
         */
        bossPool.execute(new Boss());
    
    }

    class Loader implements Runnable {

        @Override
        public void run() {
            for(;;) {
                try { 
                    /**
                     * 先獲取可用的節點列表
                     */
                    List<Node> nodes = nodeRepository.getEnableNodes(config.getHeartBeatSeconds() * 2);
                    if(nodes == null || nodes.isEmpty()) {
                        continue;
                    }
                    /**
                     * 查找還有指定時間(單位秒)才開始的主任務列表
                     */
                    List<Task> tasks = taskRepository.listNotStartedTasks(config.getFetchDuration());
                    if(tasks == null || tasks.isEmpty()) {
                        continue;
                    }
                    for(Task task:tasks) {
                        
                        boolean accept = strategy.accept(nodes, task, config.getNodeId());
                        /**
                         * 不該自己拿就不要搶
                         */
                        if(!accept) {
                            continue;
                        }
                        /**
                         * 先設置成待執行
                         */
                        task.setStatus(TaskStatus.PENDING);
                        task.setNodeId(config.getNodeId());
                        /**
                         * 使用樂觀鎖嘗試更新狀態,如果更新成功,其他節點就不會更新成功。如果其它節點也正在查詢未完成的
                         * 任務列表和當前這段時間有節點已經更新了這個任務,version必然和查出來時候的version不一樣了,這裏更新
                         * 必然會返回0了
                         */
                        int n = taskRepository.updateWithVersion(task);
                        Date nextStartTime = task.getNextStartTime();
                        if(n == 0 || nextStartTime == null) {
                            continue;
                        }
                        /**
                         * 封裝成延時對象放入延時隊列,這裏再查一次是因為上面樂觀鎖已經更新了版本,會導致後面結束任務更新不成功
                         */
                        task = taskRepository.get(task.getId());
                        DelayItem<Task> delayItem = new DelayItem<Task>(nextStartTime.getTime() - new Date().getTime(), task);
                        taskQueue.offer(delayItem);
                        
                    }
                    Thread.sleep(config.getFetchPeriod());
                } catch(Exception e) {
                    logger.error("fetch task list failed,cause by:{}", e);
                }
            }
        }
        
    }
    
    class Boss implements Runnable {
        @Override
        public void run() {
            for (;;) {
                try {
                     /**
                     * 時間到了就可以從延時隊列拿出任務對象,然後交給worker線程池去執行
                     */
                    DelayItem<Task> item = taskQueue.take();
                    if(item != null && item.getItem() != null) {
                        Task task = item.getItem();
                        /**
                         * 真正開始執行了設置成執行中
                         */
                        task.setStatus(TaskStatus.DOING);
                        /**
                         * loader線程中已經使用樂觀鎖控制了,這裏沒必要了
                         */
                        taskRepository.update(task);
                        /**
                         * 提交到線程池
                         */
                        Future future = workerPool.submit(new Worker(task));
                        /**
                         * 暫存在doingFutures
                         */
                        doingFutures.put(task.getId(),future);
                    }
                     
                } catch (Exception e) {
                    logger.error("fetch task failed,cause by:{}", e);
                }
            }
        }

    }

    class Worker implements Callable<String> {

        private Task task;

        public Worker(Task task) {
            this.task = task;
        }

        @Override
        public String call() {
            logger.info("Begin to execute task:{}",task.getId());
            TaskDetail detail = null;
            try {
                //開始任務
                detail = taskRepository.start(task);
                if(detail == null) return null;
                //執行任務
                task.getInvokor().invoke();
                //完成任務
                finish(task,detail);
                logger.info("finished execute task:{}",task.getId());
                /**
                 * 執行完后刪了
                 */
                doingFutures.remove(task.getId());
            } catch (Exception e) {
                logger.error("execute task:{} error,cause by:{}",task.getId(), e);
                try {
                    taskRepository.fail(task,detail,e.getCause().getMessage());
                } catch(Exception e1) {
                    logger.error("fail task:{} error,cause by:{}",task.getId(), e);
                }
            }
            return null;
        }

    }

    /**
     * 完成子任務,如果父任務失敗了,子任務不會執行
     * @param task
     * @param detail
     * @throws Exception
     */
    private void finish(Task task,TaskDetail detail) throws Exception {

        //查看是否有子類任務
        List<Task> childTasks = taskRepository.getChilds(task.getId());
        if(childTasks == null || childTasks.isEmpty()) {
            //當沒有子任務時完成父任務
            taskRepository.finish(task,detail);
            return;
        } else {
            for (Task childTask : childTasks) {
                //開始任務
                TaskDetail childDetail = null;
                try {
                    //將子任務狀態改成執行中
                    childTask.setStatus(TaskStatus.DOING);
                    childTask.setNodeId(config.getNodeId());
                    //開始子任務
                    childDetail = taskRepository.startChild(childTask,detail);
                    //使用樂觀鎖更新下狀態,不然這裏可能和恢複線程產生併發問題
                    int n = taskRepository.updateWithVersion(childTask);
                    if (n > 0) {
                        //再從數據庫取一下,避免上面update修改后version不同步
                        childTask = taskRepository.get(childTask.getId());
                        //執行子任務
                        childTask.getInvokor().invoke();
                        //完成子任務
                        finish(childTask, childDetail);
                    }
                } catch (Exception e) {
                    logger.error("execute child task error,cause by:{}", e);
                    try {
                        taskRepository.fail(childTask, childDetail, e.getCause().getMessage());
                    } catch (Exception e1) {
                        logger.error("fail child task error,cause by:{}", e);
                    }
                }
            }
            /**
             * 當有子任務時完成子任務后再完成父任務
             */
            taskRepository.finish(task,detail);

        }

    }

    /**
     * 添加任務
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addTask(String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        return taskRepository.insert(task);
    }

    /**
     * 添加子任務
     * @param pid
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addChildTask(Long pid,String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        task.setPid(pid);
        return taskRepository.insert(task);
    }

    /**
     * 立即執行任務,就是設置一下延時為0加入任務隊列就好了,這個可以外部直接調用
     * @param taskId
     * @return
     */
    public boolean startNow(Long taskId) {
        Task task = taskRepository.get(taskId);
        task.setStatus(TaskStatus.DOING);
        taskRepository.update(task);
        DelayItem<Task> delayItem = new DelayItem<Task>(0L, task);
        return taskQueue.offer(delayItem);
    }

    /**
     * 立即停止正在執行的任務,留給外部調用的方法
     * @param taskId
     * @return
     */
    public boolean stopNow(Long taskId) {
        Task task = taskRepository.get(taskId);
        if(task == null) {
            return false;
        }
        /**
         * 該任務不是正在執行,直接修改task狀態為已完成即可
         */
        if(task.getStatus() != TaskStatus.DOING) {
            task.setStatus(TaskStatus.STOP);
            taskRepository.update(task);
            return true;
        }
        /**
         * 該任務正在執行,使用節點配合心跳發布停用通知
         */
        int n = nodeRepository.updateNotifyInfo(NotifyCmd.STOP_TASK,String.valueOf(taskId));
        return n > 0;
    }

    /**
     * 立即停止正在執行的任務,這個不需要自己調用,是給心跳線程調用
     * @param taskId
     * @return
     */
    public boolean stop(Long taskId) {
        Task task = taskRepository.get(taskId);
        /**
         * 不是自己節點的任務,本節點不能執行停用
         */
        if(task == null || !config.getNodeId().equals(task.getNodeId())) {
            return false;
        }
        /**
         * 拿到正在執行任務的future,然後強制停用,並刪除doingFutures的任務
         */
        Future future = doingFutures.get(taskId);
        boolean flag =  future.cancel(true);
        if(flag) {
            doingFutures.remove(taskId);
            /**
             * 修改狀態為已停用
             */
            task.setStatus(TaskStatus.STOP);
            taskRepository.update(task);
        }
        /**
         * 重置通知信息,避免重複執行停用通知
         */
        nodeRepository.resetNotifyInfo(NotifyCmd.STOP_TASK);
        return flag;
    }
}

  好吧,其實實現很簡單,關鍵在於思路,不BB了,詳細代碼見: 在下告辭!

  

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

【集合系列】- 深入淺出的分析 WeakHashMap

一、摘要

在集合系列的第一章,咱們了解到,Map 的實現類有 HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties 等等。

本文主要從數據結構和算法層面,探討 WeakHashMap 的實現。

二、簡介

剛剛咱們也介紹了,在 Map 家族中,WeakHashMap 是一個很特殊的成員,它的特殊之處在於 WeakHashMap 里的元素可能會被 GC 自動刪除,即使程序員沒有显示調用 remove() 或者 clear() 方法。

換言之,當向 WeakHashMap 中添加元素的時候,再次遍歷獲取元素,可能發現它已經不見了,我們來看看下面這個例子。

public static void main(String[] args) {
        Map weakHashMap = new WeakHashMap();
        
        //向weakHashMap中添加4個元素
        for (int i = 0; i < 3; i++) {
            weakHashMap.put("key-"+i, "value-"+ i);
        }
        //輸出添加的元素
        System.out.println("數組長度:"+weakHashMap.size() + ",輸出結果:" + weakHashMap);
        
        //主動觸發一次GC
        System.gc();
        
        //再輸出添加的元素
        System.out.println("數組長度:"+weakHashMap.size() + ",輸出結果:" + weakHashMap);
    }

輸出結果:

數組長度:3,輸出結果:{key-2=value-2, key-1=value-1, key-0=value-0}
數組長度:3,輸出結果:{}

當主動調用 GC 回收器的時候,再次查詢 WeakHashMap 裏面的數據的時候,內容為空。

更直觀的說,當使用 WeakHashMap 時,即使沒有顯式的添加或刪除任何元素,也可能發生如下情況:

  • 調用兩次 size() 方法返回不同的值;
  • 兩次調用 isEmpty() 方法,第一次返回 false,第二次返回 true;
  • 兩次調用 containsKey() 方法,第一次返回 true,第二次返回 false,儘管兩次使用的是同一個key;
  • 兩次調用 get() 方法,第一次返回一個 value,第二次返回 null,儘管兩次使用的是同一個對象。

要明白 WeekHashMap 的工作原理,還需要引入一個概念:弱引用

我們都知道 Java 中內存是通過 GC 自動管理的,GC 會在程序運行過程中自動判斷哪些對象是可以被回收的,並在合適的時機進行內存釋放。

GC 判斷某個對象是否可被回收的依據是,是否有有效的引用指向該對象。如果沒有有效引用指向該對象(基本意味着不存在訪問該對象的方式),那麼該對象就是可回收的。

2.1、對象引用介紹

從 JDK1.2 版本開始,把對象的引用分為四種級別,從而使程序更加靈活的控制對象的生命周期。這四種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。

用表格整理之後,各個引用類型的區別如下:

2.1.1、強引用

強引用是使用最普遍的引用,例如,我們創建一個對象:

//強引用類型
Object object=new Object();

如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足, Java 虛擬機寧願拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。

如果不使用時,要手動通過如下方式來弱化引用,如下:

//將對象設置為null,幫助垃圾收集器回收此對象
object=null;

這個時候,GC 認為該對象不存在引用,就可以回收這個對象,具體什麼時候收集這要取決於 GC 的算法。

2.1.2、軟引用

SoftReference指向的對象,屬於軟引用,如下:

String str=new String("abc");

//軟引用
SoftReference<String> softRef=new SoftReference<String>(str);

如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會進入垃圾回收器,Java 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中,GC 進行回收處理。只要垃圾回收器沒有回收它,該對象就可以被程序使用。

當內存不足時,等價於:

If(JVM.內存不足()) {
   str = null;  // 轉換為軟引用
   System.gc(); // 垃圾回收器進行回收
}

軟引用的這種特性,比較適合內存敏感的場景,做高速緩存。在某些場景下,比如,系統內存不是很足的情況下,可以使用軟引用,GC 會自動回收,再次獲取對象的時候,可以對緩存對象進行重建,而又不影響使用。比如:

//創建一個緩存內容cache
String cache = new String("abc");

//進行軟引用處理
SoftReference<String> softRef=new SoftReference<String>(cache);

//判斷是否被垃圾回收器回收
if(softRef.get()!=null){
    //還沒有被回收器回收,直接獲取
    cache = (String) softRef.get();
}else{
    //由於內存吃緊,所以對軟引用的對象回收了
    //重建緩存對象
    cache = new String("abc");
    SoftReference<String> softRef = new SoftReference<String>(cache);
}
2.1.3、弱引用

WeakReference指向的對象,屬於弱引用,如下:

String str=new String("abc");

//弱引用
WeakReference<String> abcWeakRef = new WeakReference<String>(str);

弱引用與軟引用的區別在於:具有弱引用的對象擁有更短暫的生命周期。

在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。

當垃圾回收器進行掃描回收時,等價於:

str = null;
System.gc();

如果這個對象是偶爾的使用,並且希望在使用時隨時就能獲取到,但又不想影響此對象的垃圾收集,那麼你應該用 WeakReference 來記住此對象。

同樣的,弱引用對象進入垃圾回收器,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中,GC 進行回收處理。

2.1.4、虛引用

PhantomReference指向的對象,屬於虛引用。

虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列聯合使用,如下:

String str=new String("abc");

//創建引用隊列
ReferenceQueue<String> queue = new ReferenceQueue<String>();

//創建虛引用
PhantomReference<String> phantomReference = new PhantomReference<String>(str, queue);

虛引用,顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命周期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。

當垃圾回收器準備回收一個對象時,如果發現它是虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中,GC 進行回收處理。

2.1.5、總結

Java 4中引用的級別由高到低依次為:強引用 > 軟引用 > 弱引用 > 虛引用

用一張圖來看一下他們之間在垃圾回收時的區別:

再次回到本文要講的 WeakHashMap!

WeakHashMap 內部是通過弱引用來管理 entry 的,弱引用的特性對應到 WeakHashMap 上意味着什麼呢?將一對 key, value 放入到 WeakHashMap 里,隨時都有可能被 GC 回收。

下面,咱們一起來看看 WeakHashMap 的具體實現。

三、常用方法介紹

3.1、put方法

put 方法是將指定的 key, value 對添加到 map 里,存儲結構類似於 HashMap;
不同的是,WeakHashMap 中存儲的 Entry 繼承自 WeakReference,實現了弱引用。

打開源碼如下:

public V put(K key, V value) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length);

        for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
            if (h == e.hash && eq(k, e.get())) {
                V oldValue = e.value;
                if (value != oldValue)
                    e.value = value;
                return oldValue;
            }
        }

        modCount++;
        Entry<K,V> e = tab[i];
        tab[i] = new Entry<>(k, value, queue, h, e);
        if (++size >= threshold)
            resize(tab.length * 2);
        return null;
}

WeakHashMap 中存儲的 Entry,源碼如下:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;

        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
              
            //將key進行弱引用處理
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
        ......
}

需要注意的是,Entry 中super(key, queue),傳入的是key,因此key才是進行弱引用的,value是直接強引用關聯在this.value中,System.gc()時,對key進行了回收,而value依然保持。

value是何時被清除的呢?

閱讀源碼,可以看到,調用getTable()函數,對調用expungeStaleEntries()函數,該方法對 jvm 要回收的的 entry(quene 中) 進行遍歷,並將 entry 的 value 設置為空,進行內存回收。

private Entry<K,V>[] getTable() {
        expungeStaleEntries();
        return table;
}

expungeStaleEntries()函數,源碼如下:

private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);

                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table[i] = next;
                        else
                            prev.next = next;
                        //將value設置為null,方便GC回收
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
}

所以效果是 key 在 GC 的時候被清除,value 在 key 清除后,訪問數組內容的時候進行清除!

3.2、get方法

get 方法根據指定的 key 值返回對應的 value。

源碼如下:

public V get(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        //訪問數組內容
        Entry<K,V>[] tab = getTable();
        int index = indexFor(h, tab.length);
        Entry<K,V> e = tab[index];
        while (e != null) {
            //通過key,進行hash值和equals判斷
            if (e.hash == h && eq(k, e.get()))
                return e.value;
            e = e.next;
        }
        return null;
}

同樣的,get 方法在判斷對象之前,也調用了getTable()函數,同時,也調用了expungeStaleEntries()函數,所以,可能通過 key 獲取元素的時候,得到空值;如果 key 沒有被 GC 回收,那麼就返回對應的 value。

3.3、remove方法

remove 的作用是通過 key 刪除對應的元素。

源碼如下:

public V remove(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        
        //訪問數組內容
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length);
        Entry<K,V> prev = tab[i];
        Entry<K,V> e = prev;
        
        //循環鏈表,通過key,進行hash值和equals判斷
        while (e != null) {
            Entry<K,V> next = e.next;
            if (h == e.hash && eq(k, e.get())) {
                modCount++;
                size--;
                //找到之後,將鏈表後節點向前移動
                if (prev == e)
                    tab[i] = next;
                else
                    prev.next = next;
                return e.value;
            }
            prev = e;
            e = next;
        }

        return null;
}

同樣的,remove 方法在判斷對象之前,也調用了getTable()函數,同時,也調用了expungeStaleEntries()函數,所以,可能通過 key 獲取元素的時候,可能被垃圾回收器回收,得到空值。

四、總結

WeakHashMap 跟普通的 HashMap 不同,在存儲數據時,key被設置為弱引用類型,而弱引用類型在 java 中,可能隨時被 jvm 的 gc 回收,所以再次通過獲取對象時,可能得到空值,而value是在訪問數組內容的時候,進行清除。

可能很多人覺得這樣做很奇葩,其實不然,WeekHashMap 的這個特點特別適用於需要緩存的場景。

在緩存場景下,由於系統內存是有限的,不能緩存所有對象,可以使用 WeekHashMap 進行緩存對象,即使緩存丟失,也可以通過重新計算得到,不會造成系統錯誤。

五、參考

1、JDK1.7&JDK1.8 源碼

2、

3、

4、

作者:炸雞可樂
出處:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

d3.js 地鐵軌道交通項目實戰

上一章說了如何製作一個線路圖,當然上一章是手寫的JSON數據,當然手寫的json數據有非常多的好處,例如可以應對客戶的各種BT需求,但是大多數情況下我們都是使用地鐵公司現成的JSON文件,話不多說我們先看一下。

就是這樣的,今天我們就來完成它的大部分需求,以及地鐵公司爸爸提出來的需求。

需求如下:
1.按照不同顏色显示地鐵各線路,显示對應站點。
2.用戶可以點擊手勢縮放和平移(此項目為安卓開發)。
3.用戶在線路menu里點擊線路,對應線路平移值屏幕中心並高亮。
4.根據後台數據,渲染問題路段。
5.點擊問題路段站點,显示問題詳情。

大致需求就是這些,下面看看看代碼

1.定義一些常量和變量

const dataset = subwayData; //線路圖數據源
let subway = new Subway(dataset); //線路圖的類文件
let baseScale = 2; //基礎縮放倍率
let deviceScale = 1400 / 2640; //設備與畫布寬度比率
let width = 2640; //畫布寬
let height = 1760; //畫布高
let transX = 1320 + 260; //地圖X軸平移(將畫布原點X軸平移)
let transY = 580; //地圖X軸平移(將畫布原點Y軸平移)
let scaleExtent = [0.8, 4]; //縮放倍率限制
let currentScale = 2; //當前縮放值
let currentX = 0; //當前畫布X軸平移量
let currentY = 0; //當前畫布Y軸平移量
let selected = false; //線路是否被選中(在右上角的線路菜單被選中)
let scaleStep = 0.5; //點擊縮放按鈕縮放步長默認0.5倍
let tooltip = d3.select('#tooltip'); //提示框
let bugArray = []; //問題路段數組
let svg = d3.select('#sw').append('svg'); //畫布
let group = svg.append('g').attr('transform', `translate(${transX}, ${transY}) scale(1)`);//定義組並平移
let whole = group.append('g').attr('class', 'whole-line') //虛擬線路(用於點擊右上角響應線路可以定位當視野中心,方法不唯一)
let path = group.append('g').attr('class', 'path'); //定義線路
let point = group.append('g').attr('class', 'point'); //定義站點
const zoom = d3.zoom().scaleExtent(scaleExtent).on("zoom", zoomed); //定義縮放事件

這就是我們需要使用的一些常量和變量。注意transX不是寬度的一半,是因為北京地鐵線路網西線更密集。

2.讀官方JSON

使用d3.js數據必不可少,然而官方的數據並不通俗易懂,我們先解讀一下官方JSON數據。

每條線路對象都有一個l_xmlattr屬性和一個p屬性,l_xmlattr是整條線路的屬性,p是站點數組,我們看一下站點中我們需要的屬性。ex是否是中轉站,lb是站名,sid是站的id,rx、ry是文字偏移量,st是是否為站點(因為有的點不是站點而是為了渲染貝塞爾曲線用的),x、y是站點坐標。

3.構造自己的類方法

官方給了我們數據,但是並不是我們能直接使用的,所以我們需要構造自己的方法類

class Subway {
    constructor(data) {
        this.data = data;
        this.bugLineArray = [];
    }
    getInvent() {} //獲取虛擬線路數據
    getPathArray() {} //獲取路徑數據
    getPointArray() {} //獲取站點數組
    getCurrentPathArray() {} //獲取被選中線路的路徑數組
    getCurrentPointArray() {} //獲取被選中線路的站點數組
    getLineNameArray() {} // 獲取線路名稱數組
    getBugLineArray() {} //獲取問題路段數組
}

 

下面是我們方法內容,裏面的操作不是很優雅(大家將就看啦)
getInvent() {
    let lineArray = [];
    this.data.forEach(d => {
        let { loop, lc, lbx, lby, lb, lid} = d.l_xmlattr;
        let allPoints = d.p.slice(0);
        loop && allPoints.push(allPoints[0]);
        let path = this.formatPath(allPoints, 0, allPoints.length - 1);
        lineArray.push({
            lid: lid,
            path: path,
        })
    })
    return lineArray;
}
getPathArray() {
    let pathArray = [];
    this.data.forEach(d => {
        let { loop, lc, lbx, lby, lb, lid} = d.l_xmlattr;
        let allPoints = d.p.slice(0);
        loop && allPoints.push(allPoints[0])
        let allStations = [];
        allPoints.forEach((item, index) => item.p_xmlattr.st && allStations.push({...item.p_xmlattr, index}))
        let arr = [];
        for(let i = 0; i < allStations.length - 1; i++) {
            let path = this.formatPath(allPoints, allStations[i].index, allStations[i + 1].index);
            arr.push({
                lid: lid,
                id: `${allStations[i].sid}_${allStations[i + 1].sid}`,
                path: path,
                color: lc.replace(/0x/, '#')
            })
        }
        pathArray.push({
            path: arr,
            lc: lc.replace(/0x/, '#'),
            lb,lbx,lby,lid
        })
    })
    return pathArray;
}
getPointArray() {
    let pointArray = [];
    let tempPointsArray = [];
    this.data.forEach(d => {
        let {lid,lc,lb} = d.l_xmlattr;
        let allPoints = d.p;
        let allStations = [];
        allPoints.forEach(item => {
            if(item.p_xmlattr.st && !item.p_xmlattr.ex) {
                allStations.push({...item.p_xmlattr, lid, pn: lb, lc: lc.replace(/0x/, '#')})
            } else if (item.p_xmlattr.ex) {
                if(tempPointsArray.indexOf(item.p_xmlattr.sid) == -1) {
                    allStations.push({...item.p_xmlattr, lid, pn: lb, lc: lc.replace(/0x/, '#')})
                    tempPointsArray.push(item.p_xmlattr.sid);
                }
            }
        });
        pointArray.push(allStations);
    })
    return pointArray;
}
getCurrentPathArray(name) {
    let d = this.data.filter(d => d.l_xmlattr.lid == name)[0];
    let { loop, lc, lbx, lby, lb, lid} = d.l_xmlattr;
    let allPoints = d.p.slice(0);
    loop && allPoints.push(allPoints[0])
    let allStations = [];
    allPoints.forEach((item, index) => item.p_xmlattr.st && allStations.push({...item.p_xmlattr, index}))
    let arr = [];
    for(let i = 0; i < allStations.length - 1; i++) {
        let path = this.formatPath(allPoints, allStations[i].index, allStations[i + 1].index);
        arr.push({
            lid: lid,
            id: `${allStations[i].sid}_${allStations[i + 1].sid}`,
            path: path,
            color: lc.replace(/0x/, '#')
        })
    }
    return {
        path: arr,
        lc: lc.replace(/0x/, '#'),
        lb,lbx,lby,lid
    }
}
getCurrentPointArray(name) {
    let d = this.data.filter(d => d.l_xmlattr.lid == name)[0];
    let {lid,lc,lb} = d.l_xmlattr;
    let allPoints = d.p;
    let allStations = [];
    allPoints.forEach(item => {
        if(item.p_xmlattr.st && !item.p_xmlattr.ex) {
            allStations.push({...item.p_xmlattr, lid, pn: lb, lc: lc.replace(/0x/, '#')})
        } else if (item.p_xmlattr.ex) {
            allStations.push({...item.p_xmlattr, lid, pn: lb, lc: lc.replace(/0x/, '#')})
        }
    });
    return allStations;
}
getLineNameArray() {
    let nameArray = this.data.map(d => {
        return {
            lb: d.l_xmlattr.lb,
            lid: d.l_xmlattr.lid,
            lc: d.l_xmlattr.lc.replace(/0x/, '#')
        }
    })
    return nameArray;
}
getBugLineArray(arr) {
    if(!arr || !arr.length) return [];
    this.bugLineArray = [];
    arr.forEach(item => {
        let { start, end, cause, duration, lid, lb } = item;
        let lines = [];
        let points = [];
        let tempObj = this.data.filter(d => d.l_xmlattr.lid == lid)[0];
        let loop = tempObj.l_xmlattr.loop;
        let lc = tempObj.l_xmlattr.lc;
        let allPoints = tempObj.p;
        let allStations = [];
        allPoints.forEach(item => {
            if(item.p_xmlattr.st) {
                allStations.push(item.p_xmlattr.sid)
            }
        });
        loop && allStations.push(allStations[0]);
        for(let i=allStations.indexOf(start); i<=allStations.lastIndexOf(end); i++) {
            points.push(allStations[i])
        }
        for(let i=allStations.indexOf(start); i<allStations.lastIndexOf(end); i++) {
            lines.push(`${allStations[i]}_${allStations[i+1]}`)
        }
        this.bugLineArray.push({cause,duration,lid,lb,lines,points,lc: lc.replace(/0x/, '#'),start: points[0],end:points[points.length - 1]});
    })
    return this.bugLineArray;
這種方法大家也不必看懂,知道傳入了什麼,輸入了什麼即可,這就是我們的方法類。

4.d3渲染畫布並添加方法

這裡是js的核心代碼,既然class文件都寫完了,這裏的操作就方便了很多,主要就是下面幾個人方法,
renderInventLine(); //渲染虛擬新路
renderAllStation(); //渲染所有的線路名稱(右上角)
renderBugLine(); //渲染問題路段
renderAllLine(); //渲染所有線路
renderAllPoint(); //渲染所有點
renderCurrentLine() //渲染當前選中的線路
renderCurrentPoint() //渲染當前選中的站點
zoomed() //縮放時執行的方法
getCenter() //獲取虛擬線中心點的坐標
scale() //點擊縮放按鈕時執行的方法
下面是對應的方法體
svg.call(zoom);
svg.call(zoom.transform, d3.zoomIdentity.translate((1 - baseScale) * transX, (1 - baseScale) * transY).scale(baseScale));

let pathArray = subway.getPathArray();
let pointArray = subway.getPointArray();

renderInventLine();
renderAllStation();
renderBugLine();

function renderInventLine() {
    let arr = subway.getInvent();
    whole.selectAll('path')
    .data(arr)
    .enter()
    .append('path')
    .attr('d', d => d.path)
    .attr('class', d => d.lid)
    .attr('stroke', 'none')
    .attr('fill', 'none')
}

function renderAllLine() {
    for (let i = 0; i < pathArray.length; i++) {
        path.append('g')
        .selectAll('path')
        .data(pathArray[i].path)
        .enter()
        .append('path')
        .attr('d', d => d.path)
        .attr('lid', d => d.lid)
        .attr('id', d => d.id)
        .attr('class', 'lines origin')
        .attr('stroke', d => d.color)
        .attr('stroke-width', 7)
        .attr('stroke-linecap', 'round')
        .attr('fill', 'none')
        path.append('text')
        .attr('x', pathArray[i].lbx)
        .attr('y', pathArray[i].lby)
        .attr('dy', '1em')
        .attr('dx', '-0.3em')
        .attr('fill', pathArray[i].lc)
        .attr('lid', pathArray[i].lid)
        .attr('class', 'line-text origin')
        .attr('font-size', 14)
        .attr('font-weight', 'bold')
        .text(pathArray[i].lb)
    }
}

function renderAllPoint() {
    for (let i = 0; i < pointArray.length; i++) {
        for (let j = 0; j < pointArray[i].length; j++) {
            let item = pointArray[i][j];
            let box = point.append('g');
            if (item.ex) {
                box.append('image')
                .attr('href', './trans.png')
                .attr('class', 'points origin')
                .attr('id', item.sid)
                .attr('x', item.x - 8)
                .attr('y', item.y - 8)
                .attr('width', 16)
                .attr('height', 16)
            } else {
                box.append('circle')
                .attr('cx', item.x)
                .attr('cy', item.y)
                .attr('r', 5)
                .attr('class', 'points origin')
                .attr('id', item.sid)
                .attr('stroke', item.lc)
                .attr('stroke-width', 1.5)
                .attr('fill', '#ffffff')
            }
            box.append('text')
            .attr('x', item.x + item.rx)
            .attr('y', item.y + item.ry)
            .attr('dx', '0.3em')
            .attr('dy', '1.1em')
            .attr('font-size', 11)
            .attr('class', 'point-text origin')
            .attr('lid', item.lid)
            .attr('id', item.sid)
            .text(item.lb)
        }
    }
}

function renderCurrentLine(name) {
    let arr = subway.getCurrentPathArray(name);
    path.append('g')
    .attr('class', 'temp')
    .selectAll('path')
    .data(arr.path)
    .enter()
    .append('path')
    .attr('d', d => d.path)
    .attr('lid', d => d.lid)
    .attr('id', d => d.id)
    .attr('stroke', d => d.color)
    .attr('stroke-width', 7)
    .attr('stroke-linecap', 'round')
    .attr('fill', 'none')
    path.append('text')
    .attr('class', 'temp')
    .attr('x', arr.lbx)
    .attr('y', arr.lby)
    .attr('dy', '1em')
    .attr('dx', '-0.3em')
    .attr('fill', arr.lc)
    .attr('lid', arr.lid)
    .attr('font-size', 14)
    .attr('font-weight', 'bold')
    .text(arr.lb)
}

function renderCurrentPoint(name) {
    let arr = subway.getCurrentPointArray(name);
    for (let i = 0; i < arr.length; i++) {
        let item = arr[i];
        let box = point.append('g').attr('class', 'temp');
        if (item.ex) {
            box.append('image')
            .attr('href', './trans.png')
            .attr('x', item.x - 8)
            .attr('y', item.y - 8)
            .attr('width', 16)
            .attr('height', 16)
            .attr('id', item.sid)
        } else {
            box.append('circle')
            .attr('cx', item.x)
            .attr('cy', item.y)
            .attr('r', 5)
            .attr('id', item.sid)
            .attr('stroke', item.lc)
            .attr('stroke-width', 1.5)
            .attr('fill', '#ffffff')
        }
        box.append('text')
        .attr('class', 'temp')
        .attr('x', item.x + item.rx)
        .attr('y', item.y + item.ry)
        .attr('dx', '0.3em')
        .attr('dy', '1.1em')
        .attr('font-size', 11)
        .attr('lid', item.lid)
        .attr('id', item.sid)
        .text(item.lb)
    }
}

function renderBugLine(modal) {
    let bugLineArray = subway.getBugLineArray(modal);
    d3.selectAll('.origin').remove();
    renderAllLine();
    renderAllPoint();
    bugLineArray.forEach(d => {
        console.log(d)
        d.lines.forEach(dd => {
            d3.selectAll(`path#${dd}`).attr('stroke', '#eee');
        })
        d.points.forEach(dd => {
            d3.selectAll(`circle#${dd}`).attr('stroke', '#ddd')
            d3.selectAll(`text#${dd}`).attr('fill', '#aaa')
        })
    })
    d3.selectAll('.points').on('click', function () {
        let id = d3.select(this).attr('id');
        let bool = judgeBugPoint(bugLineArray, id);
        if (bool) {
            let x, y;
            if (d3.select(this).attr('href')) {
                x = parseFloat(d3.select(this).attr('x')) + 8;
                y = parseFloat(d3.select(this).attr('y')) + 8;
            } else {
                x = d3.select(this).attr('cx');
                y = d3.select(this).attr('cy');
            }
            let toolX = (x * currentScale + transX - ((1 - currentScale) * transX - currentX)) * deviceScale;
            let toolY = (y * currentScale + transY - ((1 - currentScale) * transY - currentY)) * deviceScale;
            let toolH = document.getElementById('tooltip').offsetHeight;
            let toolW = 110;
            if (toolY < 935 / 2) {
                tooltip.style('left', `${toolX - toolW}px`).style('top', `${toolY + 5}px`);
            } else {
                tooltip.style('left', `${toolX - toolW}px`).style('top', `${toolY - toolH - 5}px`);
            }
        }
    });
}

function judgeBugPoint(arr, id) {
    if (!arr || !arr.length || !id) return false;
    let bugLine = arr.filter(d => {
        return d.points.indexOf(id) > -1
    });
    if (bugLine.length) {
        removeTooltip()
        tooltip.select('#tool-head').html(`<span>${id}</span><div class="deletes" onclick="removeTooltip()">×</div>`);
        bugLine.forEach(d => {
            let item = tooltip.select('#tool-body').append('div').attr('class', 'tool-item');
            item.html(`
                <div class="tool-content">
                    <div style="color: #ffffff;border-bottom: 2px solid ${d.lc};">
                        <span style="background: ${d.lc};padding: 4px 6px;">${d.lb}</span>
                    </div>
                    <div>
                        <div class="content-left">封路時間</div><div class="content-right">${d.duration}</div>
                    </div>
                    <div>
                        <div class="content-left">封路原因</div><div class="content-right">${d.cause}</div>
                    </div>
                    <div>
                        <div class="content-left">封路路段</div><div class="content-right">${d.start}-${d.end}</div>
                    </div>
                </div>
            `)
        })
        d3.select('#tooltip').style('display', 'block');
        return true;
    } else {
        return false;
    }
}

function removeTooltip() {
    d3.selectAll('.tool-item').remove();
    d3.select('#tooltip').style('display', 'none');
}

function zoomed() {
    removeTooltip();
    let {x, y, k} = d3.event.transform;
    currentScale = k;
    currentX = x;
    currentY = y;
    group.transition().duration(50).ease(d3.easeLinear).attr("transform", () => `translate(${x + transX * k}, ${y + transY * k}) scale(${k})`)
}

function getCenter(str) {
    if (!str) return null;
    let x, y;
    let tempArr = [];
    let tempX = [];
    let tempY = [];
    str.split(' ').forEach(d => {
        if (!isNaN(d)) {
            tempArr.push(d)
        }
    })

    tempArr.forEach((d, i) => {
        if (i % 2 == 0) {
            tempX.push(parseFloat(d))
        } else {
            tempY.push(parseFloat(d))
        }
    })
    x = (d3.min(tempX) + d3.max(tempX)) / 2;
    y = (d3.min(tempY) + d3.max(tempY)) / 2;
    return [x, y]
}

function renderAllStation() {
    let nameArray = subway.getLineNameArray();
    let len = Math.ceil(nameArray.length / 5);
    let box = d3.select('#menu').append('div')
    .attr('class', 'name-box')
    for (let i = 0; i < len; i++) {
        let subwayCol = box.append('div')
        .attr('class', 'subway-col')
        let item = subwayCol.selectAll('div')
        .data(nameArray.slice(i * 5, (i + 1) * 5))
        .enter()
        .append('div')
        .attr('id', d => d.lid)
        .attr('class', 'name-item')
        item.each(function (d) {
            d3.select(this).append('span').attr('class', 'p_mark').style('background', d.lc);
            d3.select(this).append('span').attr('class', 'p_name').text(d.lb);
            d3.select(this).on('click', d => {
                selected = true;
                d3.selectAll('.origin').style('opacity', 0.1);
                d3.selectAll('.temp').remove();
                renderCurrentLine(d.lid);
                renderCurrentPoint(d.lid);
                let arr = getCenter(d3.select(`path.${d.lid}`).attr('d'));
                svg.call(zoom.transform, d3.zoomIdentity.translate((width / 2 - transX) - arr[0] - (arr[0] + transX) * (currentScale - 1), (height / 2 - transY) - arr[1] - (arr[1] + transY) * (currentScale - 1)).scale(currentScale));
            })
        })
    }
}

function scale(type) {
    if (type && currentScale + scaleStep <= scaleExtent[1]) {
        svg.call(zoom.transform, d3.zoomIdentity.translate((1 - currentScale - scaleStep) * transX - ((1 - currentScale) * transX - currentX) * (currentScale + scaleStep) / currentScale, (1 - currentScale - scaleStep) * transY - ((1 - currentScale) * transY - currentY) * (currentScale + scaleStep) / currentScale).scale(currentScale + scaleStep));
    } else if (!type && currentScale - scaleStep >= scaleExtent[0]) {
        svg.call(zoom.transform, d3.zoomIdentity.translate((1 - (currentScale - scaleStep)) * transX - ((1 - currentScale) * transX - currentX) * (currentScale - scaleStep) / currentScale, (1 - (currentScale - scaleStep)) * transY - ((1 - currentScale) * transY - currentY) * (currentScale - scaleStep) / currentScale).scale(currentScale - scaleStep));
    }
}

上面是大部分代碼,想看全部的可以查看demo。

原文鏈接

大家轉載請註明一下原文 謝謝大家

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※公開收購3c價格,不怕被賤賣!

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

關係型數據庫幾大範式的理解總結

範式的定義

  • 關係型數據庫中的關係是需要滿足一定條件的,滿足這些不同程度的規範化就叫做範式。

  • 範式按照規範化程度從低到高排序為第一範式,第二範式,第三範式,BC範式,第四範式,第五範式。

前導知識

函數依賴

R(U)是屬性集U的關係模型,X,Y是U的一個子集,對於R(U)中的任一個關係r,不可能存在兩個元組在X上屬性值相同,而在Y上屬性值不同。則稱X函數確定Y,或Y函數依賴X。

  • 說人話:U是表(可能不止一個表,可以是有關係的多個表)的所有列,X,Y分別是這些屬性列的一個子集,也就是若干個屬性,對於所有在X這些屬性上的值一樣的行,在Y上的屬性上也必須一樣,滿足這樣條件的這若干個屬性 X和Y叫稱其函數依賴。
  • X相同則Y必須相同,但X不同Y可以相同,也可以不同
  • 如果Y是X的子集,就叫平凡的函數依賴,一般不考慮這種,因為就是廢話,X整個都相同,子集肯定相同。
  • 如果Y不是X的子集,叫做非平凡的函數依賴
  • 如果Y函數依賴X,那麼X稱為決定因素。
  • 如果Y函數依賴X,但不依賴X的任何一個真子集,也就是X是極小的,那就稱Y完全函數依賴X,否則稱Y部分函數依賴X
  • 如果X決定Y,Y決定Z,且Y不決定X,那麼稱Z對X傳遞函數依賴

碼(鍵)

  • U是屬性全集,K是U的子集,若U完全函數依賴K,則稱K為候選碼,候選碼若有多個,任意選擇一個都可作為主碼,若U部分函數依賴K,則稱K為超碼。顯然,候選碼當然也是超碼,而且是最小的超碼。
  • 包含在任何一個候選碼的屬性都叫主屬性,其他都叫非主屬性
  • 在本書中主碼和候選碼統稱為,屬性集K不是該關係模式(表)的碼,而是另一個關係模式(表)的碼,則稱K為該關係模式(表)的外碼

求候選碼

例子:

舉例

有這樣一個配件管理表WPE(WNO,PNO,ENO,QNT),其中WNO表示倉庫號,PNO表示配件號,ENO表示職工號,QNT表示數量。

有以下約束要求:

(1)一個倉庫有多名職工;

(2)一個職工僅在一個倉庫工作;

(3)每個倉庫里一種型號的配件由專人負責,但一個人可以管理幾種配件;

(4)同一種型號的配件可以分放在幾個倉庫中。

分析表中的函數依賴關係,可以得到:

(1)ENO->WNO;

(2)(WNO,PNO)->QNT

(3)(WNO,PNO)->ENO

(4)(ENO,PNO)->QNT

觀察法?:

候選碼的定義就是一組能決定所有列(某一個元組)的屬性。

所以根據這4個函數依賴關係,(WNO,PNO)顯然肯定是,因為它可以決定QNT,也可以決定ENO,加上它本身,就是屬性全集U了。

而(ENO,PNO),雖然只有一個決定QNT,但是ENO可以單獨決定WNO,所以顯然(ENO,PNO)也就能一起決定QNT和WNO,因此也是候選碼。

六大範式?

第一範式

定義

滿足最基本的條件,每一個分量都是不可分的數據項。

  • 說人話,每一列對應只有一個值。

第二範式

定義

R屬於第一範式,且每一個非主屬性完全函數依賴於任何一個候選碼,則R屬於第二範式

  • 說人話,除了主碼候選碼之外的其他屬性都要完全函數依賴於主碼。
  • 因為任意一個候選碼都能作為主碼,所以,也就是說,如果存在某個屬性不是完全函數依賴於某一個候選碼,可能是部分函數依賴,那就沒了。
  • 比如主鍵是(學號,課程號),但是現在有一個屬性完全函數依賴於學號,而部分函數依賴於(學號,課程號),那就不滿足第二範式。

第三範式

定義

R屬於第二範式,若R中不存在碼X,屬性子集Y,非主屬性Z,使得X決定Y,Y不決定X,Y決定Z,則R屬於第三範式。

  • 說人話,非主屬性必須直接完全函數依賴於主鍵,中間不能有其他函數,即不能是傳遞函數依賴。

BC範式

定義

R屬於第一範式,若X決定Y,且Y不是X的子集時X必含有碼,即每一個決定因素都包含碼,則R屬於BC範式。

  • 說人話, 若R是第一範式,且每個屬性不部分函數依賴於候選碼也不傳遞函數依賴於候選碼,則R是BC範式,具體以下三點。
    • 所有非主屬性對每一個碼都是完全函數依賴。(也是第二範式要求)
    • 所有主屬性對每一個不包含它的碼也是完全函數依賴。(也就是排除了所有屬性對碼的部分依賴)
    • 沒有任何屬性完全函數依賴於非碼的任何一組屬性。(排除傳遞函數依賴)
  • 實際上,BC範式就是在第三範式的基礎上消除了主屬性的傳遞依賴

第四範式

多值依賴

  • 說人話,多值依賴就是一個表中多對多的關係,如果可以分成兩列,這兩列多對多,這就平凡的多值依賴,如果是分成三列,固定某一列的值,其他兩列多對多,這就是非平凡的多值依賴,第四範式要消除的就是非平凡的多值依賴。

  • 函數依賴是特殊的多值依賴,因為多對多其實也是一對多。

定義

R屬於第一範式,對應R的每一個非平凡多值依賴,X->->Y,X都含有碼,則R屬於第四範式。

  • 說人話,在滿足第三範式的基礎上,關係表中不能含有一個實體的兩個或多個相互獨立的多值因子。
  • 或者說,滿足第四範式即要求每個非平凡的多值依賴都含有碼,也就是實際上是函數依賴。

第五範式

定義

第五範式是指關係模式R依賴均由R候選碼所隱含。

這輩子應該不會用到的內容,就不管了。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

Spring中常見的設計模式——工廠模式

一、簡單工廠模式

  簡單工廠模式(Simple Factory Pattern)由一個工廠對象決定創建哪一種產品類的實例,簡單工廠模式適用於工廠類負責創建對象較少的情況,且客戶端只需要傳入工廠類的參數,對於如何創建對像不關心。

public  interface IBlog {
     // 寫隨筆
    public  void write();
}
public  class JavaBlog implements IBlog {
    @Override
    public  void write() {
        System.out.println( "寫java隨筆" );
    }
}
public  class WriteBlog {
     public  static  void main(String[] args) {
        IBlog blog = new JavaBlog();
        blog.write();
    }
}

  上述代碼中,父類 IBlog 指向子類JavaBlog 的引用,應用層需要依賴JavaBlog,如果增加PythonBlog等等更多的課程,客戶端就會越來越臃腫。因此要把依賴減弱,把創建細節隱藏。現在我們用簡單工廠優化:

public class BlogFactory {
    public IBlog create(Class<? extends IBlog> clazz) {
        if (null != clazz) {
            try {
                return clazz.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

客戶端改變:

  public static void main(String[] args) {
        BlogFactory blogFactory = new BlogFactory();
        IBlog blog = blogFactory.create(JavaBlog. class );
        blog.write();
    }

  簡單工廠模式在JDK中很常見,如Calender類(感興趣去看源碼),還有logback,LoggerFactory中有很多重載的方法getLogger()。但是簡單工廠也有缺點:工廠類的職責相對過重,不易於擴展過於複雜的產品結構。

二、工廠方法模式

   工廠方法模式(Factory Method Pattern)是指定義一個創建對象的接口,但讓實現這個接口的類來決定實例化哪個類,工廠方法模式讓類的實例化推遲到子類中進行。在工廠方法模式中,用戶只需要關心所需產品對應工廠,無須關心創建的細節,而且加入新產品時符合開閉原則。

  工廠方法模式主要解決產品擴展問題。在簡單工廠模式中,隨著產品的增多,如果不同語言書寫隨筆的邏輯各不相同,工廠職責越來越多,那工廠裏面就會亂搞一氣,狗屁不通。根據單一職責原則,我們將只能進行拆分,不同工廠做不同事,Java隨筆由Java工廠創建,Python隨筆由Python工廠創建,對工廠本身進行抽象。

先創建工廠類:

public  interface IBlogFactory {
    IBlog create();
}

再創建對應工廠:

public  class JavaBlogFactory implements IBlogFactory {
    @Override
    public IBlog create() {
        return new JavaBlog();
    }
}

public class PythonBlogFactory implements IBlogFactory {
    @Override
    public IBlog create() {
        return new PythonBlog();
    }
}

客戶端:

public class CreateBlog {
    public static void main(String[] args) {
        IBlogFactory factory = new PythonBlogFactory();
        IBlog blog = factory.create();
        blog.write();

        factory = new JavaBlogFactory();
        blog = factory.create();
        blog.write();
    }
}

總結來說就是:不同工廠抽像出一個工廠頭子,不同的工廠創建不同的實例。

工廠方法模式適用於以下場景:

1.創建對象需要大量重複代碼。

2.客戶端(應用層)不依賴於產品類實例如何被創建、如何被實現等細節。

3.一個類通過其子類來指定創建哪個對象。

缺點:

1.類的個數容易過多,增加複雜度。

2.增加了系統的抽象性和理解難度。

三、抽象工廠

  抽象工廠(Abstract Factory Pattern)提供一個黃健一系列相關或相互依賴對象的接口,無需指定具體類。客戶端(應用層)不依賴於產品類實例如何被創建、如何被實現等細節,強調的是一系列相關得產品對象(屬於同一產品族)一起使用創建對象需要大量重複代碼。需要提供一個產品類的庫,所有產品以同樣接口出現,從而是客戶端不依賴於具體實現。

產品族:同一家的不同產品,比如小米,華為,蘋果;

產品等級:不同種類的產品,比如手機,電視,電腦。

工廠要做的就是生產我們牌子的所有產品。以博客為例,java分類的博客有隨筆、文章、日記等。

首先創建文章和日記的抽象接口:

public  interface IDocument {
     void write();
}

public  interface INote {
     void make();
}

再創建抽象工廠:

public  interface BlogFactory {
    INote createNote();

    IDocument createDocument();
}

實現Java文章和日記:

public  class JavaDocument implements IDocument {
    @Override
    public  void write() {
        System.out.println( "寫Java文章" );
    }
}

public  class JavaNote implements INote {
    @Override
    public  void make() {
        System.out.println( "寫Java筆記" );
    }
}

實現Java產品族具體工廠:

public  class JavaBlogFactory implements BlogFactory {
    @Override
    public INote createNote() {
         return  new JavaNote();
    }

    @Override
    public IDocument createDocument() {
         return  new JavaDocument();
    }
}

實現Python文章和日記、實現Python具體工廠參考Java的。

客戶端調用:

public  class BlogTest {
     public  static  void main(String[] args) {
        JavaBlogFactory factory = new JavaBlogFactory();
        factory.createDocument().write();
        factory.createNote().make();
    }
}

  上述代碼描述了兩個產品族的工廠,如果想要擴展產品等級(就是再加點評啥的),要調整抽象工廠、具體工廠。由此可見抽象工廠模式的缺點:

1.規定所有可能被創建的產品集合,產品族(Java系列)中擴展新產品很困難,需要修改抽象工廠及實現;

2.增加系統抽象性和理解難度;

  我們可以利用工廠模式創建好數據源連接池並放到容器中,業務需要時再取出。就避免了用一次創建一次的尷尬。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

二、從零開始搭建自己的靜態博客 — 主題篇

我們已經成功地在本地搭建了一個博客網站,它使用的是pelican默認的notmyidea主題;

如果你不太記得了,可以再看看這篇文章:;

其實,pelican擁有眾多的開源主題庫,我們可以在上選擇一個自己喜歡的主題應用到項目中;

網站提供在線預覽主題的功能;

我選擇的是主題,它的在線Demo是:;

下面,我們來一步一步的將其應用到我們的項目中;

1. 下載主題

我粗略的瀏覽了一下pelican-alchemy的文檔和issue列表,考慮到後續有可能會做一些修改,所以我決定先將其fork到自己的倉庫;

然後,我在項目根目錄新建一個目錄themes/用於存放所有下載的主題,然後將fork後的pelican-alchemy作為一個獨立的子倉庫克隆到目錄下:

λ mkdir themes
λ git submodule add git@github.com:luizyao/pelican-alchemy.git themes/pelican-alchemy

注意:

git submodule add <url> <path>命令是將一個倉庫添加到指定的目錄下作為獨立的子倉庫;

如果你仔細觀察,會發現我們的根目錄下多了一個文件:.gitmodules,它記錄了子倉庫的信息;

例如:我們項目中這個文件的內容是:

[submodule "themes/pelican-alchemy"]
    path = themes/pelican-alchemy
    url = git@github.com:luizyao/pelican-alchemy.git

常用的和子倉庫的相關的操作有下面幾個:

  • 克隆父倉庫時,連同子倉庫一起克隆:

    git clone --recurse-submodules <URL> <directory>
  • 查看父倉庫中所有子倉庫的狀態:

    λ git submodule status
    3381c5031bf30d3b1212619b662898f178d695f1 themes/pelican-alchemy (v2.1-43-g3381c50)

    3381c5031bf30d3b1212619b662898f178d695f1是對當前Commit IdSHA-1加密字串;

  • 刪除子倉庫:

    git rm <submodule path> && git commit

    再手動刪除.git/modules/<name>/目錄

如果你想了解更多關於git submodule的內容,可以通過git submodule --help閱讀它的官方文檔;

2. 使用主題

2.1. 基本配置

# pelicanconf.py

# 主題所在的相對目錄
THEME = 'themes/pelican-alchemy/alchemy'

# 副標題
SITESUBTITLE = '戒驕戒躁 砥礪前行'

# 頭像
SITEIMAGE = '/images/profile.png width=200 height=200'

# 友鏈
LINKS = (
    ('pytest-chinese-doc', 'https://luizyao.github.io/pytest-chinese-doc/'),
)

# 代碼高亮的樣式
PYGMENTS_STYLE = 'friendly'

# 使用 Bootswatch 樣式:https://bootswatch.com/
BOOTSTRAP_CSS = 'https://cdn.bootcss.com/bootswatch/4.3.1/lux/bootstrap.min.css'

# 生成 sitemap.xml 文件,它是一個對爬蟲友好的文件,方便搜索引擎抓取網站頁面
DIRECT_TEMPLATES = ['index', 'tags', 'categories', 'authors', 'archives', 'sitemap']
SITEMAP_SAVE_AS = 'sitemap.xml'

# 構建后的 html 文件路徑和 URL 標識
ARTICLE_URL = 'posts/{date:%Y}/{date:%m}/{slug}.html'
ARTICLE_SAVE_AS = ARTICLE_URL
DRAFTS_URL = 'drafts/{date:%Y}/{date:%m}/{slug}.html'
DRAFTS_SAVE_AS = ARTICLE_URL
PAGE_URL = 'pages/{slug}.html'
PAGE_SAVE_AS = PAGE_URL

# RSS 訂閱
FEED_ALL_RSS = 'feeds/all.rss.xml'

具體細節可以參考:

2.2. 高級配置

2.2.1. 配置網站圖標

通過在線工具可以生成適配各種平台和瀏覽器的favicon文件:

下載上面生成的favicon包,並解壓到項目content/extras目錄下:

λ ls content/extras/
android-chrome-192x192.png  favicon.ico         safari-pinned-tab.svg
android-chrome-384x384.png  favicon-16x16.png   site.webmanifest
apple-touch-icon.png        favicon-32x32.png
browserconfig.xml           mstile-150x150.png

修改模版中的base.html文件:

<!-- themes/pelican-alchemy/alchemy/templates/base.html --> 

{% if RFG_FAVICONS %}
  <link rel="apple-touch-icon" href="{{ SITEURL }}/apple-touch-icon.png" sizes="180x180">
  <link rel="icon" type="image/png" href="{{ SITEURL }}/favicon-32x32.png" sizes="32x32">
  <link rel="icon" type="image/png" href="{{ SITEURL }}/favicon-16x16.png" sizes="16x16">
  <link rel="manifest" href="{{ SITEURL }}/manifest.json">
  <meta name="theme-color" content="#333333">
{% endif %}

<!-- 改成 --> 

{% if RFG_FAVICONS %}
  <link rel="apple-touch-icon" href="{{ SITEURL }}/apple-touch-icon.png" sizes="180x180">
  <link rel="icon" type="image/png" href="{{ SITEURL }}/favicon-32x32.png" sizes="32x32">
  <link rel="icon" type="image/png" href="{{ SITEURL }}/favicon-16x16.png" sizes="16x16">
  <link rel="manifest" href="{{ SITEURL }}/site.webmanifest">
  <link rel="mask-icon" href="{{ SITEURL }}/safari-pinned-tab.svg" color="#5bbad5">
  <meta name="msapplication-TileColor" content="#da532c">
  <meta name="theme-color" content="#ffffff">
{% endif %}

修改pelicanconf.py配置文件:

# pelicanconf.py

# 在構建中,它們會無損的拷貝到 output 的同名目錄下
STATIC_PATHS = ['extras', 'images', 'css']

# 構建時,extras/android-chrome-192x192.png文件,拷貝到output/android-chrome-192x192.png,不再是output/extras/android-chrome-192x192.png
EXTRA_PATH_METADATA = {
    'extras/android-chrome-192x192.png': {'path': 'android-chrome-192x192.png'},
    'extras/android-chrome-512x512.png': {'path': 'android-chrome-512x512.png'},
    'extras/apple-touch-icon.png': {'path': 'apple-touch-icon.png'},
    'extras/browserconfig.xml': {'path': 'browserconfig.xml'},
    'extras/favicon-16x16.png': {'path': 'favicon-16x16.png'},
    'extras/favicon-32x32.png': {'path': 'favicon-32x32.png'},
    'extras/favicon.ico': {'path': 'favicon.ico'},
    'extras/manifest.json': {'path': 'manifest.json'},
    'extras/mstile-150x150.png': {'path': 'mstile-150x150.png'},
    'extras/safari-pinned-tab.svg': {'path': 'safari-pinned-tab.svg'},
    # 自定義樣式
    'css/custom.css': {'path': 'theme/css/custom.css'},
}

# 自定義樣式的URL目錄
THEME_CSS_OVERRIDES = ('theme/css/custom.css',)

RFG_FAVICONS = True

2.2.2.更新Font Awesome的版本

pelican-alchemy使用Font Awesome 4.7.0版本,並且使用的是靜態資源的相對引用;

我們將其修改為最新的5.11.2版本的CDN引入,修改主題模版中的base.html文件:

<!-- themes/pelican-alchemy/alchemy/templates/base.html --> 

<link rel="stylesheet" href="{{ SITEURL }}/theme/css/font-awesome.min.css">

<!-- 改成 --> 

<link href="https://cdn.bootcss.com/font-awesome/5.11.2/css/fontawesome.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/font-awesome/5.11.2/css/solid.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/font-awesome/5.11.2/css/brands.css" rel="stylesheet">

除了上面的步驟,我們還有一個額外的工作要做:因為5.x的版本已經不使用fa前綴,取而代之的是fas()和fab();

所以,對於主題中那些類似class="fa fa-github"的樣式,應該修改為class="fab fa-github",主要涉及article.htmlindex.htmlheader.html這些文件;

最後,修改pelicanconf.py文件中關於ICONS配置的格式,需要額外指定樣式類別:

# pelicanconf.py

# 社交屬性,請到<https://fontawesome.com/icons>網站確定圖標樣式的類別
ICONS = [
    ('fab', 'github', 'https://github.com/luizyao'),
    ('fas', 'blog', 'https://www.cnblogs.com/luizyao/'),
    ('fas', 'rss', 'feeds/all.rss.xml')
]

pelican-alchemy有一個openissue:是關於Font Awesome版本的,後續可能會更新到5.x版本,目前issue處於接收反饋的狀態;

至於為什麼不使用CDN,貌似還和偉大的防火牆有關呢。

I’m sure you’ve heard of the Great Firewall of China; India, Russia, some African countries are doing similar things. You never know which URL or IP might become inaccessible

2.2.3.使用Bootstrap的樣式

我們可以為特定類型的元素添加Bootstrap的官方樣式;例如:為每個img元素添加class = "img-fluid"的樣式;

首先,安裝依賴包:

# beautifulsoup4為插件所依賴的第三方包
λ pipenv install beautifulsoup4

然後,下載插件:

λ mkdir plugins
λ git submodule add git@github.com:ingwinlu/pelican-bootstrapify.git plugins/pelican-bootstrapify

最後,修改pelicanconf.py配置文件:

# 到哪裡尋找插件
PLUGIN_PATHS = ['plugins']

# 想要使用的插件名
PLUGINS = ['pelican-bootstrapify']

# 想要添加的 Bootstrap 樣式
BOOTSTRAPIFY = {
    'table': ['table', 'table-striped', 'table-hover'],
    'img': ['img-fluid'],
}

2.3. 定製主題

下面我們為pelican-alchemy做一些定製化的操作,添加一些新的功能;

2.3.1. 添加返回頂部鏈接

修改base.html文件,在<head>中添加如下部分:

<!-- themes/pelican-alchemy/alchemy/templates/base.html --> 

<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/scrollup/2.4.1/jquery.scrollUp.min.js"></script>

<script>
  $(function () {
    $.scrollUp({
      scrollText: '<i class="fas fa-2x fa-chevron-circle-up"></i>'
    });
  });
</script>

2.3.2. 支持目錄

我自己寫了一個的插件,用於替代pelican默認的MarkdownReader,它有以下功能:

  • 使用增強的markdown解析

    • 代替markdown.extensions.extra
    • 代替markdown.extensions.codehilite
  • 支持以下方式生成文章目錄:

    1. markdown文本內的[TOC]標記處生成目錄;

    2. 通過元數據toc自定義目錄樣式;例如:

      {% if article.toc %}
        <aside class="col-md-4">
          <div class="widget widget-content">
            <h3 class="widget-title">文章目錄</h3>
            <div class="toc">
              <ul>
                {{ article.toc | safe }}
              </ul>
            </div>
          </div>
        </aside>
      {% endif %}
  • 如果沒配summary或者summary為空,支持自動截取開頭部分字符作為摘要;

使用方法:

  1. 作為一個子倉庫下載

    # 項目根目錄創建目錄
    λ mkdir plugins
    # 下載
    λ git submodule add git@github.com:luizyao/pelican-md-reader.git plugins/pelican-md-reader
  2. 修改pelicanconf.py配置文件

    # pelicanconf.py
    
    # 到哪裡尋找插件
    PLUGIN_PATHS = ['plugins']
    
    # 想要使用的插件名
    PLUGINS = ['pelican-md-reader']

更多細節可以參考:

2.3.3. 漢化

主要關鍵字漢化;

3.完整的pelicanconf.py文件

#!/usr/bin/env python
# -*- coding: utf-8 -*- #
from __future__ import unicode_literals

AUTHOR = 'luizyao'
SITENAME = "luizyao's blog"
SITEURL = ''

PATH = 'content'

DEFAULT_LANG = 'en'

# Feed generation is usually not desired when developing
FEED_ALL_ATOM = None
CATEGORY_FEED_ATOM = None
TRANSLATION_FEED_ATOM = None
AUTHOR_FEED_ATOM = None
AUTHOR_FEED_RSS = None

DEFAULT_PAGINATION = 10

# Uncomment following line if you want document-relative URLs when developing
# RELATIVE_URLS = True

# 修改時區
TIMEZONE = 'Asia/Shanghai'

# 修改默認的時間格式('%a %d %B %Y')
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M"

# 為元數據定義默認值
DEFAULT_METADATA = {
    # 默認發布的文章都是草稿,除非在文章元數據中明確指定:Status: published
    'status': 'draft',
}

# pelican-alchemy 原有的配置

# 主題所在的相對目錄
THEME = 'themes/pelican-alchemy/alchemy'

# 副標題
SITESUBTITLE = '戒驕戒躁 砥礪前行'

# 頭像
SITEIMAGE = '/images/profile.png width=200 height=200'

# 友鏈
LINKS = (
    ('pytest-chinese-doc', 'https://luizyao.github.io/pytest-chinese-doc/'),
)

# 代碼高亮的樣式
PYGMENTS_STYLE = 'friendly'

# 使用 Bootswatch 樣式:https://bootswatch.com/
BOOTSTRAP_CSS = 'https://cdn.bootcss.com/bootswatch/4.3.1/lux/bootstrap.min.css'

# 生成 sitemap.xml 文件
DIRECT_TEMPLATES = ['index', 'tags', 'categories', 'authors', 'archives', 'sitemap']
SITEMAP_SAVE_AS = 'sitemap.xml'

# 構建后的 html 文件路徑和 URL 標識
ARTICLE_URL = 'posts/{date:%Y}/{date:%m}/{slug}.html'
ARTICLE_SAVE_AS = ARTICLE_URL
DRAFTS_URL = 'drafts/{date:%Y}/{date:%m}/{slug}.html'
DRAFTS_SAVE_AS = ARTICLE_URL
PAGE_URL = 'pages/{slug}.html'
PAGE_SAVE_AS = PAGE_URL

# RSS 訂閱
FEED_ALL_RSS = 'feeds/all.rss.xml'

# 在構建中,它們會無損的拷貝到 output 的同名目錄下
STATIC_PATHS = ['extras', 'images', 'css']

# 構建時,extras/android-chrome-192x192.png文件,拷貝到output/android-chrome-192x192.png,不再是output/extras/android-chrome-192x192.png
EXTRA_PATH_METADATA = {
    'extras/android-chrome-192x192.png': {'path': 'android-chrome-192x192.png'},
    'extras/android-chrome-512x512.png': {'path': 'android-chrome-512x512.png'},
    'extras/apple-touch-icon.png': {'path': 'apple-touch-icon.png'},
    'extras/browserconfig.xml': {'path': 'browserconfig.xml'},
    'extras/favicon-16x16.png': {'path': 'favicon-16x16.png'},
    'extras/favicon-32x32.png': {'path': 'favicon-32x32.png'},
    'extras/favicon.ico': {'path': 'favicon.ico'},
    'extras/manifest.json': {'path': 'manifest.json'},
    'extras/mstile-150x150.png': {'path': 'mstile-150x150.png'},
    'extras/safari-pinned-tab.svg': {'path': 'safari-pinned-tab.svg'},
    # 自定義樣式
    'css/custom.css': {'path': 'theme/css/custom.css'},
}

# 自定義樣式的URL目錄
THEME_CSS_OVERRIDES = ('theme/css/custom.css',)

RFG_FAVICONS = True

# 到哪裡尋找插件
PLUGIN_PATHS = ['plugins']

# 想要使用的插件名
PLUGINS = ['pelican-bootstrapify', 'pelican-md-reader']

# 想要添加的 Bootstrap 樣式
BOOTSTRAPIFY = {
    'table': ['table', 'table-striped', 'table-hover'],
    'img': ['img-fluid'],
}

# 社交屬性,請到<https://fontawesome.com/icons>網站確定圖標樣式的類別
ICONS = [
    ('fab', 'github', 'https://github.com/luizyao'),
    ('fas', 'blog', 'https://www.cnblogs.com/luizyao/'),
    ('fas', 'rss', 'feeds/all.rss.xml')
]

4. 預覽

Github:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

PL真有意思(五):數據類型

前言

現在大多數程序設計語言中都有表達式和/或對象的類型概念。類型起着兩種主要作用:

  • 為許多操作提供了隱含的上下文信息,使程序員可以在許多情況下不必显示的描述這種上下文。比如int類型的兩個對象相加就是整數相加、兩個字符串類型的對象相加就是拼接字符串、在Java和C#中new object()隱含在背後的就是要分配內存返回對象的引用等等。

  • 類型描述了其對象上一些合法的可以執行的操作集合。類型系統將不允許程序員去做一個字符和一個記錄的加法。編譯器可以使用這個合法的集合進行錯誤檢查,好的類型系統能夠在實踐中捕獲很多錯誤

類型系統

從編譯方面的知識我們可以知道,計算機硬件可以按多種不同的方式去解釋寄存器里的一組二進制位。處理器的不同功能單元可能把一組二進制位解釋為指令、地址、字符、各種長度的整數或者浮點數等。當然,二進制位本身是無類型的,對存儲器的哪些位置應該如何解釋,大部分硬件也無任何保留信息。彙編語言由於僅僅是對一些二進制指令的“助記符號”翻譯,它也是這種無類型情況。高級語言中則總是關聯值與其類型,需要這種關聯的一些原因和用途就如前面說到的上下文信息和錯誤檢測。

一般來說,一個類型系統包含一種定義類型並將它們與特定的語言結構關聯的機制;以及一些關於類型等價、類型相容、類型推理的規則。 必須具有類型的結構就是那些可以有值的,或者可以引用具有值得對象的結構。類型等價規則確定兩個值得類型何時相同;類型相容規則確定特定類型的值是否可以用在特定的上下文環境里;類型推理規則基於一個表達式的各部分組成部分的類型以及其外圍上下文來確定這個表達式的類型。

在一些多態性變量或參數的語言中,區分表達式(如一個名字)的類型與它所引用的那個對象的類型非常重要,因為同一個名字在不同時刻有可能引用不同類型的對象。

在一些語言中,子程序也是有類型的,如果子程序是一級或者二級值,其值是動態確定的子程序,這時語言就需要通過類型信息,根據特定的子程序接口(即參數的個數和類型)提供給這種結構的可接受的值集合,那麼子程序就必須具有類型信息。在那些不能動態創建子程序引用的靜態作用域語言(這種語言中子程序是三級值),編譯器時就能確定一個名字所引用的子程序,因此不需要子程序具有類型就可以保證子程序的正確調用。

類型檢查

類型檢查時一個處理過程,其目的就是保證程序遵循了語言的類型相容規則,違背這種規則的情況稱為類型衝突。說一個語言是強類型的,那麼就表示這個語言的實現遵循一種禁止把任何操作應用到不支持這種操作的類型對象上的規則。說一個語言是靜態類型化(statically type)的,那麼它就是強類型的,且所有的類型檢查都能在編譯時進行(現實中很少有語言是真正的靜態類型,通常這一術語是指大部分類型檢查可以在編譯器執行,其餘一小部分在運行時檢查)。如C#我們通常都認為它是靜態類型化的語言。

動態(運行時)類型檢查是遲約束的一種形式,把大部分的檢查操作都推遲到運行的時候進行。採用動態作用域規則的語言大部分都是動態類型語言,因為它的名字和對象的引用都是在運行時確定的,而確定引用對象的類型則更是要在引用確定之後才能做出的。

類型檢查是把雙刃劍,嚴格的類型檢查會使編譯器更早的發現一些程序上的錯誤,但是也會損失一部分靈活性;動態類型檢查靈活性大大的,但是運行時的代價、錯誤的推遲檢查,各種語言的實現也都在這種利弊上進行權衡。

多態性

多態性使得同一段代碼體可以對多個類型的對象工作。它意味着可能需要運行時的動態檢查,但也未必一定需要。在Lisp、Smalltalk以及一些腳本語言中,完全的動態類型化允許程序員把任何操作應用於任何對象,只有到了運行時採取檢查一個對象是否實現了具體的操作。由於對象的類型可以看作它們的一個隱式的(未明確聲明的,一個不恰當的比喻就如C#中的this)參數,動態類型化也被說成是支持隱式的參數多態性。

雖然動態類型化具有強大的威力(靈活性),但卻會帶來很大的運行時開銷,還會推遲錯誤報告。一些語言如ML採用了一種複雜的類型推理系統,設法通過靜態類型化支持隱式的參數多態性。

在面向對象語言里,子類型多態性允許類型T的變量X引用了從T派生的任何類型的對象,由於派生類型必定支持基類型的所有操作,因此編譯器完全可以保證類型T的對象能接受的任何操作,X引用的對象也都能接受。對於簡單的繼承模型,子類型多態的類型檢查就能完全在編譯時實現。採用了這種實現的大多數語言(如C++,JAVA和C#)都提供另一種显示的參數化類型(泛型),允許程序員定義帶有類型參數的類。泛型對於容器(集合)類型特別有用,如T的列表(List )和T的棧(Stack )等,其中T只是一個類型佔位符,在初始化的這個容器對象時提供具體的類型來代替它。與子類型多態類似,泛型也可以在編譯時完成類型檢查。比如C++的模板完全就是編譯期間的東西,編譯后就完全沒有了模板的痕迹;JAVA則是利用一種“擦除”的技術實現的泛型,需要在運行時做一些檢查。

類型的含義

現在至少存在三種不同的考慮類型問題的方式,分別稱之為指稱的、構造的和基於抽象的

  • 指稱的

按照指稱的觀點,一個類型就是一組值,一個值具有某個類型的條件是他屬於這個值集合,一個對象具有某個類型的條件是他的值保證屬於這個值集合

  • 構造的

從構造的觀點看,一個類型或者是以一小組內部類型,或者是通過對一個或幾個更簡單些的類型,應用某個類型的構造符構造出來的

  • 基於抽象的

從基於抽象的角度來看,一個類型就是一個接口,由一組定義良好而且具有相互協調的語義的操作組成。

類型的分類

在不同語言里,有關類型的術語也不相同,這裏說的通常都是常用的術語,大部分語言多提供的內部類型差不多就是大部分處理器所支持的類型:整數、字符、布爾和實數。

一般語言規範中都會規定數值類型的精度問題,以及一些字符的編碼規定。通常特殊的一個數值類型是枚舉類型,具體的語法在不同的語言中略有差異,但是其也都是一個目的(用一個字符友好的表示一個數值)。

關於枚舉類型,由一組命名元素組成。在C中可以這樣寫:

enum weekday { sun, mon, tue, wed, thu, fri, sat };

在C中這樣的寫法和直接對裏面的元素直接賦值除了語法上效果完全一樣。但是在之後的許多語言中,枚舉類型是一個真正的類型

還有一些語言中提供一種稱為子界的類型,它表示一種基於基本數值的一個連續的區間。比如Pascal中表示1到100:

type test_score = 0..100

複合類型:由一些簡單的基本類型組合成的一些類型稱為複合類型,比如常見的記錄、變體記錄、數組、集合、指針、表等,具體的都會在後面詳細介紹。

類型檢查

大多數的靜態類型語言中,定義一個對象都是需要描述清楚它的類型,進一步講,這些對象出現的上下文也都是有類型的,也就是說語言中的一些規則限制了這種上下文中可以合法出現的對象類型。

類型相容確定了一個特定類型的對象的能否用在一個特定上下文中。在最極端的情況下,對象可使用的條件就是它的類型與上下文所期望的類型等價。但是在大多數語言中,相容關係都比等價更寬鬆一些,即使對象與上下文的類型不同,它們也可以相容。

而類型推理想回答的是從一個簡單的表達式出發構造另一個表達式時,這整個的表達式的類型是什麼

類型等價

在用戶可以定義新類型的語言中,類型等價的定義一般基於兩種形式。

type R2 = record
    a : integer
    b : integer
end;

type R2 = record
    b : integer
    a : integer
end;
  • 結構等價

基於類型定義的內容,就是它們由同樣的組成部分且按照同樣的方式組合而成

它的準確定義在不同的語言中也不一樣,因為它們要決定類型之間的哪些潛在差異是重要的,哪些是可以接受的(比如上面的兩個定義,是否還認為是等價的)。結構等價是一種很直接的認識類型的方式,早期的一些語言(Algol 68、Modula-3、ML)有些事基於結構等價的,現在的大部分語言(Java、C#)大都是基於名字等價了,為何呢?因為從某種意義上看,結構等價是由底層、由實現決定的,屬於比較低級的思考方式。就如一個上下文,如果你傳遞了一個結構等價但是不是所期待對象,實施結構等價的編譯器是不會拒絕這種情況的(假如這不是你希望的,那麼你也不會得到任何提示或者錯誤信息,很難排查的)。

  • 名字等價

基於類型的詞法形式,可以認為是每一個名字都引進一個新的類型;

它基於一種假設,就是說程序員花時間定義了兩個類型,雖然它們的組成部分可能相同,但是程序員要表達的意思就是這是兩個不同的類型。名字等價的常規判斷就非常簡單了,看看聲明兩個對象的類型是否是一個就是了。但是也會有一些特殊的情況出現,比如類型別名(C、C++的程序員很熟悉這種東西吧),比如 typedef int Age; 就為int類型重新定義了一個別名”Age”。那些認為int不等價越Age的語言稱為嚴格名字等價,認為等價的稱為寬鬆名字等價。其實這兩種也是很容易區分的,只要能區分聲明和定義兩個概念的差異就可以區分。在嚴格名字等價中看待typedef int Age是認為定義了一個新類型Age,在寬鬆名字等價看來這就是一個類型聲明而已,int和Age共享同一個關於整數的定義。

類型變換和轉換

在靜態類型的語言中,如果“a=b”,那麼我們會期望b的類型和a的相同;現在假定所提供的類型和期望的類型和所提供的類型相同,那麼我們在要求某個類型的上下文中使用另外一個類型時就需要显示的寫出類型變換(或稱為類型轉換)。根據具體的變換的具體情況,在運行時執行這種變化會有以下三種主要的情況出現:

  • 所涉及的類型可以認為是結構等價的,這種情況裏面因為涉及的類型採用了相同的底層的表示,則這種變換純粹就是概念上的操作,不需要運行時執行任何代碼。

  • 所涉及的類型具有不同的值集合,但它們的值集合具有相同的表示形式。比如一個類型和它的子類型,一個整數和一個無符號的整數。拿無符號整數變換為整數來說,由於無符號整數的最大值是整數類型所容納不了的,則運行時就必須執行一些代碼來保證這種變換的合法性,如果合法則繼續下去,否則會產生一個動態語義錯誤。

  • 所涉及的類型具有不同的底層表示,但是我們可以在它們的值之間定義某種對應關係。比如32位整數可以變換到IEEE的雙精度浮點數,且不會丟失精度。浮點數也可以通過舍入或割斷的形式變換成整數,但是會丟失小數部分。

非變換的類型轉換

有這麼一種情況,我們需要改變一個值,但是不需要改變它的二進製表示形式,更通俗點說就是我們希望按照另外一個類型的方式去解釋某個類型的二進制位,這種情況稱為非變換類型轉換。最簡單的一個例子比如說,一個byte類型的數值65,按byte類型來解釋它是65,如果按照char類型來解釋它就是字符“A”。比如C++中的static_cast執行類型變換,reinterpret_cast執行非變換的類型轉換。c中出現的union形式的結構,就可以認為是這種非變換的類型轉換的合法的安全的語言結構。在比如下面C中一般性非變換類型轉換代碼:

r=*((float *) &n);

任何非變換的類型轉換都極其危險的顛覆了語言的類型系統。在弱類型系統的語言中,這種顛覆可能很難發現,在強類型系統的語言中显示的使用這種非變換的類型轉換,起碼從代碼上可以看得出來它是這麼一回事,或多或少的有利於排查問題。

類型相容

大多數語言的上下文中並不要求類型等價,相應的一般都是實施較為“寬鬆”的類型相容規則。比如賦值語句要求右值相容與左值、參數類型相容,實際返回類型與指定的返回類型相容。在語言中,只要允許把一個類型的值用到期望的另外一個類型的上下文中,語言都必須執行一個到所期望類型的自動隱式變換,稱為類型強制(比如int b;double a=b;)。就像前面說的显示的類型變換一樣,隱式的類型變換也可能需要執行底層代碼或者做一些動態類型檢查。

重載

一個重載的名字可能引用不同類型的對象,這種歧義性需要通過上下文信息進行解析。比如a+b這個表達式可以表示整數或者浮點數的加法運算,在沒有強制的語言中,a和b必須都是整數或都是浮點數。如果是有強制的語言,那麼在a或者b有一個是浮點數的情況下,編譯器就必須使用浮點數的加法運算(另外一個整數強制轉換為浮點數)。如果語言中+只是進行浮點數運算,那麼即使a和b都是整數,也會被全部轉成浮點數進行運算(這代價就高了好多了)。

通用引用類型

通用引用類型:一些語言根據實習需求,設計有通用的引用類型,比如C中的void*、C#中的Object,任意的值都可以賦值給通用引用類型的對象。但是問題是存進去容易取出來難,當通用引用類型是右值的時候,左值的類型可能支持某些操作,然而這些操作右值對象是不具備的。為了保證通用類型到具體類型的賦值安全,一種解決辦法是讓對象可以自描述(也就是這個對象包含其真實類型的描述信息),C++,JAVA,C#都是這種方式,C#中如果賦值的類型不匹配則會拋出異常,而C++則是使用dynamic_cast做這種賦值操作,具體的後果呢,也是C++程序員負責。

類型推理

通過前面的類型檢查我們可以保證表達式的各各組成部分具有合適的類型,那麼這整個表達式的類型是什麼來着?其實在大多數的語言中也是比較簡單的,算術表達式的類型與運算對象相同、比較表達式總是布爾類型、函數調用的結果在函數頭聲明、賦值結果就是其左值的類型。在一些特殊的數據類型中,這個問題並不是那麼清晰明了,比如子界類型、複合類型。比如下面的子界類型問題(Pascal):

type Atype=0..20;
type Btype=10..20;

var a: Atype;
var b: Btype;

那麼a+b什麼類型呢???它確實是不能是Atype或者Btype類型,因為它可能的結果是10-40。有人覺得那就新構造一個匿名的子界類型,邊界時10到40。實際情況是Pascal給的答案是它的基礎類型,也就是整數。

在Pascal中,字符串’abc’的類型是array[1..3] of char、而Ada則認為是一種未完全確定的類型,該類型與任何3個字符數組相容,比如在Ada中’abc’ & ‘defg’其結果是一個7字符的數組,那麼這個7字符數組的類型是array[1..7] of cahr呢還是某一個也是7個字符組成的類型array (weekday) of character呢,更或者是其他任意一個也是包含七個字符數組的另外一個類型。這種情況就必須依賴表達式所處的上下文信息才能推到出來具體的類型來。

記錄(結構)與變體(聯合)

一些語言中稱記錄為結構(struct),比如C語言。C++把結構定義為class的一種特殊形式(成員默認全局可見),Java中沒有struct的概念,而C#則對struct採用值模型,對class採用引用模型。

語法與運算

一個簡單的結構體在C中可以這樣定義:

struct element{
    char name[2];
    int number;
    double weight;
    Bool merallic;    
}; 

等價於Pascal中的:

 type two_chars=packed array [1..2] of char;
 type element - record
     name:two_chars;
     number:integer;
     weight:real;
     metallic:Boolean
 end

記錄裏面的成員(如name,number…)稱為域(field)。在需要引用記錄中的域時,大部分語言使用“.”記法形式。比如Pascal中:

 var copper:eement;
 copper.name=6.34;

大部分語言中還允許記錄的嵌套定義,比如在Pascal中:

 type short_string=packed array[1..30] of char;
 type ore=record
      name:short_string;
      element_yielded:record /*嵌套的記錄定義*/
          name:two_chars;
          number:integer;
          weight:real;
          metallic:Boolean
      end
 end

存儲布局及其影響

一個記錄的各個域通常被放入內存中的相鄰位置。編譯器在符號表中保存每個域的偏移量,裝載和保存的時候通過基址寄存器和偏移量即可得到域的內存地址。類型element在32位的機器中可能的布局如下:

此處有圖

(圖在最後面,因為markdown的這個畫表格不符合這個要求,又不想引圖了,就直接用html寫了,會被擠到最後去)

(table標籤和我博客園的樣式生成的時候會出bug,刪除了)

在對結構體的存儲布局方案上,如果使用正常排序,結構中的空洞會浪費空間。但是如果通過壓縮來節省空間,但是可能很帶來很嚴重的訪問時間的代價

數組

數組是最常見也是最重要的複合數據類型。記錄用於組合一些不同類型的域在一起;而數組則不同,它們總是同質的。從語義上看,可以把數組想象成從一個下標類型到成員(元素)類型的映射。

有些語言要求下標類型必須是integer,也有許多語言允許任何離散類型作為下標;有些語言要求數組的元素類型只能是標量,而大多數語言則允許任意類型的元素類型。也有一些語言允許非離散類型的下標,這樣產生的關聯數組只能通過散列表的方式實現,而無法使用高效的連續位置方式存儲,比如C++中的map,C#中的Dictionary。在本節中的討論中我們假定數組的下標是離散的。

語法和操作

大多數的語言都通過數組名后附加下標的方式(圓括號|方括號)來引用數組裡的元素。由於圓括號()一般用於界定子程序調用的實際參數,方括號在區分這兩種情況則有易讀的優勢。Fortran的數組用圓括號,是因為當時IBM的打卡片機器上沒有方括號

維數、上下界和分配

對於數組的形狀在聲明中就已經描述,對於這種有靜態形狀的數組,可以用通常的方式來管理內存:生存期是整個程序的數組使用棧分配,具有更一般的生存期的動態生成數組使用堆分配。但是對於在加工之前不知道其形狀的數組,或其形狀在執行期間可能改變的數組,存儲管理就會更複雜一點。

  • 內情向量

在編譯期間,符號表維護者程序中的每個數組的維度和邊界信息。對於每個記錄,它還維護着每個域的偏移量。如果數組維度的數目和邊界是靜態已知的,編譯器就可以在符號表中找出它們,以便計算數組元素的地址。如果這些值不是靜態已知的,則編譯器就必須生成代碼,在運行時從一個叫內情向量的數據結構來查找它

  • 棧分配

子程序參數是動態形狀數組最簡單的例子,其中數組的上下界在運行時才確定,調用方都會傳遞數組的數據和一個適當的內情向量,但是如果一個數組的形狀只能到加工時才知道,這種情況下仍可以在子程序的棧幀里為數組分配空間,但是需要多做一層操作

  • 堆分配

在任意時間都可以改變形狀的數組,有時被稱為是完全動態的。因為大小的變化不會以先進先出的順序進行,所以棧分配就不夠用了。完全動態的數組必須在堆中分配。比如Java中的ArrayList

#### 內存布局

大多數語言的實現里,一個數組都存放在內存的一批連續地址中,比如第二個元素緊挨着第一個,第三個緊挨着第二個元素。對於多維數組而言,則是一個矩陣,會出現行優先和列優先的選擇題,這種選擇題對於語言使用者而言是透明的,而對語言的實現者則需要考慮底層方面的優化問題了。

在一些語言中,還有另外一種方式,對於數組不再用連續地址分配,也不要求各行連續存放,而是允許放置在內存的任何地方,再創建一個指向各元素的輔助指針數組,如果數組的維數多於兩維,就再分配一個指向指針數組的指針數組。這種方式稱為行指針布局,這種方式需要更多的內存空間,但是卻有兩個優點:

  • 首先,可能加快訪問數組裡單獨元素的速度;
  • 其次,允許創建不用長度的行,而且不需要再各行的最後留下對齊所用的空洞空間,這樣節省下來的空間有時候可能會超過指針佔據的空間。C,C++和C#都支持連續方式或行指針方式組織多維數組,從技術上講,連續布局才是真正的多維數組,而行指針方式則只是指向數組的指針數組。

字符串

許多語言中,字符串也就是字符的數組。而在另一些語言中,字符串的情況特殊,允許對它們做一些其他數組不能用的操作,比如Icon以及一些腳本語言中就有強大的字符串操作功能。

字符串是編程中非常重要的一個數據類型,故而很多語言都對字符串有特殊的處理以便優化其性能以及存儲(比如C#中的字符串不可變性保證了性能,字符串駐留技術照顧了存儲方面的需要),由於這些特殊的處理,故而各各語言中為字符串提供的操作集合嚴重依賴語言設計者對於實現的考慮。

集合

程序設計語言中的一個集合,也就是具有某個公共類型的任意數目的一組值的一種無序彙集。集合的元素所具有的類型叫做元類型或者基類型。現在的大多數程序設計語言都對集合提供了很大的支持,為集合提供了很多相關的操作

指針和遞歸類型

所謂的遞歸類型,就是可以在其對象中包含一個或多個本類型對象的引用類型。遞歸類型用於構造各種各樣的“鏈接”數據結構,比如樹。在一些對變量採用引用模型的語言中,很容易在創建這種遞歸類型,因為每個變量都是引用;在一些對變量採用值模型的語言中,定義遞歸類型就需要使用指針的概念,指針就是一種變量,其值是對其他對象的引用。

對於任何允許在堆里分配新對象的語言,都存在一個問題:若這種對象不在需要了,何時以及以何種方式收回對象佔用的空間?對於那些活動時間很短的程序,讓不用的存儲留在那裡,可能還可以接受,畢竟在它不活動時系統會負責回收它所使用的任何空間。但是大部分情況下,不用的對象都必須回收,以便騰出空間,如果一個程序不能把不再使用的對象存儲回收,我們就認為它存在“內存泄漏”。如果這種程序運行很長一段時間,那麼它可能就會用完所有的空間而崩潰。許多早期的語言要求程序員显示的回收空間,如C,C++等,另一些語言則要求語言實現自動回收不再使用的對象,如Java,C#以及所有的函數式語言和腳本語言。显示的存儲回收可以簡化語言的實現,但會增加程序員忘記回收不再使用的對象(造成內存泄漏),或者不當的回收了不該回收的正在使用的對象(造成懸空引用)的可能性。自動回收可以大大簡化程序員的工作,但是為語言的實現帶來了複雜度。

語法和操作

對指針的操作包括堆中對象的分配和釋放,對指針間接操作以訪問被它們所指的對象,以及用一個指針給另一個指針賦值。這些操作的行為高度依賴於語言是函數式還是命令式,以及變量/名字使用的是引用模型還是值模型。

函數式語言一般對名字採用某種引用模型(純的函數式語言里根本沒有變量和賦值)。函數式語言里的對象傾向於採取根據需要自動分配的方式。

命令式語言里的變量可能採用值模型或引用模型,有時是兩者的某種組合。比如 A=B;

  • 值模型: 把B的值放入A。
  • 引用模型: 使A去引用B所引用的那個對象。

Java的實現方式區分了內部類型和用戶定義的類型,對內部類型採用值模型,對用戶定義的類型採用則採用引用模型,C#的默認方式與Java類似,另外還提供一些附加的語言特性,比如“unsafe”可以讓程序員在程序中使用指針。

懸空引用

在前兩篇的名字、作用域和約束中我們列舉了對象的3種存儲類別:靜態、棧和堆。靜態對象在程序的執行期間始終是活動的,棧對象在它們的聲明所在的子程序執行期間是活動的,而堆對象則沒有明確定義活動時間。

在對象不在活動時,長時間運行的程序就需要回收該對象的空間,棧對象的回收將作為子程序調用序列的一部分被自動執行。而在堆中的對象,由程序員或者語言的自動回收機制負責創建或者釋放,那麼如果一個活動的指針並沒有引用合法的活動對象,這種情況就是懸空引用。比如程序員显示的釋放了仍有指針引用着的對象,就會造成懸空指針,再進一步假設,這個懸空指針原來指向的位置被其他的數據存放進去了,但是實際卻不是這個懸空指針該指向的數據,如果對此存儲位置的數據進行操作,就會破壞正常的程序數據。

那麼如何從語言層面應對這種問題呢?Algol 68的做法是禁止任何指針指向生存周期短於這個指針本身的對象,不幸的是這條規則很難貫徹執行。因為由於指針和被指對象都可能作為子程序的參數傳遞,只有在所有引用參數都帶有隱含的生存周期信息的情況下,才有可能動態的去執行這種規則的檢查。

廢料收集

對程序員而已,显示釋放堆對象是很沉重的負擔,也是程序出錯的主要根源之一,為了追蹤對象的生存軌跡所需的代碼,會導致程序更難設計、實現,也更難維護。一種很有吸引力的方案就是讓語言在實現層面去處理這個問題。隨着時間的推移,自動廢料收集回收都快成了大多數新生語言的標配了,雖然它的有很高的代價,但也消除了去檢查懸空引用的必要性了。關於這方面的爭執集中在兩方:以方便和安全為主的一方,以性能為主的另一方。這也說明了一件事,編程中的很多地方的設計,架構等等方面都是在現實中做出權衡。

廢料收集一般有這兩種思想,就不詳細說了。

  • 引用計算
  • 追溯式收集

表具有遞歸定義的結構,它或者是空表,或者是一個有序對,有序對由一個對象和另一個表組成。表對於函數式或者邏輯式語言程序設計非常適用,因為那裡的大多數工作都是通過遞歸函數或高階函數來完成的。

在Lisp中:

(cons 'a '(b))  => (a b)
(car '(a b))    => a
(cdr '(a b c))  => (b c)

在Haskell和Python還由一個非常有用的功能,叫做列表推導。在Python中可以這樣推導出一個列表

[i * i for i in range(1, 100) if i % 2 == 1]

文件和輸入/輸出

輸入/輸出(I/O)功能使程序可以與外部世界通信。在討論這種通信時,將交互式I/O和文件I/O分開可能有些幫助。交互式IO通常意味着與人或物理設備通信,人或設備都與運行着的程序并行工作,送給程序的輸入可能依賴程序在此之前的輸出。文件通常對應於程序的地址空間之外的存儲器,由操作系統實現。

有些語言提供了內置的File數據類型,另外一些語言將IO工作完全委託給庫程序包,這些程序包導出一個file類型。所以IO也算作是一種數據類型

相等檢測和賦值

對於簡單的基本數據類型,如整數、浮點數和字符,相等檢測和賦值相對來說都是直截了當的操作。其語義和實現也很明確,可以直接按照二進制位方式比較或複製,但是,對於更加複雜或抽象的數據類型,就可能還需要其它的比較方式

  • 相互是別名?
  • 二進制位是否都相等?
  • 包含同樣的字符序列?
  • 如果打印出來,看起來完全一樣?

就許多情況下,當存在引用的情況下,只有兩個表達式引用相同的對象時它們才相等,這種稱為淺比較。而對於引用的對象本身存在相等的含義時,這種比較稱為深比較。對於複雜的數據結構,進行深比較可能要進行遞歸的遍歷。所以相對來說,賦值也有深淺之分。深賦值時是進行完整的拷貝。

大多數的語言都使用淺比較和淺賦值

小結

本文從語言為何需要類型系統出發,解釋了類型系統為語言提供了那些有價值的用途:1是為許多操作提供隱含的上下文,使程序員在許多情況下不必显示的描述這種上下文;2是使得編譯器可以捕捉更廣泛的各種各樣的程序錯誤。 然後介紹了類型系統的三個重要規則:類型等價、類型相容、類型推理。以此3個規則推導出的強類型(絕不允許把任何操作應用到不支持該操作的對象上)、弱類型以及靜態類型化(在編譯階段貫徹實施強類型的性質)、動態類型化的性質以及在對語言的使用方面的影響。以及後續介紹了語言中常見的一些數據類型的用途以及語言在實現這種類型方面所遇到的問題以及其大致的實現方式。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

Python 深入淺出支持向量機(SVM)算法

相比於邏輯回歸,在很多情況下,SVM算法能夠對數據計算從而產生更好的精度。而傳統的SVM只能適用於二分類操作,不過卻可以通過核技巧(核函數),使得SVM可以應用於多分類的任務中。

本篇文章只是介紹SVM的原理以及核技巧究竟是怎麼一回事,最後會介紹sklearn svm各個參數作用和一個demo實戰的內容,盡量通俗易懂。至於公式推導方面,網上關於這方面的文章太多了,這裏就不多進行展開了~

1.SVM簡介

支持向量機,能在N維平面中,找到最明顯得對數據進行分類的一個超平面!看下面這幅圖:

如上圖中,在二維平面中,有紅和藍兩類點。要對這兩類點進行分類,可以有很多種分類方法,就如同圖中多條綠線,都可以把數據分成兩部分。

SVM做的,是找到最好的那條線(二維空間),或者說那個超平面(更高維度的空間),來對數據進行分類。這個最好的標準,就是最大間距

至於要怎麼找到這個最大間距,要找到這個最大間距,這裏大概簡單說一下,兩個類別的數據,到超平面的距離之和,稱之為間隔。而要做的就是找到最大的間隔。

這最終就變成了一個最大化間隔的優化問題。

2.SVM的核技巧

核技巧,主要是為了解決線性SVM無法進行多分類以及SVM在某些線性不可分的情況下無法分類的情況。

比如下面這樣的數據:

這種時候就可以使用核函數,將數據轉換一下,比如這裏,我們手動定義了一個新的點,然後對所有的數據,計算和這個新的點的歐式距離,這樣我們就得到一個新的數據。而其中,離這個新點距離近的數據,就被歸為一類,否則就是另一類。這就是核函數。

這是最粗淺,也是比較直觀的介紹了。通過上面的介紹,是不是和Sigmoid有點像呢?都是通過將數據用一個函數進行轉換,最終得到結果,其實啊,Sigmoid就是一鍾核函數來着,而上面說的那種方式,是高斯核函數。

這裏補充幾點:

  • 1.上面的圖中只有一個點,實際可以有無限多個點,這就是為什麼說SVM可以將數據映射到多維空間中。計算一個點的距離就是1維,2個點就是二維,3個點就是三維等等。。。
  • 2.上面例子中的紅點是直接手動指定,實際情況中可沒辦法這樣,通常是用隨機產生,再慢慢試出最好的點。
  • 3.上面舉例這種情況屬於高斯核函數,而實際常見的核函數還有多項式核函數,Sigmoid核函數等等。

OK,以上就是關於核技巧(核函數)的初步介紹,更高級的這裏也不展開了,網上的教程已經非常多了。

接下來我們繼續介紹sklearn中SVM的應用方面內容。

3.sklearn中SVM的參數

def SVC(C=1.0, 
             kernel='rbf', 
             degree=3, 
             gamma='auto_deprecated',
             coef0=0.0, 
             shrinking=True, 
             probability=False,
             tol=1e-3, 
             cache_size=200, 
             class_weight=None,
             verbose=False, 
             max_iter=-1, 
             decision_function_shape='ovr',
             random_state=None)
 
- C:類似於Logistic regression中的正則化係數,必須為正的浮點數,默認為 1.0,這個值越小,說明正則化效果越強。換句話說,這個值越小,越訓練的模型更泛化,但也更容易欠擬合。
- kernel:核函數選擇,比較複雜,稍後介紹
- degree:多項式階數,僅在核函數選擇多項式(即“poly”)的時候才生效,int類型,默認為3。
- gamma:核函數係數,僅在核函數為高斯核,多項式核,Sigmoid核(即“rbf“,“poly“ ,“sigmoid“)時生效。float類型,默認為“auto”(即值為 1 / n_features)。
- coef0:核函數的獨立項,僅在核函數為多項式核核Sigmoid核(即“poly“ ,“sigmoid“)時生效。float類型,默認為0.0。獨立項就是常數項。
- shrinking:不斷縮小的啟髮式方法可以加快優化速度。 就像在FAQ中說的那樣,它們有時會有所幫助,有時卻沒有幫助。 我認為這是運行時問題,而不是收斂問題。
- probability:是否使用概率評估,布爾類型,默認為False。開啟的話會評估數據到每個分類的概率,不過這個會使用到較多的計算資源,慎用!!
- tol:停止迭代求解的閾值,單精度類型,默認為1e-3。邏輯回歸也有這樣的一個參數,功能都是一樣的。
- cache_size:指定使用多少內存來運行,浮點型,默認200,單位是MB。
- class_weight:分類權重,也是和邏輯回歸的一樣,我直接就搬當時的內容了:分類權重,可以是一個dict(字典類型),也可以是一個字符串"balanced"字符串。默認是None,也就是不做任何處理,而"balanced"則會去自動計算權重,分類越多的類,權重越低,反之權重越高。也可以自己輸出一個字典,比如一個 0/1 的二元分類,可以傳入{0:0.1,1:0.9},這樣 0 這個分類的權重是0.1,1這個分類的權重是0.9。這樣的目的是因為有些分類問題,樣本極端不平衡,比如網絡攻擊,大部分正常流量,小部分攻擊流量,但攻擊流量非常重要,需要有效識別,這時候就可以設置權重這個參數。
- verbose:輸出詳細過程,int類型,默認為0(不輸出)。當大於等於1時,輸出訓練的詳細過程。僅當"solvers"參數設置為"liblinear"和"lbfgs"時有效。
- max_iter:最大迭代次數,int類型,默認-1(即無限制)。注意前面也有一個tol迭代限制,但這個max_iter的優先級是比它高的,也就如果限制了這個參數,那是不會去管tol這個參數的。
- decision_function_shape:多分類的方案選擇,有“ovo”,“ovr”兩種方案,也可以選則“None”,默認是“ovr”,詳細區別見下面。
- random_state:隨時數種子。

sklearn-SVM參數,kernel特徵選擇

kernel:核函數選擇,字符串類型,可選的有“linear”,“poly”,“rbf”,“sigmoid”,“precomputed”以及自定義的核函數,默認選擇是“rbf”。各個核函數介紹如下:
“linear”:線性核函數,最基礎的核函數,計算速度較快,但無法將數據從低維度演化到高維度
“poly”:多項式核函數,依靠提升維度使得原本線性不可分的數據變得線性可分
“rbf”:高斯核函數,這個可以映射到無限維度,缺點是計算量比較大
“sigmoid”:Sigmoid核函數,對,就是邏輯回歸裏面的那個Sigmoid函數,使用Sigmoid的話,其實就類似使用一個一層的神經網絡
“precomputed”:提供已經計算好的核函數矩陣,sklearn不會再去計算,這個應該不常用
“自定義核函數”:sklearn會使用提供的核函數來進行計算
說這麼多,那麼給個不大嚴謹的推薦吧
樣本多,特徵多,二分類,選擇線性核函數
樣本多,特徵多,多分類,多項式核函數
樣本不多,特徵多,二分類/多分類,高斯核函數
樣本不多,特徵不多,二分類/多分類,高斯核函數

當然,正常情況下,一般都是用交叉驗證來選擇特徵,上面所說只是一個較為粗淺的推薦。

sklearn-SVM參數,多分類方案

其實這個在邏輯回歸裏面已經有說過了,這裏還是多說一下。

原始的SVM是基於二分類的,但有些需求肯定是需要多分類。那麼有沒有辦法讓SVM實現多分類呢?那肯定是有的,還不止一種。

實際上二元分類問題很容易推廣到多元邏輯回歸。比如總是認為某種類型為正值,其餘為0值

舉個例子,要分類為A,B,C三類,那麼就可以把A當作正向數據,B和C當作負向數據來處理,這樣就可以用二分類的方法解決多分類的問題,這種方法就是最常用的one-vs-rest,簡稱OvR。而且這種方法也可以方便得推廣到其他二分類模型中(當然其他算法可能有更好的多分類辦法)。

另一種多分類的方案是Many-vs-Many(MvM),它會選擇一部分類別的樣本和另一部分類別的樣本來做二分類

聽起來很不可思議,但其實確實是能辦到的。比如數據有A,B,C三個分類。

我們將A,B作為正向數據,C作為負向數據,訓練出一個分模型。再將A,C作為正向數據,B作為負向數據,訓練出一個分類模型。最後B,C作為正向數據,C作為負向數據,訓練出一個模型。

通過這三個模型就能實現多分類,當然這裏只是舉個例子,實際使用中有其他更好的MVM方法。限於篇幅這裏不展開了。

MVM中最常用的是One-Vs-One(OvO)。OvO是MvM的特例。即每次選擇兩類樣本來做二元邏輯回歸。

對比下兩種多分類方法,通常情況下,Ovr比較簡單,速度也比較快,但模型精度上沒MvM那麼高。MvM則正好相反,精度高,但速度上比不過Ovr。

4.sklearn SVM實戰

我們還是使用鳶尾花數據集,不過這次只使用其中的兩種花來進行分類。首先準備數據:

import matplotlib.pyplot as plt
import numpy as np
from sklearn import svm,datasets
import pandas as pd
tem_X = iris.data[:, :2]
tem_Y = iris.target
new_data = pd.DataFrame(np.column_stack([tem_X,tem_Y]))
#過濾掉其中一種類型的花
new_data = new_data[new_data[2] != 1.0]
#生成X和Y
X = new_data[[0,1]].values
Y = new_data[[2]].values

然後用數據訓練,並生成最終圖形


# 擬合一個SVM模型
clf = svm.SVC(kernel='linear')
clf.fit(X, Y)

# 獲取分割超平面
w = clf.coef_[0]
# 斜率
a = -w[0] / w[1]
# 從-5到5,順序間隔採樣50個樣本,默認是num=50
# xx = np.linspace(-5, 5)  # , num=50)
xx = np.linspace(-2, 10)  # , num=50)
# 二維的直線方程
yy = a * xx - (clf.intercept_[0]) / w[1]
print("yy=", yy)

# plot the parallels to the separating hyperplane that pass through the support vectors
# 通過支持向量繪製分割超平面
print("support_vectors_=", clf.support_vectors_)
b = clf.support_vectors_[0]
yy_down = a * xx + (b[1] - a * b[0])
b = clf.support_vectors_[-1]
yy_up = a * xx + (b[1] - a * b[0])

# plot the line, the points, and the nearest vectors to the plane
plt.plot(xx, yy, 'k-')
plt.plot(xx, yy_down, 'k--')
plt.plot(xx, yy_up, 'k--')

plt.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1], s=80, facecolors='none')


plt.scatter(X[:, 0].flat, X[:, 1].flat, c='#86c6ec', cmap=plt.cm.Paired)
# import operator
# from functools import reduce
# plt.scatter(X[:, 0].flat, X[:, 1].flat, c=reduce(operator.add, Y), cmap=plt.cm.Paired)

plt.axis('tight')
plt.show()

最終的SVM的分類結果如下:

以上~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Python中lambda的使用,與它的三個好基友介紹!

匿名函數lambda

除了def語句,python還提供了一種生成函數對象的表達式形式。由於它與LISP語言中的一個工具類似,所以稱為lambda。

就像def一樣,這個表達式創建了一個之後能夠調用的函數,但是它返回一個函數而不是將這個函數賦值給一個變量。這些就是lambda叫做匿名函數的原因。實際上,他常常以一種行內進行函數定義的方式使用,或者用作推遲執行一些代碼。

lambda的一般形式是關鍵字lambda之後跟着一個或多個參數(與一個def頭部內用括號括起來的參數列表類似),緊跟着是一個冒號,之後是表達式

lambda arg1,arg2,argn:expression using arguments

由lambda表達式所返回的函數對象與由def創建並複製后的函數對象工作起來是完全一致的,但lambda有一些不同之處,讓其扮演特定的角色時更有用:

lambda是一個表達式,而不是一個語句

因為這一點,lambda可以出現在python語法不允許def出現的地方。
此外,作為一個表達式,lambda返回一個值(一個新的函數),可以選擇性的賦值給一個變量
相反,def語句總是得在頭部將一個新的函數賦值給一個變量,而不是將這個函數作為結果返回。

lambda的主題是單個表達式,而不是一個代碼塊

這個lambda的主題簡單的就好像放在def主體return語句中的代碼一樣。
簡單的將結果寫成一個順暢的表達式,而不是明確的返回。
但由於它僅限於表達式,故lambda通常要比def功能少…你僅能夠在lambda主體中封裝有限的邏輯進去,因為他是一個為編寫簡單函數而設計的。
除了上述這些差別,def和lambda都能過做同樣種類的工作

def與lambda的相同用法

x = lambda x, y, z: x + y + z
x(2, 3, 4)
>>> 9

y = (lambda a='hello', b='world': a + b)
y(b='Python')
>>> 'hellopython'

為什麼使用lambda

看過上面的兩個小例子,很多人會說這個和def沒什麼差別,我們又為什麼要使用lambda呢?

通常來說,lambda起到一種函數的速寫作用,允許在使用的代碼內嵌一個函數的定義,他完全是可選的(是可以使用def代替他們),但是在你僅需要切入一段可執行代碼的情況下,它會帶來一個更簡潔的書寫效果。

lambda通常用來編寫跳轉表,也就是行為的列表或者字典,能夠按照需求執行操作,比如:

l = [lambda x: x ** 2, lambda x: x ** 3, lambda x: x ** 4]
for f in l:
    print(f(2))
>>> 4
>>> 8
>>> 16
print(l[0](3))
>>> 9

當需要把小段的可執行代碼編寫進def語句從語法上不能實現的地方是,lambda表達式作為def的一種速寫來說,是最為有用的,如果上面的代碼用def編寫,則變為:

def f1(x):
    return x ** 2
 
def f2(x):
    return x ** 3
 
def f3(x):
    return x ** 4
 
l = [f1, f2, f3]

for f in l:
    print(f(2))
print(l[0](3))

實際上,我們可以用python中的字典或者其他的數據結構來構建更多種類的行為表,從而做同樣的事情。

lambda中實現if-else

Python中具備的單行表達式:if a:b else c語法在lambda中同樣適用:

lower = lambda x,y:x if x<y else y
lower(4,5)
>>> 4

看了半天,大家可能也並未覺得lambda在python中到底比def優越與便利在哪裡,那麼說到lambda,就必須要提及三個函數map、filter、reduce,當你接觸了這三個函數,那麼你才能感受到lambda真實的方便之處

map 函數

程序對列表或者其他序列常常要做的一件事就是對每個元素進行一個操作,並把其結果集合起來。
python提供了一個工具map,它會對一個序列對象中的每一個元素應用該的函數,並返回一個包含了所有函數調用結果的列表。

舉個栗子,我們有一個列表,需要將列表的每一個字段+10,我們該如何操作?

list_show = [1, 2, 3, 4]
# 方式1
new_list_show = []
for i in list_show:
    new_list_show.append(i + 10)

print(new_list_show)

# 方式2
def adds(x):
    return x + 10

print(list(map(adds, list_show)))

# 更優雅的方式3:
print(list(map(lambda x: x + 10, list_show)))

看看上面三個實現方式,你覺得那種更加Pythonic?

eg:需要注意一點,map在python3中是一個可迭代對象,引入需要使用列表調用來使它生成所有的結果用於显示,python2不必如此。

當然map的闡述函數,不僅僅支持自己編寫的,同樣也支持python自帶的多種函數,比如:

list_show = [1, -2, 3, -4, 5, -6]
print(list(map(abs, list_show)))
>>> [1, 2, 3, 4, 5, 6]

filter函數

filter通過字面意思,大家就知道它的用處了,用於數據的過濾操作,它也是lambda的一個好基友,舉個栗子。
我們需要過濾0-9中,能被2整除的数字組成一個列表,我們該如何操作?只需要一行代碼:

print(list(filter(lambda x: x % 2 == 0, range(10))))
>>> [0, 2, 4, 6, 8]

沒錯,filter就是這麼的簡單實用….

reduce的妙用

reduce在python2中是一個簡單的函數,但在python3中它責備收錄與functools中。
它接收一個迭代器來處理並返回一個單個的結果。

list_show = [1, 2, 3, 4]
print(reduce(lambda x, y: x + y, list_show))
>>> 10
print(reduce(lambda x, y: x * y, list_show))
>>> 24

lambda的實用與它的好基友就介紹到這裏,希望對大家有所幫助。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?