元某人也元某人也

C# 知识点概要

C# 语言基础与常用语法知识点概要。

本文目录

C# 知识点概要


1. 方法重载和重写

说明
重载同一类中多个同名、形参不同的方法
重写用 virtual / override 使基类与派生类方法行为不同

注:抽象类的抽象方法必须在派生类重写;虚方法可以重写。


2. 面向对象三大特征

封装

将属性与方法结合,限制外部直接访问内部数据;通过公共方法访问,提高安全性。

继承

  • 作用:提高代码复用与可维护性,集中管理子类公共属性。
  • 特性:传递性、单根性(C# 单继承)。

多态

通过统一接口调用不同类对象,得到不同结果;主要体现为重载与重写。


3. 值类型和引用类型

值类型

项说明
存储直接存实际数据
类型int、float、bool、char、double、struct、enum 等
位置一般在栈(作为局部变量时)

引用类型

项说明
存储栈上存引用(地址),堆上存实际数据
类型string、object、class、interface、delegate
位置对象数据在堆

注:类的成员变量在堆中;方法内局部变量:值类型与引用各自按规则分配(值在栈,引用变量在栈、对象在堆)。


4. 堆与栈

栈

  • 内容:值类型、引用(地址)、参数、局部变量、返回地址等
  • 生命周期:随作用域自动创建/销毁
  • 内存:地址连续,无碎片问题

堆

  • 内容:引用类型的实际数据(如 new 出的对象)
  • 生命周期:由 GC 回收;有引用指向则继续存在
  • 内存:地址分散,可能有碎片

5. 装箱与拆箱

说明
装箱值类型 → 引用类型(如 object)
拆箱引用类型 → 值类型

常见触发

  • 值类型赋给 object 或接口变量
  • 以值类型调用 object/接口方法
  • 非泛型集合(如 ArrayList)存值类型

避免

  • 使用泛型集合 List<T>、泛型方法
  • 避免把值类型转为 object
  • 避免在 Unity 周期函数中频繁装拆箱
  • 高频值类型可用 NativeArray<T>

6. private、public 与 protected

修饰符访问范围
public任意类、任意成员
private仅当前类
protected当前类 + 派生类

7. ArrayList 与 List

ArrayListList<T>
类型非泛型,元素为 object泛型,类型安全
开销存取可能装/拆箱无类型转换,更高效

8. 接口与抽象类

接口

  • 完全抽象(只定义不实现,C# 8+ 可有默认实现例外此处按原文)
  • 多继承
  • 成员一般为 public
  • 多类实现同一接口 → 同一调用、不同效果

抽象类

  • 可有抽象方法(不实现)+ 具体方法(已实现)
  • 单继承
  • 成员可为 public / protected / private(private 不能修饰抽象方法)
  • 子类复用具体方法,并必须实现抽象方法
虚方法 virtual抽象方法 abstract
实现可有默认实现,子类可选 Override无实现,子类必须 Override
所在抽象类或普通类仅抽象类

9. Sealed 关键字

  • 修饰类:禁止被继承
  • 修饰方法:禁止派生类重写(需与 override 等配合使用场景见语言规范)

10. unsafe 关键字

  • 作用:允许指针操作,直接访问内存
  • 用法:可修饰代码块、方法、类、结构体

11. ref 与 out 关键字

ref

  • 调用前必须初始化
  • 传入现有值,方法内修改会影响实参
1
2
3
4
5
void AddOne(ref int num) { num++; }

int x = 5;
AddOne(ref x);
Console.WriteLine(x); // 6

out

  • 调用前无需初始化
  • 用于输出多个结果,方法内必须赋值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
bool 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
else
    Console.WriteLine(num); // 0

注意

  1. ref / out 参数不创建新存储位置,操作的是实参别名。
  2. 普通参数传的是引用的副本;ref 传的是变量本身的引用。

12. 结构体与类

结构体 struct

  • 值类型;赋值/传参产生副本
  • 不能无参构造(C# 10 前常见考点);可定义全参构造
  • 适合数据容器
  • 不能继承类,可实现接口;结构体赋给接口变量会装箱
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface IPrint { void ShowInfo(); }

[System.Serializable]
public struct PlayerInfo : IPrint
{
    public void ShowInfo() { }
}

public class StructInterfaceDemo : MonoBehaviour
{
    public PlayerInfo myPlayer;

    void Start()
    {
        myPlayer.ShowInfo();      // 无装箱
        RunShow(myPlayer);        // 泛型,无装箱

        // IPrint print = myPlayer; // 会装箱
        // print.ShowInfo();
    }

    void RunShow<T>(T target) where T : IPrint
    {
        target.ShowInfo();
    }
}

类 class

  • 引用类型;赋值/传递复制的是引用
  • 可有无参构造
  • 适合复杂行为、多实例、需要继承的场景

13. 构造函数

  • 概念:与类/结构体同名,创建对象时执行
  • 不能被继承;非静态构造函数每次 new 都会执行
类型说明
默认未写时编译器生成无参构造
有参初始化字段
重载多个构造函数,参数列表不同
静态static、无参,初始化静态成员,类首次使用时执行一次
1
2
3
4
5
6
7
8
9
public class Person
{
    public string Name;

    public Person(string name, int age)
    {
        Name = name;
    }
}

14. 泛型

  • 概念:类 / 接口 / 方法上的类型占位符,编译期确定具体类型

泛型类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Box<T>
{
    public T Value;
    public void SetValue(T value) => Value = value;
    public T GetValue() => Value;
}

Box<int> intBox = new Box<int>();
intBox.SetValue(100);
int num = intBox.GetValue();

泛型方法

即使类不是泛型,也可定义泛型方法。

泛型接口

1
2
3
4
5
public interface IIterable<T>
{
    bool HasNext();
    T Next();
}

泛型约束(常见)

约束含义
where T : classT 为引用类型
where T : structT 为值类型
where T : new()T 有无参公共构造函数
where T : IPrintT 实现某接口(示例见结构体一节)

15. 泛型容器与非泛型容器

泛型非泛型
List<T>ArrayList
Dictionary<TKey,TValue>Hashtable
Queue<T>Queue
Stack<T>Stack
容器特点
List<T>有序、可重复、动态数组;索引快,中间插删慢
HashSet<T>不重复;快速判断是否存在
QueueFIFO
StackLIFO

原文无第 16 节,下接第 17 节。


17. 字典(Dictionary)内部实现原理

1) 概念

基于哈希表的键值对集合。

2) 内部核心结构

结构作用
buckets[]桶数组,存 entries 的索引;下标为哈希值;初始为质数长度,扩容为下一质数
entries[]存条目结构体:键、值、Next(下一索引)
辅助字段count、version、freeList、freeCount

关系:buckets[哈希值] = entries 中的索引(链表头)。

3) 哈希冲突(拉链法)

  • 冲突:不同键算出相同桶下标
  • 处理:同桶多条目成链表,Next 串联

