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

1 类型基础

面试出现频率:基本上确定出现css

重要程度:10/10,身家性命般重要。一般这也是各类招聘工做的第一个要求,即“熟悉C#”的一部分。连这部分都不清楚的人,能够说根本不知道本身天天都在干什么。咱们每天使用C#写程序,但若是连C#基础的东西都不懂,怎么证实你“熟悉C#”呢?怎么让人觉的你对C#有兴趣呢?html

不少人去面试一发现面试官开始问基础题,就十分不爽,被淘汰了以后,还写博客说面试官垃圾,怎么不问问项目经历,哥但是作过很多项目的。却不知,面试官知道你作过那些项目,但一般来讲,若是那些项目不是牛逼透顶的级别(例如你参与了淘宝双11致使数据库并发问题的改进,或者AlphaGo的算法设计),或者正好是面试官所在公司须要的类型,则这并非什么很厉害的事情,是个程序员就有几个项目在身,“作过很多项目”的牛逼程度,差很少等于“活过20几年”(我都活了20几年了,我牛逼么?)。每一个人都有的东西,有什么好问的,问你了你能肯定你能答得比别人好么?可是若是你不能答出什么是装箱,你会引起面试官如下的猜测:前端

  • 这人连最基础的东西都不知道,还写了熟悉C#,他还写了熟悉XX,熟悉YY,看来他对那些东西可能也就了解皮毛。呵呵他还说他懂设计模式
  • 这人连最基础的东西都不知道,说明他日常不看书。连书都不看,对技术确定没有什么兴趣
  • 这人写了他作过20个项目,但在我看来,他们大同小异,和作过1个项目也没区别
  • 这人还写了他有管理经验,本身都这样,小弟的水平。。。

最重要的是,若是你装箱都不知道,面试官后面的N个连环问题立刻胎死腹中,他可能会一脸尴尬,由于“我只是用这个问题当破冰的啊,你怎么已经倒地了”,甚至不知道该问你啥,你才知道。C#话题就此终结,和蔼点的面试官,可能会问问你在简历上写的其余东西。但不管如何,你的价值已经狂跌了不止一个档次。jquery

在老外看来,这部份内容更为重要。不少老外,尤为是从优越国家来的那帮人,认为人找工做确定是为了兴趣,钱只不过是顺带得到的。他们根本不知道这世界上还有人会考虑一个自身毫无兴趣,仅仅是工资高的工做。若是他们发现,你连装箱都不知道是什么,他们会以为你不熟悉C#,对C#一点兴趣都没有,直接把你请出面试室,尽管你可能已经用C#写了几十个工程,手下可能已经有了几个小弟。也许你会央求面试官转换一个话题,例如问问设计模式,但我的认为,基础有问题的人,即便知道设计模式,作过不少项目,他写出来的asp.net代码多是一坨屎的概率要远远高于基础没问题,但彻底不懂asp.net的人。这也是为何不少老外的C#书籍前几章的内容好像都是些“毫无心义的”,“莫名其妙的”东西。CLR via C#更是其中的战斗机,你彻底不用看这本书,也能写出一个后台用asp.net MVC,前端html+css+jquery的ERP系统出来,先后端使用ajax通信,后端连数据库,用sql查数据,作CRUD。可是你不能凭借这个找到一个工资高的工做,由于会干这个的人实在是太多了,以致于不够值钱。若是你以为这已是了不得的成就,那么你这一辈子也就停留在这里了。若是你还想挣得更多,那么你就得会别人不会或者嗤之以鼻的东西。程序员

而工资高的工做,或对性能有很高的要求,或若是你写的代码太差,那真的会出大事,因此不能请基础差的人(固然好公司会有层层环境把关,但若是你的代码老出问题,你的水平弱于公司平均水平太多,他们也不会请你)。小公司尤为是外包,或者没什么名气的公司写的产品,自己也没有多少人用,崩溃了不会死人,因此代码垃圾一点无妨,只要能按时完成任务就得。面试

