1.方法重载和重写
重载:在同一个类中定义多个同名但形参不同的方法。
重写:通过使用virtual与override关键字,实现基类与派生类方法内容不同。
注:抽象类必须派生类必须重写,虚方法均可。
2.面向对象三大特征
1.封装:将属性和方法结合,并限制外部直接访问数据能力。保护对象的内部状态,同时提供公共方法让外部访问,增强数据安全性。
2.继承
- 作用:提高代码重复利用率,增强可维护性。将子类的公共属性集合在一起,方便共同管理。
- 特性:传递性与单根性
3.多态:通过统一的接口调用不同类的对象,从而实现统一操作的不同结果,主要是重载 和重写。
3.值类型和引用类型
- 值类型
- 存储方式:直接存储实际数据。
- 包含类型:int,float,bool,char,double,struct,enum 等。
- 存储位置:栈内存。
2.引用类型
- 存储方式:存储数据的引用(内存地址)(栈内存)+ 实际数据(堆内存)。
- 包含类型:string,object,class,interface,delegate。
- 存储位置:堆内存。
4.堆与栈
1.栈
- 存储内容:值类型数据,引用(地址),函数调用中的传入参数,局部变量与返回地址等。
- 生命周期:随作用域自动创建与销毁。
- 物理内存:地址连续且无内存碎片的问题。
2.堆
- 存储内容:引用类型实际数据(如new关键词创建的对象等)。
- 生命周期:由GC决定销毁时机,只要有引用指向该对象,就会继续存在。
- 物理内存:地址分散且有内存碎片的问题。
5.装箱与拆箱
1.装箱:将值类型转换为object类型。
2.拆箱:object类型转换为值类型。
3.触发时机:
- 将值类型赋值给变量或接口变量。
- 调用object或接口方法,传入值类型参数。
- 非泛型集合存储值类型(ArrayList等)。
4.避免不必要的装拆箱
- 使用泛型集合(List<T>)与方法。
- 避免将值类型转化为object。
- 避免在Unity周期函数中拆装箱。
- 使用NativeArray<T>处理高频值类型数据。
6.private,public与protected
1.public:对任何类和成员公开,无限制访问。
2.private:仅对该类公开。
3.protected:对该类及其派生类公开。
7.ArrayList与List
1.ArrayList:非泛型集合,存储的数据类型为Object(引用类型),可存储任何类型的对象,但使用时会进行类型转换(装/拆箱)。
2.List:泛型集合,提供类型参数T,避免类型转换,更加高效安全。
8.接口与抽象类
1.接口
- 完全抽象类型,只定义不实现。
- 多继承,成员只能是public。
- 定义的功能能被多个类实现,不同的类实现不同的效果,这样调用不同类的相同接口方法,就可以执行不同的效果。
2.抽象类
- 包含抽象方法(不实现)与具体方法(实现)
- 单继承,成员可以是public,protected,private(不可以修饰抽象方法)。
- 子类可以继承并重复使用父类的具体方法,也必须让子类重写抽象类实现特定方法。
注:
- 虚方法:有默认实现,子类可以选择性覆写(Override),可以在抽象类或普通类中。
- 抽象方法:无默认实现,子类必须覆写,且只能在抽象类中。
9.Sealed关键字
类声明时可防止其他类继承此类,在方法中声明则可防止派生类重写此方法。
10.unsafe关键字
- 作用:启用指针操作,允许开发者直接操作内存地址。
- 用法:修饰代码块,方法,类和结构体。
11.ref与out关键字
1.ref关键字
-
修饰引用参数,调用前必须初始化。
-
作用:利用变量现有值,并在使用后更改。
-
示例:
csvoid AddOne(ref int num) { num++; } int x = 5; AddOne(ref x); Console.WriteLine(x); // 输出6(原变量被修改)
2.out关键字
-
修饰输出参数,调用前无需初始化。
-
作用:强制输出多结果。
-
示例:
csbool TryParseInt(string input, out int result) { if (int.TryParse(input, out result)) { return true; } result = 0; return false; } string str = "123"; if (TryParseInt(str, out int num)) { Console.WriteLine(num); // 输出123(方法为num赋值) }else { Console.WriteLine(num); // 输出0 }
3.注意事项:
1)引用参数和输出参数不会创建新的存储位置。
2)普通参数传递的是地址的副本,而ref传递的是变量本身的引用,修改会直接影响实参。
12.结构体与类
1.结构体
- 值类型 ,赋值/传递时,会创建一个副本,不能定义无参构造,只能定义全参构造。
- 用法:常作为数据容器。
2.类
- 引用类型,赋值/传递时会复制引用(地址),可以定义无参构造。
- 用法:常用于复杂行为,实例较多,需要继承或被继承。
13.构造函数
1.概念:一种名字与类或结构体相同的方法,用于创建对象时执行。
2.常见类型:
-
默认:若类中未定义构造函数,C#会自动生成一个无参数的默认构造函数。
-
有参:
cspublic class Person { public string Name; public Person(string name, int age) { Name = name; } } -
重载:一个类可定义多个构造函数,参数不同。
-
静态:static修饰且无参,用于初始化类的静态成员,由系统自动调用(仅在类被使用时执行一次)
3.注意事项:构造函数属于当前类,不能被继承,非静态构造函数在每次对象实例化都会执行。
14.泛型
1.概念:定义类,接口与方法时使用的类型占位符的一种机制。
2.常见类型
-
类:
cspublic class Box<T> { public T Value; public void SetValue(T value) { Value = value; } public T GetValue() { return Value; } }csBox<int> intBox = new Box<int>(); intBox.SetValue(100); int num = intBox.GetValue(); -
方法:即使类不是泛型,也可以定义泛型方法。
-
接口:
cspublic interface IIterable<T> { bool HasNext(); T Next(); }
3.泛型约束:
|------------------|-------------------------------|
| where T : class | T必须是引用类型(如string、class) |
| where T : struct | T必须是值类型(如int、struct) |
| where T : new() | T必须有公共无参构造函数 |
15.泛型容器与非泛型容器
1.泛型容器:List<T>,Dictionary<Tkey,TValue>,Queue<T>,Stack<T>。
2.非泛型容器:ArrayList,Hashtable(哈希表),Queue,Stack。
3.常见容器
- List<T>:存储有序,可重复元素,动态扩容。按索引访问速度快,插入删除效率低。
- HashSet<T>:存储不重复元素,快速判断元素是否存在。
- Queue(队列):先进先出,顺序处理数据。
- Stack(栈):先进后出,顶部添加,顶部移除。
17.字典(Dictionary)内部实现原理
1)概念
基于哈希表实现的键值对集合。
2)内部核心结构
- buckets数组:作为哈希表的"桶",存储entries数组的索引,初始长度为最小质数,动态扩容(翻倍为下一质数)。buckets[哈希值(索引)] = entries数组中的索引。
- entries数组:存储实际键值对,每一个元素是一个结构体(键,值,键的哈希码,下一索引)
- 辅助字段:count(键值对数量),version(版本号),freeList(空闲条目索引),freeCount(空闲条目数量)
3)哈希冲突的处理
- 哈希冲突:当两个及以上不同键计算出相同的桶的索引。
- 处理办法:将桶中的条目形成链表,也就是一个桶多个条目,而桶本身存储的是索引(对应的条目)是链表的头,通过Next(下一索引)将其串联。
4)关键操作
- 扩容:当哈希表的负载因子(元素数量/桶的数量)超过阈值,会自动扩容,重新计算所有元素的哈希值,分配到新桶,通过增加桶的数量,减少单个桶中链表的长度,维持效率。
- 查找:从对应的桶的第一个条目开始遍历链表(比较哈希码,再比较键,都对应返回值,否则返回false)
注:条目即为键值对,桶存储的数据是entries数组的索引,而buckets数组的索引是哈希值,entries数组存储的是键值对。
18.C#与C++中的哈希表
1. 底层实现
1)C# Hashtable:
- 基于 拉链法(Separate Chaining)实现,每个桶(Bucket)是一个链表。
- 使用 双散列 解决哈希冲突。
- 非泛型 ,键值类型为
object,存在装箱拆箱开销。 - 已被泛型
Dictionary<TKey, TValue>取代,但在旧代码中仍可见。
2)C++ std::unordered_map:
- 基于 开放寻址法(Open Addressing)或拉链法(不同编译器实现不同)。
- 泛型,键值类型在编译时确定,无类型转换开销。
- 通常通过模板实现,内存布局更紧凑。
2. 线程安全性
1)C# Hashtable:
- 默认非线程安全 ,但可通过
Hashtable.Synchronized方法创建线程安全版本。 - 线程安全版本使用锁机制,性能较低。
2)C++ std::unordered_map:
- 标准库实现非线程安全 ,需用户自行加锁 (如
std::mutex)。 - C++11 后支持
std::unordered_map的并发版本(如std::unordered_map+std::shared_mutex)。
3. 内存管理
1)C# Hashtable:由 .NET 垃圾回收器(GC)自动管理内存,无需手动释放。
2)C++ std::unordered_map:需手动管理内存(如插入/删除时构造/析构对象)。
4. 性能对比
| 特性 | C# Hashtable | C++ std::unordered_map |
|---|---|---|
| 查找时间复杂度 | O(1)(平均) | O(1)(平均) |
| 内存开销 | 较高(链表节点 + 装箱开销) | 较低(连续内存布局) |
| 线程安全支持 | 内置(但性能低) | 需手动实现 |
19.元数据与反射
1)元数据
- 概念:描述程序集内部所有结构信息的数据表。
- 注:反射API的核心就是读取元数据。
2)反射
- 概念:一种允许程序在运行时获取和操作类型信息(类,方法,对象和数据)的机制。
- 核心类:
1.Type:表示类型的元数据(是反射的 "入口",包含类的所有信息)。
2.Assembly:表示程序集,用于加载程序集并获取其中的类型。
3.MethodInfo:表示方法的元数据,用于动态调用方法。
4.PropertyInfo:表示属性的元数据,用于动态访问 / 修改属性。
5.FieldInfo:表示字段的元数据,用于动态访问 / 修改字段。
-
示例:
cspublic class Person { public string Name;// 字段 public int Age { get; set; }// 属性 public int Add(int a, int b) { return a + b; } }csclass ReflectionDemo { static void Main() { // 获取Type对象(反射的入口) Type personType = typeof(Person); // 获取所有公共属性 PropertyInfo[] properties = personType.GetProperties(); foreach (var prop in properties) { Console.WriteLine($"- 属性:{prop.Name},类型:{prop.PropertyType.Name}"); } // 获取所有公共方法 MethodInfo[] methods = personType.GetMethods(); foreach (var method in methods) { // 过滤掉从object继承的方法(如ToString、GetHashCode等) if (method.DeclaringType == personType) { Console.WriteLine($"- 方法:{method.Name}"); } } // 3. 动态创建对象(无需显式new Person()) Person person = (Person)Activator.CreateInstance(personType); // 调用无参构造函数 // 给字段赋值 FieldInfo nameField = personType.GetField("Name"); nameField.SetValue(person, "Alice"); // 等价于 person.Name = "Alice" // 给属性赋值 PropertyInfo ageProp = personType.GetProperty("Age"); ageProp.SetValue(person, 25); // 等价于 person.Age = 25 // 调用带参数的方法Add MethodInfo addMethod = personType.GetMethod("Add"); object result = addMethod.Invoke(person, new object[] { 3, 5 }); // 等价于 person.Add(3,5) Console.WriteLine($"3 + 5 = {result}"); // 输出 8 } } -
作用:分析类型结构,动态创建对象、调用方法、访问与修改属性和字段,加载并使用编译时未知的程序集(插件)。
20.for,foreach与Enumerator
1.for循环
- 通过索引访问集合元素。
- 可通过索引直接访问和修改集合元素。
2.foreach循环
- 依赖枚举器(Enumerator)实现遍历,无需索引。
- 不可以修改(添加/删除)集合元素。
3.Enumerator
- 枚举器接口本身。
- foreach循环的底层实现:获取枚举器对象(任何集合类对象均有GetEnumerator()),调用MoveNext()移动枚举器,Current属性获取当前元素,遍历结束自动调用Dispose()释放资源。
- 注:这个枚举器对象不是集合类对象,而是一个独立的类对象。
21.C#编译过程中的文件与概念
1.Unity中C#生命过程
- 编写:C#源代码
- 编译:编译器将源代码编译为IL与元数据,并打包成程序集文件
- 分发/部署:程序集文件被发布
- 执行:IL2CPP工具将IL与元数据转换为C++源代码,使用编译器将C++源代码编译成本地机器码,链接成本地可执行文件,在目标设备上直接执行本地机器码。
22.小知识点总结
1)A实例化赋给B,将B删除,A是否存在?
答:存在,B只是复制A栈中的地址,A的实例在堆中没有改变。
2)Foreach循环迭代时,若把其中的某个元素删除,程序报错,怎么处理?
答:由于foreach不能删除元素,因此需要记录找到索引或key值,迭代结束后再进行删除。
3)函数中多次使用string的+=处理,会产生大量内存垃圾,有什么好的方法可以解决?
答:使用StringBuilder高效拼接字符串,不会产生临时对象。
cs
StringBuilder sb = new StringBuilder(100000); // 预估容量,减少扩容
for (int i = 1; i <= 10000; i++)
{
sb.Append(i).Append(", ");
}
4)当需要频繁创建使用某个对象时,有什么好的程序设计方案来节省内存?
答:单例模式或者对象池。
5)单例模式违反了什么设计原则?
答:违反了依赖倒置原则。