高级 C# 进阶手册(二)

原文:Getting Started with Advanced C#

协议:CC BY-NC-SA 4.0

三、Lambda 表达式

Lambda 表达式和匿名方法是高级编程中的两个重要概念。总的来说,它们通常被称为匿名函数。C# 2.0 引入了匿名方法的概念,C# 3.0 引入了 lambda 表达式。随着时间的推移,lambda 表达式变得比匿名方法更受欢迎。如果你瞄准了。NET Framework 3.5 或更高版本,建议您使用 lambda 表达式。本章向你展示了使用 lambda 表达式的不同方法,以及如何在高级编程中有效地使用它们。

Lambda 表达式的有用性

lambda 表达式是一种以易于阅读的形式编写的匿名方法。什么是匿名方法,为什么它有用?顾名思义,匿名方法就是没有名字的方法。在某些情况下,它们非常有用。例如,当您使用委托指向一个方法,但该方法出现在源文件中的不同位置时(或者,在极端情况下,它出现在不同的源文件中)。这种分离的代码很难理解、调试和维护。在这种情况下,匿名方法很有帮助,因为您可以定义一个没有名称的“内联”方法来满足您的目的。

lambda 这个词来自 lambda calculus,它模拟了一个图灵机。它用希腊字母 lambda (λ)拼写,而你的键盘上没有。要表示一个 lambda 运算符,可以使用= >符号。运算符的左侧指定输入参数(如果有),运算符的右侧指定表达式或语句块。= >是右关联的,其优先级与=相同。当读取包含 lambda 运算符的代码时,将 lambda 运算符替换为转到 转到 箭头 **,**或成为。比如你读x=> x+5;因为x去了x+5。同样,你把(x,y)=>x+y;读成xyx+y

C# 编译器可以将 lambda 表达式转换为委托实例或表达式树(这在 LINQ 中经常使用)。这本书没有详细讨论 LINQ,但是你已经了解了代表,并且在第一章看到了几个关于他们的例子。让我们在这里关注委托实例。

Note

当 lambda 表达式转换为委托类型时,结果取决于输入参数和返回类型。如果一个 lambda 表达式没有返回类型,它可以被转换成一个Action委托类型;如果它有一个返回类型,它可以被转换成一个Func委托类型。FuncAction是通用的代表,你会在第四章中了解到。

演示 1

我从一个简单的程序开始,这个程序使用各种方法计算两个整数(21 和 79)的和。第一种方法使用普通的方法(这是您所熟悉的)。您可以使用这个方法来计算int的总和。接下来,我将向您展示如何使用一个委托实例来做同样的事情。最后两段代码分别展示了匿名方法和 lambda 表达式的用法。每个程序段生成相同的输出。这个程序让你选择方法。为了可读性,请浏览支持性注释。

using System;

namespace LambdaExpressionEx1
{
    public delegate int Mydel(int x, int y);

    class Program
    {
        public static int Sum(int a, int b) { return a + b; }

        static void Main(string[] args)
        {
            Console.WriteLine("***Exploring the use of a lambda expression and comparing it with other techniques. ***");
            // Without using delgates or lambda expression
            Console.WriteLine(" Using a normal method.");
            int a = 21, b = 79;
            Console.WriteLine(" Invoking the Sum() method in a common way without using a delegate.");
            Console.WriteLine("Sum of {0} and {1} is : {2}", a,b, Sum(a, b));

            /* Using Delegate(Initialization with a named method)*/
            Mydel del1 = new Mydel(Sum);
            Console.WriteLine("\n Using delegate now.");
            Console.WriteLine("Invoking the Sum() method with the use of a delegate.");
            Console.WriteLine("Sum of {0} and {1} is : {2}", a, b, del1(a, b));

            // Using Anonymous method (C# 2.0 onwards)
            Mydel del2 = delegate (int x, int y) { return x + y; };
            Console.WriteLine("\n Using anonymous method now.");
            Console.WriteLine("Invoking the Sum() method using an anonymous method.");
            Console.WriteLine("Sum of {0} and {1} is : {2}", a, b, del2(a, b));

            // Using Lambda expression(C# 3.0 onwards)
            Console.WriteLine("\n Using lambda expression now.");
            Mydel sumOfTwoIntegers = (x, y) => x + y;
            Console.WriteLine("Sum of {0} and {1} is : {2}", a, b, sumOfTwoIntegers(a, b));
            Console.ReadKey();
        }
    }
}

输出

以下是运行该程序的输出。

***Exploring the use of a lambda expression and comparing it with other techniques.***
Using a normal method.
Invoking the Sum() method in a common way without using a delegate.
Sum of 21 and 79 is : 100

 Using delegate now.
Invoking the Sum() method with the use of a delegate.
Sum of 21 and 79 is : 100

 Using anonymous method now.
Invoking the Sum() method using an anonymous method.
Sum of 21 and 79 is : 100

 Using lambda expression now.
Sum of 21 and 79 is : 100

分析

让我们回顾一下用于匿名方法和 lambda 表达式的语句。对于匿名方法,我使用了

delegate (int x, int y) { return x + y; };

对于 lambda 表达式,我使用

(x, y) => x + y;

如果您熟悉匿名方法,但不熟悉 lambda 表达式,您可以使用以下步骤从匿名方法中获取 lambda 表达式。

对于第 1 步,从匿名方法表达式中删除 delegate 关键字,这将产生如图 3-1 所示的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-1

从匿名方法表达式中移除 delegate 关键字

也就是你得到(int x, int y) {return x + y; };

在步骤 2 中,添加一个 lambda 操作符,结果如图 3-2 所示。(它还会产生有效的 lambda 表达式。)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-2

在步骤 1 表达式中添加 lambda 运算符

请注意,在本例中,我处理的是一个 return 语句。在这种情况下,作为第 3 步,您可以删除花括号、分号和回车,结果如图 3-3 所示(这是一个有效的 lambda 语句)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-3

从步骤 2 的表达式中删除花括号、分号和“return”

也就是说,你得到:(int x, int y) => x + y;

在大多数情况下,编译器在处理 lambda 表达式时可以识别输入参数和返回类型。用编程术语来说,这叫做类型推理。尽管如此,在某些特殊情况下,您可能需要保留这种类型信息,以便让编译器正确地计算表达式。但这是一个非常简单的情况,编译器可以正确理解它(在这种情况下,请注意委托声明),即使您没有提到输入参数的类型。因此,对于步骤 4,您可以从输入参数中移除类型信息,并使表达式更短,如图 3-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-4

从步骤 3 的表达式中删除类型信息以获得最短的表达式

也就是你得到(x, y) => x + y;

有(和没有)参数的 Lambda 表达式

lambda 表达式可以接受一个或多个参数。也可以使用不接受任何参数的 lambda 表达式。

在演示 1 中,您看到当一个 lambda 表达式使用多个参数时,您将它们列在用逗号分隔的括号中,就像(x, y)=> x+y;

如果 lambda 表达式只接受一个参数,可以省略括号。例如,您可以使用(x)=> x*x;x=>x*x;。两者都有相同的目的。

最后,() => Console.WriteLine("No parameter.");是一个没有任何参数的 lambda 表达式的例子。演示 2 展示了所有案例。

演示 2

本演示涵盖了带有不同参数的 lambda 表达式的用法。

using System;

namespace LambdaExpTest2
{
    class Program
    {
        public delegate void DelegateWithNoParameter();
        public delegate int DelegateWithOneIntParameter(int x);
        public delegate void DelegateWithTwoIntParameters(int x, int y);
        static void Main(string[] args)
        {

            Console.WriteLine("***Experimenting lambda expressions with different parameters.***\n");
            // Without lambda exp.
            Method1(5, 10);

            // Using Lambda expression

            DelegateWithNoParameter delWithNoParam = () => Console.WriteLine("Using lambda expression with no parameter, printing Hello");
            delWithNoParam();

            DelegateWithOneIntParameter delWithOneIntParam = (x) => x * x;
            Console.WriteLine("\nUsing a lambda expression with one parameter, square of 5 is {0}", delWithOneIntParam(5));

            DelegateWithTwoIntParameters delWithTwoIntParam = (int x, int y) =>
              {
                  Console.WriteLine("\nUsing lambda expression with two parameters.");
                  Console.WriteLine("It is called a statement lambda because it has a block of statements in it's body.");
                  Console.WriteLine("This lambda accepts two parameters.");
                  int sum = x + y;
                  Console.WriteLine("Sum of {0} and {1} is {2}", x, y, sum);
              };

            delWithTwoIntParam(10,20);

            Console.ReadKey();
        }

        private static void Method1(int a, int b)
        {
            Console.WriteLine("\nThis is Method1() without lambda expression.");
            int sum = a + b;
            Console.WriteLine("Sum of {0} and {1} is {2}", a, b, sum);
        }
    }
}

输出

以下是运行该程序的输出。

***Experimenting lambda expressions with different parameters.***
This is Method1() without lambda expression.
Sum of 5 and 10 is 15

Using lambda expression with no parameter, printing Hello

Using a lambda expression with one parameter, square of 5 is 25

Using lambda expression with two parameters.
It is called a statement lambda because it has a block of statements in it's body.
This lambda accepts two parameters.
Sum of 10 and 20 is 30

Lambda 表达式的类型

理想情况下,lambda 表达式用于单行方法。但是在演示 2 中,您看到了 lambda 表达式可以不止一行。

在编程术语中,您将 lambda 表达式分为表达式 lambdas 和语句 lambdas。表达式 lambda 只有一个表达式,而语句 lambda 包含一组语句。语句 lambdas 可以使用花括号、分号和 return 语句。一个语句 lambda 可以包含任意数量的语句,但一般来说,它们包含两个或三个语句。如果在一个 lambda 表达式中使用三行以上,可能会使理解变得复杂;在这些情况下,你可能更喜欢普通的方法而不是 lambda 表达式。

表情丰富的成员

Lambda 表达式最早出现在 C# 3.0 中,但从 C# 6.0 开始,它们提供了额外的灵活性:如果你在一个类中有一个非 lambda 方法,你可以使用相同的表达式语法来定义相同的方法。例如,在下面的演示中,有一个名为 Test 的类。

    class Test
    {
        public int CalculateSum1(int a, int b)
        {
            int sum = a + b;
            return sum;
        }
        // Expression-bodied method is not available in C#5
        public int CalculateSum2(int a, int b) => a + b; // ok
    }

注意非 lambda 方法CalculateSum1。这是一个简单的方法,接受两个整数**,**计算它们的和,并返回结果(也是一个整数)。

从 C# 6.0 开始,您可以编写 lambda 表达式来定义 CalculateSum1 的等效版本。下面是这样的表达。

public int calculatesum 2(int a,int b)=>a+b;

如果您在 C# 6.0 之前的 C# 版本中使用它(比如在 C# 5.0 中),您会得到下面的编译时错误。

CS8026: Feature 'expression-bodied method' is not available in C# 5\. Please use language version 6 or greater.

图 3-5 是一个 Visual Studio IDE 截图,供大家参考。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-5

C# 5 中没有“表达式主体方法”功能

我保留了这些评论来帮助你理解。但是需要注意的是,当您的方法可以用单个表达式表示时(即,在方法实现中只有一行代码),您可以使用这个概念。换句话说,它适用于表达式λ语法,但不能用于语句λ语法。在演示 3 中,如果取消对以下代码段的注释,就会出现编译时错误。

//int CalculateSum3(int a, int b) =>{
//    int sum = a + b;
//    return sum;
//}

演示 3

这个完整的演示展示了表达式体方法的使用。

using System;

namespace ExpressionBodiedMethodDemo
{
    class Test
    {
        public int CalculateSum1(int a, int b)
        {
            int sum = a + b;
            return sum;
        }
        /*
        Expression-bodied method is not available in C#5.
        C#6.0 onwards,you can use same expression syntax to define a non-lambda method within a class
        It is ok for single expression, i.e. for
        expression lambda syntax,but not for statement lambda.
        */

