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

最近,小黑哥在一個業務改造中,使用三目運算符重構了業務代碼,沒想到測試的時候竟然發生 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地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

※回頭車貨運收費標準

C#中的Singleton模式

目錄

  • 前言
  • 實現思路
  • 實現方法
    • 最簡單的實現方法
    • 如果多線程亂入?
    • 線程安全的單例模式
      • Lock版本
      • 靜態構造器版本
      • Lazy版本
  • 總結

前言

Singleton是二十三個設計模式中比較重要也比較經常使用的模式。但是這個模式雖然簡單,實現起來也會有一些小坑,讓我們一起來看看吧!

實現思路

首先我們看看這個設計模式的UML類圖。

很清晰的可以看到,有三點是需要我們在實現這個模式的時候注意的地方。

  • 私有化的構造器
  • 全局唯一的靜態實例
  • 能夠返回全局唯一靜態實例的靜態方法

其中,私有化構造器是防止外部用戶創建新的實例而靜態方法用於返回全局唯一的靜態實例供用戶使用。原理清楚了,接下來我們看看一些典型的實現方式和其中的暗坑。

實現方法

最簡單的實現方法

最簡單的實現方法自然就是按照UML類圖直接寫一個類,我們看看代碼。

    class Program
    {
        static void Main(string[] args)
        {
        	var single1 = Singleton.Instance;
            var single2 = Singleton.Instance;
            Console.WriteLine(object.ReferenceEquals(single1, single2));
            Console.ReadLine();
        }
    }

    class Singleton
    {
        private static Singleton _Instance = null;
        private Singleton()
        {
            Console.WriteLine("Created");
        }

        public static Singleton Instance
        {
            get
            {
                if (_Instance == null)
                {
                    _Instance = new Singleton();
                }
                return _Instance;
            }
        }

        public void DumbMethod()
        {

        }
    }

這段代碼忠實的實現了UML類圖裡面的一切,查看輸出結果,

證實了Singleton確實起了作用,多次調用僅僅產生了一個實例,似乎這麼寫就可以實現這個模式了。但是,真的會那麼簡單嗎?

如果多線程亂入?

現在我們給剛剛的例子加點調料,假設多個對實例的調用,並不是簡單的,彬彬有禮的順序關係,二是以多線程的方式調用,那麼剛剛那種實現方法,還能從容應對嗎?讓我們試試。把Main函數裏面的調用改成這樣。

	static void Main(string[] args)
        {
            int TOTAL = 10000;
            Task[] tasks = new Task[TOTAL];
            for (int i = 0; i < TOTAL; i++)
            {
                tasks[i] = Task.Factory.StartNew(() =>
                {
                    Singleton.Instance.DumbMethod();
                });
            }
			Task.WaitAll(tasks);
            Console.ReadLine();
        }

通過Factory創造出1萬個Task,幾乎同時去請求這個單例,看看輸出。

咦,我們剛剛寫的Singleton模式失效了,這個類被創造了5次(這段代碼運行多次,這個数字不一定相同),一定是多線程搞的鬼,我們剛剛寫的代碼沒有辦法應對多線程,換句話說,是非線程安全的(thread-safe),那有沒有辦法來攻克這個難關呢?

線程安全的單例模式

Lock版本

提到線程安全,很多同學第一反應就是用lock,不錯,lock是個可行的辦法,讓我們試試。添加一個引用類型的對象作為lock對象,修改代碼如下(什麼?你問我為什必須是引用類型的對象而不能是值類型的對象?因為lock的時候,如果對象是值類型,那麼lock僅僅鎖住了它的一個副本,另外一個線程可以暢通無阻的再次lock,這樣lock就失去了阻塞線程的意義)

	private static object _SyncObj = new object();
        public static Singleton Instance
        {
            get
            {
                lock (_SyncObj)
                {
                    if (_Instance == null)
                    {
                        _Instance = new Singleton();
                    }
                    return _Instance;
                }                
            }
        }

