C# 语言进阶(八)自定义特性、委托、事件、多线程

本篇 核心知识点 :自定义 Attribute 特性完整规则、委托 delegate、多播委托、event 事件、委托与事件核心区别、观察者模式实战、匿名方法、多线程基础、线程抢占资源冲突、lock 线程锁、单例多线程安全

一、自定义特性 Attribute 完整规则

1. 概念

特性是附着在类 / 方法 / 字段 / 属性上的元数据标记,运行时通过反射读取,用来存储描述、约束、版本、功能标识等附加信息,不改变代码执行逻辑。

2. 自定义特性基类规则

所有自定义特性必须继承Attribute,类名约定以Attribute结尾,使用时可省略后缀。

3. 特性三大核心构造参数(继承 Attribute 时控制生效范围)

构造函数三个参数(顺序:使用范围、允许多标记、是否可继承)

  1. 第一个参数:AttributeTargets 标记适用对象枚举,限定该特性能写在哪些代码元素上;

    可选值:Class/Method/Field/Property/All等;用|分隔可同时多选;

    例:AttributeTargets.Class | AttributeTargets.Method代表类、方法都能使用。

  2. 第二个参数:AllowMultiple(bool)

    false(默认):同一个元素只能贴 1 个该特性,不能重复;

    true:同一个类 / 方法可多次叠加同一特性(技能同时带火 + 毒属性)。

  3. 第三个参数:Inherited(bool)

    false(默认):父类标记特性,派生类不会自动继承;

    true:子类自动拥有父类附加的同特性,无需重复标注。

代码示例:技能类型自定义特性

复制代码
using System;
// 自定义技能特性:允许类、方法使用,可重复、支持继承
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class SkillAttr : Attribute{
    // 构造函数:存储技能类型
    public string SkillType { get; }
    public SkillAttr(string type){
        SkillType = type;
    }
}
​
// 使用特性(省略后缀Attr)
[Skill("火系")]
[Skill("灼烧")] // AllowMultiple=true 允许重复
class FireSkill{
    [Skill("单体伤害")]
    public void Release() { }
}

拓展系统内置特性

[Obsolete("提示", true)] 标记弃用代码,第二个参数 true 直接编译报错,false 仅警告。

二、委托 delegate

1. 概念

委托是函数引用类型 ,等价 C/C++ 函数指针,用来存储、传递、调用方法;规定统一的返回值、参数列表,只有签名匹配的方法才能存入委托变量。

2. 基础语法

复制代码
// 定义委托类型:无返回、两个int参数
public delegate void CalcDelegate(int a, int b);

3. 基础使用(单委托)

步骤
  1. 使用new 委托类型(方法名)绑定;

  2. 直接委托变量(参数)执行;

    class Test{
    // 匹配委托签名的三个方法
    static void Add(int a, int b) => Console.WriteLine(a + b);
    static void Sub(int a, int b) => Console.WriteLine(a - b);
    static void Mul(int a, int b) => Console.WriteLine(a * b);

    static void Main(){
    CalcDelegate del = new CalcDelegate(Add);
    del(3, 5); // 输出8
    del = Sub;
    del(3, 5); // 输出-2
    }
    }

4. 多播委托(+=/-= 增减方法)

概念

一个委托变量绑定多个同签名方法,调用时按绑定顺序全部执行;

+=:追加方法监听;

-=:移除指定监听方法;

=:直接覆盖,清空所有绑定(不安全)。

复制代码
static void Main(){
    CalcDelegate del = Add;
    del += Sub; // 追加
    del += Mul;
    del(3, 5); // 依次执行Add、Sub、Mul
    del -= Sub; // 移除减法
    del(3, 5); // 仅执行Add、Mul
}

多播缺陷

委托变量可直接用del = 新方法覆盖,会清空全部已绑定函数,破坏多监听逻辑,不安全,由此引出 event 事件。

三、event 事件(基于委托封装)

1. 核心概念

事件是受保护的委托变量,专门实现观察者(发布订阅)模式;

发布者:内部可以+=/-=绑定、也可以调用触发事件;

外部订阅者:仅允许+=/-=增删监听,禁止直接赋值 =、禁止外部调用触发,解决多播委托覆盖安全漏洞。

2. 语法规则

  1. 先定义配套委托类型;

  2. 类内用event 委托类型 事件名声明;

  3. 触发只能写在本类内部方法中。

实战案例(老鼠触发事件,猫、主人订阅)

