C#基础知识整理

Posted by JP on October 15, 2017

值类型与引用类型

1.值类型和引用类型的区别?

值类型包括:简单类型、结构体类型和枚举类型 ,引用类型包括:自定义类、数组、接口、委托等。
1.赋值方式:将一个值类型变量赋给另一个值类型变量时,将复制包含的值。这与引用类型变量的赋值不同,引用类型变量的赋值只复制对象的引用(即内存地址,类似C++中的指针),而不复制对象本身。
2.继承:值类型不可能派生出新的类型,所有的值类型均隐式派生自 System.ValueType。但与引用类型相同的是,结构也可以实现接口。
3.null:与引用类型不同,值类型不可能包含 null 值。然而,可空类型功能允许将 null 赋给值类型。
4.每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值,值类型初始会默认为0,引用类型默认为null。
5.值类型存储在栈中,引用类型存储在托管堆中。

2. 结构和类的区别?

结构体是值类型,类是引用类型,主要区别如题1。
其他的区别: 结构不支持无惨构造函数,不支持析构函数,并且不能有protected修饰;
结构常用于数据存储,类class多用于行为;
class需要用new关键字实例化对象,struct可以不适用new关键字;
class可以为抽象类,struct不支持抽象;

3. delegate是引用类型还是值类型?enum、int[]和string呢?

enum枚举是值类型,其他都是引用类型。

4. 堆和栈的区别?

线程堆栈:简称栈 Stack
托管堆: 简称堆 Heap
值类型大多分配在栈上,引用类型都分配在堆上;
栈由操作系统管理,栈上的变量在其作用域完成后就被释放,效率较高,但空间有限。堆受CLR的GC控制;
栈是基于线程的,每个线程都有自己的线程栈,初始大小为1M。堆是基于进程的,一个进程分配一个堆,堆的大小由GC根据运行情况动态控制;

5.“结构”对象可能分配在堆上吗?什么情况下会发生,有什么需要注意的吗?

结构是值类型,有两种情况会分配在对上面:
结构作为class的一个字段或属性,会随class一起分配在堆上面;
装箱后会在堆中存储,尽量避免值类型的装箱,值类型的拆箱和装箱都有性能损失,下一篇会重点关注;

6. 理解参数按值传递?以及按引用传递?

按值传递:对于值类型传递的它的值拷贝副本,而引用类型传递的是引用变量的内存地址,他们还是指向的同一个对象。
按引用传递:通过关键字out和ref传递参数的内存地址,值类型和引用类型的效果是相同的。

7. out 和 ref的区别与相同点?

out 和 ref都指示编译器传递参数地址,在行为上是相同的;
他们的使用机制稍有不同,ref要求参数在使用之前要显式初始化,out要在方法内部初始化;
out 和 ref不可以重载,就是不能定义Method(ref int a)和Method(out int a)这样的重载,从编译角度看,二者的实质是相同的,只是使用时有区别;

8. 有几种方法可以判定值类型和引用类型?

简单来说,继承自System.ValueType的是值类型,反之是引用类型。

9. C#支持哪几个预定义的值类型?C#支持哪些预定义的引用类型?

值类型:整型、浮点数、字符、bool和decimal
引用类型:object,string

10. 说说值类型和引用类型的生命周期?

值类型在作用域结束后释放。
引用类型由GC垃圾回收期回收。这个答案可能太简单了,更详细的答案在后面的文章会说到。

12. 如果结构体中定义引用类型,对象在内存中是如何存储的?例如下面结构体中的class类 User对象是存储在栈上,还是堆上?

public struct MyStruct 
{ 
  public int Index; 
  public User User; 
}

MyStruct存储在栈中,其字段User的实例存储在堆中,MyStruct.User字段存储指向User对象的内存地址。

装箱与拆箱

1.什么是拆箱和装箱?

装箱就是值类型转换为引用类型,拆箱就是引用类型(被装箱的对象)转换为值类型。

2.什么是箱子?

就是引用类型对象。

3.箱子放在哪里?

托管堆上。

4.装箱和拆箱有什么性能影响?

装箱和拆箱都涉及到内存的分配和对象的创建,有较大的性能影响。

5.如何避免隐身装箱?

编码中,多使用泛型、显示装箱。

6.箱子的基本结构?

上面说了,箱子就是一个引用类型对象,因此她的结构,主要包含两部分: 值类型字段值; 引用类型的标准配置,引用对象的额外空间:TypeHandle和同步索引块,关于这两个概念在本系列后面的文章会深入探讨。

7.装箱的过程?

