C#高级编程(第10版) C# 6 & .NET Core 1.0 (.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

6.6 泛型方法

除了定义泛型类之外,还可以定义泛型方法。在泛型方法中,泛型类型用方法声明来定义。泛型方法可以在非泛型类中定义。

Swap<T>()方法把T定义为泛型类型,该泛型类型用于两个参数和一个变量temp:

        void Swap<T>(ref T x, ref T y)
        {
          T temp;
          temp = x;
          x = y;
          y = temp;
        }

把泛型类型赋予方法调用,就可以调用泛型方法:

        int i = 4;
        int j = 5;
        Swap<int>(ref i, ref j);

但是,因为C#编译器会通过调用Swap()方法来获取参数的类型,所以不需要把泛型类型赋予方法调用。泛型方法可以像非泛型方法那样调用:

        int i = 4;
        int j = 5;
        Swap(ref i, ref j);

6.6.1 泛型方法示例

下面的例子使用泛型方法累加集合中的所有元素。为了说明泛型方法的功能,下面使用包含Name和Balance属性的Account类(代码文件GenericMethods/Account.cs):

注意:在.NET Core中,这个示例需要引用NuGet包System. Collections。

        public class Account
        {
          public string Name { get; }
          public decimal Balance { get; private set; }
          public Account(string name, Decimal balance)
          {
            Name = name;
            Balance = balance;
          }
        }

其中应累加余额的所有账户操作都添加到List<Account>类型的账户列表中(代码文件GenericMethods/Program.cs):

        var accounts = new List<Account>()
        {
          new Account("Christian", 1500),
          new Account("Stephanie", 2200),
          new Account("Angela", 1800),
          new Account("Matthias", 2400)
        };

累加所有Account对象的传统方式是用foreach语句遍历所有的Account对象,如下所示。foreach语句使用IEnumerable接口迭代集合的元素,所以AccumulateSimple()方法的参数是IEnumerable类型。foreach语句处理实现IEnumerable接口的每个对象。这样,AccumulateSimple()方法就可以用于所有实现IEnumerable<Account>接口的集合类。在这个方法的实现代码中,直接访问Account对象的Balance属性(代码文件GenericMethods/Algorithm.cs):

        public static class Algorithms
        {
          public static decimal AccumulateSimple(IEnumerable<Account> source)
          {
            decimal sum = 0;
            foreach (Account a in source)
            {
              sum += a.Balance;
            }
            return sum;
          }
       }

AccumulateSimple()方法的调用方式如下:

       decimal amount = Algorithms.AccumulateSimple(accounts);

6.6.2 带约束的泛型方法

第一个实现代码的问题是,它只能用于Account对象。使用泛型方法就可以避免这个问题。

Accumulate()方法的第二个版本接受实现了IAccount接口的任意类型。如前面的泛型类所述,泛型类型可以用where子句来限制。用于泛型类的这个子句也可以用于泛型方法。Accumulate()方法的参数改为IEnumerable<T>。IEnumerable<T>是泛型集合类实现的泛型接口(代码文件GenericMethods/ Algorithms.cs)。

        public static decimal Accumulate<TAccount>(IEnumerable<TAccount> source)
            where TAccount: IAccount
        {
          decimal sum = 0;
          foreach (TAccount a in source)
          {
            sum += a.Balance;
          }
          return sum;
        }