運行一下,輸出

只有一個實例創建,證明Lock起作用了,這個模式可行!不過有些不喜歡用Lock的同學可能要問,還有沒有其他辦法呢?答案是有的。

靜態構造器版本

回想一下,C#中的類靜態構造器,只會在這個類第一次被使用的時候調用一次,天然的線程安全,那我們試試不用Lock使用類靜態構造器?修改Singleton類如下:

    class Singleton
    {
        private static Singleton _Instance = null;
        private Singleton()
        {
            Console.WriteLine("Created");
        }

        static Singleton()
        {
            _Instance = new Singleton();
        }

        //private static object _SyncObj = new object();
        public static Singleton Instance
        {
            get { return _Instance; }
        }

        public void DumbMethod()
        {

        }
    }

去掉了Lock,添加了一個類靜態構造器,試一試。

完美!對於不喜歡用Lock(在這個例子中,實例只會創建一次但是之後的所有線程都要先排隊Lock再進入Critical code進行檢查,效率比較低下)的同學,類靜態構造器提供了一種很好的選擇。
不過俗話說,人心苦不足 , 我們總是追求卓越。這個版本比Lock版本似乎更好一點,那還有沒有更好的版本呢?有的。

Lazy版本

從net 4.0開始,C#開始支持延遲初始化,通過Lazy關鍵字,我們可以聲明某個對象為僅僅當第一次使用的時候,再初始化,如果一直沒有調用,那就不初始化,省去了一部分不必要的開銷,提升了效率。如果你不熟悉Lazy或者想更多了解它,請參考。我們今天關注的重點在於,Lazy也是天生線程安全的,所以我們嘗試用它來實現Singleton模式?修改代碼如下:

    class Singleton
    {
        private static Lazy<Singleton> _Instance = new Lazy<Singleton>(() => new Singleton());
        private Singleton()
        {
            Console.WriteLine("Created");
        }

        public static Singleton Instance
        {
            get
            {
                return _Instance.Value;
            }
        }

        public void DumbMethod()
        {

        }
    }

輸出結果中可以看到,我們達到了想要的效果:

在上面的代碼中,私有變量_Instance現在是被聲明為延遲初始化,這樣不但天然實現了線程安全,同時在沒有調用Instance靜態方法的時候(也即沒有調用_Instance.Value),初始化不會發生,這樣就提高了效率。

總結

Singleton模式很常見,實現起來也很簡單,只是要小心線程安全。以上三種方法都可以實現線程安全的Singleton模式。如果net 版本在4.0之上,建議使用Lazy版本,畢竟對比Lock版本,Lazy版本可以免去實現手動Lock之苦,對比Static版本,又有延遲初始化的性能優勢,何樂而不為呢?

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

基於 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 Ultra 實機動手玩影片流出!配備 120x 變焦相機和副螢幕

在本月 25 日 Redmi K40 系列才要發表,也外傳小米接下來在不久後也會發表多款小米11 系列新機,其中也包括最大尺寸、規格最旗艦的小米11 Ultra 。稍早,在 YouTube 頻道 Tech Buff PH 聲稱取得兩款尚未發表的「小米11 Ultra」手機,也讓小米11 Ultra 就這樣突如其來其然的被提前曝光!

小米11 Ultra 實機動手玩影片流出!配備 120x 變焦相機和副螢幕

日前小米才剛在「小米11 全球線上發佈會」正式在國際市場率先在歐洲推出小米11 標準版後,也傳聞小米11 系列有望推出多款新機,包括小米11 Lite 、小米11 Pro 和小米11 Ultra 。稍早有名菲律賓 YouTuber Tech Buff PH 豪不客氣地直接搶先洩密聲稱取得分別為白色和黑色的兩款小米11 Ultra 實機,儘管無法完全確認這次洩漏的真實性,不過各方面透露的資訊可信度確實相當高。

