(转).NET面试题系列[4] - C# 基础知识(2)

2 类型转换

面试出现频率:主要考察装箱和拆箱。对于有笔试题的场合也可能会考一些基本的类型转换是否合法。

重要程度:10/10

CLR最重要的特性之一就是类型安全性。在运行时,CLR总是知道一个对象是什么类型。对于基元类型之间的相互转换,可以显式或者隐式执行,例如将一个int转换为long。但如果将精度较大的类型转化为精度较小的类型,必须显式执行,且可能会丢失精度,但不会发生异常。可以利用checked关键字强制掷出OverflowException异常。

CLR允许将一个对象转化为它的任何基类型。C#不要求任何特殊语法即可将一个对象转换为它的任何基类型。然而,将对象转换为它的某个派生类型时,C#要求开发人员只能进行显式转换,因为这样的转换可能在运行时失败。

2.1 基元类型的类型转换

对基元类型进行转换时,可以显式或者隐式执行。如果遇到丢失精度的情况,C#将会向下取整(即无论如何都是舍去)。例如,对int的最大值转换为byte,将会得到255。对一个小数位精度较高的数转化为小数位精度较低的数,则简单的舍去多余的小数位。

1             int a = int.MaxValue;
2             Console.WriteLine(a);
3             byte b = (byte) a;             //255

如果去掉(byte),改为隐式执行,则会无法通过编译。可以利用checked关键字检查是否有溢出的情况。

1             checked
2             {
3                 byte b = (byte)a;             //Overflow
4                 Console.WriteLine(a + 1);     //Overflow
5                 Console.WriteLine(b);
6             }

也可以使用unchecked关键字忽略所有的精度和溢出检查。但由于这就是编译器的默认行为,所以unchecked关键字很少用到。

2.2 引用类型之间的类型转换

可以将一个对象转化为它的任何基类型。转换时,将等号右边的和左边的类型进行比较。如果左边的是基类,则安全,否则发生编译时异常,必须进行显式转换。例如object a = new Manager可以读为:Manager是一个object,所以这个(隐式)转换是安全的。但反过来就错误。显式转换永远发生运行时而不是编译时异常。
例如下面的测试题,假定有如下的定义:

    public class B
    {

    }

    public class D : B
    {

    }

回答下面每一行代码是可以执行,还是造成编译时错误,或运行时错误:
Object o1 = new Object();
可以执行。

Object o2 = new B();
可以执行。这将会在栈上新建一个名为o2的对象,类型为Object。他指向堆上的B类型对象。因为Object类型是B的基类,所以类型安全。但由于o2的类型是Object,o2将只拥有Object的那几个方法(你可以自行在IDE中试验一下)。如果你执行Console.WriteLine(o2.GetType()),你会得到[命名空间名称].B,也就是说,GetType返回指向的类型对象的具体类型名称。

Object o3 = new D();
可以执行,原因同上。

Object o4 = o3;
可以执行,可以将其看成Object o4 = new D();

在执行完上面四句话之后,内存中的状况如图:


image

如果你执行Console.WriteLine(object.ReferenceEquals(o3, o4)),会得到true的返回值,因为它们指向同一个实例。我们继续往下看:

B b1 = new B();
可以执行。

B b2 = new D();
可以执行。原因同第二个。

D d1 = new D();
可以执行。

B b3 = new Object();
编译时错误。不能将Object类型转为B。

D d2 = new Object();
编译时错误。原因同上。在执行完上面所有语句之后,内存中的状况如图(省略了类型对象指针)

image

B b4 = d1;
可以执行因为左边的B是基类,d1是派生类D。

D d3 = b2;
编译时错误。左边的是派生类,而b2的类型是B(在栈上的类型)。

D d4 = (D) d1;
可以执行。因为d1也是D类型,故没有发生实际转换。在执行完上面所有语句之后,内存中的状况如图(省略了类型对象指针):


image

D d6 = (D) b1;
运行时错误。在显式转换中,b1的类型是B,不能转换为其派生类D。通过显式转换永远不会发生编译时错误。

B b5 = (B) o1;
运行时错误。在显式转换中,o1的类型是基类Object,不能转换为其派生类B。

2.3 什么是拆箱和装箱?它们对性能的损耗体现在何处?

拆箱与装箱就是值类型与引用类型的转换,其是值类型和引用类型之间的桥梁。之所以可以这样转换是因为C#所有类型都源自Object(所有值类型都源于ValueType,而ValueType源于Object)。通过深入了解拆箱和装箱的过程,我们可以知道其包含了对堆上内存的操作,故其会消耗性能,因为这是完全不必要的。当了解了新建对象时内存的活动之后,装箱的内存活动就可以很容易的推断出来。

装箱的过程
对于简单的例子来说:

1             int x = 1023;
2             object o = x; //装箱

