31.C#进阶语法知识总结

  1. 31.总结
    1. 31.1 知识点
      1. 学习的主要内容
      2. 注意
      3. 强调
    2. 31.2 核心要点速览
      1. 数据结构存储概念回顾
      2. 简单数据结构类
      3. 泛型
        1. 泛型相关分类对比
        2. 泛型约束(where)
        3. 何时用泛型
      4. 常用泛型数据结构
      5. 委托(Delegate)
      6. 事件(Event)
      7. 匿名函数(Anonymous Function)
      8. Lambda 表达式
      9. 闭包(Closure)
      10. List.Sort
      11. 协变(Covariance) 和 逆变(Contravariance)
        1. 语法示例
        2. 接口中使用
        3. 适用范围
      12. 多线程
        1. 理解进程 vs 线程
        2. 启动新线程的关键步骤
        3. 线程控制与调度
        4. 线程间共享数据与加锁
        5. 多线程的价值
      13. 编译器与预处理器指令
      14. 反射
        1. 程序集和元数据
        2. 什么是反射
        3. 反射相关核心类与用途
        4. 使用反射的经典场景
        5. 注意事项
      15. 特性
        1. 特性概念
        2. 自定义特性
        3. 特性使用
      16. 反射读取特性
        1. 限制使用范围
        2. 常用系统特性
      17. 迭代器
        1. 迭代器作用
        2. 实现标准迭代器(传统写法)
        3. foreach 本质流程
        4. yield return 语法糖实现
        5. 泛型迭代器实现(配合 yield return)
        6. 迭代器总结
      18. 特殊写法
        1. var隐式类型
        2. 对象初始化简写
        3. 集合初始化简写
        4. 匿名类型
        5. 可空类型 Nullable
        6. 空合并操作符 ??
        7. 字符串内插
        8. 单句逻辑简写
      19. 值和引用知识补充
        1. 如何判断值类型与引用类型
        2. 语句块与变量作用域
        3. 变量生命周期
        4. 使用高层变量记录底层变量
        5. 结构体中的值类型与引用类型
        6. 类中的值类型与引用类型
        7. 数组存储规则
        8. 结构体继承接口(装箱/拆箱)
    3. 31.3 面试题精选
      1. 基础题
        1. 1. ArrayList 与 List<T> 的区别与选型
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 委托与事件的区别
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 顺序存储与链式存储的选型
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      2. 进阶题
        1. 1. 泛型约束 where
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 闭包与 for 循环中的 lambda 陷阱
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 协变与逆变(out / in)
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      3. 深度题
        1. 1. 多线程:lock 与 IsBackground
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 反射的概念与常见用法
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章

31.总结


31.1 知识点

学习的主要内容

注意

强调



31.2 核心要点速览

数据结构存储概念回顾

  • 顺序存储(Array, List)

    • 元素紧密连续
    • :O(1)
    • 增/删(中间):O(n)
  • 链式存储(LinkedList)

    • 通过指针链接
    • 增/删:O(1)(在已知节点位置)
    • 查/改:O(n)(需遍历)

简单数据结构类

命名空间:using System.Collections;。以下均为 object 存储,存在装箱拆箱。

容器 底层结构 存取规则 常用操作 遍历方式
ArrayList 可变长 object[] 任意索引增删改 - 增:AddAddRangeInsert
- 删:RemoveRemoveAtClear
- 查改:索引器 list[i] get/set、ContainsIndexOfLastIndexOf
- for (i=0…Count-1)
- foreach
- 查看容量:Count / Capacity
Stack object[] 栈,LIFO 只能顶端进出 - Push(item)
- Pop()(移除并返回顶)
- Peek()(查看顶,不移除)
- ContainsClearCount
- foreach(从顶到底)
- ToArray() + for
- while (Pop())
Queue object[] 队列,FIFO 只能队首进队首出 - Enqueue(item)
- Dequeue()(移除并返回队首)
- Peek()(查看队首,不移除)
- ContainsClearCount
- foreach(从队首到队尾)
- ToArray() + for
- while (Dequeue())
Hashtable 键/值对散列表,object 按键存取 - Add(key,value)
- Remove(key)Clear
- 索引器 ht[key] get/set(key 不存在返 null)
- ContainsKeyContainsValue
- foreach (DictionaryEntry kv in hashtable)
- foreach (var k in Keys) + ht[k]
- IDictionaryEnumerator

Tip: 如类型已知,优先使用泛型版本(List<T>/Stack<T>/Queue<T>/Dictionary<TKey,TValue>),以避免装箱拆箱并增强类型安全。

泛型

泛型相关分类对比

分类 语法示例 说明
泛型类 class MyList<T> 在声明时使用类型占位符T,使用时指定具体类型。
泛型接口 interface IRepository<T> 与泛型类类似,可定义泛型属性/方法。
泛型方法 void Swap<T>(ref T a, ref T b) 方法级别的类型占位,可与泛型类并存;占位符可多个。

