C#泛型委托事件详解及实例

73 篇文章 3 订阅
45 篇文章 0 订阅

泛型委托:由于类型参数决定泛型委托能够接收什么样的方法;所以相较非泛型委托更加灵活。

声明自定义的泛型委托

public delegate R ReportResult<T, R>(T pams);

定义泛型委托变量

/// <summary>
/// 泛型委托的构造类型:需要一个形参为int类型、返回值类型为bool类型的方法对其进行初始化
/// </summary>
private static ReportResult<int, bool> _isOlderThanHim;
/// <summary>
/// 泛型委托的构造类型:需要一个形参为string类型、返回值类型为int类型的方法对齐进行初始化
/// </summary>
private static ReportResult<string, int> _caculatorAge;

赋值并调用泛型委托变量

class Program
{
    /// <summary>
    /// 泛型委托的构造类型:需要一个形参为int类型、返回值类型为bool类型的方法对其进行初始化
    /// </summary>
    private static ReportResult<int, bool> _isOlderThanHim;

    /// <summary>
    /// 泛型委托的构造类型:需要一个形参为string类型、返回值类型为int类型的方法对齐进行初始化
    /// </summary>
    private static ReportResult<string, int> _caculatorAge;
    
    static void Main(string[] args)
    {
        _isOlderThanHim = new Student().IsAdult;
        var ret = _isOlderThanHim.Invoke(14);
        Console.WriteLine(ret);
        
        _caculatorAge=new Student().AfterYears;
        var age= _caculatorAge.Invoke("5");
        Console.WriteLine(age);
        
        Console.ReadLine();
    }
}

/// <summary>
/// 定义一种操作:根据传入的实参返回相应的结果
/// </summary>
/// <param name="pams"></param>
/// <typeparam name="T"></typeparam>
/// <typeparam name="R"></typeparam>
public delegate R ReportResult<T, R>(T pams);

class Student
{
    private int StudentAge { get; set; } = 15;

    /// <summary>
    /// 返回学生的年龄与传入的数值的比较结果
    /// </summary>
    /// <param name="age"></param>
    /// <returns></returns>
    public bool IsAdult(int age)
    {
        var ret = age >= StudentAge;
        return ret;
    }

    /// <summary>
    /// 返回学生year年后的年龄
    /// </summary>
    /// <param name="years"></param>
    /// <returns></returns>
    public int AfterYears(string years)
    {
        try
        {
            return StudentAge + Convert.ToInt32(years);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            return StudentAge;
        }
    }
}

运行结果:

False
20

分析:如果想要调用Student类中的两个方法一个只声明一个非泛型委托是不够的。这里便体现出了泛型委托的优点。

.NET预定义的泛型委托类型

.NET给我们预定义了很多泛型委托,日常编码工作中使用这些类型的泛型委托基本就已足够,下面我们只介绍三个常用的预定义的泛型委托。

第一个:Predicate

使用场景:当我们需要判断数据是否满足某些条件时,就采用Predicate委托。
 


使用这个泛型委托,我们就可以调用Student类中返回值类型为bool的方法了

class Program
{
    private static ReportResult<int, bool> _isOlderThanHim;
    private static Predicate<int> _isOlder;

    static void Main(string[] args)
    {
        _isOlderThanHim = new Student().IsAdult;
        var ret = _isOlderThanHim.Invoke(14);
        Console.WriteLine(ret);

        _isOlder = new Student().IsAdult;
        var retWithPredicate = _isOlder.Invoke(18);
        Console.WriteLine(retWithPredicate);

        Console.ReadLine();
    }
}
/*
运行结果:
False
True
*/

第二个:Func<T,TResult>

使用场景:当我们对数据进行操作后、还希望将处理结果返回时,就采用Func<T,TResult>委托。
 


由于Func拥有两个类型参数,它接收的方法要比Predicate更灵活;所以使用Func<T,TResult>能够调用Student中的两个方法

class Program
{
    private static ReportResult<int, bool> _isOlderThanHim;
    private static ReportResult<string, int> _caculatorAge;
    
    private static Predicate<int> _isOlder;
    private static Func<string, int> _caculatorAgeFunc;
    
