面试出现频率:主要考察装箱和拆箱。对于有笔试题的场合也可能会考一些基本的类型转换是否合法。html
重要程度:10/10面试
CLR最重要的特性之一就是类型安全性。在运行时,CLR老是知道一个对象是什么类型。对于基元类型之间的相互转换,能够显式或者隐式执行,例如将一个int转换为long。但若是将精度较大的类型转化为精度较小的类型,必须显式执行,且可能会丢失精度,但不会发生异常。能够利用checked关键字强制掷出OverflowException异常。c#
CLR容许将一个对象转化为它的任何基类型。C#不要求任何特殊语法便可将一个对象转换为它的任何基类型。然而,将对象转换为它的某个派生类型时,C#要求开发人员只能进行显式转换,由于这样的转换可能在运行时失败。安全
对基元类型进行转换时,能够显式或者隐式执行。若是遇到丢失精度的状况,C#将会向下取整(即不管如何都是舍去)。例如,对int的最大值转换为byte,将会获得255。对一个小数位精度较高的数转化为小数位精度较低的数,则简单的舍去多余的小数位。ide
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关键字不多用到。性能
能够将一个对象转化为它的任何基类型。转换时,将等号右边的和左边的类型进行比较。若是左边的是基类,则安全,不然发生编译时异常,必须进行显式转换。例如object a = new Manager能够读为:Manager是一个object,因此这个(隐式)转换是安全的。但反过来就错误。显式转换永远发生运行时而不是编译时异常。测试
例以下面的测试题,假定有以下的定义:优化
1 public class B 2 { 3 4 } 5 6 public class D : B 7 { 8 9 }
回答下面每一行代码是能够执行,仍是形成编译时错误,或运行时错误:ui
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();
在执行完上面四句话以后,内存中的情况如图:
若是你执行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();
编译时错误。缘由同上。在执行完上面全部语句以后,内存中的情况如图(省略了类型对象指针):
B b4 = d1;
能够执行由于左边的B是基类,d1是派生类D。
D d3 = b2;
编译时错误。左边的是派生类,而b2的类型是B(在栈上的类型)。
D d4 = (D) d1;
能够执行。由于d1也是D类型,故没有发生实际转换。在执行完上面全部语句以后,内存中的情况如图(省略了类型对象指针):
D d6 = (D) b1;
运行时错误。在显式转换中,b1的类型是B,不能转换为其派生类D。经过显式转换永远不会发生编译时错误。
B b5 = (B) o1;
运行时错误。在显式转换中,o1的类型是基类Object,不能转换为其派生类B。
拆箱与装箱就是值类型与引用类型的转换,其是值类型和引用类型之间的桥梁。之因此能够这样转换是由于C#全部类型都源自Object(全部值类型都源于ValueType,而ValueType源于Object)。经过深刻了解拆箱和装箱的过程,咱们能够知道其包含了对堆上内存的操做,故其会消耗性能,由于这是彻底没必要要的。当了解了新建对象时内存的活动以后,装箱的内存活动就能够很容易的推断出来。
对于简单的例子来讲:
1 int x = 1023; 2 object o = x; //装箱
执行完第一句后,托管堆没有任何东西,栈上有一个整形变量。第二句就是装箱。由于object是一个引用类型,它必须指向堆上的某个对象,而x是值类型,没有堆上的对应对象。因此须要使用装箱,在堆上创造一个x。装箱包括了如下的步骤:
注意,不须要初始化int的类型对象,由于其在执行程序以前,编译以后,就已经被CLR初始化了。
拆箱并非把装箱的过程倒过来,拆箱的代价比装箱低得多。拆箱不须要额外分配内存。
1 int i = 1; 2 object o = i; 3 var j = (byte) o;
拆箱包括了如下的步骤:
一般避免无谓的装箱和拆箱,能够经过使用泛型,令对象成为强类型,从而也就没有了转换类型的可能。也能够经过IL工具,观察代码的IL形式,检查是否有关键字box和unbox。
可使用is或as关键字进行类型转换。
is将检测一个对象是否兼容于指定的类型,并返回一个bool。它永远不会抛出异常。若是转型对象是null,就返回false。典型的应用is进行类型转换的方式为:
1 object o = new object(); 2 class A 3 { 4 5 } 6 7 if (o is A) //执行第一次类型兼容检查 8 { 9 A a = (A) o; //执行第二次类型兼容检查 10 }
因为is实际上会形成两次类型兼容检查,这是没必要要的。as关键字在必定程度上,能够改善性能。as永远不会抛出异常,若是转型对象是null,就返回null。典型的应用as进行类型转换的方式为:
1 object o = new object(); 2 class B 3 { 4 } 5 B b = o as B; //执行一次类型兼容检查 6 if (b != null) 7 { 8 MessageBox.Show("b is B's instance."); 9 }
面试出现频率:基本上确定出现。特别是对字符串相加的性能问题的考察(由于也没有什么其余好问的)。若是你指出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 }
字符串的行为很像值类型:
咱们考虑将N个字符串链接起来的场景。在N极少时(小于8左右),StringBuilder的性能并不必定优于简单的使用+运算符。因此此时,咱们不须要使用StringBuilder。
当N很大(例如超过100)时,StringBuilder的效能大大优于使用+运算符。
当N很大,但你知道N的肯定数值时,考虑使用String.Concat方法。这个方法的速度之因此快,主要有如下缘由:
不过,若是你能够肯定最终字符串长度的值,并将其做为初始长度分配给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
字符串的不变性指的是字符串一经赋值,其值就不能被更改。当使用代码将字符串变量等于一个新的值时,堆上会出现一个新的字符串,而后栈上的变量指向该新字符串。没有任何办法更改原来字符串的值。
有时咱们不得不处理这样的状况,例如从WPF应用的某个文本框中得到一个值,并将其转换为整数。以int为例,其提供了两个静态方法Parse和TryParse。当转换失败时,Parse会掷出异常,使用Parse的异常处理比较麻烦:
1 int quantity; 2 try 3 { 4 quantity = int.Parse(txtQuantity.Text); 5 } 6 catch (FormatException) 7 { 8 quantity = 0; 9 } 10 catch (OverflowException) 11 { 12 quantity = 0; 13 }
而TryParse不会引起异常,它会返回一个bool值提示转换是否成功:
1 int quantity; 2 if (int.TryParse(txtQuantity.Text, out quantity) == false) 3 { 4 quantity = 0; 5 }
代码变得十分简单易懂。固然,直接使用显式转换也是一种方法。显式转换和TryParse并无显著的性能区别。
历来没有人问过我关于这方面的问题,我也是不久以前才学到的。简单来讲,字符串驻留是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管辖,因此要到程序结束才会释放。