泛型约束(where

约束符 形式 含义 例子
struct where T : struct T 必须是值类型(包括所有结构体) Test1<T> where T: struct
class where T : class T 必须是引用类型 Test2<T> where T: class
new() where T : new() T 必须有公共无参构造函数 Test3<T> where T: new()
类名 where T : BaseType T 必须是指定类或其派生类 Test4<T> where T: Animal
接口名 where T : IFly T 必须实现指定接口 Test5<T> where T: IFly
泛型参数 where T : U T 必须是另一个泛型参数 U 或其派生类 Test6<T,U> where T: U

组合示例

class Manager<T, K>
    where T : class, new()
    where K : struct
{ }

注意:class 须写在 new() 前,否则报错;structnew() 不可组合(struct 自带无参构造)。

何时用泛型

  • 代码复用:同一数据结构/算法,可用于多种类型。
  • 类型安全:避免向 object 容器中装箱拆箱。
  • 性能优化:减少运行时类型转换和垃圾产生。

常用泛型数据结构

容器 本质 常用操作 遍历方式
List<T> 可变长度的泛型数组 AddAddRangeInsert
RemoveRemoveAtClear
list[i] = x
索引器 list[i]
Contains
IndexOf/LastIndexOf
for(int i=0;i<Count;i++)
foreach(var x in list)
Dictionary<K,V> 泛型的键/值对哈希表 Add(key,value)
Remove(key)Clear
dict[key] = v
索引器 dict[key](key 不存在抛异常,Hashtable 返 null)
TryGetValue/ContainsKey 避免
foreach(var kv in dict)
foreach(var k in Keys)
LinkedList<T> 泛型的双向链表 AddFirstAddLastAddBeforeAddAfter
RemoveRemoveFirstRemoveLast
node.Value = x
First/Last
Find(item)
Contains
foreach(var x in ll)
for(node=First; node!=null; node=node.Next)
Stack<T> 泛型的后进先出栈(LIFO) Push(item)
Pop()
Peek()
Clear()
Contains(item) foreach(var x in stack)
while(stack.Count>0){ var x=stack.Pop(); }
Queue<T> 泛型的先进先出队列(FIFO) Enqueue(item)
Dequeue()
Peek()
Clear()
Contains(item) foreach(var x in queue)
while(queue.Count>0){ var x=queue.Dequeue(); }

容器选择:固定一组数据用数组;常按下标查、常增删用 List;常按 key 查用 Dictionary;先进后出用 Stack(如 UI 面板栈);先进先出用 Queue(如消息队列);常中间插入/删除、少按索引查用 LinkedList。

委托(Delegate)

  1. 概念

    • 委托是“方法的容器”——将方法视为可存储、传递的第一类对象。
    • 定义了一种签名(返回值类型 + 参数列表),只有与该签名完全匹配的方法才能被存入。
  2. 声明语法

    // namespace 或 class 中都可声明
    [public] delegate 返回类型 委托名(参数类型1 名称1, 参数类型2 名称2, …);
    
    • public delegate void MyAction(int x, string y);
    • 与方法声明几乎相同,只是前置 delegate 关键字。
  3. 绑定与调用

    MyAction a1 = new MyAction(SomeMethod);
    MyAction a2 = SomeMethod;        // 语法糖
    a1(arg1, arg2);                  // 等价于 a1.Invoke(arg1, arg2);
    
    • 绑定时不带括号;调用时既可使用 Invoke 也可直接像方法一样写 a1()
  4. 多播功能

    • 可以向一个委托变量中累加多个方法:

      a += MethodA;
      a += MethodB;
      
    • 调用时会按添加顺序依次执行;用 -= 移除首个匹配的方法。

    • 执行前要判空: if (a != null) a();

    • 有返回值时多播委托只得到最后一项返回值;若需每一项的返回值,用 GetInvocationList() 得到委托数组再逐项 Invoke

  5. 作为成员和参数

    • 成员字段:用于类内部或外部的事件/回调存储。

      public MyAction OnComplete;
      
    • 方法参数:允许调用者传入自己的逻辑到方法内部延后执行。

      void DoSomething(MyAction callback) { … callback(); }
      
  6. 系统内置委托

    • Action:无返回值,支持 0–16 个输入参数。
    • **Func<…>**:有返回值,支持 0–16 个输入参数,最后一个泛型参数为返回类型。
  7. 使用场景

    • 事件处理与回调机制(UI 事件、异步完成通知等)
    • 策略模式或动态行为切换
    • LINQ、异步编程及并行执行的基础

事件(Event)

  1. 概念

    • 事件是“安全封装”的委托,只能在声明它的类/结构体/接口内部被触发或清空。
    • 用于发布—订阅模式,防止外部随意重置或调用委托。
  2. 声明语法

    [public] event 委托类型 事件名;
    // 例如:
    public event Action OnCompleted;
    
  3. 与委托的异同

    特性 委托(Delegate) 事件(Event)
    赋值/清空 在任何地方可用 = 只能在类内部可用 +=/-=
    触发/调用 在任何地方可直接 Invoke() 只能在声明它的类内部触发(调用)
    作为局部变量 可以 不可(编译器禁止)
    主要用途 通用回调、函数指针模拟 发布—订阅、安全的回调管理
  4. 使用流程

    1. 在类中声明事件:

      public event Action DataReady;
      
    2. 外部订阅/退订:

      obj.DataReady += Handler;    // 注册处理器
      obj.DataReady -= Handler;    // 注销处理器
      
    3. 内部触发:

      if (DataReady != null)
          DataReady();             // 或 DataReady.Invoke();
      

匿名函数(Anonymous Function)

  1. 定义

    • 没有名称的方法体,可直接内联声明。

    • 形如:

      delegate(参数列表)
      {
          // 方法体
      };
      
  2. 何时使用

    • 作为委托参数:简化回调传递,无需额外命名方法。
    • 作为委托返回值:动态构建并返回方法逻辑。
    • 典型场景:事件处理器、LINQ 查询、异步回调等。
  3. 基本语法变体

    // 无参无返回
    Action a = delegate() { /*…*/ };
    
    // 有参无返回
    Action<int, string> a2 = delegate(int x, string s) { /*…*/ };
    
    // 无参有返回
    Func<string> f = delegate() { return "hello"; };
    
    // 直接传递给方法
    obj.DoSomething(delegate(int x) { Console.WriteLine(x); });
    
  4. 优点

    • 内联:无需额外命名、减少样板代码。
    • 临时性:回调逻辑就地定义,更具可读性。
  5. 缺点

    • 无法单独移除:因为没有名称,delegate { … } 无法用 -= 精确移除单个匿名条目。
    • 调试:栈跟踪里看不到方法名,仅显示“anonymous method”。
  6. 最佳实践

    • 小而精的回调逻辑适合匿名函数。
    • 复杂或需多次移除的处理器,应使用具名方法并存到变量中,以便管理。

Lambda 表达式

  • 定义:匿名函数的简洁写法,用 => 分隔参数和函数体。

  • 语法

    (参数列表) => { /* 函数体 */ };
    

    与匿名函数等价:

    delegate(参数列表) { /* 函数体 */ };
    
  • 示例

    // 无参无返回
    Action a = () => Console.WriteLine("Hello");
    
    // 单参无返回(类型可省略)
    Action<int> a2 = x => Console.WriteLine(x);
    
    // 多参有返回
    Func<int, int, int> add = (x, y) => x + y;
    
  • 使用场景

    • 传递给需要委托/事件的 API(如 LINQ、事件绑定)。
    • 简化一次性短逻辑,无需额外命名。
  • 优缺点

    • 优点:更简洁、易读,省去 delegate 关键字和方法名。
    • 缺点:与匿名函数一样,无法精确地从多播委托/事件中移除单个 lambda。

闭包(Closure)

  • 概念:函数(或委托)连同其定义时所处环境(局部变量)一起“打包”,并保持对这些外部变量的访问能力,即使外层作用域已结束。

  • 核心特点

    • 捕获的是变量本身的引用,而非当时的值。
    • 外层局部变量的生命周期被延长,直到所有引用它的闭包都不再使用为止。
  • 示例

    Func<int, int> Counter(int increment)
    {
        int count = 0;                   // 被捕获的外部变量
        return number => {               // 闭包 lambda
            count += increment;
            return number + count;
        };
    }
    
    var c = Counter(5);
    Console.WriteLine(c(10)); // 15  (count从0变为5)
    Console.WriteLine(c(20)); // 25  (count从5变为10)
    
  • for 循环与闭包陷阱

    for (int i = 0; i < 3; i++)
    {
        list.Add(() => Console.WriteLine(i));
    }
    // 调用后,所有 lambda 都打印“3”……
    

    解法:在循环体内引入新的局部变量:

    for (int i = 0; i < 3; i++)
    {
        int copy = i;
        list.Add(() => Console.WriteLine(copy)); // 捕获 copy 而非 i
    }
    
  • 注意事项

    • 被捕获的局部变量只释放于闭包不再引用它时。
    • 过度使用闭包可能隐藏内存泄漏风险。

List.Sort

  1. 默认排序

    • 方法list.Sort()
    • 要求元素类型:必须实现 IComparable<T>(或非泛型 IComparable)。
    • 行为:调用元素的 CompareTo,小于 0 放前面,等于 0 保持, 大于 0 放后面;默认升序。
  2. 自定义类型实现接口排序

    • 接口IComparable<Item>

    • 实现

      public class Item : IComparable<Item>
      {
          public int money;
          public int CompareTo(Item other)
              => this.money.CompareTo(other.money);
      }
      
    • 调用

      var items = new List<Item>{...};
      items.Sort();  // 调用 Item.CompareTo
      
  3. 传委托 Comparison 排序

    • 方法重载list.Sort(Comparison<T> comparison)

    • 定义 Comparison 函数

      static int CompareShop(ShopItem a, ShopItem b)
          => a.id.CompareTo(b.id);
      
    • 调用

      shopList.Sort(CompareShop);
      
    • 匿名函数

      shopList.Sort(delegate(ShopItem a, ShopItem b) {
          return a.id - b.id;
      });
      
    • Lambda 写法

      shopList.Sort((a,b) => a.id - b.id);
      
  4. 自定义排序要点

    • 返回值<0 → 前, 0 → 不变, >0 → 后。
    • 升序 vs 降序:调换返回值符号(或参数顺序)可实现降序。
    • 性能List<T>.Sort 使用快速排序和插入排序的混合实现,平均 O(n log n)。
    • 本系列还包含插入/希尔/归并/快速/堆排序的手写实现;应用层掌握 List.Sort + Comparison<T> 即可。

协变(Covariance) 和 逆变(Contravariance)

特性 关键字 修饰位置 用途限制 里氏替换示例
协变 out 泛型接口 / 泛型委托 只能用于返回值(不能作参数) TestOut1<Father> f = sonProvider;
Func<Son> 可赋给 Func<Father>
逆变 in 泛型接口 / 泛型委托 只能用于参数(不能作返回值) TestIn1<Son> s = fatherConsumer;
Action<Father> 可赋给 Action<Son>

语法示例

  • 协变委托

    public delegate T Producer<out T>();
    Producer<Son> sonProducer = () => new Son();
    Producer<Father> fatherProducer = sonProducer; // OK (Son → Father)
    
  • 逆变委托

    public delegate void Consumer<in T>(T arg);
    Consumer<Father> fatherConsumer = f => { /*…*/ };
    Consumer<Son> sonConsumer = fatherConsumer; // OK (Father → Son)
    

接口中使用

  • 协变接口

    interface IReadOnly<out T>
    {
        T GetItem(int index);
        // void Add(T item); // ❌ 不允许
    }
    
  • 逆变接口

    interface IWriteOnly<in T>
    {
        void Add(T item);
        // T GetItem(); // ❌ 不允许
    }
    

适用范围

  • 仅限:泛型 接口泛型委托
  • 不适用:泛型类、结构体等(会编译报错)

核心思想:用 out 标记“生产者”→只输出;用 in 标记“消费者”→只输入,二者都借助里氏替换原则让“父⇄子”泛型能够安全转换。

多线程

理解进程 vs 线程

  • 进程(Process):程序在操作系统中的一次运行实例,拥有独立的内存和资源。
  • 线程(Thread):进程内可并发执行的最小调度单位,共享所在进程的资源和内存。

启动新线程的关键步骤

  1. 声明循环控制标识(若线程内部需要持续运行/可停止):

    static bool isRunning = true;
    
  2. 封装线程逻辑方法(无参无返回值):

    static void NewThreadLogic()
    {
        while (isRunning)
        {
            // … 执行周期性或耗时任务 …
        }
    }
    
  3. 创建并启动线程(需 using System.Threading;):

    Thread t = new Thread(NewThreadLogic);
    t.IsBackground = true;  // 设置为后台线程
    t.Start();              // 启动
    
  4. 结束线程

    • 安全停止:在主线程中 isRunning = false;,让循环自然退出

    • 强制终止(.NET Core 不推荐/可能不支持):

      t.Abort();
      t = null;
      

线程控制与调度

  • Thread.Sleep(ms):让当前线程休眠指定毫秒。
  • IsBackground:将线程标记为后台,主线程结束时后台线程随进程一同退出。

线程间共享数据与加锁

  • 进程内所有线程共享同一内存,并发访问同一资源可能造成冲突。

  • 锁(lock) 用法:

    static object lockObj = new object();
    …
    lock (lockObj)
    {
        // 仅允许一个线程进入此代码块
    }
    
  • 场景示例:一个线程绘制红色圆,一个线程绘制黄色方块,通过 lock 保证绘制命令的原子性,避免光标定位和颜色设置交叉干扰。

多线程的价值

  • 耗时操作(如网络请求、寻路、文件 I/O、复杂计算)放到子线程,避免阻塞主线程,保持界面或逻辑的流畅响应。

编译器与预处理器指令

  • 编译器:将高级语言翻译为机器能识别的中间代码或目标语言(如 DLL 或 EXE)。

  • 预处理器指令:以 # 开头,用于编译前的判断与控制,不以分号结尾。

  • 常用指令

    • #define / #undef:定义或取消符号。
    • #if / #elif / #else / #endif:条件编译,控制不同代码块是否参与编译。
    • #warning / #error:编译时发出警告或终止报错。
    • #region / #endregion:代码折叠用,提升可读性。
  • 用途

    • 实现跨平台或版本适配(如判断 Unity 版本)。
    • 控制测试/调试代码是否参与编译。
    • 提前在编译前排除无效路径,提高开发效率。

反射

程序集和元数据

  • 程序集:编译后的代码集合(DLL/EXE),包含元数据、IL代码等。
  • 元数据:程序中的结构信息(类、字段、方法等),用于描述程序自身。

什么是反射

  • 反射:程序运行时动态查看、调用、修改自身或其他程序集的结构和行为(类型、构造、字段、方法等)。
  • 意义:提升程序扩展性、灵活性,可用于插件、脚本系统、序列化、Unity编辑器拓展等。

反射相关核心类与用途

  • Type

    • 获取类型信息:typeof(ClassName)obj.GetType()Type.GetType("全限定名")类名须含命名空间,否则找不到)
    • 获取成员:GetFields()GetMethods()GetConstructors()GetMembers()(需 using System.Reflection
    • 获取特定成员并操作:GetField("name")GetMethod("name")、使用 SetValue/GetValue/Invoke
  • ConstructorInfo / FieldInfo / MethodInfo

    • 反射构造函数并 Invoke() 实例化对象
    • 获取/设置字段值
    • 调用方法
  • Activator

    • 快速实例化对象:Activator.CreateInstance(type, 参数...)
    • 不需要手动查找构造函数,更简洁
  • Assembly

    • 动态加载程序集(DLL):Load() / LoadFrom() / LoadFile()
    • 通过程序集获取类型并反射使用其中的类

使用反射的经典场景

  • 动态插件加载
  • 编辑器工具开发(如自定义 Inspector)
  • 自定义序列化/反序列化
  • 事件总线、属性注入(依赖注入)

注意事项

  • 反射性能略低,不宜频繁使用
  • 非公共成员默认无法访问,需指定 BindingFlags 或使用私有访问策略
  • 参数类型不匹配、类不存在等问题会导致运行时报错(非编译期)

特性

特性概念

  • 本质是类,为程序结构添加元数据
  • 可修饰类、方法、字段等,配合反射读取使用。

自定义特性

  • 继承自 Attribute
  • 命名惯例:XXXAttribute(使用时可省略后缀)。
  • 构造函数决定传参格式。

特性使用

  • 语法:[特性名(参数)]
  • 放在声明上一行,实质是构造函数调用。

反射读取特性

  • IsDefined():判断是否应用某特性。
  • GetCustomAttributes():获取全部特性对象并强转使用。

限制使用范围

  • [AttributeUsage(AttributeTargets.XX | …, AllowMultiple = bool, Inherited = bool)]:指定可用位置、同一目标是否允许多个实例、是否被派生/重写继承。

常用系统特性

  • Obsolete:标记过时成员,支持警告或错误提示。
  • 调用者信息CallerFilePath / CallerLineNumber / CallerMemberName
  • Conditional:配合 #define 控制方法是否编译。
  • DllImport:绑定调用 C/C++ DLL 中的方法。

迭代器

迭代器作用

  • 提供统一方式遍历集合,不暴露内部结构。
  • 可用 foreach 遍历的类型,都实现了迭代器模式。

实现标准迭代器(传统写法)

  • 接口与命名空间:IEnumerableIEnumeratorusing System.Collections;

  • 实现接口:IEnumerable(用于遍历) + IEnumerator(用于控制位置)。

  • 核心方法:

    • GetEnumerator():返回自身(IEnumerator);
    • MoveNext():移动光标;
    • Current:返回当前项;
    • Reset():重置光标(避免 foreach 多次使用时无效)。

foreach 本质流程

  1. 调用对象的 GetEnumerator()
  2. 不断调用 MoveNext() → 若为 true 则读取 Current
  3. 否则结束循环。

yield return 语法糖实现

  • 只需继承 IEnumerable,简化逻辑。
  • 系统自动生成 IEnumerator 实现。
  • 每次 yield return 会记录当前位置并在下次继续。

泛型迭代器实现(配合 yield return)

  • 自定义类继承 IEnumerable
  • yield return 返回泛型元素,无需额外泛型接口。
  • 可用于任意类型集合的遍历。

迭代器总结

  • 迭代器支持 foreach 的根本。

  • 两种方式实现:

    • 传统:实现 IEnumerator + IEnumerable
    • 简洁:使用 yield return + IEnumerable
  • 重点掌握 yield return,写法简单且易维护。

特殊写法

var隐式类型

  • var 只能用于局部变量,且必须初始化。
  • 推导类型后不可更改;适合临时变量或简化代码。

对象初始化简写

  • 使用 {} 初始化对象的公共字段和属性。
  • 可与构造函数同时使用,花括号内赋值优先级更高

集合初始化简写

  • 数组、List、Dictionary 可在声明时使用 {} 初始化元素。
  • List 可以包含自定义对象,支持嵌套初始化。

匿名类型

  • 使用 var obj = new {} 创建临时、无命名的类型。
  • 只支持字段,不支持方法。
  • 常用于局部临时数据封装。

可空类型 Nullable

  • 值类型后加 ? 可为空(如 int?),底层是 Nullable<T>

  • 常用属性方法:

    • HasValue:是否有值;
    • Value:取值;
    • GetValueOrDefault():空时返回默认值;
    • ?.:语法糖,判空后再操作。

空合并操作符 ??

  • 表达式:a ?? b
  • a 为 null,返回 b;否则返回 a
  • 可用于引用类型和可空值类型的默认值处理。

字符串内插

  • 使用 $"{变量}" 进行字符串拼接,更清晰简洁

单句逻辑简写

  • if / for / while 等语句中只有一句代码时可省略 {}

  • 支持属性、方法的简写:

    • get => value;
    • 方法名 => 逻辑;

值和引用知识补充

如何判断值类型与引用类型

  • 使用 F12 查看源码定义:struct 是值类型,class 是引用类型。

语句块与变量作用域

  • 上层语句块:类、结构体;中层:函数;底层:条件分支/循环。
  • 成员变量在上层;临时变量在中、底层。
  • 代码逻辑主要写在函数、条件、循环中。

变量生命周期

  • 成员变量长期存活;临时变量随语句块结束回收。
  • 引用类型:栈上地址被释放,堆中数据变垃圾待 GC。
  • 减少循环内重复创建临时变量有助于性能。

使用高层变量记录底层变量

  • 用成员字段或静态变量记录函数内的中间值,避免被销毁。

结构体中的值类型与引用类型

  • 结构体本身是值类型,存在栈中。
  • 内部值类型存具体值;引用类型存地址,指向堆中对象。

类中的值类型与引用类型

  • 类本身是引用类型,存在堆中。
  • 值类型成员与类共存在堆;引用类型成员是指向另一个堆空间的地址。

数组存储规则

  • 数组是引用类型,栈上存地址。

    • 值类型数组:堆中存实际值。
    • 引用类型数组:堆中存的是其他堆地址(默认值为 null)。

结构体继承接口(装箱/拆箱)

  • 结构体实现接口会发生 装箱(复制到堆)。
  • 接口变量互赋共享地址,修改其中一个影响另一个。
  • 接口强转为结构体会发生 拆箱(复制回栈)。
  • 装箱带来性能开销,结构体应谨慎使用接口。

31.3 面试题精选

基础题

1. ArrayList 与 List<T> 的区别与选型

题目

ArrayList 和 List<T> 有什么区别?实际开发中更推荐用哪个,为什么?

深入解析

ArrayListSystem.Collections):本质是 object[],元素以 object 存储。存取任意类型方便,但存在装箱拆箱:存 int 时装箱成 object,取出要拆箱,有性能开销和类型安全隐患。

