沒天理!回頭率爆表的總統級座駕 竟然比寶馬5系還便宜?

0T兩款發動機,匹配6AT變速箱,高配車型有四驅系統搭配,整體動力輸出數值十分高,尤其是3。0T車型的參數十分恐怖。就目前的信息來看,預售的三款車型配置都十分高,可變懸挂/胎壓監測/一鍵啟動/電動後備廂/前後雷達/真皮座椅/主動剎車系統都是標配。

外觀

霸氣敦實

外觀上林肯新大陸的設計和上次咱們試駕過的林肯MKZ有很大的相似度,除了車尾之外,其它地方設計基本相同,不過在車門把手上大陸還是十分有標誌性,由於車身尺寸碩大,加上碩大的中網和犀利的大燈,林肯大陸的氣場十分強大,看上去有幾分大型豪華轎車的感覺。

內飾

用料奢華

內飾上林肯大陸的用料十分值得讚歎,雖然在中控台的造型設計上不算多麼出彩,和德系車的車廂相比設計感欠缺,但是細膩的皮革到處都是,配置也十分豐富,大陸的內飾也值得讚歎,在MKZ上那個十分有福特味道的儀錶盤並沒有出現在大陸上,大陸使用一塊液晶儀錶盤,显示信息更加豐富,效果也更加棒。

空間

足夠寬敞

由於大陸的尺寸十分龐大,軸距也接近3米,所以大陸的內部空間還是十分有保障的,和同價位的加長對手相比也絲毫不遜色,後備箱空間十分規整但是寬度比較窄,實用性受到一定的影響。

動力

數據強大

動力上林肯大陸使用了2.0T/3.0T兩款發動機,匹配6AT變速箱,高配車型有四驅系統搭配,整體動力輸出數值十分高,尤其是3.0T車型的參數十分恐怖。

就目前的信息來看,預售的三款車型配置都十分高,可變懸挂/胎壓監測/一鍵啟動/電動後備廂/前後雷達/真皮座椅/主動剎車系統都是標配。結合42萬的預售價來看相比德系對手優勢不小。林肯大陸有着龐大的尺寸,精緻豪華的內飾氛圍和寬敞的空間,和德系對手相比它的價格更有誘惑力,配置更加豐富,唯一需要擔心的就是品牌了,還是那句話,人家不認識林肯標啊!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

這款歐系SUV品質好 而且20萬內配置完虐日產奇駿?

內飾設計兩台車的內飾設計都屬於乏善可陳,沒有什麼太大的亮點。不過,奇駿烤黑鋼琴漆的中控面板會更顯高級,而科雷嘉的設計則偏向於簡潔,甚至略顯可愛。乘坐空間與感受奇駿的後排奇駿的軸距2706mm,而科雷嘉的軸距則為2645mm,這就有了61mm的差距。

科雷嘉與逍客同為雷諾-日產聯盟CMF的產物,但是科雷嘉的價格已經越級去挑戰了奇駿的售價區間,那麼下面編者我就分析一下科雷嘉可否有能力挑戰奇駿的地位。首先,為了公平起見,我會拿科雷嘉的2.0L 兩驅尊貴版與奇駿的2.0L CVT舒適版 2WD來作對比,兩者售價均為19.68萬,而終端優惠都在2萬元左右,可能奇駿還能有更大一點點的優惠。

外觀設計

科雷嘉的前臉看上去很有流線感,而且很妖嬈,充滿法式浪漫。大燈處,兩個半矩形框分別把遠近光燈半包圍住,盡顯銳利。尾部則略為簡潔,僅僅是尾燈處向側面作了延伸。

奇駿的前臉用了倒梯形的設計,各處有棱有角,既具時尚,也不失陽剛之氣。整個側面相當簡練,C柱往後線條開始上揚。

動力總成

雖然兩者的發動機型號不一樣,但是各種參數均高度一致,最大輸出150馬力和200牛米。同樣地,兩款車都搭載了CVT變速箱,不過開起來就略有差異了。

科雷嘉給人的感覺是不緊不慢,踩下油門,轉速先升上去,然後車速再慢慢攀升。雖然是CVT變速箱,但模擬出了7個擋位,因此,在換擋時,能感受到變速箱的節奏感。

奇駿則與科雷嘉調性相反,油門響應很快,市區中開起來相當輕快。然而,一旦開快以後再加速,則會有多數小排量發動機存在的問題,就是光吼不走。至於CVT的匹配很好,開起來也平順,動力銜接流暢。

內飾設計

兩台車的內飾設計都屬於乏善可陳,沒有什麼太大的亮點。不過,奇駿烤黑鋼琴漆的中控面板會更顯高級,而科雷嘉的設計則偏向於簡潔,甚至略顯可愛。

乘坐空間與感受

奇駿的後排

奇駿的軸距2706mm,而科雷嘉的軸距則為2645mm,這就有了61mm的差距。進到車廂內的後排會發現奇駿的腿部空間相當富餘,不過座椅太大,包裹性略顯不足,但舒適性可以給高分,很柔軟。值得表揚的一點就是配有後座出風口。

科雷嘉的後排

科雷嘉的後排空間就不是那麼樂觀了,腿部空間尚還可以,然而座椅靠背較直,填充料較硬大大影響了乘坐舒適性。還有一點就是缺乏後座出風口,這就不太對得起次頂配這個配置了。

配置對比

奇駿用的是腳剎,而科雷嘉用的是电子駐車。而且,科雷嘉的18寸輪胎從視覺感受與抓地力上要優於奇駿的17寸輪胎,不過油耗上會略有吃虧。除此以外,科雷嘉比奇駿多出了前後排頭部氣囊和車頂行李架。其餘的配置差異,可以看下錶對比。

油耗及保養費用

鑒於動力總成上的一致性,同時整備質量也只差了十幾公斤。因此,兩台車均做出了百公里綜合油耗8.9L的好成績。

在保養費用的對比上,奇駿以6萬公里總保養費用5225元勝出,而科雷嘉的費用為6562元。奇駿送兩次保養,同時變速箱油終身免維護,而火花塞每10萬公里更換一次,這些都是它的保養費用低於科雷嘉的原因。

編者總結:

從以上對比不難看出,科雷嘉對比奇駿優勝在更時尚的外觀,以及一些配置。但是在駕駛和乘坐感受上都要遜於奇駿。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

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

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

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

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

世界上最著名的操作系統是用什麼語言編寫的?

Unix 與 C 語言的關係

在服務器領域,最著名的系統莫過於Unix了,即便Linux也是類Unix的操作系統。Unix 是用 C 語言編寫的,而且是世界上第一個用 C 語言編寫的操作系統。但是 Unix 是怎麼產生的?C 語言又是怎麼產生的?Unix 為什麼要用 C 語言來編寫?相信看完這篇文章你很快就會有了答案。

 

Multics 和 Unix

1964年,在 Unix 誕生之前,美國貝爾實驗室、通用電氣、和麻省理工學院聯合開發了一個項目,叫做 Multics操作系統。Multics 在當時取得了巨大的成功,因為它給了人們一個真正可用並且很穩定的操作系統。當時從學校到政府機關,到處都在安裝 Multics。

 

但是時間到了 1969年,因為種種原因,貝爾實驗室的 Ken Thompson 和 Dennis Ritchie 決定放棄Multics,於是貝爾實驗室退出了這個項目。Ken Thompson 和 Dennis Ritchie 決定開發一個更為簡單並且高效的操作系統,並把它命名為 Unix,於是他們倆就成為了 Unix 之父。

 

和 Unix 相比,Multics 有兩個比較致命的缺點,一是需要安裝在非常昂貴的機器上,普通的計算機沒有辦法安裝。二是設計非常複雜,這也是貝爾實驗室的工程師退出的主要原因。不過 Ken Thompson 和 Dennis Ritchie 還是從 Multics 上得到很多啟發,可以說 Unix 是 Multics 的簡化以及更新。

 

說到這裏,C 語言還沒有出場,因為它在那個時候還沒有被發明出來。Unix 操作系統的第一個版本是純粹用彙編語言編寫出來的。一直到了 1974年,第四個版本才改用 C 語言進行開發。在講 C 語言之前,我們還不得不先說一下 BCPL 和 B 語言。

 

BCPL 與 B 語言

Ken Thompson 和 Dennis Ritchie 最初用彙編語言來開發 Unix,因為在那個時候,所有的操作系統都是用彙編語言來開發的。不過當時 Multics 是一個例外,Multics 項目組使用了 PL/1 來進行開發。它的價值就在於向世人展示了用一門高級語言也可以開發出一套操作系統。Ken Thompson 和 Dennis Ritchie 也受到了鼓舞,他們決定用彙編之外的語言重新開發 Unix。他們最初的選擇是 BCPL。BCPL 最早是由 Martin Richards 發明用來開發編譯器的語言。但是 Ken Thompson 和 Dennis Ritchie很快發現,如果他們想在小型計算機 DEC PDP-7 上使用 BCPL,就需要對這個語言本身進行一些修改。於是就產生了後來的 B 語言。

 

 B 語言作為系統開發語言,也存在很多的缺點,於是後來它又被改進為 NB 語言 (New B)。可是 NB 還是有很多的問題,於是 Dennis Ritchie 就又發明了 C 語言,最終在 1974年,Ken Thompson 和 Dennis Ritchie 一起用 C 語言重新編寫了第四版的 Unix 系統。現在,C 語言已經成為了操作系統開發最常用的編程語言。

 

通往 C 語言與 Unix 之路

Dennis Ritchie 曾經解釋過自己為什麼要發明 C 語言,以及使用 B 語言過程中遇到的一些困難:

  • 只能處理計算機字:B語言所有的操作都是通過計算機字來處理的,在那個時候,能夠處理字節的編程語言對工程師們來說是一個夢。

  • 沒有類型:準確的說,B語言只有一種類型,計算機字。如果你有一個字符串,可能你需要把它分成多個變量來保存,比如一個21個字符的字符串,就需要 6 個變量。

  • 不支持浮點類型運算:在那個時候,大部分硬件已經都可以開始支持浮點運算了,但是 B 語言還不支持這種操作。 

 

