歷久彌新的慢時尚「萬秀的洗衣店」在 Today at Apple 分享穿搭美照創作心得

在崇尚「快時尚」的人眼裡,洗衣店可能是種不被需要的存在。因疫情而有更多時間與經營「萬秀的洗衣店」的阿公阿嬤相處的張瑞夫(Reef Chang),觀察到隨時間流逝,被遺忘的不僅是店中近十年未被領走的珍貴衣服,與一手拉拔自己長大親人的關係,乃至於阿公阿嬤對於自己專業職人的認同感,可能才是更被深埋的存在。

突然有了這樣感觸的 Reef,選擇將自己與阿公阿嬤加上老洗衣店所構築而成的慢時尚經典創作搬上社群平台。現在,他們希望將這種富含療癒生活溫度與惜物觀念的創作精神,傳遞給更多人一起尋找那被遺忘的美好。繼續閱讀歷久彌新的慢時尚「萬秀的洗衣店」在 Today at Apple 分享穿搭美照創作心得報導內文。

歷久彌新的慢時尚「萬秀的洗衣店」在 Today at Apple 分享穿搭美照創作心得

Reef 在今天「萬秀的洗衣店」於 Today at Apple 課程預覽中,分享了很多拍攝阿公阿嬤充滿「慢時尚感」的人像拍攝心得與教學 — 報名要快啊!推薦聽聽他的心得分享,跟作品一樣充滿療癒感與感動!

技巧的部分其實真的並不難 — 他會習慣以較為方正的構圖帶來更專業的感覺,所以會建議大家可以啟動 iPhone 拍攝設定內的「格線」功能,並適時啟動人像拍攝模式來營造更好的淺景深效果。

課程分享中,他常常利用 iPhone XR 開始提供的 1x 廣角人像模式來創作;新世代 iPhone 12 Pro 系列搭載的廣角人像「夜間」模式也讓 Reef 感到十分驚艷,即便是在洗衣店掛衣區的昏暗光線下,依然可以拍到運算式攝影加持後畫質足夠好的人像照片。

共感溝通,細膩觀察

Reef 分享的拍攝技巧真的不艱澀難懂,甚至來到 Pose 與穿搭的環節,他還認為很多時候根本要歸功於阿公阿嬤自己的發揮(是真的很可愛!)。但這中間的關鍵其實是在於與被攝者的溝通與引導,更與細膩的生活觀察脫離不了關係,才能拍攝出他心中追求「先感動自己,再讓別人感受生活中的溫暖」的「最基礎卻最真實」的影像。

▲圖:Reef 現場示範平常創作拍攝時的溝通與拍攝技巧。

之所以會在 Instagram 開始萬秀洗衣店的系列影像創作。是因為阿公阿嬤面對近在身邊卻有溝通隔閡的家人,淡淡說出的一句「我已經沒用了(台語)」。

這讓 Reef 驚覺養育自己長大、彼此之間曾經可以活潑共處的阿公阿嬤,好像隨著 70 多年歷史生意漸漸平淡的洗衣店一起失去活力。彷彿與掛在店深處近十年、洗得一塵不染卻沒人領回的衣物一樣,成了被遺忘的美好。

為了幫帶著自己長大的阿公阿嬤找回以往的活力,拾回身為職人的尊重感。Reef 認為自己應該嘗試扭轉以往兩個世代彼此間偏單向的溝通方式,改為嘗試向彼此解釋互相在意的事物,想要透過「共感」的方式為生活帶來改變。

以隨手可得的題材,像送洗近十年未取的老衣服,透過一起「玩」攝影的方式,讓萬秀洗衣店 WANT SHOW as young — 透過影像秀出八十多歲的萬吉與秀娥內心的那個時尚靈魂,不因為有年紀了就不做年輕時敢於實踐的事。

IG 上的「萬秀的洗衣店」的簡介,寫的是孫子不忍心看阿公阿嬤每天無聊發呆的產物。但從照片中看到的,其實更多是 Reef 對於阿公阿嬤與自己的生活溫暖瞬間的細膩觀察。Reef:「要做的,其實是用他們能懂得語言和他們對話,讓時代縮短距離。」

他會詢問像是「你以前是怎麼看小姐的?」這類有趣的問題,引導阿公與阿嬤擺出有趣的互動表情與姿勢。也懂得觀察他們平常的生活姿態,透過時尚的衣服穿搭、攝影構圖與適當的引導,拍攝出專業感十足但又充滿可愛細節的人像作品。更藉此有機會聽到更多以往沒有聽過,屬於他們倆的小故事。

▲圖右:翻出阿公 24 歲當兵時,兩人曬恩愛讓人閃瞎照片信物的阿嬤。

為了不無聊出發的「萬秀的洗衣店」,不僅成為連總統都 Feat. 的 IG 網紅。更大的重點在於,現在阿公阿嬤也更願意秀出彼此的愛,更樂於與家人分享過去的照片與穿搭,證明自己以前其實也是非常時尚的,甚至還會提早為 Reef 的拍攝構思企劃(!),提供接下來的創作靈感,這些都是很大的改變。

▲圖:現場示範衣服上的洗標,證明了他們的確曾經也是某些人的「故事」。不過 Reef 也強調,這些衣服真的是放了 8、9 年以上都未取(基本上把別人的店當衣櫃也不付錢了…XD),所以不用擔心洗衣店會沒事就拿客人送洗的衣服來拍照 — 倒是有人看到他們的新聞,才想到衣服放了一年還沒拿(咦)。

這些當初啟動「萬秀的洗衣店」創作拍攝時從沒想過的收穫。也讓 Reef 認為既然受到了這麼大的關注,似乎也應該藉此傳達更多理念。他們在嘖嘖上發起的「重新定衣計畫,建立洗衣店「被遺忘衣服」的循環平台」就是一個源自於更惜物、有慈善意義、更永續環保的計畫。

人類對於速度的追求永無止盡,暖心溫度的凝聚卻是永遠快不起來也勉強不來的存在。透過「萬秀的洗衣店」對於時尚詮釋引起的更多共感,也許可以成為你我利用手邊的創作工具,與親友有更深溝通交流的契機。

如果你喜歡他們的作品的話,可以報名 Today at Apple 課程學習如何利用手上的 iPhone,拍出充滿「萬秀的洗衣店」風格的溫暖照片。

          在 Instagram 查看這則貼文                      

萬秀的洗衣店|WANT SHOW as young(@wantshowasyoung)分享的貼文

          在 Instagram 查看這則貼文                      

萬秀的洗衣店|WANT SHOW as young(@wantshowasyoung)分享的貼文

          在 Instagram 查看這則貼文                      

萬秀的洗衣店|WANT SHOW as young(@wantshowasyoung)分享的貼文

          在 Instagram 查看這則貼文                      

萬秀的洗衣店|WANT SHOW as young(@wantshowasyoung)分享的貼文

更多「萬秀的洗衣店」作品,請至 Instagram 觀看。或動手開始自己拍出更時尚的親友吧!

延伸閱讀:

iPhone 12 Pro Max 深度實拍體驗:ProRAW 讓 iPhone 踏入專業

M1 MacBook Air 開箱體驗:最驚喜的不變

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

【其他文章推薦】

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

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

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

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

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

※網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

iOS 14.5 Beta 2 暗示將推出 MagSafe 充電配件,磁吸無線充電行動電源即將來臨?

Apple 針對 iPhone 推出的聰穎電池護殼,一直是有著「原廠控」的果粉們選擇為 iPhone 補給電力會列入考慮的配件選項。但隨著 iPhone 12 系列支持 MagSafe 磁吸無線充電後,至今仍 Apple 就未推出聰穎電池護殼這樣內建電池的保護殼配件。然而,日前在針對開發者釋出的 iOS 14.5 Beta 2 預覽版本,除了新增多達 217 組的全新表情符號,從中似乎也找到些暗示未來即將推出對應 MagSafe 介面的行動電源配件。

▲圖片來源:Steve Moser (Twitter/ @SteveMoser)

iOS 14.5 Beta 2 暗示將推出「MagSafe 充電配件」,磁吸無線充電行動電源即將來臨?

▲圖片來源:MacRumors

據相關消息表示, Apple 將在未來 iOS 更新加入「行動充電模式(Mobile Charge Mode)」的全新電力補給方式,這預期能讓像是 iPhone 12 系列支持 MagSafe 的手機能讓電力維持在 90% 以上,以應付一整天的使用需求。
除了連接行動電源之外,過去若在外想讓 iPhone 隨時保持在充足電量,就得考慮「聰穎電池護殼」這類將行動電源設計與手機背蓋結合的充電保護殼配件。
然而這樣的配件補充電力相當方便,但也伴隨著需要隨時讓手機多了一個笨重、厚重的電池背在手機上,這也是除了價格考量外讓許多消費者卻步的原因。

▲圖片來源:Steve Moser (Twitter/ @SteveMoser)

不過,隨著 iPhone 12 系列在機身背面加入 MagSafe 介面,讓未來能透過支持 MagSafe 的行動電源或相關 MagSafe 無線充電配件,預期能直接透過無線充電方式補充電力。這項改變有別於過去聰穎電池護殼需要長時間都安裝在手機上,而是只在用戶需要為 iPhone 12 系列充電時再吸上即可。
而 MagSafe 無線充電帶來的另一項好處,則是手機只需要裸機或使用支持 MagSafe 的保護殼即可讓充電配件固定在最佳的充電位置,確保無線充電過程順暢。此外,除了 Apple 原廠的 MagSafe 配件,近期也陸續有更多第三方配件廠商陸續推出支持 Apple MFM (Made for MagSafe)認證的配件。
近期隨著傳聞 Apple 將在未來加入行動充電模式後,網路上也有許多人陸續合成 MagSafe 充電配件的概念圖:

▲圖片來源:Antonio Adrian Chavez (Twitter/ @antonio07341176)

此外,在 iOS 14.5 Beta 2 預覽版本中加入 217 組全新表情符號:

▲圖片來源:MacRumors

有趣的是耳機的表情符號也改為 Apple 自家的主動式降噪頭戴式耳機 AirPods Max :
▲圖片來源:MacRumors

消息來源:MacRumors|Steve Moser (Twitter/ @SteveMoser)

延伸閱讀:
傳聞 Apple 將於 3/16 舉行春季新品發表會,或將推出全新 iPad mini 和 AirTags 等新品

iPhone 12 mini DXOMARK 相機評測成績揭曉:總分 122 與 iPhone 12 同分

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

iPhone 13 傳聞將搭載的 AOD 顯示,概念影片帶來多種可能性

日前傳聞 Apple 將在 iPhone 13 系列首次為 iPhone 帶來 AOD(Always-On Display)顯示的消息,而究竟 Apple 是會單純地「複製」 Android 手機在 AOD 顯示的方式,或延續 Apple Watch Series 5/Series 6 的方式讓螢幕在處於休眠狀態下能持續顯示豐富的內容,最近也有 YouTube 頻道就製作概念影片帶來多種可能方案。

▲圖片來源:A Better Computer (YouTube)

iPhone 13 傳聞將搭載的 AOD 顯示,概念影片帶來多種可能性

根據 EverythingApplePro 之前的爆料, iPhone 13 系列除了 iPhone 13 Pro 和 iPhone 13 Pro Max 螢幕將搭載 120Hz 螢幕更新率的 LTPO OLED 顯示器,同時也可能首次在 iPhone 加入 AOD 顯示。即便 Always On Display(AOD)息屏顯示在 Android 手機已經行之有年,不過目前在 Apple 旗下產品中也只有 Apple Watch Series 5 和 Apple Watch Series 6 有支援「永遠顯示」這項功能。
未來若真的在 iPhone 13 系列加入 AOD 顯示,即便用戶未抬起手機喚醒螢幕,時間顯示和其他資訊都能一目了然,用戶也能自定義 AOD 的顯示效果設定。

▲圖片來源:EverythingApplePro EAP(YouTube)

隨著 iPhone 13 搭載 AOD 顯示的傳聞曝光後,也有 YouTube 頻道「A Better Computer 」就製作了一段影片分析若 iPhone 真的搭載這項功能,會以哪幾種方式呈現的可能。
首先,如果最基本的可行方案就是比照目前有支持 AOD 功能的 Android 系統手機,在開啟這項功能後於螢幕休眠狀態下以單色方式顯示時間、天氣和有新通知的應用程式圖示。不過依照 Apple 過去的慣例,不太可能只單純使用這樣的方案。

▲圖片來源:A Better Computer (YouTube)

若參考目前已經有採用 AOD 顯示功能的 Apple Watch Series 5 、 Apple Watch Series 6 ,在螢幕顯示轉換為 AOD 顯示狀態時,依然保有基本的顯示內容,僅有亮度、對比度和色彩的有較明顯的差異,使用者依然能保有原本的使用體驗。

▲圖片來源:A Better Computer (YouTube)

不過如果也只是將 iOS 主畫面亮度、彩度、對比度都調低的顯示方式,考量自從 iOS 14 起加入桌面小工具讓內容更豐富的全新 iOS 主畫面,如果直接降低畫面亮度、彩度和對比度的做法,看來似乎也不太可行:

▲圖片來源:A Better Computer (YouTube)

倘若 Apple 以較保守的方式,可能會選擇將以深色顯示鎖定畫面桌布搭配基本時間、天氣顯示作為 AOD 顯示的畫面:

▲圖片來源:A Better Computer (YouTube)

也不排除將鎖定畫面下方的手電筒和相機快速啟動按鈕一併在 AOD 模式顯示:

▲圖片來源:A Better Computer (YouTube)

最終目前看來較有可能的方向則是在 iPhone 轉換為 AOD 顯示狀態時,除了以暗色畫面呈現保留基本的時間、天氣資訊外,在此模式下以深色模式呈現幾種自訂的 iOS 桌面小工具:

▲圖片來源:A Better Computer (YouTube)

相較於螢幕喚醒狀態,讓用戶在 AOD 模式下自訂有限、常用的桌面小工具,如此一來不僅能達到與 Android 系統之間的差異,也能將 iOS 小工具帶來更多應用的可能性:

▲圖片來源:A Better Computer (YouTube)

完整影片

 

消息來源:A Better Computer (YouTube)

延伸閱讀:
Jon Prosser 爆料全新 iPad Pro 與 AirTags 將於 3 月發表

小米11 Lite 最新概念渲染圖曝光!主打輕薄、平面挖孔螢幕的小米11 平價版

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

【其他文章推薦】

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

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

※想知道最厲害的網頁設計公司"嚨底家"!

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※回頭車貨運收費標準

