Unity/C#基礎複習(3) 之 String與StringBuilder的關係

来源:https://www.cnblogs.com/sword-magical-blog/archive/2019/03/18/10550220.html
-Advertisement-
Play Games

參考資料 [1] @毛星雲【《Effective C 》提煉總結】 https://zhuanlan.zhihu.com/p/24553860 [2] 《C 捷徑教程》 [3] @flashyiyi【C NoGCString】 https://zhuanlan.zhihu.com/p/3552560 ...


參考資料

[1] @毛星雲【《Effective C#》提煉總結】 https://zhuanlan.zhihu.com/p/24553860
[2] 《C# 捷徑教程》
[3] @flashyiyi【C# NoGCString】 https://zhuanlan.zhihu.com/p/35525601
[4] 如何理解 String 類型值的不可變? @胖君和@程式媛小雙的回答 https://www.zhihu.com/question/20618891

基礎知識

  1. String類型在C#中用於保存字元,為引用類型,一旦創建,就不能再進行修改,其底層是根據字元數組(char[])實現的。
  2. StringBuilder表示可變字元字元串類型,其中的字元可以被改變、增加、刪除,當向一個已滿的StringBuilder添加字元時,其會自動申請記憶體進行擴容。
  3. Unity中Profiler視窗的GC Alloc那一列的信息表示的是當前幀產生了多少垃圾(指一塊存儲不再使用的數據的記憶體)。Unity官方文檔對此標簽是這樣的解釋的:

The GC Alloc column shows how much memory has been allocated in the current frame, which is later collected by the garbage collector.

大致意思是,GC Alloc這一列表示當前幀有多少記憶體被分配,這些記憶體將會在之後被垃圾回收器進行清理。

疑難解答

  1. 如何理解String類型值的不可變?
  2. 為什麼String類型的連接(加法和Concat)性能低下?與之相比,為什麼StringBuilder更快?
  3. String類型與GC(垃圾回收器)的關係?
  4. 如何正確的使用String與StringBuilder?

如何理解String類型值的不可變?

在C#中string類型的底層由char[],即字元數組進行實現,但我們並不能像修改字元數組的方式來對字元串進行修改。事實上,我們以為的修改(字元串的連接,字元串的賦值)對於字元串來說都不是真正的修改,每當我們對字元串進行賦值時,底層會進行兩個操作。

  1. 首先會去查找字元串池,如果字元串池有這個字元串,那麼直接將當前變數指向字元串池內的字元串。
  2. 如果字元串池內沒有這個字元串,那麼在堆上創建一塊記憶體用於放置這個字元串,並將當前變數指向這個新建的字元串。

一個新建字元串的簡單例子如下:

public static void Main(string[] args) {
    string s = "abc";
    Console.WriteLine(s);
    s = "123";
    Console.WriteLine(s);
}

其中第4行s的賦值語句並不是將原本"abc"的字元串修改成"123",而是另外在堆上創建了一個新的記憶體"123",並將s變數指向這個新字元串,而舊的字元串"abc"就被丟棄了,但它仍然在堆上占據著記憶體,等待GC將其回收。

對於字元串的連接(加法或Concat函數),其原理同上,事實上原來的字元串並沒有真正在後面增加了字元,而是創建了一個新的字元串,其值是兩個字元串連接後的結果。

字元串的這種特性,使得它的賦值和連接操作很容易造成記憶體浪費,因為每一次都將在堆上創建一個新的字元串對象。所以一個比較明確的思路是,不要頻繁的調用字元串的連接操作(比如放在Unity的Update函數中)。

既然不可變特性使得我們不得不小心的使用字元串,那麼字元串為什麼還會被設計成不可變的形式呢?很顯然,不可變的形式對於字元串可變的形式是利大於弊的,下麵根據參考資料[4][3],嘗試列舉、闡述一下為什麼字元串一定要是不可變的。

  1. 線程安全。在多線程環境下,只有對資源的修改是有風險的,而不可變對象只能對其進行讀取而非修改,所以是線程安全。如果字元串是可修改的,那麼在多線程環境下,需要對字元串進行頻繁加鎖,這是比較影響性能的。
  2. 為了安全(防止程式員意外修改了字元串)。想象下麵這樣一種情況,一個靜態方法用於給字元串(或StringBuilder)後面增加一個字元串。
public class StringTest{

    public static string AppendString(string s) {
        s += "abc";
        return s;
    }

