今天参加了一场面试,其中有几个关于c#中string的题目我觉得很有意思,这里拿出来分享一下。
题目0,热个身
题目内容:
在C#中,对一个string str赋值,str = null
和str = ""
有什么区别?
这个问题很简单,我们都知道在C#中,string是一个特殊的自带类,它有着类似于struct的性质,但实际上是一个引用类型,这也是为什么可以赋值为null的原因。
那这两个有什么区别呢?
当然是一个是null
一个是空字符串啦(逃
说白了,null这个值就是个空引用,如果用它来调各种各样的string中的成员方法肯定会抛出空引用的错误,而空字符串它也是一个空字符串,它只是长度为0,还是可以用它来调用方法的。
当然,最好的办法是测试:
其实这个问题在微软官方文档中也有解释:String文档,并且还给出了建议的判空方式,也就是使用IsNullOrEmpty
或IsNullOrWhiteSpace
。
题目1,关于C#中String的编码格式
题目内容:
在c#中有这样一段代码,请问最后输出的内容是什么?
string str = "abcdefg某某某";
byte[] strArr = System.Text.Encoding.Default.GetBytes(str);
Console.WriteLine(str.Length);
Console.WriteLine(strArr.Length);
这个题目考察的内容很明确,就是考察对编码的一点基本认识,只要了解C#中string的编码就能得出答案。
不过因为很久没有注意这个问题,我只隐约记得似乎以前初学的时候见过这样的说法:
c#中一个汉字使用两个字节表示
于是我在做题的时候,写上了10和13,分别是string中字符的个数和编码的字节个数。
回来以后我始终不太确定答案,于是建了一个控制台项目进行了测试,结果令我感到奇怪——
显示的结果竟然是10和16!
看到16的时候我第一反应是,这个长度正好是单个字符 * 7 + 汉字字符 * 3 ,也就是1*7+3*3=16,这个长度大概率是UTF-8,于是我加了一行代码:
string str = "abcdefg某某某";
byte[] strArr = System.Text.Encoding.Default.GetBytes(str);
Console.WriteLine(str.Length);
Console.WriteLine(strArr.Length);
Console.WriteLine(System.Text.Encoding.Default.BodyName); //新增的一行代码
结果果然不出我所料,输出了UTF-8!再确认了一下某字在UTF-8中的编码:
嗯,没错了。
难道真的是我写错了?于是我又上网看了一些其他人的测试,果然也有人说是一个汉字两个字符。
脑筋一转,为了确认一下是不是编码问题,我去查了一下GB2312中“某”字的编码,确认确实是两个字节:
于是我怀疑是自己的PC设置有问题,检查了一下系统语言,也并没有勾选unicode编码,说明系统还是用的GB2312,那为什么我的程序里会用这个呢?
本着原因可能一下找不到的心态,我想先试试直接使用GB2312来检查一下长度,于是我在微软的官方文档中找到了GB2312的代码页:936
但是在运行的时候直接报错了:
简单翻译一下,这里提示说不包含代码页为936的编码数据,这下我更奇怪了!
思考了一下又想到另一个可能——我是不是有可能用的是.NET Core的控制台,而不是.NET Framework!因为.NET Core是为多平台准备的,很可能默认使用的就是UTF-8!这两个平台的具体区别我就不展开讲了,有兴趣的可以自己去查查。
接下来我检查了一下我VS的设置,果然默认是.NET Core的,翻了翻,终于在很后面找到了.NET Framework的控制台程序:
我怀着强烈的激动建了一个.NET Framework的项目,然后把最开始一样的代码放到了这里:
果然!结果是13!
不过这一样来又出现了另外一个问题,.NET Core和.NET Framework使用了不一样的默认编码,那么mono作为一个多平台的环境,是不是也不一样呢?
——老规矩,一样的代码在unity里再跑一次:
结果来了,mono和.NET Core不一样,还是支持GB2312的,但是它默认使用的Encoding却是UTF-8。
题目1的正确答案
所以上面这个面试题的正确答案是什么呢?
前提:没有手动修改过Default的Encoding。
可能性1 :在.NET Framework平台的项目中,是GB2312编码,它使用两个字节表示一个汉字,答案是 10 13;
可能性2 :在.NET Core和Unity平台的项目中,是UTF-8编码,对于“某”字,它使用三个字节来表示,那么答案是 10 16
题目2,关于C#中new String的问题
题目内容:
在C#中有这样一行代码:
String str = new String("");
它会在内存中生成几个String Object对象?
这个问题我着实反应了好一会儿,因为使用c++这一段时间里一直对构造函数很敏感,并且c++的堆栈处理方式有一些区别,看到new关键字的时候也会特别注意。
话扯得有点远,让我们还是来看代码。
籍由C#的优秀关键字提醒,我们可以看到在通过这种方式创建一个string的时候,实际上调用的是String(ReadOnlySpan<char> value)
这个构造函数,那么这个泛型ReadOnlySpan<T>
又是个什么呢?
这时候就要请出官方文档了:
文档说得很清楚,简单来说,使用这个泛型构造的对象只能存在于栈
上,而不能出现在托管堆
上(堆和栈的区别这里就不展开了,这算是非常重要的基础知识)。
当然,更重要的性质在这个ref结构
关键字上:
一定要仔细看,这个ref结构
很关键!
在这几条内容下,将这种类型的使用限制得非常死,这样的类只要使用了,那么就必然会跟随当前的栈被清掉,甚至也无法被制作成闭包!这基本可以算是一个彻底的临时变量。
那么回到我们的题目上来,在使用new String("")
这种方式来构造一个string的时候,string还是会被放在堆里,但是它会在栈上生成一个ReadOnlySpan<char>
类型的临时变量,然后再根据这个临时变量来在堆中创建一个string。
在此之外还有另外几个来自书籍 CLR via C# 的知识点:
- 在CLR的机制中,对string有一个“留用”机制,也就是说,如果发现某个字符串曾经被创建过,那么在之后再次遇到这个字符串的时候会再使用它,而不创建一个新的StringObject。
- 在C#的早期版本中,是不支持非unsafe模式下
new String
这种形式创建string的,但是之后放开了,如果不可以使用new的形式,而由系统提供,那么可以很大程度上利用在堆中的StringObject。 - 对于字面值字符串,也就是我们直接写入的
"aaa"
这种形式的字符串,C#在编译时只会写入一次,而在后面直接调用这个字符串。 - 对于上面所说的“留用”机制,也是可以关闭的,也就是说即使使用直接赋值的形式,也会直接创建不同的StringObject,但默认都是开启的。
上面说了这么多,到底使用new的形式跟我们直接使用string str = ""
有什么区别呢?区别有下面点:
- 使用new方式会创建一个新的string,不会管这个string是否在内存中已经存在,也就是说必然会创建一个新的,而使用直接赋值的形式会重用堆中已经存在的StringObject;
- 使用new方式创建的对象,很可能不会被重用——举个例子,假设我要创建一个字符串
aaa
,即使之前没有使用过aaa
,在下一次使用字面值字符串的形式对一个string赋值的时候,得到的这个StringObject与之前的那个StringObject也不是同一个。 - 使用new方式创建时,还会额外创建一个
ReadOnlySpan<char>
类型的临时变量。
最后,还是来做一下测试:
string str_1 = "a";
String str_2 = new String(str_1);
String str_3 = str_1;
String str_4 = "a";
String str_5 = new String("a");
Console.WriteLine("str_1 - str_2 \t" + ReferenceEquals(str_1,str_2));
Console.WriteLine("str_1 - str_3 \t" + ReferenceEquals(str_1,str_3));
Console.WriteLine("str_1 - str_4 \t" + ReferenceEquals(str_1,str_4));
Console.WriteLine("str_1 - str_5 \t" + ReferenceEquals(str_1,str_5));
Console.WriteLine("str_2 - str_5 \t" + ReferenceEquals(str_2,str_5));
内容很简单:
str_1是一个字面值字符串赋值的字符串
str_2是使用str_1使用new形式创建的一个字符串
str_3是直接使用str_1来赋值的字符串
str_4是完全跟str_1一样使用字面值字符串赋值的字符串
str_5是一个使用字面值字符串来new的字符串
然后设计了五组对比:
第一组对比是str_1与str_2,这一组对比是为了证明在托管堆中,使用new形式会创建一个新的StringObject,我们的预期结果是false;
第二组对比是str_1与str_3,这一组是为了证明使用字符串赋值时,在托管堆中实际上使用的是同一个SringObject,预期结果是true;
第三组对比是str_1与str_4,这一组是为了证明在托管堆中,存在“留用”的机制预期结果是true;
第四组对比是str_1与str_5,这一组对比是为了证明在托管堆中,即使是使用字符串字面值来new,也会创建一个新的StringObject,我们的预期结果是false;
第五组对比是str_2与str_5,这一组是为了不同次数的new形式之间也会生成不同的StringObject,我们的预期结果是false;
开始我们的测试:
与我们的预期一致。
题目2的正确答案
使用new来创建,会在内存中生成一个新的StringObject对象,但是同时,会在栈上生成一个ReadOnlySpan<char>
类型的临时变量。