在機身背面顯示的型號為 M2102K1G ,與最近通過印度 BIS 和歐盟 EEC 認證的小米裝置相吻合。

根據 Tech Buff PH 的描述,小米11 Ultra 硬體規格方面除了支持高螢幕更新率螢幕、搭載 Qualacomm Snapdragon 888 旗艦行動平台,內建 5000mAh 大容量電池快速充電支持小米三重快充技術,分別為 67W 有線快充、67W 無線快充以及 10W 無線反向充電。

螢幕方面,小米11 Ultra 同樣配備 6.81 吋 WQHD+ 解析度、120Hz 更新率的 TrueColor AMOLED 四曲面螢幕,螢幕採用孔洞式設計並配備 2000 萬像素前置自拍相機,螢幕表面也比照小米11 覆蓋康寧最新 Gorilla Glass Victus 大猩猩玻璃保護。

另外,從影片多角度也顯示這款手機預載 MIUI 12.5 Global 版本,或許這次也不像是去年小米10 Ultra(小米10 至尊紀念版)僅限於中國市場販售,終於要將小米旗艦系列最高規的機種推向國際市場。

至於相機也是小米11 Ultra 最具辨識度的設計重點,畢竟在機身背面橫向排列著如此龐大具有凸起的相機模組,內部容納了三鏡頭主相機和一個小型的副螢幕:

根據他的說明,小米11 Ultra 主相機配備 5000 萬像素主鏡頭、 4800 萬像素超廣角鏡頭和 4800 萬像素潛望式望遠變焦鏡頭所組成,在相機上文字顯示「120X Ultra Pixel AI Camera」可得知小米11 Ultra 相機擁有著 120X 變焦、 12-120mm 等效焦距,不過目前尚無法得知光學變焦的倍率為何。

小米11 Ultra 機身背面主相機位置除了擁有碩大的鏡頭,另一側則搭載微型的副螢幕設計,即便目前無法得知該顯示器的規格、尺寸尺寸看似也不大,但實用性或許比大家想像中來得高呀!因為這塊小型的副螢幕並非只能顯示新訊息、未接來電這種通知類的內容,
,從 Tech Buff PH 的動手玩影片我們能看到他可以顯示小米11 Ultra 主螢幕的完整畫面,甚至是開啟瀏覽器或各種應用程式都沒問題。

當然,這塊副螢幕最主要的目的並非是用來觀看手機螢幕的整個畫面,主要的用途還是用於輔助用戶進行自拍照片或影片。筆者認為,小米11 Ultra 之所以能顯示整個主螢幕的原因在於,許多用戶在自拍時並不會使用系統內建的相機 App ,而是使用這種自己慣用的美顏 App ,那麼這樣的設計就顯得相當貼心了。

小米11 Ultra 採用四曲面機身設計,機身中框比照去年小米10 Ultra 的模式改為不鏽鋼材質,也毫無意外配備 Harman Kardon 專業調音的雙立體揚聲器系統,小米11 Ultra 提供影院級音效體驗。其他方面,小米11 Ultra 當然也支持 IP68 防水等級。

最後,即便現階段無法證實 Tech Buff PH 影片洩漏的小米11 Ultra 外觀以及透露的規格配置是否屬為真,以及小米11 Ultra 何時才會發表,不過根據先前微博的爆料指出小米11 Ultra 預計會在 Redmi K40 之後,最快在 3 月份就會正式發表。

完整影片(備份)

目前 Tech Buff PH 已將原影片瀏覽權限改為私人,不過目前已經有人將影片備份上傳(如影片下架請見諒)。

 

圖片/消息來源: TECH BUFF PH(YouTube)

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

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

您也許會喜歡:

【推爆】終身$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維修中心

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

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

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

Jon Prosser 爆料全新 iPad Pro 與 AirTags 將於 3 月發表

