当设计应用程序时,如何使用 Func<> 和 Action<>?

73

我能找到的 Func<> 和 Action<> 的所有示例都很简单,例如下面的示例,您可以看到它们在技术上如何工作,但我想看到它们用于解决以前无法解决或只能以更复杂的方式解决的问题的示例。我知道它们的工作原理并且它们非常简洁和强大,因此我想从更广泛的角度了解它们解决哪些问题以及如何在应用程序设计中使用它们。

您如何使用 Func<> 和 Action<> 的方式(模式)来解决实际问题?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestFunc8282
{
    class Program
    {
        static void Main(string[] args)
        {
            //func with delegate
            Func<string, string> convert = delegate(string s)
            {
                return s.ToUpper();
            };

            //func with lambda
            Func<string, string> convert2 = s => s.Substring(3, 10);

            //action
            Action<int,string> recordIt = (i,title) =>
                {
                    Console.WriteLine("--- {0}:",title);
                    Console.WriteLine("Adding five to {0}:", i);
                    Console.WriteLine(i + 5);
                };

            Console.WriteLine(convert("This is the first test."));
            Console.WriteLine(convert2("This is the second test."));
            recordIt(5, "First one");
            recordIt(3, "Second one");

            Console.ReadLine();

        }
    }
}
9个回答

59

它们还适用于重构 switch 语句。

以以下(尽管简单)示例为例:

public void Move(int distance, Direction direction)
{
    switch (direction)
    {
        case Direction.Up :
            Position.Y += distance;
            break;
        case Direction.Down:
            Position.Y -= distance;
            break;
        case Direction.Left:
            Position.X -= distance;
            break;
        case Direction.Right:
            Position.X += distance;
            break;
    }
}

使用Action委托,您可以将其重构如下:

static Something()
{
    _directionMap = new Dictionary<Direction, Action<Position, int>>
    {
        { Direction.Up,    (position, distance) => position.Y +=  distance },
        { Direction.Down,  (position, distance) => position.Y -=  distance },
        { Direction.Left,  (position, distance) => position.X -=  distance },
        { Direction.Right, (position, distance) => position.X +=  distance },
    };
}

public void Move(int distance, Direction direction)
{
    _directionMap[direction](this.Position, distance);
}

12
这是一项非常有用的技术,有许多原因支持。与 switch 语句不同,您可以从外部数据动态地填充 action 映射表。此外,键不必是 'int' 或 'string' 类型。 - Robert Rossney
这在需要时非常强大,但请记住,switch语句通常非常快,至少在可以使用跳转表的实现中是如此。我无法确定.NET是否使用它们。 - Samantha Branham
这可能会有所帮助 http://akshaya-m.blogspot.com/2015/03/elegant-way-to-switch-if-else.html#comment-form - Akxaya

15

使用linq。

List<int> list = { 1, 2, 3, 4 };

var even = list.Where(i => i % 2);

Where方法的参数是一个Func<int, bool>

匿名函数是我最喜欢C#的部分之一。:)


4
Where 方法的参数实际上是一个 Func<T, bool>,而不是一个 Action<T, bool> - LukeH
msdn似乎表明它是一个Func<TSource,bool>类型的函数。http://msdn.microsoft.com/en-us/library/bb534803.aspx - Yannick Motton
@Yannick M. 确实如此。但在我的例子中,它是从通用 List 中的 T 派生出来的。 - Daniel A. White
我知道,我的评论是针对你认为它是一个 Action<>Action 的返回类型为 void,因此假设它会在 where 中使用是不合逻辑的。 - Yannick Motton
哦,我以为你用 TSource 指的是它。 - Daniel A. White
@leppie - 是的,但是现在许多编译器内部都有这个功能。 - Daniel A. White

