目录
1)常用关键字(Thread、Task、ThreadPool)
一、基本概念
1)装箱和拆箱
装箱的过程,是将 值类型 转换为 引用类型 的过程; 拆箱则是将引用类型转换为值类型。
int val = 100;
object obj = val; //装箱
int num = (int) obj; //拆箱
装箱拆箱进阶
1、装箱拆箱的“箱”是什么,“箱”存放在哪里?
装箱(Boxing)操作会创建一个堆上的对象,而拆箱(Unboxing)操作则会从堆上的对象中提取值。
"箱"代表的是创建的堆上对象,它是一个将值类型包装为引用类型的容器。当进行装箱操作时,CLR会在堆上分配内存以存储值类型的值,并返回对该对象的引用。这个对象实际上是值类型的副本,被封装在引用类型内。
具体来说,当进行装箱操作时,CLR执行以下步骤:
- 在堆上分配内存,以存储值类型的值;
- 将值类型的值复制到刚分配的内存中;
- 返回对这个堆上对象的引用;
所以,装箱后的对象存放在堆上。
当进行拆箱操作时,CLR会将装箱后的对象中存储的值类型的值提取出来,并将其存储在相应的值类型变量中。
2、装箱快还是拆箱快?
一般来说,拆箱的性能要优于装箱。
装箱操作需要将值类型转换为引用类型,并在堆上创建一个对象来存储值类型的拷贝。这个过程涉及到内存分配、复制数据和类型检查,相对比较耗时。
拆箱操作则是从装箱后的对象中提取值类型的值,并将其存储在相应的值类型变量中。这个过程主要涉及到数据的复制和类型检查,相对来说相对简单和高效。
因此,一般情况下,拆箱的性能要优于装箱。频繁进行装箱操作可能会对性能产生负面影响,特别是在循环或大规模数据处理等性能敏感的场景下。所以,在需要高性能的情况下,应尽量避免不必要的装箱操作,而尽可能使用直接操作值类型的方式。
然而,需要根据具体的应用场景和代码逻辑来综合评估装箱和拆箱的性能开销。对于某些特定的情况,如需要将值类型存储在集合类(如List、ArrayList)中,不可避免地需要进行装箱操作,但要注意避免过度的装箱和拆箱操作,以提高代码的性能。
3、装箱和拆箱有什么性能影响?
装箱的性能影响:
内存分配:装箱操作需要在堆上分配额外的内存用于存储值类型的拷贝。这涉及到内存分配和释放的开销,可能导致内存碎片化。
数据复制:装箱操作会将值类型的值复制到堆上创建的对象中。这个过程涉及到数据的复制,增加了额外的时间消耗。
类型检查:CLR会进行类型检查,确保装箱后的对象是合法的引用类型。这个检查会引入附加的开销。拆箱的性能影响:
数据复制:拆箱操作会从装箱后的对象中提取值类型的值,并将其存储在相应的值类型变量中。这个过程主要涉及到数据的复制,可以说是相对较快的步骤。
类型检查:CLR会进行类型检查,确保拆箱的目标类型与装箱对象的类型匹配。这个检查会引入一定的开销。
总体而言,装箱和拆箱操作都会涉及到数据的复制和类型检查,这些操作都需要耗费额外的时间和内存。在频繁使用和大规模数据处理的场景下,过多的装箱和拆箱操作可能会降低性能并增加资源消耗。为了提高性能,应该尽量避免不必要的装箱和拆箱操作,特别是在循环或性能敏感的代码中。可以通过使用泛型集合(如List<T>)来替代装箱的集合类,或者使用值类型数组等直接操作值类型的方式,从而避免装箱和拆箱带来的性能损失。
2)值类型和引用类型的区别
值类型通常被分配在栈上,它的变量直接包含变量的实例,使用效率比较高。
引用类型分配在托管堆上,引用类型的变量通常包含一个指向实例的指针,变量通过该指针来引用实例。
值类型继承自 ValueType (注意:而 System. ValueType 又继承自 System.Object);而引用类型继承自 System.Object。
值类型变量包含其实例数据,每个变量保存了其本身的数据拷贝(副本),因此在默认情况下,值类型的参数传递不会影响参数本身;而引用类型变量保存了其数据的引用地址,因此以引用方式进行参数传递时会影响到参数本身,因为两个变量会引用了内存中的同一块地耻。
值类型有两种表示:装箱与拆箱;引用类型只有装箱一种形式。我会在下节以专门的篇幅来深入讨论这个话题。
值类型的内存不由 GC(垃圾回收,Gabage Collection)控制,作用域结束时,值类型会自行释放,減少了托管堆的压力,因此具有性能上的优势。例如,通常 struct 比 class 更高效;而引用类型的内存回收,由 GC 来完成,微软甚至建议用户最好不要自行释放内存。
值类型是密封的(sealed),因此值类型不能作为其他任何类型的基类,但是可以单继承或者多继承接口;而引用类型一般都有继承性。
值类型不具有多态性;而引用类型有多态性。
值类型变量不可为 null 值,值类型都会自行初始化为 0 值;而引用类型变量默认情况下,创建为 null 值,表示没有指向任何托管堆的引用地址。对值为 null的引用类型的任何操作,都会抛出 NullReferenceException 异常。
值类型有两种状态:装箱和未装箱,运行库提供了所有值类型的已装箱形式;而引用类型通常只有一种形式:装箱
1.值类型和引用类型分别是哪些
值类型:基本数据类型(int、long、short、byte、float、double、char、bool)、枚举(enum)、结构体(struct)、可空类型(在后面加问号,如:int?、double?)
引用类型:class、interface、delegate、array、string以及装箱后的可空类型
访问权限修饰符
private | 私有成员, 在类的内部才可以访问(只能从其声明上下文中进行访问) |
protected | 保护成员,该类内部和从该类派生的类中可以访问 |
protected internal | 在派生类或同一程序集内都可以访问。 |
public | 公共成员,完全公开,没有访问限制。 |
internal | 在同一程序集(dll 或 exe)中可以访问。 |
3)委托(delegate)
委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。说白了就是类似指向函数方法的指针。
1.什么是委托链
委托本身不是链表。它们只是一个存储方法地址的变量。你可以将委托看作是一个方法的引用,就像一个指针。
然而,委托可以用来实现链表的概念——委托链,它是一组委托对象,可以在这组委托中添加、删除和执行委托。
示例:
public delegate void MyDelegate(); class Program { static void Main() { MyDelegate myDelegate = Method1; //用 += 运算符添加委托会创建一个委托链。移除则使用-= myDelegate += Method2; myDelegate += Method3; myDelegate();//输出:Method1 Method2 Method3 Console.ReadKey(); } static void Method1() { Console.Write("Method1"); } static void Method2() { Console.Write("Method2"); } static void Method3() { Console.Write("Method3"); } }
2. 委托链用途
委托链非常有用,可以以简单优美的方式表示程序控制流。例如,我们可以使用委托链在事件的触发和处理中实现松耦合。使用场景例如:事件处理、拦截器、插件系统等。
4)事件(event)是委托吗
不是委托,但它们之间有紧密的联系。事件基于委托,为委托提供了一个发布/订阅机制。事件是一种特殊的委托,它用于实现观察者模式,允许对象在特定事件发生时通知其他对象。事件的声明使用event关键词,它的返回类型是一个委托类型。在编码中尽量使用规范命名,通常以名字+Event作为事件的名称。
5)虚函数(virtual/override)
为了指明某个成员函数具有多态性,用关键字virtual来标记其为虚函数,表示可以被派生类重写。关键字override实现。
详细实现示例:
class A
{
public virtual void Func()//注意virtual关键字,表明这是一个虚函数
{
Console.WriteLine("A");
}
}
class B:A // 注意B是从A类继承,所以A是父类,B是子类
{
public override void Func() // 注意override关键字,表明重新实现了虚函数
{
Console.WriteLine("B");
}
}
class C : B // 注意C是从B类继承,所以B是父类,C是子类
{
}
class D : A // 注意D是从A类继承,所以A是父类,D是子类
{
public new void Func() // 注意new ,表明覆盖父类里的同名类,而不是重新实现
{
Console.WriteLine("D");
}
}
class Program
{
static void Main(string[] args)
{
A a = new A();//A为声明类,4为实例类//A为声明类,B为实例类//A为声明类,C为实例类
A b = new B();
A c = new C();
A d= new D(); //A为声明类,D为实例类
D d1 = new D(); //D为声明类,D为实例类
a.Func(); // 执行过程: 1.先检查声明类 2.检查到是虚方法 3.转去检查实例类A,就为本身 4.执行实例类A中的方法 5.输出结果 A
b.Func();// 执行过程: 1.先检查声明类A 2.检查到是虚方法 3.转去检查实例类B,有重载的 4.执行实例类B中的方法 5.输出结果 B
c.Func();// 执行过程: 1.先检查声明类 2.检查到是虚方法 3.转去检查实例类C,无重载的 4.转去检查类C的父类B,有重载的 5.执行父类B中的方法 6.输出结果 B
d.Func(); //执行过程: 1.先检查声明类 2.检查到是虚方法 3.转去检查实例类D,无重载的(注意,虽然D里有实现Func,但没有使用override关键字 ,所以不会被认为是重载) 4.转去检查类D的父类,就为本身 5.执行父类中的方法 5.输出结果 A
d1.Func(); // 执行D类里的Fun,输出结果D
Console.ReadLine();
}
}
1.构造函数、析构函数可以写成虚函数么?
构造函数:不行
原因:构造函数是在创建对象时调用的特殊方法,用于初始化对象的状态。由于构造函数是在对象创建时立即调用的,因此它们不能被声明为虚函数。
虚函数是可以在派生类中被覆盖的函数,它们可以在运行时根据对象的实际类型动态地调用。然而,构造函数是在对象创建时立即调用的,因此它们不能被覆盖或重写。
如果需要在派生类中初始化对象的状态,可以在派生类的构造函数中调用基类的构造函数,或者在派生类中添加新的构造函数。
析构函数:不行
原因:析构函数是用于释放对象所占用的资源的方法,它们是在对象被垃圾回收器回收之前调用的。由于析构函数是在对象生命周期结束时调用的,它们也不能被声明为虚函数。
虚函数主要用于实现多态性,即在运行时根据对象的实际类型动态地调用不同的方法。而析构函数和构造函数是用于初始化或释放对象的状态,它们是在对象创建或销毁时立即调用的,因此不能被声明为虚函数。
如果需要在派生类中释放对象所占用的资源,可以在派生类的析构函数中调用基类的析构函数,或者在派生类中添加新的析构函数。
(在C++中析构函数可以写出虚函数,详情:C++构造函数、析构函数可以写成虚函数么?)