        public int CalculateSum2(int a, int b) => a + b;//ok

        // Following causes compile-time error
        // For expression-bodied methods, you cannot use
        // statement lambda
        //int CalculateSum3(int a, int b) =>{
        //    int sum = a + b;
        //    return sum;
        //}
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Experimenting lambda expression with expression-bodied method.***\n");
            // Using Normal method
            Test test = new Test();
            int result1 = test.CalculateSum1(5, 7);
            Console.WriteLine("\nUsing a normal method, CalculateSum1(5, 7) results: {0}", result1);
            // Using expression syntax

            int result2 = test.CalculateSum2(5, 7);
            Console.WriteLine("\nUsing expression syntax for CalculateSum2(5,7),result is: {0}", result2);
            Console.ReadKey();
        }
    }
}

输出

以下是运行该程序的输出。

***Experimenting lambda expression with expression-bodied method.***

Using a normal method, CalculateSum1(5, 7) results: 12

Using expression syntax for CalculateSum2(5,7),result is: 12

Points to Remember

定义非 lambda 方法的表达式语法不适用于语句 lambdas 。你只能对表情 lambdas 使用它。

演示 4

演示 3 向您展示了表达式体方法的使用,但它也适用于属性、构造函数和终结器。在演示 4 中,您将看到它是如何与构造函数、只读属性和读写属性一起使用的。所以,让我们把重点放在重要的代码段上,并将它们与通常的实现进行比较。

假设您有一个Employee类,其中有雇员 ID、公司名称和雇员姓名。在代码中,我将它们分别表示为empIdcompanyname。当你初始化一个Employee对象时,你提供了empIdCompany是只读属性,Name是读写属性。

下面是公共构造函数的通常实现,它只有一个参数。

public Employee(int id)
{
 empId = id;
}

下面显示了表达式主体构造函数。

public Employee(int id) => empId = id;

下面是只读属性Company的通常实现。

public string Company
{
  get
  {
    return company;
  }
}

下面显示了只读属性的表达式体定义。

public string Company => company;

下面是读写Name属性的通常实现。

public string Name
{
  get
  {
    return name;
  }
  set
  {
    name = value;
  }
}

下面显示了读写属性的表达式体定义。

public string Name
 {
    get => name;
    set => name = value;
 }

我们来看一下完整的演示和输出,如下所示。

using System;

namespace Expression_BodiedPropertiesDemo
{
    class Employee
    {
        private int empId;
        private string company = "XYZ Ltd.";
        private string name = String.Empty;

        //Usual implementation of a constructor.
        //public Employee(int id)
        //{
        //    empId = id;
        //}
        //Following shows an expression-bodied constructor
        public Employee(int id) => empId = id;//ok

        //Usual implementation of a read-only property
        //public string Company
        //{
        //    get
        //    {
        //        return company;
        //    }
        //}
        //Read-only property.C#6.0 onwards.
        public string Company => company;

        //Usual implementation
        //public string Name
        //{
        //    get
        //    {
        //        return name;
        //    }
        //    set
        //    {
        //        name = value;
        //    }
        //}

        //C#7.0 onwards , we can use expression-body definition for the get //and set accessors.
        public string Name
        {
            get => name;
            set => name = value;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Experimenting lambda expressions with expression-bodied properties.***");
            Employee empOb = new Employee(1);
            //Error.Company is read-only
            //empOb.Company = "ABC Co.";
            empOb.Name = "Rohan Roy ";//ok
            Console.WriteLine("{0} works in {1} as an employee.", empOb.Name,empOb.Company);
            Console.ReadKey();
        }
    }
}

输出

以下是运行该程序的输出。

***Experimenting lambda expressions with expression-bodied properties.***
Rohan Roy  works in XYZ Ltd. as an employee.

Points to Remember

在 C# 6.0 中,我们得到了对表达式体方法和只读属性的支持。在 C# 7.0 中,这种支持扩展到了属性、索引器、构造函数和终结器。

Lambda 表达式中的局部变量

你可能已经注意到局部变量在 lambda 表达式中的使用。在这种情况下,变量必须在范围内。演示 5 展示了 lambda 表达式中局部变量的简单用法。

演示 5

该演示将您的注意力吸引到以下几点。

  • 您可以在程序中使用查询语法或方法调用语法。我已经展示了两者的用法。(如果你熟悉 LINQ 编程,你知道查询语法;否则,您可以跳过这段代码,直到您了解它。)考虑下面的代码,尤其是粗体部分:

    IEnumerable<int> numbersAboveMidPoint = intList.Where(x => x > midPoint);
    
    
  • midPoint是一个局部变量。lambda 表达式可以访问此变量,因为它在此位置的范围内。

  • 本例中使用了List<int>IEnumerable<int>。它们是泛型编程中最简单的构造。如果你是泛型的新手,你可以暂时跳过这个例子,在第四章讲述泛型编程后再回来。

让我们来看一下演示。

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

namespace TestingLocalVariableScopeUsingLambdaExpression
{
   class Program
   {
        static void Main(string[] args)
        {
            Console.WriteLine("***Testing local variable scope with a lambda expression.***\n");
            #region Using query syntax
            /* Inside lambda Expression,you can access the variable that are in scope (at that location).*/
            int midPoint = 5;
             List<int> intList = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
            var myQueryAboveMidPoint = from i in intList
                                       where i > midPoint
                                       select i;
            Console.WriteLine("Numbers above mid point(5) in intList are as follows:");
            foreach (int number in myQueryAboveMidPoint)
            {
                Console.WriteLine(number);
            }
            #endregion
            #region Using method call syntax
            // Alternative way( using method call syntax)
            Console.WriteLine("Using a lambda expression, numbers above mid point(5) in intList are as follows:");
            IEnumerable<int> numbersAboveMidPoint = intList.Where(x => x > midPoint);
            foreach (int number in numbersAboveMidPoint)
            {
                Console.WriteLine(number);
            }
            #endregion
            Console.ReadKey();
        }
    }
}

输出

以下是运行该程序的输出。

***Testing local variable scope with a lambda expression.***
Numbers above mid point(5) in intList are as follows:
6
7
8
9
10
Using a lambda expression, numbers above mid point(5) in intList are as follows:
6
7
8
9
10

在 Lambda 表达式中使用元组

从 C# 7.0 开始,就有了对元组的内置支持。在许多应用中,元组有内置的委托(例如,FuncAction等)。)和 lambda 表达式。你将在第四章中了解内置代理。现在,让我们在用户定义的委托的上下文中使用元组。

在下面的例子中,我将一个元组传递给一个方法。为了简单起见,我们假设元组只有两个组件。你想把这个元组传递给一个方法参数,反过来,你想得到一个元组,在这个元组中你得到每个组件的双精度值。下面的方法代表了这样一个示例。

static Tuple<int, double> MakeDoubleMethod(Tuple<int, double> input)
{
    return Tuple.Create(input.Item1 * 2, input.Item2 * 2);
}

可以看到在 tuple 内部,第一个组件是一个int,第二个是一个double。我只是将输入参数乘以 2,以获得每个组件的 double 值,并返回包含另一个元组的结果。

在 Main 方法中,我如下调用了这个方法。

var resultantTuple = MakeDoubleMethod(inputTuple);

因为我是从静态上下文中调用方法,所以我将MakeDoubleMethod设为静态。

现在你知道如何在一个方法中使用元组了。让我们使用 lambda 表达式来实现这个概念。

首先,声明一个委托,如下所示。

delegate Tuple<int, double> MakeDoubleDelegate(Tuple<int, double> input);

现在您有了委托,所以您可以使用 lambda 表达式,如下所示。

MakeDoubleDelegate delegateObject =
                (Tuple<int, double> input) => Tuple.Create(input.Item1 * 2, input.Item2 * 2);

如果不使用命名组件,默认情况下,元组的字段被命名为Item1Item2Item3等等。要获得预期的结果,可以使用下面几行代码。

var resultantTupleUsingLambda= delegateObject(inputTuple);
Console.WriteLine("Using lambda expression, the content of resultant tuple is as follows:");
Console.WriteLine("First Element: " + resultantTupleUsingLambda.Item1);
Console.WriteLine("Second Element: " + resultantTupleUsingLambda.Item2);

像本书中的许多其他例子一样,我保留了两种方法来获得预期的结果。它有助于比较 lambda 表达式的使用和类似上下文中的普通方法。接下来是完整的演示。

演示 6

using System;

namespace UsingTuplesInLambdaExp
{
    delegate Tuple<int, double> MakeDoubleDelegate(Tuple<int, double> input);
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Using Tuples in Lambda Expression.***");
            var inputTuple = Tuple.Create(1, 2.3);
            Console.WriteLine("Content of input tuple is as follows:");
            Console.WriteLine("First Element: " + inputTuple.Item1);
            Console.WriteLine("Second Element: " + inputTuple.Item2);
            var resultantTuple = MakeDoubleMethod(inputTuple);

            Console.WriteLine("\nPassing tuple as an input argument in a normal method which again returns a tuple.");
            Console.WriteLine("Content of resultant tuple is as follows:");
            Console.WriteLine("First Element: " + resultantTuple.Item1);
            Console.WriteLine("Second Element: " + resultantTuple.Item2);

            Console.WriteLine("\nUsing delegate and lambda expression with tuple now.");
            MakeDoubleDelegate delegateObject =
                (Tuple<int, double> input) => Tuple.Create(input.Item1 * 2, input.Item2 * 2);
            var resultantTupleUsingLambda= delegateObject(inputTuple);
            Console.WriteLine("Using lambda expression, the content of resultant tuple is as follows:");
            Console.WriteLine("First Element: " + resultantTupleUsingLambda.Item1);
            Console.WriteLine("Second Element: " + resultantTupleUsingLambda.Item2);
            Console.ReadKey();
        }
        static Tuple<int, double> MakeDoubleMethod(Tuple<int, double> input)
        {
            return Tuple.Create(input.Item1 * 2, input.Item2 * 2);
        }
    }

}

输出

以下是运行该程序的输出。

***Using Tuples in Lambda Expression.***
Content of input tuple is as follows:
First Element: 1
Second Element: 2.3

Passing tuple as an input argument in a normal method which again returns a tuple.
Content of resultant tuple is as follows:
First Element: 2
Second Element: 4.6

Using delegate and lambda expression with tuple now.
Using lambda expression, the content of resultant tuple is as follows:
First Element: 2
Second Element: 4.6

带有 Lambda 表达式的事件订阅

可以对事件使用 lambda 表达式。

演示 7

为了演示一个案例,我们来看一下第二章的第一个程序,并对其进行修改。由于我们关注的是 lambda 表达式,这一次,您不需要创建一个Receiver类,它有一个名为GetNotificationFromSender ,的方法,用于在myIntSender类对象中发生变化时处理事件通知。在那个例子中,Sender类也有一个GetNotificationItself方法来处理它自己的事件。它向您展示了一个Sender类也可以处理自己的事件。

这是完整的演示。

using System;

namespace UsingEventsAndLambdaExp
{
    class Sender
    {
        private int myInt;
        public int MyInt
        {
            get
            {
                return myInt;
            }
            set
            {
                myInt = value;
                //Whenever we set a new value, the event will fire.
                OnMyIntChanged();
            }
        }
        // EventHandler is a predefined delegate which is used to handle //simple events.
        // It has the following signature:
        //delegate void System.EventHandler(object sender,System.EventArgs e)
        //where the sender tells who is sending the event and
        //EventArgs is used to store information about the event.
        public event EventHandler MyIntChanged;
        public void OnMyIntChanged()
        {
            if (MyIntChanged != null)
            {
                MyIntChanged(this, EventArgs.Empty);
            }
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Demonstration-.Exploring events with lambda expression.***");
            Sender sender = new Sender();

            //Using lambda expression as an event handler
            //Bad practise
            //sender.MyIntChanged += (Object sender, System.EventArgs e) =>
            // Console.WriteLine("Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value . ");
            //Better practise
            EventHandler myEvent =
               (object sender, EventArgs e) =>
              Console.WriteLine("Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value . ");
            sender.MyIntChanged += myEvent;

            sender.MyInt = 1;
            sender.MyInt = 2;
            //Unregistering now
            //sender.MyIntChanged -= receiver.GetNotificationFromSender;
            //No notification sent for the receiver now.
            //but there is no guarantee if you follow the bad practise
            //sender.MyIntChanged -= (Object sender, System.EventArgs e) =>
            // Console.WriteLine("Unregistered event notification.");

            //But now it can remove the event properly.
            sender.MyIntChanged -= myEvent;
            sender.MyInt = 3;

            Console.ReadKey();

        }
    }
}

