补充知识点
本文为C#补充知识点,面向Unity开发,不系统且不完整
一、ArrayList和List的主要区别?
非泛型集合ArrayList存在不安全类型(ArrayList会把所有插入其中的数据都当做Object来处理),装箱拆箱的操作(费时)。 List是泛型类,功能跟ArrayList相似,但不存在ArrayList的安全问题,其必须为同一类型的值更为规范,更适合日常的使用。
ArrayList存的是通用类型,涉及拆装箱操作;
二、拆箱与装箱
-
装箱(box):将值类型转换为引用类型的过程
-
拆箱(unbox):将引用类型转换为值类型的过程
注:只有装箱后才能拆箱 装箱可以隐式转换,而拆箱必须显示转换
装箱过程
对值类型在堆中分配一个对象实例,并将 该值复制到新的对象中。按三步进行。 第一步:新分配托管堆内存(大小为值类型实例大小加上一个方法表指针(也称类型对象指针)和一个SyncBlockIndex同步块索引)。 第二步:将值类型的实例字段拷贝到新分配的内存中。 第三步:返回托管堆中新分配对象的地址。这个地址就是一个指向对象的引用了。
过程耗时耗空间 所以应尽量减少拆装箱次数
三、同步块和同步块索引
在程序运行时,CLR 管理一个同步块数组。它是一个总共 32/64 位的多功能结构,其中,前 6 位的值提示访问者目前同步块索引的功能是什么,高 6 位就像 6 个开关,不同位的打开和关闭有着不同的意义。
它的用处非常广泛,例如线程同步和 GC 都会用到它,它还会储存对象的哈希码。
同步块索引在线程同步中用来判断对象是被使用还是闲置。
默认的情况是,同步块索引被赋予一个特殊的值, 一般是 -1,此时对象没有被线程独占。当一个线程拿到对象,并打算对其操作时,它会检查对象的同步块索引。
如果索引的值为特殊值,说明没有任何线程正在操作它,此时这个线程获得它的操作权,并在 CLR 的同步块数组中分配一个新的同步块给它,并将该块的索引值写入实例的同步块索引值中。
这时,如果有其他线程来访问该实例,它就不能操作这个实例了,因为它的同步块索引的值不为特殊值。
当独占的线程操作完之后,同步块索引的值被重设回特殊值。
四、类型对象指针(方法表指针)
类型对象指针是指向类型对象的引用。类型对象是反射的重要操作对象,实际上是 System.Type
的实例对象。类型对象中存有该类型的方法表和静态字段,创建之后就不会再改变,通过这个事实可以验证静态字段的全局性。
类型对象指针可以简单理解为 System.Type
获取的类型对象引用,其指向的是实际类型对象。
方法表记录了类型的所有方法,包括静态方法和实例方法。方法会在初次执行时,经由 JIT 编译为机器码,并将机器码存在内存之中,获得一个入口地址。
下次调用该方法时,直接跳到入口地址,无需再次编译。
注意所有对象内部均具有类型对象指针,普通对象指向其对应的类型对象,而类型对象的类型对象指针指向自身。每个对象均继承来自 Object
的 GetType()
方法,用来获取对象内部的类型对象指针。
五、C#内存布局和对齐
C# 的内存布局如下:
- 同步块索引
- 方法表指针(指向类型对象中的方法表)
- 类型的父类实例成员(静态成员存储在类型对象中)
- 类型自己的实例成员(静态成员存储在类型对象中)
同步块索引和方法表指针只在引用类型中有,在32位机器上占4字节,在64位机器上占8字节。
引用类型的内存分配从同步块索引开始,接着是方法表指针。每个引用类型都至少有这8个字节,接下来是类型的实例字段。
在32位机上,对象的字节数必须是4的倍数,而在64位机上,必须是8的倍数。CLR 会智能地合并可合并的字段以节省空间。
六、sealed
关键字的作用
sealed
修饰的类为密封类,防止其他类继承此类。sealed
修饰的方法防止派生类重写此方法。
类似于 Java 的 final
关键字。
七、private
,public
,protected
,internal
的区别
public
: 对所有类和成员公开,任何地方都可以访问。private
: 仅对本类内部可见。protected
: 对该类及其派生类公开。internal
: 仅在同一程序集内访问。
八、Interface
与抽象类的区别
-
接口:
- 所有成员必须是
public abstract
类型。 - 只能包含抽象方法和属性成员。
- 支持多重实现。
- 所有成员必须是
-
抽象类:
- 可以有普通方法和字段。
- 可以包含
private
方法。 - 继承是单继承。
抽象类仍是类,可以封装成员方法,而接口是对外的行为规范,通常用于定义“我能做什么”的契约。
九、.NET 与 Mono 的关系
- .NET 是一个开源的开发者平台,类似于 Java 虚拟机,可以运行其支持的多种语言下编写的程序。
- .NET Framework 针对 Windows 平台的 .NET 实现。
- .NET Core 和 Mono 支持跨平台开发。
FCL(框架类库)是 .NET 的类库实现,CIL(通用中间语言)是语言编译后的中间代码,CLR(公共语言运行时)则是执行 CIL 的环境。
十、C# 的堆和栈
-
栈:结构为后进先出,存放局部变量和方法参数,编译器自动释放。
-
堆:由 CLR 管理,存放引用类型的对象,当托管堆满了时自动进行垃圾回收。
-
栈:速度快,存储局部值类型和引用类型的引用。
-
堆:空间大,由 GC 程序管理,存放引用类型的对象。
十一、结构体和类的区别
语法层面:
- 结构体不能继承,而类可以。
- 结构体是值类型,类是引用类型。
- 结构体无法声明无参数构造函数。
- 结构体在堆栈中分配,类在堆中分配。
结构体适合轻量级对象如点和颜色,而类适合更复杂的对象。对于频繁传递的对象,如果只需一份,建议使用类;若需要复制,则可考虑使用结构体。
十二、ref
参数和 out
参数的区别
ref参数:当一个参数作为ref参数传递时,要求在方法调用之前就初始化该参数。通过ref传递的是引用,而不是值,因此方法可以更改传递进来的变量的值。
out参数:与ref参数类似,但out参数在传递之前不需要被初始化。在方法内,必须对out参数进行赋值,才能在方法调用结束后返回值。
区别:
- ref参数要求在调用前必须初始化,而out参数则不要求在调用前初始化。
- out参数必须在方法内部赋值,否则编译器会报错。 示例:
void ExampleMethod(ref int refParameter, out int outParameter)
{
refParameter = 10; // ref 参数传递进来时已初始化
outParameter = 20; // out 参数必须在方法内部赋值
}
十三、GC垃圾回收机制的工作原理
垃圾回收器(GC)是负责自动回收不再使用的对象,避免内存泄漏的机制。C# 的GC采用了分代回收的机制,将托管堆划分为三个代(0代、1代、2代)。GC主要负责以下工作:
- 分代管理:0代是最新分配的对象,1代和2代是经过一次或多次垃圾回收仍存活的对象。
- 标记清除:GC通过“根对象”(例如局部变量、静态字段等)追踪哪些对象仍在使用,然后将未被引用的对象进行清除。
- 压缩内存:GC在清除无用对象后,会将存活的对象进行内存压缩,减少内存碎片。
GC 的回收过程分为几个步骤:
- 标记阶段:标记出哪些对象是可达的(仍被引用),哪些是不可达的。
- 清除阶段:清除所有不可达的对象,释放它们占用的内存。
- 压缩阶段:在堆上重新排列剩余的对象,释放连续的内存块。
C#的GC机制使得开发者不必手动管理内存,但仍然需要注意避免无意的内存泄漏,例如长时间存在的对象引用(静态变量等)。
十四、值类型和引用类型的区别
-
值类型:包括基本类型(如
int
、float
)、struct
和enum
,它们在内存中存储的是实际的值,值类型变量直接包含其数据,并存储在栈中。当值类型被赋值或传递时,会创建它的一个副本,互不影响。 -
引用类型:包括类(
class
)、接口(interface
)、数组和delegate
等,存储在堆内存中,引用类型的变量存储的是对象的地址。通过引用类型传递的是对该对象的引用,多个引用可以指向同一个对象。
十五、什么是委托和事件?
十六、C#中的接口是什么?如何实现接口?
接口是一种定义了行为的契约,用于规定实现该接口的类或结构体必须实现的成员(方法、属性、事件或索引器)。接口定义了行为的规范,但不提供具体的实现细节。类或结构体可以通过实现接口中的成员来具备这些行为。
接口的定义和实现:
public interface IMyInterface
{
void DoSomething();
int MyProperty { get; set; }
}
public class MyClass : IMyInterface
{
public void DoSomething()
{
Console.WriteLine("Doing something...");
}
private int myValue;
public int MyProperty
{
get { return myValue; }
set { myValue = value; }
}
}
实现接口后,类必须提供接口中所有成员的具体实现。接口可以让类具备多种行为,C#还允许一个类实现多个接口。
十七、什么是抽象类?与接口的区别是什么?
抽象类是一种不能直接实例化的类,它可以包含抽象成员(没有实现的方法)和具体实现的成员。抽象类通常用于定义一类事物的通用特征,并且要求子类提供某些特定行为的实现。
抽象类的定义:
public abstract class MyBaseClass
{
public abstract void AbstractMethod(); // 抽象方法
public void ConcreteMethod() // 具体方法
{
Console.WriteLine("This is a concrete method.");
}
}
子类必须实现抽象方法:
public class DerivedClass : MyBaseClass
{
public override void AbstractMethod()
{
Console.WriteLine("Abstract method implemented.");
}
}
如果不重写,那么子类也为抽象类,无法实例化;
抽象类与接口的区别:
- 抽象类可以包含具体实现的方法,而接口中的所有方法默认都是抽象的(C# 8.0允许接口定义默认实现,但通常仍然用作行为约定)。
- 一个类只能继承一个抽象类,但可以实现多个接口。
- 抽象类可以包含字段、构造函数、属性等,而接口不能包含字段或实现构造函数。
- 抽象类更适合用来表示一种“是”的关系(比如
Dog
是Animal
的一种),接口则用来表示一种“能”的能力(比如Dog
能Bark
)。
十八、什么是多态性?如何在C#中实现多态?
多态性是面向对象编程中的一个核心特性,它允许同一方法在不同对象上有不同的表现形式。C#中的多态性可以通过虚方法、抽象方法、接口等来实现。
C#实现多态性有两种主要方式:
- 方法重写(覆盖):通过在基类中定义虚方法(
virtual
),并在派生类中通过override
关键字重写方法。 - 接口多态性:通过实现同一接口的不同类,实现同一方法的不同行为。