List<T>System.Collections.Generic):本质是可变长度的泛型数组,编译期确定类型。无装箱拆箱、类型安全,且 API 与 ArrayList 类似(Add、Remove、Insert、索引器等)。

实际开发中**更推荐 List<T>**:类型已知时用泛型集合,避免装箱拆箱、提高性能、减少运行时类型错误。同理,键值对推荐 Dictionary<K,V> 而非 Hashtable;栈、队列推荐 Stack<T>Queue<T> 而非非泛型版本。

答题示例

ArrayList 底层是 object 数组,什么都能装,但存值类型会装箱、取的时候要拆箱,有性能损耗,而且取出来要自己强转,容易出错。List<T> 是泛型列表,编译期就定好类型,没有装箱拆箱,也更安全。所以只要类型能确定,我们都用 List<T>、Dictionary 这类泛型集合,不用 ArrayList、Hashtable。

参考文章
  • 2.简单数据结构类-ArrayList
  • 6.泛型-泛型
  • 8.常用泛型数据结构类-List

2. 委托与事件的区别

题目

C# 里委托和事件有什么区别?事件为什么不能在类外部赋值和调用?

深入解析

委托:方法的容器,可存储、传递与委托签名匹配的方法。可作为类的成员、方法参数、局部变量;在任意位置都能用 = 赋值、用 +=/-= 增减,以及直接 Invoke()变量() 调用。