输出

以下是运行该程序的输出。

***Demonstration-.Exploring events with lambda expression.***
Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value .
Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value .

问答环节

3.1 你为什么要编写额外的代码?我发现你可以简单地写下下面的内容来订阅这个活动。

sender.MyIntChanged += (Object sender, System.EventArgs e) =>
  Console.WriteLine("Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value . ");

你可以用它来替换下面几行。

EventHandler myEvent = (object sender, EventArgs e) =>
Console.WriteLine("Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value . ");
sender.MyIntChanged += myEvent;

这是正确的吗?

接得好,但这是必要的。假设您使用以下代码行来订阅事件。

sender.MyIntChanged += (Object sender, System.EventArgs e) =>
Console.WriteLine("Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value . ");

然后用下面一行取消订阅。

sender.MyIntChanged -= (Object sender, System.EventArgs e) =>
   Console.WriteLine("Unregistered event notification.");

不能保证编译器会取消订阅正确的事件。例如,在这种情况下,当我执行程序时,我注意到输出中的第三种情况是不需要的(因为在我将myInt的值设置为 3 之前,我想取消订阅事件通知)。以下是输出。

***Demonstration-.Exploring events with lambda expression.***
Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value .
Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value .
Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value .

所以,专家建议,在这种情况下,应该将匿名方法/lambda 表达式存储在一个委托变量 中,然后将这个委托添加到事件中。这样一来,你就可以随时关注它 并且如果你愿意,你可以适当地取消订阅*。当您希望以后取消订阅某个事件时,通常建议不要使用匿名函数来订阅该事件。这是因为,为了避免现实应用中的内存泄漏,一旦您订阅了一个事件,您就应该在预期的工作完成后取消订阅。*

*3.2 什么是表达式?

根据微软的说法,表达式可以是运算符和操作数的组合。它可以计算为单个值、方法、对象或命名空间。表达式可以包括方法调用、带有操作数的运算符、文本值(文本是没有名称的常量值),或者只是变量、类型成员、方法参数、命名空间或类型的名称。

下面是一个简单的表达式语句示例。

int i=1;

这里,i是一个简单的名字,1 是字面值。文字和简单名称是两种最简单的表达式。

3.3 什么是图灵机?

图灵机是一种抽象机器,它可以通过遵循规则来操纵磁带的符号。它是许多编程语言的数学基础。

3.4 语句 lambda 和表达式 lambda 有什么区别?

表达式 lambda 只有一个表达式,但语句 lambda 在 lambda 运算符的右侧有一个语句块。使用语句 lambda,可以在花括号中包含任意数量的语句。在撰写本文时,您不能将语句 lambda 用于表达式体方法,但是您可以在这些上下文中使用表达式 lambda。

在表达式树中,只能使用表达式 lambdas 但是你不能在这些上下文中使用 lambda 语句。我排除了对表达式树的讨论,因为它是与 LINQ 相关的特性,超出了本书的范围。

3.5 如果不提供参数,编译器仍然可以确定类型。但是当您提供它们时,它们必须匹配委托类型。这是正确的吗?

是的。但有时编译器无法推断出来。在这种情况下,您需要提供参数。在这种情况下,您需要记住输入参数必须是隐式的或显式的。

3.6 你说输入参数必须是隐式的或者显式的。这是什么意思?

让我们假设您有以下委托。

delegate string MyDelegate(int a, int b);

如果你写了下面这段代码,你会得到一个编译时错误,如图 3-6 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-6

由于 lambda 参数用法不一致而导致的编译时错误

MyDelegate resultOfLambdaExp =(int x,  y)=> (x > y) ? "Yes." : "No.";

补救方法如下。

MyDelegate resultOfLambdaExp =(int x, int y)=> (x > y) ? "Yes." : "No.";

或者,您可以删除这两个 int,如下所示。

MyDelegate resultOfLambdaExp =( x, y)=> (x > y) ? "Yes." : "No.";

3.7 lambda 表达式有什么限制?

Lambda 表达式是匿名方法的超集。适用于匿名方法的所有限制也适用于 lambda 表达式(例如,在匿名方法的上下文中,不能使用定义方法的 ref 或 out 参数)。作为参考,请记住以下几点。

  • 在“is”或“as”运算符的左侧不允许出现 Lambdas。在这种情况下,您可能还记得 C# 6.0 规范中的语句,它说,“匿名函数本身没有值或类型,但可以转换为兼容的委托或表达式树类型。”

  • 不能使用breakgotocontinue跳出 lambda 表达式范围。

  • 不能在 lambda 表达式中使用unsafe代码。例如,假设您有以下委托:

delegate void DelegateWithNoParameter();

如果您编写以下代码段,您会在所有操作指针的地方(我用注释标记了这些地方)得到一个编译时错误。

      DelegateWithNoParameter delOb = () =>
            {
                int a = 10;
                //CS 0214:Pointers and fixed sized buffers may //only be used only in an unsafe context
                int* p = &a;//Error
                //Console.WriteLine("a={0}", a);
                //Printing using string interpolation
                Console.WriteLine($"a={a}");
                Console.WriteLine($"*p={*p}");//Error CS0214
            };

3.8 你说过在匿名方法的上下文中,不能使用定义方法的 ref 或 out 参数。你能详细说明一下吗?

让我们考虑一下演示 1 中的 Sum 方法,并将其修改如下。

public static int Sum(ref int a, ref int b)
        {
            //return a + b;
            //Using Anonymous method(C# 2.0 onwards)
            Mydel del2 = delegate (int x, int y)
            {
                //Following segment will NOT work
                x = a;//CS1628
                y = b;//CS1628
                return x + y;

                //Following segment will work
                //return x + y;
            };
            return del2(a, b);
        }
Where the Mydel delegate is unchanged and as follows:
public delegate int Mydel(int x, int y);

对于这段代码,您将得到一个编译时错误(标记为 CS1628)。CS1628 指出,在匿名方法或 lambda 表达式中,不能在参数中使用 ref、out、。图 3-7 是 Visual Studio 2019 错误截图,供大家参考。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-7

编译时错误。不能在匿名方法或 lambda 表达式中使用 ref、out 或 in 参数

您可以参考前面代码段中使用注释行显示的潜在解决方案。

最后的话

目前关于兰姆达斯就这些了。在你离开这一章之前,我想提醒你,尽管很酷的特性很容易使用,但是代码的可读性和可理解性应该是你最优先考虑的。

接下来,我们转到本书的第二部分(从第四章开始),在这里你可以看到目前为止你所学概念的用法。虽然第二部分是这本书的核心,但第一部分的内容(第 1 、 2 和 3 章)是它们的组成部分。

摘要

本章讨论了以下关键问题。

  • 为什么匿名方法和 lambda 表达式有用?

  • 如何将匿名方法转换成 lambda 表达式?

  • 如何使用 lambda 表达式来接受不同数量的参数?

  • 如何在 lambda 表达式中使用局部变量?

  • 什么是表达式λ?

  • 什么是语句 lambda?

  • 如何使用表达式语法来定义非 lambda 方法?以及它的使用限制是什么?

  • 适用于 lambda 表达式的关键限制是什么?*

四、泛型编程

在这一章中,你将学习泛型编程,并了解 C# 最酷的特性之一泛型。它是高级编程不可或缺的一部分。泛型编程仅仅意味着泛型的有效使用。它最早出现在 C# 2.0 中。随着时间的推移,这个强大的特性增加了额外的灵活性,现在,您会发现现实生活中很少有应用的核心不使用泛型。

泛型背后的动机

当您在应用中使用泛型类型时,您不必为实例提交特定的类型。例如,当您实例化一个泛型类时,您可以说您希望您的对象处理 int 类型,但在另一个时候,您可以说您希望您的对象处理 double 类型、string 类型、object 类型等等。简而言之,这种编程允许您创建一个类型安全的类,而不必提交任何特定的类型。

这不是什么新概念,也绝对不局限于 C#。在其他语言中也可以看到类似的编程,例如 Java 和 C++(使用模板)。以下是使用通用应用的一些优点。

  • 你的程序是可重用的。

  • 更好的类型安全丰富了你的程序。

  • 您的程序可以避免典型的运行时错误,这些错误可能是由于不正确的类型转换引起的。

为了解决这些问题,我将从一个简单的非泛型程序开始,并分析其潜在的缺点。之后,我会向您展示一个相应的泛型程序,并进行对比分析,以发现泛型编程的优势。我们开始吧。

演示 1

演示 1 有一个名为NonGenericEx的类。这个类有两个实例方法:DisplayMyIntegerDisplayMyString

public int DisplayMyInteger(int myInt)
{
 return myInt;
}
public string DisplayMyString(string myStr)
{
 return myStr;
}

你有没有注意到这两个方法基本上在做同样的操作,但是一个方法在处理一个int而另一个方法在处理一个string?这种方法不仅难看,而且还有另一个潜在的缺点,您将在分析部分看到这一点。但是在我们分析它之前,让我们执行程序。

using System;

