C# Action 和 Func

前言

整理归纳一下他俩的联系和区别。

一、联系

Action 和 Func 都是 .Net 预定义的 委托类型

二、区别

看一下 Action 的定义,有两类:

public delegate void Action();
public delegate void Action<in T>(T obj);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
public delegate void Action<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);
...

再看一下 Func 的定义,也有两类:

public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
public delegate TResult Func<in T1, in T2, in T3, out TResult>(T1 arg1, T2 arg2, T3 arg3);
...

可以清楚的看到:

1,Action 没有返回值, Func 有返回值。

2,Func 参数列表里最后一个,是该委托方法的返回值类型。(通过定义里的 in 和 out 可以分辨)

三、代码示例

下面通过代码,演示他们的用法和区别。

Action

1,最简单的情况,不带参数的 Action(),同步执行:

class Program
{
    static void Main(string[] args)
    {
        Action action = Printf_done;

        //写法1
        action();

        //写法2
        action.Invoke(); //加不加 Invoke() 有什么区别吗

        //写法2
        Printf();

        Console.ReadKey();
    }

    static void Printf_done()
    {
        Console.WriteLine("done");
    }
}

上面 3 种写法的结果都打印出 “done”,写法 1 和写法 2 最后生成的 IL 指令是一样的,可见 action() 是 action.Invoke() 的简写。

2,不带参数的 Action<in T>(T obj) ,异步执行,需要回调函数,回调必须带参数:

class Program
{
    static void Main(string[] args)
    {
        Action action = Printf_done;
        action.Invoke();

        action.BeginInvoke(Printf_CallBack, "优蓝盛夏");

        Console.WriteLine("Main 结束, 线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
        Console.ReadKey();
    }

    static void Printf_done()
    {
        Console.WriteLine("done, 线程ID:{0}",Thread.CurrentThread.ManagedThreadId);
    }
    
    //AsyncCallback 类型的方法
    static void Printf_CallBack(IAsyncResult async)
    {
        Thread.Sleep(2000);
        Console.WriteLine("{0} done, 线程ID:{1}", async.AsyncState, 
        Thread.CurrentThread.ManagedThreadId);
    }
}

执行结果:

可以看到,Invoke 是同步的,BeginInvoke 是异步的。BeginInvoke 异步的时候,必须指定一个 AsyncCallback  类型的方法,这个方法很简单,包含一个 IAsyncResult 类型的参数就可以了。

3,带参数的 Action<in T>(T obj) ,同步执行和异步执行对比:

class Program
{
    static void Main(string[] args)
    {
        Action<string> action = Printf_done;
        action.Invoke("同步 action");  //与 action("action");有什么区别吗?

        action.BeginInvoke("异步 action",Printf_CallBack, "优蓝盛夏");
        Console.WriteLine("Main 结束, 线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
        Console.ReadKey();
    }

    static void Printf_done(string name)
    {
        Console.WriteLine("{0} done, 线程ID:{1}", name,Thread.CurrentThread.ManagedThreadId);
    }

    //AsyncCallback 类型的方法
    static void Printf_CallBack(IAsyncResult async)
    {
         Thread.Sleep(3000);
         Console.WriteLine("{0}, 线程ID:{1}", async.AsyncState, 
         Thread.CurrentThread.ManagedThreadId);
    }
}

执行结果:

可以看出,带参数的时候, Invoke 和 BeginInvoke 的参数列表都要带有实参,BeginInvoke 的时候,和回调函数写在一起。

4,匿名方法,这里穿插一个匿名方法的 “发展史”:

class Program
{
    static void Main(string[] args)
    {
        //写法 1
        Action<string> action = (string name) =>
        {
            Console.WriteLine("{0} done, 线程ID:{1}", name, 
            Thread.CurrentThread.ManagedThreadId);
        };

        //写法2
        Action<string> action = (name) =>
        {
            Console.WriteLine("{0} done, 线程ID:{1}", name, 
            Thread.CurrentThread.ManagedThreadId);
        };

        //写法3
        Action<string> action = name =>
        {
            Console.WriteLine("{0} done, 线程ID:{1}", name, 
            Thread.CurrentThread.ManagedThreadId);
        };

        action.Invoke("同步 action");
        action.BeginInvoke("异步 action", Printf_CallBack, "优蓝盛夏");

        Console.WriteLine("Main 结束, 线程ID:{0}", Thread.CurrentThread.ManagedThreadId);
        Console.ReadKey();
    }

    //AsyncCallback 类型的方法
    static void Printf_CallBack(IAsyncResult async)
    {
        Thread.Sleep(3000);
        Console.WriteLine("{0}, 线程ID:{1}", async.AsyncState,
        Thread.CurrentThread.ManagedThreadId);
    }
}

写法 1 、2、3 的运行结果都一样,但是写法却 “笑容逐渐变态”。

写法 1 ,匿名方法把返回值类型匿了,因为可以编译器推断(Action 肯定为 void);把方法名匿了,因为编译器可以自己取个名。

写法 2,青出于蓝,把参数类型都匿了,因为编译器可以根据方法体进行推断(Action<string> 已经指定了类型)。

写法 3,胜于蓝,把参数列表的括号都省略了。注意:当没有参数和有多个参数的时候,括号可是要保留的哦。

Func

这里直接写匿名方法,示例同步情况下无参和有两个参的情况,返回值是必须要有的。异步的情况和 Action 一样,不赘述了:

class Program
{
    static void Main(string[] args)
    {
        //没有参数,但是有返回值
        Func<string> func = () =>
        {
            return "Func";
        };
        Console.WriteLine(func.Invoke() + " done, 线程ID:{0}", 
        Thread.CurrentThread.ManagedThreadId);
        
        //两个参数,但是有返回值
        Func<int, int, string> func = (x, y) =>
        {
             Console.Write("Func ");
             return (x + y).ToString();
        };
        Console.WriteLine(func.Invoke(2, 7) + " done, 线程ID:{0}", 
        Thread.CurrentThread.ManagedThreadId);

        Console.ReadKey();
    }
}

执行结果:

可以看出,Action 和 Func 除了有无返回值之外,没有任何区别。他们的重载也都是最多有16个参数。如果想要有多个返回值,可以使用委托自己定义,返回类型用 tuple 或者 Dictionary 或者 List 都可以。

后记

异步操作的时候,使用了回调函数,实际上也必须使用回调,不然无法异步,为什么这么说,其实这是语法的关系

举个例子,下面这句代码:

action.BeginInvoke("异步 action", Printf_CallBack, "优蓝盛夏");

Printf_CallBack 是方法名,”优蓝盛夏” 是 Printf_CallBack 的参数。那么问题来了,为什么 BeginInvoke 的参数列表,要把方法名和方法的参数分开写,如果这样写岂不是更好理解:

action.BeginInvoke("异步 action", Printf_CallBack("优蓝盛夏"));

其实这就是个语法问题,Printf_CallBack(“优蓝盛夏”); 表示的是立刻执行 Printf_CallBack 方法并且返回,而我们是要异步啊,我们不知道这个方法会什么时候执行,所以要把方法名和参数拆开丢给 BeginInvoke 。

喜欢砸锅卖铁,哦不,喜欢打破砂锅问到底的话,可以看一下编译后的 IL 代码。

发表评论