4) 关键操作

  • 扩容:负载因子超阈值 → 桶数量增加(常为质数)→ 重算哈希、重新挂链
  • 查找:从桶头遍历链表:比哈希码 → 比键 → 返回值或失败

注:条目=键值对;桶存的是 entries 索引;buckets 下标是哈希值;entries 存真实键值对。


18. C# 与 C++ 中的哈希表

1. 底层实现

C# Hashtable

  • 拉链法(Separate Chaining),桶为链表
  • 使用双散列处理冲突(原文表述)
  • 非泛型,object,有装拆箱
  • 已被 Dictionary<TKey,TValue> 取代,旧代码仍可见

C++ std::unordered_map

  • 开放寻址或拉链法(因编译器/实现而异)
  • 模板泛型,编译期定类型
  • 内存通常更紧凑

2. 线程安全

C# HashtableC++ unordered_map
默认非线程安全非线程安全
安全用法Hashtable.Synchronized(锁,性能低)用户自加 std::mutex 等

3. 内存管理

  • C#:GC 自动管理
  • C++:插入/删除时构造/析构,需关注对象生命周期

4. 性能对比

特性C# HashtableC++ std::unordered_map
查找(平均)O(1)O(1)
内存开销较高(链表 + 装箱)较低(布局更紧)
线程安全内置同步包装(性能低)需手动实现