B 語言的這些問題,開發低效,在機器上運行緩慢等等,都迫使 Dennis Ritchie 發明一種新的編程語言。最開始被稱為 New B,後來逐漸演化成了 C 語言。

 

C 語言解決了 B 語言的很多缺陷,並很快成為了開發操作系統最流行的一種編程語言。新版本的 Unix 以及今天很多類 Unix 的操作系統都是用 C 語言開發出來的。

 

好了,講到這裏,我想大家都清楚了 Unix 和 C 語言是怎麼來的了,以及為什麼要用 C 語言來編寫 Unix。現在有很多非常流行的語言比如 Java 和 Python,它們是一種更高級的編程語言,是在 C 語言這一層之上的。但是如果大家想做設備驅動或者系統內核模塊的修改,還是離不開 C 語言這把利器。

 

 

推薦閱讀:

《一篇文章帶你快速弄清楚什麼是終端》

《一篇文章看清楚 Linux 的職業發展方向》

《2020年你最需要掌握的11種編程語言》

《微軟:懸賞10萬美金破解 Linux 系統》

《安卓到底是不是Linux》

 

  

 

獲取知識乾貨、增加面試經驗、了解程序人生

歡迎關注微信公眾號

回復【面試】,獲取技術大廠面試寶典

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

【其他文章推薦】

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

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

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

※回頭車貨運收費標準

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

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

我去,這麼簡單的條件表達式竟然也有這麼多坑

最近,小黑哥在一個業務改造中,使用三目運算符重構了業務代碼,沒想到測試的時候竟然發生 NPE 的問題。

重構代碼非常簡單,代碼如下:

// 方法返回參數類型為 Integer
//  private Integer code;
SimpleObj simpleObj = new SimpleObj();
// 其他業務邏輯
if (simpleObj == null) {
    return -1;
} else {
    return simpleObj.getCode();
}

這段 if 判斷,小黑哥看到的時候,感覺很是繁瑣,於是使用條件表達式重構了一把,代碼如下:

// 方法返回參數類型為 Integer
SimpleObj simpleObj = new SimpleObj();
// 其他業務邏輯
return simpleObj == null ? -1 : simpleObj.getCode();

測試的時候,第四行代碼拋出了空指針,這裏代碼很簡單,顯然只有 simpleObj#getCode才有可能發生 NPE 問題。

但是我明明為 simpleObj做過判空判斷,simpleObj 對象肯定不是 null,那麼只有 simpleObj#getCode 返回為 null。但是我的代碼並沒有對這個方法返回值做任何操作,為何會觸發 NPE?

難道是又是自動拆箱導致的 NPE 問題?

在解答這個問題之前,我們首先複習一下條件表達式。

點贊再看,養成習慣。微信搜索『程序通事』,關注查看最新文章~

三目運算符

三目運算符,官方英文名稱:Conditional Operator ? :,又叫條件表達式,本文不糾結名稱,統一使用條件表達式。

條件表達式的基本用法非常簡單,它由三個操作數的運算符構成,形式為:

<表達式 1>?<表達式 2>:<表達式 3>

條件表達式的計算從左往右計算,首先需要計算計算表達式 1 ,其結果類型必須為 Booleanboolean,否則發生編譯錯誤。

當表達式 1 的結果為 true,將會執行表達式 2,否則將會執行表達式 3。

表達式 2 與表達式 3 最後的類型必須得有返回結果,即不能為是 void,若為 void ,編譯時將會報錯。

最後需要注意的是,表達式 2 與表達式 3 不會被同時執行,兩者只有一個會被執行。

踩坑案例

了解完三目運算符的基本原理,我們簡化一下開頭例子,復現一下三目運算符使用過程的一些坑。假設我們的例子簡化成如下:

boolean flag = true; //設置成true,保證表達式 2 被執行
int simpleInt = 66;
Integer nullInteger = null;

案例 1

第一個案例我們根據如下計算 result 的值。

int result = flag ? nullInteger : simpleInt;

這個案例為開頭的例子的簡化版本,運算上述代碼,將會發生 NPE 的。

為什麼會發發生 NPE 呢?

這裏可以給大家一個小技巧,當我們從代碼上沒辦法找到答案時,我們可以試試查看一下編譯之後字節碼,或許是 Java 編譯之後增加某些東西,從而導致問題。

使用 javap -s -c class 查看 class 文件字節碼,如下:

可以看到字節碼中加入一個拆箱操作,而這個拆箱只有可能發生在 nullInteger

那麼為什麼 Java 編譯器在編譯時會對表達式進行拆箱?難道所有数字類型的包裝類型都會進行拆箱嗎?

條件表達式表達式發生自動拆箱,其實官方在 「The Java Language Specification(簡稱:JLS)」15.25 節中做出一些規定,部分內容如下:

JDK7 規範

If the second and third operands have the same type (which may be the null type), then that is the type of the conditional expression.

If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T.

用大白話講,如果表達式 2 與表達式 3 類型相同,那麼這個不用任何轉換,條件表達式表達式結果當然與表達式 2,3 類型一致。

當表達 2 或表達式 3 其中任一一個是基本數據類型,比如 int,而另一個表達式類型為包裝類型,比如 Integer,那麼條件表達式表達式結果類型將會為基本數據類型,即 int

ps:有沒有疑問?為什麼不規定最後結果類型都為包裝類那?

這是 Java 語言層面一種規範,但是這個規範如果強制讓程序員執行,想必平常使用三目運算符將會比較麻煩。所以面對這種情況, Java 在編譯器在編譯過程加入自動拆箱進制。

所以上述代碼可以等同於下述代碼:

int result = flag ? nullInteger.intValue() : simpleInt;

如果我們一開始的代碼如上所示,那麼這裏錯誤點其實就很明顯了。

案例 2

接下來我們在第一個案例基礎上修改一下:

boolean flag = true; //設置成true,保證表達式 2 被執行
int simpleInt = 66;
Integer nullInteger = null;
Integer objInteger = Integer.valueOf(88);

int result = flag ? nullInteger : objInteger;

運行上述代碼,依然會發生 NPE 的問題。當然這次問題發生點與上一個案例不一樣,但是錯誤原因卻是一樣,還是因為自動拆箱機制導致。

這一次表達式 2 與表達式 3 都為包裝類 Integer,所以條件表達式的最後結果類型也會是 Integer

但是由於 result是 int 基本數據類型,好傢伙,數據類型不一致,編譯器將會對條件表達式的結果進行自動拆箱。由於結果為 null,自動拆箱將報錯了。

上述代碼等同為:

int result = (flag ? nullInteger : objInteger).intValue();

案例 3

我們再稍微改造一下案例 1 的例子,如下所示:

boolean flag = true; //設置成true,保證表達式 2 被執行
int simpleInt = 66;
Integer nullInteger = null;
Integer result = flag ? nullInteger : simpleInt;

案例 3 與案例 1 右邊部分完全相同,只不過左邊部分的類型不一樣,一個為基本數據類型 int,一個為 Integer

按照案例 1 的分析,這個也會發生 NPE 問題,原因與案例 1 一樣。

這個之所以拿出來,其實想說下,上述條件表達式的結果為 int 類型,而左邊類型為 Integer,所以這裏將會發生自動裝箱操作,將 int類型轉化為 Integer

上述代碼等同為:

Integer result = Integer.valueOf(flag ? nullInteger.intValue() : simpleInt);

案例 4

最後一個案例,與上面案例都不一樣,代碼如下:

boolean flag = true; //設置成true,保證表達式 2 被執行
Integer nullInteger = null;
Long objLong = Long.valueOf(88l);

Object result = flag ? nullInteger : objLong;

運行上述代碼,依然將會發生 NPE 的問題。

這個案例表達式 2 與表達式 3 類型不一樣,一個為 Integer,一個為 Long,但是這兩個類型都是 Number的子類。

面對上述情況,JLS 規定:

Otherwise, binary numeric promotion (§5.6.2) is applied to the operand types, and the type of the conditional expression is the promoted type of the second and third operands.

Note that binary numeric promotion performs value set conversion (§5.1.13) and may perform unboxing conversion (§5.1.8).

大白話講,當表達式 2 與表達式 3 類型不一致,但是都為数字類型時,低範圍類型將會自動轉為高範圍數據類型,即向上轉型。這個過程將會發生自動拆箱。

Java 中向上轉型並不需要添加任何轉化,但是向下轉換必須強制添加類型轉換。

上述代碼轉化比較麻煩,我們先從字節碼上來看:

第一步,將 nullInteger拆箱。

第二步,將上一步的值轉為 long 類型,即 (long)nullInteger.intValue()

第三步,由於表達式 2 變成了基本數據類型,表達式 3 為包裝類型,根據案例 1 講到的規則,包裝類型需要轉為基本數據類型,所以表達式 3 發生了拆箱。

第四步,由於條件表達式最後的結果類型為基本數據類型:long,但是左邊類型為 Object,這裏就需要把 long 類型裝箱轉為包裝類型。

所以最後代碼等同於:

Object result = Long.valueOf(flag ? (long)nullInteger.intValue() : objLong.longValue());

總結

看完上述四個案例,想必大家應該會有種感受,沒想到這麼簡單的條件表達式,既然暗藏這麼多「殺機」。

不過大家也不用過度害怕,不使用條件表達式。只要我們在開發過程重點注意包裝類型的自動拆箱問題就好了,另外也要注意條件表達式的計算結果再賦值的時候自動拆箱引發的 NPE 的問題。

最好大家在開發過程中,都遵守一定的規範,即保持表達式 2 與表達式 3 的類型一致,不讓 Java 編譯器有自動拆箱的機會。

建議大家沒事經常看下阿里出品的『Java 開發手冊』,在最新的「泰山版」就增加條件表達式的這一節規範。

