Unity/C#基础复习(3) 之 String与StringBuilder的关系
参考资料
[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
基础知识
- String类型在C#中用于保存字符,为引用类型,一旦创建,就不能再进行修改,其底层是根据字符数组(char[])实现的。
- StringBuilder表示可变字符字符串类型,其中的字符可以被改变、增加、删除,当向一个已满的StringBuilder添加字符时,其会自动申请内存进行扩容。
- 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这一列表示当前帧有多少内存被分配,这些内存将会在之后被垃圾回收器进行清理。
疑难解答
- 如何理解String类型值的不可变?
- 为什么String类型的连接(加法和Concat)性能低下?与之相比,为什么StringBuilder更快?
- String类型与GC(垃圾回收器)的关系?
- 如何正确的使用String与StringBuilder?
如何理解String类型值的不可变?
在C#中string类型的底层由char[],即字符数组进行实现,但我们并不能像修改字符数组的方式来对字符串进行修改。事实上,我们以为的修改(字符串的连接,字符串的赋值)对于字符串来说都不是真正的修改,每当我们对字符串进行赋值时,底层会进行两个操作。
- 首先会去查找字符串池,如果字符串池有这个字符串,那么直接将当前变量指向字符串池内的字符串。
- 如果字符串池内没有这个字符串,那么在堆上创建一块内存用于放置这个字符串,并将当前变量指向这个新建的字符串。
一个新建字符串的简单例子如下:
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],尝试列举、阐述一下为什么字符串一定要是不可变的。
- 线程安全。在多线程环境下,只有对资源的修改是有风险的,而不可变对象只能对其进行读取而非修改,所以是线程安全。如果字符串是可修改的,那么在多线程环境下,需要对字符串进行频繁加锁,这是比较影响性能的。
- 为了安全(防止程序员意外修改了字符串)。想象下面这样一种情况,一个静态方法用于给字符串(或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一样)。
- 因为字符串的不可变特性,所以其可以放心地作为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进行连接操作时,它会经历以下步骤:
- 检查当前字符数量是否大于长度,如果大于,那么对StringBuilder进行扩容。
- 向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类
Unity/C#基础复习(3) 之 String与StringBuilder的关系的更多相关文章
- Unity/C#基础复习(5) 之 浅析观察者、中介者模式在游戏中的应用与delegate原理
参考资料 [1] <Unity 3D脚本编程 使用C#语言开发跨平台游戏>陈嘉栋著 [2] @张子阳[C#中的委托和事件 - Part.1] http://www.tracefact.ne ...
- java基础复习之对于String对象,能够使用“=”赋值,也能够使用newkeyword赋值,两种方式有什么差别?
String类型是实际工作中经经常使用到的类型,从数据类型上划分,String是一个引用类型,是API中定义的一个类.所以String类型的对象能够用new创建,比如String name=new S ...
- Java基础(40)String、StringBuilder和StringBuffer的区别(TODO)
一.String String实现了Serializable接口.Comparable<String>接口和CharSequence接口,并且使用final char value[]不可变 ...
- JAVA基础部分复习(一、8中基础类型,以及String相关内容)
以下是关于java中8种基本类型的介绍说明: package cn.review.day01; /** * java基础复习,8种数据类型 * (byte,short,long,int,double, ...
- [java基础]复习 java三大特性,异常,接口,String
继承 关键字extends 继承是为了不同的实现(龙生九子,各不相同) 单继承,一个类最多只能有一个父类 除了私有的外,子类可以访问父类的方法.属性. new过程中,父类先进行初始化,可通过super ...
- JAVA基础复习与总结<五> String类_File类_Date类
String类 .Java字符串就是Unicode字符序列,例如串“Java”就是4个Unicoe字符组成. .Java没有内置的字符串类型,而是在标准java类库中提供了一个预定义的类String, ...
- C#基础复习(4) 之 浅析List、Dictionary
参考资料 [1] .netCore 源码 https://github.com/dotnet/corefx [2] <Unity 3D脚本编程 使用C#语言开发跨平台游戏>陈嘉栋著 [3] ...
- Java基础复习笔记系列 九 网络编程
Java基础复习笔记系列之 网络编程 学习资料参考: 1.http://www.icoolxue.com/ 2. 1.网络编程的基础概念. TCP/IP协议:Socket编程:IP地址. 中国和美国之 ...
- Java基础复习笔记系列 八 多线程编程
Java基础复习笔记系列之 多线程编程 参考地址: http://blog.csdn.net/xuweilinjijis/article/details/8878649 今天的故事,让我们从上面这个图 ...
随机推荐
- Hibernate一级缓存(补)
------------------siwuxie095 什么是缓存 缓存是介于应用程序和永久性数据存储源(如:硬盘上的 ...
- 安装使用phpStudy在本机配置php运行环境
前言: php开发的初学者,强烈推荐使用phpStudy集成环境,一方面这个的确很好用(本人电脑安装了jspStudy,可以同时调试php和jsp),另一方面呢,虽然本人是技术控,但对这些繁杂的安装部 ...
- 使用HttpModule实现网址重写
1. 修改配置文件: <httpModules> <</span>add name="html" type="HttpModule&quo ...
- VMware安装centos虚拟机 通过NAT与主机互通并能上网
1.关于centos虚拟机的安装,我这里就不详细说明了,网上有很多教程,默认你们已经安装好. (我的环境是centos6.6 x86 最小安装版) 2.右键虚拟主机,选择设置选项. 3.在 ...
- 模板练习(LUOGU)
1:并查集 P3183食物链 #define man 300050 ; int find(int x){ if(fa[x]==x) return fa[x]; return fa[x]=find(fa ...
- url传递数据
一.post传递数据 $ci = curl_init($url); curl_setopt($ci, CURLOPT_HEADER, 0); curl_setopt($ci, CURLOPT_RETU ...
- Vue.js 登录注册实现
转载 http://www.jb51.net/article/118003.htm
- 图片素材类Web原型制作分享-Pexels
Pexels是一个高清图片下载服务站点,为用户提供海量共享图片素材的网站,每周都会定量更新. 菜单栏和底部栏都是悬浮在固定位置,内容区域滚动.首页图片排列采用瀑布流的方式,多图片滚动.包含的页面有:浏 ...
- 在EF中使用MySQL的方法及常见问题
有时需要在网上租用空间或数据库,Mysql成本低一些,所以想将sql server转成mysql…… 注意:在安装Mysql时要选择文字集为utf8,否则将不能使用中文(当前也可以在创建数据库时使用u ...
- 【Mac】使用QuickTime Player录制屏幕录像
我门分享都需要用到录屏软件,Mac系统有自带的QuickTime Player软件可以录制屏幕录像 环境与工具 1.mac系统 2.mac自带的QuickTime Player软件 使用方法 1.打开 ...