Unity C# 爆破计划(十三):委托与 Lambda

15 篇文章 1 订阅


十三、委托和 Lambda

Covers:委托与事件,Lambda 是补充内容

我们刚刚学过泛型,委托与泛型有着很多联系,Lambda 又常常与委托配合使用,因此我们先讨论这一块。

委托

概念

现代的通用计算机在架构上属于冯 · 诺依曼机,它是由美籍匈牙利科学家约翰 · 冯 · 诺依曼等人提出的计算机架构。世界上第一台冯氏机,即“原型机”是 1949 年的 EDVAC。冯氏机机要求计算机上的 一切数据,以及对数据进行的一切操作,都是可存储的;因此,冯氏机是一种以存储为中心的计算机架构。

看了上面的科普,你会发现:原来“对数据进行的操作”也是可存储的,这意味着 程序和数据,在物理地位上是等同的,在程序运行过程中,它们 都存储在计算机的 “内存”这个高速 存储器中

那么,既然可以通过编程来操作各种数据,是否也可以像操作数据那样直接操作函数呢?C 和 C++ 中,我们用函数指针操作其他函数,函数指针也是指针(若不了解指针,请把它想象成 C# 中的引用),它指向函数,其实就是指向了函数编译后存储在内存中的位置,可以对函数指针使用函数调用运算符 () 来调用它指向的函数;Java 中并没有任何类似的特性。

C# 引入了 委托 的概念,它对应着 C/C++ 中的函数指针,也是一种特殊的类,委托对象能够保存其它方法,并在需要时调用

委托与类的地位一样,因此我们 要在命名空间作用域中定义它(常见的错误是将其定义在类内),语法为 ACCESS delegate RET_TYPE NAME(PARAMS);

  • 这声明了名为 NAME 的委托类型;
  • NAME 类型指代一种方法,它具有 RET_TYPE 类型的返回值,形参表如同 PARAMS(委托形参的名称无意义,随便指定即可);
  • NAME 的可见性是 ACCESS 的。

我们写一个小例子:

using System;

namespace LearnDelegate
{
    public delegate void SimpleOperation();

    class Program
    {
        static void Hello()
        {
            Console.WriteLine("Hello, delegate!");
        }

        public static void Main()
        {
            SimpleOperation f = Hello;
            f();
        }
    }
}

我们定义了委托类型 SimpleOperation,它可以表示任何无返回值、不接受参数的方法;在 Program 中我们定义了一个方法 Hello,它的签名符合 SimpleOperation 的要求;在 Main 中我们先产生一个 SimpleOperation 的对象“f”(注意它可以不用 new 关键字),并为其赋予方法“Hello”(这里的方法名后面不能加括号,加上就是调用 Hello 了);接下来有趣的事情出现了:我们 可以直接“调用”委托对象 f,好像它是一个方法一样

泛型委托

学习了泛型之后,我们就会想用泛型来扩大一个定义的适用范围。委托经常与泛型联用:

using System;

namespace LearnDelegate
{
    public delegate T BinaryOperation<T>(T l, T r);

    static class Math
    {
        public static double Add(double l, double r) => l + r;
        public static double Subtract(double l, double r) => l - r;
        public static double Multiply(double l, double r) => l * r;
        public static double Divide(double l, double r) => l * (1.0 / r);
        public static double Power(double l, double r) => System.Math.Pow(l, r);
    }

    class Program
    {
        static void Main()
        {
            BinaryOperation<double> calculator = Math.Power;
            Console.WriteLine(calculator(2, 10));
        }
    }
}

我们定义了“二元操作”泛型委托类型 BinaryOperation,它能够代表任何“接受两个同型参数,返回同型值”的方法,且类型不受限制;接着我们定义了 Math 类,它封装了一些简单快乐的算术方法(System.Math.Pow 是求乘方的内置方法,这样就不用手写这个函数了);在测试代码中我们赋予 T 以 double 类型,从而产生了一个委托对象“calculator”,它就像一个功能未定的计算器,我们赋予它乘方的功能,并调用了它。

Func 和 Action 委托

委托类本身是没有定义体的,说到底它们只是一些对函数签名的约定;如果我们要大量使用委托,一个个定义或许令人感到机械和低效。.NET 为我们写好了两大类泛型委托 Func 和 Action,它们跟我们手写的委托用了完全一样的语法,使用上完全没有区别,因此 大多数情况下我们是不需要自己手写委托的

  • System.Action 对应 无返回值、接受 0~15 个参数的方法
  • System.Func 对应 有返回值、接受 0~15 个参数的方法返回值的类型总是在类型参数列表的最后一个,比如 Func<int, int, double> 对应接受 2 个 int 型参数,返回 double 型的方法。

你问这种泛型可变形参表是用了什么技巧?额,微软其实 逐个 定义了它们,不过 32 个而已。

用 Func 委托改写上面“计算器”例子的测试代码:

static void Main()
{
    Func<double, double, double> calculator = Math.Power;
    Console.WriteLine(calculator(2, 10));
}

如果很长的类型参数列表让你感到丑陋,可以用我们的朋友 using 关键字来给某一种 Func 起别名:

using System;

namespace LearnDelegate
{
    using BiOp = Func<double, double, double>;

    class Math ...

    class Program
    {
        static void Main()
        {
            BiOp calculator = Math.Power;
            Console.WriteLine(calculator(2, 10));
        }
    }
}

注意:using 语句只能出现在命名空间的开头,或整个文件的开头。

多播

C# 的委托提供了 多播 特性,一个委托可以按顺序执行一系列方法。使用多播的语法简单得难以置信,只要给委托对象做加法即可:

...
    Action dele = Method1;
    dele += Method2;
...

在执行 dele 时,就会依次调用 Method1、Method2……调用的顺序就是你将方法加给 dele 的顺序。

并发执行

委托可以派生线程,在其他线程中执行,这是 C# 常用的并发方式之一。由于这里涉及到并发编程知识,展开讲太费笔墨,因此这一节仅供有基础的朋友作简单参考。

  • 使用委托对象的 BeginInvoke 方法,令其派生线程并在其中执行委托所绑定的方法,BeginInvoke 接受两个参数,第一个是线程结束后的回调,不需要时给 null 即可;第二个是非静态方法的主调对象,也可以给 null
  • 使用委托对象的 EndInvoke 方法等待派生出的线程返回,要使用该方法,必须获得 BeginInvoke 方法返回的 IAsyncResult 对象,将其作为 EndInvoke 的唯一参数传入。

下面的例子演示了 BeginInvoke 和 EndInvoke 的使用,引入的 Threading 命名空间仅用于线程睡眠,非必须:

using System;
using System.Threading;

namespace LearnAsyncDelegate
{
    class Actor
    {
        string _name;

        public Actor(string name)
        {
            _name = name;
        }

        public void Run()
        {
            Console.WriteLine(_name + ".Run called");
            
            // Sleep for a period of time:
            Thread.Sleep(200);
            
            Console.WriteLine(_name + ".Run returning");
        }
    }

    class Program
    {
        static void Main()
        {
            // All objects:
            Actor[] staff =
            {
                new Actor("#1"),
                new Actor("#2"),
                new Actor("#3"),
                new Actor("#4"),
                new Actor("#5"),
            };
            
            // All delegates:
            Action[] roles =
            {
                staff[0].Run,
                staff[1].Run,
                staff[2].Run,
                staff[3].Run,
                staff[4].Run,
            };
            
            // A pre-allocated IAsyncResult array:
            var results = new IAsyncResult[5];

            // Begin threads:
            for (int i = 0; i < roles.Length; ++i)
            {
                results[i] = roles[i].BeginInvoke(null, null);
            }

            // Wait for threads to join:
            for (int i = 0; i < roles.Length; ++i)
            {
                roles[i].EndInvoke(results[i]);
            }
        }
    }
}

可能的控制台输出:

#1.Run called
#2.Run called
#5.Run called
#3.Run called
#4.Run called
#2.Run returning
#4.Run returning
#3.Run returning
#5.Run returning
#1.Run returning

Lambda

匿名函数

Lambda(常称为 Lambda 表达式)是 匿名函数,它是函数,但它没有名字,但我们通过学习委托已经知道,函数其实与数据一样,都可以被程序所处理。我们可以将 Lambda 赋予委托对象,再通过委托调用之。

Lambda 的定义语法是:

  • (PARAMS)=>{BODY},即 用括号包围的形参表与函数执行体之间用一个 => 连接
  • 同简单方法一样,直接返回的 Lambda 也可以用 => RET_VAL 的语法,省去 BODY 直接返回 RET_VAL;
  • 将 Lambda 赋予委托时,如果委托所要求的函数签名已经确定(非泛型委托,或泛型已经绑定了实际类型),则 Lambda 的各形参类型也能够推断,因此可以不写形参的类型;

使用 Lambda 表达式可以避免定义很多只在一个作用域中使用的细碎方法,我们写一个例子:

using System;

namespace LearnDelegate
{
    using BiOp = Func<double, double, double>;

    class Program
    {
        static void Main()
        {
            BiOp add = (l, r) => l + r;
            BiOp subtract = (l, r) => l - r;
            BiOp multiply = (l, r) => l * r;
            BiOp divide = (l, r) => l * (1.0 / r);

            Console.WriteLine("3.0 + 7.0 = " + add(3.0, 7.0));
            Console.WriteLine("3.0 - 7.0 = " + subtract(3.0, 7.0));
            Console.WriteLine("3.0 * 7.0 = " + multiply(3.0, 7.0));
            Console.WriteLine("3.0 / 7.0 = " + divide(3.0, 7.0));
        }
    }
}

上面的程序没有定义任何细碎的小方法,命名空间比较干净。

局部函数

上面的例子中,绑定了 Lambda 的委托对象就像一个“可以调用的局部变量”一样;委托作为一种对象,也要占用一部分资源,那我们是否可以抛开委托,直接把函数像一个局部变量那样定义出来,同时也能用完就扔呢?

局部函数 是函数,因为它可以被调用;它又是局部的,因为它只在其作用域内有效。我们用 RET_TYPE FUNCTION(PARAMS) => {BODY} 来定义局部函数,它可以直接定义在程序的执行体中,除了能够被调用,它就像一个局部变量。

局部函数不再是匿名的(必须为其命名),因为如果它也没有名字,就再也没什么符号可以指代这个函数了;局部函数也可以被绑定到委托上。

继续改写上面的例子:

static void Main()
{
    double Add(double l, double r) => l + r;
    double Subtract(double l, double r) => l - r;
    double Multiply(double l, double r) => l * r;
    double Divide(double l, double r) => l * (1.0 / r);

    Console.WriteLine("3.0 + 7.0 = " + Add(3.0, 7.0));
    Console.WriteLine("3.0 - 7.0 = " + Subtract(3.0, 7.0));
    Console.WriteLine("3.0 * 7.0 = " + Multiply(3.0, 7.0));
    Console.WriteLine("3.0 / 7.0 = " + Divide(3.0, 7.0));
}

注意,我们抛开了委托的概念(这段代码里没有用到委托),因此函数的形参类型就无法从委托的签名要求中推断了,因此不能省略。


T.B.C.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值