ps:公號消息回復:『開發手冊』,獲取最新版的 Java 開發手冊。

最後一定要做好的單元測試,不要慣性思維,覺得這麼簡單的一個東西,看起來根本不可能出錯的。

參考

  1. Java 開發手冊-泰山版
  2. 《Java 開發手冊》解讀:三目運算符為何會導致 NPE?

歡迎關注我的公眾號:程序通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:studyidea.cn

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

【JVM故事】一個Java字節碼文件的誕生記

萬字長文,完全虛構。

 

 

組裡來了個實習生,李大胖面完之後,覺得水平一般,但還是留了下來,為什麼呢?各自猜去吧。

李大胖也在心裏開導自己,學生嘛,不能要求太高,只要肯上進,慢慢來。就稱呼為小白吧。

小白每天來的很早,走的很晚,都在用功學習,時不時也向別人請教。只是好像天資差了點。

都快一周了,才能寫些“簡單”的代碼,一個註解,一個接口,一個類,都來看看吧:

public @interface Health {

    String name() default "";
}


public interface Fruit {

    String getName();

    void setName(String name);

    int getColor();

    void setColor(int color);
}


@Health(name = "健康水果")
public class Apple implements Fruit {

    private String name;
    private int color;
    private double weight = 0.5;

    @Override
    public String getName() {
        return name;
    }

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

    @Override
    public int getColor() {
        return color;
    }

    @Override
    public void setColor(int color) {
        this.color = color;
    }

    public double weight() {
        return weight;
    }

    public void weight(double weight) {
        this.weight = weight;
    }
}

與周圍人比起來,小白進步很慢,也許是自己不夠聰明,也許是自己不適合干這個,小白好像有點動搖了。

這幾天,小白明顯沒有一開始那麼上進了,似乎有點想放棄,這不,趴在桌子上竟然睡着了。

 

(二)

 

在夢中,小白來到一個奇怪又略顯陰森的地方,眼前有一個破舊的小房子,從殘缺不全的門縫裡折射出幾束光線。

小白有些害怕,但還是鎮定了下,深呼吸幾口,徑直朝着小房子走去。

小白推開門,屋裡沒有人。只有一個“機器”在桌子旁大口大口“吃着”東西,背後也不時的“拉出”一些東西。

小白很好奇,就湊了上去,準備仔細打量一番。

“你要幹嘛,別影響我工作”。突然冒出一句話,把小白嚇了一大跳,慌忙後退三步,媽呀,心都快蹦出來了。

“你是誰呀?”,驚慌中小白說了句話。

“我是編譯器”,哦,原來這個機器還會說話,小白這才緩了過來。

“編譯器”,小白好像聽說過,但一時又想不起,於是猜測到。

“網上評論留言里說的小編是不是就是你啊”?

“你才是呢”,編譯器白了一眼,沒好聲氣的說到。

要不是看在長得還行的份上,早就把你趕走了,編譯器心想。

“哦,我想起來了,編譯器嘛,就是編譯代碼的那個東西”,小白恍然大悟到。

“請注意你的言詞,我不是個東西,哦,不對,我是個東西,哦,好像也不對,我。我。”,編譯器自己也快暈了。

編譯器一臉的無奈,遇上這樣的人,今天我認栽了。

小白才不管呢,心想,今天我竟然見到了編譯器,我得好好請教請教他。

那編譯器會幫助她嗎?

 

 

(三)

 

小白再次走上前來,定睛一看,才看清楚,編譯器吃的是Java源碼,拉的是class(字節碼)文件。

咦,為啥這個代碼這麼熟悉呢,不就是我剛剛寫的那些。“停,停,快停下來了”。編譯器被小白叫停了。

“你又要幹嘛啊”?編譯器到。

“嘻嘻,這個代碼是我寫的,我想看看它是怎麼被編譯的”,小白到。

編譯器看了看這個代碼,這麼“簡單”,她絕對是個菜鳥。哎,算了,還是讓她看看吧。

不過編譯器又到,“整個編譯過程是非常複雜的,想要搞清楚裏面的門道是不可能的,今天也就只能看個熱鬧了”。

“編譯后的內容都是二進制數據,再通俗點說,就是一個長長的字節數組(byte[])”,編譯器繼續說,“通常把它寫入文件,就是class文件了”。

“但這不是必須的,也可以通過網絡傳到其它地方,或者保存在內存中,用完之後就丟棄”。

“哇,還可以這樣”,小白有些驚訝。編譯器心想,你是山溝里出來的,沒見過世面,大驚小怪。

繼續到,“從數據結構上講,數組就是一段連續的空間,是‘沒有結構’的,就像一個線段一樣,唯一能做的就是按索引訪問”。

小白到,“編譯后的內容一定很繁多,都放到一個數組裡面,怎麼知道什麼東西都在哪呢?不都亂套了嘛”。

編譯器覺得小白慢慢上道了,心裏有一絲安慰,至少自己的講解不會完全白費。於是繼續到。

“所以JVM的那些大牛們早就設計好了字節碼的格式,而且還把它們放入到了一個字節數組裡面”。

小白很好奇到,“那是怎麼實現的呢”?

“其實也沒有太高深的內容,既然數組是按位置的,那就規定好所有內容的先後順序,一個接一個往數組裡放唄”。

“如果內容的長度是固定(即定長)的,那最簡單,直接放入即可”。

“如果內容長度是不固定(即變長)的,也很簡單,在內容前用一到兩個字節存一下內容的長度不就OK了”。

 

 

(四)

 

“字節碼的前4個字節必須是一個固定的数字,它的十進制是3405691582,大部分人更熟悉的是它的十六進制,0xCAFEBABE”。

“通常稱之為魔術数字(Magic),它主要是用來區分文件類型的”,編譯器到。

“擴展名(俗稱後綴名)不是用來區分文件類型的嗎”?小白說到,“如.java是Java文件,.class是字節碼文件”。

“擴展名確實可以區分,但大部分是給操作系統用的,或給人看到。如我們看到.mp3時知道是音頻、.mp4是知道是視頻、.txt是文本文件”。

“操作系統可以用擴展名來關聯打開它的軟件,比如.docx就會用word來打開,而不會用文本文件”。編譯器繼續到。

“還有一個問題就是擴展名可以很容易被修改,比如把一個.java手動改為.class,此時讓JVM來加載這個假的class文件會怎樣呢”?

“那JVM先讀取開頭4個字節,發現它不是剛剛提到的那個魔數,說明它不是合法的class文件,就直接拋異常唄”,小白說到。

“很好,真是孺子可教”,編譯器說道,“不過還有一個問題,不知你是否注意到?4個字節對應Java的int類型,int類型的最大值是2147483647”。

“但是魔數的值已經超過了int的最大值,那怎麼放得下呢,難道不會溢出嗎”?

“確實啊,我怎麼沒發現呢,那它到底是怎麼放的呢”?小白到。

“其實說穿了不值得一提,JVM是把它當作無符號數對待的。而Java是作為有符號數對待的。無符號數的最大值基本上是有符號數最大值的兩倍”。

“接下來的4個字節是版本號,不同版本的字節碼格式可能會略有差異,其次在運行時會校驗,如JDK8編譯后的字節碼是不能放到JDK7上運行的”。

“這4個字節中的前2個是次(minor)版本,后2個是主(major)版本”。編譯器繼續到,“比如我現在用的JDK版本是1.8.0_211,那次版本就是0,主版本就是52”。

“所以前8個字節的內容是,0xCAFEBABE,0,52,它們並不是源代碼里的內容”。

Magic [getMagic()=0xcafebabe]
MinorVersion [getVersion()=0]
MajorVersion [getVersion()=52]

 

(五)

 

當編譯器讀到源碼中的public class的時候,然後就就去查看一個表格,如下圖:

自顧自的說著,“public對應的是ACC_PUBLIC,值為0x0001,class默認就是,然後又讀ACC_SUPER的值0x0020”。

“最後把它倆合起來(按位或操作),0x0001 | 0x0020 => 0x0021,然後把這個值存起來,這就是這個類的訪問控制標誌”。

小白這次算是開了眼界了,只是還有一事不明,“這個ACC_SUPER是個什麼鬼”?

編譯器解釋到,“這是歷史遺留問題,它原本表達在調用父類方法時會特殊處理,不過現在已經不再管它了,直接忽略”。

接着讀到了Apple,它是類名。編譯器首先要獲取類的全名,org.cnt.java.Apple。

然後對它稍微轉換一下形式,變成了,org/cnt/java/Apple,“這就是類名在字節碼中的表示”。

編譯器發現這個Apple類沒有顯式繼承父類,表明它繼承自Object類,於是也獲取它的全名,java/lang/Object。

接着讀到了implements Fruit,說明該類實現了Fruit接口,也獲取全名,org/cnt/java/Fruit。

小白說到,“這些比較容易理解,全名中把點號(.)替換為正斜線(/)肯定也是歷史原因了。但是這些信息如何存到數組裡呢”?

“把點號替換為正斜線確實是歷史原因”,編譯器繼續到,“這些字符串雖然都是類名或接口名,但本質還是字符串,類名、接口名只是賦予它的意義而已”。

“除此之外,像字段名、方法名也都是字符串,同理,字段名、方法名也是賦予它的意義。所以字符串是一種基本的數據,需要得到支持”。

“除了字符串之外,還有整型数字,浮點数字,這些也是基本的數據,也需要得到支持”。

因此,設計者們就設計出了以下幾種類型,如圖:

“左邊是類型名稱,方便理解,右邊是對應的值,用於存儲”,編譯器繼續到。

“這裏的Integer/Long/Float/Double和Utf8都是具體保存數據用的,表示整型數/浮點數和字符串。其它的類型大都是對字符串的引用,並賦予它一定的意義”。

“所以類名首先被存儲為一個字符串,也就是Utf8,它的值對應的是1”。編譯器接着到,“由於字符串是一個變長的,所以就先用兩個字節存儲字符串的長度,接着跟上具體的字符串內容”。

