.NET面试题解析(04)-类型、方法与继承

作技术是清苦的。一我的,一台机器,相对无言,代码纷飞,bug无情。须梦里挑灯,左思右想,肝血暗耗,板凳坐穿。世界繁华竞逐,而你独钓寒江,看尽千山暮雪,听彻寒更雨歇。——来自《技术人的慰藉html

  常见面试题目:

1. 全部类型都继承System.Object吗?面试

2. 解释virtual、sealed、override和abstract的区别c#

3. 接口和类有什么异同?ide

4. 抽象类和接口有什么区别?使用时有什么须要注意的吗?函数

5. 重载与覆盖的区别?学习

6. 在继承中new和override相同点和区别?看下面的代码,有一个基类A,B1和B2都继承自A,而且使用不一样的方式改变了父类方法Print()的行为。测试代码输出什么?为何?测试

public void DoTest()
{
    B1 b1 = new B1(); B2 b2 = new B2();
    b1.Print(); b2.Print();      //按预期应该输出 B一、B2

    A ab1 = new B1(); A ab2 = new B2();
    ab1.Print(); ab2.Print();   //这里应该输出什么呢?
}
public class A
{
    public virtual void Print() { Console.WriteLine("A"); }
}
public class B1 : A
{
    public override void Print() { Console.WriteLine("B1"); }
}
public class B2 : A
{
    public new void Print() { Console.WriteLine("B2"); }
}

7. 下面代码中,变量a、b都是int类型,代码输出结果是什么?编码

int a = 123;
int b = 20;
var atype = a.GetType();
var btype = b.GetType();
Console.WriteLine(System.Object.Equals(atype,btype));
Console.WriteLine(System.Object.ReferenceEquals(atype,btype));

8.class中定义的静态字段是存储在内存中的哪一个地方?为何会说她不会被GC回收?spa

  类型基础知识梳理

微笑 类型Type简述

经过本系列前面几篇文章,基本了解了值类型和引用类型,及其相互关系。以下图,.NET中主要的类型就是值类型和引用类型,全部类型的基类就是System.Object,也就是说咱们使用FCL提供的各类类型的、自定义的全部类型都最终派生自System.Object,所以他们也都继承了System.Object提供的基本方法。线程

System.Object能够说是.NET中的万物之源,若是非要较真的话,好像只有接口不继承她了。接口是一个特殊的类型,能够理解为接口是普通类型的约束、规范,她不能够实例化。(实际编码中,接口能够用object表示,只是一种语法支持,此见解不知是否准确,欢迎交流)

在.NET代码中,咱们能够很方便的建立各类类型,一个简单的数据模型、复杂的聚合对象类型、或是对客观世界实体的抽象。类 (class) 是最基础的 C# 类型(注意:本文主要探讨的就是引用类型,文中所述类型如没注明都为引用类型),支持继承与多态。一个c# 类Class主要包含两种基本成员:

  • 状态(字段、常量、属性等)
  • 操做(方法、事件、索引器、构造函数等)

利用建立的类型(或者系统提供的),能够很容易的建立对象的实例。使用 new 运算符建立,该运算符为新的实例分配内存,调用构造函数初始化该实例,并返回对该实例的引用,以下面的语法形式:

<类名>  <实例名> = new <类名>([构造函数的参数])

建立后的实例对象,是一个存储在内存上(在线程栈或托管堆上)的一个对象,那能够创造实例的类型在内存中又是一个什么样的存在呢?她就是类型对象(Type Object)

微笑 类型对象(Type Object)

看看下面的代码:

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

任何对象都有一个GetType()方法(基类System.Object提供的),该方法返回一个对象的类型,类型上面包含了对象内部的详细信息,如字段、属性、方法、基类、事件等等(经过反射能够获取)。在上面的代码中两个不一样的int变量的类型(int.GetType())是同一个Type,说明int在内存中有惟一一个(相似静态的)Systen.Int32类型。

上面获取到的Type对象(Systen.Int32)就是一个类型对象,她同其余引用类型同样,也是一个引用对象,这个对象中存储了int32类型的全部信息(类型的全部元数据信息)。

关于类型类型对象(Object Type):

>每个类型(如System.Int32)在内存中都会有一个惟一的类型对象,经过(int)a.GetType()能够获取该对象;

>类型对象(Object Type)存储在内存中一个独立的区域,叫加载堆(Load Heap),加载堆是在进程建立的时候建立的,不受GC垃圾回收管制,所以类型对象一经建立就不会被释放的,他的生命周期从AppDomain建立到结束;

>前问说过,每一个引用对象都包含两个附加成员:TypeHandle和同步索引块,其中TypeHandle就指向该对象对应的类型对象;