※台中搬家公司費用怎麼算?

【asp.net core 系列】5 布局頁和靜態資源

0. 前言

在之前的4篇的內容里,我們較為詳細的介紹了路由以及控制器還有視圖之間的關係。也就是說,系統如何從用戶的HTTP請求解析到控制器里,然後在控制器里處理數據,並返回給視圖,在視圖中显示出來。這一篇我將為大家介紹基礎的最後一部分,布局頁和靜態資源引入。

1. 布局頁

在控制器和視圖那一篇,我們了解到_ViewStart 里設置了一個Layout屬性的值,這個值正是用來設置布局頁的。所謂的布局頁,就是視圖的公用代碼。在實際開發中,布局頁通常存放我們為整個系統定義的頁面框架,視圖裡寫每個視圖的頁面。

回顧一下,默認的_ViewStart里的內容是:

@{
    Layout = "_Layout";
}

默認的布局頁指定的是名為_Layout的布局頁,在本系列第三篇中,我們得知這個視圖應當在Shared文件夾下,那我們進去看一下這個視圖有什麼內容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - MvcWeb</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcWeb</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2020 - MvcWeb - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>

這是默認的布局頁內容,看着挺多的,但是除了一些html代碼,裏面還有一些關鍵的地方需要注意。

1.1 RenderSection

RenderSection 分部渲染,在頁面中創建一個標記,表示這個頁面塊將在子視圖(或者是路由的實際渲染視圖)中添加內容。

來,我們看一下微軟官方給的註釋:

In layout pages, renders the content of the section named name.

意思就是在布局頁中,渲染名稱為name的分部內容。

新創建一個分佈頁,名稱為_Layout1:

<html>
    <head>
        <title>Render 測試</title>
    </head>
    <body>
        @RenderSection("SectionDemo")
    </body>
</html>

這個布局頁里什麼都沒有,只有一個RenderSection。現在我們新建一個控制器:

using Microsoft.AspNetCore.Mvc;

namespace MvcWeb.Controllers
{
    public class RenderTestController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}

創建對應的視圖:

Views / RenderTest/Index.cshtml

先設置布局頁為_Layout1:

@{
    Layout = "_Layout1";
}

先試試啟動應用,訪問:

http://localhost:5006/RenderTest/Index

正常情況下,你應該能看到這個頁面:

仔細看一下信息,意思是在 RenderTest/Index.cshtml 視圖中沒有找到 SectionDemo 的分部內容。

那麼,如何在視圖中設置分部內容呢?

@{
    Layout = "_Layout1";
}
@section SectionDemo{
    <h1>你好</h1>

}

使用 @section <Section 名稱> 後面跟一對大括號,在大括號中間的內容就是這個section(分部)的內容。

重啟應用,然後刷新頁面,你能看到這樣的頁面:

如果不做特殊要求的話,定義在布局頁中的分部塊,視圖必須實現。當然,RenderSection還有一個參數,可以用來設置分部不是必須的:

public HtmlString RenderSection(string name, bool required);

1.2 RenderBody

先看下微軟給的官方註釋:

In a Razor layout page, renders the portion of a content page that is not within a named section.

簡單講,如果在布局頁中設置了@RenderBody,那麼在使用了這個布局頁的視圖裡所有沒被分部塊包裹的代碼都會渲染到布局頁中聲明了@RenderBody的地方。

修改_Layout1.cshtml:

<html>
    <head>
        <title>Render 測試</title>
    </head>
    <body>
        <h1>RenderBody 測試 -之前</h1>
        @RenderBody()
        <h1>RenderBody 測試 -之後</h1>
    </body>
</html>

修改RenderTest/Index.cshtml:

@{
    Layout = "_Layout1";
}

RenderBody測試
<h1>我是視圖的內容!</h1>

重啟應用,刷新剛剛訪問的頁面:

可以看出,RenderBody渲染的位置。

2. 靜態資源引入

通常情況下,靜態資源的引入與HTML引用js和css等資源是一致的,但是對於我們在編寫系統時自己創建的腳本和樣式表,asp.net core提供了不同的處理方式。那就是服務器端壓縮功能。

asp.net core 3.0 的mvc 默認項目是不啟動這個功能的,需要我們額外的開啟支持。

2.1 開啟支持

先引入 BuildBundleMinifier

cd MvcWeb # 切換目錄到MvcWeb項目下
dotnet add package BuildBundleMinifier

創建 bundleconfig.json

[
    {
        "outputFileName": "wwwroot/css/site.min.css",
        "inputFiles": [
            "wwwroot/css/site.css"
        ]
    },
  	{
        "outputFileName": "wwwroot/js/site.min.js",
        "inputFiles": [
          "wwwroot/js/site.js"
        ],
        "minify": {
          "enabled": true,
          "renameLocals": true
        },
        "sourceMap": false
      }
]

每個節點允許設置項:

  • outputFileName 生成的捆綁壓縮文件,通常路徑攜帶wwwroot
  • inputFiles 數組,包含要壓縮到此次輸出文件的文件路徑,會按照添加的順序依次加入
  • minify 輸出類型的縮小選項,可選。 默認是 enabled: true
  • sourceMap 表示是否為捆綁的文件生成源映射的標記
  • sourceMapRootPath 源映射文件的路徑

2.2 使用

正常情況下在布局頁中,把壓縮后的文件路徑引入即可。不過在開發中,通常按照以下方式引用:

<environment exclude="Development">
    <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
<environment include="Development">
    <link rel="stylesheet" href="~/css/site.css" />
</environment>

注: asp-append-version 表示在引用路徑追加一個版本號,這是針對html靜態資源緩存的問題的一個解決方案,這一步是由程序決定的。

environment表示環境,現在大家知道這個寫法就行,在接下來的篇幅會講。

3. 靜態資源目錄

我們知道到目前為止,我們的靜態資源都是在wwwroot目錄下。那麼我們是否可以修改或者添加別的目錄作為靜態資源目錄呢?

在Startup.cs文件內的Configure方法下有這樣一行代碼:

app.UseStaticFiles();

這行代碼的意思就是啟用靜態文件,程序自動從 wwwroot尋找資源。那麼,我們就可以從這個方法入手,設置我們自己的靜態資源:

public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, StaticFileOptions options);

我們找到了這個方法的另一個重載版本,裏面有一個參數類:

public class StaticFileOptions : SharedOptionsBase
{
    public StaticFileOptions();
    public StaticFileOptions(SharedOptions sharedOptions);
    public IContentTypeProvider ContentTypeProvider { get; set; }
    public string DefaultContentType { get; set; }
    public HttpsCompressionMode HttpsCompression { get; set; }
    public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
    public bool ServeUnknownFileTypes { get; set; }
}

並沒有發現我們想要的,先別慌,它還有個父類。我們再去它的父類里看看:

public abstract class SharedOptionsBase
{
    protected SharedOptionsBase(SharedOptions sharedOptions);
    public IFileProvider FileProvider { get; set; }
    public PathString RequestPath { get; set; }
    protected SharedOptions SharedOptions { get; }
}

這下就比較明了了,需要我們提供一個文件提供器,那麼我們來找一個合適的IFileProvider實現類吧:

public class PhysicalFileProvider : IFileProvider, IDisposable

這個類可以滿足我們的要求,它位於命名空間:

namespace Microsoft.Extensions.FileProviders

那麼,添加一組我們自己的配置吧:

using Microsoft.Extensions.FileProviders;

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // 省略其他代碼,僅添加以下代碼
    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(),"OtherStatic")),
    });
}

在項目的根目錄創建名為OtherStatic的文件夾,然後在裏面創建個文件夾,例如: files,並在這個文件夾里隨便添加一個文件。

然後啟動應用訪問:

http://localhost:5006/files/<你添加的文件(包括後綴名)>

然後能在瀏覽器中看到這個文件被正確響應。

當然,這裏存在一個問題,如果在 OtherStatic中的文件在wwwroot也有相同目錄結構的文件存在,這樣訪問就會出現問題。這時候,可以為我們新加的這個配置設置一個請求前綴:

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(),"OtherStatic")),
    RequestPath = "/other"
});

重啟程序,然後訪問:

http://localhost:5006/other/files/<你添加的文件(包括後綴名)>

然後就能看到剛才響應的文件,重新訪問之前的路徑,發現瀏覽器提示404。

4. 總結

在這一篇,我們講解了布局頁的內容,靜態資源的壓縮綁定以及添加一個新的靜態資源目錄。通過這幾篇內容,讓我們對asp.net core mvc有了一個基本的認知。下一篇,我們將重新創建一個項目,並結合之前的內容,以實戰為背景,帶領大家完成一個功能完備的web系統。

更多內容煩請關注我的博客《高先生小屋》

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

【其他文章推薦】

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

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

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

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

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

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

[OpenGL](翻譯+補充)投影矩陣的推導

簡介

基本是翻譯和補充 http://www.songho.ca/opengl/gl_projectionmatrix.html

計算機显示器是一個2D的平面,一個3D的場景要被OpenGL渲染必須被投影到2D平面上以生成2D的圖像。在OpenGL中,GL_PROJECTION矩陣可以用來進行投影變換。首先,它將所有的頂點數據從相機坐標系(eye coordinates)轉換到裁剪坐標系(clip coordinates),然後通過除以裁剪空間坐標的w值,將裁剪空間坐標系轉換到歸一化設備坐標系(normalized device coordinates,NDC)

我們需要注意的一點就是,裁剪和NDC變換都通過GL_PROJECTION矩陣來完成。之後的文章,將會利用6個參數來構建投影矩陣,這六個參數是:left,right,bottom,top,near,far,分別為近裁剪面的左右下上邊界,近裁剪面,遠裁剪面。

視錐體剔除是在裁剪坐標下進行的,在轉換到NDC坐標系之前。已經變換到裁剪坐標系的坐標\(x_c,y_c,z_c\)會和\(w_c\)進行比較,如果裁剪坐標大於\(w_c\)或小於\(-w_c\),則頂點會被剔除,OpenGL會重建多邊形的邊。
ps.解釋一下為什麼要和\(w_c\)進行比較。因為NDC坐標的範圍是\([-1,1]\),而裁剪坐標和NDC坐標之間的關係是\(x_c/w_c = x_n\),所以\(x_c\)必須得在\([-w_c,w_c]\)之間才可見,其他兩個軸同理。不是在NDC坐標階段進行裁剪,是因為不可見的頂點,沒有必要在對其進行運算,會消耗資源。在作用完投影矩陣后,得到的是齊次坐標,OpenGL會自動除以\(w_c\),以得到笛卡爾坐標,OpenGL應該是在除以\(w_c\)之前進行視錐體剔除工作。

透視投影

在透視投影中,1個3D的點在一個像被切了一刀的金字塔的視錐體中,此時的坐標系是相機坐標系,這個坐標系會被映射正方體的NDC坐標系中。

  • \(x:[l,r]->[-1,1]\)
  • \(y:[b,t]->[-1,1]\)
  • \(z:[-n,-f]->[-1,1]\)

相機坐標系定義在右手坐標系,NDC是左手坐標系,所以相機朝着-Z的方向看去,而NDC朝着+Z的方向看去。因為glFrustum()裁剪面的參數必須為正數,所以在創建投影矩陣的時候,我們要對其進行去取反。
ps.glFrustum是opengl類庫中的函數,它是將當前矩陣與一個透視矩陣相乘,把當前矩陣轉變成透視矩陣,在使用它之前,通常會先調用glMatrixMode(GL_PROJECTION).
void glFrustum(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble nearVal, GLdouble farVal),left,right指明相對於垂直平面的左右坐標位置,bottom,top指明相對於水平剪切面的下上位置,nearVal,farVal指明相對於深度剪切面的遠近的距離,兩個必須為正數。

在OpenGL中,1個3D的點將會被投影到近裁剪平面上,下圖展示了點\((x_e,y_e,z_e)\)如何投影到\((x_p,y_p,z_p)\)。

在視錐體的頂視圖,我們可以利用相似三角形計算\(x_p\)的值

\[\frac{x_p}{x_e} = \frac{-n}{z_e}\\ x_p = \frac{-nx_e}{z_e}=\frac{nx_e}{-z_e} \]

同理,在側視圖中,利用相似三角形計算\(y_p\)的值

\[\frac{y_p}{y_e} = \frac{-n}{z_e}\\ y_p = \frac{-ny_e}{z_e}=\frac{ny_e}{-z_e} \]

我們觀察到\(x_p,y_p\)都依賴於\(z_e\),他們都除以\(z_e\),這是第一個線索,來幫助我們構建透視投影矩陣。當相機坐標系經過透視投影矩陣變換后,得到的是裁剪坐標系的齊次坐標,最後通過除以齊次坐標的\(w_c\),來得到NDC

\[\begin{bmatrix} x_c\\ y_c\\ z_c\\ w_c \end{bmatrix} =M_{projection} \begin{bmatrix} x_e\\ y_e\\ z_e\\ w_e \end{bmatrix} , \begin{bmatrix} x_n\\ y_n\\ z_n\\ \end{bmatrix} = \begin{bmatrix} x_c/w_c\\ y_c/w_c\\ z_c/w_c\\ \end{bmatrix} \]

因此我們可以設置\(w_c\)的值為\(-z_e\),現在投影矩陣看起來是

\[\begin{bmatrix} x_c\\ y_c\\ z_c\\ w_c \end{bmatrix} = \begin{bmatrix} .&.&.&.\\ .&.&.&.\\ .&.&.&.\\ 0&0&-1&0\\ \end{bmatrix} \begin{bmatrix} x_e\\ y_e\\ z_e\\ w_e \end{bmatrix} \]

接着,我們需要將\(x_p,y_p\)映射到\(x_n,y_n\),\([l,r]->[-1,1],[b,t]->[-1,1]\)。
相當於是給定l,我要得到-1,給定r,我要得到1,這不就是給定二維平面上的兩個點,求其直線方程的問題。

\[令x_n = kx_p+\beta,求其斜率為\frac{1-(-1)}{r-l}=\frac{2}{r-l}\\ 帶入點(r,1),1 = \frac{2r}{r-l}+\beta\\ 化簡求得\beta=-\frac{r+l}{r-l}\\ 最終得x_n = \frac{2x_p}{r-l}-\frac{r+l}{r-l} \]

\[令y_n = ky_p+\beta,求其斜率為\frac{1-(-1)}{t-b}=\frac{2}{t-b}\\ 帶入點(t,1),1 = \frac{2t}{t-b}+\beta\\ 化簡求得\beta=-\frac{t+b}{t-b}\\ 最終得y_n = \frac{2y_p}{t-b}-\frac{t+b}{t-b} \]