事件:基于委托的封装,语法是 event 委托类型 事件名。用法上“委托怎么用事件就怎么用”,但有三点限制:

  1. 外部不能赋值:不能写 obj.myEvent = null= 某方法,只能 +=/-= 订阅/退订。
  2. 外部不能调用:只能在声明事件的类内部触发(如 if (myEvent != null) myEvent();)。
  3. 不能作为局部变量:不能写 event Action e = ...

这样做的目的:防止外部把委托置空或随意调用,保证只有发布方能在内部触发,订阅方只能订阅/退订,符合发布-订阅模式,更安全。

答题示例

委托就是装方法的变量,哪里都能赋值、哪里都能调用。事件是在委托外面包了一层:外部只能用 +=、-= 订阅退订,不能直接用 = 赋值,也不能在类外面 Invoke,只能由类内部触发。这样就不会被外部随便清空或乱调,更适合做事件通知、UI 回调这类“只有发布方能发,订阅方只能听”的场景。

参考文章
  • 13.委托和事件-委托
  • 14.委托和事件-事件

3. 顺序存储与链式存储的选型

题目

List<T> 和 LinkedList<T> 底层分别是什么结构?在“查多、按索引访问”和“头尾/中间频繁增删”两种场景下,该怎么选?

深入解析

List<T>:顺序存储,底层是数组。按索引查、改为 O(1);在中间插入、删除需要移动元素,为 O(n)。适合按下标访问多、尾部增删多的场景。