>类型对象的加载由class loader负责,在第一次使用前加载;

>类型中的静态字段就是存储在这里的(加载堆上的类型对象),因此说静态字段是全局的,并且不会释放;

能够参考下面的图,第一幅图描述了对象在内存中的一个关系, 第二幅图更复杂,更准确、全面的描述了内存的结构分布。

 图片来源

image

生气 方法表

类型对象内部的主要的结构是怎么样的呢?其中最重要的就是方法表,包含了是类型内部的全部方法入口,关于具体的细节和原理这里很少赘述(太多了,能够参考文末给的参考资料),本文只是初步介绍一下,主要目的是为了解决第6题。

public class A
{
    public virtual void Print() { Console.WriteLine("A"); }
}
public class B1 : A
{
    public override void Print() { Console.WriteLine("B1"); }
}
public class B2 : A
{
    public new void Print() { Console.WriteLine("B2"); }
}

仍是以第6题的代码为例,上面的代码中,定义两个简单的类,一个基类A,,B1和B2继承自A,而后使用不一样的方式改变了父类方法的行为。当定义了b一、b2两个变量后,内存结构示意图以下:

B1 b1 = new B1();
B2 b2 = new B2();

image

方法表的加载

  • 方法表的加载时父类在前子类在后的,首先加载的是固定的4个来自System.Object的虚方法:ToString, Equals, GetHashCode, and Finalize;
  • 而后加载父类A的虚方法;
  • 加载本身的方法;
  • 最后是构造方法:静态构造函数.cctor(),对象构造函数.ctor();

方法表中的方法入口(方法表槽 )还有不少其余的信息,好比会关联方法的IL代码以及对应的本地机器码等。其实类型对象自己也是一个引用类型对象,其内部一样也包含两个附件成员:同步索引块和类型对象指针TypeHandel,具体细节、原理有兴趣的能够本身深刻了解。

方法的调用:当执行代码b1.Print()时(此处只关注方法调用,忽略方法的继承等因素),经过b1的TypeHandel找到对应类型对象,而后找到方法表槽,而后是对应的IL代码,第一次执行的时候,JIT编译器须要把IL代码编译为本地机器码,第一次执行完成后机器码会保留,下一次执行就不须要JIT编译了。这也是为何说.NET程序启动须要预热的缘由。

眨眼 .NET中的继承本质

方法表的建立过程是从父类到子类自上而下的,这是.NET中继承的很好体现,当发现有覆写父类虚方法会覆盖同名的父方法,全部类型的加载都会递归到System.Object类。

  • 继承是可传递的,子类是对父类的扩展,必须继承父类方法,同时能够添加新方法。
  • 子类能够调用父类方法和字段,而父类不能调用子类方法和字段。 
  • 子类不光继承父类的公有成员,也继承了私有成员,只是不可直接访问。
  • new关键字在虚方法继承中的阻断做用,中断某一虚方法的继承传递。

所以类型B一、B2的类型对象进一步的结构示意图以下:

  • 在加载B1类型对象时,当加载override B1.Print(“B1”)时,发现有覆写override的方法,会覆盖父类的同名虚方法Print(“A”),就是下面的示意图,简单来讲就是在B1中Print只有一个实现版本;
  • 加载B2类型对象时,new关键字表示要隐藏基类的虚方法,此时B2中的Print(“B2”)就不是虚方法了,她是B2中的新方法了,简单来讲就是在B2类型对象中Print有2个实现版本;

image

 

B1 b1 = new B1();

B2 b2 = new B2();
b1.Print(); b2.Print();      //按预期应该输出 B一、B2

A ab1 = new B1(); 
A ab2 = new B2();
ab1.Print(); ab2.Print();   //这里应该输出什么呢?

上面代码中红色高亮的两行代码,用基类(A)和用自己B1声明到底有什么区别呢?相似这种代码在实际编码中是很常见的,简单的归纳一下:

  • 不管用什么作引用声明,哪怕是object,等号右边的[ = new 类型()]都是没有区别的,也就说说对象的建立不受影响的,b1和ab1对象在内存结构上是一致的;
  • 他们的的差异就在引用指针的类型不一样,这种不一样在编码中智能提示就直观的反应出来了,在实际方法调用上也与引用指针类型有直接关系;
  • 综合来讲,不一样引用指针类型对于对象的建立(new操做)不影响;但对于对象的使用(如方法调用)有影响,这一点在上面代码的执行结果中体现出来了!

上面调用的IL代码:

image

对于虚方法的调用,在IL中都是使用指令callvirt,该指令主要意思就是具体的方法在运行时动态肯定的:

callvirt使用虚拟调度,也就是根据引用类型的动态类型来调度方法,callvirt指令根据引用变量指向的对象类型来调用方法,在运行时动态绑定,主要用于调用虚方法。

