標籤: 銷售文案

  • 20萬就能買到全新寶馬轎車?!因為它真的來了

    20萬就能買到全新寶馬轎車?!因為它真的來了

    新車基於UKL前驅平台打造,1系三廂將會是前驅車型,新車的動力系統或為1。5T 136馬力+6速自動變速箱,2。0T 192馬力/231馬力+8擋手自一體變速箱,其中官方聲稱1。5T車型的百公里油耗最低可以達到5。5L。新車將會有無鑰匙啟動、抬頭显示、倒車影像、全景天窗、LED大燈、pM2。

    寶馬1系三廂自從誕生的那一刻就備受關注,吸睛無數,小編也和大家一樣,時刻關注着寶馬1系三廂的所有信息。畢竟,哪個男人心裏沒有這一個藍天白雲夢,開寶馬也是很多人的心愿。

    但是寶馬作為豪華品牌,價格不是每個消費者都能承受的起的。寶馬進口1系的價格不是很貴,但是國人就是對兩廂車有一定的排斥心理,比較鍾愛三廂車,所以即使進口1系兩廂的價格足夠便宜,但是買單的人照樣不多。

    當然,寶馬3系是三廂車,但是3系的價格在30萬左右,還是有點小貴。所以在20萬級別這個領域,出現了一個市場空缺。如果能有一台20萬的三廂寶馬,這應該是極好的選擇。

    看看奧迪A3就知道了,作為豪華品牌的A3將價格做到了20萬元左右,在這個沒有直接競爭對手的領域,A3的銷量好的一塌糊塗,A3在11月份交出了9883輛的銷量,將近萬輛的銷量足以看出來這個細分市場有很大的潛力。

    所以寶馬義不容辭的推出了國產版的1系三廂。

    寶馬1系三廂版基於Compact Sedan概念車打造出來的,新車的設計借鑒了概念車的設計元素,同時也具有着寶馬家族化的設計風格,前大燈的造型與3系比較相似,側麵線條比較平直,從外觀看像是縮小的3系,看起來短小精悍,富有運動感。

    內飾看起來還是那麼熟悉的感覺,畢竟也是採用了寶馬家族化的設計特徵,中控台造型很有層次感,並配備了8.8英寸液晶屏,同時還有大面積的鍍鉻裝飾,可以增強內飾的精緻感。

    新車基於UKL前驅平台打造,1系三廂將會是前驅車型,新車的動力系統或為1.5T 136馬力+6速自動變速箱,2.0T 192馬力/231馬力+8擋手自一體變速箱,其中官方聲稱1.5T車型的百公里油耗最低可以達到5.5L。

    新車將會有無鑰匙啟動、抬頭显示、倒車影像、全景天窗、LED大燈、pM2.5濾清器等,這些都是國人比較看重的配置。新車的預售價為20.5萬起,這就意味着也許不到20萬的價格就可以買到帥氣的三廂寶馬,估計很多消費者都會心動吧!如果實際售價能比預售價還低一點的話,那麼A3就會瞬間感到壓力了。

    競爭對手

    奧迪A3

    指導價:18.49-28.10萬

    A3在國內的成就有目共睹,但是它真正的競爭對手預計會在2月份正式上市銷售,憑藉著“藍天白雲”在國內的號召力,A3將會面臨很大壓力,那麼它能否守住自己的陣地,就讓我們拭目以待吧!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • 為何有點小錢的人都不願買國產車?

    為何有點小錢的人都不願買國產車?

    廢話這麼多,歸根結底說白了就是一個字 – 錢。當你的財力和社會地位提升起來后,相信你的內心自然會有答案。

    2016年汽車圈發生了很多事,其中令叫獸印象最深的是自主品牌的叫好聲四起,銷量上也取得了空前的突破。不過當叫獸冷靜下來理智的想一想,當下自主品牌真的是形勢一片大好,即將崛起了嗎?

    廢話這麼多,歸根結底說白了就是一個字 – 錢。當你的財力和社會地位提升起來后,相信你的內心自然會有答案。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

  • 不要小看這些買車細節,去4S店裝老司機全靠這幾招…

    不要小看這些買車細節,去4S店裝老司機全靠這幾招…

    總結:買車畢竟是一件大事,最怕就是花了大價錢結果掉進了別人一早挖好的陷阱里,起不來活不起,當了冤大頭還要替別人數錢,所以在我們買車的時候還是多加留意,多留個心眼,將主動權抓在自己的手上。

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

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

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

  • 大發現!國3汽車的PM2.5居然比國5低?

    大發現!國3汽車的PM2.5居然比國5低?

    接下來是國三的車。所以僅從pM2。5的指數上來看,老車的成績不一定差,新車的成績也不一定好,反而車況才是更重要的因素。而那些所謂的專家。其實之前其他網友測試汽車尾氣pM2。5指數得出驚人的結果。(汽車尾氣比空氣乾淨,聽着很扯,畢竟你不可能整天去吸尾氣),就有專家出來表明,汽車的排放導致的污染並非只是直接的pM排放污染,而是“二次反應”。

    一到冬季,pM指數和霧霾就成了熱門話題,這個時候汽車就會慣例被推上輿論焦點,近期國六排放標準的出台也進一步表明了相關部門的觀點—“霧霾的很大成因就是因為汽車的排放”。每次說到空氣污染提得最多的就是pM2.5指數。

    pM指數那麼多,為什麼偏偏是pM2.5?

    pM的英文全稱為particulate Matter (顆粒物),而数字2.5是指這種顆粒物的空氣動力學直徑(aerodynamic diameter)為2.5微米( pM10則是指粒徑等於、小於10微米的顆粒物)。粒徑越小、密度越低,顆粒沉降得越慢。如果再有一些外力引起空氣微小的擾動,這些細小的顆粒即使在無風的條件下,最終也很難沉降到地面上,它們會一直在空中“遊盪”,成為危害人體健康的罪魁禍首。

    而我們常說的pM2.5的濃度或其他污染氣體的濃度是指每立方米空氣中這種污染物的質量含量,它反映了空氣的污染程度,這個值越高,就代表空氣污染越嚴重。由於空氣中污染物的種類很多,如:可吸入顆粒物、二氧化硫、氮氧化物、一氧化碳及臭氧等,為了統一評估,我國的環保部門,將每一種污染物的濃度都換算成統一的空氣污染指數,然後對外發布。俗話說病從口入,那麼我們今天就從源頭-用儀器來測試一下汽車尾氣的pM2.5指數。

    在此之前我們先看下香煙的pM指數,直接爆表,所以能戒煙朋友趕緊戒煙了。

    接着測試一下國4的車。(所測汽車均為公司同事車輛)

    然後是國5的車!

    接下來是國三的車。

    所以僅從pM2.5的指數上來看,老車的成績不一定差,新車的成績也不一定好,反而車況才是更重要的因素。

    而那些所謂的專家。。。

    其實之前其他網友測試汽車尾氣pM2.5指數得出驚人的結果。。。

    (汽車尾氣比空氣乾淨,聽着很扯,畢竟你不可能整天去吸尾氣),就有專家出來表明,汽車的排放導致的污染並非只是直接的pM排放污染,而是“二次反應”。

    這樣一群吃瓜群眾就懵逼了,你是博士后,你說什麼都對,就像一直聽說F1產生的下壓力足以讓其貼在牆上跑,但是從來沒有見過,不相信這件事的人也找不到反駁的理由。

    不管汽車的排放是不是元兇(因為現在霧霾這個鍋汽車已經背定了),相關部門對新車排放的要求越來越高並沒錯,畢竟排放再少也是排放。假設之前國三標準滿分是100分,當年生產的汽車再優秀,也只能得100。

    現在國五標準出來了,直接就說滿分是150,100分不及格,也不給你重考的機會(限制進京、限制遷入等),這種把能夠達到要求的老車也一刀切那就有點想不通了,對於車迷來說最大的損失就是中國就永遠不會有老爺車文化,因為在相關部門眼中只有報廢車。

    最後通過數據計算可以知道廣州的車輛密度比北京還要高,但是空氣質量卻要好不少,所以愛吃烤鴨的小夥伴趕緊把烤鴨吃個夠,說不定哪天烤鴨也分黃標鴨!!!

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

    【其他文章推薦】

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

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

    ※超省錢租車方案

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

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

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

  • 為什麼朋友說他夢想的車是艾母雞?看完本文後終於懂了

    為什麼朋友說他夢想的車是艾母雞?看完本文後終於懂了

    說到奔馳許多人最初的印象都是寬、大、貴外加後排坐着一個人肥,牙黃,地中海的老闆似乎奔馳一向跟年輕、運動、操控這些詞扯不上什麼關係所以坊間也流傳這麼一個說法——甚至有一些“自信狂人”遇上大奔時會抑制不住腎上腺

    說到奔馳

    許多人最初的印象都是

    寬、大、貴

    外加後排坐着一個

    人肥,牙黃,地中海的老闆

    似乎奔馳一向跟

    年輕、運動、操控這些詞

    扯不上什麼關係

    所以坊間也流傳這麼一個說法——

    甚至有一些“自信狂人”

    遇上大奔時

    會抑制不住腎上腺本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

  • 我很驚訝!十幾萬的國產SUV竟有人幹得過眾泰?

    我很驚訝!十幾萬的國產SUV竟有人幹得過眾泰?

    對於我們汽車媒體來說,這是個很方面的功能。榮威RX5配備了車身穩定系統、定速巡航、一鍵啟動、電動駐車、前撞預警系統、陡坡緩降、電動後備廂、車道保持、道路限速識別、四驅系統鎖止等豐富配置。動力系統方面,RX5將搭載1。

    國內緊湊型SUV的熱度只增不減,2016年,榮威帶來了他們潛心打造,並得到馬雲的阿里技術支持的榮威RX5,主打互聯網概念。

    榮威RX5外觀走大氣沉穩范,中規中矩但又精緻的造型給人不錯的印象。前臉“展翼格柵”將兩側前大燈融貫一體,前大燈為矩陣式LED大燈,其由24顆LED光源組成,燈組內部還集成了“如意形”LED日間行車燈。矩陣式全LED大燈更顯科技感。

    一進車內最吸睛的當屬中控10.4英寸大屏。由於大屏幕的使用,一些傳統按鍵被取消,整體設計簡潔時尚,僅在屏幕下方保留了五個實體按鍵。同時RX5在內飾用料上也多處使用了軟質皮革包裹,凸顯質感。

    RX5配備了榮威和阿里聯合開發的Yun OS,功能較為強大,可以通過大數據為用戶提供個性化服務。還可以支持綁定航拍機和運動相機,旅途中拍攝的畫面可以呈現在中控的大屏幕上。對於我們汽車媒體來說,這是個很方面的功能。

    榮威RX5配備了車身穩定系統、定速巡航、一鍵啟動、電動駐車、前撞預警系統、陡坡緩降、電動後備廂、車道保持、道路限速識別、四驅系統鎖止等豐富配置。

    動力系統方面,RX5將搭載1.5T和2.0T兩款渦輪增壓發動機,其中1.5T發動機最大功率為169馬,峰值扭矩250N·m,與之匹配的是手動/TST 7速雙離合變速箱;2.0T發動機最大功率為220馬力,峰值扭矩350N·m,與之匹配的是TST 6速濕式雙離合變速箱。

    整車開起來動力表現不錯,車輛的轉向是比較精準的,底盤偏向柔軟和舒適,但是又保留了一些路感。很符合家用SUV得定位。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

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

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

  • 一網打盡枚舉操作 .net core

    本文介紹如何使用枚舉以及,如何將枚舉類型更好的應用於項目中,看完本文可以有序的將項目中的枚舉更容易的使用到每個角落。

    1,分析枚舉

     /// <summary>
        /// 性別
        /// </summary>
        public enum Gender
        {
            /// <summary>
            ////// </summary>
            
            Man = 0,
            /// <summary>
            ////// </summary>
            
            Women = 1
        }

     

    如1所示,這是一個非常普通的枚舉類,在項目中使用的話,一般都會將它作為某實體的一個屬性,這個時候問題就來了,在頁面裡邊我們是需要拿到與之相關的描述信息和對應的值作為一個下拉框或者checkbox讓用戶去選擇,同時也不可以將Disable和enable作為給用戶最終展示的信息,需要去手動去拼,於是有了如下的方式

    2,枚舉類信息完善,增加描述信息

     /// <summary>
        /// 性別
        /// </summary>
        public enum Gender
        {
            /// <summary>
            ////// </summary>
            [Description("")]
            Man = 0,
            /// <summary>
            ////// </summary>
            [Description("女")]
            Women = 1
        }

    再給枚舉增加一個擴展方法

     /// <summary>
            /// 獲取到對應枚舉的描述-沒有描述信息,返回枚舉值
            /// </summary>
            /// <param name="enum"></param>
            /// <returns></returns>
            public static string GetDescription(this Enum @enum)
            {
                Type type = @enum.GetType();
                string name = Enum.GetName(type, @enum);
                if (name == null)
                {
                    return null;
                }
                FieldInfo field = type.GetField(name);
                if (!(Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute))
                {
                    return name;
                }
                return attribute?.Description;
            }

     好像到這一步的時候問題可以得到解決,通過getdescroption()這類的方法可以去獲取到與枚舉相應的描述信息用於展示,但是這也僅限於在mvc模式下,通過viewbag將枚舉的類中的每一項都加到枚舉集合中返回給頁面,在頁面裡邊遍歷,如果枚舉類型很多,那麼這類型的重複邏輯就會很多,非常的心煩。

    如果可以將這些操作做一個封裝,用一個接口可以返回所有枚舉類型的相關信息,就好了,於是有了如下的做法

    1 創建一個描述枚舉的類 

     public class EnumModel
        {
            /// <summary>
            ///枚舉原始值
            /// </summary>
            public ValueType Source { get; set; }
            /// <summary>
            /// 描述信息
            /// </summary>
            public string Description { get; set; }
            /// <summary>
            /// value
            /// </summary>
            public int Value { get; set; }
        }

    2,寫一個方法通過傳遞一個枚舉類類型去得到List<EnumModel>

    public static List<EnumModel> GetEnumListModels<T>()
            {
                var model = default(T);
                FieldInfo[] fieldinfo = typeof(T).GetFields();
                List<EnumModel> result = new List<EnumModel>();
                foreach (FieldInfo field in fieldinfo)
                {
                    EnumModel enumModel = new EnumModel();
                    if (!(Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute))
                    {
                        enumModel.Description = field.GetValue(model).ToString();
                    }
                    else
                    {
                        enumModel.Description = attribute.Description;
                    }
                    enumModel.Value = field.GetValue(model).GetHashCode();
                    enumModel.Source = field.GetValue(model) as ValueType;
                    if (field.GetValue(model).ToString() != "0")
                    {
                        result.Add(enumModel);
                    }
                    
                }
                return result;
            }

    3,寫一個接口,輸入枚舉的類的名稱,調用2中的方法,得到具體的返回信息

    [Route("[controller]/{name}")]
            public IActionResult GetEnumList(string name)
            {
                Assembly assembly = Assembly.Load("Ftw");
                Type t = assembly.GetType(string.Concat("Ftw.Enum.", name), throwOnError: false, ignoreCase: true);
                if (t == null)
                {
                    throw new ServiceException(string.Concat("請確保枚舉[", name, "]在 Ftw.Enum 中定義"));
                }
                Type enumhelp = typeof(EnumHelper);
                var obj = enumhelp.GetMethod("GetEnumListModels").MakeGenericMethod(t);
                return Json(obj.Invoke(t, null));
            }
    

      解釋一下:Ftw是類庫的名稱,Enum是Ftw類庫下的一個文件夾,所有的枚舉類都在Enum下邊放着,EnumHelper是 2 中方法【GetEnumListModels】所在的類,通過反射程序集得到枚舉類型,通過反射程序集將類型傳入GetEnumListModels作為 T 最後執行方法的到list.

    比如  Gender的調用,假如 GetEnumList所在的controller是EnumController,那麼調用就是通過  Enum/Gender ,對於.net core, mvc .net core api這類項目這種方式是非常有幫助的。

     

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

    【其他文章推薦】

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

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

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

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

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

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

  • 【C#】AutoMapper 使用手冊

    目錄

    • 1 入門例子
    • 2 註冊
      • 2.1 Profile
    • 3 配置
      • 3.1 命名約定
      • 3.2 配置可見性
      • 3.3 全局屬性/字段過濾
      • 3.4 識別前綴和後綴
      • 3.5 替換字符
    • 4 調用構造函數
    • 5 數組和列表映射
      • 5.1 處理空集合
      • 5.2 集合中的多態
    • 6 方法到屬性映射
    • 7 自定義映射
    • 8 扁平化映射
      • 8.1 IncludeMembers
    • 9 嵌套映射

    本文基於 AutoMapper 9.0.0

    AutoMapper 是一個對象-對象映射器,可以將一個對象映射到另一個對象。

    官網地址:http://automapper.org/

    官方文檔:https://docs.automapper.org/en/latest/

    1 入門例子

    public class Foo
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    
    public class FooDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    
    public void Map()
    {
        var config = new MapperConfiguration(cfg => cfg.CreateMap<Foo, FooDto>());
    
        var mapper = config.CreateMapper();
    
        Foo foo = new Foo { ID = 1, Name = "Tom" };
    
        FooDto dto = mapper.Map<FooDto>(foo);
    }
    

    2 註冊

    在使用 Map 方法之前,首先要告訴 AutoMapper 什麼類可以映射到什麼類。

    var config = new MapperConfiguration(cfg => cfg.CreateMap<Foo, FooDto>());
    

    每個 AppDomain 只能進行一次配置。這意味着放置配置代碼的最佳位置是在應用程序啟動中,例如 ASP.NET 應用程序的 Global.asax 文件。

    從 9.0 開始 Mapper.Initialize 方法就不可用了。

    2.1 Profile

    Profile 是組織映射的另一種方式。新建一個類,繼承 Profile,並在構造函數中配置映射。

    public class EmployeeProfile : Profile
    {
        public EmployeeProfile()
        {
            CreateMap<Employee, EmployeeDto>();
        }
    }
    
    var config = new MapperConfiguration(cfg =>
    {
        cfg.AddProfile<EmployeeProfile>();
    });
    

    Profile 內部的配置僅適用於 Profile 內部的映射。應用於根配置的配置適用於所有創建的映射。

    AutoMapper 也可以在指定的程序集中掃描從 Profile 繼承的類,並將其添加到配置中。

    var config = new MapperConfiguration(cfg =>
    {
        // 掃描當前程序集
        cfg.AddMaps(System.AppDomain.CurrentDomain.GetAssemblies());
        
        // 也可以傳程序集名稱(dll 名稱)
        cfg.AddMaps("LibCoreTest");
    });
    

    3 配置

    3.1 命名約定

    默認情況下,AutoMapper 基於相同的字段名映射,並且是 不區分大小寫 的。

    但有時,我們需要處理一些特殊的情況。

    • SourceMemberNamingConvention 表示源類型命名規則
    • DestinationMemberNamingConvention 表示目標類型命名規則

    LowerUnderscoreNamingConventionPascalCaseNamingConvention 是 AutoMapper 提供的兩個命名規則。前者命名是小寫並包含下劃線,後者就是帕斯卡命名規則(每個單詞的首字母大寫)。

    我的理解,如果源類型和目標類型分別採用了 蛇形命名法駝峰命名法,那麼就需要指定命名規則,使其能正確映射。

    public class Foo
    {
        public int Id { get; set; }
    
        public string MyName { get; set; }
    }
    
    public class FooDto
    {
        public int ID { get; set; }
    
        public string My_Name { get; set; }
    }
    
    public void Map()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<Foo, FooDto>();
    
            cfg.SourceMemberNamingConvention = new PascalCaseNamingConvention();
            cfg.DestinationMemberNamingConvention = new LowerUnderscoreNamingConvention();
        });
    
        var mapper = config.CreateMapper();
    
        Foo foo = new Foo { Id = 2, MyName = "Tom" };
    
        FooDto dto = mapper.Map<FooDto>(foo);
    }
    

    3.2 配置可見性

    默認情況下,AutoMapper 僅映射 public 成員,但其實它是可以映射到 private 屬性的。

    var config = new MapperConfiguration(cfg =>
    {
        cfg.ShouldMapProperty = p => p.GetMethod.IsPublic || p.SetMethod.IsPrivate;
        cfg.CreateMap<Source, Destination>();
    });
    

    需要注意的是,這裏屬性必須添加 private set,省略 set 是不行的。

    3.3 全局屬性/字段過濾

    默認情況下,AutoMapper 嘗試映射每個公共屬性/字段。以下配置將忽略字段映射。

    var config = new MapperConfiguration(cfg =>
    {
    	cfg.ShouldMapField = fi => false;
    });
    

    3.4 識別前綴和後綴

    var config = new MapperConfiguration(cfg =>
    {
        cfg.RecognizePrefixes("My");
        cfg.RecognizePostfixes("My");
    }
    

    3.5 替換字符

    var config = new MapperConfiguration(cfg =>
    {
        cfg.ReplaceMemberName("Ä", "A");
    });
    

    這功能我們基本上用不上。

    4 調用構造函數

    有些類,屬性的 set 方法是私有的。

    public class Commodity
    {
        public string Name { get; set; }
    
        public int Price { get; set; }
    }
    
    public class CommodityDto
    {
        public string Name { get; }
    
        public int Price { get; }
    
        public CommodityDto(string name, int price)
        {
            Name = name;
            Price = price * 2;
        }
    }
    

    AutoMapper 會自動找到相應的構造函數調用。如果在構造函數中對參數做一些改變的話,其改變會反應在映射結果中。如上例,映射后 Price 會乘 2。

    禁用構造函數映射:

    var config = new MapperConfiguration(cfg => cfg.DisableConstructorMapping());
    

    禁用構造函數映射的話,目標類要有一個無參構造函數。

    5 數組和列表映射

    數組和列表的映射比較簡單,僅需配置元素類型,定義簡單類型如下:

    public class Source
    {
        public int Value { get; set; }
    }
    
    public class Destination
    {
        public int Value { get; set; }
    }
    

    映射:

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Source, Destination>();
    });
    IMapper mapper = config.CreateMapper();
    
    var sources = new[]
    {
        new Source { Value = 5 },
        new Source { Value = 6 },
        new Source { Value = 7 }
    };
    
    IEnumerable<Destination> ienumerableDest = mapper.Map<Source[], IEnumerable<Destination>>(sources);
    ICollection<Destination> icollectionDest = mapper.Map<Source[], ICollection<Destination>>(sources);
    IList<Destination> ilistDest = mapper.Map<Source[], IList<Destination>>(sources);
    List<Destination> listDest = mapper.Map<Source[], List<Destination>>(sources);
    Destination[] arrayDest = mapper.Map<Source[], Destination[]>(sources);
    

    具體來說,支持的源集合類型包括:

    • IEnumerable
    • IEnumerable
    • ICollection
    • ICollection
    • IList
    • IList
    • List
    • Arrays

    映射到現有集合時,將首先清除目標集合。如果這不是你想要的,請查看AutoMapper.Collection。

    5.1 處理空集合

    映射集合屬性時,如果源值為 null,則 AutoMapper 會將目標字段映射為空集合,而不是 null。這與 Entity Framework 和 Framework Design Guidelines 的行為一致,認為 C# 引用,數組,List,Collection,Dictionary 和 IEnumerables 永遠不應該為 null

    5.2 集合中的多態

    這個官方的文檔不是很好理解。我重新舉個例子。實體類如下:

    public class Employee
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    
    public class Employee2 : Employee
    {
        public string DeptName { get; set; }
    }
    
    public class EmployeeDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    
    public class EmployeeDto2 : EmployeeDto
    {
        public string DeptName { get; set; }
    }
    

    數組映射代碼如下:

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Employee, EmployeeDto>().Include<Employee2, EmployeeDto2>();
        cfg.CreateMap<Employee2, EmployeeDto2>();
    });
    IMapper mapper = config.CreateMapper();
    
    var employees = new[]
    {
        new Employee { ID = 1, Name = "Tom" },
        new Employee2 { ID = 2, Name = "Jerry", DeptName = "R & D" }
    };
    
    var dto = mapper.Map<Employee[], EmployeeDto[]>(employees);
    

    可以看到,映射后,dto 中兩個元素的類型,一個是 EmployeeDto,一個是 EmployeeDto2,即實現了父類映射到父類,子類映射到子類。

    如果去掉 Include 方法,則映射后 dto 中兩個元素的類型均為 EmployeeDto

    6 方法到屬性映射

    AutoMapper 不僅能實現屬性到屬性映射,還可以實現方法到屬性的映射,並且不需要任何配置,方法名可以和屬性名一致,也可以帶有 Get 前綴。

    例如下例的 Employee.GetFullName() 方法,可以映射到 EmployeeDto.FullName 屬性。

    public class Employee
    {
        public int ID { get; set; }
    
        public string FirstName { get; set; }
    
        public string LastName { get; set; }
    
        public string GetFullName()
        {
            return $"{FirstName} {LastName}";
        }
    }
    
    public class EmployeeDto
    {
        public int ID { get; set; }
    
        public string FirstName { get; set; }
    
        public string LastName { get; set; }
    
        public string FullName { get; set; }
    }
    

    7 自定義映射

    當源類型與目標類型名稱不一致時,或者需要對源數據做一些轉換時,可以用自定義映射。

    public class Employee
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public DateTime JoinTime { get; set; }
    }
    
    public class EmployeeDto
    {
        public int EmployeeID { get; set; }
    
        public string EmployeeName { get; set; }
    
        public int JoinYear { get; set; }
    }
    

    如上例,IDEmployeeID 屬性名不同,JoinTimeJoinYear 不僅屬性名不同,屬性類型也不同。

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Employee, EmployeeDto>()
            .ForMember("EmployeeID", opt => opt.MapFrom(src => src.ID))
            .ForMember(dest => dest.EmployeeName, opt => opt.MapFrom(src => src.Name))
            .ForMember(dest => dest.JoinYear, opt => opt.MapFrom(src => src.JoinTime.Year));
    });
    

    8 扁平化映射

    對象-對象映射的常見用法之一是將複雜的對象模型並將其展平為更簡單的模型。

    public class Employee
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public Department Department { get; set; }
    }
    
    public class Department
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    
    public class EmployeeDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public int DepartmentID { get; set; }
    
        public string DepartmentName { get; set; }
    }
    

    如果目標類型上的屬性,與源類型的屬性、方法都對應不上,則 AutoMapper 會將目標成員名按駝峰法拆解成單個單詞,再進行匹配。例如上例中,EmployeeDto.DepartmentID 就對應到了 Employee.Department.ID

    8.1 IncludeMembers

    如果屬性命名不符合上述的規則,而是像下面這樣:

    public class Employee
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public Department Department { get; set; }
    }
    
    public class Department
    {
        public int DepartmentID { get; set; }
    
        public string DepartmentName { get; set; }
    }
    
    public class EmployeeDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public int DepartmentID { get; set; }
    
        public string DepartmentName { get; set; }
    }
    

    Department 類中的屬性名,直接跟 EmployeeDto 類中的屬性名一致,則可以使用 IncludeMembers 方法指定。

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Employee, EmployeeDto>().IncludeMembers(e => e.Department);
        cfg.CreateMap<Department, EmployeeDto>();
    });
    

    9 嵌套映射

    有時,我們可能不需要展平。看如下例子:

    public class Employee
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public int Age { get; set; }
    
        public Department Department { get; set; }
    }
    
    public class Department
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public string Heads { get; set; }
    }
    
    public class EmployeeDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public DepartmentDto Department { get; set; }
    }
    
    public class DepartmentDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    

    我們要將 Employee 映射到 EmployeeDto,並且將 Department 映射到 DepartmentDto

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Employee, EmployeeDto>();
        cfg.CreateMap<Department, DepartmentDto>();
    });
    

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • Android官方新推的DI庫 Hilt

    Android官方新推的DI庫 Hilt

    Hilt是Google Android官方新推薦的依賴注入工具.
    已加入到官方文檔: Dependency injection with Hilt. 目前是alpha release階段.

    Hilt是在Dagger之上, Hilt單詞的意思是: 刀把, 柄.
    代碼庫還是這個google/dagger.

    Hilt的出現, 讓我想起了曾經曇花一現的dagger.android, 不知道hilt能不能經得住時間的考驗.

    本文介紹Hilt的基本使用. 熟悉dagger的朋友可能會發現很容易.

    Hilt是花里胡哨的小打小鬧? 還是下一個主流工具? 讓我們拭目以待.

    Codelab練習例子

    • Codelab: Using Hilt in your Android app.

    我的Fork: https://github.com/mengdd/android-hilt.

    這個項目最開始是一個用ServiceLocator手動注入的例子, 功能是記錄用戶點擊button操作, 显示log. 有兩個Fragment和Activity.

    通過Codelab中一系列步驟的改造, 最終改成用Hilt做依賴注入.

    本文舉例就用其中的代碼片段了.

    原repo還貼心地附上了改造前後的對比diff: Full codelab comparison.

    Hilt依賴添加

    project root: build.gradle:

    buildscript {
        ext.hilt_version = '2.28-alpha'
        ...
        dependencies {
            ...
            classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
        }
    }
    

    需要Android 4.0以上.

    app/build.gradle:

    apply plugin: 'kotlin-kapt'
    apply plugin: 'dagger.hilt.android.plugin'
    
    android {
        ...
    }
    
    dependencies {
        // Hilt dependencies
        implementation "com.google.dagger:hilt-android:$hilt_version"
        kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
    
        // Hilt testing dependency
        androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
        // Make Hilt generate code in the androidTest folder
        kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
    }
    

    後面兩個是測試的依賴.

    概念, 用詞解釋

    • Container, 對應Component.
    • Bindings, 對應依賴.
    The information Hilt has about how to provide instances of different types are also called bindings.
    
    • Component hierarchy是指依賴在當前component中可以用, 也可以在它包含的child component中用. 舉例: application容器中的依賴activity可以用, 但是反過來不行.

    Hilt基本使用

    • @HiltAndroidApp: 標記在Application類上, 觸發代碼生成, application container是整個app的parent container.
    @HiltAndroidApp
    class LogApplication : Application()
    
    • @AndroidEntryPoint: 標記在Activity和Fragment上. 創建了一個和當前Activity/fragment生命周期相關的container. 目前支持的類型是: Activity, Fragment, View, Service, BroadcastReceiver.
    @AndroidEntryPoint
    class ExampleActivity : AppCompatActivity() { ... }
    

    每次Fragment(或Activity等)創建都會有它對應的container實例.

    • 字段注入: 用@Inject標記字段, 注意字段不能是private的.
    @AndroidEntryPoint
    class LogsFragment : Fragment() {
    
        @Inject
        lateinit var dateFormatter: DateFormatter
        ...
    }
    


    Component lifetimes可以看到每種類型的component創建和注入的時間.

    Activity是onCreate(), Fragment是onAttach().

    對Fragment來說, 在onAttach()的時候完成了對象的注入, 之後訪問對象都沒有問題.

    • 提供依賴: 用@Inject標記構造器.
    class DateFormatter @Inject constructor() {
        ...
    }
    

    它的依賴可以在構造參數中標明.

    Hilt的Scope支持

    默認所有的依賴都是沒有scope的, 每次注入依賴都創建新的實例.
    Hilt自動在對應的生命周期創建/銷毀對象: Component lifetimes.

    也可以把依賴scope到某個component, 這樣在這個component內, 依賴就是單例.

    • scope: @Singleton: application container的scope, 說明是application範圍內的單例. @ActivityScoped對應activity component.

    所有可用的scope: Component scopes.

    module

    module的使用基本和dagger一樣, 用來提供一些無法用構造@Inject的依賴, 比如接口, 第三方庫類型, Builder模式構造的對象等.

    • @Module: 標記這是一個module. 在Kotlin代碼中, module可以是一個object.
    • @Provides: 標記方法, 提供返回值類型的依賴.
    • @Binds: 標記抽象方法, 返回接口類型, 實現是方法的唯一參數.

    Hilt多了一個註解:

    • @InstallIn: 指明module對應哪個container, 也即Component.
      Generated components for Android classes
    @InstallIn(ApplicationComponent::class)
    @Module
    object DatabaseModule {
    
        @Provides
        @Singleton
        fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
            return Room.databaseBuilder(
                appContext,
                AppDatabase::class.java,
                "logging.db"
            ).build()
        }
    
        @Provides
        fun provideLogDao(database: AppDatabase): LogDao {
            return database.logDao()
        }
    }
    

    module的拆分

    module的名字最好能傳達它提供了什麼類型的依賴, 所以多種依賴拆分多個modules寫比較好.

    @InstallIn(ActivityComponent::class)
    @Module
    abstract class NavigationModule {
        @Binds
        abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
    }
    

    @Binds@Provides不能放在同一個module里.
    因為@Binds需要module是一個abstract class, 而@Provides需要module是一個object.
    放一起會報錯: error: A @Module may not contain both non-static and abstract binding methods.

    默認依賴

    有一些默認依賴是已經有的, 比如:

    • @ApplicationContext.
    • @ActivityContext.

    可以直接作為@Provides方法或@Inject構造的參數.

    默認依賴是和component對應的.

    Component default bindings

    Qualifier

    要提供同一個接口的不同實現, 可以用不同的註解來標記. (dagger之前用的是@Named).

    A qualifier is an annotation used to identify a binding.

    舉例: LoggerDataSource接口提供了內存和數據庫兩種實現.

    定義兩個註解:

    @Qualifier
    annotation class InMemoryLogger
    
    @Qualifier
    annotation class DatabaseLogger
    

    module中提供的時候用來標記相應的依賴:

    @InstallIn(ApplicationComponent::class)
    @Module
    abstract class LoggingDatabaseModule {
    
        @DatabaseLogger
        @Singleton
        @Binds
        abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
    }
    
    @InstallIn(ActivityComponent::class)
    @Module
    abstract class LoggingInMemoryModule {
    
        @InMemoryLogger
        @ActivityScoped
        @Binds
        abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
    }
    

    這裏用了兩個module因為它們對應兩個不同的component, 一個是application一個是activity, 依賴也是相應的scope.

    注入的時候也對應加上:

    @InMemoryLogger
    @Inject
    lateinit var logger: LoggerDataSource
    

    @EntryPoint

    Hilt支持最常用的Android組件, 對於默認不支持的類型, 如果還想做字段注入, 需要用@EntryPoint.

    注意這裏只是限制了字段注入的情況, 自定義類型一般用構造注入比較方便(如果能用的話).

    @EntryPoint的意思就是一個邊界點, 這裏可以通往Hilt的世界, 得到容器提供的依賴對象們.

    Codelab中的例子是一個ContentProvider.

    關鍵的部分是這段代碼:

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface LogsContentProviderEntryPoint {
        fun logDao(): LogDao
    }
    
    private fun getLogDao(appContext: Context): LogDao {
        val hiltEntryPoint = EntryPointAccessors.fromApplication(
            appContext,
            LogsContentProviderEntryPoint::class.java
        )
        return hiltEntryPoint.logDao()
    }
    

    Hilt and Dagger

    當初的dagger.android已被放棄, 它是為了簡化dagger在Android上的使用而單獨推出的. (根據Activity和Fragment的生命周期, 開發者不用手動調用inject方法, 但是確實最開始的setup code比較多.)

    Hilt相對於Dagger來說, 簡化了幾個點:

    • 不用自己創建Component.
    • 不用手動調用inject()方法來完成字段注入.
    • 不用自己在Application中保存component.
    • 提供了一些Scope, 不用自己定義和寫.
    • 提供了一些默認依賴, 比如Context.

    總體來說Hilt就是針對Android做的定製, 讓依賴注入框架用起來更方便. 畢竟dagger是一個java注入庫, 它的應用範圍不限於Android.

    因為Hilt和Dagger可以共存, 可以逐步遷移. 既然官方推薦了, 可以在項目內小範圍地先試試.

    最後推薦這個cheat sheet

    Reference

    • Dependency Injection on Android with Hilt
    • Dependency injection with Hilt
    • Dagger Hilt

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

    【其他文章推薦】

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

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

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

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

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

  • Redis的內存和實現機制

    Redis的內存和實現機制

    1. Reids內存的劃分

    1. 數據 內存統計在used_memory中
    2. 進程本身運行需要內存 Redis主進程本身運行需要的內存佔用,代碼、常量池等
    3. 緩衝內存,客戶端緩衝區、複製積壓緩衝區、AOF緩衝區。有jemalloc分配內存,會統計在used_memory中
    4. 內存碎片 Redis在分配、回收物理內存過程中產生的。內存碎片不會統計在used_memory中。如果Redis服務器中的內存碎片已經很大,可以通過安全重啟的方式減小內存碎片:因為重啟之後,Redis重新從備份文件中讀取數據,在內存中進行重排,為每個數據重新選擇合適的內存單元,減小內存碎片。

    2. Redis的數據存儲的細節

    涉及到內存分配器jemalloc, 簡單動態字符串(SDS),5種值類型對象的內部編碼,redisObject,

    1. DictEntry: Redis 是key-value數據庫,因此對每個鍵值對都會有一個dictEntry,裏面存儲了指向Key和Value的指針;next指向下一個dictEntry,與本Key-Value無關
    2. Key: 並不是以字符串存儲,而是存儲在SDS結構中
    3. RedisObject: 5種值對象不是直接以對應的類型存儲的,而是被封裝為redisObject來存儲
    4. jemalloc: 無論是DictEntry對象,還是redisObject, SDS對象,都需要內存分配器

    2.1 Jemalloc

    redis 在編譯時便會指定內存分配器, 內存分配器可以是libc、jemalloc、tcmalloc

    jemalloc作為Redis的默認內存分配器,在減小內存碎片方面做的相對比較好。jemalloc在64位系統中,將內存空間劃分為小、大、巨大三個範圍;每個範圍內又劃分了許多小的內存塊單位;當Redis存儲數據時,會選擇大小最合適的內存塊進行存儲。

    2.2 RedisObject

    redis對象的類型,內部編碼,內存回收,共享對象等功能都需要RedisObject的支持

    typedef struct redisObject{
        unsigned type: 4;
        unsigned encoding: 4;
        unsigned lru: REDIS_LRU_BITS; /*lru time*/
        int refcount;
        void *ptr;
    } robj;
    
    • type 字段 佔4bit 目前有5中類型, REDIS_STRING, REDIS_LIST, REDIS_HASH, REDIS_SET, REDIS_ZSET。 當執行type命令時,便是通過讀取redisObject對象的type字段獲取對象類型

    • encoding 佔4bit (表示對象的內部編碼),對於redis支持的每種類型,都有至少兩種內部編碼。通過object encoding命令,可以查看對象採用的編碼方式

      • 對於字符串,有int, embstr, raw 三種編碼。
      • 對於列表, 有壓縮列表和雙端列表兩種編碼方式,如果列表中元素較少,redis傾向於使用壓縮列表進行存儲,因為壓縮列表內存佔用少,而且比雙端鏈表可以更快載入;當列表對象元素較多時,壓縮列表就會轉化為更適合存儲大量元素的雙端鏈表。
    • lru 不同版本佔用內存大小不一樣,4.0版本佔用24bit,2.6版本佔用22bit

      • 記錄的是對象最後一次被命令程序訪問的時間,通過對比lru時間和當前時間,可以計算某個對象的空轉時間,object idletime命令可以显示該空轉時間 秒級別,改命令並不會改變對象的lru值,lru值除了通過object idletime命令打印之外,還與Redis的內存回收有關係:如果Redis打開了maxmemory選項,且內存回收算法選擇的是volatile-lru或allkeys—lru,那麼當Redis內存佔用超過maxmemory指定的值時,Redis會優先選擇空轉時間最長的對象進行釋放。
    • refcount 共享對象 記錄對象的引用計數,協助內存回收,引用計數可以通過 object refcount命令查看

      • ​ 共享對象的具體實現
      • Redis共享對象目前只支持整數值的對象。實際上是對內存和CPU時間的衡量。共享對象雖然會降低內存消耗,但是判斷兩個對象是否相等時需要消耗時間的。,對於整數值,判斷操作複雜度為O(1);對於普通字符串,判斷複雜度為O(n);而對於哈希、列表、集合和有序集合,判斷的複雜度為O(n^2)。
      • 雖然共享對象只能是整數值的字符串對象,但是5種類型都可能使用共享對象(如哈希、列表等的元素可以使用)。reids服務器在初始化時,會創建10000個字符串對象,值分別是0-9999的整數值。10000這個数字可以通過調整參數REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值進行改變
    • ptr 指針指向具體的數據 如 set hello world ptr指向包含字符串world的SDS

    • RedisObject對象大小16字節 4bit+4bit+24bit+4Byte+8Byte=16Byte

    3. Redis內部數據結

    3.1 SDS 簡單動態字符串

    結構

    struct sdshdr {
    	int len;  // 記錄buf數組中已使用字節的數量 等於SDS所保存字符串的長度
        int free;  // 記錄buf數組中未使用的字節數量
        char buf[];
    };
    
    1. SDS結構 佔據的空間:free+len+buf(表示字符串結尾的空字符串), 其中buf=free+len+1. 則總長度為4+4+free+len+1=free+len+9

    2. 與C字符串的比較

      在C字符串的基礎上加入了free和len字段,優勢

      • 獲取字符串長度: SDS O(1), C字符串是O(n)
      • 緩衝區溢出:使用C字符串的API時,如果字符串長度增加(如strcat操作)而忘記重新分配內存,很容易造成緩衝區的溢出;而SDS由於記錄了長度,相應的API在可能造成緩衝區溢出時會自動重新分配內存,杜絕了緩衝區溢出。
      • 修改字符串內存的重分配:對於C字符串,如果要修改字符串,必須要重新分配內存(先釋放再申請),因為如果沒有重新分配,字符串長度增大時會造成內存溢出,字符串長度減小時會造成內存泄漏。對於SDS, 由於記錄了len和free,因此解除了字符串長度和空間數組長度之間的關聯,可以在此基礎上進行優化:空間預分配(分配內存時比實際需要的多)使得字符串長度增大時重新分配內存的概率減小。惰性空間釋放策略 惰性空間釋放用於優化 SDS 的字符串縮短操作: 當 SDS 的 API 需要縮短 SDS 保存的字符串時, 程序並不立即使用內存重分配來回收縮短后多出來的字節, 而是使用 free 屬性將這些字節的數量記錄起來, 並等待將來使用。
      • 二進制安全 C 字符串中的字符必須符合某種編碼(比如 ASCII), 並且除了字符串的末尾之外, 字符串裏面不能包含空字符, 否則最先被程序讀入的空字符將被誤認為是字符串結尾 —— 這些限制使得 C 字符串只能保存文本數據, 而不能保存像圖片、音頻、視頻、壓縮文件這樣的二進制數據。
        SDS 的 API 都是二進制安全的(binary-safe): 所有 SDS API 都會以處理二進制的方式來處理 SDS 存放在 buf 數組裡的數據, 程序不會對其中的數據做任何限制、過濾、或者假設 —— 數據在寫入時是什麼樣的, 它被讀取時就是什麼樣。

      總結:

      • Redis 的字符串表示為 sds ,而不是 C 字符串(以 \0 結尾的 char*)。

      • 對比 C 字符串,sds 有以下特性:
        – 可以高效地執行長度計算(strlen);
        – 可以高效地執行追加操作(append);
        – 二進制安全;

      • sds 會為追加 操作進行優化:加快追加操作的速度,並降低內存分配的次數,代價是多佔用了一些內存,而且這些內存不會被主動釋放。

    3.3 字典

    在Redis中的應用:

    1. 實現數據庫鍵空間(key space) Redis 是一個鍵值對數據庫,數據庫中的鍵值對就由典保存:每個數據庫都有一個與之相對應的字典,這個字典被稱之為鍵空間(key space。
    2. 用作Hash類型鍵的其中一種底層實現

    Redis 的 Hash 類型鍵使用以下兩種數據結構作為底層實現:

    1. 字典;
    2. 壓縮列表

    3.3.1 字典的底層實現

    實現字典的方法有很多種:

    • 最簡單的就是使用鏈表和數組,方式只適用於元素個數不多的情況
    • 兼顧高效和簡單性,使用哈希表
    • 追求更穩定的性能特徵,並且希望高效的實現排序操作,可以是用更為複雜的平衡樹

    Reids選擇了高效且實現簡單的哈希表作為字典的底層實現。

    /* dict.h/dict
    * 字典
    *
    * 每個字典使用兩個哈希表,用於實現漸進式 rehash
    */
    
    typedef struct dict {
        dictType *type;  // 特定於類型的處理函數
        void *privdata;  // 類型處理函數的私有數據
        dictht ht[2];   // 2個哈希表
        
        int rehashidx;  // 記錄rehash 進度的標誌, 值為-1  表示rehash未進行
        
        int iterators;   // 當前正在運作的安全迭代器數量
    } dict;
    

    注: dict類型使用了兩個指針分別指向兩個哈希表

    其中,0號哈希表(ht[0])是字典主要使用的哈希表,而 1號哈希表(ht[1])則只有對0號哈希表進行rehash時才使用。

    3.3.2 哈希表的實現

    /*哈希表*/
    typedef struct dictht {
        dictEntry **table;   // 哈希表節點指針數組(俗稱桶, bucket)
        unsigned long size;  //指針數組的大小
        unsigned long sizemask;   //指針數組的長度掩碼
        unsigned long used;   // 哈希表現有的節點數量
    }dictht;
    
    /*哈希表節點*/
    typedef struct dictEntry {
        void *key;
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
        } v;
        
        // 鏈接後繼系節點
        struct dictEntry *next;
    } dictEntry;
    

    next 屬性指向另一個dictEntry結構, 多個dictEntry 可以通過next指針串連成鏈表dictht使用鏈地址法來處理鍵碰撞;當多個不同鍵擁有相同的哈希值時,哈希表用一個鏈表將這些鍵連接起來。

    3.3.3 哈希碰撞

    在哈希表實現中,當兩個不同的鍵擁有相同的哈希值時,我們稱這兩個鍵發生碰撞(collision),而哈希表實現必須想辦法對碰撞進行處理。字典哈希表所使用的碰撞解決方法被稱之為鏈地址法:這種方法使用鏈表將多個哈希值相同的節點串連在一起,從而解決衝突問題。

    假設現在有一個帶有三個節點的哈希表:

    對於一個新的鍵值對 key4 和 value4 ,如果 key4 的哈希值和 key1 的哈希值相同,那麼它們將在哈希表的 0 號索引上發生碰撞。

    3.2.4 添加新鍵值對時觸發rehash操作?

    對於使用鏈地址法來解決碰撞問題的哈希表 dictht 來說,哈希表的性能依賴於它的大小(size屬性)和它所保存的節點的數量(used 屬性)之間的比率:比率最好在1:1。

    4. 跳躍表

    跳躍表是一種隨機化數據結果,查找、添加、刪除操作都可以在對數期望時間下完成

    跳躍表目前在Redis的唯一作用就是作為有序集類型的底層數據結構之一

    Redis對跳躍表進行了修改包括:

    • score值可重複
    • 對比一個元素需要同時檢查它的score和member
    • 每個節點帶有高度為1層的後退指針,用於從表尾方向向表頭方向迭代

    Redis 為什麼用跳錶而不用平衡樹?

    4.1 skiplist與平衡樹、哈希表的比較

    • skiplist和各種平衡樹(如AVL、紅黑樹等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做單個key的查找,不適宜做範圍查找。所謂範圍查找,指的是查找那些大小在指定的兩個值之間的所有節點。
    • 在做範圍查找的時候,平衡樹比skiplist操作要複雜。在平衡樹上,我們找到指定範圍的小值之後,還需要以中序遍歷的順序繼續尋找其它不超過大值的節點。如果不對平衡樹進行一定的改造,這裏的中序遍歷並不容易實現。而在skiplist上進行範圍查找就非常簡單,只需要在找到小值之後,對第1層鏈表進行若干步的遍歷就可以實現。
    • 平衡樹的插入和刪除操作可能引發子樹的調整,邏輯複雜,而skiplist的插入和刪除只需要修改相鄰節點的指針,操作簡單又快速。
    • 從內存佔用上來說,skiplist比平衡樹更靈活一些。一般來說,平衡樹每個節點包含2個指針(分別指向左右子樹),而skiplist每個節點包含的指針數目平均為1/(1-p),具體取決於參數p的大小。如果像Redis里的實現一樣,取p=1/4,那麼平均每個節點包含1.33個指針,比平衡樹更有優勢。
    • 查找單個key,skiplist和平衡樹的時間複雜度都為O(log n),大體相當;而哈希表在保持較低的哈希值衝突概率的前提下,查找時間複雜度接近O(1),性能更高一些。所以我們平常使用的各種Map或dictionary結構,大都是基於哈希表實現的。
    • 從算法實現難度上來比較,skiplist比平衡樹要簡單得多。

    Redis的對象類型和內部編碼

    1. 字符串

    1.1 內部編碼

    • int 8個字節的長整型。字符串值是整型時,這個值使用long整型表示
    • embstr <=39字節的字符串。embstr與raw都使用redisObject和sds保存數據,區別在於,embstr的使用只分配一次內存空間(因此redisObject和sds是連續的),而raw需要分配兩次內存空間(分別為redisObject和sds分配空間)。因此與raw相比,embstr的好處在於創建時少分配一次空間,刪除時少釋放一次空間,以及對象的所有數據連在一起,尋找方便。而embstr的壞處也很明顯,如果字符串的長度增加需要重新分配內存時,整個redisObject和sds都需要重新分配空間,因此redis中的embstr實現為只讀。
    • raw: 大於39個字節的字符串

    1.2 編碼轉換

    新創建的字符串默認使用 REDIS_ENCODING_RAW 編碼,在將字符串作為鍵或者值保存進數據庫時,程序會嘗試將字符串轉為 REDIS_ENCODING_INT 編碼, 字符串的長度不超過512MB

    2. 列表

    創建新列表時Redis默認使用REDIS_ENCODING_ZIPLIST編碼,當一下任意一個條件滿足時,列表會被轉換成REDIS_ENCODING_LINKEDLIST編碼:

    • 試圖往列表新添加一個字符串值,且這個字符串的長度超過sever.list_max_ziplist_value(默認值是64)
    • ziplist 包含的節點超過server.list_max_ziplist_entries(默認的值為512)

    且編碼只可能由壓縮列錶轉化為雙端鏈表,一個列表可以存儲2^32-1個元素

    2.1 壓縮列表

    壓縮列表是Redis為了節約內存而開發的,由一系列特殊編碼的連續內存塊(而不是像雙端鏈表每個節點都是指針) 順序型數據結構;與雙端鏈表相比,壓縮列表可以節省內存空間,但是進行修改或增刪操作時,複雜度較高;因此當節點數量較少時,可以使用壓縮列表;但是節點數量多時,還是使用雙端鏈表划算。因為 ziplist 節約內存的性質,它被哈希鍵、列表鍵和有序集合鍵作為初始化的底層實現來使

    2.2 雙端鏈表

    typedef struct listNode {
        struct listNode *prev;  //前驅節點
        struct listNode *next;  // 後繼節點
        void *value;
    } listNode;
    
    typedef struct list {
        //表頭指針
        listNode *head;
        //表尾指針
        listNode *tail;
        unsigned long len; // 節點長度
        void *(*dup) (void *ptr);
        void (*freee)(void *ptr);
        int (*match) (void *ptr, void *key);
    }list;
    

    小結:

    作為Reids列表的底層實現之一; 作為通用數據結構,被其他功能模塊使用。

    • 節點帶有前驅和後繼指針,訪問前驅節點和後繼節點的複雜度為 O(1) ,並且對鏈表
      的迭代可以在從表頭到表尾和從表尾到表頭兩個方向進行;
    • 鏈錶帶有指向表頭和表尾的指針,因此對錶頭和表尾進行處理的複雜度為 O(1) ;
    • 鏈錶帶有記錄節點數量的屬性,所以可以在 O(1) 複雜度內返回鏈表的節點數量(長
      度);

    3. 哈希表

    • 當哈希表使用字典編碼時,程序將哈希表的鍵(key)保存為字典的鍵,將哈希表的值(value)保存為字典的值, 字典的鍵和值都是字符串對象

    • 壓縮列表編碼的哈希表

    • 編碼轉換

      默認使用ziplist編碼,當滿足以下條件時,自動切換為字典編碼

      • 哈希表中某個鍵或某個值的長度大於sever.hash_max_ziplist_value(默認值是64)
      • ziplist 包含的節點超過server.list_max_ziplist_entries(默認的值為512)

    4. 集合

    第一個添加到集合的元素,決定了創建集合時所使用的編碼:

    • 如果第一個元素可以表示為 long long 類型值(也即是,它是一個整數),那麼集合的初始編碼為 REDIS_ENCODING_INTSET 。
    • 否則,集合的初始編碼為 REDIS_ENCODING_HT 。

    4.1 內部編碼

    當使用 REDIS_ENCODING_HT 編碼時,集合將元素保存到字典的鍵裏面,而字典的值則統一設為 NULL

    如果一個集合使用 REDIS_ENCODING_INTSET 編碼, 當滿足以下條件的時候會轉成字典編碼

    • intset保存的整數值個數超過server.set_max_intset_entries 默認值為512
    • 試圖往集合中添加一個新的元素,這個元素不能被表示為long, long類型,類型不一樣的時候使用字典

    整數集合適用於集合所有元素都是整數且集合元素數量較小的時候,與哈希表相比,整數集合的優勢在於集中存儲,節省空間;同時,雖然對於元素的操作複雜度也由O(1)變為了O(n),但由於集合數量較少,因此操作的時間並沒有明顯劣勢。

    5 .有序集合

    有序集合與集合一樣,元素都不能重複;但與集合不同的是,有序集合中的元素是有順序的。與列表使用索引下標作為排序依據不同,有序集合為每個元素設置一個分數(score)作為排序依據

    5.1 內部編碼

    • 壓縮列表

    • 跳躍表(skiplist)

      跳躍表是一種有序數據結構,通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。除了跳躍表,實現有序數據結構的另一種典型實現是平衡樹;大多數情況下,跳躍表的效率可以和平衡樹媲美,且跳躍表實現比平衡樹簡單很多,因此redis中選用跳躍表代替平衡樹。跳躍表支持平均O(logN)、最壞O(N)的複雜點進行節點查找,並支持順序操作。Redis的跳躍表實現由zskiplist和zskiplistNode兩個結構組成:前者用於保存跳躍表信息(如頭結點、尾節點、長度等),後者用於表示跳躍表節點

    typedef struct zset {
        dict *dict;
        zskiplist *zsl;
    } zset;
    

    5.2 編碼轉換

    對於一個 REDIS_ENCODING_ZIPLIST 編碼的有序集,只要滿足以下任一條件,就將它轉換為REDIS_ENCODING_SKIPLIST 編碼

    • ziplist所保存的元素數量超過服務器屬性server.zset_max_ziplist_entries值 默認值是128
    • 新添加元素的member的長度大於服務器屬性server.zset_max_ziplist_value 默認值是64

    優化Redis 內存佔用

    1. 利用共享對象,可以減少對象的創建(同時減少了redisObject的創建),節省內存空間。目前redis中的共享對象只包括10000個整數(0-9999);可以通過調整REDIS_SHARED_INTEGERS參數提高共享對象的個數;例如將REDIS_SHARED_INTEGERS調整到20000,則0-19999之間的對象都可以共享。

      考慮這樣一種場景:論壇網站在redis中存儲了每個帖子的瀏覽數,而這些瀏覽數絕大多數分佈在0-20000之間,這時候通過適當增大REDIS_SHARED_INTEGERS參數,便可以利用共享對象節省內存空間

    內存碎片率

    mem_fragmentation_ratio=used_memory_rss (Redis進程佔據操作系統的內存(單位是字節))/ used_memory(Redis分配器分配的內存總量(單位是字節)).

    如果內存碎片率過高(jemalloc在1.03左右比較正常),說明內存碎片多,內存浪費嚴重;這時便可以考慮重啟redis服務,在內存中對數據進行重排,減少內存碎片。

    參考博文與書籍:

    1. 《redis設計與實現》
    2. Redis內存模型
    3. Redis 基礎操作 – 時間複雜度

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

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

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