C#进阶:委托

委托

委托变量是函数的容器 装载传递函数的容器

本质是对方法函数的存储行为,常用于观察者设计模式

委托支持泛型

基本语法及使用

这是传统的delegate类型

cs 复制代码
//定义委托内容------委托可以传值
delegate void MyFun();
public delegate int MyFun2(int a);


public delegate void MyDel();//声明一个自定义委托
    class Program
    {
        static void Main(string[] args)
        {
            //两种声明方式
            MyDel say1 = SayHi;// 隐式方法组转换
            MyDel say2 = new MyDel(SayHi);//显式委托构造
            //使用委托
            say1();
            say2();
        }

方法赋值给委托须知

而是:当你把一个方法赋值给委托时,你只写方法名(不带括号和实参),编译器会自动检查签名是否匹配

cs 复制代码
public delegate int Comparison<T>(T x, T y);

Comparison<ShopItem> comp = SortShopItems; // ← 没有 ()
shopItemsList.Sort(SortShopItems);         // ← 没有 ()


static int SortShopItems(ShopItem a, ShopItem b)
{
    // ...
}

Unity自带的委托类型

Action和Funchttps://blog.csdn.net/2303_80204192/article/details/156720077#t5

cs 复制代码
使用系统自带委托 需要引用using System;
无参无返回值
// 初始化:加入任务1
action = Fun;        // 队列:[Fun]
// 扩展:加入任务2
action += Fun3;      // 队列:[Fun, Fun3]
// 执行所有任务(也可以用action.invoke)
action();            // 先执行 Fun,再执行 Fun3


可以指定返回值类型的 泛型委托
Func funcString = Fun4;
Func funcInt = Fun5


static string Fun4()  
{  
    return "";  
}

Action以及Function类型的使用

Action的使用https://blog.csdn.net/2303_80204192/article/details/156720077#t8

Function的使用https://blog.csdn.net/2303_80204192/article/details/156720077#t9

委托存储多个函数

使用+=运算符 为委托新增方法

使用-=运算符 为委托移除方法

使用=null 为委托清空方法

cs 复制代码
using System;

class Program
{
    static void Main()
    {
        Action action = null;
        action += Fun;
        action += Fun1;
        action?.Invoke();//调用该委托中的全部函数
        action -=Fun;
        action += Fun1;//注意要手动取消注册函数
                    //否则可能出现对象销毁后,如果委托仍被调用,导致崩溃或异常
    }

    static void Fun()
    {
        Console.WriteLine("Fun");
    }

    static void Fun1()
    {
        Console.WriteLine("Fun1");
    }
}

事件

事件是什么

基于委托的存在: 事件本质上是委托的安全包裹,让委托的使用更具有安全性,事件是一种特殊的变量类型。

  • 安全特性:
    • 让委托的使用更具有安全性
    • 是一种特殊的变量类型

事件的使用

事件是作为 成员变量存在于类中,委托怎么用 事件就怎么用

注意

它只能作为成员存在于类和接口以及结构体中

cs 复制代码
class Test
{
委托成员变量 用于存储 函数的
public Action myFUN;
事件成员变量 用于存储 函数的
public event Action myEvent;

public Test()
{
  myFun = TestFun()
  myFun += TestFun()

  myEvent = TestFun()
  myEvent += TestFun()

}


public void TestFun()
{
}

还可以让事件被方法封装,然后在外部调用

cs 复制代码
public void DoEvent()
{
    if (myEvent != null)
    {
        myEvent();
        myEvent.invoke;
    }
}
----主函数-----
t.DoEvent()

事件与委托的区别

事件和委托在类内部使用没什么区别,danshi

事件相对于委托在类外部的区别

不能在类外部 赋值

不能再类外部 调用

cs 复制代码
static void Main(string[] args)
{
    Console.WriteLine("事件");

    Test t = new Test();

    // 委托可以在外部赋值
    t.myFun = null;
    t.myFun = TestFun;

    // 事件是不能再外部赋值的
    // t.myEvent = null;
    // t.myEvent = TestFun;

    // 虽然不能直接赋值,但是可以 加减 去添加移除记录的函数
    t.myEvent += TestFun;
    t.myEvent -= TestFun;

    // 委托是可以在外部调用的
    t.myFun();
    t.myFun.Invoke();

    // 事件不能再外部调用
    // t.myEvent();
}

这也是为什么说事件安全的原因:

因为特性就是在外面,你不能随心所欲的去清空它或者调用它,你只能在内部去调用它。

匿名函数

匿名函数是没有名字的函数,必须配合委托和事件使用,脱离委托和事件不会单独使用。

cs 复制代码
#region 知识点二 基本语法
delegate (参数列表)
{
  函数逻辑
}

何时使用?
//1.函数中传递委托参数时
//2.委托或事件赋值时

匿名函数的使用

无参无返回的匿名函数

cs 复制代码
#region 知识点三 使用
//1. 无参无返回
//这样声明匿名函数 只是在申明函数而已 还没有调用
//真正调用它的时候 是这个委托容器啥时候调用 就什么时候调用这个匿名函数

Action a = delegate ()
{
    Console.WriteLine("匿名函数逻辑");
};

a(); // 调用委托,执行匿名函数逻辑

有参无返回的匿名函数

cs 复制代码
//2. 有参
Action<int, string> b = delegate (int a, string b)
{
    Console.WriteLine(a);
    Console.WriteLine(b);
};

b(100, "123");

有返回值的匿名函数

cs 复制代码
//3. 有返回值
Func<string> c = delegate ()
{
    return "123123";
};

匿名函数作为参数/返回值

这是匿名函数的一般使用情况------函数作为参数或者返回值

这里主要是作为参数的时候

cs 复制代码
class Test
{
    public Action action;

    // 作为参数传递时
    public void Dosomething(int a, Action fun)
    {
        Console.WriteLine(a);
        fun();
    }

    public Action GetFun()
    {
        return null;
    }
}

---主函数----
Test t = new Test();
1、参数传递------这里一般需要声明函数名字才能传递,但是匿名函数可以直接传入
t.Dosomething(100, delegate ()
{
    Console.WriteLine("匿名委托执行了");
});

作为返回值

cs 复制代码
class Test
{
    public Action action;

    public Action GetFun()
    {
        return Test1;//1、正常来讲如果需要返回函数,必须有这函数声明
        return delegate()
       {
        console.writeline("返回匿名函数")//2、匿名函数则不需要声明
       }
    }

    public void Test1()
}

----主函数----
Test t = new Test();
Action ac=t.GetFun()//3、调用该函数,返回匿名函数赋值给ac
ac()//4、调用该匿名函数

//5、也可以返回了直接调用---
//t.GetFun()得到匿名函数,再加一个()调用该匿名函数
t.GetFun()()

匿名函数的缺陷

移除限制:

由于匿名函数没有名称,当被添加到委托或事件容器后,无法单独移除特定的匿名函数实例。只能清空整个容器,这会影响其他已注册的函数逻辑。

cs 复制代码
Action ac3 = delegate ()
{
    Console.WriteLine("匿名函数一");
};

ac3 += delegate ()
{
    Console.WriteLine("匿名函数二");
};

ac3();

如果我这里想移除指定匿名函数的话,是没有办法的

ac3---=delegate ()
{
    Console.WriteLine("匿名函数一");
};
没用,输出依旧有匿名函数一

只能全部清空
ac3=null

Lamda表达式

  • 本质理解: Lambda表达式是匿名函数的简写形式
  • 核心特点: 除了写法不同外,使用上和匿名函数完全一致------即必须与委托或事件配合使用,不能单独存在

基本语法:

与匿名函数相比,少了delegate类型,直接使用参数括号以及调用符号()=>

cs 复制代码
#region 知识点二 Lambda表达式语法

// 1. 匿名方法(Anonymous Method)
Action action1 = delegate ()
{
    Console.WriteLine("这是匿名方法");
};

// 2. Lambda 表达式(Lambda Expression)
Action action2 = () => Console.WriteLine("这是Lambda表达式");

// 可以同时调用两者
action1();
action2();

#endregion

注意事项:

必须通过委托(如Action)来存储和调用

空括号()表示无参数

箭头和大括号不可省略

Lamda表达式的使用

无参无返回值Lambda表达式

cs 复制代码
//1. 无参无返回
Action a = () =>
{
    Console.WriteLine("无参无返回值的lambda表达式");
};

有参无返回值Lambda表达式

cs 复制代码
Action<int> a2 = (int value) =>
{
    Console.WriteLine("有参数Lambda表达式{0}", value);
};
a2(100);

参数类型甚至都可以省略
Action<int> a2 = (value) =>
{
    Console.WriteLine("有参数Lambda表达式{0}", value);
};
a2(200);

带有参数且有返回值Lambda 表达式

cs 复制代码
//4. 有返回值
Func<string, int> a4 = (value) =>
{
    Console.WriteLine("有返回值有参数的那么大表达式{0}", value);
    return 1;
};
Console.WriteLine(a4("123123"));

闭包

**核心概念:**内层函数可以引用包含在其外层函数中的变量,即使外层函数执行已经终止

  • "闭" = 把外部变量封闭起来
  • "包" = 和函数打包在一起

**变量值特性:**该变量提供的值并非创建时的值,而是父函数范围内的最终值

普通闭包的理解

cs 复制代码
class Test
{
    public event Action action;

    public Test()
    {
        int value = 10;
        // 这里就形成了闭包
        // 因为 当构造函数执行完毕时 其中申明的临时变量value的生命周期被改变了
        action = () =>
        {
            Console.WriteLine(value);
        };
    }
}


Test t = new Test();     // 构造函数执行完毕,value 按理"死了"
t.action();              // 但这里依然输出:10

value就在Test类构造结束后被释放,但是因为被包裹进了匿名函数,所以生命周期就发生了改变。

cs 复制代码
public Test()
{
    int value = 10;
    // ... 其他代码
} //← 构造函数结束,局部变量 `value` 被释放(出栈)


public Test()
{
    int value = 10;
    action = () => Console.WriteLine(value); // ← 捕获了 value!
}

那什么时候才会被释放呢?

其实什么时候都不会被释放,除非你把它置空------即手动将action=null

遍历匿名函数的闭包

cs 复制代码
public class Test
{
    public Action action;
    public Test()
    {
        
      for (int i = 0; i < 5; i++)
        {
          
            action += () =>
            {
                Console.WriteLine(i);
            };
        }
    }   
}

internal static class Program
{
    private static void Main(string[] args)
    {
        Test test = new Test();
        test.action.Invoke();//输出结果5个5
    }
}

执行顺序:

  1. 构造函数开始执行

    • 进入 for 循环
    • i 是一个局部变量,初始为 0
  2. 每次循环:添加一个 Lambda 到 action

    • 第 1 次:i = 0 → 添加 () => Console.WriteLine(i)
    • 第 2 次:i = 1 → 再添加一个 () => Console.WriteLine(i)
    • ...
    • 第 5 次:i = 4 → 添加第 5 个 Lambda

    ⚠️ 但注意:此时这些 Lambda 并没有执行!只是被存储起来。

  3. 循环结束条件

    • i = 4 时,执行完循环体后,i++i = 5
    • 然后检查 i < 5?→ 5 < 5 为 false → 退出循环
    • 此时 i 的最终值是 5
  4. 构造函数结束

    • Test test = new Test(); 完成
  5. 调用 test.action.Invoke()

    • 此时才真正执行那 5 个 Lambda
    • 每个 Lambda 都去读取 当前的 i
    • i 现在已经是 5(并且因为闭包,它没有被销毁!)

List排序

系统自带的排序方法

cs 复制代码
static void Main(string[] args)
{
    Console.WriteLine("List排序");
    #region 知识点一 List自带排序方法
    List<int> list = new List<int>();
    list.Add(3);
    list.Add(2);
    list.Add(6);
    list.Add(1);
    list.Add(4);
    list.Add(5);

    for (int i = 0; i < list.Count; i++)
    {
        Console.WriteLine(list[i]);
    }

    // list提供了排序方法
    list.Sort();

    Console.WriteLine("****************");
    for (int i = 0; i < list.Count; i++)
    {
        Console.WriteLine(list[i]);
    }
    #endregion
}

排序前:
3 2 6 1 4 5
排序后:
1 2 3 4 5 6

Sort函数是Unity自带排序函数,可以将列表中的数据升序排序

int为什么能被排序,因为int的定义如下,有实现 IComparable<T> 接口, List<T>.Sort() 方法要求泛型类型 T 必须满足该条件

cs 复制代码
List<int> ints = new() { 3, 1, 4 };
ints.Sort(); // → [1, 3, 4]

List<double> doubles = new() { 3.5, 1.2, 4.8 };
doubles.Sort(); // → [1.2, 3.5, 4.8]

List<string> words = new() { "banana", "apple", "cherry" };
words.Sort(); // → ["apple", "banana", "cherry"]

List<DateTime> dates = new()
{
    new DateTime(2023, 3, 1),
    new DateTime(2023, 1, 1),
    new DateTime(2023, 2, 1)
};
dates.Sort(); // → 按时间升序

自定义排序类

cs 复制代码
class Item :IComparable<Item>
{
    public int money;

    public Item (int money)
    {
        this.money = money;
    }

    //想用list这个自带的排序方法,那我就必须要去继承一个排序接口,去实现一个排序的规则
    public int CompareTo(Item other)
    {
        //返回值的含义
        //小于0:放在传入对象的前面
        //等于0:保持当前位置不变
        //大于0:放在传入对象的后面

        //可以简单理解为 传入对象的位置就是0
        //如果返回负数 就放在它的左边(前面 升序)
        //如果返回正数 就放在它的右边 (后面 降序)
        if (this.money>other.money)
        {
            return 1;
        }
        else
        {
            return -1;
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        //自定义类的排序
        List<Item> itemList = new List<Item>();
        itemList.Add(new Item(100));
        itemList.Add(new Item(54));
        itemList.Add(new Item(345));
        itemList.Add(new Item(23));
        itemList.Add(new Item(11));
        itemList.Add(new Item(77));
        //排序方法 在类中要继承IComparable接口
        itemList.Sort();
        for (int i = 0; i < itemList.Count; i++)
        {
            Console.WriteLine(itemList[i].money);
        }
    }
}

这里的排序机制是:Sort函数会自动调用接口中的CompareTo方法:然后通过这个方法里面的这个排序的规则,你给它一个反馈。会给它返回一个值。

至于返回值排序的规则

相当于传入对象的位置就是0,返回值为1放在该数值右边,返回值为-1放在该数值左边

cs 复制代码
排序算法如何工作?(简化版)
List.Sort() 不会固定以第一个元素(100)为基准。而是:
比较 Item(100) 和 Item(54) → 发现 100 > 54 → 所以 54 应该在 100 前面
比较 Item(345) 和 Item(23) → 345 > 23 → 23 在前
比较 Item(54) 和 Item(23) → 54 > 23 → 23 在前
......(进行多次两两比较)
最终通过这些比较结果,构建出完整的升序序列。


如果设计为传入值大的放在左边,则会形成降序排列,即从大到小排序
 if (this.money>other.money)
        {
            return -1;
        }
        else
        {
            return 1;
        }

委托函数进行排序

cs 复制代码
class ShopItem//1、这里没有那个接口,所以直接使用Sort函数排序会报错
{
    public int id;

    public ShopItem(int id)
    {
        this.id = id;
    }
}

但是sort是有重载的:

cs 复制代码
public void Sort(Comparison<T> comparison);//这里委托函数排序用这一个
public void Sort(int index, int count, IComparer<T>? comparer);
public void Sort();

//点开后发现是该委托类型,它表示:一个接受两个相同类型的参数(T x, T y),返回一个整数的方法。
public delegate int Comparison<T>(T x, T y);

整体代码如下

cs 复制代码
class ShopItem
{
    public int id;

    public ShopItem(int id)
    {
        this.id = id;
    }
}

class Program
{
    static void Main(string[] args)
    {
        List<ShopItem> shopItemsList = new List<ShopItem>();
        shopItemsList.Add(new ShopItem(3));
        shopItemsList.Add(new ShopItem(5));
        shopItemsList.Add(new ShopItem(6));
        shopItemsList.Add(new ShopItem(2));
        shopItemsList.Add(new ShopItem(1));
        shopItemsList.Add(new ShopItem(4));
        1、通过传入函数的排序方法
        shopItemsList.Sort(SortShopItems);

        2、或者通过lambda表达式或者匿名函数和三目运算符的简便写法省去声明函数
       shopItemsList.Sort((a, b) =>{return a.id > b.id ? 1 : -1;});
    
        for (int i = 0; i < shopItemsList.Count; i++)
        {
            Console.WriteLine(shopItemsList[i].id);
        }
        //打印结果为 1 2 3 4 5 6 
    }
    
    //Sort规定的委托函数写法
   //(委托相当于是函数的容器。这里相当于将SortShopItems函数放入委托容器中)
    static int SortShopItems(ShopItem a,ShopItem b)
    {
        if (a.id>b.id)
        {
            return 1;
        }
        else
        {
            return -1;
        }
    }
}

这里需要注意一点:传参的参数中如果是函数的话,只需要传入函数名即可,至于这个函数是否含参数,那是调用时的事情。

匿名函数的使用方式的理解:

cs 复制代码
Comparison<ShopItem> joker = (a, b) => a.id.CompareTo(b.id);
shopItemsList.Sort(joker);

joker 是这个lamda表达式/匿名函数的"引用名"或"别名"------也相当于Sort也是传入函数名

协变和逆变

  • 协变定义:和谐自然的变化,遵循里氏替换原则中子类可以替换父类的特性
    • 示例:string变成object是和谐的
    • 关键字:out修饰符
  • 逆变定义:逆常规的变化,违反常规父子类关系
    • 示例:object变成string是不和谐的
    • 关键字:in修饰符

解决泛型类型转换限制

传统泛型(如 List<T>)不支持子类与父类集合的隐式转换(如 List<Person> persons = new List<Student>() 编译失败)。

协变/逆变通过类型参数修饰符(out/in)扩展兼容性,增强代码灵活性。

基本使用

用于修饰泛型替代符 只能修饰接口和委托中的泛型

cs 复制代码
//⭕out修饰的泛型 只能作为返回值
delegate T TestOut<out T>();

//⭕in修饰的泛型 只能作为参数
delegate void TestIn<in T>(T t);  

协变

父类泛型容器装载子类泛型容器

cs 复制代码
delegate T TestOut<out T>();


class Father { }
class Son:Father { }

class Program
{
    static void Main(string[] args)
    {
        1、协变 子类对象可以当作父类对象来使用,这里的T类型是Son类型(out T只能用于返回值)
        并且赋值给了os
        TestOut<Son> os = () =>
        {
            return new Son();
        };
        TestOut<Father> of = os;
        Father f = of(); //实际返回的是os里转的函数 返回的是Son
    }
}
  • 你把一个 返回 Son 的函数(os ,赋值给了一个 类型为 TestOut<Father> 的变量 of
  • 又因为Father是父类,根据里氏替换原则,随时可以被子类替换,所以此时of是返回一个Son类,可以赋值给Father

逆变

子类泛型容器装载父类泛型容器

cs 复制代码
class Father { }
class Son:Father { }

//⭕in修饰的泛型 只能作为参数
delegate void TestIn<in T>(T t);  

// 看起来像是 father → son,明明是传父类,但是你传子类,不和谐的
//泛型T自动  传入Father类
TestIn<Father> iF = (value) =>
{
    // 这里 value 是 Father 类型
    Console.WriteLine("收到一个 Father 对象");
};

TestIn<Son> iS = iF; // ← 关键!逆变赋值

iS(new Son()); // 实际上调用的是 iF
  1. iF 的定义

    cs 复制代码
    TestIn<Father> iF = (value) => { ... };
    • 它能接受任何 Father 或其子类(如 Son)作为参数
    • 因为 方法参数支持多态 :你可以把 Son 当作 Father 传进去
  2. iS = iF 发生了什么?

    • 编译器允许这个赋值,因为 TestIn<in T> 是逆变的
  3. 调用 iS(new Son())

    可以理解为:创建一个Son的实例化,然后将该对象传入参数到TestIn<Father> iF = (Father value) => { ... }
    *

    (或通过 iS(new Son()) 间接调用)时,确实发生了从 SonFather 的类型转换

额外思考:为何要多一步

在协变中:Father f = os();似乎可以用

在逆变中:IF(new Son());也能用,

TestOut<Father> of = os;

TestIn<Son> ins = inf;

为什么要分别多出这一步?

cs 复制代码
delegate T TestOut<out T>();
delegate void TestIn<in T>(T t);

class Father { }
class Son:Father { }

class Program
{
    static void Main(string[] args)
    {
        //协变 父类总能被子类替代
        TestOut<Son> os = () =>
        {
            return new Son();
        };
        TestOut<Father> of = os;
        Father f = of(); //实际返回的是os里转的函数 返回的是Son

        //逆变 父类总能被子类替代
        //看起来像是father------>son 明明是传父类 但却传子类 不和谐的
        TestIn<Father> inf = (value) =>
        {

        };
        TestIn<Son> ins = inf;
        ins(new Son()); //实际上调用的是inf
        Console.WriteLine("Hello World!");
    }
}

在单个作用域内,你不需要额外的赋值步骤。这些代码完全正确,也能编译运行

场景1:协变 - 为什么需要 of = os

假设你有一个方法,它需要一个 TestOut<Father> 类型的委

复制代码
void ProcessFactory(TestOut<Father> factory)
{
    Father f = factory(); // 调用委托
    Console.WriteLine(f.GetType());
}

现在,你有一个 TestOut<Son> 类型的委托:

复制代码
TestOut<Son> os = () => new Son();

错误写法:

复制代码
ProcessFactory(os); // ❌ 编译错误!

正确写法:

复制代码
TestOut<Father> of = os; // ✅ 协变赋值
ProcessFactory(of); // ✅ 通过类型系统检查

场景2:逆变 - 为什么需要 ins = inf

假设你有一个方法,它需要一个 TestIn<Son> 类型的委托:

复制代码
void RegisterHandler(TestIn<Son> handler)
{
    handler(new Son()); // 调用委托
}

现在,你有一个 TestIn<Father> 类型的委托:

复制代码
TestIn<Father> inf = (value) => { };

错误写法:

复制代码
RegisterHandler(inf); // ❌ 编译错误!

正确写法:

复制代码
TestIn<Son> ins = inf; // ✅ 逆变赋值
RegisterHandler(ins); // ✅ 通过类型系统检查
相关推荐
喜欢喝果茶.2 小时前
跨.cs 文件传值(C#)
开发语言·c#
就是有点傻2 小时前
C#中如何和欧姆龙进行通信的
c#
zmzb01032 小时前
C++课后习题训练记录Day74
开发语言·c++
小冷coding2 小时前
【Java】Dubbo 与 OpenFeign 的核心区别
java·开发语言·dubbo
Coder_Boy_2 小时前
基于SpringAI的在线考试系统-智能考试系统-学习分析模块
java·开发语言·数据库·spring boot·ddd·tdd
2401_894828122 小时前
从原理到实战:随机森林算法全解析(附 Python 完整代码)
开发语言·python·算法·随机森林
玄同7652 小时前
Python「焚诀」:吞噬所有语法糖的终极修炼手册
开发语言·数据库·人工智能·python·postgresql·自然语言处理·nlp
羽翼.玫瑰2 小时前
关于重装Python失败(本质是未彻底卸载Python)的问题解决方案综述
开发语言·python
CRMEB系统商城3 小时前
CRMEB多商户系统(PHP)- 移动端二开之基本容器组件使用
运维·开发语言·小程序·php