不少人反感基础题,一个很大的缘由在于,问问题的人不会问。若是问法是考定义,好比问“值类型与引用类型有何区别?” 这种问题的答案一查都找获得,也没有什么意义。较好的问法是,把概念问题融入到情景之中,或者构造一个连环问题。例如我遇到过的一个问题:你什么时候会考虑使用一个结构体?我以为一个不错的答案是”当这个对象全部的属性都是值类型时,例如刻画N维坐标系上的一个点”。若是面试者是如此做答,那么你能够继续问“能够用类型么?“这个时候,实际上仍是在问你值类型与引用类型有何区别,但相比直接问就天然不少。这个问题并非概念题,而是天天工做都会要遇到的。你总须要创建自定义的对象吧,那你就得从类型,结构,接口...中选择一个。ajax

须要理解的程度:熟悉值类型和引用类型的区别,以及它们之间是能够转换的(虽然这种转换基本上是必定要避免的)。对栈和堆上内存的活动有着清醒的认识。算法

参考资料:sql

  • http://www.cnblogs.com/anding/p/5229756.html
  • CLR via C#

1.1 公共类型系统(CTS)

公共类型系统(CTS)是用来描述IL的,它规定了IL能作什么,能定义什么样的变量,类中容许拥有什么成员等等。若是你写了一个不遵循CTS的语言(以及一个编译器),那么你的语言不能被当作是.NET平台的语言,编译出来的中间代码(若是有的话)不是IL。CTS和IL是全部.NET语言的爸爸。数据库

C#的数据类型能够分为值类型和引用类型。这是由于,CTS爸爸规定数据类型能够分为值类型和引用类型,并且C#实现了这部分功能。你能够开发一个遵循CTS的语言,但不实现任何值类型。

全部类型都从System.Object派生,接口是一个特例。下面是一些主要的System.Object提供的方法:

  • Equals(obj):虚方法。若是两个对象具备相同的引用就返回true。
    • 注意,尽管引用类型可能包含许多成员,比较引用类型时,仅仅考虑栈上的两个对象是否指向堆上相同的对象,而不会逐个成员比较,因此对于引用类型,不须要重写该方法。
    • System.ValueType(值类型)重写了该方法,使得方法不比较对象指针是否指向同一个对象,而是仅仅比较值是否相等。此时,若是值类型包含不少成员(例如结构),会使用反射逐个成员比较。为了避开反射形成的性能损失,你必须重写该方法,你只须要在其中遍历全部结构的属性,并一一进行比较便可。若是你自定义的结构的相等逻辑不要求全部的属性相等才意味着相等,而只是部分属性相等就意味着相等时,你也须要重写该方法。
    • 值得注意的是,虽然字符串是引用类型,它也重写了该方法,其行为和值类型同样。
  • Equals(obj1, obj2):静态方法,若两个输入变量均为null则返回true。若仅有一个是null则返回false。若都不是null则调用obj1.Equals(obj2)。故该方法无需重写,也不是虚方法。
  • GetHashCode:在FCL中,任何对象的任何实例都对应一个哈希码。为此,System.Object的虚方法GetHashCode能获取任意对象的哈希码。若是你定义的一个类型重写了Equal方法,那么还应重写GetHashCode方法。事实上若是你没有这么作的话,编译器会报告一条警告消息:重写了Equal但不重写GetHashCode。CLR via C#中说,通常都要重写Object的GetHashCode方法,由于它的算法性能不高。但我对这一部分没有深刻研究。
  • ToString:虚方法。返回类型的完整名称(this.GetType().FullName)。重写它的可能性很大,例如你但愿ToString遍历对象的全部属性,打印出它全部属性的值。
  • GetType:返回对象的类型对象指针指向的类型对象。
  • Finalize:在GC决定回收这个对象以后,会调用这个方法。若是要作一些额外的例如回收对象的非托管属性或对象,应当重写这个方法。只有在存在非托管对象时才须要这么作。在垃圾回收中会详细介绍。

