设为首页 加入收藏

TOP

Unity/C#基础复习(3) 之 String与StringBuilder的关系(一)
2019-09-17 18:34:43 】 浏览:35
Tags:Unity/C# 基础 复习 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

基础知识

  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要

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇如何在C#中引入CPLEX的dll(CPLEX.. 下一篇【C#基础】拥抱Lambda(1):Lambda..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目