复制代码
using System;
// 1. 定义事件配套委托
public delegate void NoiseDelegate();
​
// 发布者:老鼠(触发事件)
class Mouse{
    // 事件:噪音
    public event NoiseDelegate OnNoise;
    // 老鼠出洞,触发事件
    public void OutHole(){
        Console.WriteLine("老鼠钻出洞口,发出声响");
        // 本类内才能调用触发
        OnNoise?.Invoke(); // 空安全调用
    }
}
​
// 订阅者:猫
class Cat{
    public void Wake() => Console.WriteLine("猫被吵醒,喵喵叫");
}
​
// 订阅者:主人
class Master{
    public void Wake() => Console.WriteLine("主人被吵醒,伸懒腰");
}
​
static void Main(){
    Mouse mouse = new Mouse();
    Cat cat = new Cat();
    Master master = new Mouse();
    // 订阅:+=绑定监听
    mouse.OnNoise += cat.Wake;
    mouse.OnNoise += master.Wake;
    mouse.OutHole();
    // mouse.OnNoise(); 外部直接调用编译报错,安全限制
    // mouse.OnNoise = null; 外部赋值编译报错
}

委托 vs 事件对比表

对比项 普通委托 delegate event 事件
外部赋值= 允许,会清空所有绑定 禁止,编译报错
外部直接调用 允许 禁止,仅发布类内部可触发
安全性 低,易误覆盖监听 高,规范发布订阅
适用场景 临时回调、内部逻辑 游戏 UI、怪物死亡、消息广播

3. 观察者模式工程价值

游戏开发高频使用:怪物死亡发布事件,UI 面板、任务系统、成就系统同时订阅,解耦代码,无需硬编码耦合。

四、匿名方法(委托简化写法)

概念

不用单独定义具名函数,创建委托时直接写内嵌代码块,简化一次性回调逻辑。

代码示例

复制代码
delegate void TestDel(int num);
static void Main(){
    // 匿名方法赋值委托
    TestDel del = delegate(int n){
        Console.WriteLine("数字:" + n);
    };
    del(99);
}

拓展:内置通用委托

系统预定义无需手写 delegate:

  1. Action:无返回值,支持 0~16 个参数;

  2. Func<T>:带返回值,最后泛型为返回类型。

五、多线程基础

1. 核心概念

单线程:代码自上而下串行执行;

多线程:CPU 分片并行执行多段代码,互不阻塞;

游戏用途:网络收发、资源加载、后台计算,防止主线程界面卡死。

2. 线程抢占问题

多个线程同时读写同一份共享变量时,CPU 切换时机随机,导致数据错乱(售票多窗口超卖、负数票)。

错误示例:多窗口卖票
复制代码
static int ticket = 200;
static void SellTicket(){
    while (ticket > 0){
        // 模拟耗时操作,放大线程冲突
        System.Threading.Thread.Sleep(1);
        ticket--;
        Console.WriteLine("剩余票数:" + ticket);
    }
}
static void Main(){
    // 创建4个售票线程
    new System.Threading.Thread(SellTicket).Start();
    new System.Threading.Thread(SellTicket).Start();
    new System.Threading.Thread(SellTicket).Start();
    new System.Threading.Thread(SellTicket);
}
// 运行结果会出现ticket负数、重复票号

3. lock 线程互斥锁(解决抢占冲突)

概念

定义私有只读锁对象,lock(对象)包裹共享数据操作代码块;

同一时间仅一个线程进入锁内代码,其余线程阻塞等待,保证原子操作。

规范

锁对象必须private readonly object,禁止 string、值类型。

修复售票代码
复制代码
static int ticket = 200;
// 全局锁对象
private static readonly object lockObj = new object();
static void SellTicket(){
    while (true){
        lock (lockObj){
            if (ticket <= 0) break;
            System.Threading.Thread.Sleep(1);
            ticket--;
            Console.WriteLine("剩余票数:" + ticket);
        }
    }
}

4. 多线程单例(懒汉模式线程安全)

无锁懒汉缺陷:

多线程同时判断inst==null,会创建多个实例,破坏单例唯一性。

双重校验锁安全单例
复制代码
class ResManager{
    private static ResManager inst = null;
    private static readonly object lockObj = new object();
    // 私有构造
    private ResManager() { }
    // 只读实例属性
    public static ResManager Instance{
        get{
            if (inst == null){
                lock (lockObj){
                    if (inst == null)
                        inst = new ResManager();
                }
            }
            return inst;
        }
    }
}

六、游戏实战:多线程消息队列

场景需求

网络线程阻塞等待服务端消息,主线程刷新 UI;

多线程同时操作队列会冲突,队列读写加锁保证安全。

流程

  1. 网络子线程:接收消息,lock 后入队;

  2. 主线程每帧 lock 读取队列,处理消息并清空;

  3. 锁隔离入队 / 出队,避免数据错乱。

七、面试 & 工程拓展考点

1 自定义 Attribute 三个构造参数各自作用;

2 委托、event 核心区别,事件安全机制;

3 观察者模式实现思路、游戏业务场景;

4 多线程抢占资源产生数据错乱原因,lock 锁使用规范;

5 懒汉单例多线程安全双重校验锁写法;

6 匿名方法、Action/Func 内置委托简化使用;

7 多线程游戏网络、资源加载实战价值。