現在有了從\(x_e,y_e\)到\(x_p,y_p\)和從\(x_p,y_p\)到\(x_n,y_n\),現在聯立一下就可以得到從\(x_e,y_e\)到\(x_n,y_n\)的關係表達式。

\[x_n = \frac{2x_p}{r-l}-\frac{r+l}{r-l}\\ x_p = \frac{-nx_e}{z_e}=\frac{nx_e}{-z_e}\\ 最終可以化簡為(\underbrace{\frac{2n}{r-l}x_e+\frac{r+l}{r-l}z_e}_{x_c})/-z_e \]

同理

\[y_n = \frac{2y_p}{t-b}-\frac{t+b}{t-b}\\ y_p = \frac{-ny_e}{z_e}=\frac{ny_e}{-z_e}\\ 最終可以化簡為(\underbrace{\frac{2n}{t-b}y_e+\frac{t+b}{t-b}z_e}_{y_c})/-z_e \]

現在我們的透視矩陣現在是這個樣子

\[\begin{bmatrix} x_c\\ y_c\\ z_c\\ w_c \end{bmatrix} = \begin{bmatrix} \frac{2n}{r-l}&0&\frac{r+l}{r-l}&0\\ 0&\frac{2n}{t-b}&\frac{t+b}{t-b}&0\\ .&.&.&.\\ 0&0&-1&0\\ \end{bmatrix} \begin{bmatrix} x_e\\ y_e\\ z_e\\ w_e \end{bmatrix} \]

現在還剩下矩陣的第三行。\(z_n\)和其他兩個軸的坐標稍有不同,因為\(z_e\)總是投影到-n的近裁剪面,但是我們需要不同的z值來進行裁剪和深度測試,另外我們應該可以進行逆操作(逆變換)。因為我們知道z的值不依賴於x,y,我們借用w的值來尋找\(z_n,z_e\)之間的關係,因此我們指定第三行矩陣為

\[\begin{bmatrix} x_c\\ y_c\\ z_c\\ w_c \end{bmatrix} = \begin{bmatrix} \frac{2n}{r-l}&0&\frac{r+l}{r-l}&0\\ 0&\frac{2n}{t-b}&\frac{t+b}{t-b}&0\\ 0&0&A&B\\ 0&0&-1&0\\ \end{bmatrix} \begin{bmatrix} x_e\\ y_e\\ z_e\\ w_e \end{bmatrix} \]

\[z_n = z_c/w_c = \frac{Az_e+Bw_e}{-z_e} \]

在相機坐標系中,\(w_e\)的值是1,因此有\(z_n = \frac{Az_e+B}{-z_e}\),為了獲得A和B的值,我們使用\((z_e,z_n)\)的關係,\((-n,-1),(-f,1)\),然後將他們代入表達式。

\[\frac{-An+B}{n}=-1\\ \frac{-Af+B}{f}=1 \]

聯立,這是一個簡單二元一次方程組,容易求得

\[A = -\frac{f+n}{f-n}\\ B = -\frac{2fn}{f-n} \]

所以最終得到

\[z_n = \frac{-\frac{f+n}{f-n}z_e–\frac{2fn}{f-n}}{-z_e} \]

最終整個投影矩陣的表達式為

\[\begin{bmatrix} x_c\\ y_c\\ z_c\\ w_c \end{bmatrix} = \begin{bmatrix} \frac{2n}{r-l}&0&\frac{r+l}{r-l}&0\\ 0&\frac{2n}{t-b}&\frac{t+b}{t-b}&0\\ 0&0&-\frac{f+n}{f-n}&-\frac{2fn}{f-n}\\ 0&0&-1&0\\ \end{bmatrix} \begin{bmatrix} x_e\\ y_e\\ z_e\\ w_e \end{bmatrix} \]

這個投影矩陣是一般的視錐體,如果是對稱的話,有\(r=-l,t=-b\),那麼有

\[r+l=0,r-l=2r(width)\\ t+b=0,t-b=2t(height) \]

最後矩陣可以簡單的化為

\[\begin{bmatrix} \frac{n}{r}&0&0&0\\ 0&\frac{n}{t}&0&0\\ 0&0&-\frac{f+n}{f-n}&-\frac{2fn}{f-n}\\ 0&0&-1&0\\ \end{bmatrix} \]

注意觀察\(z_e,z_n\)的關係式,這是一個非線性的反比例函數,這意味着,在近裁剪平面的是很好,精度很高,而在遠裁剪面的時候,精度很低。當\([-n,-f]\)很大時,可能導致深度精度問題(z-fighting),一個較小的\(z_e\)的變化,在遠裁剪面可能不會影響\(z_n\)的值,n和f之間的距離應該短一些,從而最小化這個問題。
ps.因為浮點數會存在精度問題,畢竟計算機的存儲是離散的。

正交投影

正交投影的要比透視投影簡單許多,\(x_e,y_e,z_e\)相機坐標系將會線性映射到NDC坐標系。我們僅需要將長方體變為正方體,然後移動至原點。

\[x_n = \frac{1-(-1)}{r-l}x_e+\beta\\ 代入(r,1),最終可得\\ x_n = \frac{2}{r-l}x_e-\frac{r+l}{r-l} \]

同理

\[y_n = \frac{1-(-1)}{t-b}y_e+\beta\\ 代入(t,1),最終可得\\ y_n = \frac{2}{t-b}y_e-\frac{t+b}{t-b} \]

同理

\[z_n = \frac{1-(-1)}{-f-(-n)}z_e+\beta\\ 代入(-f,1),最終可得\\ z_n = \frac{-2}{f-n}z_e-\frac{f+n}{f-n} \]

因為w的值在正交投影中不必要,所以我們設置為1,因此正交投影矩陣為

\[ \begin{bmatrix} \frac{2}{r-l}&0&0&-\frac{r+l}{r-l}\\ 0&\frac{2}{t-b}&0&-\frac{t+b}{t-b}\\ 0&0&-\frac{2}{f-n}&-\frac{f+n}{f-n}\\ 0&0&0&1\\ \end{bmatrix} \]

同透視投影一樣,如果是對稱的話,那麼就可以矩陣就可以變簡單

\[ \begin{bmatrix} \frac{1}{r}&0&0&0\\ 0&\frac{1}{t}&0&0\\ 0&0&-\frac{2}{f-n}&-\frac{f+n}{f-n}\\ 0&0&0&1\\ \end{bmatrix} \]

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

【其他文章推薦】

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

※網頁設計最專業,超強功能平台可客製化

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

STL常用序列容器

這裏簡要的記述一下STL常用容器的實現原理,要點等內容。

vector

vector是比較常用的stl容器,用法與數組是非類似,其內部實現是連續空間分配,與數組的不同之處在於可彈性增加空間,而array是靜態空間,分配后不能動態擴展。vecotr的實現較為簡單,主要的關鍵點在於當空間不足時,會新分配當前空間2倍的空間,將舊空間數據拷貝到新空間,然後刪除舊空間。

struct _Vector_impl: public _Tp_alloc_type {
    pointer _M_start;   // 元素頭
    pointer _M_finish;  // 元素尾
    pointer _M_end_of_storage;  // 可用空間尾,
          // 省略部分代碼...
};

這個是向尾部添加元素的代碼實現,可以看到如果當前還有剩餘空間的話,直接在尾部添加,如果沒有剩餘空間,則會動態擴展。

void push_back(const value_type& __x) {
    if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage) {
	    _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish, __x);
	    ++this->_M_impl._M_finish;
    } else
	  _M_realloc_insert(end(), __x);
}

template<typename _Tp, typename _Alloc>
void vector<_Tp, _Alloc>::_M_realloc_insert(iterator __position, const _Tp& __x) {
    const size_type __len = _M_check_len(size_type(1), "vector::_M_realloc_insert");      // 2倍當前大小
    const size_type __elems_before = __position - begin();
    pointer __new_start(this->_M_allocate(__len));
    pointer __new_finish(__new_start);
    __try {
	  // The order of the three operations is dictated by the C++11
	  // case, where the moves could alter a new element belonging
	  // to the existing vector.  This is an issue only for callers
	  // taking the element by lvalue ref (see last bullet of C++11
	  // [res.on.arguments]).
	  _Alloc_traits::construct(this->_M_impl, __new_start + __elems_before, __x);
	  __new_finish = pointer();

	  __new_finish = std::__uninitialized_move_if_noexcept_a(this->_M_impl._M_start, __position.base(), __new_start, _M_get_Tp_allocator();

	  ++__new_finish;

	  __new_finish = std::__uninitialized_move_if_noexcept_a(__position.base(), this->_M_impl._M_finish, __new_finish, _M_get_Tp_allocator());
	}__catch(...) {
	  if (!__new_finish)
	    _Alloc_traits::destroy(this->_M_impl,
				   __new_start + __elems_before);
	  else
	    std::_Destroy(__new_start, __new_finish, _M_get_Tp_allocator());
	  _M_deallocate(__new_start, __len);
	  __throw_exception_again;
	}
      std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish, _M_get_Tp_allocator());
      _M_deallocate(this->_M_impl._M_start, this->_M_impl._M_end_of_storage - this->_M_impl._M_start);
      this->_M_impl._M_start = __new_start;
      this->_M_impl._M_finish = __new_finish;
      this->_M_impl._M_end_of_storage = __new_start + __len;
    }

// Called by _M_fill_insert, _M_insert_aux etc.
size_type _M_check_len(size_type __n, const char* __s) const {
    if (max_size() - size() < __n)
	  __throw_length_error(__N(__s));

    const size_type __len = size() + std::max(size(), __n);       // 二倍長
    return (__len < size() || __len > max_size()) ? max_size() : __len;
}

pointer _M_allocate(size_t __n) {
    typedef __gnu_cxx::__alloc_traits<_Tp_alloc_type> _Tr;
    return __n != 0 ? _Tr::allocate(_M_impl, __n) : pointer();
}

使用時可使用[],因為其已實現重載[]。

      reference
      operator[](size_type __n) _GLIBCXX_NOEXCEPT {
	      __glibcxx_requires_subscript(__n);
	      return *(this->_M_impl._M_start + __n);
      }

使用時要注意迭代器失效問題,這個在很多STL容器中都有這個問題。

list

鏈表list,與vector不同,元素在內存中不連續分配,不支持隨機存取,好處就是插入與刪除時間複雜度為O(1)。在STL中,其實現的是雙向鏈表,其節點的定義可以看到有前驅和後繼指針,實現也較為簡單。

  /// An actual node in the %list.
template<typename _Tp>
struct _List_node : public __detail::_List_node_base {
    _Tp _M_data;
    _Tp* _M_valptr() { return std::__addressof(_M_data); }
    _Tp const* _M_valptr() const { return std::__addressof(_M_data); }
};

struct _List_node_base {
    _List_node_base* _M_next;
    _List_node_base* _M_prev;

    static void swap(_List_node_base& __x, _List_node_base& __y) _GLIBCXX_USE_NOEXCEPT;
    void _M_transfer(_List_node_base* const __first, _List_node_base* const __last) _GLIBCXX_USE_NOEXCEPT;
    void _M_reverse() _GLIBCXX_USE_NOEXCEPT;
    void _M_hook(_List_node_base* const __position) _GLIBCXX_USE_NOEXCEPT;
    void _M_unhook() _GLIBCXX_USE_NOEXCEPT;
};

deque

雙端隊列,具體實現不同於vector與list,它是一小段一小段連續空間,每段連續空間之間通過指針數組(這個數組中存放的是每個連續空間數組的頭指針)串聯起來,這樣就能訪問到所有元素。之所以採用這種存儲布局,是有原因的,是有其應用場景的,等分析完源碼后,我們就明白其為何要這麼做了。

deque源碼分析

我們摘取部分源碼看一下其實現細節。雙端隊列的迭代器實現代碼如下(相較於vector與list,對元素的訪問因為其存儲布局不同,在每一段連續分配空間的邊緣要做特殊處理):

#define _GLIBCXX_DEQUE_BUF_SIZE 512       // 默認連續空間大小

  _GLIBCXX_CONSTEXPR inline size_t __deque_buf_size(size_t __size) { 
        return (__size < _GLIBCXX_DEQUE_BUF_SIZE ? size_t(_GLIBCXX_DEQUE_BUF_SIZE / __size) :size_t(1)); 
    }
