对 Func 和 Action 泛型委托使用变体 (C#)

这些示例演示如何在泛型委托FuncAction中使用协变和逆变,以便重复利用方法,并在代码中提供更大的灵活性。

有关协变和逆变的详细信息,请参阅委托中的变体 (C#)

使用具有协变类型参数的委托

下例阐释了泛型 Func 委托中的协变支持的益处。 FindByTitle 方法接收一个 String 类型的参数,并返回一个 Employee 类型的对象。 但是,您可以将此方法分配给Func<String, Person>委托,因为Employee继承Person

// Simple hierarchy of classes.  
public class Person { }  
public class Employee : Person { }  
class Program  
{  
    static Employee FindByTitle(String title)  
    {  
        // This is a stub for a method that returns  
        // an employee that has the specified title.  
        return new Employee();  
    }  
  
    static void Test()  
    {  
        // Create an instance of the delegate without using variance.  
        Func<String, Employee> findEmployee = FindByTitle;  
  
        // The delegate expects a method to return Person,  
        // but you can assign it a method that returns Employee.  
        Func<String, Person> findPerson = FindByTitle;  
  
        // You can also assign a delegate
        // that returns a more derived type
        // to a delegate that returns a less derived type.  
        findPerson = findEmployee;  
  
    }  
}  

使用具有逆变类型参数的委托

下例阐释了泛型 Action 委托中的逆变支持的益处。 AddToContacts 方法传递一个 Person 类型的参数。 但是,您可以将此方法分配给Action<Employee>委托,因为Employee继承Person

public class Person { }  
public class Employee : Person { }  
class Program  
{  
    static void AddToContacts(Person person)  
    {  
        // This method adds a Person object  
        // to a contact list.  
    }  
  
    static void Test()  
    {  
        // Create an instance of the delegate without using variance.  
        Action<Person> addPersonToContacts = AddToContacts;  
  
        // The Action delegate expects
        // a method that has an Employee parameter,  
        // but you can assign it a method that has a Person parameter  
        // because Employee derives from Person.  
        Action<Employee> addEmployeeToContacts = AddToContacts;  
  
        // You can also assign a delegate
        // that accepts a less derived parameter to a delegate
        // that accepts a more derived parameter.  
        addEmployeeToContacts = addPersonToContacts;  
    }  
}  

逆变和匿名函数

使用匿名函数(lambda 表达式)时,可能会遇到与逆变相关的反直觉行为。 请看下面的示例:

public class Person
{
    public virtual void ReadContact() { /*...*/ }
}

public class Employee : Person
{
    public override void ReadContact() { /*...*/ }
}

class Program
{
    private static void Main()
    {
        var personReadContact = (Person p) => p.ReadContact();

        // This works - contravariance allows assignment.
        Action<Employee> employeeReadContact = personReadContact;

        // This causes a compile error: CS1661.
        // Action<Employee> employeeReadContact2 = (Person p) => p.ReadContact();
    }
}

这种行为似乎自相矛盾:如果逆变允许将接受基类型的委托(Person)分配给需要派生类型的委托变量(Employee),那么为什么直接分配一个 lambda 表达式会失败呢?

主要区别是 类型推理。 在第一种情况下,lambda 表达式首先分配给具有类型的 var变量,这会导致编译器将 lambda 的类型推断为 Action<Person>。 由于委托的逆变规则,后续对 Action<Employee> 的分配成功。

第二种情况下,编译器无法直接推断 lambda 表达式(Person p) => p.ReadContact()在被Action<Person>分配到时应具有类型Action<Employee>。 匿名函数的类型推理规则在初始类型确定期间不会自动应用逆变。

解决方法

若要实现直接赋值,可以使用显式强制转换:

// Explicit cast to the desired delegate type.
Action<Employee> employeeReadContact = (Action<Person>)((Person p) => p.ReadContact());

// Or specify the lambda parameter type that matches the target delegate.
Action<Employee> employeeReadContact2 = (Employee e) => e.ReadContact();

此行为说明了委托逆变(在类型确定后起作用)和 lambda 表达式类型推理(在编译期间发生)之间的差异。

另请参阅