LinkedList<T>:链式存储,双向链表。在已知节点位置时,头尾或中间插入、删除为 O(1);但按值查找、按位置访问需要遍历,为 O(n)。适合频繁在头尾或已知节点前后增删、很少按索引随机访问的场景。

选型:固定一组数据、或常按下标查、尾部增删多 → 用 List 或数组;常从中间插入删除、且很少按索引查 → 用 LinkedList。例如 UI 面板栈用 Stack,消息队列用 Queue;需要“中间插入删除多、随机访问少”时再考虑 LinkedList。

答题示例

List 底层是数组,顺序存储,按下标访问是 O(1),中间增删要挪元素是 O(n)。LinkedList 是双向链表,在已知节点处增删是 O(1),但查找、按位置访问要遍历是 O(n)。所以查多、按索引访问多就用 List;头尾或中间频繁增删、而且很少按索引查,可以考虑 LinkedList。

参考文章
  • 10.常用泛型数据结构类-顺序存储和链式存储
  • 8.常用泛型数据结构类-List
  • 11.常用泛型数据结构类-LinkedList

进阶题

1. 泛型约束 where

题目

什么是泛型约束?where T : classwhere T : structwhere T : new() 分别表示什么?可以组合使用吗?

深入解析

泛型约束用 where 泛型字母 : 约束 限制类型参数,让泛型内部能安全地使用该类型的特定能力。

  • **where T : struct**:T 必须是值类型(包括 int、float 及自定义 struct)。
  • **where T : class**:T 必须是引用类型(类、接口、委托等)。
  • where T : new():T 必须有公共无参构造函数,这样泛型里可以写 new T()。注意:struct 自带无参构造,但 structnew() 不能一起写;classnew() 可组合,且 class 要写在 new() 前面,否则报错。
  • 还可约束为某类/接口:where T : BaseClasswhere T : IComparable;以及 where T : U(T 必须是另一泛型参数 U 或其派生类)。