1.2 New操做符

CLR要求全部对象都用new操做符来建立。对于值类型,你能够直接赋值,这至关于隐式的调用了new操做符。new操做符所作的事情有:

  1. 计算类型及其全部基类型中定义的实例字段须要的字节数,另外,若是是引用类型,还须要预留空间给”类型对象指针“和”同步块索引“。若是发现栈或者堆上的空间不足,就引起OutOfMemory异常,并激发一次垃圾回收。
  2. 若是是引用类型,从堆上分配第一步算出来的字节数。
  3. 初始化”类型对象指针“和”同步块索引“。令”类型对象指针“指向堆上该类型的类型对象。若是类型对象不存在,则建立一个。而且若是类型有静态成员,则初始化它们,若是类型有静态构造函数,调用静态构造函数,初始化或者修改(由于静态构造函数在初始化静态成员以后进行,因此可能会形成修改)类中的静态成员的值。若是类型对象已经存在,则不会再次调用静态构造函数。
  4. 调用类型的实例初始化器,初始化类型的非静态成员。

例以下面的代码中,C#首先将a初始化为5,而后再修改为10。

1     class SomeType
2     {
3         private static int a = 5;
4 
5         static SomeType()
6         {
7             a = 10;
8         }
9     }
View Code

 

1.2(转) CLR via C#上的例子

 CLR via C#上的这个例子可让咱们透彻理解前一小节的内容以及内存中的各类活动。假设咱们有以下的定义。

若是代码以下图左下角所示,则开始执行的时刻,内存中的状况以下图:

当CLR扫描完M3方法以后,发现有两个引用类型Employee和Manager,故计算这两个类型及其全部基类型中定义的全部实例字段须要的字节数,在堆上创建两个类型对象,它们的构造相同:类型对象指针(TypeHandle),同步块索引,静态字段集合与方法表(储存了全部的方法)。

由于程序还没运行到第二行,因此栈上暂时尚未那个整型对象year。当运行完前2行时,栈中多了2个成员。一个Employee对象e被建立,但其没有指向任何东西。

但运行完第三行后,new关键字在堆上新建了一个实例,并返回这个引用,使得e指向一个Manager实例,这个实例的类型对象指针指向Manager类型对象。注意,一个类型不管有多少个实例,它们在堆中的对象都指向一个类型对象。另外须要关注的是,静态字段在类型对象中,而类型对象是惟一的,因此全部该类型的实例都指向一个类型对象,意味着一个实例更改了静态字段的值,全部其余实例都会受影响。

 

第四句调用了静态方法lookup。假设结果代表,Joe是公司的一名经理,则该方法将返回一个Manager对象。此时堆中将再次建立一个新的Manager对象,而e将会被指向这个新的对象。这个新的对象将会被初始化,Joe将做为其初始化的信息的一部分(再也不是默认的值,例如0或者Null)。

注意此时第一个Manager对象将会变成垃圾,等待垃圾回收器的回收。两个Manager对象指向一个Manager类型对象。

第五句代码将调用一个Employee类型的方法,假设返回5,那么year的值将变成5。

最后一句是一个虚方法,执行虚方法时,和实方法不一样。咱们要看虚方法有没有被人重写,还要根据调用虚方法的对象(e)肯定使用父类中的方法,仍是子类中重写的方法。根据上图发现,e实际上是一个指向Manager对象的东西,因而,咱们执行在Manager类中重写的那个方法。

注意若是在第四句中,Joe仅仅是一个Employee而不是Manager的话,那么堆中将不会有第二个Manager对象,而取而代之为一个新的Employee对象。最后一句也会执行在Employee中的方法,而不是Manager中的方法。

1.3 类型对象