template<typename _Tp, typename _Ref, typename _Ptr>
    struct _Deque_iterator {
      typedef _Deque_iterator<_Tp, _Tp&, _Tp*>	     iterator;
      typedef _Deque_iterator<_Tp, const _Tp&, const _Tp*> const_iterator;
      typedef _Tp*					 _Elt_pointer;
      typedef _Tp**					_Map_pointer;

      static size_t _S_buffer_size() _GLIBCXX_NOEXCEPT { 
            return __deque_buf_size(sizeof(_Tp)); 
      }

      typedef std::random_access_iterator_tag	iterator_category;
      typedef _Tp				value_type;
      typedef _Ptr				pointer;
      typedef _Ref				reference;
      typedef size_t				size_type;
      typedef ptrdiff_t				difference_type;
      typedef _Deque_iterator			_Self;

      _Elt_pointer _M_cur;          // 當前位置
      _Elt_pointer _M_first;        // 每一小段空間的開始
      _Elt_pointer _M_last;         // 每一小段空間的結束
      _Map_pointer _M_node;         // 指針數組,可通過這裏訪問到所有連續存儲空間片段

      // 構造函數
      _Deque_iterator(_Elt_pointer __x, _Map_pointer __y) _GLIBCXX_NOEXCEPT : _M_cur(__x), _M_first(*__y),
_M_last(*__y + _S_buffer_size()), _M_node(__y) { }

      _Deque_iterator() _GLIBCXX_NOEXCEPT: _M_cur(), _M_first(), _M_last(), _M_node() { }

      _Deque_iterator(const iterator& __x) _GLIBCXX_NOEXCEPT: _M_cur(__x._M_cur), _M_first(__x._M_first),
	_M_last(__x._M_last), _M_node(__x._M_node) { }

      iterator _M_const_cast() const _GLIBCXX_NOEXCEPT {
          return iterator(_M_cur, _M_node);     // 返回當前元素迭代器
      }

      reference operator*() const _GLIBCXX_NOEXCEPT { 
          return *_M_cur; 
      }

      pointer operator->() const _GLIBCXX_NOEXCEPT { 
          return _M_cur; 
      }

      // 重載++運算符,可以看到,當_M_cur指向本段連續空間尾部時,訪問下一個元素的話是下一段連續空間的首地址
      _Self& operator++() _GLIBCXX_NOEXCEPT {   
	    ++_M_cur;     
	    if (_M_cur == _M_last) {  
	        _M_set_node(_M_node + 1);   // 移向下一段連續存儲空間
	        _M_cur = _M_first;          // 下一段連續空間的首元素
	    }
	    return *this;
      }

      _Self operator++(int) _GLIBCXX_NOEXCEPT {
	    _Self __tmp = *this;
	    ++*this;
	    return __tmp;
      }

      _Self& operator--() _GLIBCXX_NOEXCEPT {
	    if (_M_cur == _M_first) {             // 與++類似,如果當前是第一個元素,--時,就應該調到上一個連續存儲空間
	        _M_set_node(_M_node - 1);
	        _M_cur = _M_last;           // 移到上一段空間的最後,
	    }
	    --_M_cur;                       // 因為是[start, last)區間,這裏要--_M_cur;
	    return *this;
      }

      _Self operator--(int) _GLIBCXX_NOEXCEPT {
	    _Self __tmp = *this;
	    --*this;
	    return __tmp;
      }

      _Self& operator+=(difference_type __n) _GLIBCXX_NOEXCEPT {
	    const difference_type __offset = __n + (_M_cur - _M_first);
	    if (__offset >= 0 && __offset < difference_type(_S_buffer_size()))  // 如果當前連續空間滿足
	       _M_cur += __n;
	    else {  // 如果當前段連續空間不夠用了,需要計算跳到連續空間
	        const difference_type __node_offset = __offset > 0 ? __offset / difference_type(_S_buffer_size()) : -difference_type((-__offset - 1) / _S_buffer_size()) - 1;
	        _M_set_node(_M_node + __node_offset);
	        _M_cur = _M_first + (__offset - __node_offset * difference_type(_S_buffer_size()));
	    }
	    return *this;
      }

      _Self operator+(difference_type __n) const _GLIBCXX_NOEXCEPT {
	    _Self __tmp = *this;
	    return __tmp += __n;
      }

      _Self& operator-=(difference_type __n) _GLIBCXX_NOEXCEPT {
          return *this += -__n; }

      _Self operator-(difference_type __n) const _GLIBCXX_NOEXCEPT {
	    _Self __tmp = *this;
	    return __tmp -= __n;
      }

      reference operator[](difference_type __n) const _GLIBCXX_NOEXCEPT { return *(*this + __n); }

      // Prepares to traverse new_node.  Sets everything except _M_cur, which should therefore be set by the caller immediately afterwards, based on _M_first and _M_last.
      void _M_set_node(_Map_pointer __new_node) _GLIBCXX_NOEXCEPT {     // 跳到新的一段連續存儲空間
	    _M_node = __new_node;
	    _M_first = *__new_node;
	    _M_last = _M_first + difference_type(_S_buffer_size());
      }
    };

從上面deque迭代器的實現來看,主要需要注意的地方就是每段連續空間的邊緣。看完迭代器后,我們看一下deque類的實現代碼,這裏刪減掉大部分代碼,保留部分代碼。其中重點看一下deque中最常用的push_front、pop_front與push_back、pop_back的實現。push_back時間複雜度O(1)比較好理解,過程類似於vector,但push_front為什麼也是O(1)呢?如果在頭部插入一個元素,第一個連續空間距離起始start還有剩餘空間的的話,直接插入就好了,如果沒有剩餘空間的話,就創建一段新的連續空間,將首地址放到map中,如果map沒有空間放置這個首地址,就調整map,再插入首地址,詳細過程請看源碼的具體實現:

 template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
    class deque : protected _Deque_base<_Tp, _Alloc> {
      typedef _Deque_base<_Tp, _Alloc>			_Base;
      typedef typename _Base::_Tp_alloc_type		_Tp_alloc_type;
      typedef typename _Base::_Alloc_traits		_Alloc_traits;
      typedef typename _Base::_Map_pointer		_Map_pointer;

    public:
      typedef _Tp					value_type;
      typedef typename _Alloc_traits::pointer		pointer;
      typedef typename _Alloc_traits::const_pointer	const_pointer;
      typedef typename _Alloc_traits::reference		reference;
      typedef typename _Alloc_traits::const_reference	const_reference;
      typedef typename _Base::iterator			iterator;
      typedef typename _Base::const_iterator		const_iterator;
      typedef std::reverse_iterator<const_iterator>	const_reverse_iterator;
      typedef std::reverse_iterator<iterator>		reverse_iterator;
      typedef size_t					size_type;
      typedef ptrdiff_t					difference_type;
      typedef _Alloc					allocator_type;

    protected:
      static size_t _S_buffer_size() _GLIBCXX_NOEXCEPT { return __deque_buf_size(sizeof(_Tp)); }

      // Functions controlling memory layout, and nothing else.
      using _Base::_M_initialize_map;
      using _Base::_M_create_nodes;
      using _Base::_M_destroy_nodes;
      using _Base::_M_allocate_node;
      using _Base::_M_deallocate_node;
      using _Base::_M_allocate_map;
      using _Base::_M_deallocate_map;
      using _Base::_M_get_Tp_allocator;

      /**
       *  A total of four data members accumulated down the hierarchy.
       *  May be accessed via _M_impl.*
       */
      using _Base::_M_impl;

    public:
	  // 省略構造函數與析構函數......

      /*
       *  @brief  Assigns a given value to a %deque.
       *  @param  __n  Number of elements to be assigned.
       *  @param  __val  Value to be assigned.
       *
       *  This function fills a %deque with @a n copies of the given
       *  value.  Note that the assignment completely changes the
       *  %deque and that the resulting %deque's size is the same as
       *  the number of elements assigned.
       */
      void assign(size_type __n, const value_type& __val) { _M_fill_assign(__n, __val); }

	  // 省略其他assign重載函數......	


      /// Get a copy of the memory allocation object.
      allocator_type get_allocator() const _GLIBCXX_NOEXCEPT{ return _Base::get_allocator(); }

      // iterators
      /**
       *  Returns a read/write iterator that points to the first element in the
       *  %deque.  Iteration is done in ordinary element order.
       */
      iterator begin() _GLIBCXX_NOEXCEPT { return this->_M_impl._M_start; }
      const_iterator begin() const _GLIBCXX_NOEXCEPT { return this->_M_impl._M_start; }

      /**
       *  Returns a read/write iterator that points one past the last
       *  element in the %deque.  Iteration is done in ordinary
       *  element order.
       */
      iterator end() _GLIBCXX_NOEXCEPT{ return this->_M_impl._M_finish; }
      const_iterator end() const _GLIBCXX_NOEXCEPT { return this->_M_impl._M_finish; }


	// 省略其他迭代器相關代碼......

      // [23.2.1.2] capacity
      /**  Returns the number of elements in the %deque.  */
      size_type size() const _GLIBCXX_NOEXCEPT { return this->_M_impl._M_finish - this->_M_impl._M_start; }

      /**  Returns the size() of the largest possible %deque.  */
      size_type max_size() const _GLIBCXX_NOEXCEPT { return _Alloc_traits::max_size(_M_get_Tp_allocator()); }

      /**
       *  @brief  Resizes the %deque to the specified number of elements.
       *  @param  __new_size  Number of elements the %deque should contain.
       *
       *  This function will %resize the %deque to the specified
       *  number of elements.  If the number is smaller than the
       *  %deque's current size the %deque is truncated, otherwise
       *  default constructed elements are appended.
       */
      void resize(size_type __new_size) {
		  const size_type __len = size();
		  if (__new_size > __len)
	  		  _M_default_append(__new_size - __len);
		  else if (__new_size < __len)
	  		  _M_erase_at_end(this->_M_impl._M_start + difference_type(__new_size));
      }

#if __cplusplus >= 201103L
      /**  A non-binding request to reduce memory use.  */
      void shrink_to_fit() noexcept { _M_shrink_to_fit(); }
#endif

      /**
       *  Returns true if the %deque is empty.  (Thus begin() would
       *  equal end().)
       */
      bool empty() const _GLIBCXX_NOEXCEPT { return this->_M_impl._M_finish == this->_M_impl._M_start; }

      // element access
      /**
       *  @brief Subscript access to the data contained in the %deque.
       *  @param __n The index of the element for which data should be
       *  accessed.
       *  @return  Read/write reference to data.
       *
       *  This operator allows for easy, array-style, data access.
       *  Note that data access with this operator is unchecked and
       *  out_of_range lookups are not defined. (For checked lookups
       *  see at().)
       */
      reference operator[](size_type __n) _GLIBCXX_NOEXCEPT {
		  __glibcxx_requires_subscript(__n);
		  return this->_M_impl._M_start[difference_type(__n)];
      }

    protected:
      /// Safety check used only from at().
      void _M_range_check(size_type __n) const {
		  if (__n >= this->size())
	  		  __throw_out_of_range_fmt(__N("deque::_M_range_check: __n "
				       "(which is %zu)>= this->size() "
				       "(which is %zu)"), __n, this->size());
      }

    public:
      /**
       *  @brief  Provides access to the data contained in the %deque.
       *  @param __n The index of the element for which data should be
       *  accessed.
       *  @return  Read/write reference to data.
       *  @throw  std::out_of_range  If @a __n is an invalid index.
       *
       *  This function provides for safer data access.  The parameter
       *  is first checked that it is in the range of the deque.  The
       *  function throws out_of_range if the check fails.
       */
      reference at(size_type __n) {
		  _M_range_check(__n);
		  return (*this)[__n];
      }

      /**
       *  @brief  Provides access to the data contained in the %deque.
       *  @param __n The index of the element for which data should be
       *  accessed.
       *  @return  Read-only (constant) reference to data.
       *  @throw  std::out_of_range  If @a __n is an invalid index.
       *
       *  This function provides for safer data access.  The parameter is first
       *  checked that it is in the range of the deque.  The function throws
       *  out_of_range if the check fails.
       */
      const_reference at(size_type __n) const {
		  _M_range_check(__n);
		  return (*this)[__n];
      }

      /**
       *  Returns a read/write reference to the data at the first
       *  element of the %deque.
       */
      reference front() _GLIBCXX_NOEXCEPT {
		  __glibcxx_requires_nonempty();
		  return *begin();
      }

      /**
       *  Returns a read/write reference to the data at the last element of the
       *  %deque.
       */
      reference back() _GLIBCXX_NOEXCEPT {
		  __glibcxx_requires_nonempty();
		  iterator __tmp = end();
		  --__tmp;
		  return *__tmp;
      }


      /**
       *  @brief  Add data to the front of the %deque.
       *  @param  __x  Data to be added.
       *
       *  This is a typical stack operation.  The function creates an
       *  element at the front of the %deque and assigns the given
       *  data to it.  Due to the nature of a %deque this operation
       *  can be done in constant time.
       */
      void push_front(const value_type& __x) {		// 如果第一段連續空間頭部還有剩餘空間的話,直接插入元素
		  if (this->_M_impl._M_start._M_cur != this->_M_impl._M_start._M_first) {
	      	_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_start._M_cur - 1, __x);
	      	--this->_M_impl._M_start._M_cur;
	  	  } else		// 如果沒有,在前部重新分配空間
	    	  _M_push_front_aux(__x);
      }

      /**
       *  @brief  Add data to the end of the %deque.
       *  @param  __x  Data to be added.
       *
       *  This is a typical stack operation.  The function creates an
       *  element at the end of the %deque and assigns the given data
       *  to it.  Due to the nature of a %deque this operation can be
       *  done in constant time.
       */
      void push_back(const value_type& __x) {
		  if (this->_M_impl._M_finish._M_cur != this->_M_impl._M_finish._M_last - 1) {
	    	  _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish._M_cur, __x);
	          ++this->_M_impl._M_finish._M_cur;
	  	  } else 
			_M_push_back_aux(__x);
      }

      /**
       *  @brief  Removes first element.
       *
       *  This is a typical stack operation.  It shrinks the %deque by one.
       *
       *  Note that no data is returned, and if the first element's data is
       *  needed, it should be retrieved before pop_front() is called.
       */
      void pop_front() _GLIBCXX_NOEXCEPT {
		  __glibcxx_requires_nonempty();
		  if (this->_M_impl._M_start._M_cur != this->_M_impl._M_start._M_last - 1) {
	    	  _Alloc_traits::destroy(this->_M_impl,	this->_M_impl._M_start._M_cur);
	    	  ++this->_M_impl._M_start._M_cur;
	  	  } else
	  	  _M_pop_front_aux();
      }

      /**
       *  @brief  Removes last element.
       *
       *  This is a typical stack operation.  It shrinks the %deque by one.
       *
       *  Note that no data is returned, and if the last element's data is
       *  needed, it should be retrieved before pop_back() is called.
       */
      void pop_back() _GLIBCXX_NOEXCEPT {
		  __glibcxx_requires_nonempty();
		  if (this->_M_impl._M_finish._M_cur != this->_M_impl._M_finish._M_first) {
	          --this->_M_impl._M_finish._M_cur;
	    	  _Alloc_traits::destroy(this->_M_impl, this->_M_impl._M_finish._M_cur);
	  	  } else
	  	  _M_pop_back_aux();
      }

      /**
       *  @brief  Inserts given value into %deque before specified iterator.
       *  @param  __position  An iterator into the %deque.
       *  @param  __x  Data to be inserted.
       *  @return  An iterator that points to the inserted data.
       *
       *  This function will insert a copy of the given value before the
       *  specified location.
       */
      iterator insert(iterator __position, const value_type& __x);

      /**
       *  Erases all the elements.  Note that this function only erases the
       *  elements, and that if the elements themselves are pointers, the
       *  pointed-to memory is not touched in any way.  Managing the pointer is
       *  the user's responsibility.
       */
      void clear() _GLIBCXX_NOEXCEPT { _M_erase_at_end(begin()); }

    protected:
      // Internal constructor functions follow.
	  // 省略部分代碼......
	 	
      void _M_push_back_aux(const value_type&);
      void _M_push_front_aux(const value_type&);
      void _M_pop_back_aux();
      void _M_pop_front_aux();

	  // 省略部分代碼......
    };

deque的實現比vector和list要複雜的多,主要是因為其空間布局不太一樣。下面的代碼主要是對雙端隊列隊首與隊尾的操作(push_front、push_back、pop_front、pop_back)中涉及到空間變動的部分代碼實現:

// Called only if _M_impl._M_finish._M_cur == _M_impl._M_finish._M_last - 1.
template<typename _Tp, typename _Alloc>
void deque<_Tp, _Alloc>::_M_push_back_aux(const value_type& __t) {
	_M_reserve_map_at_back();
	*(this->_M_impl._M_finish._M_node + 1) = this->_M_allocate_node();      // map新指針指向新分配的連續空間
	__try {
	    this->_M_impl.construct(this->_M_impl._M_finish._M_cur, __t);
	    this->_M_impl._M_finish._M_set_node(this->_M_impl._M_finish._M_node + 1);
	    this->_M_impl._M_finish._M_cur = this->_M_impl._M_finish._M_first;
	} __catch(...) {
	    _M_deallocate_node(*(this->_M_impl._M_finish._M_node + 1));
	    __throw_exception_again;
	}
}

// Called only if _M_impl._M_start._M_cur == _M_impl._M_start._M_first.
template<typename _Tp, typename _Alloc>
void deque<_Tp, _Alloc>::_M_push_front_aux(const value_type& __t) {
	_M_reserve_map_at_front();
	*(this->_M_impl._M_start._M_node - 1) = this->_M_allocate_node();       // map指定位置指向新分配的連續空間
	__try {
	    this->_M_impl._M_start._M_set_node(this->_M_impl._M_start._M_node - 1);
	    this->_M_impl._M_start._M_cur = this->_M_impl._M_start._M_last - 1;
	    this->_M_impl.construct(this->_M_impl._M_start._M_cur, __t);
	} __catch(...) {
	    ++this->_M_impl._M_start;
	    _M_deallocate_node(*(this->_M_impl._M_start._M_node - 1));
	    __throw_exception_again;
	}
}

// Called only if _M_impl._M_finish._M_cur == _M_impl._M_finish._M_first.
template <typename _Tp, typename _Alloc>
void deque<_Tp, _Alloc>::_M_pop_back_aux() {
    _M_deallocate_node(this->_M_impl._M_finish._M_first);
    this->_M_impl._M_finish._M_set_node(this->_M_impl._M_finish._M_node - 1);
    this->_M_impl._M_finish._M_cur = this->_M_impl._M_finish._M_last - 1;
    _Alloc_traits::destroy(_M_get_Tp_allocator(), this->_M_impl._M_finish._M_cur);
}

  // Called only if _M_impl._M_start._M_cur == _M_impl._M_start._M_last - 1.
  // Note that if the deque has at least one element (a precondition for this
  // member function), and if
  //   _M_impl._M_start._M_cur == _M_impl._M_start._M_last,
  // then the deque must have at least two nodes.
  template <typename _Tp, typename _Alloc>
void deque<_Tp, _Alloc>::_M_pop_front_aux() {
    _Alloc_traits::destroy(_M_get_Tp_allocator(), this->_M_impl._M_start._M_cur);
    _M_deallocate_node(this->_M_impl._M_start._M_first);
    this->_M_impl._M_start._M_set_node(this->_M_impl._M_start._M_node + 1);
    this->_M_impl._M_start._M_cur = this->_M_impl._M_start._M_first;
}

下面的原代碼是調整map的,如果map沒有適當空間插入新的連續空間首地址,就重新分配map(這種情況比如,map的後面全部插滿了,但前面還大量空着,就需要將目前的map中的元素進行移動,使map的元素分佈在中間位置,首尾兩端是空閑的,以便於後面插入新元素; 如果是map的空間不足了,則需要新分配map空間,新空間大小要大於新指針元素數量+2)。

void _M_reserve_map_at_back(size_type __nodes_to_add = 1) {
    if (__nodes_to_add + 1 > this->_M_impl._M_map_size - (this->_M_impl._M_finish._M_node - this->_M_impl._M_map))
	    _M_reallocate_map(__nodes_to_add, false);
}

void _M_reserve_map_at_front(size_type __nodes_to_add = 1) {
    if (__nodes_to_add > size_type(this->_M_impl._M_start._M_node - this->_M_impl._M_map))
	    _M_reallocate_map(__nodes_to_add, true);
}

template <typename _Tp, typename _Alloc>
void deque<_Tp, _Alloc>::_M_reallocate_map(size_type __nodes_to_add, bool __add_at_front) {
    const size_type __old_num_nodes = this->_M_impl._M_finish._M_node - this->_M_impl._M_start._M_node + 1;
    const size_type __new_num_nodes = __old_num_nodes + __nodes_to_add;

    _Map_pointer __new_nstart;
    if (this->_M_impl._M_map_size > 2 * __new_num_nodes) {
	    __new_nstart = this->_M_impl._M_map + (this->_M_impl._M_map_size - __new_num_nodes) / 2 + (__add_at_front ? __nodes_to_add : 0);	// 這裏新map的開始往後移動了一段位置,是為了將來在前部插入的時候有剩餘空間,後部空餘一段位置也是。
	    if (__new_nstart < this->_M_impl._M_start._M_node)
	    	std::copy(this->_M_impl._M_start._M_node, this->_M_impl._M_finish._M_node + 1, __new_nstart);
	    else
	    	std::copy_backward(this->_M_impl._M_start._M_node, this->_M_impl._M_finish._M_node + 1, __new_nstart + __old_num_nodes);
	} else {
	    size_type __new_map_size = this->_M_impl._M_map_size + std::max(this->_M_impl._M_map_size, __nodes_to_add) + 2;		// 要至少空餘2個空閑位置
	    _Map_pointer __new_map = this->_M_allocate_map(__new_map_size);
	    __new_nstart = __new_map + (__new_map_size - __new_num_nodes) / 2 + (__add_at_front ? __nodes_to_add : 0);
	    std::copy(this->_M_impl._M_start._M_node, this->_M_impl._M_finish._M_node + 1, __new_nstart);
	    _M_deallocate_map(this->_M_impl._M_map, this->_M_impl._M_map_size);

	    this->_M_impl._M_map = __new_map;
	    this->_M_impl._M_map_size = __new_map_size;
	}

    this->_M_impl._M_start._M_set_node(__new_nstart);
    this->_M_impl._M_finish._M_set_node(__new_nstart + __old_num_nodes - 1);
}

更詳細的還是自己看STL的源碼吧,順便吐槽一下STL的源碼,代碼太臃腫了,看起來太累了,如果按照其實現原理,自己實現一個mini版STL,應該會簡潔許多許多。

到這裏,deque中比較核心的源碼已經基本分析完了,也基本展現了deque中幾個關鍵成員函數是如何實現的,其迭代器的實現,其map的實現與調整。

deque與vector、list的對比

vector能夠實現隨機訪問,動態擴展,但在頭部插入O(n),開銷較大,且動態擴展時需要複製所有的元素,同樣效率較低。list插入、刪除頭尾部元素效率很高O(n),但是不能隨機訪問,查找效率O(n),每個節點需要存儲前後節點指針,有較大的額外存儲開銷。而deque等於是在兩種容器的優缺點進行了一定的平衡,在收尾插入、刪除元素,效率很高O(1),在中間插入O(n)都差不多,但其能實現隨機訪問,且動態擴展時不需要複製全體元素,只需要新分配足夠的連續存儲空間,最多重新複製一下map到新map,而map是各個連續存儲空間首地址指針數組,容量相比全體元素小非常多,動態擴展時代價很小。所以,deque相比vector在更一般的情況下有更高的性能,相比list有更小的額外存儲空間(但deque擁有較大的最小內存開銷,原因是它需要map和一段連續存儲空間開銷,即元素數目非常小時開銷比list大,但當元素數目較多時,空間開銷比list少)。

stack

棧也是經常用的數據結構,其實現較為簡單,內部實現依賴deque,當然也可以用vector、list實現。

// Stack implementation -*- C++ -*-
template<typename _Tp, typename _Sequence = deque<_Tp> >
class stack {
// concept requirements
typedef typename _Sequence::value_type _Sequence_value_type;

public:
    typedef typename _Sequence::value_type		value_type;
    typedef typename _Sequence::reference		reference;
    typedef typename _Sequence::const_reference	const_reference;
    typedef typename _Sequence::size_type		size_type;
    typedef	       _Sequence			container_type;

protected:
    _Sequence c;

public:
	stack(): c() { }
	// 省略構造函數與析構函數......
      /**
       *  Returns true if the %stack is empty.
       */
    bool empty() const { return c.empty(); }

    /**  Returns the number of elements in the %stack.  */
    size_type size() const { return c.size(); }

      /**
       *  Returns a read/write reference to the data at the first
       *  element of the %stack.
       */
    reference top() {
		__glibcxx_requires_nonempty();
		return c.back();
    }
      /**
       *  @brief  Add data to the top of the %stack.
       *  @param  __x  Data to be added.
       *
       *  This is a typical %stack operation.  The function creates an
       *  element at the top of the %stack and assigns the given data
       *  to it.  The time complexity of the operation depends on the
       *  underlying sequence.
       */
    void push(const value_type& __x) { c.push_back(__x); }

      /**
       *  @brief  Removes first element.
       *
       *  This is a typical %stack operation.  It shrinks the %stack
       *  by one.  The time complexity of the operation depends on the
       *  underlying sequence.
       *
       *  Note that no data is returned, and if the first element's
       *  data is needed, it should be retrieved before pop() is
       *  called.
       */
    void pop() {
		__glibcxx_requires_nonempty();
		c.pop_back();
    }

	// 省略其他非關鍵代碼......
}; 

queue

隊列有普通的先進先出的隊列,還有優先隊列,優先級隊列不僅僅要按先後順序,更強調優先級高的先出隊列。

普通隊列的實現

普通隊列的實現與棧實現差不多,也是基於deque實現的。

template<typename _Tp, typename _Sequence = deque<_Tp> >
class queue {
// concept requirements
typedef typename _Sequence::value_type _Sequence_value_type;

public:
    typedef typename	_Sequence::value_type		value_type;
    typedef typename	_Sequence::reference		reference;
    typedef typename	_Sequence::const_reference	const_reference;
    typedef typename	_Sequence::size_type		size_type;
    typedef		_Sequence			container_type;

protected:
      /*  Maintainers wondering why this isn't uglified as per style
       *  guidelines should note that this name is specified in the standard,
       *  C++98 [23.2.3.1].
       *  (Why? Presumably for the same reason that it's protected instead
       *  of private: to allow derivation.  But none of the other
       *  containers allow for derivation.  Odd.)
       */
       ///  @c c is the underlying container.
    _Sequence c;

public:
	queue(): c() { }
	// 省略構造函數與析構函數......

    bool empty() const { return c.empty(); }
    size_type size() const { return c.size(); }

    reference front() {
		__glibcxx_requires_nonempty();
		return c.front();
    }

    reference back() {
		__glibcxx_requires_nonempty();
		return c.back();
    }

    // Add data to the end of the %queue.
    void push(const value_type& __x) { c.push_back(__x); }
      
    // Removes first element.
    void pop() {
		__glibcxx_requires_nonempty();
		c.pop_front();
    }
};

優先隊列priority_queue實現

優先隊列的實現原理是基於堆實現的,堆底層是數組,所以,這裏priority_queue底層的序列容器是vector,選則vector而不是其他容器,是因為優先隊列基於堆,而堆的各種操作中,插入、刪除、都是從尾部插入、刪除操作最後實際上物理刪除的是尾部元素,而且其擴容是2倍擴容,符合二叉樹下一層節點數目是上一次所有數目+1,二倍擴容恰好合適,當然也可以用其他容器(例如deque,但不是最優的)。至於堆實現優先隊列的原理,這裏不再敘述。源碼實現如下:

template<typename _Tp, typename _Sequence = vector<_Tp>, typename _Compare  = less<typename _Sequence::value_type> >
class priority_queue {
#ifdef _GLIBCXX_CONCEPT_CHECKS
// concept requirements
typedef typename _Sequence::value_type _Sequence_value_type;
# if __cplusplus < 201103L
    __glibcxx_class_requires(_Tp, _SGIAssignableConcept)
# endif
    __glibcxx_class_requires(_Sequence, _SequenceConcept)
    __glibcxx_class_requires(_Sequence, _RandomAccessContainerConcept)
    __glibcxx_class_requires2(_Tp, _Sequence_value_type, _SameTypeConcept)
    __glibcxx_class_requires4(_Compare, bool, _Tp, _Tp, _BinaryFunctionConcept)
#endif

#if __cplusplus >= 201103L
template<typename _Alloc>
using _Uses = typename
enable_if<uses_allocator<_Sequence, _Alloc>::value>::type;
#endif

public:
    typedef typename	_Sequence::value_type		value_type;
    typedef typename	_Sequence::reference		 reference;
    typedef typename	_Sequence::const_reference	   const_reference;
    typedef typename	_Sequence::size_type		 size_type;
    typedef		_Sequence			    container_type;
    typedef	       _Compare				    value_compare;

protected:
     _Sequence  c;
    _Compare   comp;	// 優先隊列基於堆,而堆經常需要比較操作

public:
    //    *  @brief  Default constructor creates no elements.
    explicit priority_queue(const _Compare& __x = _Compare(), const _Sequence& __s = _Sequence()): c(__s), comp(__x) { 		
        std::make_heap(c.begin(), c.end(), comp); 	// 構造堆
	}

	// 省略其他構造函數......

      /**
       *  Returns true if the %queue is empty.
       */
    bool empty() const { 
		return c.empty(); 
	}

      /**  Returns the number of elements in the %queue.  */
    size_type size() const { return c.size(); }

      /**
       *  Returns a read-only (constant) reference to the data at the first
       *  element of the %queue.
       */
    const_reference top() const {
		__glibcxx_requires_nonempty();
		return c.front();
    }

      /**
       *  @brief  Add data to the %queue.
       *  @param  __x  Data to be added.
       *
       *  This is a typical %queue operation.
       *  The time complexity of the operation depends on the underlying
       *  sequence.
       */
    void push(const value_type& __x) {		// 優先隊列中插入元素,先放到容器尾部,再進行“上移”操作使之滿足堆性質。
		c.push_back(__x);
		std::push_heap(c.begin(), c.end(), comp);
    }

      /**
       *  @brief  Removes first element.
       *
       *  This is a typical %queue operation.  It shrinks the %queue
       *  by one.  The time complexity of the operation depends on the
       *  underlying sequence.
       *
       *  Note that no data is returned, and if the first element's
       *  data is needed, it should be retrieved before pop() is
       *  called.
       */
    void pop() {		//從優先隊列中彈出首元素
		__glibcxx_requires_nonempty();
		std::pop_heap(c.begin(), c.end(), comp);
		c.pop_back();
    }
};