namespace NonGenericProgramDemo1
{
    class NonGenericEx
    {
        public int DisplayMyInteger(int myInt)
        {
            return myInt;
        }
        public string DisplayMyString(string myStr)
        {
            return myStr;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***A non-generic program demonstration.***");
            NonGenericEx nonGenericOb = new NonGenericEx();
            Console.WriteLine("DisplayMyInteger returns :{0}", nonGenericOb.DisplayMyInteger(123));
            Console.WriteLine("DisplayMyString returns :{0}", nonGenericOb.DisplayMyString("DisplayMyString method inside NonGenericEx is called."));
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***A non-generic program demonstration.***
DisplayMyInteger returns :123
DisplayMyString returns :DisplayMyString method inside NonGenericEx is called.

分析

让我们假设现在您需要处理另一个数据类型—一个double。使用当前代码,在Main中添加下面一行。

Console.WriteLine("ShowDouble returns :{0}", nonGenericOb.DisplayMyDouble(25.5));//error

您会得到下面的编译时错误。

Error  CS1061  'NonGenericEx' does not contain a definition for 'DisplayMyDouble' and no accessible extension method 'DisplayMyDouble' accepting a first argument of type 'NonGenericEx' could be found (are you missing a using directive or an assembly reference?)

这是因为您还没有一个DisplayMyDouble方法。同时,您不能使用任何现有的方法来处理double数据类型。一个显而易见的方法是引入如下所示的方法。

public double DisplayMyDouble(double myDouble)
{
 return myDouble;
}

但是你能忍受多久呢?如果您的代码大小对于所有其他数据类型都以同样的方式增长,那么您的代码将不能被不同的数据类型重用。与此同时,随着代码的增长,它看起来会很难看,整体的维护会变得非常繁忙。幸运的是,当你喜欢泛型编程胜过非泛型编程时,你有一个简单的解决方案。

首先,以下是你应该记住的要点。

  • 泛型类和方法提高了可重用性、类型安全性和效率。它们的非通用对应物不具备这些品质。您经常会看到泛型与集合以及处理它们的方法一起使用。

  • 那个。NET Framework 类库包含一个System.Collections.Generic名称空间,其中有几个基于泛型的集合类。此命名空间是在 2.0 版中添加的。这就是为什么微软建议任何以。NET Framework 2.0(或更高版本)应该使用泛型集合类,而不是它们的非泛型对应物,比如ArrayList

  • 尖括号<>用于通用程序。泛型放在尖括号中;比如在你的类定义中的。当您只处理单个泛型类型时,t 是表示泛型类型的最常见的单个字母。

  • 在泛型程序中,可以用占位符定义一个类的方法、字段、参数等类型。稍后,这些占位符将被替换为您想要使用的特定类型。

  • Here is the simple generic class used in demonstration 2:

    class GenericClassDemo<T>
        {
            public T Display(T value)
            {
                return value;
            }
        }
    
    

    T称为泛型类型参数。

    The following is an example of instantiation from a generic class:

    GenericClassDemo<int> myGenericClassIntOb = new GenericClassDemo<int>();
    
    

    注意,在这种情况下,类型参数被替换为int

  • 您可能会注意到在一个特定的声明中有多个泛型类型参数。例如,下面的类有多个泛型类型:

    public class MyDictionary<K,V>{//Some code}
    
    
  • 泛型方法可能使用其类型参数作为其返回类型。它还可以将类型参数用作形参的类型。在GenericClassDemo<T>类中,Display方法使用 T 作为返回类型。该方法还使用 T 作为其形参的类型。

  • 您可以对泛型类型施加约束。这将在本章后面探讨。

现在进行演示 2。

演示 2

演示 2 是一个简单的通用程序。在实例化泛型类之前,需要指定用类型参数替换的实际类型。在这个演示中,下面几行代码在Main中。

GenericClassDemo<int> myGenericClassIntOb = new GenericClassDemo<int>();
GenericClassDemo<string> myGenericClassStringOb = new GenericClassDemo<string>();
GenericClassDemo<double> myGenericClassDoubleOb = new GenericClassDemo<double>();

这三行代码告诉你,第一行用一个int替代类型参数;第二行用一个string;代替类型参数,第三行用一个double代替类型参数。

当您进行这种编码时,该类型会在它出现的任何地方替换类型参数。因此,您会得到一个基于您选择的类型构造的类型安全类。当您选择一个int类型并使用下面的代码行时,

GenericClassDemo<int> myGenericClassIntOb = new GenericClassDemo<int>();

您可以使用下面的代码行从Display方法中获取一个int

Console.WriteLine("Display method returns :{0}", myGenericClassIntOb.Display(1));

这是完整的演示。

using System;

namespace GenericProgramDemo1
{
    class GenericClassDemo<T>
    {
        public T Display(T value)
        {
            return value;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Introduction to Generic Programming.***");
            GenericClassDemo<int> myGenericClassIntOb = new GenericClassDemo<int>();
            Console.WriteLine("Display method returns :{0}", myGenericClassIntOb.Display(1));
            GenericClassDemo<string> myGenericClassStringOb = new GenericClassDemo<string>();
            Console.WriteLine("Display method returns :{0}", myGenericClassStringOb.Display("A generic method is called."));
            GenericClassDemo<double> myGenericClassDoubleOb = new GenericClassDemo<double>();
            Console.WriteLine("Display method returns :{0}", myGenericClassDoubleOb.Display(12.345));
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Introduction to Generic Programming.***
Display method returns :1
Display method returns :A generic method is called.
Display method returns :12.345

分析

让我们对演示 1(非泛型程序)和演示 2(泛型程序)进行比较分析。这两个程序执行相同的操作,但是它们之间有一些关键的区别,如下所示。

  • 在演示 1 中,您需要指定诸如DisplayIntegerDisplayStringDisplayDouble等方法来处理数据类型。但是在演示 2 中,只有一个通用的Display方法足以处理不同的数据类型,并且您可以用更少的代码行完成这项任务。

  • 在演示 1 中,当Main中没有DisplayDouble方法时,当我们想要处理double数据类型时,我们遇到了一个编译时错误。但是在演示 2 中,不需要定义任何额外的方法来处理 double 数据类型(或任何其他数据类型)。所以,你可以看到这个通用版本比非通用版本更灵活。

现在考虑演示 3。

演示 3

这个演示展示了一个使用ArrayList类的非泛型程序。一个ArrayList的大小可以动态增长。它有一个叫做Add的方法,可以帮助你在ArrayList的末尾添加一个对象。在接下来的演示中,我使用了以下代码行。

myList.Add(1);
myList.Add(2);
// No compile time error
myList.Add("InvalidElement");

因为该方法需要对象作为参数,所以这些行被成功编译。但是如果您使用下面的代码段获取数据,您将会面临这个问题。

foreach (int myInt in myList)
{
 Console.WriteLine((int)myInt); //downcasting
}

第三个元素不是 int(它是一个字符串),因此您会遇到一个运行时错误。运行时错误比编译时错误更糟糕,因为在这个阶段,您几乎不能做任何有成效的事情。

这是完整的演示。

using System;
using System.Collections;

namespace NonGenericProgramDemo2
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Use Generics to avoid runtime error***");
            ArrayList myList = new ArrayList();
            myList.Add(1);
            myList.Add(2);
            //No compile time error
            myList.Add("InvalidElement");
            foreach (int myInt in myList)
            {
            /*Will encounter run-time exception for the final element  which is not an int */
                Console.WriteLine((int)myInt); //downcasting
            }
            Console.ReadKey();
           }
        }
}

输出

该程序不会引发任何编译时错误,但是在运行时,您会看到如图 4-1 所示的异常。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-1

出现运行时错误 InvalidCastException

现在你明白你遇到这个运行时错误是因为第三个元素(即ArrayList中的myList [2])应该是一个 int,但是我存储了一个 string。在编译时,我没有遇到任何问题,因为它是作为对象存储的。

分析

由于装箱和向下转换,前面的演示也存在性能开销。

快速浏览列表类

在你进一步深入之前,让我们快速看一下内置的List类。这个类很常见,应用也很广泛。它是为泛型设计的,所以当你实例化一个List类时,你可以在你的列表中提到你想要的类型。例如,在下面

List<int> myList = new List<int>(); contains a list of ints.
List<double> myList = new List<double>(); contains a list of doubles.
List<string> myList = new List<string>(); contains a list of strings

List类有许多内置方法。我建议你浏览一下。这些现成的构造使您的编程生活更加容易。现在,让我们使用Add方法。使用这种方法,您可以将项目添加到列表的末尾。

这是来自 Visual IDE 的方法说明。

//
// Summary:
//   Adds an object to the end of the System.Collections.Generic.List`1.
//
// Parameters:
//   item:
//     The object to be added to the end of the //     System.Collections.Generic.List`1\. The value can be null //     for reference types.
public void Add(T item);

下面的代码段创建了一个int列表,然后向其中添加了两个条目。

List<int> myList = new List<int>();
myList.Add(10);
myList.Add(20);

现在来看重要的部分。如果您错误地将一个string添加到这个列表中,就会得到一个编译时错误。

这是错误的代码段。

//Compile time error: Cannot convert from 'string' to 'int'
//myList.Add("InvalidElement");//error

演示 4

为了与演示 3 进行比较,在下面的例子中,让我们使用List<int>而不是ArrayList,然后回顾我们到目前为止讨论过的概念。

这是完整的程序。

using System;
using System.Collections.Generic;

namespace GenericProgramDemo2
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Using Generics to avoid run-time error.***");
            List<int> myList = new List<int>();
            myList.Add(10);
            myList.Add(20);
            //Cannot convert from 'string' to 'int'
            myList.Add("InvalidElement");//Compile-time error
            foreach (int myInt in myList)
            {
                Console.WriteLine((int)myInt);//downcasting
            }
            Console.ReadKey();
        }
    }
}

输出

在这个程序中,您会得到以下编译时错误

CS1503    Argument 1: cannot convert from 'string' to 'int'

下面的代码行。

myList.Add("InvalidElement");

您不能在myList中添加一个string,因为它只用于保存整数(注意我使用的是List<int>)。因为错误是在编译时捕获的,所以您不需要等到运行时才捕获这个缺陷。

一旦注释掉错误的行,就可以编译这个程序并生成以下输出。

***Using Generics to avoid run-time error.***
1
2

分析

当您比较演示 3 和演示 4 时,您会发现

  • 为了避免运行时错误,您应该更喜欢通用版本,而不是它的对应物—非通用版本。

  • 泛型编程有助于避免装箱/拆箱带来的损失。

  • 为了存储字符串,您可以使用类似于List<string> myList2 = new List<string>();的东西来创建一个只保存字符串类型的列表。类似地,List 可以用于其他数据类型。这说明 List<T>版本比非通用版本ArrayList更灵活、更实用。

通用委托

在第一章中,你学习了用户定义的代理及其重要性。现在,让我们讨论泛型委托。在这一节中,我将介绍三个重要的内置泛型委托——称为FuncActionPredicate,它们在泛型编程中非常常见。我们开始吧。

功能委托

Func委托有 17 个重载版本。它们可以接受 0 到 16 个输入参数,但总是有一个返回类型。举个例子,

Func<out TResult>
Func<in T, out TResult>
Func<in T1, in T2,out TResult>
Func<in T1, in T2, in T3, out TResult>
......

Func<in T1, in T2, in T3,in T4, in T5, in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16, out TResult>

为了理解用法,让我们考虑下面的方法。

private static string DisplayEmployeeDetails(string name, int empId, double salary)
{
   return string.Format("Employee Name:{0},id:{1}, salary:{2}$", name, empId,salary);
}

若要使用自定义委托调用此方法,可以按照下列步骤操作。

  1. 定义一个代表(比如说,Mydel);大概是这样的:

    public delegate string Mydel(string n, int r, double d);
    
    
  2. 创建一个委托对象并使用代码指向该方法;类似于以下内容:

    Mydel myDelOb = new Mydel(DisplayEmployeeDetails);
    Or in short,
    Mydel myDelOb = DisplayEmployeeDetails;
    
    
  3. 像这样调用方法:

    myDelOb.Invoke("Amit", 1, 1025.75);
    
    

    Or, simply with this:

     myDelOb("Amit", 1, 1025.75);
    
    

如果您使用内置的Func委托,您可以使您的代码更简单、更短。在这种情况下,您可以如下使用它。

Func<string, int, double, string> empOb = new Func<string, int, double,string>(DisplayEmployeeDetails);
Console.WriteLine(empOb("Amit", 1,1025.75));

Func委托完美地考虑了所有三个输入参数(分别是一个string、一个int和一个double)并返回一个string。您可能会感到困惑,想知道哪个参数表示返回类型。如果在 Visual Studio 中将光标移到它上面,可以看到最后一个参数(TResult)被认为是函数的返回类型,其他的被认为是输入类型(见图 4-2 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-2

Func 委托的详细信息

Note

输入和输出参数的魔力将很快向您展示。

问答环节

4.1 在前面的代码段中, DisplayEmployeeDetails 有三个参数,其返回类型为 string 。通常,我有不同的方法可以接受不同数量的输入参数。我如何在那些上下文中使用 Func

Func 委托可以考虑 0 到 16 个输入参数。可以使用适合自己需求的重载版本。例如,如果你有一个方法,它接受一个 string 和一个 int 作为输入参数,并且它的返回类型是一个 string,那么这个方法就像下面这样。

private static string DisplayEmployeeDetailsShortForm(string name, int empId)
{
   return string.Format("Employee Name:{0},id:{1}", name, empId);
}

您可以使用以下重载版本的 Func。

Func<string, int, string> empOb2 = new Func<string, int, string> (DisplayEmployeeDetailsShortForm);
Console.WriteLine(empOb2("Amit", 1));

动作代表

Visual studio 描述了有关操作委托的以下内容:

封装没有参数且不返回值的方法。

    public delegate void Action();

但是通常你会注意到这个委托的通用版本,它可以接受 1 到 16 个输入参数,但是没有返回类型。重载版本如下。

Action<in T>
Action<in T1,in T2>
Action<in T1,in T2, in T3>
....
Action<in T1, in T2, in T3,in T4, in T5, in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16>

假设您有一个名为CalculateSumOfThreeInts的方法,它将三个 int 作为输入参数,其返回类型为void,如下所示。

private static void CalculateSumOfThreeInts(int i1, int i2, int i3)
{
    int sum = i1 + i2 + i3;
    Console.WriteLine("Sum of {0},{1} and {2} is: {3}", i1, i2, i3, sum);
}

您可以使用动作委托来获取三个整数的和,如下所示。

Action<int, int, int> sum = new Action<int, int, int>(CalculateSumOfThreeInts);
sum(10,3,7);

谓词委托

谓词委托计算一些东西。例如,假设您有一个定义了一些标准的方法,您需要检查一个对象是否满足标准。让我们考虑下面的方法。

private static bool GreaterThan100(int myInt)
{
    return myInt > 100 ? true : false;
}

你可以看到这个方法计算一个 int 是否大于 100。因此,您可以使用谓词委托来执行相同的测试,如下所示。

Predicate<int> isGreater = new Predicate<int>(IsGreaterThan100);
Console.WriteLine("101 is greater than 100? {0}", isGreater(101));
Console.WriteLine("99 is greater than 100? {0}", isGreater(99));

演示 5

这是一个完整的程序,演示了到目前为止讨论的所有概念。

using System;

namespace GenericDelegatesDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Using Generic Delegates.***");
            // Func
            Console.WriteLine("Using Func delegate now.");
            Func<string, int, double,string> empOb = new Func<string, int, double,string>(DisplayEmployeeDetails);
            Console.WriteLine(empOb("Amit", 1,1025.75));
            Console.WriteLine(empOb("Sumit", 2,3024.55));

            // Action
            Console.WriteLine("Using Action delegate now.");
            Action<int, int, int> sum = new Action<int, int, int>(CalculateSumOfThreeInts);
            sum(10, 3, 7);
            sum(5, 10, 15);
            /*
            Error:Keyword 'void' cannot be used in this context
            //Func<int, int, int, void> sum2 = new Func<int, int, int, void>(CalculateSumOfThreeInts);
            */

            // Predicate
            Console.WriteLine("Using Predicate delegate now.");
            Predicate<int> isGreater = new Predicate<int>(IsGreaterThan100);
            Console.WriteLine("101 is greater than 100? {0}", isGreater(101));
            Console.WriteLine("99 is greater than 100? {0}", isGreater(99));

            Console.ReadKey();
        }
        private static string DisplayEmployeeDetails(string name, int empId, double salary)
        {
            return string.Format("Employee Name:{0},id:{1}, salary:{2}$", name, empId,salary);
        }
        private static void CalculateSumOfThreeInts(int i1, int i2, int i3)
        {
            int sum = i1 + i2 + i3;
            Console.WriteLine("Sum of {0},{1} and {2} is: {3}", i1, i2, i3, sum);
        }
        private static bool IsGreaterThan100(int input)
        {
            return input > 100 ? true : false;
        }
    }
}

输出
***Using Generic Delegates.***
Using Func delegate now.
Employee Name:Amit,id:1, salary:1025.75$
Employee Name:Sumit,id:2, salary:3024.55$
Using Action delegate now.
Sum of 10,3 and 7 is: 20
Sum of 5,10 and 15 is: 30
 Using Predicate delegate now.
101 is greater than 100? True
99 is greater than 100? False

问答环节

我见过内置泛型委托的使用。如何使用我自己的泛型委托?

我使用了内置的泛型委托,因为它们让您的生活更轻松。没有人限制你使用自己的泛型委托。不过,我建议您在使用自己的委托之前,先遵循这些泛型委托的构造。例如,在前面的演示中,我使用了如下的动作委托。

Action<int, int, int> sum = new Action<int, int, int>(CalculateSumOfThreeInts);
sum(10, 3, 7);

现在,不使用内置委托,您可以定义自己的泛型委托(比如 CustomAction ),如下所示。

// Custom delegate
public delegate void CustomAction<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);

然后你可以像这样使用它。

CustomAction<int, int, int> sum2 = new CustomAction<int, int, int>(CalculateSumOfThreeInts);
sum2(10, 3, 7);

我发现当你创建代理实例时,你没有使用简写形式。有什么原因吗?

好发现。你总是可以使用简写形式。例如,不使用

Action<int, int, int> sum = new Action<int, int, int>(CalculateSumOfThreeInts);

我可以简单地使用

Action<int, int, int> sum = CalculateSumOfThreeInts;

但是由于您刚刚开始学习委托,这些长形式通常可以帮助您更好地理解代码。

4.4 我可以用 Func 委托指向一个返回 void 的方法吗?

当您的方法具有 void 返回类型时,建议您使用操作委托。如果您在前面的演示中错误地使用了下面的代码行,您会得到一个编译时错误,因为目标方法的返回类型是 void。

//Error:Keyword 'void' cannot be used in this context
Func<int, int, int, void> sum2 = new Func<int, int, int, void>(CalculateSumOfThreeInts);//error

4.5 我可以拥有泛型方法吗?

在演示 2 中,您看到了一个泛型方法,如下所示。

public T Display(T value)
{
   return value;
}

它表明,当您有一组除了类型之外完全相同的方法时,您可以选择泛型方法。

例如,在演示 2 中,您已经看到我在调用时使用了相同的命名方法:Display(1)Display("A generic method is called.")Display(12.345)

泛型中的默认关键字

说明你已经看到了 default 关键字在switch语句中的使用,其中 default 指的是一个默认情况。在泛型编程中,它有特殊的含义。您可以使用default用默认值初始化泛型类型。在这种情况下,您可能会注意到以下几点。

  • 参考类型的默认值是null

  • 值类型(struct 和 bool 类型除外)的默认值为 0

  • 对于 bool 类型,默认值是false

  • 对于结构(是值类型)类型,默认值是该结构的对象,其中所有字段都设置有它们的默认值(即,结构的默认值是通过将所有值类型字段设置为它们的默认值并将所有引用类型字段设置为 null 而产生的值。)

演示 6

考虑以下输出示例。

using System;