    public static StringBuilder AppendString(StringBuilder s) {
        s = s.Append("abc");
        return s;
    }

    public static void Main(string[] args) {
        string s = "123";
        string s2 = AppendString(s);
        Console.WriteLine("原字元串:"+s+" 經過添加後的字元串:"+s2);

        StringBuilder sb = new StringBuilder("123");
        StringBuilder sb2 = AppendString(sb);
        Console.WriteLine("原字元串:" + sb.ToString() + " 經過添加後的字元串:" + sb2.ToString());
    }
}

運行結果如下:

原字元串:123 經過添加後的字元串:123abc
原字元串:123abc 經過添加後的字元串:123abc

可以看到StringBuilder因為是可變的,所以原字元串直接在靜態方法中被修改成了"123abc",而string類型因為其不可變的特性,所以它的原字元串和修改後的新字元串是不同的,這種不可變特性也就避免了程式員直接在方法裡面直接對字元串進行連接操作,導致字元串在不知情的情況下被修改了(就像StringBuilder一樣)。

  1. 因為字元串的不可變特性,所以其可以放心地作為Dictionary和Set的鍵(在Java中則是Map和Set)。在Dictionary和Set中使用可變類型作為鍵是極其危險的事,因為可修改鍵可能會導致Set和Dictionary中鍵值的唯一性被破壞。

為什麼String類型的連接(加法和Concat)性能低下?與之相比,為什麼StringBuilder更快?

先解決第一個問題,為什麼String類型的連接(加法和Concat)性能低下?

前面提到了,因為字元串是不可變的,所以所有看似對其進行了修改的操作,都是在堆上另外創建了一個新的字元串,而這創建過程是耗費性能(申請記憶體,檢查記憶體是否足夠,不夠的情況還要讓GC對垃圾記憶體進行回收),所以可想而知字元串連接性能是比較低的。

當然,性能高低是需要有一個參照物的,與StringBuilder的連接操作相比,string類型就是相當慢了,除了慢以外,字元串的連接操作還會產生大量GC,因為每一次連接,都創建了新的字元串,而舊的字元串理所當然就被丟棄了,在沒有任何變數引用這些舊字元串的情況下,GC要對這些舊字元串占據的記憶體進行回收,而GC的觸發是十分耗費性能的(簡單來說就是費時,因為GC是要遍歷堆上所有無引用的對象),表現在Unity中,就是在某一幀相比其他幀額外消耗了幾十ms來處理GC。

那麼,StringBuilder的連接操作為什麼快呢?

這要從StringBuilder的底層開始說起,StringBuilder的底層與string一樣都是字元數組(即char[]),與string被設計為不可變不同的是,StringBuilder是可變的。

當StringBuilder進行連接操作時,它會經歷以下步驟:

  1. 檢查當前字元數量是否大於長度,如果大於,那麼對StringBuilder進行擴容。
  2. 向char[]數組後面添加字元

很顯然,只有在StringBuilder長度小於添加的字元時,才會額外申請記憶體對char[]數組進行擴容,其他情況下,就是對數組內的元素進行變換而已,與string類型每次連接都會廢棄掉一個對象相比,StringBuilder就顯得更快一些了。

當然,除了連接操作,StringBuilder還支持刪除、修改字元串,這當然也是根據其中的char []數組進行操作的(而字元串因為其不可變性,是不支持這些操作的)。

考慮到StringBuilder擴容也是會產生GC的,所以一般比較好的做法是,在StringBuilder創建時就根據之後的使用情況為其指定一個容量。

String類型與GC(垃圾回收器)的關係?

這裡主要研究在Unity3D引擎下,string類型和StringBuilder進行連接操作產生的GC Alloc情況。

之前一直說string類型的連接操作浪費記憶體,那麼具體是什麼情況呢?這裡可以使用Unity3D引擎進行試驗,下麵嘗試在每幀進行1000次字元串加法,然後使用Profiler查看GC Alloc。

public class StringAppendGC : MonoBehaviour {

    string s = "";

    // Use this for initialization
    void Start () {
        
    }

    // 測試字元串加法在每一幀帶來的GC
    void Update () {
        
        s = "";

        // 每一幀進行1000次字元串加法
        for (int i=0;i<=1000;i++) {
            s += i;            
        }
    }
}

GC產生情況如下:

可以看到上面的函數每一幀都產生了2.7M的垃圾,而Unity官方對於GC Alloc這一列的描述是這樣的:

Keep this value at zero to prevent the garbage collector from causing hiccups in your framerate

大致意思是,保持該值為0以防止垃圾回收器使得某一幀(與其他幀相比)耗費的時間過長,造成“大幀”現象。

那麼如果將上面的String改用StringBuilder會怎麼樣呢?將上面的代碼改為如下所示:

public class StringAppendGC : MonoBehaviour {


    // Use this for initialization
    void Start () {
        
    }

    // 測試字元串加法在每一幀帶來的GC
    void Update () {
        StringBuilder stringBuilder = new StringBuilder(1000);

        // 每一幀進行1000次字元串加法
        for (int i=0;i<=1000;i++) {
            stringBuilder.Append(i);
        }
    }
}

GC Alloc情況如下:

可以看到每幀分配記憶體的情況從2.7M下降到了44KB,相比於String類型有了明顯的改善。

如何正確的使用String與StringBuilder?

既然知道了String類型在某些操作上會造成浪費,那麼我們使用它的時候就要萬分小心,根據參考資料[1]淺墨大佬所說,正確使用String與StringBuilder的姿勢如下:

創建不可變類型的最終值。比如string類的+=操作符會創建一個新的字元串對象並返回,多次使用會產生大量垃圾,不推薦使用。對於簡單的字元串操作,推薦使用string.Format。對於複雜的字元串操作,推薦使用StringBuilder類


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Java數據結構-HashMap 1. HashMap數據結構 沒有哈希衝突時,為數組,支持動態擴容 哈希衝突時,分為兩種情況: 1. 當衝突長度小於8或數組長度小於64(MIN_TREEIFY_CAPACITY預設值為64)時,為數組+鏈表(Node) 2. 當衝突長度大於8時,為數組+紅黑樹/鏈 ...
  • 作為一個有多年PHP開發經驗的碼農,我也是前段時間才發現PHP處理數組有這麼好用的函數, 至此之前,我處理數組的數據基本都是使用迴圈,記錄一下兩個函數的用法: array_column() 函數 返回輸入數組中某個單一列的值。 語法: array_column(array,column_key,in ...
  • 基於Cmake、QT Creator、Visual Studio的C++項目構建、開發、編譯是初學者必要的工具,本文主要介紹這些工具的下載與安裝,以及C++及Qt項目構建,主要包括 1.基於VS構建Qt項目;2.基於Qt Creater構建,在VS中開發Qt Creater生成的項目;3.基於Cma... ...
  • 概述 在PHP中有一種代碼復用的技術, 因為單繼承的問題, 有些公共方法無法在父類中寫出, 而 Trait可以應對這種情況, 它可以定義一些復用的方法, 然後在你需要使用的類中將其引入即可. 剛開始的時候給我的感覺就是將trait代碼塊直接拿到類中的意思, 但後來我發現, 我太天真了. PHP中的T ...
  • / 【例11 10】建立一個學生成績信息(包括學號、姓名、成績)的單向鏈表,學生數據按學號由小到大順序排列,要求實現對成績信息的插入、修改、刪除和遍歷操作。 / / 用鏈表實現學生成績信息的管理 / include include include struct stud_node{ int num; ...
  • 6 2 遞歸求階乘和 (10 分) 本題要求實現一個計算非負整數階乘的簡單函數,並利用該函數求 1!+2!+3!+...+n! 的值。 函數介面定義: double fact( int n ); double factsum( int n ); 函數fact應返回n的階乘,建議用遞歸實現。函數fac ...
  • 目錄 1、什麼是日誌? 簡單的說,日誌就是記錄程式的運行軌跡,方便查找關鍵信息,也方便快速定位解決問題。我們 Java 程式員在開發項目時都是依賴 Eclipse/ Idea 等開發工具的 Debug 調試功能來跟蹤解決 Bug,在開發環境可以這麼做,但項目發佈到了測試、生產環境呢?你有可能會說可以 ...
  • 本人經過2周的學習,成功搭建了認證伺服器,資源伺服器和客戶端 。下麵是本人對 oauth2的理解,以及spring-security的使用,如果理解錯誤的地方,還望指正。 現在代碼有點凌亂,過段時間會放到github上面的,本人會在代碼中添加詳細註釋,供學習交流使用 理解 1.認證伺服器 如抖音可以 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...