咱们之前成心对 string 一带而过,是因为它十分特地。首先,它是
imutable(不可变的)
即一个 string 定义的字符串,一旦设定,就不能扭转。
在 string 上间接 F12 转到定义,就能够看到 String 类的成员。其中,只有一个只读的动态成员 Empty(倡议应用,以代替 null 和空字符串 ””):
public static readonly String Empty;
一个只读的索引器,能够获取字符串中某个下标的字符:
public char this[int index] {get;}
一个只读的属性 Length,能够获取字符串长度:
public int Length {get;}
全部都是只读的,没有任何一个可写的类成员。再看看它的办法,有几个和“批改”相干的:
string slagon = “@大神小班,拎包入住 @”; // 删除 Console.WriteLine(slagon.Remove(1)); // 后果:@ Console.WriteLine(slagon.Remove(1, 1)); // 后果:@神小班,拎包入住 @ // 插入 Console.WriteLine(slagon.Insert(2, “@”)); // 后果:@大 @神小班,拎包入住 @ // 替换 Console.WriteLine(slagon.Replace(‘ 大 ’, ‘ 小 ’)); // 后果:@小神小班,拎包入住 @ // 截取 Console.WriteLine(slagon.Substring(1, 3)); // 后果:大神小 // 留神:slagon 还是没有变 Console.WriteLine(slagon); // 后果:@大神小班,拎包入住 @
以及修剪掉字符串前后空白字符的 Trim() 办法:
string fg = ” 大 飞 哥 “; // 应用一个 @后缀显示成果 Console.WriteLine(fg.Trim() + “@”); // 删除前后空白:大 飞 哥 @ Console.WriteLine(fg.TrimStart() + “@”); // 删除后面的空白:大 飞 哥 @ Console.WriteLine(fg.TrimEnd() + “@”); // 删除前面的空白:大 飞 哥 @ //fg 自身不会扭转 Console.WriteLine(fg + “@”); // 大 飞 哥 @
还有英语字母大小写转换的办法:
string sql = “SQL”; Console.WriteLine(sql.ToLower()); // 变成小写:sql Console.WriteLine(sql); // 不变:SQL string csharp = “CSharp”; Console.WriteLine(csharp.ToUpper()); // 变成大学:CSHARP Console.WriteLine(csharp); // 不变:CSharp
然而,所有的办法都不会扭转作为参数传入的字符串 slagan 的值,因为字符串是 immutable(不可变)的。同样,咱们本人写一个办法:
static void say(string words) {words += “oh,yeah!”;}
而后进行调用:
say(slagon); Console.WriteLine(slagon); //slagon 没有发生变化
运行的后果,slagon 没变,“感觉”就像是传递了一个 slagon 的正本给办法 say() 一样——当然其实并不一样。
演示:传入 say() 办法的是一个堆地址
咱们能够这样记忆:
- string 是援用类型
- 但它是一个十分非凡的援用类型
- 因为它在太多方面体现得和值类型一样(本质起因是 imutable)
另一个更间接的证据:在 string 上间接 F12 转到定义,你就会发现:
public sealed class String
这阐明 string 是一个类,类就是援用类型啊。留神它是 sealed 的,因为.NET 不心愿 string 被继承,免得毁坏它的 immutable 特色。绝大多数时候,咱们齐全能够把 string 当做值类型来应用,不会有任何问题,这也更合乎咱们的惯常思维。
此外,string 的比拟运算符:
==
也设计得和值类型一样。
一般来说,如果是援用类型,== 运算符会比拟两个对象的堆地址;但值类型,== 运算符间接比拟两个对象的值。咱们看看 string 的比拟:
string center = “ 源栈 ”, greet = “ 欢迎您 ”; string a = center + greet; string b = $”{center}{greet}”; Console.WriteLine(a == b); // 后果为 true
是不是像一个值类型一样?比拟的是 a 和 b 外面存储的值。你看 a 和 b 的所寄存的堆地址也不一样:
这就是通过咱们之前讲过的运算符重载实现的:
public static bool operator ==(String a, String b);
你可能会问,为什么咱们的示例代码这么简单?又是拼接(+)又是内插($)的。因为如果这样的话会出问题:
string a = “ 源栈 ”; string b = “ 源栈 ”; Console.WriteLine(a == b);
代码的后果依然是 true,然而 a 和 b 存储的是雷同的堆地址:
这就不能阐明 string 的“值类型”特色,因为 a 和 b 变量中存储还是堆地址而不是字符串的值。你可能奇怪,明明没有任何传递,为什么 a 和 b 会援用同样的一个 string 对象呢?这又波及一个编译器优化技术:
字符串池(string pool)
简略的说,在编译的时候(留神:是编译的时候,不是运行的时候),编译器会设置一个字符串“池(pool)”。每次要实例化一个新字符串的时候,首先在池中进行查看:
- 如果池中曾经有完全相同的字符串,间接将这个字符串的堆地址赋值给新变量;否则
- 实例化这个字符串,而后放到字符串池里
这样,就能够节俭很多的堆空间,尤其是当雷同的字符串十分多的时候。
“池”这种优化技术,在.NET 中大量应用,咱们当前还会屡次接触到。^_^
这时候,你可能会问:搞得这么简单,为什么不把 string 间接改成 struct 呢?
这是因为 string 有可能十分十分大,像咱们之前讲过的,太大的对象就不适宜放在栈中,免得占用贵重的栈资源。(温习:值类型和援用类型)
最初,咱们来看一看 string 的:
其余办法
一些办法能够进行判断。比方判断是否为空:
string a = null; string b = “”; string c = ” “; //IsNullOrEmpty():是不是为 Null 值或者为空 //IsNullOrWhiteSpace():是不是为 Null 或者空白字符串 Console.WriteLine(string.IsNullOrEmpty(a)); //True Console.WriteLine(string.IsNullOrWhiteSpace(a)); //True Console.WriteLine(string.IsNullOrEmpty(b)); //True Console.WriteLine(string.IsNullOrWhiteSpace(b)); //True Console.WriteLine(string.IsNullOrEmpty(c)); //False Console.WriteLine(string.IsNullOrWhiteSpace(c)); //True
以及其余返回 bool 值的判断,蕴含(Contain)和开始(Starts)/ 完结(Ends)
string slagon = “ 飞哥,还有源栈欢迎您!”; // 蕴含 ” 源栈 ” Console.WriteLine(slagon.Contains(“ 源栈 ”)); //True // 以 ” 飞哥 ” 开始 /” 欢迎您!” 结尾 Console.WriteLine(slagon.StartsWith(“ 飞哥 ”)); //True Console.WriteLine(slagon.EndsWith(“ 欢迎您!”)); //True
还有返回 int 类型下标的 IndexOf() 和 LastIndexOf():
string slagon = “ 源栈欢迎您!!!”; Console.WriteLine(slagon.IndexOf(“!”)); //10 Console.WriteLine(slagon.LastIndexOf(“!”)); //12
此外,还有连贯和拆分:
- Contact() 能够间接将字符串连接起来
- Join() 用指定字符(分隔符)将字符串连接起来
string a = “ 源栈 ”; string b = “,”; string c = “ 欢迎您!”; // 间接把 abc 连接起来 Console.WriteLine(string.Concat(a, b, c)); // 把 abc 用 ’ ‘ 连接起来 string joined = string.Join(‘ ‘, a, b, c); Console.WriteLine(joined); // 留神空格:源栈,欢迎您!
被 Join() 用分隔符连接起来的的字符串还能够再应用 Split() 拆分,取得一个 string 数组:
string[] splitted = joined.Split(‘ ‘); // 用 ’ ‘ 分隔 for (int i = 0; i < splitted.Length; i++) {Console.WriteLine(splitted[i]); } // 被 ’ ‘ 分隔之后,splitted 共三个元素:// 源栈 //,// 欢迎您!
此外,还有一个能够把字符串转换成字符数组的:
char[] ofA = a.ToCharArray(); // 后果:’ 源 ’ 和 ’ 栈 ’
这样转换之后,就能够对字符串的每个字符进行过滤筛选。
除了 Join() 和 Contact() 办法,其实咱们更多的都是间接应用加号(+)间接连贯字符串。然而,你或者看到有这样的倡议:不要应用加号(+)进行字符串拼接,而应该应用:
StringBuilder
真的是这样么?同学们留神肯定要警觉这样的“简洁明了”的论断。你能够反过来想一想:StringBuilder 和 string 是同时推出的,如果“用加号(+)进行字符串拼接”真的不行,为什么微软要搞这么一个语法进去?
但凡波及性能的问题,咱们这曾经是陈词滥调了,肯定要牢记几个准则:
- 天下没有收费的午餐
- 首先找到瓶颈
- no profile no improvement
而后,不仅要知其然,更要知其所以然。
所以咱们首先来看一看怎么应用它:
// 实例化一个 StringBuilder 对象 StringBuilder sb = new StringBuilder(); // 始终往 StringBuilder 对象上增加(Append)字符串 sb.Append(“ 源栈 ”); sb.Append(“,”); sb.Append(“ 欢迎您!”); // 不要忘了应用 ToString() 将 StringBuilder 对象转换成字符串 string slagon = sb.ToString();
首先能够看进去,整个应用过程比拟繁琐,如果用 + 的话就一行:
string slagon = “ 源栈 ” + “,” + “ 欢迎您!”;
而后 StringBuilder 是一个能够实例化的类,咱们 F12 查看其定义,能够看到它的次要办法,除了 Append(),还有:
- Insert():插入,须要指定插入的地位 index
- Replace():替换
- Remove():删除指定地位 index,肯定长度 length 的内容
- Clear():革除全部内容
sb.Remove(0, 1); // 删除了从下标为 0 开始的一个字符 sb.Replace(“!”, “……”); // 将!替换成…… sb.Insert(0, “○”); // 在下标为 0 的中央插入一个○ //sb.Clear(); // 全副革除
最初,它有好几个重载的构造函数。其中最重要的参数有两个:
public StringBuilder(int capacity); public StringBuilder(string value);
- value:指定 StringBuilder 最开始“装”着的字符串
- capacity:指定 StringBuilder 最后的“容量”。这实际上就是的 StringBuilder 所谓“性能晋升”的要害。
咱们来看看加号(+)拼接和 StringBuilder.Append() 的
区别
在 C# 中,虽说字符串被称之为“串”,但其余它不是像链表那样把字符一个一个串起来的,而是由一个字符数组予以寄存。
演示:查看源代码
所以,两个字符串 a 和 b 的加号拼接的过程应该是这样的:
- 计算出 a 和 b 的长度,而后相加达到总长度
- 按总长度新生成一个新的 char[] 数组
- 将 a 和 b 的内容顺次复制到新的 char[] 数组
- 将新的 char[] 数组合成字符串
留神,失常来说(因为不排除编译器优化的可能),不要认为三个字符串的拼接.NET 就能够计算出 a +b+ c 的长度,而后按下面的那样做,它还是得先实现 a + b 的拼接,再进行 a + b 和 c 的拼接。即便 a +b+ c 能够优化,但这样的 for 循环代码也是十分常见的:
string[] students = { “ 王新 ”, “ 陈元 ”, “ 彭志强 ”}; for (int i = 1; i < students.Length; i++) {students[0] += students[i]; }
这总不能编译器优化了吧?那么如果拼接的次数多了,比如说 100 次,就会产生 99 次没有必要的字符串长度计算,99 次没有必要的内存划分(须要 new 一个新的 char[]),99(?)次没有必要的字符串复制……
为了节俭掉这些不必要的性能损耗,.NET 为咱们提供了 StringBuilder,其工作原理是:
- 在 StringBuilder 实例化的时候,生成一个长度(capacity)或者由结构函数参数指定,或者默认为 16 的 char[] 数组
- 将 a、b、c、d……等字符串顺次往 char[] 数组里装,如果
- char[] 数组的长度不够了,StringBuilder 主动裁减其 capacity,生成一个双倍长度的新数组持续装
- 直到调用 ToString(),将 char[] 数组转换成字符串
对比方下图所示:
所以,如果是大规模的字符串拼接,应用 StringBuilder 的确有性能上的劣势。那么为什么加号拼接依然这么常见呢?^_^,当然是图不便啦!因为当拼接的次数不多,拼接也没有造成性能瓶颈的时候,开发效率是第一位的——好吧,我抵赖,就是懒,哈哈。
然而,你也要明确一点:懒,并不一定是好事。尤其是对于程序员而言。
作业:
- 确保文章(Article)的题目不能为 null 值,也不能为一个或多个空字符组成的字符串,而且如果题目前后有空格,也予以删除
- 设计一个实用的机制,能确保用户(User)的昵称(Name)不能含有 admin、17bang、管理员等敏感词。
- 确保用户(User)的明码(Password):
- 长度不低于 6
- 必须由大小写英语单词、数字和特殊符号(~!@#$%^&*()_+)组成
- 实现 GetCount(string container, string target) 办法,能够统计出 container 中有多少个 target
- 不应用 string 自带的 Join() 办法,定义一个 mimicJoin() 办法,能将若干字符串用指定的分隔符连接起来,比方:mimicJoin(“-“,”a”,”b”,”c”,”d”),其运行后果为:a-b-c-d