一个类型不管有多少个实例,它们在堆中的对象的类型对象指针都指向同一个类型对象。之因此只有一个类型对象,是由于不须要有多于一个(全部相同类型的定义都相同,都有相同的方法表)。因此,类型对象是储存类型静态成员最恰当的地方。类型对象由CLR在堆中的一个特殊地方(加载堆)建立(在第一次使用前),其中包括了类型的静态字段和方法表。建立完以后,就不会改变,经过这个事实,能够验证静态字段的全局(被全部同类型的实例共享)性。

类型对象是反射的重要操做对象。若是你要处理一个谜之对象,你不知道他有什么方法,那么你只能经过访问它的类型对象,你才知道这个谜通常的对象究竟包括什么方法。而后你就能够调用这些方法。GetType方法会返回对象指向的类型对象(包括静态成员和方法表)。

加载堆不受GC控制,因此静态字段和属性也不受GC控制。

1 int a = 123;                                          // 建立int类型实例a
2 int b = 20;                                           // 建立int类型实例b
3 var atype = a.GetType();                        // 获取对象实例a的类型Type
4 var btype = b.GetType();                        // 获取对象实例b的类型Type
5 Console.WriteLine(System.Object.Equals(atype,btype));           //输出:True
6 Console.WriteLine(System.Object.ReferenceEquals(atype, btype)); //输出:True
View Code


这意味着,内存中只有一个Int32类型对象,不然ReferenceEquals是不可能输出True的。

注意,类型对象也有类型对象指针,这是由于类型对象本质上也是对象。全部的类型对象的“类型对象指针”都指向System.Type类型对象。特别的,System.Type类型对象自己也是一个对象,内部的“类型对象指针”指向它本身。

1.4 什么是基元类型?

属于BCL而非任何某个语言的类型叫作基元类型(Primitive Type)。你能够在mscorlib.dll中找到它们。例如:

IL 类型                      C# 关键字           VB.NET关键字

System.Byte              byte                   Byte

Sytem.Int16              short                  Short

System.Int64             int                     Integer

特别的,string映射到基元类型String。因此它们并无任何区别。

1.5 值类型与引用类型有何区别?

C#的数据类型能够分为值类型和引用类型,它们的区别主要有:

  1. 全部值类型隐式派生自System.ValueType。该类确保值类型所有分配在栈上(结构体除外,结构体若是含有引用类型,则那部分也会分配在堆上)。全部引用类型隐式派生自System.Object。引用类型初始化在栈和堆上。
  2. 引用类型的初值为null。值类型则是0。由于字符串的初值为null,故字符串为引用类型。由于接口是一种特殊的抽象类,因此接口是引用类型。由于委托是密封类,因此委托是引用类型。
  3. 栈中会有一个变量名和变量类型,指向堆中的对象实例的地址。值类型仅有栈中的变量名和类型,不包括指向实例的指针。
  4. 值类型不能有继承,引用类型则能够。典型的例子是结构体,他是值类型,结构体不能被继承。但结构体里面能够包括引用类型。值类型也能够有本身的方法,例如Int.TryParse方法。但方法是隐式的密封方法。
  5. 值类型的生命周期是其定义域。当值类型离开其定义域后将被马上销毁。引用类型则会进入垃圾回收分代算法。咱们不知道什么时候才会销毁。
  6. 当咱们建立了某个引用类型的实例后,再复制一个新的时,将只会复制指针。例如:

A a = new A();

A a2 = a;