执行完第一句后,托管堆没有任何东西,栈上有一个整形变量。第二句就是装箱。因为object是一个引用类型,它必须指向堆上的某个对象,而x是值类型,没有堆上的对应对象。所以需要使用装箱,在堆上创造一个x。装箱包括了以下的步骤:
1.分配内存。这个例子中需要一个整形变量,加上托管堆上所有的对象都有的两个额外成员(类型对象指针和同步块索引)那么多的内存。类型对象指针指向int类型对象。
2.值类型的变量复制到新分配的堆内存。
3.返回对象的地址。
注意,不需要初始化int的类型对象,因为其在执行程序之前,编译之后,就已经被CLR初始化了。

拆箱的过程
拆箱并不是把装箱的过程倒过来,拆箱的代价比装箱低得多。拆箱不需要额外分配内存。

1             int i = 1;            
2             object o = i;
3             var j = (byte) o;

拆箱包括了以下的步骤:
1.如果已装箱实例为null,抛出NullReference异常
2.如果对象不是null但类型不是原先未装箱的值类型,则抛出InvalidCast异常,比如上面的代码
3.获取已装箱实例中值类型字段的地址
4.创建一个新的值类型变量,其值使用第三步获取到的值(复制)
通常避免无谓的装箱和拆箱,可以通过使用泛型,令对象成为强类型,从而也就没有了转换类型的可能。也可以通过IL工具,观察代码的IL形式,检查是否有关键字box和unbox。

2.4 使用is或as关键字进行类型转换

可以使用is或as关键字进行类型转换。

is将检测一个对象是否兼容于指定的类型,并返回一个bool。它永远不会抛出异常。如果转型对象是null,就返回false。典型的应用is进行类型转换的方式为:

object o = new object();
class A
{
 
}

if (o is A)  //执行第一次类型兼容检查
{
  A a = (A) o;  //执行第二次类型兼容检查
}

由于is实际上会造成两次类型兼容检查,这是不必要的。as关键字在一定程度上,可以改善性能。as永远不会抛出异常,如果转型对象是null,就返回null。典型的应用as进行类型转换的方式为:

object o = new object();
class B
{
}
B b = o as B;  //执行一次类型兼容检查
if (b != null)
{  
  MessageBox.Show("b is B's instance.");
}

3. 字符串

面试出现频率:基本上肯定出现。特别是对字符串相加的性能问题的考察(因为也没有什么其他好问的)。如果你指出StringBuilder是一个解决方案,并强调一定要为其设置一个初始容量,面试官将会很高兴。

重要程度:10/10。

字符串是引用类型。可以通过字符串的默认值为null来记忆这点。string是基元类型String在c#中的别名,故这两者没有任何区别。

注意字符串在修改时,是在堆上创建一个新的对象,然后将栈上的字符串指向新的对象(旧的对象变为垃圾等待GC回收)。字符串的值是无法被修改的(具有不变性)。考虑使用StringBuilder来防止建立过多对象,减轻GC压力。

字符串的==操作和.Equal是相同的,因为==已经被重写为比较字符串的值而不是其引用。作为引用类型,==本来是比较引用的,但此时被重写,这也是字符串看起来像值类型的一个原因。

当使用StringBuilder时,如果你大概知道要操作的字符串的长度范围,请指明它的初始长度。这可以避免StringBuilder初始化时不断扩容导致的资源消耗。

你经常会有机会扩展这个类,例如为这个类扩展一个颠倒的字符串方法:

1     public static string Reverse(string s)
2     {
3         char[] charArray = s.ToCharArray();
4         Array.Reverse(charArray);
5         return new string(charArray);
6     }

3.1 字符串和普通的引用类型相比有什么特别的地方吗?

字符串的行为很像值类型:

  • 字符串使用等于号互相比较时,比较的是字符串的值而不是是否指向同一个引用,这和引用类型的比较不同,而和值类型的比较相同。
  • 字符串虽然是引用类型,但如果在某方法中,将字符串传入另一方法,在另一方法内部修改,执行完之后,字符串的值并不会改变,而引用类型无论是按值传递还是引用传递,值都会发生变化。

3.2 关于StringBuilder的性能问题

我们考虑将N个字符串连接起来的场景。在N极少时(小于8左右),StringBuilder的性能并不一定优于简单的使用+运算符。所以此时,我们不需要使用StringBuilder。

当N很大(例如超过100)时,StringBuilder的效能大大优于使用+运算符。

当N很大,但你知道N的确定数值时,考虑使用****String.Concat****方法。这个方法的速度之所以快,主要有以下原因:

  1. 当N****确定,每个字符串也确定时,最终的字符串长度就确定了。此时,可以一次性为其分配这么大块的内存。而StringBuilder如果没有指明初始长度,或指定了一个较小的长度,则会不断扩容,消耗资源。扩容的动作分为如下几步:在内存中分配一个更大的空间,然后将现有的字符串复制过去(还余下一些空位for further use)
  2. StringBuilder有线程安全的考虑,故会拖慢一点时间