2021 年 Apple 也預計會發表許多新產品,其中第一波的新品可能將在不久後登場。根據 Jon Prosser 上個月的爆料指出 Apple 計劃在 3 月推出搭載 Mini LED 螢幕的全新年式 iPad Pro (2021) ,當時就有傳聞 AirTags 有機會同時登場。稍早, Jon Prosser 在 Twitter 再次發文提及 AirTags 將會在 3 月推出,預計 Apple 會採線上發表的型式宣佈此消息。另外,針對 iPad Pro 的發表時間的疑問也再次回應會在下個月一併亮相。

▲圖片來源:MacRumors

Jon Prosser 爆料全新 iPad Pro 與 AirTags 將於 3月發表

過去多次精準預測、但在去年也有幾次失準紀錄的 Jon Prosser 在今日稍早於 Twitter 發文表示 AirTags 仍在進行中、預計會在 3 月發表,而且這次並沒有聽到會有延誤。但是,這並非他第一次聲稱取得 AirTags 的相關消息,因為在早在去年他就曾表示 AirTags 肯定會在 11 月中的活動中亮相,最終該活動則是推出多款搭載 Apple M1 晶片的 Mac 電腦,並沒有 AirTags 。後續也提及 AirTags 將與 iOS 14.3/iPadOS 14.3 一同發表,但事實也非如此。
這次 Jon Prosser 宣稱 2021 年 3 月是 AirTags 的發表月份,值得注意過去 Apple 通常不會在每年 3 月之前舉行活動發表新品,因此這次的說法似乎是有根據的猜測。

▲圖片來源:Jon Prosser(Twitter/@jon_prosser)

早在去年, Jon Prosser 就在 Twitter 釋出 AirTags 的外觀渲染圖,後續也曾分享 3D 動圖版本的渲染圖,不過目前還未見到這款 Apple 的藍牙接收裝置發表。Ming-Chi Kuo 在上個月也表示 AirTags 有望在 2021 年發表,不過他並未提供具體的時程。

▲圖片來源:Jon Prosser(Twitter/@jon_prosser)

另外也有網友在該則貼文底下詢問 iPad Pro 的發表時間, Jon Prosser 則再次回應時間會在 3 月:

▲圖片來源:Jon Prosser(Twitter/@jon_prosser)

早在之前, Jon Prosser 就曾透露接下來在 3 月就可能會見到搭載 Mini LED 的 iPad Pro 登場。據悉全新 mini LED 螢幕的 2021 年款 iPad Pro 將搭載 5nm 製程的 A14X 仿生晶片,同時和 iPhone 12 系列一樣採用 Qualcomm Snapdragon X55 5G 連網數據晶片、支持 mmWave 毫米波和 6GHz 以下頻段,記憶體則有 6GB RAM。

▲圖片來源:MacRumors

也有網友詢問今年 Apple 是否還會推出具備 Touch ID 的 iPhone?關於這一點 Jon Prosser 回應也確認了此消息。

▲圖片來源:Jon Prosser(Twitter/@jon_prosser)

上個月, Apple Lab 在 Twitter 爆料指出 Apple 今年預計會推出全新 2021 年版本、螢幕更大的 iPhone SE 後繼機型,預計採用「iPhone SE Plus」。傳聞這款新機將配備 6.1 吋 LCD 螢幕、 A14 仿生晶片、 12MP 單鏡頭主相機和 700 萬像素視訊鏡頭、和 iPhone 12 系列一樣俱備 Smart HDR 3 拍攝,值得注意的是傳聞這款新機將保留 Touch ID 指紋辨識,不過預計會將指紋感應器比照 iPad Air 4 整合在機身側邊的電源鍵上。價格方面,傳聞 iPhone SE Plus 售價比起 2020 年的 iPhone SE(第 2 代)售價提升 100 美元左右,換算屆時在台灣的售價預計落在 16,900 元起。

