c#基础系列2---深入理解 String
“大菜”:源于自己刚踏入猿途混沌时起,自我感觉不是一般的菜,因而得名“大菜”,于自身共勉。
扩展阅读:[深入理解值类型和引用类型](https://www.cnblogs.com/zhanlang/p/9603592.html)
## 基本概念
string(严格来说应该是System.String) 类型是我们日常coding中用的最多的类型之一。那什么是String呢?^ ~ ^
String是一个不可变的连续16位的Unicode代码值的集合,它直接派生自System.Object类型。
与之对应的还有一个不常用的安全字符串类型System.Security.SecureString,它会在非托管的内存上分配,以便避开GC的黑手。主要用于安全性特高的场景。[具体可查看msdn这里不展开讨论了。=>msdn查看详情
特性
- 由于String类型直接派生于Object,所以它是引用类型,那就意味着String对象的实例总是存在于堆上。
- String具有不变性,也就是说一旦初始化,它的值将永远不变。
- String类型是封闭的,换言之,你的任何类型不能继承String。
- 定义字符串实例的关键字string只是System.String 类型的一个映射。
注意事项
关于字符串中的回车符和换行符一般大家喜欢直接硬编码‘\r\n’,但是不建议这么做,一旦程序迁移到其他平台,将出现错误。相反,推荐使用System.Environment类的NewLine属性来生成回车符和换行符,可以跨平台使用的。
常量字符串的拼接和非常量字符串在CLR中行为是不一样的。具体请查看性能部分。
字符串之前加@符号会改变编译器的行为,如果加了@符号,编译器会把String中的转义字符视为正常字符来显示。也就是我定义的什么内容就是什么内容,主要在使用文件路径或者目录字符串中使用。以下两个String内容的输出将完全一致。
static void Main(string[] args)
{
string a = "c:\\temp\\1";
string b = @"c:\temp\1";
Console.WriteLine(a);
Console.WriteLine(b);
Console.Read();
}
性能
- c#的编译器直接支持String类型,并将定义的常量字符串在编译期直接存放到模块的元数据中。然后会在运行时直接加载。这也说明String类型的常量在运行时是有特殊待遇的。
- 由于字符串的不变性,也就意味着多个线程同时操作该字符串不会有任何线程安全的问题。这在某些共享配置的设计中很有用。
- 如果程序经常会对比重复度比较高的字符串,这会造成性能上的影响,因为对比字符串是要经过几个步骤的。为此CLR引入了一个字符串重用的技术,学名叫做‘字符串留用’。原理就是:CLR会在初始化的时候创建一个内部的哈希表,key是字符串,value就是留用字符串在托管堆上的引用。
String类型提供了两个静态方法来操作这个哈希表:
String.Intern
String.IsInterned
具体请查看msdn(https://msdn.microsoft.com/zh-cn/library/system.string.isinterned(v=vs.110).aspx)
但是c#编译器默认是不开启字符串留用功能的,因为如果程序大量把字符串留用,应用程序总体性能可能会变得更慢。(微软也是挺纠结的,程序员TMD的更纠结)
4. 如果我们的程序中有很多个一模一样值的常量字符串, c#的编译器会在编译期间把这些字符串合并为一个并写入模块的元数据中,然后修改所有引用该字符串的代码。这也是一种字符串重用技术,学名‘字符串池’。这意味着什么呢?这意味着所有值相同的常量字符串其实引用的是同一个内存地址的实例,在相同值非常多的情况下能显著提高性能和节省大量内存。
string s1 = "hello 大菜";
string s2 = "hello 大菜";
unsafe
{
fixed (char* p = s1)
{
Console.WriteLine("字符串地址= 0x{0:x}", (int)p);
}
fixed (char* p = s2)
{
Console.WriteLine("字符串地址= 0x{0:x}", (int)p);
}
}
输出结果:
字符串地址= 0x80002d84
字符串地址= 0x80002d84
可见实例的值只分配了一次,但是有一点需要说明,字符串仅用于编译期能确定值的字符串,也就是常量字符串。如果我的程序修改为:
args = new string[] { "dfasfdsa"};
string s1 = "hello 大菜"+ args[0];
string s2 = "hello 大菜"+args[0];
unsafe
{
fixed (char* p = s1)
{
Console.WriteLine("字符串地址= 0x{0:x}", (int)p);
}
fixed (char* p = s2)
{
Console.WriteLine("字符串地址= 0x{0:x}", (int)p);
}
}
运行结果:
字符串地址= 0x2e3c
字符串地址= 0x2e7c
- 平时coding避免不了字符串的连接,如果一个频繁拼接字符串的场景下使用‘+’,对程序整体性能和GC影响还是挺大的,为此c#推出了 StringBuilder类型来优化字符串的拼接。相对于String类型的不变性来说,StringBuilder更像是可变的字符串类型。它的底层数据结构是一个Char的数组。另外还有容量(默认为16),最大容量(默认为int.MaxValue)等属性。StringBuilder的优势在于字符总数未超过‘容量’的时候,底层数组不会重新分配,这和String每次都重新分配形成最大的对比。如果字符总数超过‘容量’,StringBuilder会自动倍增容量属性,用一个新的数组来容纳原来的值,原来数组将会被GC回收。可见如果StringBuilder频繁的动态扩容也会损害性能,但是影响可能会比String小的多。 合理的设置StringBuilder初始容量对程序有很大帮助。测试如下:
int count = 100000;
Stopwatch sw = new Stopwatch();
sw.Start();
string s = "";
for (int i = 0; i < count; i++)
{
s += i.ToString();
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
运行结果:
14221
查看GC的情况
Gc执行的是如此频繁。 性能是可想而知的。接着看一下StringBuilder
int count = 100000;
Stopwatch sw = new Stopwatch();
sw.Start();
StringBuilder sb = new StringBuilder();//听说程序员都这样命名StringBuilder
for (int i = 0; i < count; i++)
{
sb.Append(i.ToString());
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
运行结果:
12
GC情况:
几乎没有GC(可能还未达到触发GC的临界点),如果我合理初始化了StringBuilder 容量,生产环境中结果差距将会更大。 呵呵 ^ ~ ^
其他
关于字符串留用和字符串池
- 一个程序集加载的时候,CLR默认会留用该程序集元数据中描述的所有文本常量字符串。由于可能会出现额外的哈希表查找造成的性能下降的现象,所以现在可以禁用这个特性了。
- coding中我们平常比较两个字符串是否相等,那这个过程是怎么样的呢?
- 首先判断字符的数量是否相等。
- CLR逐个对比字符最终确定是否相等。
这个场景是适合字符串留用的。因为不再需要经过以上的两个步骤,直接哈希表拿到value就可以对比确定了。
关于字符串拼接性能
基于以上所有知识,那是不是StringBuilder拼接字符串性能永远都高于符号‘+’呢?答案是否定的。
static void Main(string[] args)
{
int count = 10000000;
Stopwatch sw = new Stopwatch();
sw.Start();
string str1 = "str1", str2 = "str2", str3 = "str3";
for (int i = 0; i < count; i++)
{
string s = str1 + str2 + str3;
}
sw.Stop();
Console.WriteLine($@"+用时: {sw.ElapsedMilliseconds}" );
sw.Reset();
sw.Start();
for (int i = 0; i < count; i++)
{
StringBuilder sb = new StringBuilder();//听说程序员都这样命名StringBuilder
sb.Append(str1).Append(str2).Append(str3);
}
sw.Stop();
Console.WriteLine($@"StringBuilder.Append 用时: {sw.ElapsedMilliseconds}");
Console.Read();
}
运行结果:
+用时: 553
StringBuilder.Append 用时: 975
符号‘+’最终会调用String.Concat方法,当同时连接几个字符串时,并不是每连接一个都分配一次内存,而是把几个字符都作为 String.Concat方法的参数,只分配一次内存。所以在拼接的字符串个数比较少的场景下,String.Concat 性能是略高于StringBuilder.Append。string.Format 方法最终调用的是StringBuilder,这里不做展开讨论了,请自行参考其他文档。
所以万事都不是绝对的!!每个事物都有适合自己的场景,我们都需要自己去探索。(程序员太累了)
以上都是非生产环境测试结果,如果错误,请及时指正
请尊重一个猿的辛苦,转载请标明出处 ^ ~ ^ 。部分图片来源于网络,如有侵权请及时联系。让我们一起进步吧
一个不止于IT圈内容的微信公众号,欢迎关注,交流更多的IT知识。不定时会有惊喜奥 ^ ~ ^
c#基础系列2---深入理解 String的更多相关文章
- c#基础系列3---深入理解ref 和out
"大菜":源于自己刚踏入猿途混沌时起,自我感觉不是一般的菜,因而得名"大菜",于自身共勉. 扩展阅读 c#基础系列1---深入理解 值类型和引用类型 c#基础系 ...
- Netty基础系列(3) --彻底理解NIO
前言 上一节中我们提到了同步异步与阻塞非阻塞的区别,知道了同步并不等于阻塞.而本节的主角NIO是一种同步非阻塞的I/O模型,并且是I/O多路复用模型.NIO在java中被称为 New I/O.它并不能 ...
- Java基础3:深入理解String及包装类
更多内容请关注微信公众号[Java技术江湖] 这是一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM.SpringBoot.MySQL.分布式.中间件.集群.Linux ...
- c#基础系列1---深入理解值类型和引用类型
"大菜":源于自己刚踏入猿途混沌拾起,自我感觉不是一般的菜,因而得名"大菜",于自身共勉. 不知不觉已经踏入坑已10余年之多,对于c#多多少少有一点自己的认识, ...
- Netty基础系列(2) --彻底理解阻塞非阻塞与同步异步的区别
引言 在进行I/O学习的时候,阻塞和非阻塞,同步和异步这几个概念常常被提及,但是很多人对这几个概念一直很模糊.要想学好Netty,这几个概念必须要掌握清楚. 同步和异步 同步与异步的区别在于,异步基于 ...
- Netty基础系列(4) --堆外内存与零拷贝详解
前言 到目前为止,我们知道Nio当中有三个最最核心的组件,分别是:Selelctor,Channel,Buffer.在Netty基础系列(3) --彻底理解NIO 这一篇文章中只是进行了大致的介绍. ...
- Java基础系列2:深入理解String类
Java基础系列2:深入理解String类 String是Java中最为常用的数据类型之一,也是面试中比较常被问到的基础知识点,本篇就聊聊Java中的String.主要包括如下的五个内容: Strin ...
- 夯实Java基础系列3:一文搞懂String常见面试题,从基础到实战,更有原理分析和源码解析!
目录 目录 string基础 Java String 类 创建字符串 StringDemo.java 文件代码: String基本用法 创建String对象的常用方法 String中常用的方法,用法如 ...
- 夯实Java基础系列9:深入理解Class类和Object类
目录 Java中Class类及用法 Class类原理 如何获得一个Class类对象 使用Class类的对象来生成目标类的实例 Object类 类构造器public Object(); register ...
随机推荐
- Luncene介绍
1.Luncene介绍 案例:实现一个文件的搜索功能,通过关键字搜索文件,凡是文件名或文件内容包括关键字文件都需要找出来.还可以根据中文词语进行查询,并且需要支持多个条件查询.Lucene可以解决 数 ...
- 洗礼灵魂,修炼python(42)--巩固篇—type内置函数与类的千丝万缕关系
type函数的隐藏属性 相信大家都知道内置函数type是用来查看对象的数据类型的.例: 那比如我对int类查看类型呢? 有朋友会说,int是内置类啊,用自定义的应该不会这样,我们自定义一个类呢? 还是 ...
- MySQL 8.0 —— CATS事务调度算法的性能提升
原文地址:https://mysqlserverteam.com/contention-aware-transaction-scheduling-arriving-in-innodb-to-boost ...
- 服务器较稳妥的磁盘阵列方案:RAID5+热备盘
最近公司这边要整个数据中心,但是我们这边磁盘阵列的方案选择方面需要做好万全的测试 内部测试的服务器当初损坏的磁盘1个做的raid5直接换个新磁盘做替换rebuild就行了 但是现在想的方法是5+1的方 ...
- OffsetDateTime工具类
java8时间API介绍及使用 java7及之前版本,推荐使用jodatimejava8提供的datetime api参考了jodatime 在新的时间API中, Instant表示一个精确的时间点, ...
- Hadoop2.7.6_07_HA高可用
1. Hadoop的HA机制 前言:正式引入HA机制是从hadoop2.0开始,之前的版本中没有HA机制 1.1. HA的运作机制 (1)hadoop-HA集群运作机制介绍 所谓HA,即高可用(7*2 ...
- percona-toolkit大表操作DDL使用
1. 系统与安装数据库 [root@zhang ~]# cat /etc/redhat-release # 也可以使用其他版本 CentOS Linux release (Core) [root@zh ...
- 反射型XSS+文件上传+CSRF—DVWA
在学习的过程中,想到将几种漏洞进行组合练习,记录下学习过程.大佬请绕过!谢谢!! 测试环境:DVWA,安装方法参考上一篇:https://www.cnblogs.com/aq-ry/p/9220584 ...
- centos7下安装docker(13docker存储)
Docker为容器提供了两种存放数据的资源: 1.storage driver管理的镜像层和容器层 2.Data volume Storage driver 之前我们学习镜像的时候知道镜像的分层结构: ...
- Tensorflow基本概念
[本文摘自网络,仅供学习使用] 官网上对TensorFlow的介绍是,一个使用数据流图(data flow graphs)技术来进行数值计算的开源软件库.数据流图中的节点,代表数值运算:节点节点之间的 ...