不过,如果你可以确定最终字符串长度的值,并将其作为初始长度分配给StringBuilder,则StringBuilder将不需要扩容,其性能将与String.Concat方法几乎相同(由于还有性能安全的考虑,故会稍微慢一点点)。

参考:
http://blog.zhaojie.me/2009/11/string-concat-perf-1-benchmark.html
http://blog.zhaojie.me/2009/12/string-concat-perf-2-stringbuilder-implementations.html
http://blog.zhaojie.me/2009/12/string-concat-perf-3-profiling-analysis.html

3.3 什么是字符串的不变性?

字符串的不变性指的是字符串一经赋值,其值就不能被更改。当使用代码将字符串变量等于一个新的值时,堆上会出现一个新的字符串,然后栈上的变量指向该新字符串。没有任何办法更改原来字符串的值。

3.4 字符串转换为值类型

有时我们不得不处理这样的情况,例如从WPF应用的某个文本框中获得一个值,并将其转换为整数。以int为例,其提供了两个静态方法Parse和TryParse。当转换失败时,Parse会掷出异常,使用Parse的异常处理比较麻烦:

int quantity;
try
{
    quantity = int.Parse(txtQuantity.Text);
}
catch (FormatException)
{
    quantity = 0;
}
catch (OverflowException)
{
    quantity = 0;
}

而TryParse不会引发异常,它会返回一个bool值提示转换是否成功:

1 int quantity;
2 if (int.TryParse(txtQuantity.Text, out quantity) == false)
3 {
4     quantity = 0;
5 }

代码变得十分简单易懂。当然,直接使用显式转换也是一种方法。显式转换和TryParse并没有显著的性能区别。

3.5 字符串的驻留(interning)

从来没有人问过我关于这方面的问题,我也是不久之前才学到的。简单来说,字符串驻留是CLR的JIT做代码优化时,送给我们的一个小礼物。CLR会维护一个字符串驻留池(内部哈希表),并在新建字符串时,探查是否已经有相同值的字符串存在。只有以下两种情况才会自动探查。

  1. 如果编译器发现已经有相同值的字符串存在,则不新建字符串(在堆上),而是让新旧两字符串变量在栈上指向同一个堆上的字符串值。如果没有则在驻留池中增加一个新的成员。
var s1 = "123";
var s2 = "123";
Console.WriteLine(System.Object.Equals(s1, s2));  //输出 True
Console.WriteLine(System.Object.ReferenceEquals(s1, s2));  //输出 True

这意味着,堆上只有一条字符串“123”(隐式驻留)。如果我们预先知道许多字符串对象都可能有相同的值,就可以利用这点来提高性能。字符串的驻留的另一个体现方式是常量字符串相加的优化。下面例子输出结果也是两个True:

string st1 = "123" + "abc";
string st2 = "123abc";
Console.WriteLine(st1 == st2);
Console.WriteLine(System.Object.ReferenceEquals(st1, st2));

堆上的字符串只有一个 ----“123abc”。下面例子则稍有不同:

string s1 = "123";
string s2 = s1 + "abc";
string s3 = "123abc";
Console.WriteLine(s2 == s3);
Console.WriteLine(System.Object.ReferenceEquals(s2, s3));

第二个布尔值为False,因为变量和常量相加的动作不会被编译器优化。

并非每次新建字符串,或者通过某种方式生成了一条新的字符串时,其都会被驻留。例如,上面例子中,变量字符串和常量字符串相加,就没有触发驻留行为,同理ToString,ToUpper等方法也不会(只有上面两种情况才会)。我们也可以通过访问驻留池来显式留用字符串。我们可以使用方法string.Intern为驻留池新增一个字符串,或者使用方法IsInterned探查字符串是否已经被驻留。

因为变量字符串和常量字符串相加无法利用驻留行为,所以无论我们怎么改进,上面的最后一行总是会输出False。例如:

string s1 = "123";
String.Intern("123abc");
string s2 = s1 + "abc";

string s3 = "123abc";
Console.WriteLine(s2 == s3);
Console.WriteLine(System.Object.ReferenceEquals(s2, s3));

此时s2的创建根本不会搭理驻留池。同理,这样也不行:

string s1 = "123";
String.Intern("123");
string s2 = 123.ToString();
Console.WriteLine(System.Object.ReferenceEquals(s2, s1));

通常来说,字符串驻留只有在常量字符串的分配和相加时才有意义。而且,我们要注意到字符串驻留的一个负面影响:驻留池的内存不受GC管辖,所以要到程序结束才会释放。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容