咱们之前成心对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