▲圖片來源:Apple Lab(Twitter/@aaple_lab)

消息來源:Jon Prosser(Twitter/@jon_prosser)|MacRumors|Apple Lab(Twitter/@aaple_lab)

延伸閱讀:
Redmi K40 / K40 Pro、小米10 新版本外觀認證照曝光

小米11 Pro 新外觀渲染圖、規格曝光?!小米11 系列還有兩款高階機型尚未推出,不過小米10 將有 S870 小改升級版本

您也許會喜歡:

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

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

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

小米11 非官方拆解影片來了!有更多官方未提到的設計小巧思

在小米11 國際版於歐洲發表後,在 YouTube 的知名虐機頻道 JerryRigEverything 日前除了針對這款小米發表的首款 Snapdragon 888 旗艦手機小米11 進行虐機測試,其實也有針對這款小米新旗艦進行手機拆解,比起先前小米官方親自釋出的拆機影片, JerryRigEverything 的拆機則帶大家看到更多小米官方未提及的設計細節。 

小米11 非官方拆解影片來了!有更多官方未提到的設計小巧思

除了虐機測試考驗著手機螢幕、機身的耐用性表現,近日 JerryRigEverything 也為大家帶來小米11 的拆解影片,在卸下小米11 的玻璃後蓋後,可以看到一片大尺寸的無線充電線圈,可幫助小米11 完成最高 50W 的無線充電。

在小米11 機身內部的周圍的螺絲可以發現以不同顏色區分,例如其中有包括大量綠色的螺絲,這對於維修人員在作業時也更為方便:

另外,小米11 主相機的鏡頭蓋採用金屬製成:

小米11 搭載 2000 萬像素前置鏡頭相機:

小米11 搭載 1.08 億像素 AI 三鏡頭主相機,分別為 1.08 億像素廣角鏡頭為主鏡頭,搭配 1300 萬像素 123° 超廣角鏡頭 500 萬像素長焦微距鏡頭:

接著看到電池部分,小米11 內部電池旁有拆卸指引的貼片,只需依照指示即可輕鬆拆卸電池進行維修。

電池容量部分,小米11 內建等效 4,600mAh (typ) 高容量電池,實際上使用兩節單獨的電池以雙電池形式組合在一起,雙電池設計比起傳統單體電池組具備更快的充電效率。小米11 在充電方面支持 55W 有線快充、 50W無線快充以及10W 無線反充,小米11 國際版也將標配 55W GaN 氮化鎵充電器。

小米11 完整拆機影片

 

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

延伸閱讀:
小米11 骨牌挑戰影片!以 4100支成功完成挑戰

小米MIX系列「可量產」新機確認將在今年即將回歸,另有小米平板也將登場

您也許會喜歡:

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

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

【其他文章推薦】

※回頭車貨運收費標準

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

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

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

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

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

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

完爆繽智逍客之流,這輛超低油耗SUV能把人帥暈

加上脫胎於豐田普銳斯平台以及前後獨立懸架使得它駕駛質感就像是一輛兩廂車。無論是1。8L混動版本還是1。2T渦輪增壓版本,駕駛起來都是非常的輕快,1。8L混動版本在溫柔的駕駛風格有着最為安靜的駕駛體驗,但若是你以一種激進的方式去駕駛這輛車,發動機介入后的轟鳴就會從防火牆一直傳遞到你的耳朵,所以我們還是建議溫柔一點開。

前言

合資小型SUV市場,基本可以認為是本田繽智、H-RV兩兄弟以及日產逍客所佔領,其他車系就僅僅有着昂科拉以及yeti這些車型,完全不足以向它們發起衝鋒。而豐田在這個競爭激烈的小型SUV卻一直沒有參与,但是“遲到總比沒到好”,豐田的最新小型SUV——CH-R似乎有着超強的競爭實力。