重构的Account类现在实现接口IAccount(代码文件GenericMethods/Account.cs):

        public class Account: IAccount
        {
          //...

IAccount接口定义了只读属性Balance和Name(代码文件GenericMethods/IAccount.cs):

        public interface IAccount
        {
          decimal Balance { get; }
          string Name { get; }
        }

将Account类型定义为泛型类型参数,就可以调用新的Accumulate()方法(代码文件GenericMethods/Program.cs):

        decimal amount = Algorithm.Accumulate<Account>(accounts);

因为编译器会从方法的参数类型中自动推断出泛型类型参数,所以以如下方式调用Accumulate()方法是有效的:

        decimal amount = Algorithm.Accumulate(accounts);

6.6.3 带委托的泛型方法

泛型类型实现IAccount接口的要求过于严格。下面的示例提示了,如何通过传递一个泛型委托来修改Accumulate()方法。第9章详细介绍了如何使用泛型委托,以及如何使用lambda表达式。

这个Accumulate()方法使用两个泛型参数T1和T2。第一个参数T1用于实现IEnumerable<T1>参数的集合,第二个参数使用泛型委托Func<T1, T2, TResult>。其中,第2个和第3个泛型参数都是T2类型。需要传递的方法有两个输入参数(T1和T2)和一个T2类型的返回值(代码文件Generic-Methods/Algorithm.cs):

        public static T2 Accumulate<T1, T2>(IEnumerable<T1> source,
                                      Func<T1, T2, T2> action)
        {
          T2 sum = default(T2);
          foreach (T1 item in source)
          {
            sum = action(item, sum);
          }
          return sum;
        }

在调用这个方法时,需要指定泛型参数类型,因为编译器不能自动推断出该类型。对于方法的第1个参数,所赋予的accounts集合是IEnumerable<Account>类型。对于第2个参数,使用一个lambda表达式来定义Account和decimal类型的两个参数,返回一个小数。对于每一项,通过Accumulate()方法调用这个lambda表达式(代码文件GenericMethods/Program.cs):

        decimal amount = Algorithm.Accumulate<Account, decimal>(
                          accounts, (item, sum) => sum += item.Balance);

不要为这种语法伤脑筋。该示例仅说明了扩展Accumulate()方法的可能方式。

6.6.4 泛型方法规范

泛型方法可以重载,为特定的类型定义规范。这也适用于带泛型参数的方法。Foo()方法定义了4个版本,第1个版本接受一个泛型参数,第2个版本是用于int参数的专用版本。第3个Foo方法接受两个泛型参数,第4个版本是第3个版本的专用版本,其第一个参数是int类型。在编译期间,会使用最佳匹配。如果传递了一个int,就选择带int参数的方法。对于任何其他参数类型,编译器会选择方法的泛型版本(代码文件Specialization/Program.cs):

        public class MethodOverloads
        {
          public void Foo<T>(T obj)
          {
            WriteLine($"Foo<T>(T obj), obj type: {obj.GetType().Name}");
          }
          public void Foo(int x)
          {
            WriteLine("Foo(int x)");
          }
          public void Foo<T1, T2>(T1 obj1, T2 obj2)
          {
            WriteLine($"Foo<T1, T2>(T1 obj1, T2 obj2); {obj1.GetType().Name} " +
              $"{obj2.GetType().Name}");
          }
          public void Foo<T>(int obj1, T obj2)
          {
            WriteLine($"Foo<T>(int obj1, T obj2); {obj2.GetType().Name}");
          }
          public void Bar<T>(T obj)
          {
            Foo(obj);
          }
        }

Foo()方法现在可以通过任意参数类型来调用。下面的示例代码传递了int和string值,调用所有4个Foo方法:

        static void Main()
        {
          var test = new MethodOverloads();
          test.Foo(33);
          test.Foo("abc");
          test.Foo("abc", 42);
          test.Foo(33, "abc");
        }

运行该程序,可以从输出中看出选择了最佳匹配的方法:

        Foo(int x)
        Foo<T>(T obj), obj type: String
        Foo<T1, T2>(T1 obj1, T2 obj2); String Int32
        Foo<T>(int obj1, T obj2); String

需要注意的是,所调用的方法是在编译期间而不是运行期间定义的。这很容易举例说明:添加一个调用Foo()方法的Bar()泛型方法,并传递泛型参数值:

        public class MethodOverloads
        {
          // ...
          public void Bar<T>(T obj)
          {
            Foo(obj);
          }

Main()方法现在改为调用传递一个int值的Bar()方法:

        static void Main()
        {
          var test = new MethodOverloads();
          test.Bar(44);

从控制台的输出可以看出,Bar()方法选择了泛型Foo()方法,而不是用int参数重载的Foo()方法。原因是编译器是在编译期间选择Bar()方法调用的Foo()方法。由于Bar()方法定义了一个泛型参数,而且泛型Foo()方法匹配这个类型,所以调用了Foo()方法。在运行期间给Bar()方法传递一个int值不会改变这一点。

        Foo<T>(T obj), obj type: Int32