2 类型转换

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

重要程度:10/10

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

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

2.1 基元类型的类型转换

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

 int a = int.MaxValue;
Console.WriteLine(a);
byte b = (byte) a; //

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

             checked
{
byte b = (byte)a; //Overflow
Console.WriteLine(a + ); //Overflow
Console.WriteLine(b);
}

也可以使用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();

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

如果你执行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。

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

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

装箱的过程

对于简单的例子来说:

 int x = ;
object o = x; //装箱

执行完第一句后,托管堆没有任何东西,栈上有一个整形变量。第二句就是装箱。因为object是一个引用类型,它必须指向堆上的某个对象,而x是值类型,没有堆上的对应对象。所以需要使用装箱,在堆上创造一个x。装箱包括了以下的步骤:

  1. 分配内存。这个例子中需要一个整形变量,加上托管堆上所有的对象都有的两个额外成员(类型对象指针和同步块索引)那么多的内存。类型对象指针指向int类型对象。
  2. 值类型的变量复制到新分配的堆内存。
  3. 返回对象的地址。

注意,不需要初始化int的类型对象,因为其在执行程序之前,编译之后,就已经被CLR初始化了。

拆箱的过程

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

             int i = ;
object o = i;
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初始化时不断扩容导致的资源消耗。

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

     public static string Reverse(string s)
{
char[] charArray = s.ToCharArray();
Array.Reverse(charArray);
return new string(charArray);
}

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

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

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

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 = ;
}
catch (OverflowException)
{
quantity = ;
}

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

 int quantity;
if (int.TryParse(txtQuantity.Text, out quantity) == false)
{
quantity = ;
}

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

3.5 字符串的驻留(interning)

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

1. 如果编译器发现已经有相同值的字符串存在,则不新建字符串(在堆上),而是让新旧两字符串变量在栈上指向同一个堆上的字符串值。如果没有则在驻留池中增加一个新的成员。

var s1 = "";
var s2 = "";
Console.WriteLine(System.Object.Equals(s1, s2)); //输出 True
Console.WriteLine(System.Object.ReferenceEquals(s1, s2)); //输出 True

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

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

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

string s1 = "";
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 = "";
String.Intern("123abc");
string s2 = s1 + "abc"; string s3 = "123abc";
Console.WriteLine(s2 == s3);
Console.WriteLine(System.Object.ReferenceEquals(s2, s3));

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

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

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

.NET面试题系列[4] - C# 基础知识(2)的更多相关文章

  1. .NET面试题系列[3] - C# 基础知识(1)

    1 类型基础 面试出现频率:基本上肯定出现 重要程度:10/10,身家性命般重要.通常这也是各种招聘工作的第一个要求,即“熟悉C#”的一部分.连这部分都不清楚的人,可以说根本不知道自己每天都在干什么. ...

  2. Spring Ioc源码分析系列--Ioc的基础知识准备

    Spring Ioc源码分析系列--Ioc的基础知识准备 本系列文章代码基于Spring Framework 5.2.x Ioc的概念 在Spring里,Ioc的定义为The IoC Containe ...

  3. 【Linux系列】Linux基础知识整理

    Linux操作系统在服务器领域广泛的使用到,作为一个后台开发工程师很有必要了解Linux相关的知识. 本篇日志是我学习Linux过程中的简单记录和总结.本着"理论够用,实践为主"的 ...

  4. 前端系列之CSS基础知识概述

    1.什么是DIV (1).div就是html一个普通标签,进行区域划分.特性:独自占一行.独自不能实现复杂效果.必须结合css样式进行渲染. (2).div通常其是块级元素 (3).div是定义文档中 ...

  5. Objective-C系列总结之基础知识

    //第一个程序示例 #import <Foundation/Foundation.h> int main(int argc,const char * argv[]) { @autorele ...

  6. 玩耍Hibernate系列(一)补充--基础知识

    基本概述: Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,使得java程序员可以随心所欲的使用对象编程思维来操纵数据库,Hibernate可以应用在任何 ...

  7. EJB系列 - 会话Bean基础知识

    本人博客文章网址:https://www.peretang.com/basic-knowledge-of-session-bean/ 什么是会话 有限的时间周期内,客户端和服务器之间的连接 为什么使用 ...

  8. 前端系列之JavaScript基础知识概述

    ​微信公众号:compassblog 欢迎关注,欢迎转发,互相学习,共同进步! 有任何问题,请后台留言联系! 1.什么是JavaScript (1).JavaScript是web上一种功能强大的编程语 ...

  9. 前端系列之HTML基础知识概述

    1.什么是HTML HTML:Hyper Text Markup Language :超文本标记语言. 超文本:功能比普通文本更加强大. 标记语言:使用一组标签对内容进行描述的语言,它不是编程语言. ...

随机推荐

  1. ExtJS 4.2 组件介绍

    目录 1. 介绍 1.1 说明 1.2 组件分类 1.3 组件名称 1.4 组件结构 2. 组件的创建方式 2.1 Ext.create()创建 2.2 xtype创建 1. 介绍 1.1 说明 Ex ...

  2. junit4进行单元测试

    一.前言 提供服务的时候,为了保证服务的正确性,有时候需要编写测试类验证其正确性和可用性.以前的做法都是自己简单写一个控制层,然后在控制层里调用服务并测试,这样做虽然能够达到测试的目的,但是太不专业了 ...

  3. .Net 分布式云平台基础服务建设说明概要

    1)  背景 建设云平台的基础框架,用于支持各类云服务的业务的构建及发展. 2)  基础服务 根据目前对业务的理解和发展方向,总结抽象出以下几个基础服务,如图所示 3)  概要说明 基础服务的发展会根 ...

  4. Android之SharedPreferences数据存储

    一.SharedPreferences保存数据介绍 如果有想要保存的相对较小键值集合,应使用SharedPreferences API.SharedPreferences对象指向包含键值对的文件并提供 ...

  5. SQL*Plus生成html文件

    最近使用SQL*Plus命令生成html文件,遇到一些有意思的知识点,顺便记录一下,方便以后需要的时候而这些知识点又忘记而捉急.好记性不如烂笔头吗! 为什么要用SQL*Plus生成html文件?   ...

  6. TCP/IP之TCP_NODELAY与TCP_CORK

    TCP/IP之Nagle算法与40ms延迟提到了Nagle 算法.这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,使用TCP_NODELAY选项可以禁止Nagl ...

  7. x01.os.22: ubuntu 常用设置

    新组装了个 64 位电脑,i5 CPU,进入 ubuntu 后,又是一通搜索设置,整理如下,以备后用. 安装 .dep 包 sudo dpkg -i [filename.dep] 在 ubuntu 中 ...

  8. babel-loader-presets

    babel-loader的presets的设置有一定的顺序.es2015必须出现在stage-0前面,我记得这是因为es2015是ES6的标准,state-0等是对ES7一些提案的支持, state- ...

  9. 分布式服务注册和发现consul 简要介绍

    Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置.与其他分布式服务注册与发现的方案,Consul的方案更"一站式",内置了服务注册与发现框 架 ...

  10. APP漏洞扫描用地址空间随机化

    APP漏洞扫描用地址空间随机化 前言 我们在前文<APP漏洞扫描器之本地拒绝服务检测详解>了解到阿里聚安全漏洞扫描器有一项静态分析加动态模糊测试的方法来检测的功能,并详细的介绍了它在针对本 ...