1.在堆中申请内存,内存大小为值类型的大小,再加上额外固定空间(引用类型的标配:TypeHandle和同步索引块);
2.将值类型的字段值(x=1023)拷贝新分配的内存中;
3.返回新引用对象的地址(给引用变量object o)

8.拆箱的过程?

1.检查实例对象(object o)是否有效,如是否为null,其装箱的类型与拆箱的类型(int)是否一致,如检测不合法,抛出异常;
2.指针返回,就是获取装箱对象(object o)中值类型字段值的地址;
3.字段拷贝,把装箱对象(object o)中值类型字段值拷贝到栈上,意思就是创建一个新的值类型变量来存储拆箱后的值;

string与字符串操作

1.字符串是引用类型类型还是值类型?

引用类型。

2.在字符串连加处理中,最好采用什么方式,理由是什么?

少量字符串连接,使用String.Concat,大量字符串使用StringBuilder,因为StringBuilder的性能更好,如果string的话会创建大量字符串对象。

3.使用 StringBuilder时,需要注意些什么问题?

少量字符串时,尽量不要用,StringBuilder本身是有一定性能开销的;
大量字符串连接使用StringBuilder时,应该设置一个合适的容量;

类与接口

1. 所有类型都继承System.Object吗?

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

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

1.virtual声明虚方法的关键字,说明该方法可以被重写
2.sealed说明该类不可被继承
3.override重写基类的方法
4.abstract申明抽象类和抽象方法的关键字,抽象方法不提供实现,由子类实现,抽象类不可实例化。

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

不同点:

1.接口不能直接实例化。
2.接口只包含方法或属性的声明,不包含方法的实现。
3.接口可以多继承,类只能单继承。
4.类有部分类(partial)的概念,定义可在不同的源文件之间进行拆分,而接口没有。(这个地方确实不对,接口也可以分部,谢谢@xclin163的指正)
5.表达的含义不同,接口主要定义一种规范,统一调用方法,也就是规范类,约束类,类是方法功能的实现和集合

相同点:

1.接口、类和结构都可以从多个接口继承。
2.接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。
3.接口和类都可以包含事件、索引器、方法和属性。

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

1.继承:接口支持多继承;抽象类不能实现多继承。
2.表达的概念:接口用于规范,更强调契约,抽象类用于共性,强调父子。抽象类是一类事物的高度聚合,那么对于继承抽象类的子类来说,对于抽象类来说,属于”Is A”的关系;而接口是定义行为规范,强调“Can Do”的关系,因此对于实现接口的子类来说,相对于接口来说,是”行为需要按照接口来完成”。
3.方法实现:对抽象类中的方法,即可以给出实现部分,也可以不给出;而接口的方法(抽象规则)都不能给出实现部分,接口中方法不能加修饰符。
4.子类重写:继承类对于两者所涉及方法的实现是不同的。继承类对于抽象类所定义的抽象方法,可以不用重写,也就是说,可以延用抽象类的方法;而对于接口类所定义的方法或者属性来说,在继承类中必须重写,给出相应的方法和属性实现。
5.新增方法的影响:在抽象类中,新增一个方法的话,继承类中可以不用作任何处理;而对于接口来说,则需要修改继承类,提供新定义的方法。
6.接口可以作用于值类型(枚举可以实现接口)和引用类型;抽象类只能作用于引用类型。
7.接口不能包含字段和已实现的方法,接口只包含方法、属性、索引器、事件的签名;抽象类可以定义字段、属性、包含有实现的方法。

5. 重载与覆盖的区别?

重载:
当类包含两个名称相同但签名不同(方法名相同,参数列表不相同)的方法时发生方法重载。用方法重载来提供在语义上完成相同而功能不同的方法。
覆盖:
在类的继承中使用,通过覆写子类方法可以改变父类虚方法的实现。
主要区别:
1.方法的覆盖是子类和父类之间的关系,是垂直关系;方法的重载是同一个类中方法之间的关系,是水平关系。
2.覆盖只能由一个方法,或只能由一对方法产生关系;方法的重载是多个方法之间的关系。
3.覆盖要求参数列表相同;重载要求参数列表不同。
4.覆盖关系中,调用那个方法体,是根据对象的类型来决定;重载关系,是根据调用时的实参表与形参表来选择方法体的。

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

static void Main(string[] args)
{
    B1 b1 = new B1(); 
    B2 b2 = new B2();
    b1.Print();
    b2.Print();     //输出 B1、B2

    A ab1 = new B1();
    A ab2 = new B2();
    ab1.Print();
    ab2.Print();   //输出 B1、A
    Console.ReadLine();
}
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.class中定义的静态字段是存储在内存中的哪个地方?为什么会说她不会被GC回收?

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