组合示例:where T : class, new() 表示 T 是引用类型且可无参构造。

答题示例

泛型约束用 where 限制 T 能是什么类型。struct 表示只能是值类型,class 表示只能是引用类型,new() 表示必须有公共无参构造,这样泛型里才能 new T()。可以组合,比如 where T : class, new(),但 class 要写在 new() 前面;struct 不能和 new() 一起写,因为结构体本身就有无参构造。

参考文章
  • 7.泛型-泛型约束

2. 闭包与 for 循环中的 lambda 陷阱

题目

什么是闭包?在 C# 里 for 循环中写 lambda 并加到委托/事件里,为什么经常出现“都打印同一个数”的问题,如何避免?

深入解析

闭包:一个函数(或委托)连同其引用到的外部变量一起形成的封闭作用域。内层函数可以引用外层变量,且捕获的是变量本身(引用),不是当时的值;外层执行结束后,只要闭包还在用,该变量的生命周期会被延长。

for 循环陷阱:for 里声明的循环变量(如 i)在整个 for 内是同一个变量。lambda 捕获的是这个变量的引用,等循环结束、委托被调用时,i 已经是终值(例如 3),所以多个委托执行时都读到 3。

避免方式:在循环体内引入新的局部变量再被 lambda 捕获,例如 int copy = i;,然后 () => Console.WriteLine(copy)。这样每次循环的 copy 是不同的变量,捕获的是各自的值。