    static void Main(string[] args)
    {
        _isOlderThanHim = new Student().IsAdult;
        var ret = _isOlderThanHim.Invoke(14);
        Console.WriteLine(ret);

        _isOlder = new Student().IsAdult;
        var retWithPredicate = _isOlder.Invoke(18);
        Console.WriteLine(retWithPredicate);

        _caculatorAge = new Student().AfterYears;
        var age = _caculatorAge.Invoke("5");
        Console.WriteLine(age);
        
        _caculatorAgeFunc = new Student().AfterYears;
        var ageWithFunc = _caculatorAgeFunc.Invoke("7");
        Console.WriteLine(ageWithFunc);

        Console.ReadLine();
    }
}
/*
输出:
20
22
*/

第三个:Action

使用场景:当我们仅仅需要用数据执行一些操作,并不希望得到返回值时,使用Action委托
 


我们给Studnet类中添加一个拥有一个形参,返回值为void的方法

class Student
{
    private int StudentAge { get; set; } = 15;

    /// <summary>
    /// 打印出学生year年后的年龄
    /// </summary>
    /// <param name="year"></param>
    private void ShowAfterYear(string year)
    {
        try
        {
            Console.WriteLine(StudentAge + Convert.ToInt32(year));
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
}

下面演示如何使用Action委托

class Program
{
    private static Action<string> _printAgeAciton;

    static void Main(string[] args)
    {
        _printAgeAciton = new Student().ShowAfterYear;
        _printAgeAciton.Invoke("12");
        Console.ReadLine();
    }
}
/*
运行结果:
27
*/

C#的LINQ特性中,很多地方都使用了泛型委托

下面展示一下Linq中用到的泛型委托:

namespace GenericDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Student> list = new List<Student>();
            list.Add(new Student() {StudentName = "唐三", StudentAge = 17, ClassGroup = "史莱克学院"});
            list.Add(new Student() {StudentName = "风笑天", StudentAge = 18, ClassGroup = "神风学院"});
            list.Add(new Student() {StudentName = "邪月", StudentAge = 22, ClassGroup = "武魂殿"});
            list.Add(new Student() {StudentName = "胡烈娜", StudentAge = 20, ClassGroup = "武魂殿"});
            list.Add(new Student() {StudentName = "朱竹青", StudentAge = 19, ClassGroup = "史莱克学院"});
            list.Add(new Student() {StudentName = "焱", StudentAge = 19, ClassGroup = "武魂殿"});

            Console.WriteLine("遍历所有人员");
            list.ForEach(new Action<Student>(PrintStudentInfo));
            Console.WriteLine("=================");

            Console.WriteLine("按照班级分组");
            var grouList = list.GroupBy(new Func<Student, string>(GetGroup));
            foreach (IGrouping<string, Student> grouping in grouList)
            {
                foreach (Student student in grouping)
                {
                    Console.WriteLine(student);
                }
            }

            Console.Write("队伍中有未成年么?");
            var isHasNotAdult = list.Any(new Func<Student, bool>(IsNoAdult));
            Console.WriteLine(isHasNotAdult);
            Console.WriteLine("=================");

            Console.WriteLine("找出未成年的人员");
            var isNoAdult = list.Where(new Func<Student, bool>(IsNoAdult));
            foreach (Student student in isNoAdult)
            {
                Console.WriteLine(student);
            }

            Console.ReadLine();
        }

        private static void PrintStudentInfo(Student stu)
        {
            Console.WriteLine(stu);
        }

        private static bool IsNoAdult(Student stu)
        {
            return stu.StudentAge < 18;
        }

        private static string GetGroup(Student stu)
        {
            return stu.ClassGroup;
        }
    }

    class Student
    {
        public string StudentName { get; set; }
        public int StudentAge { get; set; }
        public string ClassGroup { get; set; }

        public override string ToString()
        {
            return $"{this.ClassGroup}:{this.StudentName},{this.StudentAge}";
        }
    }
}
/*
运行结果:
遍历所有人员
史莱克学院:唐三,17
神风学院:风笑天,18
武魂殿:邪月,22
武魂殿:胡烈娜,20
史莱克学院:朱竹青,19
武魂殿:焱,19
=================
按照班级分组
史莱克学院:唐三,17
史莱克学院:朱竹青,19
神风学院:风笑天,18
武魂殿:邪月,22
武魂殿:胡烈娜,20
武魂殿:焱,19
队伍中有未成年么?True
=================
找出未成年的人员
史莱克学院:唐三,17
*/