此时在堆中只有一个A的实例,而a和a2都指向它。因此若是咱们更改了a中某个成员的值,a2中相应的成员也会更改。(这称为浅复制,与之对应的深复制则是要逐一复制对象全部成员的值,C#没有深复制的方法,要本身实现)值类型则彻底不一样,复制值类型将进行逐字段的复制,而没有指针参与。因此值类型是相互独立的。更改其中一个对另一个不会有影响。

 

1.6 类和结构的主要区别?结构对象可能分配在堆上吗?什么时候考虑使用结构体?

类和结构是C#两个最主要的研究对象:

  1. 结构是值类型,它继承自System.ValueType,而类是引用类型。
  2. 由于值类型不能被继承,故结构不能被继承。
  3. 结构能够有本身的方法,一个典型的例子为.NET中的结构体Int32含有方法Parse,TryParse等等。
  4. 结构能够实现接口。
  5. 虽然结构是值类型,这不意味着结构中不能包括引用类型(但若是一个结构里面包含引用类型,考虑使用类)。结构体若是含有引用类型,则那部分也会分配在堆上。
  6. 结构体的构造函数必须初始化它的全部成员。结构的构造函数不会被自动调用。

当试图表现例如点(X维坐标上的),形状(长,宽,面积等属性)等所有为值类型组成的对象时,考虑使用结构体。例如,若是声明一个 1000 个 Point 对象组成的数组,为了引用每一个对象,则需分配更多内存(堆上的1000个实例);这种状况下,使用结构能够节约资源。当数组不用时,若是是使用结构体,则1000个对象将立刻销毁,若是是使用类,则还要等GC,无形中提高了GC压力。

1.6.1 在.NET的基础类库中,举出一个是类和一个是结构的例子

Console是一个类。

Int32是一个结构。其只含有两个常数的,Int32类型的字段(最小值和最大值),和若干方法。

这二者均位于基础类库mscorlib中。

 

1.6.2 实例构造函数(类型)

类型的实例构造函数不能被继承。它负责将类型的实例字段初始化。对于静态字段,由静态构造函数负责。

若是类型没有定义任何构造函数,则编译器将定义一个没有参数的构造函数。其会简单地调用基类的无参构造函数。特别的,因为System.Object没有任何实例字段,因此它的构造函数什么也不作。

能够声明多个不一样的构造函数。能够利用this关键字来调用其它构造函数。

 

1.6.3 实例构造函数(结构)

结构体的构造函数必须初始化它的全部成员。结构的构造函数不会被自动调用。

不能显式地为结构声明无参数的构造函数。

 

1.6.4 静态构造函数

静态构造函数是一个特殊的构造函数,它会在这个类型第一次被实例化引用任何静态成员以前,CLR在堆上建立类型对象时执行,它具备如下特色:

  1. 静态构造函数既没有访问修饰符,也没有参数
  2. 在建立第一个实例或引用任何静态成员以前,将自动调用静态构造函数来初始化类(的类型对象)。这个静态构造函数只会执行一次。
  3. 没法直接调用静态构造函数。它的访问修饰符是private(不须要写明)。
  4. 在程序中,用户没法控制什么时候执行静态构造函数。
  5. 静态构造函数不该该调用基类型的静态构造函数。这是由于类型不可能有静态字段是从基类型分享或继承的。

若是咱们不了解堆上的内存分配方式,对静态构造函数的理解会十分困难。为何是在建立第一个实例以前?为何不能直接调用?为何不能有参数?咱们彻底没法理解,只能经过死记硬背的方式记住这些性质。但若是你知道静态成员在类型对象中,并不存在于任何的实例中,可能你就会理解这些性质。

当咱们清楚的了解了类型对象以及CLR对类型对象的处理方式时,理解静态构造函数以及类型的静态成员就显得十分天然了。当建立第一个实例以前,堆上没有类型对象,因此要调用静态构造函数,当引用静态成员以前,堆上也没有类型对象,而静态成员属于类型对象,因此也要调用静态构造函数,这两种状况的最终结果,都是堆上最终出现了一个类型对象。由于类型对象只须要创建一次,因此这个静态构造函数也只能运行一次。

为何静态构造函数既没有访问修饰符,也没有参数?这是由于静态构造函数只负责初始化静态成员,只负责维护类型对象,它和类型的实例对象没有关系,因此你加入任何参数(你试图为非静态的字段或属性赋值?这是不可能的,由于根本就没有实例)都是没有意义的。

没法直接调用静态构造函数:如今显然十分容易理解了,由于类型对象只能有一个,若是能够随便被调用,则可能会创造出好几个类型对象,破坏静态字段的全局性。CLR也选择不把控制权交给用户。

相关文章
相关标签/搜索