跳到主要内容

补充知识点

注意

本文为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 编译为机器码,并将机器码存在内存之中,获得一个入口地址。

下次调用该方法时,直接跳到入口地址,无需再次编译。

注意所有对象内部均具有类型对象指针,普通对象指向其对应的类型对象,而类型对象的类型对象指针指向自身。每个对象均继承来自 ObjectGetType() 方法,用来获取对象内部的类型对象指针。

五、C#内存布局和对齐

C# 的内存布局如下:

  1. 同步块索引
  2. 方法表指针(指向类型对象中的方法表)
  3. 类型的父类实例成员(静态成员存储在类型对象中)
  4. 类型自己的实例成员(静态成员存储在类型对象中)

同步块索引和方法表指针只在引用类型中有,在32位机器上占4字节,在64位机器上占8字节。

引用类型的内存分配从同步块索引开始,接着是方法表指针。每个引用类型都至少有这8个字节,接下来是类型的实例字段。

在32位机上,对象的字节数必须是4的倍数,而在64位机上,必须是8的倍数。CLR 会智能地合并可合并的字段以节省空间。

六、sealed 关键字的作用

  • sealed 修饰的类为密封类,防止其他类继承此类。
  • sealed 修饰的方法防止派生类重写此方法。

类似于 Java 的 final 关键字。

七、privatepublicprotectedinternal 的区别

  • public: 对所有类和成员公开,任何地方都可以访问。
  • private: 仅对本类内部可见。
  • protected: 对该类及其派生类公开。
  • internal: 仅在同一程序集内访问。

八、Interface 与抽象类的区别

  • 接口

    • 所有成员必须是 public abstract 类型。
    • 只能包含抽象方法和属性成员。
    • 支持多重实现。
  • 抽象类

    • 可以有普通方法和字段。
    • 可以包含 private 方法。
    • 继承是单继承。

抽象类仍是类,可以封装成员方法,而接口是对外的行为规范,通常用于定义“我能做什么”的契约。

九、.NET 与 Mono 的关系

  • .NET 是一个开源的开发者平台,类似于 Java 虚拟机,可以运行其支持的多种语言下编写的程序。
  • .NET Framework 针对 Windows 平台的 .NET 实现。
  • .NET CoreMono 支持跨平台开发。

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 的回收过程分为几个步骤:

  1. 标记阶段:标记出哪些对象是可达的(仍被引用),哪些是不可达的。
  2. 清除阶段:清除所有不可达的对象,释放它们占用的内存。
  3. 压缩阶段:在堆上重新排列剩余的对象,释放连续的内存块。

C#的GC机制使得开发者不必手动管理内存,但仍然需要注意避免无意的内存泄漏,例如长时间存在的对象引用(静态变量等)。

十四、值类型和引用类型的区别

  • 值类型:包括基本类型(如intfloat)、structenum,它们在内存中存储的是实际的值,值类型变量直接包含其数据,并存储在栈中。当值类型被赋值或传递时,会创建它的一个副本,互不影响。

  • 引用类型:包括类(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.");
}
}

注意

如果不重写,那么子类也为抽象类,无法实例化;

抽象类与接口的区别

  1. 抽象类可以包含具体实现的方法,而接口中的所有方法默认都是抽象的(C# 8.0允许接口定义默认实现,但通常仍然用作行为约定)。
  2. 一个类只能继承一个抽象类,但可以实现多个接口。
  3. 抽象类可以包含字段、构造函数、属性等,而接口不能包含字段或实现构造函数。
  4. 抽象类更适合用来表示一种“”的关系(比如DogAnimal的一种),接口则用来表示一种“”的能力(比如DogBark)。

十八、什么是多态性?如何在C#中实现多态?

多态性是面向对象编程中的一个核心特性,它允许同一方法在不同对象上有不同的表现形式。C#中的多态性可以通过虚方法抽象方法接口等来实现。

C#实现多态性有两种主要方式:

  1. 方法重写(覆盖):通过在基类中定义虚方法(virtual),并在派生类中通过override关键字重写方法。
  2. 接口多态性:通过实现同一接口的不同类,实现同一方法的不同行为。

十九、什么是封装?C#中如何实现封装?

封装是将对象的状态(数据)和行为(操作)隐藏在类的内部,只对外暴露必要的接口。通过封装,类的内部实现细节对外部用户是不可见的,从而保证了类的可维护性和安全性。

C#中通过访问修饰符实现封装:

  • public:成员可以在任何地方访问。
  • private:成员只能在类的内部访问。
  • protected:成员可以在类的内部和派生类中访问。
  • internal:成员只能在同一个程序集内访问。

通过这种方式,name字段被封装起来,只能通过Name属性进行访问。

二十、什么是协变和逆变?

协变逆变用于处理泛型委托和泛型接口中的类型兼容性。它们允许将某些类型转换规则应用于接口或委托的类型参数,从而增强类型灵活性。

  • 协变(Covariance):允许你将派生类对象赋给泛型接口的基类对象。协变应用于返回类型,使用out关键字标识。
  • 逆变(Contravariance):允许你将基类对象赋给泛型接口的派生类对象。逆变应用于参数类型,使用in关键字标识。

通过协变和逆变,可以在泛型委托和接口的不同类型间实现更灵活的类型转换。