可以看到只要理解了堆的實現原理,優先隊列的實現原理就非常容易理解,堆的相關STL源碼分析不在這裏繼續分析。

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

【其他文章推薦】

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

※網頁設計最專業,超強功能平台可客製化

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

08_提升方法_AdaBoost算法

  今天是2020年2月24日星期一。一個又一個意外因素串連起2020這不平凡的一年,多麼希望時間能夠倒退。曾經覺得電視上科比的畫面多麼熟悉,現在全成了陌生和追憶。

GitHub:https://github.com/wangzycloud/statistical-learning-method

提升方法

引入

  提升方法是一種常用的統計學習方法,還是比較容易理解的。在分類問題中,通過改變訓練樣本的權重,學習多個分類器,並將這些分類器進行線性組合,從而提高分類的性能。其實說白了,就是一個人干不好的活,我讓兩個人干;兩個人干不好,那就三個人四個人都來干。但是人多了不能像三個和尚那樣沒水喝,都不幹活。包工頭要採取一些措施,措施一:一個人幹活的時候,哪裡乾的不好?就讓第二個人補充在這個地方;兩個人幹活的時候,哪裡乾的不好?就讓第三個人補充在這個地方。措施二:這三四個人在同一個地方幹活,怎樣確定這個活的結果乾的好不好呢?有的人幹活細緻認真,自然對結果增益大;幹活粗糙的,對結果增益不多,因此需要有一種組合策略進行判斷。通過幾個人共同努力,就解決了一個人干不好的事情。

  接下來的內容,將按照書中順序,先介紹提升方法的思路和代表性的提升算法AdaBoost;再從前向分步加法模型角度解釋AdaBoost;最後看一個具體實例—提升樹。

提升方法AdaBoost算法

  提升方法基於這樣一種思想:對於一個複雜的任務來說,將多個專家的判斷進行適當的綜合所得出的判斷,要比其中任何一個專家單獨的判斷好。實際上,就是“三個臭皮匠頂個諸葛亮“的道理。書中上來提到了兩個稍晦澀的概念“強可學習”、“弱可學習”,下文沒有繼續闡述這兩個概念,這裏簡單的複述一下。在概率近似正確學習的框架中(不懂),一個概念,如果存在一個多項式的學習算法能夠學習它,並且正確率很高,那麼就稱這個概念是強可學習的;一個概念,如果存在一個多項式的學習算法能夠學習它,學習的正確率僅比隨機猜測略好,那麼就稱這個概念是弱可學習的。這兩個概念之間的相互關係,被證明是等價的,也就是說在一定的學習框架下,一個概念是強可學習的充分必要條件是這個概念是弱可學習的。(模型效果好==》強可學習;模型效果差==》弱可學習;兩者有聯繫)

  這樣一來,在學習的過程中如果已經發現了“弱學習算法”,那麼能否將它提升為“強學習算法”。我們知道,發現弱學習算法通常要比發現強學習算法容易得多,如何具體實施提升,是我們開發提升方法時所要解決的問題。

  對於分類問題而言,給定一個訓練數據樣本集,求比較粗糙的分類規則(弱分類器)要比求精確的分類規則容易得多。提升方法就是從弱學習算法出發,反覆學習,得到一系列弱分類器(又稱為基本分類器),然後組合這些弱分類器,構成一個強分類器。並且大多數的提升方法都是改變訓練數據的概率分佈(訓練數據的權值分佈),針對不同的訓練數據分佈調用弱學習算法學習一系列弱分類器。可以理解為難分的數據加大權重,讓下一個弱分類器重點學習該數據。

  這樣,對於提升方法來說,有兩個問題需要解決:一是在每一輪如何改變訓練數據的權值或概率分佈;二是如何將弱分類器組合成一個強分類器。我們接下來要學習的AdaBoost算法,針對第一個問題,提高那些被前一輪分類器錯誤分類樣本的權值,而降低那些被正確分類樣本的權值。這樣一來,那些沒有得到正確分類的數據,由於其權重的加大而受到后一輪弱分類器的更多關注。於是,分類問題被一系列的弱分類器“分而治之”。針對第二個問題,AdaBoost採取加權多數表決的方法。具體的,加大分類誤差率小的弱分類器的權值,使其在表決中起較大的作用;減小分類誤差率大的弱分類器的權值,使其在表決中起到較小的作用。

AdaBoost算法

  這裏先簡單的羅列AdaBoost算法過程,先趕完整體進度,後期編寫代碼時再整理細節。

  假設給定一個二類分類的訓練數據集

AdaBoost算法各個步驟的說明:

  步驟(1)假設訓練數據集具有均勻的權值分佈,即每個訓練樣本在基本分類器的學習中作用相同,這一假設保證第一步能夠在原始數據上學習基本分類器G1(x)。

  步驟(2)AdaBoost反覆學習基本分類器,在每一輪m=1,2,…,M順次執行四步操作:

  第a步,使用當前分佈Dm加權的訓練數據集,學習基本分類器Gm(x);

  第b步,計算基本分類器Gm(x)在加權訓練數據集上的分類誤差率。實際上,第m個分類器的分類誤差率,就是被分類器Gm(x)誤分類樣本的權值之和,這表明權重大的樣本起的作用更大一些。如果將其誤分類,則誤差率大大增加,從而進一步影響Gm(x)的係數;

  第c步,從公式8.8中,可以看出em≤1/2時,αm≥0,並且αm隨着em的減小而增大,所以分類誤差率越小的基本分類器在最終分類器中的作用越大;

  第d步,更新訓練數據的權值分佈為下一輪做準備。可以看到,誤分類樣本在下一輪學習中起更大的作用,不改變所給的訓練數據,而不斷改變訓練數據權值的分佈,使得訓練數據在基本分類器的學習中起不同的作用。

  步驟(3)線性組合f(x)實現M個基本分類器的加權表決。係數αm表示了基本分類器Gm(x)的重要性,這裏所有αm之和並不為1。f(x)的符號決定實例x的類,f(x)的絕對值表示分類的確信度。

AdaBoost的例子

AdaBoost算法的訓練誤差分析

  該訓練誤差分析部分,用證明出來的定理形式,表明AdaBoost算法的最基本性質就是能在學習過程中不斷減少訓練誤差。也就是“該算法能夠降低在訓練數據集的分類誤差率”這件事用數學方式證明了。分別是AdaBoost訓練誤差上界定理8.1、二分類問題的AdaBoost訓練誤差上界定理8.2和推論8.1。

  該定理說明,可以在每一輪選取適當的Gm使得Zm最小,從而使訓練誤差下降的最快。對於二分類問題,有定理8.2。

  該推論表明,在此條件下,AdaBoost的訓練誤差是以指數速率下降的(看着像,不懂)。

AdaBoost算法的解釋

  本節內容從另外一個已經驗證的模型來分析AdaBoost算法,並得到該算法的另外解釋(我的理解是:AdaBoost算法是一般化的加法模型的特例,現在要從加法模型的角度分析)。可以認為AdaBoost算法是模型為加法模型、損失函數為指數函數、學習算法為前向分佈算法時的二類分類學習方法。接下來的內容,先對加法模型及前向分佈算法做簡單介紹,通過對損失函數的分析,得到與AdaBoost算法等價的參數表示。

  考慮加法模型,b(x)函數相當於基分類器:

  在給定訓練數據及損失函數L(y,f(x))的條件下,學習加法模型f(x)成為經驗風險極小化及損失函數極小化問題:

  一般來說,加法模型f(x)中的M次基函數相加模型是一個複雜的優化問題,利用前向分佈算法可以求解該優化問題。前向分佈算法的思想是:因為學習的是加法模型,如果能夠從前向後,每一步只學習一個基函數及其係數,逐步逼近優化目標8.14,那麼就可以簡化優化的複雜度。具體的,每一步只需優化以下損失函數(相當於每一步優化一個基函數,優化一個前進一個):

  那麼,前向分佈算法如下:

  本來公式8.14中的同時求解從m=1到M所有參數β、γ的優化問題,通過前向分佈算法簡化成了逐次求解各個β、γ的優化問題。

  那麼,前向分佈算法與AdaBoost算法有什麼聯繫呢?我們可以用定理的形式敘述之間的聯繫,也就是我們可以由前向分佈算法推導出AdaBoost。定理如下:

  以上內容直接截圖了。本是作為筆記進行整理,數學推導插不上解釋的嘴。要是以後文章被看到的多了,手推一下,再把內容整理上來。

提升樹

  我們知道,提升方法實際上是採用加法模型(即基函數的線性組合)與前向分步算法。本節提升樹是以決策樹作為基函數的提升方法,對分類問題決策樹是二叉分類樹,對回歸問題決策樹是二叉回歸樹。提升樹被認為是統計學習中性能最好的方法之一。

  在例8.1中看到的基本分類器x<v或x>v,可以看作是一個根節點直接連接兩個恭弘=叶 恭弘節點的簡單決策樹,即所謂的決策樹樁。提升樹模型可以表示為決策樹的加法模型:

  即使數據中的輸入與輸出之間的關係很複雜,樹的線性組合都可以很好地擬合訓練數據。分類問題與回歸問題的區別主要在於使用的損失函數不同,回歸問題中一般使用平方誤差損失函數,分類問題一般使用指數損失函數。對於二分類問題,提升樹算法只需要將AdaBoost算法8.1中的基本分類器限製為二類分類即可。下面看一下用於回歸問題的提升樹:

  R是當前模型擬合數據的殘差,所以對回歸問題的提升樹算法來說,只需要簡單的擬合當前模型的殘差即可。具體算法如下:

  具體例子8.2:

  算法8.1代碼已上傳至github,先理解決策樹章節的算法5.5,該提升樹會非常好理解~

代碼效果

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

【其他文章推薦】

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

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

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

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

※回頭車貨運收費標準

RocketMQ系列(二)環境搭建

RocketMQ的基本概念在上一篇中給大家介紹了,這一節將給大家介紹環境搭建。RocketMQ中最基礎的就是NameServer,我們先來看看它是怎麼搭建的。

NameServer

RocketMQ要求的環境是JDK8以上,我們先檢查一下環境,

[root@centOS-1 ~]# java -version
openjdk version "11.0.3" 2019-04-16 LTS
OpenJDK Runtime Environment 18.9 (build 11.0.3+7-LTS)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.3+7-LTS, mixed mode, sharing)

我的這個機器並沒有刻意的安裝JDK,而是系統自帶的OpenJDK 11,這應該也是沒有問題的。然後我們從RocketMQ官網下載最新的安裝包,並且上傳到/opt目錄下,

[root@centOS-1 opt]# ll
-rw-r--r--.  1 root  root 13838456 6月   3 08:49 rocketmq-all-4.7.0-bin-release.zip

然後我們解壓這個zip包,

[root@centOS-1 opt]# unzip rocketmq-all-4.7.0-bin-release.zip

這裏使用的是unzip命令,如果你的機器里沒有這個命令,可以使用yum install安裝一個。解壓以後,進入到RocketMQ的主目錄,並且啟動一下NameServer。

[root@centOS-1 opt]# cd rocketmq-all-4.7.0-bin-release
[root@centOS-1 rocketmq-all-4.7.0-bin-release]# ./bin/mqnamesrv
OpenJDK 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.
Unrecognized VM option 'UseCMSCompactAtFullCollection'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

這裏出了一個錯誤Error: Could not create the Java Virtual Machine,這是由於RocketMQ的啟動文件都是按照JDK8配置的,而我們這裏使用的是OpenJDK11,有很多命令參數不支持導致的,如果小夥伴們使用的是JDK8,正常啟動是沒有問題的。

在這裏我們改一下RocketMQ的啟動文件,