上面的每个Linq方法都接收一个泛型委托作为形参;通过上面的代码我们可以明显感受到使用泛型委托能够给我们带来很大的方便。

以上便是对泛型委托的总结。

/************************************************ 

​C#规范整理·泛型委托事件

基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用。同时,它减少了泛型类及泛型方法中的转型,确保了类型安全。委托本身是一种引用类型,它保存的也是托管堆中对象的引用,只不过这个引用比较特殊,它是对方法的引用。事件本身也是委托,它是委托组,C#中提供了关键字event来对事件进行特别区分。
  一旦我们开始编写稍微复杂的C#代码,就肯定离不开泛型、委托和事件。
![](https://img2018.cnblogs.com/blog/710776/201906/710776-20190613112138058-2026075746.png)

1.总是优先考虑泛型#

泛型的优点是多方面的,无论是泛型类还是泛型方法都同时具备可重用性、类型安全和高效率等特性,这都是非泛型类和非泛型方法无法具备的

2.避免在泛型类型中声明静态成员#

  1. 实际上,随着你为T指定不同的数据类型,MyList<T>相应地也变成了不同的数据类型,在它们之间是不共享静态成员的。
  2. 但是若T所指定的数据类型是一致的,那么两个泛型对象间还是可以共享静态成员的,如局部的List和List的变量。但是,为了规避因此而引起的混淆,仍旧建议在实际的编码工作中,尽量避免声明泛型类型的静态成员。
    非泛型类型中的泛型方法并不会在运行时的本地代码中生成不同的类型。
    例如:
 
  
Copy
static void Main(string[]args) { Console.WriteLine(MyList.Func<int>()); Console.WriteLine(MyList.Func<int>()); Console.WriteLine(MyList.Func<string>()); } class MyList { static int count; public static int Func<T>() { return count++; }} 输出 0 ;1;2

3.为泛型参数设定约束#

在编码过程中,应该始终考虑为泛型参数设定约束。约束使泛型参数成为一个实实在在的“对象”,让它具有了我们想要的行为和属性,而不仅仅是一个object。

指定约束示例:

  • 指定参数是值类型。(除Nullable外) where T:struct
  • 指定参数是引用类型 。 where T:class
  • 指定参数具有无参数的公共构造方法。 where T:new()

注意,CLR目前只支持无参构造方法约束。

  • 指定参数必须是指定的基类,或者派生自指定的基类。
  • 指定参数必须是指定的接口,或者实现指定的接口。
  • 指定T提供的类型参数必须是为U提供的参数,或者派生自为U提供的参数。 where T:U
  • 可以对同一类型的参数应用多个约束,并且约束自身可以是泛型类型。

4.使用default为泛型类型变量指定初始值#

有些算法,比如泛型集合List<T>的Find算法,所查找的对象有可能会是值类型,也有可能是引用类型。在这种算法内部,我们常常会为这些值类型变量或引用类型变量指定默认值。于是,问题来了:值类型变量的默认初始值是0值,而引用类型变量的默认初始值是null值,显然,这会导致下面的代码编译出错:

 
  
Copy
public T Func<T>() { T t=null; T t=0; return t; }

代码"T t=null;"在Visual Studio编译器中会警示:错误1不能将Null转换为类型形参“T”,因为它可能是不可以为null值的类型。请考虑改用“default(T)”.
代码"T t=0;"会警示:错误1无法将类型“int”隐式转换为“T”。
改进

 
  
Copy
public T Func<T>() { T t=default(T); return t; }

5.使用FCL中的委托声明#

  • 要注意FCL中存在三类这样的委托声明,它们分别是:Action、Func、Predicate。尤其是在它们的泛型版本出来以后,已经能够满足我们在实际编码过程中的大部分需要。
  • 我们应该习惯在代码中使用这类委托来代替自己的委托声明。
  • 除了Action、Func和Predicate外,FCL中还有用于表示特殊含义的委托声明。
 
  
Copy
//如用于表示注册事件方法的委托声明: public delegate void EventHandler(object sender,EventArgs e); public delegate void EventHandler<TEventArgs>(object sender,TEventArgs e); //表示线程方法的委托声明: public delegate void ThreadStart(); public delegate void ParameterizedThreadStart(object obj); //表示异步回调的委托声明: public delegate void AsyncCallback(IAsyncResult ar);

在FCL中每一类委托声明都代表一类特殊的用途,虽然可以使用自己的委托声明来代替,但是这样做不仅没有必要,而且会让代码失去简洁性和标准性。在我们实现自己的委托声明前,应该首先查看MSDN,确信有必要之后才这样做。

6.使用Lambda表达式代替方法和匿名方法#

在实际的编码工作中熟练运用它,避免写出烦琐且不美观的代码。

7.小心闭包中的陷阱#

如果匿名方法(Lambda表达式)引用了某个局部变量,编译器就会自动将该引用提升到该闭包对象中,即将for循环中的变量i 修改成了引用闭包对象(编译器自动创建)的公共变量i。
示例如下:

 
  
Copy
static void Main(string[]args) { List<Action>lists=new List<Action>(); for(int i=0;i<5;i++) { Action t=()=> { Console.WriteLine(i.ToString()); }; lists.Add(t); } foreach(Action t in lists) { t(); } }

以上结果全部输出5;
另外一种实现方式;

 
  
Copy
static void Main(string[]args) { List<Action>lists=new List<Action>(); TempClass tempClass=new TempClass(); for(tempClass.i=0;tempClass.i<5;tempClass.i++) { Action t=tempClass.TempFuc; lists.Add(t); } foreach(Action t in lists) { t(); } } class TempClass { public int i; public void TempFuc() { Console.WriteLine(i.ToString()); } }

这段代码所演示的就是闭包对象。所谓闭包对象,指的是上面这种情形中的TempClass对象(在第一段代码中,也就是编译器为我们生成的“<>c__DisplayClass2”对象)。如果匿名方法(Lambda表达式)引用了某个局部变量,编译器就会自动将该引用提升到该闭包对象中,即将for循环中的变量i修改成了引用闭包对象的公共变量i。这样一来,即使代码执行后离开了原局部变量i的作用域(如for循环),包含该闭包对象的作用域也还存在。理解了这一点,就能理解代码的输出了。

8.了解委托的本质#

理解C#中的委托需要把握两个要点:

  1. 委托是方法指针。
  2. 委托是一个类,当对其进行实例化的时候,要将引用方法作为它的构造方法的参数。

9.使用event关键字为委托施加保护#

首先没有event加持的委托,我们可以对它随时进行修改赋值,以至于一个方法改动了另一个方法的委托链引用,比如赋值为null,另外一个方法中调用的时候将抛出异常。
如果有event加持的时候,我们修改的时候,比如:

 
  
Copy
fl.FileUploaded=null; fl.FileUploaded=Progress; fl.FileUploaded(10);

以上代码编译会出现错误警告:
事件 “ConsoleApplication1.FileUploader.FileUploaded ”
只能出现在+=或-=的左边(从类型“ConsoleApplication1.FileUploader”中使用时除外)

10.实现标准的事件模型#

有了上面的event加持,但是还不能够规范。
EventHandler的原型声明:

 
  
Copy
public delegate void EventHandler(object sender,EventArgs e);

微软为事件模型设定的几个规范:

  • 委托类型的名称以EventHandler结束;
  • 委托原型返回值为void;
  • 委托原型具有两个参数:sender表示事件触发者,e表示事件参数;
  • 事件参数的名称以EventArgs结束。

11.使用泛型参数兼容泛型接口的不可变性#

  • 让返回值类型返回比声明的类型派生程度更大的类型,就是“协变”。
  • 编译器对于接口和委托类型参数的检查是非常严格的,除非用关键字out特别声明,不然这段代码只会编译失败。比如下例
    例如:
 
  
Copy
class Program{ static void Main(string[]args) { ISalary<Programmer>s=new BaseSalaryCounter<Programmer>(); PrintSalary(s); } static void PrintSalary(ISalary<Employee>s) { s.Pay(); } } interface ISalary<T> { void Pay(); } class BaseSalaryCounter<T>:ISalary<T> { public void Pay() { Console.WriteLine("Pay base salary"); } } class Employee { public string Name{get;set;} } class Programmer:Employee{} class Manager:Employee{}

报错: 无法从“ConsoleApplication4.ISalary<ConsoleApplication4.Programmer>”转换为“ConsoleApplication4.ISalary<ConsoleApplication4.Employee>”
要让PrintSalary完成需求,我们可以使用泛型类型参数:

 
  
Copy
static void PrintSalary<T>(ISalary<T>s) { s.Pay(); }

实际上,只要泛型类型参数在一个接口声明中不被用来作为方法的输入参数,我们都可姑且把它看成是“返回值”类型的。所以,泛型类型参数这种模式是满足“协变”的定义的。但是,只要将T作为输入参数,便不满足“协变”的定义了。如:

 
  
Copy
interface ISalary<out T> { void Pay(T t); }

编译会提示:差异无效:类型参数“T”必须是在“ISalary.Pay(T)”上有效的逆变式。“T”为协变。

12.让接口中的泛型参数支持协变#

除了11中提到的使用泛型参数兼容泛型接口的不可变性外,还有一种办法就是为接口中的泛型声明加上out关键字来支持协变。
out关键字是FCL 4.0中新增的功能,它可以在泛型接口和委托中使用,用来让类型参数支持协变性。通过协变,可以使用比声明的参数派生类型更大的参数。通过下面例子我们应该能理解这种应用。
比如:

 
  
Copy
static void Main(string[]args) { ISalary<Programmer>s=new BaseSalaryCounter<Programmer>(); ISalary<Manager>t=new BaseSalaryCounter<Manager>(); PrintSalary(s); PrintSalary(t); } static void PrintSalary(ISalary<Employee>s)//用法正确 { s.Pay(); } } interface ISalary<out T> //使用了out关键字 { void Pay(); }

FCL 4.0对多个接口进行了修改以支持协变,如IEnumerable<out T>、IEnumerator<out T>、IQuerable<out T>等。由于IEnumerable<out T>现在支持协变,所以上段代码在FCL 4.0中能运行得很好。
在我们自己的代码中,如果要编写泛型接口,除非确定该接口中的泛型参数不涉及变体,否则都建议加上out关键字。协变增大了接口的使用范围,而且几乎不会带来什么副作用。

13.理解委托中的协变#

委托中的泛型变量天然是部分支持协变的。
比如:

 
  
Copy
public delegate T GetEmployeeHanlder<T>(string name); static void Main(){ GetEmployeeHanlder<Employee>getAEmployee=GetAManager; Employee e=getAEmployee("Mike"); }

因为存在下面这样一种情况,所以编译通不过:

 
  
Copy
GetEmployeeHanlder<Manager>getAManager=GetAManager;GetEmployeeHanlder<Employee>getAEmployee=getAManager; static Manager GetAManager(string name) { Console.WriteLine("我是经理:"+name); return new Manager(){Name=name}; } static Employee GetAEmployee(string name) { Console.WriteLine("我是雇员:"+name); return new Employee(){Name=name}; }

要让上面的代码编译通过,同样需要为委托中的泛型参数指定out关键字:

 
  
Copy
public delegate T GetEmployeeHanlder<out T>(string name);

FCL 4.0中的一些委托声明已经用out关键字来让委托支持协变了,如我们常常会使用到的:

 
  
Copy
public delegate TResult Func<out TResult>()和 public delegate TOutput Converter<in TInput,out TOutput>(TInput input)

14.为泛型类型参数指定逆变#

逆变是指方法的参数可以是委托或泛型接口的参数类型的基类。FCL 4.0中支持逆变的常用委托有:

 
  
Copy
Func<in T,out TResult> Predicate<in T> //常用泛型接口有: IComparer<in T>

举例:

 
  
Copy
class Program { static void Main() { Programmer p=new Programmer{Name="Mike"}; Manager m=new Manager{Name="Steve"}; Test(p,m); } static void Test<T>(IMyComparable<T>t1,T t2) { //省略 }} public interface IMyComparable<in T> { int Compare(T other); } public class Employee:IMyComparable<Employee> { public string Name{get;set;} public int Compare(Employee other) { return Name.CompareTo(other.Name); } } public class Programmer:Employee,IMyComparable<Programmer> { public int Compare(Programmer other) { return Name.CompareTo(other.Name); } } public class Manager:Employee{ }

在上面的这个例子中,如果不为接口IMy-Comparable的泛型参数T指定in关键字,将会导致Test(p, m)编译错误。由于引入了接口的逆变性,这让方法Test支持了更多的应用场景。在FCL4.0之后版本的实际编码中应该始终注意这一点。

  • 7
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值