面试题(一)

来源:互联网 发布:君知其难也的其 编辑:程序博客网 时间:2024/05/22 17:24

1.C#引用类型和值类型

  1. c#语言中的所有类型都由基类System.Object继承,包括int、byte、short等,万物接对象。
  2. c#中定义的值类型包含 原类型(Sbyte、Byte、Short、Ushort、Int、Unit、Long、Ulong、Float、Double、Bool、Decimal)、枚举(enum)、结构(struct);引用类型包括:类、数组、借口、委托、字符串、链表、字典等。
  3. 值类型就是在栈中分配内存,在申明的同时就初始化,以确保数据不为NULL,值类型超出作用范围,系统会自动释放。引用型是在堆中分配内存,初始化为null,应用型是需要Garbage collection来回收内存的。
  4. 值类型的变量包含自身的数据,引用类型的变量是指向数据的内存块,而不是直接存储数据。对于值类型,每个变量有一份只属于自己的数据,对于另一个值类型变量的操作是不会影响到这个变量的值。而对于引用类型,两个变量有可能引用同一对象,因此对一个变量的操作会影响到另一个变量。
int i=1;//(那么i就是等于1的对象,在内存中会开辟一个空间用于存放i的数据)int j=2;//(那么j就是等于2的对象,在内存中开辟一个空间用于存放j的数据)j=3;//当我们修改j的时候不会影响到变量i的变化//新建一个学生的类, 是一个引用类型public class Student{    public string name; }//在主线程中调用class Program{public static void main(){    Student s1=new Student();    Student s2=s1;    s2.name="小明";    Console.WriteLine(“{0},{1}”,a.value,b.value);    }}//输出结果都是小明
  1. 什么是值类型和引用类型
    1)什么是值类型?
    进一步研究文档,你会发现所有的结构都是抽象类型System.ValueType的直接派生类,而System.ValueType本身又是直接从System.Object派生的。根据定义所知,所有的值类型都必须从System.ValueType派生,所有的枚举都从System.Enum抽象类派生,而后者又从System.ValueType派生。
    所有的值类型都是隐式密封的(sealed),目的是防止其他任何类型从值类型进行派生。
    2)什么是引用类型?
    在c#中所有的类都是引用类型,包括接口。
  2. 常见误区
    1)引用类型分配在托管堆上,值类型分配在线程栈上:其实这种说法的前半部分是对的,后半部分是错的。因为变量的值在它声明的位置存储的,所以假如某一个引用类型中有一个值类型的变量, 那么该变量的值总是和该引用类型的对象的其它数据在一起,也就是分配在堆上。(只有局部变量(方法内部声明的变量)和方法的参数在栈上)
    2)结构是轻量级的类:这种错误的信息主要是因为有人认为值类型不应该有方法或者其它有意义的行为-它们应该作为简单的数据转移来使用,所以很多人分不清DateTime到底是值类型还是引用类型。
    3)对象在c#中默认的是用过引用传递的:其实在调用方法的时候,参数值(对象的一个引用)是以传值得方式传递的,如果你想以引用方式传递的话,可以使用ref或者out关键字。

2.C#装箱和拆箱

  1. 装箱就是隐式的将一个值型转换为引用型对象。在装箱时,你需要知道编译器内部都干了什么事:
    1)在托管堆中分配好内存,分配的内存量是值类型的各个字段需要的内存量加上托管堆上所以对象的两个额外成员(类型对象指针,同步块索引)需要的内存量
    2)值类型的字段复制到新分配的堆内存中
    3)返回对象的地址,这个地址就是这个对象的引用。如下为装箱:
int i=0;System.Object obj=i;
  1. 拆箱就是讲一个应用型对象转换成任意值型。注意:拆箱不是直接将装箱过程倒过来,拆箱的代价比装箱要低的多,拆箱其实就是获取一个指针的过程。一个已装箱的实例在拆箱时,编译器在内部都干了下面这些事:
    1)如果包含了“对已装箱类型的实例引用”的变量为null时,会抛出一个NullReferenceException异常。
    2)如果引用指向的对象不是所期待的值类型的一个已装箱实例,会抛出一个InvalidCastException异常(例如上面代码的第4行)。 如下将上一步骤的应用型数据拆箱。