豐田C-HR是一輛為年輕人所設計的小型SUV,在外觀極其動感,使用了家族式Keen-look設計原因,修長的鷹眼大燈非常有進攻性,整體就像是一輛小RAV4。

鷹眼式大燈非常引人注目,使用的是氙氣大燈,下方的是閃電狀LED日間行車燈,年輕人對這種設計的抵禦能力幾乎是“零”。

車身側面可以看到兩個突出的輪轂,並且雙腰線的設計,還有一條從前輪一直延伸到尾燈處,將車輪、車門、尾燈連成一體,充分表現出力量感。並且使用了溜背+懸浮式車頂設計,動感十分。值得一提的是為了不破壞車身側麵線條,後門把手使用了隱藏式設計,位置是在車門的右上角。

尾部使用的是雙C型尾燈,和本田思域非常類似。后擋風玻璃傾斜程度很大,使得這輛小型SUV更像是一輛性感的掀背車。

尾部還有稍微地突出,有着一種小鴨尾的感覺。

尾燈燈源使用的是LED燈源,不僅美觀,還有着很高的辨識率。

內飾也是和外觀一般驚艷,是豐田最新的設計,做工也是豐田最高的標準,值得稱讚。中控有着裝飾條從一側車門延伸到另一側。懸浮式中控屏幕位置較高便於使用,但是稍顯突兀。

飛機把桿式的空調按鈕頗有Mini的感覺。

檔把也是全新的設計,後方有着电子手剎、自動駐車、EV模式以及ESp車身穩定系統開關。

CH-R的項目負責人介紹道CH-R為了打造這外觀,加上認為目標用戶基本不使用後排,所以犧牲了後排空間以及行李廂容積,而且後排不僅較小,而且視野極差。

C-HR的動感不僅僅表現在外觀,也表現在操控上,有着一個精準的轉向。雖然它看着較高,但是比起普銳斯更短更寬的車身以及優秀的設計使得它有着較低的重心。加上脫胎於豐田普銳斯平台以及前後獨立懸架使得它駕駛質感就像是一輛兩廂車。

無論是1.8L混動版本還是1.2T渦輪增壓版本,駕駛起來都是非常的輕快,1.8L混動版本在溫柔的駕駛風格有着最為安靜的駕駛體驗,但若是你以一種激進的方式去駕駛這輛車,發動機介入后的轟鳴就會從防火牆一直傳遞到你的耳朵,所以我們還是建議溫柔一點開。而且混動版本那讓人捉摸不透的剎車也是一個問題。

對於1.2T版本,手動變速箱檔位清晰,能帶來更多的駕駛樂趣,當然你也可以選擇CVT版本。這個1.2T發動機動力表現比起1.6L自然吸氣發動機要更好,已經在我國實現了量產,裝載到雷凌以及卡羅拉上,唯獨是5500轉的斷油轉速實在是有點低。

H-RV在主動安全配置方面也是有着較高的水準,ACC自適應巡航、全車影像、車道偏離預警、主動剎車均有配備。

在中國市場乃至是整個世界,豐田C-HR的主要對手是本田繽智、日產逍客、斯柯達yeti這類的小型SUV,進入中國的價位極有可能在13-20萬之間,這次C-HR的亮相已經給到對手明顯的競爭壓力。相比之下,繽智之流的SUV在顏值落下了不止了一個層級,其次是主動安全配置。

本田有着思域這款“高性價比”以及驚艷的產品,而豐田此次的C-HR也同樣驚艷,而且加上1.2T發動機、混動動力總成的國產化,價格肯定也是相當的低。不過如此優秀的產品是否會加價呢?

這個問題已經在編輯部引起了強烈的爭議,有人認為如此優秀的產品不加價是沒有可能的,有人認為豐田慣例不會加價只是讓你等,也有人認為這輛後排空間小的車會比較小眾,所以不會加價。那麼,你是怎樣看的呢?要是真的13萬起售,你會選擇加價提車嗎?本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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