19. 元数据与反射

1) 元数据

描述程序集内结构信息的数据;反射 API 本质是读取元数据。

2) 反射

运行时获取、操作类型信息(类、方法、字段、属性等)的机制。

类型作用
Type类型元数据入口
Assembly程序集,加载并枚举类型
MethodInfo方法,动态调用
PropertyInfo属性,动态读/写
FieldInfo字段,动态读/写
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Person
{
    public string Name;
    public int Age { get; set; }
    public int Add(int a, int b) => a + b;
}

class ReflectionDemo
{
    static void Main()
    {
        Type personType = typeof(Person);

        foreach (var prop in personType.GetProperties())
            Console.WriteLine($"- 属性:{prop.Name},类型:{prop.PropertyType.Name}");

        foreach (var method in personType.GetMethods())
            if (method.DeclaringType == personType)
                Console.WriteLine($"- 方法:{method.Name}");

        Person person = (Person)Activator.CreateInstance(personType);

        personType.GetField("Name").SetValue(person, "Alice");
        personType.GetProperty("Age").SetValue(person, 25);

        object result = personType.GetMethod("Add").Invoke(person, new object[] { 3, 5 });
        Console.WriteLine($"3 + 5 = {result}"); // 8
    }
}

用途:分析类型、动态创建对象、调用方法、访问成员、加载未知程序集(插件)等。


20. for、foreach 与 Enumerator

forforeach
访问索引枚举器,无索引
修改集合可按索引改元素不能在遍历中增删元素

Enumerator

  • GetEnumerator() → MoveNext() → Current → 结束 Dispose()
  • 枚举器是独立对象,不是集合本身

21. C# 编译过程中的文件与概念

Unity 中 C# 大致生命周期

  1. 编写:C# 源代码(.cs)
  2. 编译:编译为 IL + 元数据,打包为程序集(.dll)
  3. 分发/部署:发布程序集
  4. 执行(IL2CPP):IL2CPP 将 IL + 元数据转为 C++ → 再编译为本地机器码 → 链接为可执行文件 → 设备上运行本地码
  flowchart LR
    A[.cs 源码] --> B[IL + 元数据 / 程序集]
    B --> C[IL2CPP]
    C --> D[C++]
    D --> E[本地机器码]

22. 小知识点总结

1)A 实例化赋给 B,删除 B,A 是否存在?

存在。B 只是复制了栈上的引用;堆上由 A 创建的对象仍在(若无其它引用则由 GC 回收)。

2)foreach 中删除元素报错怎么办?

foreach 不能在迭代时删元素。应先记录索引或 key,遍历结束后再删除。

3)函数里多次 string += 产生 GC 怎么办?

用 StringBuilder,减少临时字符串对象。

1
2
3
4
5
6
StringBuilder sb = new StringBuilder(100000); // 预估容量,减少扩容

for (int i = 1; i <= 10000; i++)
{
    sb.Append(i).Append(", ");
}

4)频繁创建同一类对象如何省内存?

单例模式或对象池。

5)单例模式违反了什么设计原则?

依赖倒置原则(具体类被硬编码依赖,不利于替换与测试)。


要点速查

主题一句话
重载 vs 重写同名不同参 vs virtual/override
值 / 引用栈+副本 vs 栈引用+堆对象
装拆箱值↔object,用泛型规避
struct vs class值类型小数据 vs 引用类型行为/继承
Dictionarybuckets + entries 拉链法
反射读元数据,运行时操作类型
UnityIL → IL2CPP → 本地码

补充:类与结构体、foreach

