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