1、协变和抗变
- 泛型接口的协变
如果泛型类型用 out 关键字标注,泛型接口就是协变的。这也意味着返回类型只能是 T。 接口IIndex 与类型T 是协变的,并从一个制度索引器中返回这个类型。
-
1 public interface IIndex<out T> 2 { 3 T this[int index]{ get; } 4 int Count{ get; } 5 }
如果对接口IIndex 使用了读写索引器,就把泛型类型T 传递给方法,并从方法中检索这个类型。这不能通过协变来实现—泛型类型必须定义为不变的。不使用out 和 in 标注,就可以把类型定义为不变的。
- 泛型接口的抗变
如果泛型类型用 in 关键字标注,泛型接口就是抗变的。这样,接口只能把泛型类型 T 用作其方法的输入。
-
1 public interface IDisplay<in T> 2 { 3 void Show(T item); 4 }
2、因为可空类型使用得非常频繁,所以 C# 有一种特殊的语法,它用于定义可空类型的变量。定义这类变量时,不适用泛型结构的语法,而是用 “?”运算符。在下面的李子中,变量 x1和x2 都是可空的int 类型的实例:
-
Nullable <int> x1; int ?x2;
可空类型可以与null 和数字比较,如上所示。这里,x的值与null 比较,如果x 不是 null,它就与小于 0的值比较:
-
1 int ? x =GetNullableType(); 2 if(x==null) 3 { 4 Console.WriteLine("x id null"); 5 } 6 else if(x < 0) 7 { 8 Console.WriteLine("x is smaller than 0"); 9 }
知道了 Nullable<T> 是如何定义的之后,下面就使用可空类型。可空类型还可以与算术运算符一起使用。变量 x3 是 x1 和x2 的和。 如果这两个可空变量中任何一个的值是 null ,他们的和就是 null
-
1 int ? x1=GetNullableType(); 2 int ? x2=GetNullableType(); 3 int ? x3=x1+x2;
!这里调用的 GetNullableThype() 只是一个占位符,它对于任何方法都返回一个可空的 int 。
3、非可空类型可以转换为可空类型。从非可空类型转换为可空类型时,在不需要强制类型转换的地方可以进行隐式转换。
-
1 int y1=4; 2 int ? x1=y1;
但从可空类型转换为非可空类型可能会失败。如果可空类型的值是null,并且把null 值赋予非可空类型,就会抛出InvalidOperationException 类型的异常。这就是需要类型强制转换运算符进行显示转换的原因:
-
int ? x1=GetNullableType(); int y1=(int) x1;
如果不进行显示类型转换,还可以使用合并运算符从可空类型转换为非可空类型,合并运算符的语法是“??”,为转换定义了一个默认值,以防可空类型的值是null。这里,如果 x1 是 null,y1的值就是 0
-
int ? x1 = GetNullableType(); int y1 = x1 ?? 0;
4、泛型方法:
除了定义泛型类之外,还可以定义泛型方法。在泛型犯法中,泛型类型用方法声明来定义。泛型方法可以在非泛型类中定义。
-
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); //或者 Swap(ref i,ref j);
泛型方法实例,下面的例子使用泛型方法累加集合中的所有元素。为了说明泛型方法的功能,下面使用包含Name 和Balance 属性的Account 类
-
1 public class Account 2 { 3 public string Name{ get; private set;} 4 public decimal Balance{get; private set;} 5 6 public Account(string name,Decimal balance) 7 { 8 this.Name=name; 9 this.Balance=balance; 10 } 11 }
其中应累加余额的所有账户操作都添加到 List<Account> 类型的账户列表中
-
1 var accounts=new List<Account>() 2 { 3 new Account("Christian",1500), 4 new Account("Stephanie",2200), 5 new Account("Angela",1800), 6 new Account("Matthias",2400) 7 };
累加所有Account对象的传统方式是用foreach 语句遍历所有的Account 对象,如下所示,foreach 语句使用 IEnumerable 接口迭代集合的元素,所以 AccumulateSimple() 方法的参数是 IEnumerable类型。foreach语句处理实现 IEunmerable 接口的每个对象。这样,AccumulateSimple() 方法就可以用于所有实现 IEnumerable<Account> 接口的集合类。在这个方法的实现代码中,直接访问Account 对象的 Balance 属性。
-
1 public static class Algorithm 2 { 3 public static decimal AccumulateSimple(IEnumerable<Account> source) 4 { 5 decimal sum=0; 6 foreach(Account a in source) 7 { 8 sum+=a.Balance; 9 } 10 return sum; 11 } 12 }
调用如下:
-
decimal amount=Algorthm.AccumulateSimple(accounts);
带约束的泛型方法
第一个实现代码的问题是,它只能用于Account 对象。使用泛型方法就可以避免这个问题。
Accumulate() 方法的第二个版本接受实现了 IAccount 接口的任意类型。如前面的泛型类所述,泛型类型可以用where 子句来限制。
-
-
1 public static decimal Accmulate<TAccount>(IEnumerable<TAccount> source) where TAccount : IAccount 2 { 3 decimal sum=0; 4 5 foreach(TAccount a in source) 6 { 7 sum+= a.Balance; 8 } 9 return sum; 10 }
重构Account 类实现 IAccount 接口
-
public class Account : IAccount { //...
IAccount 接口定义了只读属性 Balance 和 Name
-
1 public interface IAccount 2 { 3 decimal Balance {get; } 4 string Name{get ;} 5 }
新的调用方式:
-
decimal amount =Algorthim.Accmulate<Account>(accounts); //或者 decimail amount =Alhorthim.Accmulate(accounts);
-
带委托的泛型方法
泛型类型实现了 IAccount 接口的要求过于严格。下面的示例提示了,如何通过传递一个泛型委托来修改 Accumulate()方法。这个Accumulate() 方法使用两个泛型参数 T1 和 T2。第一个参数T1 用于实现了IEnumerable<T1> 参数的集合,第二个参数使用泛型委托Fun<T1, T2, TResult>。 其中, 第二个和第三个泛型参数都是 T2 类型。需要传递的方法有两个输入参数(T1 和 T2)和一个 T2类型的返回值。
-
-
1 public static T2 Accmulate<T1,T2>(IEnumerable<T1> source, Func<T1,T2,T2> action) 2 { 3 T2 sum=default(T2); 4 foreach(T1 item in source) 5 { 6 sum=action(item,sum); 7 } 8 return sum; 9 }
在调用这个方法时,需要指定泛型参数类型,因为编译器不能自动推断出该类型。对于方法的第一个参数,所赋予的accounts集合是IEnumerable<Account> 类型。对于第二个参数,使用一个 Lambda 表达式来定义Account 和 decimal 类型的两个参数,返回一个小数。对于每一项通过Accumulate() 方法调用这个 Lambda 表达式
-
decimal amount=Algorithm.Accumulate<Account,decimal>(accounts,(item,sum)=>sum+=item.Balance);
-
泛型方法规范
泛型方法可以重载,为特定的类型定义规范。这也适用于带泛型参数的方法。Foo()方法定义了两个版本,第一个版本接受一个泛型参数,第二个版本是用于int 参数的专有版本。在编译期间,会使用最佳匹配。如果传递了一个int,就选择带int 参数的方法。对于任何其他参数类型,编译器会选择方法的泛型版本
-
-
1 public class MethodOverloads 2 { 3 public void Foo<T>(T obj) 4 { 5 Console.WriteLine("Foo<T>(T obj),obj type :{0}",obj.GetType().Name); 6 } 7 public void Foo(int x) 8 { 9 Console.WriteLine("Foo(int x)"); 10 } 11 public void Bar<T>(T obj) 12 { 13 Foo(obj); 14 } 15 }
Foo()方法现在可以通过任意参数类型来调用。下面的示例代码给该方法传递了一个 int 和一个 string
-
1 static void Main() 2 { 3 var test =new MethodOverloads(); 4 test.Foo(33); 5 test.Foo("abc"); 6 }
运行该程序,可以从输出中看出选择了最佳匹配的方法:
-
Foo(int x) Foo<T>(T obj), obj type: String
需要注意的是,所调用的方法是在编译期间定义的,而不是运行期间。这很容易举例说明:添加一个调用Foo() 方法的Bar() 泛型方法,并传递泛型参数值:
-
public class MethodOverloads { //... public void Bar<T> (T obj) { Foo(obj); } }
Main()方法现在改为调用传递以个 int 值 的Bar()方法:
-
static void Main() { vat test=new MethodOverloads(); test.Bar(44);
从控制台的输出可以看出,Bar()方法选择了泛型Foo()方法,而不是用 int参数重载的Foo()方法。原因是编译器是在编译期间选择Bar() 方法调用的Foo() 方法。由于Bar() 方法定义了一个泛型参数,而且泛型Foo()方法匹配这个类型,所以调用了Foo() 方法。在运行期间给Bar() 方法传递一个 int 值不会改变这一点。
-
Foo<T>(T obj), obj type: Int32
-
【小结】:泛型。通过泛型类可以创建独立于类型的类,泛型方法是独立于类型的方法。接口、结构、和委托也可以用泛型的方式创建。泛型引入了一种新的编程方式。我们介绍了如何实现相应的算法(尤其是操作和谓词)以用于不同的类,而且他们呢都是类型安全的。泛型委托可以去除集合中的算法。