两个容易被“会用”掩盖的问题:值类型和引用类型到底差在哪,foreach 编译后成了什么。

1. 类和结构体的区别以及具体的使用场景

类(Class)
  • 类属于引用类型,
  • 内存分配在堆上,传递方式时直接去传递引用地址,会产生GC垃圾
  • 支持继承和多态
  • 数据结构较大时:如果一个数据结构包含大量字段,每次传递都复制整个对象(结构体的行为)会带来巨大的性能开销。此时,传递一个引用(类的行为)会高效得多。
结构体(struct)
  • 结构体属于值类型
  • 内存分配在栈或者堆上,传递值的副本,不会产生GC垃圾
  • 不支持继承但可以实现接口
结构体经典的应用场景

Unity内置范例:Vector2、Vector3、Quaternion、Color、RaycastHit等都是结构体。它们都是小型、不可变的数据集合。

小型、纯数据聚合体:

  • 当你的数据结构非常小(官方建议小于16字节),
  • 主要用来组合几个简单数据,
  • 不会被频繁地作为参数传递
  • 它不会被频繁地装箱
堆栈内存本质:
栈:
  • 每个线程独立拥有,是一个后进先出(LIFO)的弹夹,只在栈顶操作
  • 访问速度极快,分配内存是栈顶指针移动操作,相当于CPU一个指令周期
  • 大小固定一般默认只有1到4MB不能分配过大的值类型,否则会引发栈溢出
  • 内存自动分配与释放,当一个变量超出作用域时,系统会立即确定性的将其从栈顶弹出并释放你存无GC开销,执行效率高,
堆:
  • 进程共享,大小动态增长,
  • 访问速度相对较慢,分配时需要遍历堆中空闲的内存块,找到最够大的空间,并维护复杂的内存表,频繁分配会产生内存碎片,进一步降低速度
  • 大小比较大,所有需要占用大内存的对象都必须放在堆上。
  • 需要手动管理或者Mono Gc管理,释放闲置内存时时会产生GC停顿,内存碎片化严重,需要GC进行压缩整理
值类型可能存在于堆上:

当值类型作为引用类型的字段、被装箱、或在迭代器 / 异步方法中捕获时,会被分配到堆上

局部变量中的类实例(引用类型)→ 实例的实际数据存放在堆,而指向该数据的引用指针(IntPtr)存放在栈。

2. Foreach

一、foreach的底层编译展开
原foreeach代码
1
2
3
4
5
List<int> list = new List<int> { 1,2,3 };
foreach (var item in list)
{
    Console.WriteLine(item);
}
编译器翻译成的等价代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
IEnumerator<int> enumerator = list.GetEnumerator();//调用集合 GetEnumerator() 获取迭代器对象;
try
{
    //循环调用 MoveNext(),返回 true 就取出 Current;MoveNext() 返回 false 代表遍历结束,退出循环。
    while (enumerator.MoveNext())
    {
        int item = enumerator.Current;
        Console.WriteLine(item);
    }
}
finally
{
    // 释放迭代器(IDisposable)
    enumerator.Dispose();
}

一轮遍历走完就销毁迭代器,不会跨帧保留;

二、和for的区别
  • for循环是基于索引直接访问集合,通过维护局部变量,改变下表直接寻址取值,全程不产生额外的堆对象
  • foreach循环是基于迭代器模式的会创建迭代器对象,产生额外的迭代器状态机对象产生GC,靠MoveNext()/Current诸葛获取元素;
  • for循环只支持有索引结构的数据结构:Array,List等,支持通过下标修改元素,可在循环内安全增删合集
  • foreach只要实现IEnumerable就能用,不用关心内部存储结构,不能修改集合长度,item是拷贝,修改item不影响原容器,只能从头到尾顺序遍历,无法自定义步长,无法倒序遍历,必须现反转合集
  • foreach 会创建迭代器,带来微小 GC 开销,一帧内连续不停的调用MoveNext一次性遍历完。

本文由 2 篇笔记合并整理。

仅用于交流学习,记录学习心得