14
我经常使用ActionFunc委托。通常我会用Lambda语法声明它们,以节省空间,并主要用于减小大型方法的大小。在审查我的方法时,有时会突出显示相似的代码段。在这些情况下,我将相似的代码片段封装到ActionFunc中。使用委托可以减少冗余代码,为代码段提供良好的签名,并且如果需要,可以轻松地升级为方法。
我曾经编写Delphi代码,你可以在函数内部声明一个函数。Action和Func让我在C#中实现了相同的行为。
这是使用委托重新定位控件的示例:
private void Form1_Load(object sender, EventArgs e)
{
    //adjust control positions without delegate
    int left = 24;

    label1.Left = left;
    left += label1.Width + 24;

    button1.Left = left;
    left += button1.Width + 24;

    checkBox1.Left = left;
    left += checkBox1.Width + 24;

    //adjust control positions with delegate. better
    left = 24;
    Action<Control> moveLeft = c => 
    {
        c.Left = left;
        left += c.Width + 24; 
    };
    moveLeft(label1);
    moveLeft(button1);
    moveLeft(checkBox1);
}

5
有趣的是,这两段文字的行数一样。 - JustLoren
@JustLoren,随着操作变得越来越大,行数会减少。但无论如何,你会有更少的维护噩梦,这才是真正的关键。 - nawfal

9

我使用它的一项功能是对于那些在相同输入的情况下永远不会改变的昂贵方法调用进行缓存:

public static Func<TArgument, TResult> Memoize<TArgument, TResult>(this Func<TArgument, TResult> f)
{
    Dictionary<TArgument, TResult> values;

    var methodDictionaries = new Dictionary<string, Dictionary<TArgument, TResult>>();

    var name = f.Method.Name;
    if (!methodDictionaries.TryGetValue(name, out values))
    {
        values = new Dictionary<TArgument, TResult>();

        methodDictionaries.Add(name, values);
    }

    return a =>
    {
        TResult value;

        if (!values.TryGetValue(a, out value))
        {
            value = f(a);
            values.Add(a, value);
        }

        return value;
    };
}

默认的递归斐波那契例子:

class Foo
{
  public Func<int,int> Fibonacci = (n) =>
  {
    return n > 1 ? Fibonacci(n-1) + Fibonacci(n-2) : n;
  };

  public Foo()
  {
    Fibonacci = Fibonacci.Memoize();

    for (int i=0; i<50; i++)
      Console.WriteLine(Fibonacci(i));
  }
}

1
我认为这里有一个错误:fib不应该调用自身,或者你是想让它调用属性(该属性是对自身的委托引用)吗? - Joel Coehoorn
1
它确实应该调用委托。否则,您将无法拦截递归调用。思路是封装调用,以便缓存实际上变得有用。 - Yannick Motton

6

我使用一个Action来优雅地封装在事务中执行数据库操作:

public class InTran
{
    protected virtual string ConnString
    {
        get { return ConfigurationManager.AppSettings["YourDBConnString"]; }
    }

    public void Exec(Action<DBTransaction> a)
    {
        using (var dbTran = new DBTransaction(ConnString))
        {
            try
            {
                a(dbTran);
                dbTran.Commit();
            }
            catch
            {
                dbTran.Rollback();
                throw;
            }
        }
    }
}

现在,要在事务中执行操作,我只需要这样做:
new InTran().Exec(tran => ...some SQL operation...);

InTran类可以放置在一个通用库中,减少重复,并提供一个单一的位置进行未来功能调整。


1
抱歉,你能详细说明一下吗?这样使用的意义是什么? - shanmugharaj

6

2

实际上,我在stackoverflow上发现了这个想法(至少是这个想法):

public static T Get<T>  
    (string cacheKey, HttpContextBase context, Func<T> getItemCallback)
            where T : class
{
    T item = Get<T>(cacheKey, context);
    if (item == null) {
        item = getItemCallback();
        context.Cache.Insert(cacheKey, item);
    }

    return item;
}

不好意思,我不能。那是一个“技巧和窍门”问题。 - Arnis Lapsa
这些是泛型,不是Func或Action。不同的东西。 - Alex
4
有时我在想,人们在发帖前会先阅读吗?@Alex,请再次检查函数参数。 - Arnis Lapsa

2
通过将它们保持通用性并支持多个参数,我们可以避免创建强类型委托或执行相同操作的冗余委托。

0

我有一个单独的表单,构造函数接受一个通用的Func或Action以及一些文本。它在一个单独的线程上执行Func/Action,同时在表单中显示一些文本并显示动画。

这是我的个人Util库中的内容,每当我想要进行中等长度的操作并以非侵入性的方式阻止UI时,我都会使用它。

我考虑在表单上放置一个进度条,以便它可以执行更长时间的操作,但我还没有真正需要它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接