不一样的类型指针在虚拟方法表中有不一样的附加信息做为标志来区别其访问的地址区域,称为offset。不一样类型的指针只能在其特定地址区域内进行执行。编译器在方法调用时还有一个原则:

执行就近原则:对于同名字段或者方法,编译器是按照其顺序查找来引用的,也就是首先访问离它建立最近的字段或者方法。

所以执行如下代码时,引用指针类型的offset指向子类,以下图,,按照就近查找执行原则,正常输出B一、B2

B1 b1 = new B1();

 B2 b2 = new B2();
b1.Print(); b2.Print();      //按预期应该输出 B一、B2

image

而当执行如下代码时,引用指针类型都为父类A,引用指针类型的offset指向父类,以下图,按照就近查找执行原则,输出B一、A。

A ab1 = new B1(); 
A ab2 = new B2(); ab1.Print(); ab2.Print(); //这里应该输出什么呢?

image

  .NET中的继承

大笑 什么是抽象类

抽象类提供多个派生类共享基类的公共定义,它既能够提供抽象方法,也能够提供非抽象方法。抽象类不能实例化,必须经过继承由派生类实现其抽象方法,所以对抽象类不能使用new关键字,也不能被密封。

基本特色:

  • 抽象类使用Abstract声明,抽象方法也是用Abstract标示;
  • 抽象类不能被实例化;
  • 抽象方法必须定义在抽象类中;
  • 抽象类能够继承一个抽象类;
  • 抽象类不能被密封(不能使用sealed);
  • 同类Class同样,只支持单继承;

一个简单的抽象类代码:

public abstract class AbstractUser
{
    public int Age { get; set; }
    public abstract void SetName(string name);
}

IL代码以下,类和方法都使用abstract修饰:

image

大笑 什么是接口?

接口简单理解就是一种规范、契约,使得实现接口的类或结构在形式上保持一致。实现接口的类或结构必须实现接口定义中全部接口成员,以及该接口从其余接口中继承的全部接口成员。

基本特色:

  • 接口使用interface声明;
  • 接口相似于抽象基类,不能直接实例化接口;
  • 接口中的方法都是抽象方法,不能有实现代码,实现接口的任何非抽象类型都必须实现接口的全部成员:
  • 接口成员是自动公开的,且不能包含任何访问修饰符。
  • 接口自身可从多个接口继承,类和结构可继承多个接口,但接口不能继承类。

下面一个简单的接口定义:

public interface IUser
{
    int Age { get; set; }
    void SetName(string name);
}

下面是IUser接口定义的IL代码,看上去是否是和上面的抽象类AbstractUser的IL代码差很少!接口也是使用.Class ~ abstract标记,方法定义同抽象类中的方法同样使用abstract virtual标记。所以能够把接口看作是一种特殊的抽象类,该类只提供定义,没有实现

image

另一个小细节,上面说到接口是一个特殊的类型,不继承System.Object,经过IL代码其实能够证明这一点。不管是自定义的任何类型仍是抽象类,都会隐式继承System.Object,AbstractUser的IL代码中就有“extends [mscorlib]System.Object”,而接口的IL代码并无这一段代码。

大笑 关于继承

关于继承,太概念性了,就不细说了,主要仍是在平时的搬砖过程当中多思考、多总结、多体会。在.NET中继承的主要两种方式就是类继承和接口继承,二者的主要思想是不同的:

  • 类继承强调父子关系,是一个“IS A”的关系,所以只能单继承(就像一我的只能有一个Father);
  • 接口继承强调的是一种规范、约束,是一个“CAN DO”的关系,支持多继承,是实现多态一种重要方式。

更准确的说,类能够叫继承,接口叫“实现”更合适。更多的概念和区别,能够直接看后面的答案,更多的仍是要本身理解。

  题目答案解析:

1. 全部类型都继承System.Object吗?

基本上是的,全部值类型和引用类型都继承自System.Object,接口是一个特殊的类型,不继承自System.Object。

2. 解释virtual、sealed、override和abstract的区别

  • virtual申明虚方法的关键字,说明该方法能够被重写
  • sealed说明该类不可被继承
  • override重写基类的方法
  • abstract申明抽象类和抽象方法的关键字,抽象方法不提供实现,由子类实现,抽象类不可实例化。

3. 接口和类有什么异同?

不一样点:

一、接口不能直接实例化。

二、接口只包含方法或属性的声明,不包含方法的实现。

三、接口能够多继承,类只能单继承。

四、类有分部类的概念,定义可在不一样的源文件之间进行拆分,而接口没有。(这个地方确实不对,接口也能够分部,谢谢@xclin163的指正)