所以字符串的結構就是這樣,如圖:

“類名字符串的存儲數據為,1、18、org/cnt/java/Apple。第一個字節為1,表明是Utf8類型,第2、3兩個字節存儲18,表示字符串長度是18,接着存儲真正的字符串。所以共用去1 + 2 + 18 => 21個字節”。

“父類名字符串存儲為,1、16、java/lang/Object。共用去19個字節”。

“接口名字符串存儲為,1、18、org/cnt/java/Fruit。共用去21個字節”。

小白聽的不住點頭,編譯器喘口氣,繼續講解。

“字符串存好后,就該賦予它們意義了,在後續的操作中肯定涉及到對這些字符串的引用,所以還要給每個字符串分配一個編號”。

如Apple為#2,即2號,Object為#4,Fruit為#6。

“由於這三個字符串都是類名或接口名,按照設計規定應該使用Class表示,對應的值為7,然後再指定一個字符串的編號即可”。

因此類或接口的表示如下圖:

“先用1個字節指明是類(接口),然後再用2個字節存儲一個字符串的編號。整體意思很直白,就是把這個編號的字符串當作類名或接口名”。

“類就表示為,7、#2。7表示是Class,#2表示類名稱那個字符串的存儲編號。共用去3個字節”。

“父類就表示,7、#4。共用去3個字節。接口就表示為,7、#6。共用去3個字節”。

其實這三個Class也分別給它們一個編號,方便別的地方再引用它們。

 

 

(六)

 

“其實上面這些內容都是常量,它們都位於常量池中,它們的編號就是自己在常量池中的索引”。編譯器說到。

“常量池很多人都知道,起碼至少是聽說過。但絕大多數人對它並不十分熟悉,因為很少有人見過它”。

編譯器繼續到,“今天你可算是來着了”,說著就把小白寫的類編譯後生成的常量池擺到了桌子上。

“這是什麼東西啊,這麼多,又很奇怪”,小白說到,這也是她第一次見。

ConstantPoolCount [getCount()=46]
ConstantPool [
#0 = null
#1 = ConstantClass [getNameIndex()=2, getTag()=7]
#2 = ConstantUtf8 [getLength()=18, getString()=org/cnt/java/Apple, getTag()=1]
#3 = ConstantClass [getNameIndex()=4, getTag()=7]
#4 = ConstantUtf8 [getLength()=16, getString()=java/lang/Object, getTag()=1]
#5 = ConstantClass [getNameIndex()=6, getTag()=7]
#6 = ConstantUtf8 [getLength()=18, getString()=org/cnt/java/Fruit, getTag()=1]
#7 = ConstantUtf8 [getLength()=4, getString()=name, getTag()=1]
#8 = ConstantUtf8 [getLength()=18, getString()=Ljava/lang/String;, getTag()=1]
#9 = ConstantUtf8 [getLength()=5, getString()=color, getTag()=1]
#10 = ConstantUtf8 [getLength()=1, getString()=I, getTag()=1]
#11 = ConstantUtf8 [getLength()=6, getString()=weight, getTag()=1]
#12 = ConstantUtf8 [getLength()=1, getString()=D, getTag()=1]
#13 = ConstantUtf8 [getLength()=6, getString()=<init>, getTag()=1]
#14 = ConstantUtf8 [getLength()=3, getString()=()V, getTag()=1]
#15 = ConstantUtf8 [getLength()=4, getString()=Code, getTag()=1]
#16 = ConstantMethodRef [getClassIndex()=3, getNameAndTypeIndex()=17, getTag()=10]
#17 = ConstantNameAndType [getNameIndex()=13, getDescriptorIndex()=14, getTag()=12]
#18 = ConstantDouble [getDouble()=0.5, getTag()=6]
#19 = null
#20 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=21, getTag()=9]
#21 = ConstantNameAndType [getNameIndex()=11, getDescriptorIndex()=12, getTag()=12]
#22 = ConstantUtf8 [getLength()=15, getString()=LineNumberTable, getTag()=1]
#23 = ConstantUtf8 [getLength()=18, getString()=LocalVariableTable, getTag()=1]
#24 = ConstantUtf8 [getLength()=4, getString()=this, getTag()=1]
#25 = ConstantUtf8 [getLength()=20, getString()=Lorg/cnt/java/Apple;, getTag()=1]
#26 = ConstantUtf8 [getLength()=7, getString()=getName, getTag()=1]
#27 = ConstantUtf8 [getLength()=20, getString()=()Ljava/lang/String;, getTag()=1]
#28 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=29, getTag()=9]
#29 = ConstantNameAndType [getNameIndex()=7, getDescriptorIndex()=8, getTag()=12]
#30 = ConstantUtf8 [getLength()=7, getString()=setName, getTag()=1]
#31 = ConstantUtf8 [getLength()=21, getString()=(Ljava/lang/String;)V, getTag()=1]
#32 = ConstantUtf8 [getLength()=16, getString()=MethodParameters, getTag()=1]
#33 = ConstantUtf8 [getLength()=8, getString()=getColor, getTag()=1]
#34 = ConstantUtf8 [getLength()=3, getString()=()I, getTag()=1]
#35 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=36, getTag()=9]
#36 = ConstantNameAndType [getNameIndex()=9, getDescriptorIndex()=10, getTag()=12]
#37 = ConstantUtf8 [getLength()=8, getString()=setColor, getTag()=1]
#38 = ConstantUtf8 [getLength()=4, getString()=(I)V, getTag()=1]
#39 = ConstantUtf8 [getLength()=3, getString()=()D, getTag()=1]
#40 = ConstantUtf8 [getLength()=4, getString()=(D)V, getTag()=1]
#41 = ConstantUtf8 [getLength()=10, getString()=SourceFile, getTag()=1]
#42 = ConstantUtf8 [getLength()=10, getString()=Apple.java, getTag()=1]
#43 = ConstantUtf8 [getLength()=25, getString()=RuntimeVisibleAnnotations, getTag()=1]
#44 = ConstantUtf8 [getLength()=21, getString()=Lorg/cnt/java/Health;, getTag()=1]
#45 = ConstantUtf8 [getLength()=12, getString()=健康水果, getTag()=1]
]

“在常量池前面會用2個字節來存儲常量池的大小,需要記住的是,這個大小不一定就是池中常量的個數。但它減去1一定是最大的索引”。