namespace UsingdefaultKeywordinGenerics
{
    class MyClass
    {
        // Some other stuff as per need
    }
    struct MyStruct
    {
        // Some other stuff as per need
    }
    class Program
    {
        static void PrintDefault<T>()
        {
            T defaultValue = default(T);
            string printMe = String.Empty;
            printMe = (defaultValue == null) ? "null" : defaultValue.ToString();
            Console.WriteLine("Default value of {0} is {1}", typeof(T), printMe);
            // C# 6.0 onwards,you can use interpolated string
            //Console.WriteLine($"Default value of {typeof(T)} is {printMe}.");
        }
        static void Main(string[] args)
        {
            Console.WriteLine("***Using default keyword in Generic Programming.***");
            PrintDefault<int>();//0
            PrintDefault<double>();//0
            PrintDefault<bool>();//False
            PrintDefault<string>();//null
            PrintDefault<int?>();//null
            PrintDefault<System.Numerics.Complex>(); //(0,0)
            PrintDefault<System.Collections.Generic.List<int>>(); // null
            PrintDefault<System.Collections.Generic.List<string>>(); // null
            PrintDefault<MyClass>(); //null
            PrintDefault<MyStruct>();
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Using default keyword in Generic Programming.***
Default value of System.Int32 is 0
Default value of System.Double is 0
Default value of System.Boolean is False
Default value of System.String is null
Default value of System.Nullable`1[System.Int32] is null
Default value of System.Numerics.Complex is (0, 0)
Default value of System.Collections.Generic.List`1[System.Int32] is null
Default value of System.Collections.Generic.List`1[System.String] is null
Default value of UsingdefaultKeywordinGenerics.MyClass is null
Default value of UsingdefaultKeywordinGenerics.MyStruct is UsingdefaultKeywordinGenerics.MyStruct

Note

输出的最后一行是打印structure>;<namespace>.<Name,基本上你不能为一个结构设置默认值。更具体地说,结构的默认值是该结构的默认构造函数返回的值。如前所述,结构的默认值是通过将所有值类型字段设置为默认值并将所有引用类型字段设置为 null 而产生的值。每个结构中的隐式无参数构造函数设置这些默认值。您不能定义显式的无参数构造函数供自己使用。了解 C# 中的简单类型(如 int、double、bool 等)也很有用。通常被称为结构类型。

问答环节

4.6 泛型编程中如何使用 default 关键字?

您已经看到 default 关键字可以帮助您找到类型的默认值。在泛型编程中,有时您可能希望为泛型类型提供默认值。在前面的例子中,您看到了默认值根据值类型或引用类型的不同而不同。在这个例子中,请注意PrintDefault<T>()方法。

不使用下面的代码行

T defaultValue = default(T);

如果你使用类似

T defaultValue = null;//will not work for value types

你会得到一个编译时错误,

Error  CS0403  Cannot convert null to type parameter 'T' because it could be a non-nullable value type. Consider using 'default(T)' instead.

或者,如果您使用下面的代码行

T defaultValue = 0;//will not work for reference types

您会得到一个编译时错误,

Error  CS0029  Cannot implicitly convert type 'int' to 'T'

实现通用接口

就像泛型类一样,你也可以拥有泛型接口。泛型接口可以包含泛型方法和非泛型方法。如果要实现泛型接口方法,可以遵循通常实现非泛型接口方法时使用的相同方法。下面的程序演示如何实现泛型接口的方法。

演示 7

为了涵盖这两种场景,在这个例子中,通用接口GenericInterface<T>有一个称为GenericMethod(T param)的通用方法和一个称为NonGenericMethod()的非通用方法。第一个方法有一个通用的返回类型T,第二个方法有一个void返回类型。

剩下的部分比较好理解,我保留了评论,供大家参考。

using System;

namespace ImplementingGenericInterface
{
    interface GenericInterface<T>
    {
        //A generic method
        T GenericMethod(T param);
        //A non-generic method
        public void NonGenericMethod();

    }
    //Implementing the interface
    class ConcreteClass<T>:GenericInterface<T>
    {
        //Implementing interface method
        public T GenericMethod(T param)
        {
            return param;
        }

        public void NonGenericMethod()
        {
            Console.WriteLine("Implementing NonGenericMethod of GenericInterface<T>");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Implementing generic interfaces.***\n");
            //Using 'int' type
            GenericInterface<int> concreteInt = new ConcreteClass<int>();
            int myInt = concreteInt.GenericMethod(5);
            Console.WriteLine($"The value stored in myInt is : {myInt}");
            concreteInt.NonGenericMethod();

            //Using 'string' type now
            GenericInterface<string> concreteString = new ConcreteClass<string>();
            string myStr = concreteString.GenericMethod("Hello Reader");
            Console.WriteLine($"The value stored in myStr is : {myInt}");
            concreteString.NonGenericMethod();

            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Implementing generic interfaces.***

The value stored in myInt is : 5
Implementing NonGenericMethod of GenericInterface<T>
The value stored in myStr is : 5
Implementing NonGenericMethod of GenericInterface<T>

分析

在前一个例子中有一些有趣的地方需要注意。让我们检查他们。

  • If you have another concrete class that wants to implement GenericInterface<T>, and you write following code block, you get compile-time errors.

        class ConcreteClass2 : GenericInterface<T>//Error
        {
            public T GenericMethod(T param)
            {
                throw new NotImplementedException();
            }
    
            public void NonGenericMethod()
            {
                throw new NotImplementedException();
            }
        }
    
    

    这是因为我没有将类型参数 T 传递给 ConcreteClass2。您有三个相同的编译时错误“错误 CS0246 找不到类型或命名空间名称“T ”(您是否缺少 using 指令或程序集引用?)."消息。

  • 如果您编写以下代码段,您会得到同样的错误:

class ConcreteClass2<U> : GenericInterface<T>//Error

原因很明显:找不到 T。

当您实现泛型接口时,实现类需要处理相同的 T 类型参数。这就是下面这段代码有效的原因。

class ConcreteClass<T> : GenericInterface<T>
{//remaining code}

问答环节

在前面的例子中,我的实现类可以处理多个类型参数吗?

是的。以下两个代码段也是有效的。

class ConcreteClass2<U,T> : GenericInterface<T>//valid
{//remaining code}

class ConcreteClass2<T, U> : GenericInterface<T>//also valid
{remaining code}

要记住的关键是,你的实现类需要提供接口所需的参数(例如,在这种情况下,实现类必须包含 T 参数,它出现在GenericInterface<T>接口中。

假设你有以下两个界面。

interface IFirstInterface1<T> { }
interface ISecondInterface2<T, U> { }

您能预测下面的代码段能否编译吗?

Segment 1:
class MyClass1<T> : IFirstInterface<T> { }
Segment 2:
class MyClass2<T> : ISecondInterface<T, U> { }
Segment 3:
class MyClass3<T> : ISecondInterface<T, string> { }
Segment 4:
class MyClass4<T> : ISecondInterface<string, U> { }
Segment 5:
class MyClass5<T> : ISecondInterface<string, int> { }
Segment 6:
class MyClass6 : ISecondInterface<string, int> { }

只有段 2 和段 4 不会编译。在段 2 中,MyClass2 不包括 U 参数。在段 4 中,MyClass4 不包含 T 参数。

在段 1 和段 3 中,MyClass1 和 MyClass3 分别具有所需的参数。

第 5 段和第 6 段没有任何问题,因为在这些情况下,各自的类在构造封闭的接口上工作。

通用约束

您可以对泛型类型参数进行限制。例如,您可以选择泛型类型必须是引用类型或值类型,或者它应该从任何其他基类型派生,等等。但是为什么要在代码中允许约束呢?简单的答案是,通过使用约束,你可以对你的代码有很多控制,并且你允许 C# 编译器预先知道你将要使用的类型。因此,C# 编译器可以帮助您在编译时检测错误。

要指定一个约束,可以使用where关键字和一个冒号(:)操作符,如下所示。

class EmployeeStoreHouse<T> where T : IEmployee

或者,

class EmployeeStoreHouse<T> where T : IEmployee,new()

IEmployee是一个接口。

一般来说,使用以下约束。

  • where T : struct表示类型 T 必须是值类型。(请记住,结构是一种值类型。)

  • where T: class表示类型 T 必须是引用类型。(记住,类是一个引用类型。)

  • where T: IMyInter表示 T 类型必须实现IMyInter接口。

  • where T: new()意味着类型 T 必须有一个默认的(无参数的)构造函数。(如果与其他约束一起使用,则将其放在最后一个位置。)

  • where T: S意味着类型 T 必须从另一个泛型类型 s 派生。它有时被称为裸类型约束

现在让我们进行一次演示。

演示 8

在演示 8 中,IEmployee接口包含一个抽象的Position方法。在将雇员的详细信息存储在雇员存储中之前,我使用这个方法来设置雇员的名称(可以把它看作一个简单的雇员数据库)。Employee类继承自IEmployee,所以它需要实现这个接口方法。Employee类有一个公共构造函数,它可以接受两个参数:第一个参数设置雇员姓名,第二个参数表示工作年限。我正在根据员工的经验设定一个名称。(是的,为了简单起见,我只考虑多年的经验来定位。)

在本演示中,您会看到下面一行。

class EmployeeStoreHouse<T> where T : IEmployee

泛型参数的约束只是告诉你泛型类型T必须实现IEmployee接口。

最后,我使用了基于范围的 switch 语句,从 C# 7.0 开始就支持这种语句。如果您使用的是遗留版本,可以用传统的 switch 语句替换代码段。

这是完整的演示。

using System;
using System.Collections.Generic;

namespace UsingConstratintsinGenerics
{
    interface IEmployee
    {
        string Position();
    }
    class Employee : IEmployee
    {
        public string Name;
        public int YearOfExp;
        //public Employee() { }
        public Employee(string name, int yearOfExp)
        {
            this.Name = name;
            this.YearOfExp = yearOfExp;
        }
        public string Position()
        {
            string designation;
      //C#7.0 onwards range based switch statements are allowed.
            switch (YearOfExp)
            {
                case int n when (n <= 1):
                    designation = "Fresher";
                    break;

                case int n when (n >= 2 && n <= 5):
                    designation = "Intermediate";
                    break;
                default:
                    designation = "Expert";
                    break;
            }
            return designation;
        }

    }
    class EmployeeStoreHouse<T> where T : IEmployee
    {
        private List<Employee> EmpStore = new List<Employee>();
        public void AddToStore(Employee element)
        {
            EmpStore.Add(element);
        }
        public void DisplayStore()
        {
            Console.WriteLine("The store contains:");
            foreach (Employee e in EmpStore)
            {
                Console.WriteLine(e.Position());
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Using constraints in generic programming.***\n");
            //Employees
            Employee e1 = new Employee("Suresh", 1);
            Employee e2 = new Employee("Jack", 5);
            Employee e3 = new Employee("Jon", 7);
            Employee e4 = new Employee("Michael", 2);
            Employee e5 = new Employee("Amit", 3);

            //Employee StoreHouse
            EmployeeStoreHouse<Employee> myEmployeeStore = new EmployeeStoreHouse<Employee>();
            myEmployeeStore.AddToStore(e1);
            myEmployeeStore.AddToStore(e2);
            myEmployeeStore.AddToStore(e3);
            myEmployeeStore.AddToStore(e4);
            myEmployeeStore.AddToStore(e5);

            //Display the Employee Positions in Store
            myEmployeeStore.DisplayStore();

            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Using constraints in generic programming.***

The store contains:
Fresher
Intermediate
Expert
Intermediate
Intermediate

问答环节

4.9 为什么我在下面一行中得到多个编译时错误?

class EmployeeStoreHouse<T> where T : new(),IEmployee

目前有两个问题。首先,您没有将新的()约束作为最后一个约束。其次,Employee 类没有公共的无参数构造函数。Visual Studio 为您提供了关于这两种错误的线索;错误截图如图 4-3 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-3

由于不正确使用 new()约束而导致的编译时错误

简单的补救方法是

  • 在最后一个位置放置一个new()约束

  • 在 Employee 类中定义一个公共的无参数构造函数,例如

public Employee() { }

4.10 我可以对构造函数应用约束吗?

当您为泛型类型使用new()约束时,您实际上是将约束放在了构造函数上。例如,在下面的代码中,该类型必须有一个无参数的构造函数。

public class MyClass<T> where T:new()

在这种情况下,一定要注意不能使用“参数化”的构造函数约束。例如,如果在下面的代码中使用 new(int)这样的代码,就会出现几个编译时错误。

class EmployeeStoreHouse<T> where T : IEmployee,new(int) //Error

一个错误说,

Error CS0701 'int' is not a valid constraint. A type used as a constraint must be an interface, a nonsealed class or a type parameter.

4.11 我可以在单一类型上应用多个接口作为约束吗?

是的。例如,如果您使用现成的 List 类,您将看到以下内容。

public class List<[NullableAttribute(2)]T>
    : ICollection<T>, IEnumerable<T>, IEnumerable, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection, IList
    {//some other stuff}

你可以看到ICollection<T>IEnumerable<T>IList<T>应用在List<T>上。

使用协方差和逆变

在第一章关于委托的讨论中,你了解到协变和逆变支持委托最早出现在 C# 2.0 中。从 C# 4.0 开始,这些概念可以应用于泛型类型参数、泛型接口和泛型委托。第一章也与非通用代表探讨了这些概念。在本章中,我们将通过更多的案例继续探讨这些概念。

在继续之前,请记住以下几点。

  • 协方差和逆变处理带有参数和返回类型的类型转换。

  • 英寸 NET 4 以后,您可以在泛型委托和泛型接口中使用这些概念。(在早期版本中,会出现编译时错误。)

  • 逆变通常被定义为调整或修改。当你试图在编码世界中实现这些概念时,你就明白了下面的道理(或者类似的道理)。

    • 所有的足球运动员都是运动员,但反过来却不是这样(因为有许多运动员打高尔夫、篮球、曲棍球等。)同样,你可以说所有的公共汽车或火车都是交通工具,但反过来就不一定了。

    • 在编程术语中,所有派生类都是基于类型的类,但反之则不然。例如,假设您有一个名为Rectangle的类,它是从名为Shape的类派生而来的。那么你可以说所有的矩形都是形状,但反过来就不成立了。

    • 根据 MSDN 的说法,协方差和逆变处理数组、委托和泛型类型的隐式引用转换。协方差保持赋值兼容性,逆变则相反。

从。NET Framework 4,在 C# 中,有关键字将接口和委托的泛型类型参数标记为协变或逆变。对于协变接口和委托,您会看到使用了out关键字(表示值出来了)。逆变接口和委托与关键字in相关联(表示值正在进入)。

考虑一个内置的 C# 构造。我们来查看一下IEnumerable<T>在 Visual Studio 中的定义,如图 4-4 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-4

来自 Visual Studio 2019 的 IEnumerable 界面部分截图

你可以看到outIEnumerable相关联。这仅仅意味着你可以将IEnumerable<DerivedType>分配给IEnumerable<BaseType>。这就是为什么你可以将IEnumerable<string>分配给IEnumerable<object>。所以,你可以说IEnumerable<T>T上的协变。

现在在 Visual Studio 中检查Action<T>委托的定义,如图 4-5 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-5

来自 Visual Studio 2019 的动作委托的部分截图

或者,你可以在 Visual Studio 中查看IComparer<T>接口的定义,如图 4-6 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-6

Visual Studio 2019 的 IComparer 界面部分截图

您可以看到inAction委托和IComparer接口相关联。这仅仅意味着你可以将Action<BaseType>分配给Action<DerivedType>。所以,你可以说动作是 t 上的逆变

类似地,因为 type 参数在the IComparer接口中是逆变的,所以您可以使用您指定的实际类型或者任何更通用(或者更少派生)的类型。

问答环节

4.12 在一个 Func 委托中,我看到了同时存在的 in out 参数。比如在 T1 中的 Func < in T,out TResult > Func <,在 T2,out TResult >,这些定义我该怎么解读?

它只是告诉你Func委托有协变的返回类型和逆变的参数类型。

4.13“任务兼容性”是什么意思?

下面是一个示例,您可以将更具体的类型(或派生类型)分配给兼容的不太具体的类型。例如,整数变量的值可以存储在对象变量中,如下所示:

 int i = 25;
 object o = i;//Assignment Compatible

具有泛型委托的协方差

让我们用一个泛型委托来检查协方差。在下面的演示中,我声明了一个具有协变返回类型的泛型委托,如下所示。

delegate TResult CovDelegate<out TResult>();

在这个例子中,Vehicle是父类,Bus是派生类,所以您看到了层次结构。(我没有在这些类中添加任何额外的方法/代码,因为本演示不需要它们。)

class Vehicle
{
      //Some code if needed
}
class Bus : Vehicle
{
     //Some code if needed
 }

此外,您会看到以下两个静态方法的存在:GetOneVehicle()GetOneBus()。第一个返回一个Vehicle对象,第二个返回一个Bus对象。

private static Vehicle GetOneVehicle()
{
    Console.WriteLine("Creating one vehicle and returning it.");
        return new Vehicle();
}
private static Bus GetOneBus()
{
    Console.WriteLine("Creating one bus and returning the bus.");

下面的代码段简单易懂,因为它们与委托签名相匹配。

CovDelegate<Vehicle> covVehicle = GetOneVehicle;
covVehicle();
CovDelegate<Bus> covBus = GetOneBus;
covBus();

现在有趣的部分来了。注意下面的赋值。

covVehicle = covBus;

这种赋值不会引发任何编译错误,因为我使用了具有协变返回类型的委托。但是需要注意的是,如果没有使用 out 参数使委托的返回类型协变,这种赋值会导致下面的编译时错误。

Error CS0029  Cannot implicitly convert type 'CovarianceWithGenericDelegates.CovDelegate<CovarianceWithGenericDelegates.Bus>' to 'CovarianceWithGenericDelegates.CovDelegate<CovarianceWithGenericDelegates.Vehicle>'

演示 9

进行完整的演示。请参考支持性注释来帮助您理解。

using System;

namespace CovarianceWithGenericDelegates
{
    //A generic delegate with covariant return type
    //(Notice the use of 'out' keyword)
    delegate TResult CovDelegate<out TResult>();

    //Here 'out' is not used(i.e. it is non-covariant)
    //delegate TResult CovDelegate<TResult>();

    class Vehicle
    {
        //Some code if needed
    }
    class Bus : Vehicle
    {
        //Some code if needed
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Testing covariance with a Generic Delegate.***");
            Console.WriteLine("Normal usage:");
            CovDelegate<Vehicle> covVehicle = GetOneVehicle;
            covVehicle();
            CovDelegate<Bus> covBus = GetOneBus;
            covBus();
            //Testing Covariance
            //covBus to covVehicle (i.e. more specific-> more general) is //allowed through covariance
            Console.WriteLine("Using covariance now.");
            //Following assignment is Ok, if you use 'out' in delegate //definition
Otherwise, you'll receive compile-time error
            covVehicle = covBus;//Still ok
            covVehicle();
            Console.WriteLine("End covariance testing.\n");
            Console.ReadKey();
        }

        private static Vehicle GetOneVehicle()
        {
            Console.WriteLine("Creating one vehicle and returning it.");
            return new Vehicle();
        }
        private static Bus GetOneBus()
        {
            Console.WriteLine("Creating one bus and returning the bus.");
            return new Bus();
        }
    }
}

输出

这是输出。

***Testing covariance with a Generic Delegate.***
Normal usage:
Creating one vehicle and returning it.
Creating one bus and returning the bus.
Using covariance now.
Creating one bus and returning the bus.
End covariance testing.

通用接口的协变

让我们用一个通用接口来检查协方差。在这个例子中,我使用了 C# 中另一个名为IEnumerable<T>的内置结构。这是一个为 C# 中最重要的功能提供基础的接口。如果您想对集合中的每一项做一些有意义的事情,并逐个处理它们,那么可以在一个foreach循环中使用IEnumerable<T>。包含多个元素的. NET Framework 实现了此接口。例如,常用的List类实现了这个接口。

演示 10

和前面的演示一样,在这个例子中,Vehicle是父类,Bus是派生类,但是这次,我在它们中分别放置了一个名为ShowMe()的实例方法。你已经在IEnumerable<T>中看到,T 是协变的,所以这一次,我可以应用下面的赋值。

IEnumerable<Vehicle> vehicleEnumerable= busEnumerable;

busEnumerable是一个IEnumerable<Bus>对象,可能如下所示。

IEnumerable<Bus> busEnumerable=new List<Bus>();

在许多现实生活中的应用中,使用返回IEnumerable<T>.的方法是一种常见的做法,当您不想向他人公开实际的具体类型并且能够循环遍历项目时,这很有用。

现在浏览完整的演示,如果需要的话可以参考支持性的注释。

using System;
using System.Collections.Generic;

namespace CovarianceWithGenericInterface
{
    class Vehicle
    {
        public virtual void ShowMe()
        {
            Console.WriteLine("Vehicle.ShowMe().The hash code is : " + GetHashCode());
        }
    }
    class Bus : Vehicle
    {
        public override void ShowMe()
        {
            Console.WriteLine("Bus.ShowMe().Here the hash code is : " + GetHashCode());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            //Covariance Example
            Console.WriteLine("***Using Covariance with Generic Interface.***\n");
            Console.WriteLine("**Remember that T in IEnumerable<T> is covariant");
            //Some Parent objects
            //Vehicle vehicle1 = new Vehicle();
            //Vehicle vehicle2 = new Vehicle();
            //Some Bus objects
            Bus bus1 = new Bus();
            Bus bus2 = new Bus();
            //Creating a child List
            //List<T> implements IEnumerable<T>
            List<Bus> busList = new List<Bus>();
            busList.Add(bus1);
            busList.Add(bus2);
            IEnumerable<Bus> busEnumerable = busList;
            /*
             An object which was instantiated with a more derived type argument (Bus) is assigned to an object instantiated with a less derived type argument(Vehicle).Assignment compatibility is preserved here.
            */
            IEnumerable<Vehicle> vehicleEnumerable = busEnumerable;
            foreach (Vehicle vehicle in vehicleEnumerable)
            {
                vehicle.ShowMe();
            }

            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Using Covariance with Generic Interface.***

**Remember that T in IEnumerable<T> is covariant
Bus.ShowMe().Here the hash code is : 58225482
Bus.ShowMe().Here the hash code is : 54267293

与泛型委托相反

让我们用一个泛型委托来检查 contravariance。在这个演示中,我声明了一个泛型逆变委托,如下所示。

delegate void ContraDelegate<in T>(T t);

同样,Vehicle是父类,Bus是派生类,它们都包含一个名为ShowMe()的方法。您会看到下面的代码段。

class Vehicle
{
    public virtual void ShowMe()
    {
        Console.WriteLine(" Vehicle.ShowMe()");
    }
}
class Bus : Vehicle
{
    public override void ShowMe()
    {
        Console.WriteLine(" Bus.ShowMe()");
    }
}

除了这些类,您还会看到下面两个静态方法的存在:ShowVehicleType()ShowBusType()。(第一个从Vehicle对象调用ShowMe(),第二个从Bus对象调用ShowMe()。)

private static void ShowVehicleType(Vehicle vehicle)
{
    vehicle.ShowMe();
}
private static void ShowBusType(Bus bus)
{
    bus.ShowMe();
}

下面的代码段简单易懂,因为它们与委托签名相匹配。(输出也显示在注释中。)

ContraDelegate<Vehicle> contraVehicle = ShowVehicleType;
contraVehicle(obVehicle); // Vehicle.ShowMe()
ContraDelegate<Bus> contraBus = ShowBusType;
contraBus(obBus); // Bus.ShowMe()

现在到了有趣的部分,它与协方差相反。注意下面的赋值。

contraBus = contraVehicle;

这个赋值不会引发任何编译错误,因为我使用的是逆变委托。 但是需要注意的是,如果没有使用 in 参数使委托逆变,这种赋值会导致下面的编译时错误。

Error CS0029 Cannot implicitly convert type 'ContravarianceWithGenericDelegates.ContraDelegate<ContravarianceWithGenericDelegates.Vehicle>' to 'ContravarianceWithGenericDelegates.ContraDelegate<ContravarianceWithGenericDelegates.Bus>'

演示 11

现在浏览完整的演示,并参考支持注释来帮助您理解。

using System;

namespace ContravarianceWithGenericDelegates
{
    // A generic contravariant delegate
    delegate void ContraDelegate<in T>(T t);
    // A generic non-contravariant delegate
    //delegate void ContraDelegate<T>(T t);
    class Vehicle
    {
        public virtual void ShowMe()
        {
            Console.WriteLine(" Vehicle.ShowMe()");
        }
    }
    class Bus : Vehicle
    {
        public override void ShowMe()
        {
            Console.WriteLine(" Bus.ShowMe()");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("*** Testing Contra-variance with Generic Delegates.***");
            Vehicle obVehicle = new Vehicle();
            Bus obBus = new Bus();
            Console.WriteLine("Normal usage:");
            ContraDelegate<Vehicle> contraVehicle = ShowVehicleType;
            contraVehicle(obVehicle);
            ContraDelegate<Bus> contraBus = ShowBusType;
            contraBus(obBus);
            Console.WriteLine("Using contravariance now.");
            /*
            Using general type to derived type.
            Following assignment is Ok, if you use 'in' in delegate definition.
            Otherwise, you'll receive compile-time error.
            */
            contraBus = contraVehicle;//ok
            contraBus(obBus);
            Console.ReadKey();
        }

        private static void ShowVehicleType(Vehicle vehicle)
        {
            vehicle.ShowMe();
        }
        private static void ShowBusType(Bus bus)
        {
            bus.ShowMe();
        }
    }

}

输出

这是输出。

*** Testing Contra-variance with Generic Delegates.***
Normal usage:
 Vehicle.ShowMe()
 Bus.ShowMe()
Using contravariance now.
 Bus.ShowMe()

与通用接口相反

现在你明白了协变和逆变。您已经看到了在泛型委托中使用协变和逆变,以及使用泛型接口实现协变。我将剩下的情况作为家庭作业,您需要编写一个完整的程序,并使用通用接口实现逆变的概念。

我提供了可以帮助您实现它的部分代码段。如果您愿意,可以使用以下代码段作为参考来验证您的实现。为了更好地理解,您也可以参考相关的注释。

部分实施

这是一个通用的逆变接口。

// Contravariant interface
interface IContraInterface<in T>{ }
// Following interface is neither covariant nor contravariant
//interface IContraInterface< T> { }
class Implementor<T>: IContraInterface<T> { }

这是一个继承层次结构。

class Vehicle
{
   // Some code if needed
}
class Bus : Vehicle
{
    // Some code if needed
}

这是关键任务。

IContraInterface<Vehicle> vehicleOb = new Implementor<Vehicle>();
IContraInterface<Bus> busOb = new Implementor<Bus>();
// Contravarince allows the following
// but you'll receive a compile-time error
// if you do not make the interface contravariant using 'in'
busOb = vehicleOb;

问答环节

当我使用协方差时,看起来好像是在使用一种简单的多态性技术。例如,在前面的演示中,您使用了以下代码行。

IEnumerable<Vehicle> vehicleEnumerable = busEnumerable;

这是正确的吗?

是的。

4.15 我可以覆盖一个泛型方法吗?

是的。您需要遵循应用于非泛型方法的相同规则。让我们看看演示 12。

演示 12

在本演示中,BaseClass<T>是父类。它有一个名为MyMethod的方法,接受T作为参数,它的返回类型也是TDerivedClass<T>从此父类派生并覆盖此方法。

using System;

namespace MethodOverridingDemo
{
    class BaseClass<T>
    {
        public virtual T MyMethod(T param)
        {
            Console.WriteLine("Inside BaseClass.BaseMethod()");
            return param;
        }
    }
    class DerivedClass<T>: BaseClass<T>
    {
        public override T MyMethod(T param)
        {
            Console.WriteLine("Here I'm inside of DerivedClass.DerivedMethod()");
            return param;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Overriding a virtual method.***\n");
            BaseClass<int> intBase = new BaseClass<int>();
            // Invoking Parent class method
            Console.WriteLine($"Parent class method returns {intBase.MyMethod(25)}");//25
            // Now pointing to the child class method and invoking it.
            intBase = new DerivedClass<int>();
            Console.WriteLine($"Derived class method returns {intBase.MyMethod(25)}");//25
            // The following will cause compile-time error
            //intBase = new DerivedClass<double>(); // error
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Overriding a virtual method.***

Inside BaseClass.BaseMethod()
Parent class method returns 25
Here I'm inside of DerivedClass.DerivedMethod()
Derived class method returns 25

分析

您可以看到,通过遵循一个简单的多态性,我使用父类引用(intBase)指向子类对象。这种编码没有问题,因为两种情况都只处理int类型。但是下面几行带注释的代码很容易理解,因为使用intBase,你不能指向一个处理不同类型的对象(在这个例子中是double)。

// The following will cause compile-time error
//intBase = new DerivedClass<double>(); // error

为了打印输出消息,我使用了字符串插值技术。我用它只是为了一个改变,但在这种情况下,你需要使用 C# 6.0 或更高版本;否则,您可以使用传统的方法。

问答环节

4.16 我可以重载一个泛型方法吗?

是的。在这种情况下,您也需要遵循应用于非泛型方法的相同规则,但是您必须小心使用接受类型参数的方法。在这种情况下,上的类型差异不被认为是泛型类型***;*** 代替 这要看你把 替换成什么类型参数了。

4.17 你说泛型类型上不考虑类型差异;相反,它取决于您替换为类型参数的类型参数。你能详细说明一下吗?

我的意思是,有时看起来你已经完美地遵循了重载的规则,但是当你重载一个接受类型参数的泛型方法时,还需要考虑更多的东西。

你知道对于重载,数量和/或类型参数是不同的。所以,如果你的类中有以下两个方法,你可以说这是一个重载的例子。

public void MyMethod2(int a, double b) { // some code };
public void MyMethod2(double b, int a) { // some code };

现在考虑下面的代码段,它涉及泛型类型参数。

class MyClass<T,U>
{
    public  void MyMethod(T param1, U param2)
    {
        Console.WriteLine("Inside MyMethod(T param1, U param2)");
    }
    public void MyMethod(U param1, T param2)
    {
        Console.WriteLine("Inside MyMethod(U param1, T param2)");
    }
}

似乎有两个 MyMethod 的重载版本,因为泛型类型参数的顺序不同。但是有潜在的歧义,当你练习下面的代码段时,你就会明白了。

MyClass<int, double> object1 = new MyClass<int, double>();
object1.MyMethod(1, 2.3); // ok
MyClass<int, int> object2 = new MyClass<int, int>();
// Ambiguous call
object2.MyMethod(1, 2); // error

对于这段代码,您会得到下面的编译时错误(对于标有// error 的行)。

CS0121 The call is ambiguous between the following methods or properties: 'MyClass<T, U>.MyMethod(T, U)' and 'MyClass<T, U>.MyMethod(U, T)'

演示 13

这是完整的演示。

using System;

namespace MethodOverloadingDemo
{
    class MyClass<T,U>
    {
        public  void MyMethod(T param1, U param2)
        {
            Console.WriteLine("Inside MyMethod(T param1, U param2)");
        }
        public void MyMethod(U param1, T param2)
        {
            Console.WriteLine("Inside MyMethod(U param1, T param2)");
        }
           public void MyMethod2(int a, double b)
        {
            Console.WriteLine("Inside MyMethod2(int a, double b).");
        }
        public void MyMethod2(double b, int a)
        {
            Console.WriteLine("MyMethod2(double b, int a) is called here.");
        }    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Method overloading demo.***\n");
            MyClass<int, double> object1 = new MyClass<int, double>();
            object1.MyMethod(1, 2.3);//ok
            object1.MyMethod2(1, 2.3);//ok
            object1.MyMethod2(2.3, 1);//ok
            MyClass<int, int> object2 = new MyClass<int, int>();
            // Ambiguous call
            object2.MyMethod(1, 2); // error
            Console.ReadKey();
        }
    }
}

输出

同样,您会得到下面的编译时错误。

CS0121 The call is ambiguous between the following methods or properties: 'MyClass<T, U>.MyMethod(T, U)' and 'MyClass<T, U>.MyMethod(U, T)'

您可以注释掉这个不明确的调用,如下所示,然后编译并运行程序。

//object2.MyMethod(1, 2);//error

这一次,您将获得以下输出。

***Method overloading demo.***

Inside MyMethod(T param1, U param2)
Inside MyMethod2(int a, double b).
MyMethod2(double b, int a) is called here.

自引用泛型类型

有时你可能需要比较一个类的两个实例。在这种情况下,你有两个选择。

  • 使用内置结构。

  • 自己写比较法。

当您对使用内置构造感兴趣时,您有多种选择。例如,您可以使用IComparable<T>CompareTo方法或IEquitable<T>Equals方法。你可能会注意到在 C# 中也有一个非泛型的IComparable

以下是来自 Visual Studio 的关于CompareTo的信息。

//
// Summary:
//     Compares the current instance with another object of the same //     type and returns an integer that indicates whether the current instance //     precedes, follows, or occurs in the same position in the sort //     order as the other object.
//
// Parameters:
//   other:
//     An object to compare with this instance.
//
// Returns:
//     A value that indicates the relative order of the objects being //     compared. The return value has these meanings: Value Meaning Less //     than zero This instance precedes other in the sort order. Zero //     This instance occurs in the same position in the sort order as other. //     Greater than zero This instance follows other in the sort order.
   int CompareTo([AllowNull] T other);

以下是来自 Visual Studio 的关于Equals的信息。

//
// Summary:
//     Indicates whether the current object is equal to another object of//     the same type.
//
// Parameters:
//   other:
//     An object to compare with this object.
//
// Returns:
//     true if the current object is equal to the other parameter; //     otherwise, false.
    bool Equals([AllowNull] T other);

如果您的类实现了这些接口中的任何一个,您可以使用这些方法并根据需要重写它们。这些接口在System名称空间中可用,它们由内置类型实现,如intdoubleand string

然而,在许多情况下,您可能想要编写自己的比较方法。我在演示 14 中这样做了。

当类型关闭类型参数时,它可以将自己命名为具体类型。

演示 14

在这个演示中,Employee类实现了IIdenticalEmployee<T>,它有一个名为CheckEqualityWith的抽象方法。让我们假设在您的Employee类中,您有员工 id 和部门名称。一旦我实例化了来自Employee类的对象,我的任务就是比较这些对象。

为了便于比较,我简单地检查了两个雇员的deptNameemployeeID是否相同。如果匹配,则员工是相同的。(使用单词,我指的只是这些对象的内容,而不是对堆的引用。)

这就是比较法。

public string CheckEqualityWith(Employee obj)
{
    if (obj == null)
    {
        return "Cannot Compare with a Null Object";
    }
    else
    {
       if (this.deptName == obj.deptName && this.employeeID == obj.employeeID)
       {
           return "Same Employee.";
       }
       else
       {
           return "Different Employees.";
       }
   }
}

现在查看完整的实现和输出。

using System;

namespace SelfReferencingGenericTypeDemo
{
    interface IIdenticalEmployee<T>
    {
        string CheckEqualityWith(T obj);
    }
    class Employee : IIdenticalEmployee<Employee>
    {
           string deptName;
           int employeeID;
           public Employee(string deptName, int employeeId)
           {
               this.deptName = deptName;
               this.employeeID = employeeId;
           }
           public string CheckEqualityWith(Employee obj)
           {
               if (obj == null)
               {
                   return "Cannot Compare with a null Object";
               }
               else
               {
                   if (this.deptName == obj.deptName && this.employeeID == obj.employeeID)
                   {
                       return "Same Employee.";
                   }
                   else
                   {
                       return "Different Employees.";
                   }
               }
           }
   }
   class Program
   {
        static void Main(string[] args)
        {
            Console.WriteLine("**Self-referencing generic type demo.***\n");
            Console.WriteLine("***We are checking whether two employee objects are same or different.***");
            Console.WriteLine();
            Employee emp1 = new Employee("Chemistry", 1);
            Employee emp2 = new Employee("Maths", 2);
            Employee emp3 = new Employee("Comp. Sc.", 1);
            Employee emp4 = new Employee("Maths", 2);
            Employee emp5 = null;
            Console.WriteLine("Comparing emp1 and emp3 :{0}", emp1.CheckEqualityWith(emp3));
            Console.WriteLine("Comparing emp2 and emp4 :{0}", emp2.CheckEqualityWith(emp4));
            Console.WriteLine("Comparing emp2 and emp5 :{0}", emp2.CheckEqualityWith(emp5));
            Console.ReadKey();
        }
    }
}

输出

这是输出。

**Self-referencing generic type demo.***
***We are checking whether two employee objects are same or different.***

Comparing emp1 and emp3 :Different Employees.
Comparing emp2 and emp4 :Same Employee.
Comparing emp2 and emp5 :Cannot Compare with a null Object

分析

此示例向您展示了当类型关闭类型参数时,它可以将自己命名为具体类型。它演示了如何使用一个 自引用泛型 类型。 同样,在这个例子中,我使用了这个词,我指的只是对象的内容,而不是对堆的引用。

问答环节

4.18 你能总结一下泛型的关键用法吗?

您可以促进类型安全,而不必创建大量非常相似的类型,尤其是仅在它们使用的类型上有所不同的类型。因此,您可以避免运行时错误,并降低装箱和拆箱的成本。

4.19 静态变量如何在泛型编程环境中工作?

静态数据对于每个封闭类型都是唯一的。考虑下面的程序和输出供您参考。

示范 15

在本演示中,让我们关注 count 变量,看看当用不同类型实例化MyGenericClass<T>泛型类时,它是如何递增的。

using System;

namespace TestingStaticData
{
    class MyGenericClass<T>
    {
        public static int count;
        public void IncrementMe()
        {
            Console.WriteLine($"Incremented value is : {++count}");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Testing static in the context of generic programming.***");
            MyGenericClass<int> intOb = new MyGenericClass<int>();
            Console.WriteLine("\nUsing intOb now.");
            intOb.IncrementMe();//1
            intOb.IncrementMe();//2
            intOb.IncrementMe();//3

            Console.WriteLine("\nUsing strOb now.");
            MyGenericClass<string> strOb = new MyGenericClass<string>();
            strOb.IncrementMe();//1
            strOb.IncrementMe();//2

            Console.WriteLine("\nUsing doubleOb now.");
            MyGenericClass<double> doubleOb = new MyGenericClass<double>();
            doubleOb.IncrementMe();//1
            doubleOb.IncrementMe();//2

            MyGenericClass<int> intOb2 = new MyGenericClass<int>();
            Console.WriteLine("\nUsing intOb2 now.");
            intOb2.IncrementMe();//4
            intOb2.IncrementMe();//5

            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Testing static in the context of generic programming.***

Using intOb now.
Incremented value is : 1
Incremented value is : 2
Incremented value is : 3

Using strOb now.
Incremented value is : 1
Incremented value is : 2

Using doubleOb now.
Incremented value is : 1
Incremented value is : 2

Using intOb2 now.
Incremented value is : 4
Incremented value is : 5

问答环节

4.20 使用泛型有什么重要的限制?

以下是一些需要注意的重要限制。

  • 静态数据对于每个封闭类型是唯一的,但是对于不同的构造类型来说不是

  • 不能在泛型方法中使用外部修饰符。因此,下面的代码段

    using System;
    using System.Runtime.InteropServices;
    class GenericClassDemo2<T>
    {
        [DllImport("avifil32.dll")] // error in generic method
        private static extern void AVIFileInit();
    }
    
    raises the following compile-time error:
    Error CS7042  The DllImport attribute cannot be applied to a method that is generic or contained in a generic type.
    
    
  • 不能将指针类型用作类型参数。因此,下面代码段

    class GenericClassDemo2<T>
    {
        static unsafe void ShowMe()
        {
            int a = 10; // ok
            int* p; // ok
            p = &a; // ok
    
            T* myVar; // error
        }
    }
    
    

    中的最后一行引发了下面的编译时错误:

    Error CS0208  Cannot take the address of, get the size of, or declare a pointer to a managed type ('T')
    
    
  • 在问答环节问题 4.9 中,你看到了如果你有多个约束,new()约束必须放在最后。

最后的话

我希望这一章能够揭开泛型编程的关键特性。起初,泛型语法可能看起来有点令人不知所措,但是实践和重复使用这些概念将帮助您掌握它们,并且您将能够使用 C# 开发出高质量的软件。

现在让我们跳到下一章,在那里你将学习线程编程。

摘要

本章讨论了以下关键问题。

  • 什么是泛型程序?为什么它很重要?

  • 泛型编程比非泛型编程有什么优势?

  • 为什么 default 关键字在泛型的上下文中有用?如何在我的程序中使用它?

  • 如何在程序中使用内置委托——函数、动作和谓词?

  • 在泛型编程中如何施加约束?

  • 如何对泛型委托和接口使用协变和逆变?

  • 如何重载泛型方法?你为什么要小心呢?

  • 如何重写泛型方法?

  • 如何使用自引用泛型类型?

  • 静态变量在泛型程序中是如何表现的?

  • 泛型的一些关键限制是什么?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值