"大菜": 源于自己刚踏入猿途混沌时起, 自我感觉不是一般的菜, 因而得名 "大菜", 于自身共勉.
基本概念
string(严格来说应该是 System.String) 类型是我们日常 coding 中用的最多的类型之一. 那什么是 String 呢?^ ~ ^
String 是一个不可变的连续 16 位的 Unicode 代码值的集合, 它直接派生自 System.Object 类型.
与之对应的还有一个不常用的安全字符串类型 System.Security.SecureString, 它会在非托管的内存上分配, 以便避开 GC 的黑手. 主要用于安全性特高的场景.[具体可查看 msdn 这里不展开讨论了.=>msdn 查看详情 https://msdn.microsoft.com/zh-cn/library/system.security.securestring(v=vs.100).aspx
特性
由于 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 的更纠结)
如果我们的程序中有很多个一模一样值的常量字符串, c# 的编译器会在编译期间把这些字符串合并为一个并写入模块的元数据中, 然后修改所有引用该字符串的代码. 这也是一种字符串重用技术, 学名'字符串池'. 这意味着什么呢? 这意味着所有值相同的常量字符串其实引用的是同一个内存地址的实例, 在相同值非常多的情况下能显著提高性能和节省大量内存.
- string s1 = "hello 大菜";
- string s2 = "hello 大菜";
- unsafe
- {
- ixed (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, 这里不做展开讨论了, 请自行参考其他文档.
所以万事都不是绝对的!! 每个事物都有适合自己的场景, 我们都需要自己去探索.(程序员太累了)
以上都是非生产环境测试结果, 如果错误, 请及时指正
来源: https://www.cnblogs.com/zhanlang/p/9612521.html