[root@centOS-1 rocketmq-all-4.7.0-bin-release]# vim bin/runserver.sh 
export JAVA_HOME
export JAVA="$JAVA_HOME/bin/java"
export BASE_DIR=$(dirname $0)/..
#在CLASSPATH中添加RocketMQ的lib目錄
#export CLASSPATH=.:${BASE_DIR}/conf:${CLASSPATH}
export CLASSPATH=.:${BASE_DIR}/lib/*:${BASE_DIR}/conf:${CLASSPATH}

修改的地方我們增加了註釋,在ClassPath里添加了lib目錄,然後在這個文件的末尾,註釋掉升級JDK后不支持的幾個參數,

JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
#JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8  -XX:-UseParNewGC"
JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails"
#JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages"
#JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib"
#JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n"
JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"
JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"

好了,修改完以後,我們保存退出,再次啟動,這次我們在後台啟動NameServer,

[root@centOS-1 rocketmq-all-4.7.0-bin-release]# nohup ./bin/mqnamesrv &

[root@centOS-1 rocketmq-all-4.7.0-bin-release]# tail -500f ~/logs/rocketmqlogs/namesrv.log 

然後查看一下日誌,在日誌中看到main - The Name Server boot success. serializeType=JSON,說明NameServer啟動成功了。

單點的NameServer肯定是不能滿足我們的要求的,怎麼也要做個集群吧。NameServer是一個無狀態的服務,節點之間沒有任何數據往來,所以NameServer的集群搭建不需要任何的配置,只需要啟動多個NameServer服務就可以了,它不像Zookeeper集群搭建那樣,需要配置各個節點。在這裏我們就啟動3個NameServer節點吧,對應我們的3台機器,192.168.73.130,192.168.73.131,192.168.73.132。

Broker

NameServer集群搭建完成,下面就搭建Broker了,Broker呢,我們要搭建一個兩主兩從結構的,主從之間異步備份,保存磁盤也是使用異步的方式。如果你對主從同步和保存磁盤的方式還不了解,看看上一節的內容吧。異步兩主兩從這種結構的配置,在RocketMQ中已經有例子了,我們先一下配置文件。

[root@centOS-1 rocketmq-all-4.7.0-bin-release]# vim conf/2m-2s-async/broker-a.properties 

這個配置文件是broker-a“主”的配置文件,

brokerClusterName=RocketMQ-Cluster
brokerName=broker-a
brokerId=0
deleteWhen=04
fileReservedTime=48
brokerRole=ASYNC_MASTER
flushDiskType=ASYNC_FLUSH

其中,

  • brokerClusterName是MQ集群的名稱,我們改為RocketMQ-Cluster。
  • brokerName是隊列的名字,配置為broker-a。
  • brokerId是隊列的id,0代表是“主”,其他正整數代表着“從”。
  • deleteWhen=04 代表着commitLog過期了,就會被刪除。
  • fileReservedTime是commitLog的過期時間,單位是小時,這裏配置的是48小時。
  • brokerRole,隊列的角色,ASYNC_MASTER是異步主。
  • flushDiskType,保存磁盤的方式,異步保存。

再看看broker-a的從配置,

brokerClusterName=RocketMQ-Cluster
brokerName=broker-a
brokerId=1
deleteWhen=04
fileReservedTime=48
brokerRole=SLAVE
flushDiskType=ASYNC_FLUSH

其中,集群的名字一樣,隊列的名字一樣,只是brokerId和brokerRole不一樣,這裏的配置代表着它是隊列broker-a的“從”。broker-b的配置和broker-a是一樣的,只是brokerName不一樣而已,在這裏就不貼出來了。

兩主兩從的配置文件都已經配置好了,我們來規劃一下,我們的NameServer是3台192.168.73.130,192.168.73.131,192.168.73.132,broker按照如下部署:

  • broker-a(主):192.168.73.130
  • broker-a(從):192.168.73.131
  • broker-b(主):192.168.73.131
  • broker-b(從):192.168.73.130

接下來,我們啟動broker,在192.168.73.130上啟動 broker-a(主)和broker-b(從)。和NameServer一樣,我們需要修改一下啟動的腳本,否則也會報錯誤。我們修改的是runbroker.sh這個文件,修改的內容和前面是一樣的,這裏就不贅述了。在啟動文件中,內存大小配置的是8g,如果機器的內存不夠,可以適當減少一下內存。

這裏還要做個說明,由於我們在一台機器上啟動了兩個broker實例,監聽端口和日誌存儲的路徑都會有衝突。那麼我們在192.168.73.130的broker-b(從)的配置文件中,增加配置,如下:

brokerClusterName=RocketMQ-Cluster
brokerName=broker-b
brokerId=1
deleteWhen=04
fileReservedTime=48
brokerRole=SLAVE
flushDiskType=ASYNC_FLUSH

listenPort=11911
storePathRootDir=~/store-b                       

broker-b(從)的端口改為11911,區別默認的10911;storePathRootDir改為~/store-b,區分默認的~/store。

同樣在192.168.73.131的broker-a(從)也要做修改,如下:

brokerClusterName=RocketMQ-Cluster
brokerName=broker-a
brokerId=1
deleteWhen=04
fileReservedTime=48
brokerRole=SLAVE
flushDiskType=ASYNC_FLUSH

listenPort=11911
storePathRootDir=~/store-a

然後,我們在192.168.73.130上啟動,如下,

nohup ./bin/mqbroker -c conf/2m-2s-async/broker-a.properties -n '192.168.73.130:9876;192.168.73.131:9876;192.168.73.132:9876' &

nohup ./bin/mqbroker -c conf/2m-2s-async/broker-b-s.properties -n '192.168.73.130:9876;192.168.73.131:9876;192.168.73.132:9876' &

  • -c 指定的是配置文件,分別指定的是broker-a(主)和broker-b(從)。
  • -n 指定的是NameServer的地址,指定了3個,用,隔開。

再在192.168.73.131上啟動,如下,

nohup ./bin/mqbroker -c conf/2m-2s-async/broker-b.properties -n '192.168.73.130:9876;192.168.73.131:9876;192.168.73.132:9876' &

nohup ./bin/mqbroker -c conf/2m-2s-async/broker-a-s.properties -n '192.168.73.130:9876;192.168.73.131:9876;192.168.73.132:9876' &

好,如果沒有出現錯誤,到這裏,集群就搭建成功了。這裏邊有個小坑,大家一定要注意,就是-n後面的地址一定要用”括起來,並且地址之間要用;,否則,我們在查看集群列表時,是看不到的。

mqadmin

集群已經搭建好了,我們可以查看一下集群的狀態,查看集群的狀態,我們可以使用mqadmin,命令如下:

./bin/mqadmin clusterlist -n '192.168.73.130:9876;192.168.73.131:9876;192.168.73.132:9876'
  • clusterlist 是查看集群的命令
  • -n 後面是NameServer的地址,注意這裏也要用”括起來,並且地址之間要用;隔開

執行結果如下:

#Cluster Name     #Broker Name            #BID  #Addr                  #Version                #InTPS(LOAD)       #OutTPS(LOAD) #PCWait(ms) #Hour #SPACE
RocketMQ-Cluster  broker-a                0     192.168.73.130:10911   V4_7_0                   0.00(0,0ms)         0.00(0,0ms)          0 442039.47 -1.0000
RocketMQ-Cluster  broker-a                1     192.168.73.131:11911   V4_7_0                   0.00(0,0ms)         0.00(0,0ms)          0 442039.47 0.2956
RocketMQ-Cluster  broker-b                0     192.168.73.131:10911   V4_7_0                   0.00(0,0ms)         0.00(0,0ms)          0 442039.47 0.2956
RocketMQ-Cluster  broker-b                1     192.168.73.130:11911   V4_7_0                   0.00(0,0ms)         0.00(0,0ms)          0 442039.47 -1.0000

我們可以看到在這個NameServer中心中,只有一個broker集群RocketMQ-Cluster,有兩個broker,broker-a和broker-b,而且每一個broker都有主從,broker的ip我們也可以看到。

好了~ 到這裏RocketMQ的集群就搭建好了,有問題評論區留言哦~~

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

【其他文章推薦】

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

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

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

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

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

※網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

不知道怎麼提高代碼質量?來看看這幾種設計模式吧!

提高代碼質量的目的

程序猿的本職工作就是寫代碼,寫出高質量的代碼應該是我們的追求和對自己的要求,因為:

  1. 高質量的代碼往往意味着更少的BUG,更好的模塊化,是我們擴展性,復用性的基礎
  2. 高質量的代碼也意味着更好的書寫,更好的命名,有利於我們的維護

什麼代碼算好的質量

怎樣來定義代碼質量的”好”,業界有很多標準,本文認為好的代碼應該有以下特點:

  1. 代碼整潔,比如縮進之類的,現在有很多工具可以自動解決這個問題,比如eslint。
  2. 結構規整,沒有漫長的結構,函數拆分合理,不會來一個幾千行的函數,也不會有幾十個if...else。這要求寫代碼的人有一些優化的經驗,本文會介紹幾種模式來優化這些情況。
  3. 閱讀起來好理解,不會出現一堆a,b,c這種命名,而是應該盡量語義化,變量名和函數名都盡量有意義,最好是代碼即註釋,讓別人看你的代碼就知道你在幹嘛。

本文介紹的設計模式主要有策略/狀態模式,外觀模式,迭代器模式,備忘錄模式。

策略/狀態模式

策略模式基本結構

假如我們需要做一個計算器,需要支持加減乘除,為了判斷用戶具體需要進行哪個操作,我們需要4個if...else來進行判斷,如果支持更多操作,那if...else會更長,不利於閱讀,看着也不優雅。所以我們可以用策略模式優化如下:

function calculator(type, a, b) {
  const strategy = {
    add: function(a, b) {
      return a + b;
    },
    minus: function(a, b) {
      return a - b;
    },
    division: function(a, b) {
      return a / b;
    },
    times: function(a, b) {
      return a * b;
    }
  }
  
  return strategy[type](a, b);
}

// 使用時
calculator('add', 1, 1);

上述代碼我們用一個對象取代了多個if...else,我們需要的操作都對應這個對象裏面的一個屬性,這個屬性名字對應我們傳入的type,我們直接用這個屬性名字就可以獲取對應的操作。

狀態模式基本結構

狀態模式和策略模式很像,也是有一個對象存儲一些策略,但是還有一個變量來存儲當前的狀態,我們根據當前狀態來獲取具體的操作:

function stateFactor(state) {
  const stateObj = {
    status: '',
    state: {
      state1: function(){},
      state2: function(){},
    },
    run: function() {
      return this.state[this.status];
    }
  }
  
  stateObj.status = state;
  return stateObj;
}

// 使用時
stateFactor('state1').run();

if...else其實是根據不同的條件來改變代碼的行為,而策略模式和狀態模式都可以根據傳入的策略或者狀態的不同來改變行為,所有我們可以用這兩種模式來替代if...else。

實例:訪問權限

這個例子的需求是我們的頁面需要根據不同的角色來渲染不同的內容,如果我們用if...else寫就是這樣:

// 有三個模塊需要显示,不同角色看到的模塊應該不同
function showPart1() {}
function showPart2() {}
function showPart3() {}

// 獲取當前用戶的角色,然後決定显示哪些部分
axios.get('xxx').then((role) => {
  if(role === 'boss'){
    showPart1();
    showPart2();
    showPart3();
  } else if(role === 'manager') {
    showPart1();
    showPart2();
  } else if(role === 'staff') {
    showPart3();
  }
});

上述代碼中我們通過API請求獲得了當前用戶的角色,然後一堆if...else去判斷應該显示哪些模塊,如果角色很多,這裏的if...else就可能很長,我們可以嘗試用狀態模式優化下:

// 先把各種角色都包裝到一個ShowController類裏面
function ShowController() {
  this.role = '';
  this.roleMap = {
    boss: function() {
      showPart1();
      showPart2();
      showPart3();
    },
    manager: function() {
      showPart1();
    	showPart2();
    },
    staff: function() {
      showPart3();
    }
  }
}

// ShowController上添加一個實例方法show,用來根據角色展示不同的內容
ShowController.prototype.show = function() {
  axios.get('xxx').then((role) => {
    this.role = role;
    this.roleMap[this.role]();
  });
}

// 使用時
new ShowController().show();

上述代碼我們通過一個狀態模式改寫了訪問權限模塊,去掉了if...else,而且不同角色的展示都封裝到了roleMap裏面,後面要增加或者減少都會方便很多。

實例:複合運動

這個例子的需求是我們現在有一個小球,我們需要控制他移動,他移動的方向可以是上下左右,還可以是左上,右下之類的複合運動。如果我們也用if...else來寫,這頭都會寫大:

// 先來四個方向的基本運動
function moveUp() {}
function moveDown() {}
function moveLeft() {}
function moveRight() {}

// 具體移動的方法,可以接收一個或兩個參數,一個就是基本操作,兩個參數就是左上,右下這類操作
function move(...args) {
  if(args.length === 1) {
    if(args[0] === 'up') {
      moveUp();
    } else if(args[0] === 'down') {
      moveDown();        
    } else if(args[0] === 'left') {
      moveLeft();        
    } else if(args[0] === 'right') {
      moveRight();        
    }
  } else {
    if(args[0] === 'left' && args[1] === 'up') {
      moveLeft();
      moveUp();
    } else if(args[0] === 'right' && args[1] === 'down') {
      moveRight();
      moveDown();
    }
    // 後面還有很多if...
  }
}

可以看到這裏if...else看得我們頭都大了,還是用策略模式來優化下吧:

// 建一個移動控制類
function MoveController() {
  this.status = [];
  this.moveHanders = {
    // 寫上每個指令對應的方法
    up: moveUp,
    dowm: moveDown,
    left: moveLeft,
    right: moveRight
  }
}

// MoveController添加一個實例方法來觸發運動
MoveController.prototype.run = function(...args) {
  this.status = args;
  this.status.forEach((move) => {
    this.moveHanders[move]();
  });
}

// 使用時
new MoveController().run('left', 'up')

上述代碼我們也是將所有的策略都封裝到了moveHanders裏面,然後通過實例方法run傳入的方法來執行具體的策略。

外觀模式

基本結構

當我們設計一個模塊時,裏面的方法可以會設計得比較細,但是暴露給外面使用的時候,不一定非得直接暴露這些細小的接口,外部使用者需要的可能是組合部分接口來實現某個功能,我們暴露的時候其實就可以將這個組織好。這就像餐廳裏面的菜單,有很多菜,用戶可以一個一個菜去點,也可以直接點一個套餐,外觀模式提供的就類似於這樣一個組織好的套餐:

function model1() {}

function model2() {}

// 可以提供一個更高階的接口,組合好了model1和model2給外部使用
function use() {
  model2(model1());
}

實例:常見的接口封裝

外觀模式說起來其實非常常見,很多模塊內部都很複雜,但是對外的接口可能都是一兩個,我們無需知道複雜的內部細節,只需要調用統一的高級接口就行,比如下面的選項卡模塊:

// 一個選項卡類,他內部可能有多個子模塊
function Tab() {}

Tab.prototype.renderHTML = function() {}    // 渲染頁面的子模塊
Tab.prototype.bindEvent = function() {}    // 綁定事件的子模塊
Tab.prototype.loadCss = function() {}    // 加載樣式的子模塊

// 對外不需要暴露上面那些具體的子模塊,只需要一個高級接口就行
Tab.prototype.init = function(config) {
  this.loadCss();
  this.renderHTML();
  this.bindEvent();
}

上述代碼這種封裝模式非常常見,其實也是用到了外觀模式,他當然也可以暴露具體的renderHTML,bindEvent,loadCss這些子模塊,但是外部使用者可能並不關心這些細節,只需要給一個統一的高級接口就行,就相當於改變了外觀暴露出來,所以叫外觀模式。

實例:方法的封裝

這個例子也很常見,就是把一些類似的功能封裝成一個方法,而不是每個地方去寫一遍。在以前還是IE主導天下的時候,我們需要做很多兼容的工作,僅僅是一個綁定事件就有addEventListener,attachEvent,onclick等,為了避免每次都進行這些檢測,我們可以將他們封裝成一個方法:

function addEvent(dom, type, fn) {
  if(dom.addEventListener) {
    return dom.addEventListener(type, fn, false);
  } else if(dom.attachEvent) {
    return dom.attachEvent("on" + type, fn);
  } else {
    dom["on" + type] = fn;
  }
}

然後將addEvent暴露出去給外面使用,其實我們在實際編碼時經常這樣封裝方法,只是我們自己可能沒意識到這個是外觀模式。

迭代器模式

基本結構

迭代器模式模式在JS裏面很常見了,數組自帶的forEach就是迭代器模式的一個應用,我們也可以實現一個類似的功能:

function Iterator(items) {
  this.items = items;
}

Iterator.prototype.dealEach = function(fn) {
  for(let i = 0; i < this.items.length; i++) {
    fn(this.items[i], i);
  }
}

上述代碼我們新建了一個迭代器類,構造函數接收一個數組,實例方法dealEach可以接收一個回調,對實例上的items每一項都執行這個回調。

實例:數據迭代器

其實JS數組很多原生方法都用了迭代器模式,比如find,find接收一個測試函數,返回符合這個測試函數的第一個數據。這個例子要做的是擴展這個功能,返回所有符合這個測試函數的數據項,而且也可以接收兩個參數,第一個參數是屬性名,第二個參數是值,同樣返回所有該屬性與值匹配的項:

// 外層用一個工廠模式封裝下,調用時不用寫new
function iteratorFactory(data) {
  function Iterator(data) {
    this.data = data;
  }
  
  Iterator.prototype.findAll = function(handler, value) {
    const result = [];
    let handlerFn;
    // 處理參數,如果第一個參數是函數,直接拿來用
    // 如果不是函數,就是屬性名,給一個對比的默認函數
    if(typeof handler === 'function') {
      handlerFn = handler;
    } else {
      handlerFn = function(item) {
        if(item[handler] === value) {
          return true;
        }
        
        return false;
      }
    }
    
    // 循環數據裏面的每一項,將符合結果的塞入結果數組
    for(let i = 0; i < this.data.length; i++) {
      const item = this.data[i];
      const res = handlerFn(item);
      if(res) {
        result.push(item)
      }
    }
    
    return result;
  }
  
  return new Iterator(data);
}

// 寫個數據測試下
const data = [{num: 1}, {num: 2}, {num: 3}];
iteratorFactory(data).findAll('num', 2);    // [{num: 2}]
iteratorFactory(data).findAll(item => item.num >= 2); // [{num: 2}, {num: 3}]

上述代碼封裝了一個類似數組find的迭代器,擴展了他的功能,這種迭代器非常適合用來處理API返回的大量結構相似的數據。

備忘錄模式

基本結構

備忘錄模式類似於JS經常使用的緩存函數,內部記錄一個狀態,也就是緩存,當我們再次訪問的時候可以直接拿緩存數據:

function memo() {
  const cache = {};
  
  return function(arg) {
    if(cache[arg]) {
      return cache[arg];
    } else {
      // 沒緩存的時候先執行方法,得到結果res
      // 然後將res寫入緩存
      cache[arg] = res;
      return res;
    }
}

實例:文章緩存

這個例子在實際項目中也比較常見,用戶每次點進一個新文章都需要從API請求數據,如果他下次再點進同一篇文章,我們可能希望直接用上次請求的數據,而不再次請求,這時候就可以用到我們的備忘錄模式了,直接拿上面的結構來用就行了:

function pageCache(pageId) {
  const cache = {};
  
  return function(pageId) {
    // 為了保持返回類型一致,我們都返回一個Promise
    if(cache[pageId]) {
      return Promise.resolve(cache[pageId]);
    } else {
      return axios.get(pageId).then((data) => {
        cache[pageId] = data;
        return data;
      })
    }
  }
}

上述代碼用了備忘錄模式來解決這個問題,但是代碼比較簡單,實際項目中可能需求會更加複雜一些,但是這個思路還是可以參考的。

實例:前進後退功能

這個例子的需求是,我們需要做一個可以移動的DIV,用戶把這個DIV隨意移動,但是他有時候可能誤操作或者反悔了,想把這個DIV移動回去,也就是將狀態回退到上一次,有了回退狀態的需求,當然還有配對的前進狀態的需求。這種類似的需求我們就可以用備忘錄模式實現:

function moveDiv() {
  this.states = [];       // 一個數組記錄所有狀態
  this.currentState = 0;  // 一個變量記錄當前狀態位置
}

// 移動方法,每次移動記錄狀態
moveDiv.prototype.move = function(type, num) {
  changeDiv(type, num);       // 偽代碼,移動DIV的具體操作,這裏並未實現
  
  // 記錄本次操作到states裏面去
  this.states.push({type,num});
  this.currentState = this.states.length - 1;   // 改變當前狀態指針
}

// 前進方法,取出狀態執行
moveDiv.prototype.forward = function() {
  // 如果當前不是最後一個狀態
  if(this.currentState < this.states.length - 1) {
    // 取出前進的狀態
    this.currentState++;
    const state = this.states[this.currentState];
    
    // 執行該狀態位置
    changeDiv(state.type, state.num);
  }
}

// 後退方法是類似的
moveDiv.prototype.back = function() {
  // 如果當前不是第一個狀態
  if(this.currentState > 0) {
    // 取出後退的狀態
    this.currentState--;
    const state = this.states[this.currentState];
    
    // 執行該狀態位置
    changeDiv(state.type, state.num);
  }
}

上述代碼通過一個數組將用戶所有操作過的狀態都記錄下來了,用戶可以隨時在狀態間進行前進和後退。

總結

本文講的這幾種設計模式策略/狀態模式,外觀模式,迭代器模式,備忘錄模式都很好理解,而且在實際工作中也非常常見,熟練使用他們可以有效減少冗餘代碼,提高我們的代碼質量。

  1. 策略模式通過將我們的if條件改寫為一條條的策略減少了if...else的數量,看起來更清爽,擴展起來也更方便。狀態模式跟策略模式很像,只是還多了一個狀態,可以根據這個狀態來選取具體的策略。
  2. 外觀模式可能我們已經在無意間使用了,就是將模塊一些內部邏輯封裝在一個更高級的接口內部,或者將一些類似操作封裝在一個方法內部,從而讓外部調用更加方便。
  3. 迭代器模式在JS數組上有很多實現,我們也可以模仿他們做一下數據處理的工作,特別適合處理從API拿來的大量結構相似的數據。
  4. 備忘錄模式就是加一個緩存對象,用來記錄之前獲取過的數據或者操作的狀態,後面可以用來加快訪問速度或者進行狀態回滾。
  5. 還是那句話,設計模式的重點在於理解思想,實現方式可以多種多樣。

本文是講設計模式的最後一篇文章,前面三篇是:

(500+贊!)不知道怎麼封裝代碼?看看這幾種設計模式吧!

(100+贊!)框架源碼中用來提高擴展性的設計模式

不知道怎麼提高代碼復用性?看看這幾種設計模式吧

文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支持是作者持續創作的動力。

本文素材來自於網易高級前端開發工程師微專業唐磊老師的設計模式課程。

作者博文GitHub項目地址: https://github.com/dennis-jiang/Front-End-Knowledges

作者掘金文章匯總:https://juejin.im/post/5e3ffc85518825494e2772fd

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

《痞子衡嵌入式半月刊》 第 9 期

痞子衡嵌入式半月刊: 第 9 期

這裏分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農曆年分二十四節氣,希望在每個交節之日準時發布一期。

本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。

上期回顧 :《痞子衡嵌入式半月刊: 第 8 期》

嘮兩句

今天是芒種,芒種節氣在農耕上有着相當重要的意義,它指導着農事耕種。民諺有“芒種不種,再種無用”一說。

近期總理提出的地攤經濟正席捲全國,這是特殊時期的一種特殊經濟刺激手段。地攤經濟,是最傳統的經濟模式,這意味着人人可以無門檻做生意。還等什麼,痞子衡打算淘寶進一批7天無理由退貨的小玩意去步行街擺攤了。

本期共收錄 3條資訊、3個項目、1個工具、1個RT產品,希望對你有幫助!

資訊類

1、瑞芯微AI芯片加持百度飛槳,攜手加速AI應用落地

瑞芯微Rockchip近日宣布,旗下AI芯片RK1808、RK1806適配百度飛槳(PaddlePaddle)開源深度學習平台,充分兼容飛槳輕量化推理引擎Paddle Lite。此次瑞芯微與百度合作,旨在為AI行業賦能更多應用場景,加速AI產品落地進程。

資訊主頁: https://www.rock-chips.com/a/cn/news/rockchip/2020/0513/1084.html

瑞芯微AI芯片RK1808及RK1806,內置獨立NPU神經計算單元,INT8 算力高達3.0TOPs;採用22nm FD-SOI工藝,相同性能下的功耗相比主流28nm工藝產品降低約30%,在算力、性能、功耗等指標上均有優異的表現。經實測,瑞芯微AI芯片在Paddle Lite中運行MobileNet V1耗時僅為6.5 ms,幀率高達153.8 FPS,二者充分兼容並高效穩定運行。

如上圖所示的實測結果可以看出,與手機等移動端常用的國內外主流CPU相比,RK18系列NPU在MobileNET_v1的耗時更少,表現出色,由此證明在AI相關領域,如圖像分類、目標檢測、語音交互上,專用的AI芯片將帶來更出色的效果。

2、ZLG正式發布AWTK v1.4

近日,ZLG開源GUI引擎AWTK v1.4正式發布。相對於v1.3,新版本中完善了許多細節,增加了部分特性、控件以及API等,同時新增對iOS平台,以及Python、Java、C++等語言的支持。

資訊主頁: https://www.zlg.cn/index/pub/awtk.html

AWTK v1.4新增特性:

- 無文件系統時支持多主題
- OpenGL ES支持snapshot
- edit和mledit支持自己指定的軟鍵盤名稱
- 點擊鼠標右鍵觸發EVT_CONTEXT_MENU事件
- 增加awtk_main.inc,用於標準程序的主函數
- 用SDL重新實現PC版本的線程和同步相關函數 
- edit增加input_type為"custom_password"的類型

AWTK全稱為Toolkit AnyWhere,是ZLG傾心打造的一套基於C語言開發的GUI框架。旨在為用戶提供一個功能強大、高效可靠、簡單易用、可輕鬆做出炫酷效果的GUI引擎,支持跨平台同步開發,一次編程,到處編譯,跨平台使用。 AWTK還配套了所見即所得的AWTK Designer界面設計工具、經典示例以及入門指南文檔等 。

3、Microchip推出軟件開發工具包和神經網絡IP,助力低功耗FPGA智能嵌入式視覺解決方案

隨着人工智能、機器學習和物聯網的興起,對邊緣應用的解決方案提出了更高的需求,例如縮小體積、減少產熱、提高計算性能等。近日,Microchip Technology Inc.(美國微芯科技公司)發布的智能嵌入式視覺解決方案,致力於讓軟件開發人員能夠更方便地在PolarFire®現場可編程門陣列(FPGA)內執行算法,進而滿足邊緣應用對節能型推理功能日益增長的需求。

資訊主頁: http://www.microchip.com.cn/newcommunity/index.php?m=Article&a=show&id=594

VectorBlox加速器軟件開發工具包(SDK)是Microchip嵌入式解決方案組合的重要新成員。受益於FPGA的高運算能力,優秀的能耗比,FPGA成為了邊緣人工智能應用的理想選擇。Microchip的VectorBlox加速器SDK可以幫助開發人員在不學習FPGA工具流或者不具備設計經驗的前提下,利用Microchip PolarFire FPGA創建靈活的低功耗覆蓋神經網絡應用。

項目類

1、EmbedXrpc – 面向單片機的嵌入式小型RPC

EmbedXrpc類似於Google的gRPC,但是應用場景是單片機。RPC遠程調用極大的方便了開發,使得不必關注於協議解析,數據的序列化和反序列化等繁瑣的工作。

項目主頁: https://gitee.com/snikeguo/EmbedXrpc

EmbedXrpc應用場景:單片機近距離Client/Server交互場景(USB、串口、CAN(如J1939 、ISO15765協議等),)只要是流協議都支持。

項目提供了一個Sample1工程,這是最簡單的例子,除了main.cpp的代碼是手工寫的之外,其他的代碼都是工具生成的!此Sample1工程演示了:

1.客戶端每一秒向服務端發送1、2 服務端計算出來3后,把3發送給客戶端
2.服務端每1秒廣播當前的時間,客戶端打印到控制台上

2、m4vgalib – 基於單片機的VGA格式視頻生成庫

m4vgalib庫能使得微控制器(比如STM32F40x/1x)輸出高質量、高分辨率彩色圖形,並且這個庫使用很少的外部組件。

項目主頁: https://github.com/cbiffle/m4vgalib

該庫示例單片機STM32F407是一個Cortex-M4微控制器,它既沒有視頻控制器,也沒有足夠的RAM用於任何合理分辨率的幀緩衝區。m4vgalib圍繞這一點工作,生成穩定的800×600(或640×480)256色視頻。m4vgalib不使用視頻控制器,而是使用兩個定時器、一個DMA控制器和一個GPIO端口。

儘管m4vgalib在一個不是為任何類型設計的處理器上維護320Mb/s的數據流,但是大多數CPU和硬件資源都留給應用程序使用。為了避免引入抖動,應用程序必須同意在執行的某些階段避開AHB1。(比如可以使用中斷來通知應用程序。)

3、cmd-parser – 一個非常簡單好用的命令解析器

cmd-parser是一個非常簡單好用的命令解析器,佔用資源極少極少,採用哈希算法超快匹配命令。

項目主頁: https://github.com/jiejieTop/cmd-parser

簡單來說,如果你希望你的開發板,可以通過命令執行一些處理,比如說用串口發一個命令A,開發板就執行A的一些處理,或者,在調試某些AT模組的時候,當收到模組返回的一些指令后,自動執行一些處理。當然,還有其他的地方可以用得上的,大家可以自行挖掘!

cmd-parser特點如下:

1. 用戶無需關心命令的存儲區域與大小,由編譯器靜態分配。
2. 加入哈希算法超快速匹配命令,時間複雜度從O(n*m)變為O(n)。
3. 命令支持忽略大小寫。
4. 非常易用與非常簡潔的代碼(不足150行)。

工具類

1、SpeedCrunch – 高精度科學計算器

SpeedCrunch是一款開源的高精度科學計算器,具有快速,鍵盤驅動的用戶界面。

軟件主頁: https://github.com/speedcrunch/SpeedCrunch

SpeedCrunch內置80多個數學函數,允許用戶使用複數,数字基數,單位轉換等執行最高50位精度的計算,其自動完成功能可加快工作速度,提升效率。SpeedCrunch還內置公式簿,可方便用戶查看和插入常用的公式,例如圓錐體的體積計算公式等。

i.MXRT出品

1、魚躍醫療 – 高流量呼吸濕化治療儀

這款高流量呼吸濕化治療儀所支持的經鼻高流量氧療(HFNC)是一項新型的氧療方式,已被國內外大量臨床研究證實在缺氧改善治療中有很高的應用價值。HFNC通過柔軟的鼻塞導管將最高達75L/min流量的空氧混合氣體,經由加溫加濕后輸送給患者。

RT芯片:i.MXRT1052
產品主頁: https://www.yuyue.com.cn/index.php/news/info/795.html
參考售價: 未知

歡迎訂閱

文章會同時發布到我的 博客園主頁、CSDN主頁、微信公眾號 平台上。

微信搜索”痞子衡嵌入式“或者掃描下面二維碼,就可以在手機上第一時間看了哦。

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

【其他文章推薦】

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

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

※想知道最厲害的網頁設計公司"嚨底家"!

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※回頭車貨運收費標準

※台中搬家公司費用怎麼算?