“因為,常量池中為0的位置(#0)永遠不使用,還有Long和Double類型一個常量佔2個連續索引(沒錯,又是歷史原因),實際只是用了第1個索引,第2個索引永遠空着(參見#18、#19)”。

編譯器繼續到,“#0是特殊的,用來表示‘沒有’的意思,其它地方如果想表達沒有的話,可以指向它。如Object是沒有父類的,所以它的父類指向#0,即沒有”。

“所以常量都是從#1開始。可以看看#1到#6的內容,就是剛剛上面講的”。編譯器說到。

“真是學到不少知識啊”,小白說到,“關於常量池能不能再多講點”?編譯器只好繼續講。

 

 

(七)

 

“常量池就是一個容器,它裏面放了各種各樣的所有信息,並且為每個信息分配一個編號(即索引),如果想要在其它地方使用這些信息,直接使用這個編號就行了”。

編譯器繼續到,“這個常量池在一些語言中也被稱為‘符號表’,通過編號來使用的這種方式也被稱為‘符號引用’”。

相信很多愛學習的同學對符號表和符號引用這兩個詞都很熟悉,不管之前是不是真懂,至少現在應該是真的搞懂了。因為你已經看到了。

“採用這種常量池和常量引用方式的好處其實很多,就說個最容易想到的,就是重複利用,節省空間,便於管理”。編譯器繼續說。

“比如一個類里有10個方法,每個方法里都定義一個length的局部變量,那麼length這個名字就會出現在常量池裡面,且只會出現一次,那10個方法都是對它的引用而已”。

“如果有一個方法的名字也叫length的話,那也是對同一個常量的引用,因為這個length常量只是個字符串數據而已,本身沒有明確含義,它的含義來自於引用它的常量”。

“哦,原來如此”,小白開悟到,“‘符號表、符號引用’這些‘高大上’的叫法,不過就是根據索引去列表裡獲取元素罷了”,哈哈。

編譯器看到小白這麼開心,就準備拋出一個問題,“打壓”一下她。於是說到。

“常量池看上去和數組/列表非常相似,都是容器且都是基於索引訪問的。為啥常量池只被稱為符號表,而不是符號數組或符號列表呢”?

小白自然回答不上來。編譯器繼續說,“表的英文單詞是Table。它和數組/列表的唯一區別就是,數組/列表裡的元素長度都是固定的。表裡的元素長度是不固定的”。

“常量池中的好幾種常量的長度都是變長的,所以自然是表了”。

小白點了點頭,心裏想,這編譯器就是厲害,我這輩子看來都無法達到他的高度了。

編譯器繼續說到,“字節碼的前8個字節存儲魔數和版本,接着的2個(9和10)字節存儲常量池的大小,後面接着(從11開始)就是整個常量池的內容了”。

“之所以把常量池放這麼靠前,是因為後面的所有內容都要依賴它、引用它”。

緊跟在常量池之後的就是這個類的基本信息,如下:

“首先用2個字節存儲上面已經計算好的訪問控制標誌,即0x0021”。

“然後用2個字節存儲這個類在常量池中的索引,就是#1”。

“然後用2個字節存儲該類的父類在常量池中的索引,就是#3”。

“由於接口可以有多個,所以再用2個字節存儲接口的個數,因為只實現了1個接口,所以就存儲数字1”。

“接着存儲所有接口在常量池中的索引,每個接口用2個字節。因為只實現了1個接口,所以存儲的索引就是#5”。

AccessFlags [getAccessFlags()=0x21, getAccessFlagsString()=[ACC_PUBLIC, ACC_SUPER]]
ThisClass [getClassIndex()=1, getClassName()=org/cnt/java/Apple]
SuperClass [getClassIndex()=3, getClassName()=java/lang/Object]
InterfacesCount [getCount()=1]
Interfaces [getClassIndexes()=[5], getClassNames()=[org/cnt/java/Fruit]]

 

 

 

(八)

 

編譯器繼續到,“接下來該讀取字段信息了”。當讀到private時,就去下面這張表裡找:

找到ACC_PRIVATE,把它的值0x0002保存以下,這就是該字段的訪問控制標誌。

接着讀到的是String,這是字段的類型,然後會把這個String類型存入常量池,對應的索引是#8。

可以看到是一個Utf8,說明是字符串,內容是 Ljava/lang/String; ,以大寫L開頭,已分號;結尾,中間是類型全名,這是在字節碼中表示類(對象)類型的方式。

接着讀到的是name,這是字段名稱,也是個字符串,同樣也把它放入常量池,對應的索引是#7。

編譯器說到,“現在一個字段的信息已經讀取完畢,按照相同的方式把剩餘的兩個字段也讀取完畢”。

“那字段的信息又該怎麼存儲呢”?小白問到。“不要着急嘛”,編譯器說著就拿出了字段的存儲格式:

首先2個字節是訪問控制標誌,接着2個字節是字段名稱在常量池中的索引,接着2個字節是字段描述(即類型)在常量池中的索引。

接着2個字節就是屬性個數,然後就是具體的屬性信息了。例如字段上標有註解的話,這個註解信息就會放入屬性信息里。

編譯器繼續說到,“屬性信息是字節碼中比較複雜的內容,這裏就不說太多了”。接着就可以按格式整理數據了。

因為一個類的字段可以有多個,所以先用2個字節存儲一下字段數目,本類有3個字段,所以就存儲個3。

第一個字段,0x0002、#7、#8、0。共用去8個字節,因為自動沒有屬性內容。

第二個字段,0x0002、#9、#10、0。共用去8個字節。

第二個字段,0x0002、#11、#12、0。共用去8個字節。

編譯器接着說,“所以存儲這3個字段信息共用去2 + 8 + 8 + 8 => 26個字節”。

小白說到,“我現在基本已經搞明白套路了。其實有些東西沒有想象中的那麼複雜啊”。

“複雜的東西還是有的,我們現在先不考慮”,編譯器說到,“還有一個問題,不知你發現了沒有”。

字段color的類型是int,但是在常量池中卻變為大寫字母I,同樣weight的類型是double,常量池中卻是大寫字母D。

小白說到,“我來猜測一下吧,int、double是Java中的數據類型,I、D是與之對應的在JVM中的表示形式。對吧”?

“算你聰明”,編譯器說到,“其實Java和JVM之間關於類型這塊有一個映射表”,如下:

有兩個需要注意。“第一點上面已經說過了,就是類都會映射成LClassName;這種形式,如Object映射為Ljava/lang/Object;”。

第二點是數組,“數組在Java中用一對中括號([])表示,在JVM中只用左中括號([)表示。也就是[]映射為[”。

“多維數組也一樣,[][][]映射為[[[”。然後還有類型,“Java是把類型放到前面,JVM是把類型放到後面”。如double[]映射為[D。

“double[][][]映射為[[[D”。同理,“String[]映射為[Ljava/lang/String;,Object[][]映射為[[Ljava/lang/Object;”。

“我似乎又明白了一些,Java有自己的規範,字節碼也有自己的規範,它們之間的映射關係早都已經定義好了”。小白繼續到。

“只要按照這種映射關係,就能把Java源碼給轉換為字節碼。是吧”?

“粗略來說,可以這麼理解,其實這就是編譯了,但一定要清楚,真正的編譯是非常複雜的一個事情”,編譯器到。

小白說到,“字段完了之後,肯定該方法了,就交給我吧,讓我也試試”。

“年輕人啊,就是生猛,你來試試吧”。編譯器說到。

FieldsCount [getCount()=3]
Fields [
#0 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=7, getName()=name, getDescriptorIndex()=8, getDescriptor()=Ljava/lang/String;, getAttributesCount()=0, getAttributes()=[]]
#1 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=9, getName()=color, getDescriptorIndex()=10, getDescriptor()=I, getAttributesCount()=0, getAttributes()=[]]
#2 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=12, getDescriptor()=D, getAttributesCount()=0, getAttributes()=[]]
]

 

 

 

(九)

 

小白說,“方法呢肯定也有自己的格式,你把它找出來我看看”。

“好好,我這就找”,編譯器苦笑到。我堂堂一個編譯器,今天竟然成了小白的助手,慚愧啊。

說著編譯器就找到了,於是放到了桌子上:

“咦,怎麼和字段的一模一樣”,小白到。那這就更簡單了。

先是訪問控制標誌,接着是方法名稱索引,然後是方法描述索引,最後是和方法關聯的屬性。於是照貓畫虎,小白就開始了。

先讀到public關鍵字,這是個訪問控制修飾符,肯定也有一張表和它對應,可以找到這個關鍵字對應的數值。

還沒等小白開口,編譯器就趕緊把表找出來了:

小白繼續,ACC_PUBLIC對應的值是0x0001,就把這個值先保存起來。

然後是方法的名字,getName,是一個字符串,照例把它存入常量池,並且有一個索引,就是#26。

接着該方法的描述了,小白認為方法和字段是不同的,除了有返回類型之外,還有參數呢,這該咋整呢?

於是就問編譯器,“方法的描述應該也有格式吧”?

“你越來越聰明了”,編譯器說到,“其實也很簡單,我來簡單說下吧”。

“在Java中如果把訪問控制符、方法名、參數名、方法體都去掉,其實就剩下‘方法簽名’了”。

例如,沒有入參沒有返回值的,就是這個樣子,void()。

返回值為String,入參為int,double,String的,其實就是這樣個子,String(int, double, String)。

“這個方法簽名其實就是在Java中對方法的描述,在字節碼中和它差不多,就是把返回類型放到後面,把參數間的逗號去掉”。

因此void()映射為()V,這裏要注意的是void對應的是大寫字母V。

String(int, double, String)映射為(IDLjava/lang/String;)Ljava/lang/String;

“不難,不難”,小白說到,於是又繼續開始了。

小白按照這種格式,把剛剛的那個方法描述也存入了常量池,得到的索引就是#27。

小白按這個套路把6個方法都整理好了,接下來該按格式把數據寫入字節數組了。

編程新說注:方法的代碼對應的是JVM的指令,這裏就忽略不談了,後續可能會單獨再說。

編譯器提醒小白說,“你是不是還漏掉了一個方法啊”?

小白又看了一遍Java源碼,仔細數了數,是6個呀,沒錯啊。

編譯器說到,你在學習時有沒有見過這樣一句話,“當類沒有定義構造函數時,編譯器會為它生成一個默認的無參構造函數”。

小白連忙點頭,“嗯嗯嗯,見過的”。

“這就是了”,編譯器說道,“不過需要注意的是,在字節碼中構造方法的名字都是<init>,返回類型都是V”。

“這也是規定的吧”,小白說到,編譯器點了點頭。

編譯器又說到,“其實還有方法的參數信息,如參數位置,參數類型,參數名稱,參數的訪問控制標誌等”。

“這些信息都是放在方法格式里最後的屬性信息中的,咱們也暫時不說它們了”。

編程新說注

在JDK7及以前,字節碼中不包含方法的參數名。因為JVM執行指令時,參數是按位置傳入的,所以參數名對代碼的執行沒有用處。

由於越來越多的框架採用按方法參數名進行數值綁定,Java也只好在JDK8時加入了對參數名的支持。

不過需要設置一下編譯器的–parameters參數,這樣才能把方法參數名也放入字節碼中。

可以看看常量池中的#32是“MethodParameters”字符串,說明字節碼中已經包含參數名了。

常量池中#7、#9、#11三個字符串就是參數名,同時也是字段名,這就是復用的好處。

編程新說注方法的格式和字段的格式完全一樣,就不再演示寫入過程了。

因此這個類共有7個方法。

MethodsCount [getCount()=7]
Methods [
#0 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=13, getName()=<init>, getDescriptorIndex()=14, getDescriptor()=()V, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=3, getMaxLocals()=1, getCodeLength()=12, getJvmCode()=JvmCode [getCode()=12], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=3, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=8], LineNumTable [getStartPc()=4, getLineNumber()=12], LineNumTable [getStartPc()=11, getLineNumber()=8]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=12, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#1 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=26, getName()=getName, getDescriptorIndex()=27, getDescriptor()=()Ljava/lang/String;, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=1, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=16]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#2 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=30, getName()=setName, getDescriptorIndex()=31, getDescriptor()=(Ljava/lang/String;)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=2, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=21], LineNumTable [getStartPc()=5, getLineNumber()=22]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=7, getDescriptorIndex()=8, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=7, getAccessFlags()=0x0]]]]]
#3 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=33, getName()=getColor, getDescriptorIndex()=34, getDescriptor()=()I, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=1, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=26]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#4 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=37, getName()=setColor, getDescriptorIndex()=38, getDescriptor()=(I)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=2, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=31], LineNumTable [getStartPc()=5, getLineNumber()=32]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=9, getDescriptorIndex()=10, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=9, getAccessFlags()=0x0]]]]]
#5 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=39, getDescriptor()=()D, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=35]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#6 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=40, getDescriptor()=(D)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=3, getMaxLocals()=3, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=39], LineNumTable [getStartPc()=5, getLineNumber()=40]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=11, getDescriptorIndex()=12, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=11, getAccessFlags()=0x0]]]]]
]

編程新說注方法部分的輸出內容很多,是因為包含了方法體的代碼的信息。

 

 

(十)

 

“真是後生可畏啊”,編譯器感慨到。“小白竟然也能按照套路去在做點事情了”。

不過編譯器並不自危,因為最核心的內容是,可執行代碼如何轉換為JVM指令集中的指令,這可是“壓箱底”的乾貨,可不能隨便告訴別人,長得再好看也不行。哈哈,O(∩_∩)O。

接着編譯器拿出一個完整的字節碼文件格式圖給小白看:

小白看完后說,“和剛剛講的一樣,只是最後也有這個屬性信息啊”。

編譯器說,“屬性信息是字節碼文件中非常複雜的內容,可以暫時不管用了”。

上面已經說了,至少註解的相關內容是放在屬性信息里的。

那就看看你寫的這個類的屬性信息都是什麼吧:

AttributesCount [getCount()=2]
Attributes [
#0 = SourceFile [getSourcefileIndex()=42]
#1 = RuntimeVisibleAnnotations [getNumAnnotations()=1, getAnnotations()=[Annotation [getTypeIndex()=44, getNumElementValuePairs()=1, getElementValuePairs()=[ElementValuePair [getElementNameIndex()=7, getElementValue()=ElementValue [getTag()=ElementValueTag [getTagChar()=s], getUnion()=ElementValueUnion [getConstValueIndex()=45]]]]]]]
]

編譯器繼續說,共有2條屬性信息,第一條是源代碼文件的名字,在常量池中的#42。其實就是Apple.java了。

第二條是運行時可見的註解信息,本類共有1個註解,註解類型是常量池中的#44。其實就是Lorg/cnt/java/Health;了。

該註解共顯式設置了1對屬性值。屬性名稱是常量池中的#7,就是name了,類型是小寫的s,表示String類型,屬性值是#45,也就是“健康水果”了。

下圖中的這些類型,都是可以用於註解屬性的類型:

最後,編譯器打印出一行信息:

—–bytes=1085—–

小白說,“這是什麼意思”?“這是編譯后產生的字節碼的總長度,是1085個字節”,編譯器到。

小白剛想表達對編譯器的感謝,忽然聞到一陣香味,而且是肉香。

PS:最後幾句話就不寫了,請你來補充完整吧,嘻嘻。

 

 

>>> 熱門文章集錦 <<<

 

畢業10年,我有話說

【面試】我是如何面試別人List相關知識的,深度有點長文

我是如何在畢業不久只用1年就升為開發組長的

爸爸又給Spring MVC生了個弟弟叫Spring WebFlux

【面試】我是如何在面試別人Spring事務時“套路”對方的

【面試】Spring事務面試考點吐血整理(建議珍藏)

【面試】我是如何在面試別人Redis相關知識時“軟懟”他的

【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(乾貨 | 建議珍藏)

【面試】如果你這樣回答“什麼是線程安全”,面試官都會對你刮目相看(建議珍藏)

【面試】迄今為止把同步/異步/阻塞/非阻塞/BIO/NIO/AIO講的這麼清楚的好文章(快快珍藏)

【面試】一篇文章幫你徹底搞清楚“I/O多路復用”和“異步I/O”的前世今生(深度好文,建議珍藏)

【面試】如果把線程當作一個人來對待,所有問題都瞬間明白了

Java多線程通關———基礎知識挑戰

品Spring:帝國的基石

 

 

 

作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號的二維碼,歡迎關注!

 

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

【其他文章推薦】

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

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

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

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

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

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

※回頭車貨運收費標準

基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)

上篇文章完成了分類和標籤頁面相關的共6個接口,本篇繼續來寫博客增刪改查API的業務。

供前端查詢用的接口還剩下一個,這裏先補上。

友鏈列表

分析:返回標題和對應的鏈接即可,傳輸對象FriendLinkDto.cs

//FriendLinkDto.cs
namespace Meowv.Blog.Application.Contracts.Blog
{
    public class FriendLinkDto
    {
        /// <summary>
        /// 標題
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// 鏈接
        /// </summary>
        public string LinkUrl { get; set; }
    }
}

添加查詢友鏈列表接口和緩存接口。

//IBlogService.FriendLink.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog
{
    public partial interface IBlogService
    {
        /// <summary>
        /// 查詢友鏈列表
        /// </summary>
        /// <returns></returns>
        Task<ServiceResult<IEnumerable<FriendLinkDto>>> QueryFriendLinksAsync();
    }
}
//IBlogCacheService.FriendLink.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Caching.Blog
{
    public partial interface IBlogCacheService
    {
        /// <summary>
        /// 查詢友鏈列表
        /// </summary>
        /// <param name="factory"></param>
        /// <returns></returns>
        Task<ServiceResult<IEnumerable<FriendLinkDto>>> QueryFriendLinksAsync(Func<Task<ServiceResult<IEnumerable<FriendLinkDto>>>> factory);
    }
}

接下來,實現他們。

//BlogCacheService.FriendLink.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using static Meowv.Blog.Domain.Shared.MeowvBlogConsts;

namespace Meowv.Blog.Application.Caching.Blog.Impl
{
    public partial class BlogCacheService
    {
        private const string KEY_QueryFriendLinks = "Blog:FriendLink:QueryFriendLinks";

        /// <summary>
        /// 查詢友鏈列表
        /// </summary>
        /// <param name="factory"></param>
        /// <returns></returns>
        public async Task<ServiceResult<IEnumerable<FriendLinkDto>>> QueryFriendLinksAsync(Func<Task<ServiceResult<IEnumerable<FriendLinkDto>>>> factory)
        {
            return await Cache.GetOrAddAsync(KEY_QueryFriendLinks, factory, CacheStrategy.ONE_DAY);
        }
    }
}
//BlogService.FriendLink.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.Domain.Blog;
using Meowv.Blog.ToolKits.Base;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog.Impl
{
    public partial class BlogService
    {
        /// <summary>
        /// 查詢友鏈列表
        /// </summary>
        /// <returns></returns>
        public async Task<ServiceResult<IEnumerable<FriendLinkDto>>> QueryFriendLinksAsync()
        {
            return await _blogCacheService.QueryFriendLinksAsync(async () =>
            {
                var result = new ServiceResult<IEnumerable<FriendLinkDto>>();

                var friendLinks = await _friendLinksRepository.GetListAsync();
                var list = ObjectMapper.Map<IEnumerable<FriendLink>, IEnumerable<FriendLinkDto>>(friendLinks);

                result.IsSuccess(list);
                return result;
            });
        }
    }
}

直接查詢所有的友鏈數據,這裏使用前面講到的AutoMapper處理對象映射,將IEnumerable<FriendLink>轉換為IEnumerable<FriendLinkDto>

MeowvBlogAutoMapperProfile.cs中添加一條配置:CreateMap<FriendLink, FriendLinkDto>();,在BlogController中添加API。

/// <summary>
/// 查詢友鏈列表
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("friendlinks")]
public async Task<ServiceResult<IEnumerable<FriendLinkDto>>> QueryFriendLinksAsync()
{
    return await _blogService.QueryFriendLinksAsync();
}

編譯運行,打開查詢友鏈的API,此時沒數據,手動添加幾條數據進去再試試吧。

文章管理

後台文章管理包含:文章列表、新增、更新、刪除文章,接下來依次完成這些接口。

文章列表

這裏的文章列表和前台的文章列表差不多,就是多了一個Id,以供編輯和刪除使用,所以可以新建一個模型類QueryPostForAdminDto繼承QueryPostDto,添加PostBriefForAdminDto繼承PostBriefDto同時新增一個字段主鍵Id。

QueryPostForAdminDto中隱藏基類成員Posts,使用新的接收類型:IEnumerable<PostBriefForAdminDto>

//PostBriefForAdminDto.cs
namespace Meowv.Blog.Application.Contracts.Blog
{
    public class PostBriefForAdminDto : PostBriefDto
    {
        /// <summary>
        /// 主鍵
        /// </summary>
        public int Id { get; set; }
    }
}
//QueryPostForAdminDto.cs
using System.Collections.Generic;

namespace Meowv.Blog.Application.Contracts.Blog
{
    public class QueryPostForAdminDto : QueryPostDto
    {
        /// <summary>
        /// Posts
        /// </summary>
        public new IEnumerable<PostBriefForAdminDto> Posts { get; set; }
    }
}

添加分頁查詢文章列表的接口:QueryPostsForAdminAsync(),關於後台的一些接口就不添加緩存了。

//IBlogService.Admin.cs
using Meowv.Blog.Application.Contracts;
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog
{
    public partial interface IBlogService
    {
        /// <summary>
        /// 分頁查詢文章列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        Task<ServiceResult<PagedList<QueryPostForAdminDto>>> QueryPostsForAdminAsync(PagingInput input);
    }
}

然後實現這個接口。

//BlogService.Admin.cs
using Meowv.Blog.Application.Contracts;
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using Meowv.Blog.ToolKits.Extensions;
using System.Linq;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog.Impl
{
    public partial class BlogService
    {
        /// <summary>
        /// 分頁查詢文章列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<ServiceResult<PagedList<QueryPostForAdminDto>>> QueryPostsForAdminAsync(PagingInput input)
        {
            var result = new ServiceResult<PagedList<QueryPostForAdminDto>>();

            var count = await _postRepository.GetCountAsync();

            var list = _postRepository.OrderByDescending(x => x.CreationTime)
                                      .PageByIndex(input.Page, input.Limit)
                                      .Select(x => new PostBriefForAdminDto
                                      {
                                          Id = x.Id,
                                          Title = x.Title,
                                          Url = x.Url,
                                          Year = x.CreationTime.Year,
                                          CreationTime = x.CreationTime.TryToDateTime()
                                      })
                                      .GroupBy(x => x.Year)
                                      .Select(x => new QueryPostForAdminDto
                                      {
                                          Year = x.Key,
                                          Posts = x.ToList()
                                      }).ToList();

            result.IsSuccess(new PagedList<QueryPostForAdminDto>(count.TryToInt(), list));
            return result;
        }
    }
}

實現邏輯也非常簡單和之前一樣,就是在Select的時候多了一個Id,添加一個新的Controller:BlogController.Admin.cs,添加這個接口。

//BlogController.Admin.cs
using Meowv.Blog.Application.Contracts;
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using static Meowv.Blog.Domain.Shared.MeowvBlogConsts;

namespace Meowv.Blog.HttpApi.Controllers
{
    public partial class BlogController
    {
        /// <summary>
        /// 分頁查詢文章列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        [HttpGet]
        [Authorize]
        [Route("admin/posts")]
        [ApiExplorerSettings(GroupName = Grouping.GroupName_v2)]
        public async Task<ServiceResult<PagedList<QueryPostForAdminDto>>> QueryPostsForAdminAsync([FromQuery] PagingInput input)
        {
            return await _blogService.QueryPostsForAdminAsync(input);
        }
    }
}

因為是後台的接口,所以加上AuthorizeAttribute,指定接口組為GroupName_v2,參數方式為[FromQuery]

當沒有進行授權的時候,是無法訪問接口的。

新增文章

在做新增文章的時候要注意幾點,不是單純的添加文章數據就結束了,要指定文章分類,添加文章的標籤。添加標籤我這裡是從標籤庫中去取得數據,只存標籤Id,所以添加標籤的時候就可能存在添加了標籤庫中已有的標籤。

新建一個新增和更新文章的通用輸出參數模型類,起名:EditPostInput,繼承PostDto,然後添加標籤Tags字段,返回類型IEnumerable<string>

//EditPostInput.cs
using System.Collections.Generic;

namespace Meowv.Blog.Application.Contracts.Blog.Params
{
    public class EditPostInput : PostDto
    {
        /// <summary>
        /// 標籤列表
        /// </summary>
        public IEnumerable<string> Tags { get; set; }
    }
}

添加新增文章的接口:InsertPostAsync

/// <summary>
/// 新增文章
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<ServiceResult> InsertPostAsync(EditPostInput input);

然後去實現這個接口,實現之前,配置AutoMapper實體映射。

CreateMap<EditPostInput, Post>().ForMember(x => x.Id, opt => opt.Ignore());

EditPostInput轉換為Post,並且忽略Id字段。

/// <summary>
/// 新增文章
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<ServiceResult> InsertPostAsync(EditPostInput input)
{
    var result = new ServiceResult();

    var post = ObjectMapper.Map<EditPostInput, Post>(input);
    post.Url = $"{post.CreationTime.ToString(" yyyy MM dd ").Replace(" ", "/")}{post.Url}/";
    await _postRepository.InsertAsync(post);

    var tags = await _tagRepository.GetListAsync();

    var newTags = input.Tags
                       .Where(item => !tags.Any(x => x.TagName.Equals(item)))
                       .Select(item => new Tag
                       {
                           TagName = item,
                           DisplayName = item
                       });
    await _tagRepository.BulkInsertAsync(newTags);

    var postTags = input.Tags.Select(item => new PostTag
    {
        PostId = post.Id,
        TagId = _tagRepository.FirstOrDefault(x => x.TagName == item).Id
    });
    await _postTagRepository.BulkInsertAsync(postTags);

    result.IsSuccess(ResponseText.INSERT_SUCCESS);
    return result;
}

URL字段,根據創建時間按照yyyy/MM/dd/name/格式拼接。

然後找出是否有新標籤,有的話批量添加至標籤表。

再根據input.Tags構建PostTag列表,也進行批量保存,這樣才算是新增好一篇文章,最後輸出ResponseText.INSERT_SUCCESS常量,提示成功。

BlogController.Admin.cs添加API。

/// <summary>
/// 新增文章
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost]
[Authorize]
[Route("post")]
[ApiExplorerSettings(GroupName = Grouping.GroupName_v2)]
public async Task<ServiceResult> InsertPostAsync([FromBody] EditPostInput input)
{
    return await _blogService.InsertPostAsync(input);
}

更新文章

更新操作和新增操作輸入參數一樣,只新增一個Id用來標識更新那篇文章,添加UpdatePostAsync更新文章接口。

/// <summary>
/// 更新文章
/// </summary>
/// <param name="id"></param>
/// <param name="input"></param>
/// <returns></returns>
Task<ServiceResult> UpdatePostAsync(int id, EditPostInput input);

同樣的實現這個接口。

/// <summary>
/// 更新文章
/// </summary>
/// <param name="id"></param>
/// <param name="input"></param>
/// <returns></returns>
public async Task<ServiceResult> UpdatePostAsync(int id, EditPostInput input)
{
    var result = new ServiceResult();

    var post = await _postRepository.GetAsync(id);
    post.Title = input.Title;
    post.Author = input.Author;
    post.Url = $"{input.CreationTime.ToString(" yyyy MM dd ").Replace(" ", "/")}{input.Url}/";
    post.Html = input.Html;
    post.Markdown = input.Markdown;
    post.CreationTime = input.CreationTime;
    post.CategoryId = input.CategoryId;

    await _postRepository.UpdateAsync(post);

    var tags = await _tagRepository.GetListAsync();

    var oldPostTags = from post_tags in await _postTagRepository.GetListAsync()
                      join tag in await _tagRepository.GetListAsync()
                      on post_tags.TagId equals tag.Id
                      where post_tags.PostId.Equals(post.Id)
                      select new
                      {
                          post_tags.Id,
                          tag.TagName
                      };

    var removedIds = oldPostTags.Where(item => !input.Tags.Any(x => x == item.TagName) &&
                                               tags.Any(t => t.TagName == item.TagName))
                                .Select(item => item.Id);
    await _postTagRepository.DeleteAsync(x => removedIds.Contains(x.Id));

    var newTags = input.Tags
                       .Where(item => !tags.Any(x => x.TagName == item))
                       .Select(item => new Tag
                       {
                           TagName = item,
                           DisplayName = item
                       });
    await _tagRepository.BulkInsertAsync(newTags);

    var postTags = input.Tags
                        .Where(item => !oldPostTags.Any(x => x.TagName == item))
                        .Select(item => new PostTag
                        {
                            PostId = id,
                            TagId = _tagRepository.FirstOrDefault(x => x.TagName == item).Id
                        });
    await _postTagRepository.BulkInsertAsync(postTags);

    result.IsSuccess(ResponseText.UPDATE_SUCCESS);
    return result;
}

ResponseText.UPDATE_SUCCESS是常量更新成功。

先根據Id查詢到數據庫中的這篇文章數據,然後根據input參數,修改需要修改的數據,最後保存。

注意的是,如果修改的時候修改了標籤,有可能新增也有可能刪除,也許會又有新增又有刪除。

這時候就需要注意,這裏做了一個比較通用的方法,找到數據庫中當前文章Id的所有Tags,然後根據參數input.Tags可以找出被刪掉的標籤的PostTags的Id,調用刪除方法刪掉即可,同時也可以獲取到新增的標籤,批量進行保存。

完成上面操作后,才保存新加標籤與文章對應的數據,最後提示更新成功,在BlogController.Admin添加API。

/// <summary>
/// 更新文章
/// </summary>
/// <param name="id"></param>
/// <param name="input"></param>
/// <returns></returns>
[HttpPut]
[Authorize]
[Route("post")]
[ApiExplorerSettings(GroupName = Grouping.GroupName_v2)]
public async Task<ServiceResult> UpdatePostAsync([Required] int id, [FromBody] EditPostInput input)
{
    return await _blogService.UpdatePostAsync(id, input);
}

[HttpPut]指定請求方式為put請求,一般需要修改用put,添加用post。

[Required]指定參數id必填且是FromQuery的方式,input為[FromBody]

更新一下上面新增的數據試試。

刪除文章

刪除相對來說就非常簡單了,一般刪除都會做邏輯刪除,就是避免某些手殘刪除了,有找回的餘地,我們這裏就直接Delete了,也沒什麼重要數據。

添加接口:DeletePostAsync

/// <summary>
/// 刪除文章
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<ServiceResult> DeletePostAsync(int id);

實現接口。

/// <summary>
/// 刪除文章
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<ServiceResult> DeletePostAsync(int id)
{
    var result = new ServiceResult();

    var post = await _postRepository.GetAsync(id);
    if (null == post)
    {
        result.IsFailed(ResponseText.WHAT_NOT_EXIST.FormatWith("Id", id));
        return result;
    }

    await _postRepository.DeleteAsync(id);
    await _postTagRepository.DeleteAsync(x => x.PostId == id);

    result.IsSuccess(ResponseText.DELETE_SUCCESS);
    return result;
}

刪除的時候同樣去查詢一下數據,來判斷是否存在。

ResponseText.DELETE_SUCCESS是添加的常量刪除成功,刪除成功同時也要將post_tags表的標籤對應關係也幹掉才算完整,在BlogController.Admin添加API。

/// <summary>
/// 刪除文章
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpDelete]
[Authorize]
[Route("post")]
[ApiExplorerSettings(GroupName = Grouping.GroupName_v2)]
public async Task<ServiceResult> DeletePostAsync([Required] int id)
{
    return await _blogService.DeletePostAsync(id);
}

[HttpDelete]指定請求方式是刪除資源,[Required]指定參數Id必填。

刪掉上面添加的文章看看效果。

至此,完成了博客文章的增刪改接口,未完待續…

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

搭配下方課程學習更佳 ↓ ↓ ↓

http://gk.link/a/10iQ7

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

小米11 虐機耐用性測試,究竟能通過一連串嚴苛考驗嗎?

在 YouTube 的知名虐機頻道 JerryRigEverything 上個月才回顧完 2020 年智慧型手機耐用獎,而 2021 年目前為止已發表的旗艦新機中,不久前才在中國以外市場進行全球發表的小米11 ,這次也不意外在 JerryRigEverything 進行一連串的虐機耐用性測試,未來小米11 也有機會在不久後引進台灣市場販售,對於未來有意入手的消費者不妨能將這次測試作為購買前的參考依據。

小米11 虐機耐用性測試,究竟能通過一連串嚴苛考驗嗎?

小米於 2020 年底在中國發表首款搭載高通 Snapdragon 888 旗艦手機小米11 ,也很快在本月 8 日「小米11 全球線上發佈會」正式在國際市場推出,後續也將在台灣登場。
在國際市場發表後, YouTube 以嚴苛耐用性測試著名的 JerryRigEverything 日前也釋出了小米11 虐機耐用性測試影片,其中小米11 螢幕升級為 6.81 吋 WQHD+ 解析度、120Hz TrueColor AMOLED 四曲面螢幕,加上螢幕玻璃則採用康寧最新大猩猩玻璃 Gorilla Glass Victus 保護,官方抗摔性相較前代提升 1.5倍、耐刮性能提升 2 倍,至於在實測下來如何呢?就讓 JerryRigEverything 帶著大家看下去吧!

小米11 不僅盒裝標配軟質的手機保護套,出廠時也預先貼上螢幕保護貼,讓小米11 用戶能省下一筆保護貼的開銷,也同時為手機能有螢幕有基礎保護。不過既然要測試耐用性,還是得將原廠的保護貼給卸下:

按照慣例在手機螢幕上貼上寫著數字的標籤,接著莫氏硬度標準進行測試:

小米11 的螢幕玻璃採用康寧大猩猩玻璃 Victus ,不過實測下來與之前像是採用超瓷晶盾面板的 iPhone 12 系列,或其他數十款智慧型手機沒有太大差別,耐刮程度仍為第 6 級開始出現刮痕,接著在第 7 級開始會出現更深的刮痕:

小米11 在邊框則採用電鍍工藝的 7000系鋁合金邊框,不過在銳利的刀片的刮過還是留下明顯痕跡:

至於機身背面,小米11 之前在中國版推出時分為 AG工藝的玻璃材質或皮質質感的聚碳酸酯塑料材質,不過先前在小米11 國際版僅有玻璃材質可選擇。不過機身背面的玻璃在美工刀這類物品劃過並未留下刮痕:

主相機鏡頭部份也順利通過考驗:

後來也針對小米11 進行了耐火測試,但小米11 並未像是之前採用超瓷晶盾面板的 iPhone 12 系列那般令人意外強大,最終在打火機燃燒後還是留下因熱量讓顯示面板的像素損壞的痕跡:

最後,也針對小米11 進行彎曲測試,所幸還是輕易地通過考驗沒有被扳彎。

完整測試影片

 

圖片/消息來源: JerryRigEverything(YouTube)

延伸閱讀:
小米11 國際版正式發表:盒裝標配 55W GaN 氮化鎵充電器、售價約 25,150 元

小米11 骨牌挑戰影片!以 4100支成功完成挑戰

您也許會喜歡:

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

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

EPIC 推出 MetaHuman Creator,在網頁上就能製作擬真人像

EPIC Games 宣布推出一款全新的瀏覽器工具,該工具由 Unreal Engine 所構建,名為「MetaHuman Creator」,可讓用戶製作高度逼真的人臉,並幫助打造更逼真的身體動作與臉部動畫。該公司也推出一系列影片來展示使用這項工具的成品範例(稱為 MetaHuman)。

EPIC 推出 MetaHuman Creator,在網頁上就能製作擬真人像

EPIC 表示,MetaHuman Creator 可以與現代動作捕捉和動畫技術結合使用,為遊戲、電影、電視和其他內容建立逼真的動作和人類互動場景。該公司說,這些逼真的擬真人像可以在數分鐘內生成,並且擁有獨特的服裝與髮型,並且可進一步地定製,以滿足創作者的各種需求。 這些 MetaHuman 可以在工具內直接即時操作,不需下載安裝,透過網路瀏覽器即可完成。

截至目前為止,3D 內容創作中最困難的部分之一就是構建出具說服力的數位人類,即使是經驗豐富的老手也需要耗費大樣時間、精力與精良的設備來雕琢一個角色。使用 EPIC 新工具所創造的擬真人像並不是以接近照片為目的的現實主義,但作為一款遊戲角色創作工具,MetaHuman Creator 能夠創造令人驚豔的影像,可以更容易地捕捉現實生活中的動作。

目前還不知道 EPIC 打算在 2021 年的何時推出瀏覽器外的版本,但感興趣的開發者或創作者可以在官方網站上註冊獲取更新。其實擬真人像、機器人最可怕的恐怖谷理論目前還沒有能逃離的例子,只能等工具真正掌握在第三方創作者手中後從成品裡見真章。

◎資料來源:The Verge

您也許會喜歡:

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

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價

Intel 官方 Twitter 釋出調侃 M1 廣告,這些是你在 Mac 上無法做的事

自從 Apple M1 推出以來好評就不斷,不少國外媒體與 YouTuber 測試數據都指出各方面勝出 Intel 許多。而沉寂許久的 Intel 近期終於開始反擊,繼幾天前釋出官方測試數據外,稍早更於官方 Twitter 分享調侃廣告,告訴你哪些事情只有 PC 能做到,Mac 則無法。

Intel 官方 Twitter 釋出調侃 M1 廣告

一直以來 Intel 與 Apple 都有密切合作,也因此關係不錯,即便現在還是有一些 Mac 機型依舊搭載 Intel 處理器。但不知道是不是 Mac 逐漸要改用 Apple 自家晶片關係,Intel 不再顧慮到這點,近期可說來勢洶洶。

最近在 Intel 官方 Twitter 上,陸續出現調侃廣告,像下面這則關於遊戲支援性,「如果你可以啟動火箭聯盟,那你就不該使用 Mac」,火箭聯盟遊戲去年三月就不支援 Mac:

Only a PC can power scientists and gamers alike. #GoPC

— Intel (@intel) February 10, 2021

雖然 Mac 改用 M1 晶片之後,能玩的遊戲確實比過去舊機型多,但相較於 Windows 差距還是非常大,更別提一些 3A 遊戲大作。

另外還有一則「只有 PC 能一台實現平版模式、觸控螢幕與支援觸控筆功能,Mac 不行」:

Only a PC offers tablet mode, touch screen and stylus capabilities in a single device. #GoPC

— Intel (@intel) February 2, 2021

這兩則廣告都連結到一部由國外知名 YouTuber 錄製的影片,裡面就介紹著 Intel 最新 Evo 平台筆電的特色,以及跟 M1 MacBook 比較的優勢,像是有標準的 USB 傳輸埠、觸控螢幕、支援 eGPU 等等,有興趣的人也能看看:

 

看到 Intel 硬起來還真有點不習慣,不過廣告說的這些事情,也確實是事實,Mac 現在完全無法做到。

下方是前幾天 Intel 釋出的官方測試數據圖(M1 MacBook Pro 16GB vs Intel Core i7-1185G7 16GB),詳細報導可閱讀這篇。

在內容創作效能部分,Intel 都比 M1 還要好,特別是 Topaz Labs:

生產力效能方面,Intel 也勝出不少:

遊戲效能測試也沒缺席,Hitman 雖然 M1 表現最好,但其餘都輸 Intel,還有不少款遊戲因為不支援,所以無法測試:

圖片來源:Toms Hardware

國外零售商洩漏 Intel 第 11 代 Rocket Lake-S 處理器的售價,i9-11900K 價格比上一代便宜一些

您也許會喜歡:

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

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

iPhone 12 Pro Max 對決三星 Galaxy S21 Ultra 應用程式啟動速度實戰

先前我們才分享過 YouTube 頻道 PhoneBuff 進行 iPhone 12 Pro Max 對決三星 Galaxy S21 Ultra 跌落測試,當時 iPhone 12 Pro Max 在耐摔方面取得勝利。不過回歸日常使用手機使用面,選擇這類旗艦級手機的用戶往往對於手機效能更為要求,因此開啟應用程式的開啟速度也是最為有感的一種方式。
這次 PhoneBuff 就找來 iPhone 12 Pro Max 進營最直接的應用程式啟動速度實戰,究竟三星在這一回 Galaxy S21 Ultra 能扳回勝利嗎?

iPhone 12 Pro Max 對決三星 Galaxy S21 Ultra 應用程式啟動速度實戰

在開始測試前比較一下兩款手機的規格配置,首先三星 Galaxy S21 Ultra 搭載 Qualcomm Snapdragon 888 行動平台、配備 12GB RAM 和 128GB ROM ,而 iPhone 12 Pro Max 則搭載 Apple A14 仿生晶片、 6GB RAM 和 128GB ROM :

這次 PhoneBuff 的測試將同時分別在 Galxay S21 Ultra 與 iPhone 12 Pro Max 依序開啟相同的應用程式,藉此比較出兩者在開啟各款 App 所需的時間。在第一輪開啟的前五款 App 依序為 Facebook 、 Starbucks 、 Word 、 Excel 以及相機 App ,在這階段 Galaxy S21 Ultra 以些微差距領先:

不過隨著後續執行更多不同類型的 App 後,換成 iPhone 12 Pro Max 取得領先:

在第一輪開啟共 16 款不同類型的 App ,計算出 iPhone 12 Pro Max 所需要的時間為 1 分 46 秒 74 ,而 Galaxy S21 Ultra 則需要 1 分  54 秒 64 :

接著第二輪重新開啟第一輪的所有 App ,最終在第二輪 iPhone 12 Pro Max 用了 43 秒 23 ,而 Galaxy S21 Ultra 則需要 45 秒 98 :

最終分析 iPhone 12 Pro Max 和 Galaxy S21 Ultra 開啟不同款 App 所需要的時間整理如下:

完整測試影片

 

圖片/消息來源: PhoneBuff(YouTube)

延伸閱讀:
華碩傳聞將在今年推出小尺寸旗艦 ZenFone mini , ROG Phone 5 於 4 月推出、價格將高於 ROG Phone 3

小米首款「四曲瀑布螢幕概念手機」正式亮相!四邊 88° 超曲面螢幕、螢幕下前鏡頭、整機一體無孔化設計

您也許會喜歡:

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

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

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