常量、字段、属性、特性与委托

1. const和readonly有什么区别?

const关键字用来声明编译时常量,readonly用来声明运行时常量。都可以标识一个常量,主要有以下区别:
1.初始化位置不同。const必须在声明的同时赋值;readonly即可以在声明处赋值,也可以在构造方法里赋值。
2.修饰对象不同。const即可以修饰类的字段,也可以修饰局部变量;readonly只能修饰类的字段 。
3.const是编译时常量,在编译时确定该值,且值在编译时被内联到代码中;readonly是运行时常量,在运行时确定该值。
4.const默认是静态的;而readonly如果设置成静态需要显示声明 。
5.支持的类型时不同,const只能修饰基元类型或值为null的其他引用类型;readonly可以是任何类型。

public class Program
{
    public readonly int PORT;
    static void Main(string[] args)
    {
        B a = new B();
        Console.WriteLine(a.PORT);
        Console.WriteLine(B.PORT2);
        Console.WriteLine(a.PORT3.ccc);
        Console.ReadLine();
    }
}

class B
{
    //readonly即可以在声明处赋值,也可以在构造方法里赋值
    public readonly int PORT;
    //const必须在声明的同时赋值
    public const int PORT2 = 10;
    //错误public const A PORT3 =  new A()    const只能修饰基元类型或值为null的其他引用类型
    //readonly可以是任何类型
    public readonly A PORT3 = new A();
    public B()
    {
        PORT = 11;
    }
}
class A
{
    public string ccc = "aaaa";
}

2. 字段与属性有什么异同?

1.属性提供了更为强大的,灵活的功能来操作字段
2.出于面向对象的封装性,字段一般不设计为public
3.属性允许在set和get中编写代码
4.属性允许控制set和get的可访问性,从而提供只读或者可读写的功能 (逻辑上只写是没有意义的)
5.属性可以使用 override 和 new

3. 静态成员和非静态成员的区别?

1.静态变量使用 static 修饰符进行声明,静态成员在加类的时候就被加载(上一篇中提到过,静态字段是随类型对象存放在Load Heap上的),通过类进行访问。
2.不带有static 修饰符声明的变量称做非静态变量,在对象被实例化时创建,通过对象进行访问 。
3.一个类的所有实例的同一静态变量都是同一个值,同一个类的不同实例的同一非静态变量可以是不同的值 。
4.静态函数的实现里不能使用非静态成员,如非静态变量、非静态函数等。

4. 特性是什么?如何使用?

特性与属性是完全不相同的两个概念,只是在名称上比较相近。
Attribute特性就是关联了一个目标对象的一段配置信息,本质上是一个类,其为目标元素提供关联附加信息,这段附加信息存储在dll内的元数据,它本身没什么意义。运行期以反射的方式来获取附加信息。

5. C#中的委托是什么?事件是不是一种委托?

什么是委托?简单来说,委托类似于 C或 C++中的函数指针,允许将方法作为参数进行传递。
1.C#中的委托都继承自System.Delegate类型;
2.委托类型的声明与方法签名类似,有返回值和参数;
3.委托是一种可以封装命名(或匿名)方法的引用类型,把方法当做指针传递,但委托是面向对象、类型安全的;
事件可以理解为一种特殊的委托,事件内部是基于委托来实现的。

GC与内存管理

1. 简述一下一个引用对象的生命周期?

new创建对象并分配内存
1.对象初始化
2.对象操作、使用
3.资源清理(非托管资源)
4.GC垃圾回收

2. GC进行垃圾回收时的主要流程是?

1.标记:先假设所有对象都是垃圾,根据应用程序根Root遍历堆上的每一个引用对象,生成可达对象图,对于还在使用的对象(可达对象)进行标记(其实就是在对象同步索引块中开启一个标示位)。
2.清除:针对所有不可达对象进行清除操作,针对普通对象直接回收内存,而对于实现了终结器的对象(实现了析构函数的对象)需要单独回收处理。清除之后,内存就会变得不连续了,就是步骤3的工作了。
3.压缩:把剩下的对象转移到一个连续的内存,因为这些对象地址变了,还需要把那些Root跟指针的地址修改为移动后的新地址。

6. GC在哪些情况下回进行回收工作?

1.内存不足溢出时(0代对象充满时)
2.Windwos报告内存不足时,CLR会强制执行垃圾回收
3.CLR卸载AppDomian,GC回收所有
4.调用GC.Collect 5.其他情况,如主机拒绝分配内存,物理内存不足,超出短期存活代的存段门限