五、表达的含义不一样,接口主要定义一种规范,统一调用方法,也就是规范类,约束类,类是方法功能的实现和集合

相同点:

一、接口、类和结构均可以从多个接口继承。

二、接口相似于抽象基类:继承接口的任何非抽象类型都必须实现接口的全部成员。

三、接口和类均可以包含事件、索引器、方法和属性。

4. 抽象类和接口有什么区别?

一、继承:接口支持多继承;抽象类不能实现多继承。

二、表达的概念:接口用于规范,更强调契约,抽象类用于共性,强调父子。抽象类是一类事物的高度聚合,那么对于继承抽象类的子类来讲,对于抽象类来讲,属于"Is A"的关系;而接口是定义行为规范,强调“Can Do”的关系,所以对于实现接口的子类来讲,相对于接口来讲,是"行为须要按照接口来完成"。

三、方法实现:对抽象类中的方法,便可以给出实现部分,也能够不给出;而接口的方法(抽象规则)都不能给出实现部分,接口中方法不能加修饰符。

四、子类重写:继承类对于二者所涉及方法的实现是不一样的。继承类对于抽象类所定义的抽象方法,能够不用重写,也就是说,能够延用抽象类的方法;而对于接口类所定义的方法或者属性来讲,在继承类中必须重写,给出相应的方法和属性实现。

五、新增方法的影响:在抽象类中,新增一个方法的话,继承类中能够不用做任何处理;而对于接口来讲,则须要修改继承类,提供新定义的方法。

六、接口能够做用于值类型(枚举能够实现接口)和引用类型;抽象类只能做用于引用类型。

七、接口不能包含字段和已实现的方法,接口只包含方法、属性、索引器、事件的签名;抽象类能够定义字段、属性、包含有实现的方法。

5. 重载与覆盖的区别?

重载:当类包含两个名称相同但签名不一样(方法名相同,参数列表不相同)的方法时发生方法重载。用方法重载来提供在语义上完成相同而功能不一样的方法。

覆写:在类的继承中使用,经过覆写子类方法能够改变父类虚方法的实现。

主要区别

一、方法的覆盖是子类和父类之间的关系,是垂直关系;方法的重载是同一个类中方法之间的关系,是水平关系。
二、覆盖只能由一个方法,或只能由一对方法产生关系;方法的重载是多个方法之间的关系。
三、覆盖要求参数列表相同;重载要求参数列表不一样。
四、覆盖关系中,调用那个方法体,是根据对象的类型来决定;重载关系,是根据调用时的实参表与形参表来选择方法体的。

6. 在继承中new和override相同点和区别?看下面的代码,有一个基类A,B1和B2都继承自A,而且使用不一样的方式改变了父类方法Print()的行为。测试代码输出什么?为何?

public void DoTest()
{
    B1 b1 = new B1(); B2 b2 = new B2();
    b1.Print(); b2.Print();      //按预期应该输出 B一、B2

    A ab1 = new B1(); A ab2 = new B2();
    ab1.Print(); ab2.Print();   //这里应该输出什么呢?输出B一、A
}
public class A
{
    public virtual void Print() { Console.WriteLine("A"); }
}
public class B1 : A
{
    public override void Print() { Console.WriteLine("B1"); }
}
public class B2 : A
{
    public new void Print() { Console.WriteLine("B2"); }
}

7. 下面代码中,变量a、b都是int类型,代码输出结果是什么?

int a = 123;
int b = 20;
var atype = a.GetType();
var btype = b.GetType();
Console.WriteLine(System.Object.Equals(atype,btype));          //输出True
Console.WriteLine(System.Object.ReferenceEquals(atype,btype)); //输出True

8.class中定义的静态字段是存储在内存中的哪一个地方?为何会说她不会被GC回收?

随类型对象存储在内存的加载堆上,由于加载堆不受GC管理,其生命周期随AppDomain,不会被GC回收。

 

版权全部,文章来源:http://www.cnblogs.com/anding

我的能力有限,本文内容仅供学习、探讨,欢迎指正、交流。

.NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引

  参考资料:

书籍:CLR via C#

书籍:你必须知道的.NET

Interface继承至System.Object?:http://www.cnblogs.com/whitewolf/archive/2012/05/23/2514123.html

关于CLR内存管理一些深层次的讨论[下篇]

[你必须知道的.NET]第十五回:继承本质论

深刻.NET Framework内部, 看看CLR如何建立运行时对象的

 

后记:本文写的有点难产,可能仍是技术不够熟练,对于文中的“继承中的方法表”那一部分理解的还不够透彻,也花了很多时间(包括画图),一直犹豫要不要发出来,惧怕理解有误,最终仍是发出来了,欢迎交流、指正!

相关文章
相关标签/搜索