答题示例

闭包就是函数带着它用到的外部变量一起“打包”,捕获的是变量本身不是快照。for 里写的 i 在整个 for 里是同一个变量,lambda 捕获的是这个引用,等真正执行时 i 已经变成最后一次的值了,所以会都打印同一个数。解决办法是在循环里再声明一个局部变量,比如 int copy = i,让 lambda 捕获 copy,每次循环的 copy 不同,就不会串了。

参考文章
  • 16.委托和事件-lambda表达式

3. 协变与逆变(out / in)

题目

C# 里协变和逆变是什么?outin 分别修饰泛型的什么位置?只能用在泛型接口和泛型委托上吗?

深入解析

协变:和谐的变化,子类→父类。用 out 修饰泛型参数,表示该类型只能作为返回值,不能作为参数。例如 Func<Son> 可以赋给 Func<Father>,因为返回的是子类,用父类接符合里氏替换。

逆变:逆着来的变化,父类→子类。用 in 修饰泛型参数,表示该类型只能作为参数,不能作为返回值。例如 Action<Father> 可以赋给 Action<Son>,因为能处理父类的委托,传入子类时也能处理。

适用范围只有泛型接口和泛型委托可以用 out/in;泛型类、结构体等会报错。作用是把“生产者”标成 out(只输出)、把“消费者”标成 in(只输入),在保证类型安全的前提下让父子泛型可以互相赋值。