int j=(int)obj;
  1. 通过上面的分析我们已经知道了,装箱和拆箱/复制操作会对应用程序的速度和内存消耗产生不利的影响(例如消耗内存,增加垃圾回收次数,复制操作),所以我们应该注意编译器在什么时候会生成代码来自动这些操作,并尝试手写这些代码,尽量避免自动生成代码的情况。
  2. 下面来讨论一下编译器都会在什么时候自动生成代码来完成这些操作
    1)使用非泛型集合时:比如ArrayList,因为这些集合需要的对象都是object,如果你将一个值类型的对象添加到集合中时会执行一次装箱操作,当你取值时会执行一次拆箱操作,所以在应用程序中应避免使用这种非泛型的集合。
    2)大家都知道System.Object是所有类型的基类,当你调用object类型的非虚方法时会进行装箱操作(例如GetType方法)。在调用object的虚方法时,如果你的值类型没有重写虚方法也要进行装箱操作,所以在定义自己的值类型时,应重写object内部的虚方法(例如ToString方式)
    3)将值类型转化为接口类型时也会进行装箱操作,这是因为接口类型必须包含对堆上的一个对象的引用。

3.泛型

  1. 泛型是CLR和编程语言提供的一种特殊机制,在C#2中才被提供出来。
  2. 泛型的作用是避免装箱,在使用泛型时需要制定要装配的类型,这样可以减少装箱操作。
  3. c#中常见的泛型集合:Queue;Stack;List;Dictionary

4.类和结构体

  1. 结构体是一种特殊的值类型,所以它拥有值类型的所有特权(实力一般分配在线程栈中)和限制(不能被派生,所有没有abstrack和sealed,未装箱的实例不能进行线程同步的访问)。
  2. 什么情况下选择结构体?什么情况下选择类?
    1)在大多数的情况下,都应该选择类,除非满足以下情况,才考虑选择结构体:
    a.类型具有基元类型的行为。
    b.类型不需要从其他任何类型继承。
    c.类型也不会派生出任何其他类型。
    d.类型的实例较小(约为16字节或更小)
    e.类型实例较大,单不作为方法的参数传递,也不作为方法的返回值。

  3. 上文中提到的基元类型对应表如果下,例如:int的基元类型是Int,可以理解成一个别名的意思,基元类型只是值类型的别名
    这里写图片描述

  4. 上文提到sealed:
    1)应用于某个类时,sealed修饰符可以阻止其他类继承自该类。
class A{}sealed class B:A{}//类B继承自类A,但其他类不可以继承类B

2)还可以对替代基类的虚方法或属性的方法或属性使用sealed修饰符。这使你可以允许类派生自你的类并防止他们替代特定虚方法或属性

class X{    protected virtual void F() { Console.WriteLine("X.F"); }    protected virtual void F2() { Console.WriteLine("X.F2"); }}class Y : X{    sealed protected override void F() { Console.WriteLine("Y.F"); }    protected override void F2() { Console.WriteLine("Y.F2"); }}class Z : Y{    // Attempting to override F causes compiler error CS0239.    // protected override void F() { Console.WriteLine("C.F"); }    // Overriding F2 is allowed.    protected override void F2() { Console.WriteLine("Z.F2"); }}//Z 继承自 Y,但 Z 无法替代在 X 中声明并在 Y 中密封的虚函数 F

3)将abstract修饰符与密封类结合使用时错误的,因为抽象类必须由提供抽象方法或属性的实现的类来继承。
4)应用于方法或属性时,sealed修饰符必须始终与override集合使用
因为结构体是隐式密封,所以无法继承它们。