7. using() 语法是如何确保对象资源被释放的?如果内部出现异常依然会释放资源吗?

using() 只是一种语法形式,其本质还是try…finally的结构,可以保证Dispose始终会被执行。

8. 解释一下C#里的析构函数?为什么有些编程建议里不推荐使用析构函数呢?

C#里的析构函数其实就是终结器Finalize,因为长得像C++里的析构函数而已。
有些编程建议里不推荐使用析构函数要原因在于:第一是Finalize本身性能并不好;其次很多人搞不清楚Finalize的原理,可能会滥用,导致内存泄露,因此就干脆别用了

9. Finalize() 和 Dispose() 之间的区别?

Finalize() 和 Dispose()都是.NET中提供释放非托管资源的方式,他们的主要区别在于执行者和执行时间不同:
1.finalize由垃圾回收器调用;dispose由对象调用。
2.finalize无需担心因为没有调用finalize而使非托管资源得不到释放,而dispose必须手动调用。
3.finalize不能保证立即释放非托管资源,Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间;而dispose一调用便释放非托管资源。
4.只有class类型才能重写finalize,而结构不能;类和结构都能实现IDispose。
5.另外一个重点区别就是终结器会导致对象复活一次,也就说会被GC回收两次才最终完成回收工作,这也是有些人不建议开发人员使用终结器的主要原因。

10. Dispose和Finalize方法在何时被调用?

1.Dispose一调用便释放非托管资源;
2.Finalize不能保证立即释放非托管资源,Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间;

11. .NET中的托管堆中是否可能出现内存泄露的现象?

是的,可能会。比如:
1.不正确的使用静态字段,导致大量数据无法被GC释放;
2.没有正确执行Dispose(),非托管资源没有得到释放;
3.不正确的使用终结器Finalize(),导致无法正常释放资源;
4.其他不正确的引用,导致大量托管对象无法被GC释放;

12. 在托管堆上创建新对象有哪几种常见方式?

1.new一个对象;
2.字符串赋值,如string s1=”abc”;
3.值类型装箱;

多线程编程与线程同步

1. 描述线程与进程的区别?

1.一个应用程序实例是一个进程,一个进程内包含一个或多个线程,线程是进程的一部分;
2.进程之间是相互独立的,他们有各自的私有内存空间和资源,进程内的线程可以共享其所属进程的所有资源;

2. 多线程和异步有什么关系和区别?

1.多线程是实现异步的主要方式之一,异步并不等同于多线程。实现异步的方式还有很多,比如利用硬件的特性、使用进程或线程等。
2.在.NET中就有很多的异步编程支持,比如很多地方都有Begin、End的方法,就是一种异步编程支持,它内部有些是利用多线程,有些是利用硬件的特性来实现的异步编程。

3. 线程池的优点有哪些?又有哪些不足?

优点:
减小线程创建和销毁的开销,可以复用线程;也从而减少了线程上下文切换的性能损失;在GC回收时,较少的线程更有利于GC的回收效率。
缺点:
线程池无法对一个线程有更多的精确的控制,如了解其运行状态等;不能设置线程的优先级;加入到线程池的任务(方法)不能有返回值;对于需要长期运行的任务就不适合线程池。

4. lock为什么要锁定一个参数,可不可锁定一个值类型?这个参数有什么要求?

1.lock的锁对象要求为一个引用类型。她可以锁定值类型,但值类型会被装箱,每次装箱后的对象都不一样,会导致锁定无效。
2.对于lock锁,锁定的这个对象参数才是关键,这个参数的同步索引块指针会指向一个真正的锁(同步块),这个锁(同步块)会被复用。

5. Mutex和lock有何不同?一般用哪一个作为锁使用更好?

互斥锁(英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。Mutex是一个基于内核模式的互斥锁,支持锁的递归调用,而Lock是一个混合锁,一般建议使用Lock更好,因为lock的性能更好。

托管资源与非托管资源

1.托管资源:一般是指被CLR控制的内存资源,这些资源由CLR来管理。可以认为是.net 类库中的资源。
2.非托管资源:不受CLR控制和管理的资源,比如文件流,数据库的连接,网络连接,系统的窗口句柄,打印机资源等,这类资源一般不存在堆上。可以认为操作系统资源的一组API。
3.对于托管资源,GC负责垃圾回收。对于非托管资源,GC可以跟踪非托管资源的生存期,但是不知道如何释放它,这时候就要人工进行释放。

参考文档:

1.堆栈和托管堆 c#
2.腾讯面试题04.进程和线程的区别?