答题示例

协变用 out,表示泛型只能当返回值,子类可以当父类返回,所以像 Func 可以赋给 Func。逆变用 in,表示泛型只能当参数,能处理父类的委托也能处理子类,所以 Action 可以赋给 Action。out 和 in 只能用在泛型接口和泛型委托上,不能用在泛型类上。

参考文章
  • 18.协变逆变

深度题

1. 多线程:lock 与 IsBackground

题目

多线程访问同一块共享数据时为什么要加 lock?Thread 的 IsBackground 设为 true 有什么作用?不设会有什么问题?

深入解析

为什么要 lock:多个线程共享进程内存,若同时读写同一变量或同一资源(例如控制台光标、同一集合),执行顺序不确定,会导致数据错乱或结果不符合预期。lock(引用类型对象) 保证同一时刻只有一个线程进入锁内代码,其它线程等待锁释放后再执行,从而避免并发冲突。代价是可能降低并发效率。

IsBackground:线程分为前台线程和后台线程。前台线程会阻止进程退出,只要有一个前台线程没结束,进程就不会关。后台线程不会阻止进程退出;当前台线程都结束时,进程结束,后台线程会被直接终止。把通过 Thread 开出来的、带死循环或长任务的线程设为 IsBackground = true,主线程退出后进程可以正常关闭;否则主线程结束了,子线程还在跑死循环,进程关不掉。

答题示例

多线程共享同一块数据时,如果不加锁,几个线程同时改,顺序乱了就会出错。lock 用一个对象当锁,同一时间只有一个线程能进锁里的代码,其它等着,这样就不会抢同一资源。IsBackground 设为 true 表示这是后台线程,主线程结束时进程会直接退出,后台线程也会被停掉;不设的话前台线程会拖住进程,比如子线程里有个死循环,主线程退出了进程也关不掉。

参考文章
  • 19.多线程

2. 反射的概念与常见用法

题目

什么是反射?Type 对象有哪几种常用获取方式?反射一般用在什么场景?

深入解析

反射:程序在运行时查看自身或其它程序集的元数据(类、方法、字段、构造函数等),并可以动态实例化对象、调用方法、读写字段。元数据保存在程序集(DLL/EXE)中;反射提高了扩展性和灵活性,代价是性能比直接调用差。

获取 Type
1)obj.GetType():从实例获取。
2)typeof(类名):从类型名获取。
3)Type.GetType("完整类名"):从字符串获取,类名必须带命名空间,否则可能找不到。

有了 Type 后,可用 GetConstructorsGetMethodsGetFields 等获取成员,用 Invoke 调用构造或方法,或用 Activator.CreateInstance(type, 参数...) 快速实例化。配合特性可做“带特性的类/方法在运行时被扫描并处理”。

常见场景:插件/模块动态加载、编辑器扩展(如 Unity Inspector)、序列化/反序列化、依赖注入、根据配置或字符串名创建类型等。

答题示例

反射就是在运行时拿到程序集里的元数据,能拿到类型、方法、字段这些信息,还能动态 new 对象、调方法。Type 常用三种方式:对象.GetType()、typeof(类名)、Type.GetType(“命名空间.类名”),字符串那种要带命名空间。反射一般用在插件加载、编辑器工具、序列化、或者根据配置字符串创建类型这类需要“运行时才知道类型”的场景,缺点是比较耗性能。

参考文章
  • 21.反射和特性-反射
  • 22.反射和特性-特性


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