现在的位置: 首页 > 综合 > 正文

C# 泛型简介 2

2014年02月26日 ⁄ 综合 ⁄ 共 5706字 ⁄ 字号 评论关闭

一般约束

使用 C# 泛型,编译器会将一般代码编译为 IL,而不管客户端将使用什么样的类型实参。因此,一般代码可以尝试使用与客户端使用的特定类型实参不兼容的一般类型参数的方法、属性或成员。这是不可接受的,因为它相当于缺少类型安全。在 C# 中,您需要通知编译器客户端指定的类型必须遵守哪些约束,以便使它们能够取代一般类型参数而得到使用。存在三个类型的约束。派生约束指示编译器一般类型参数派生自诸如接口或特定基类之类的基类型。默认构造函数约束指示编译器一般类型参数公开了默认的公共构造函数(不带任何参数的公共构造函数)。引用/值类型约束将一般类型参数约束为引用类型或值类型。一般类型可以利用多个约束,您甚至可以在使用一般类型参数时使 IntelliSense 反射这些约束,例如,建议基类型中的方法或成员。

需要注意的是,尽管约束是可选的,但它们在开发一般类型时通常是必不可少的。没有它们,编译器将采取更为保守的类型安全方法,并且只允许在一般类型参数中访问 Object 级别功能。约束是一般类型元数据的一部分,以便客户端编译器也可以利用它们。客户端编译器只允许客户端开发人员使用遵守这些约束的类型,从而实施类型安全。

以下示例将详细说明约束的需要和用法。假设您要向代码块 3 的链表中添加索引功能或按键搜索功能:

public class LinkedList
{
   T Find(K key)
   {...}
   public T this[K key]
   {
      get{return Find(key);}
   }
}

这使客户端可以编写以下代码:

LinkedList list = new LinkedList();

list.AddHead(123,"AAA");
list.AddHead(456,"BBB");
string item = list[456];
Debug.Assert(item == "BBB");

要实现搜索,您需要扫描列表,将每个节点的键与您要查找的键进行比较,并且返回键匹配的节点的项。问题在于,Find() 的以下实现无法编译:

T Find(K key)
{
   Node current = m_Head;
   while(current.NextNode != null)
   {
      if(current.Key == key) //Will not compile
         break;
      else
         
         current = current.NextNode;
   }
   return current.Item; 
}

原因在于,编译器将拒绝编译以下行:

if(current.Key == key)

上述行将无法编译,因为编译器不知道 K(或客户端提供的实际类型)是否支持 == 运算符。例如,默认情况下,结构不提供这样的实现。您可以尝试通过使用 IComparable 接口来克服 == 运算符局限性:

public interface IComparable 
{
   int CompareTo(object obj);
}

如果您与之进行比较的对象等于实现该接口的对象,则 CompareTo() 返回 0;因此,Find() 方法可以按如下方式使用它:

if(current.Key.CompareTo(key) == 0)

遗憾的是,这也无法编译,因为编译器无法知道 K(或客户端提供的实际类型)是否派生自 IComparable

您可以显式强制转换到 IComparable,以强迫编译器编译比较行,除非这样做需要牺牲类型安全:

if(((IComparable)(current.Key)).CompareTo(key) == 0)

如果客户端使用的类型不是派生自 IComparable,则会导致运行时异常。此外,当所使用的键类型是值类型而非键类型参数时,您可以对该键执行装箱,而这可能具有一些性能方面的影响。

派生约束

在 C# 2.0 中,可以使用 where 保留关键字来定义约束。在一般类型参数中使用 where 关键字,后面跟一个派生冒号,以指示编译器该一般类型参数实现了特定接口。例如,以下为实现 LinkedList 的 Find() 方法所必需的派生约束:

public class LinkedList where K : IComparable
{
   T Find(K key)
   {
      Node current = m_Head;
      while(current.NextNode != null)
      {
         if(current.Key.CompareTo(key) == 0)
            
            break;
         else      
            
            current = current.NextNode;
      }
      return current.Item; 
   }
   //Rest of the implementation 
}

您还将在您约束的接口的方法上获得 IntelliSense 支持。

当客户端声明一个 LinkedList 类型的变量,以便为列表的键提供类型实参时,客户端编译器将坚持要求键类型派生自 IComparable,否则,将拒绝生成客户端代码。

请注意,即使该约束允许您使用 IComparable,它也不会在所使用的键是值类型(例如,整型)时,消除装箱所带来的性能损失。为了克服该问题,System.Collections.Generic 命名空间定义了一般接口 IComparable

public interface IComparable 
{
   int CompareTo(T other);
   bool Equals(T other);
}

您可以约束键类型参数以支持 IComparable,并且使用键的类型作为类型参数;这样,您不仅获得了类型安全,而且消除了在值类型用作键时的装箱操作:

public class LinkedList where K : IComparable
{...}

实际上,所有支持 .NET 1.1 中的 IComparable 的类型都支持 .NET 2.0 中的 IComparable。这使得可以使用常见类型(例如,int、string、GUID、DateTime 等等)的键。

在 C# 2.0 中,所有约束都必须出现在一般类的实际派生列表之后。例如,如果 LinkedList 派生自 IEnumerable 接口(以获得迭代器支持),则需要将 where 关键字放在紧跟它后面的位置:

public class LinkedList : IEnumerable where K : IComparable
{...}

通常,只须在需要的级别定义约束。在链表示例中,在节点级别定义 IComparable 派生约束是没有意义的,因为节点本身不会比较键。如果您这样做,则您还必须将该约束放在 LinkedList 级别,即使该列表不比较键。这是因为该列表包含一个节点作为成员变量,从而导致编译器坚持要求:在列表级别定义的键类型必须遵守该节点在一般键类型上放置的约束。

换句话说,如果您按如下方式定义该节点:

class Node where K : IComparable
{...}

则您必须在列表级别重复该约束,即使您不提供 Find() 方法或其他任何与此有关的方法:

public class LinkedList where KeyType : IComparable
{
   Node<KeyType,DataType> m_Head;
}

您可以在同一个一般类型参数上约束多个接口(彼此用逗号分隔)。例如:

public class LinkedList where K : IComparable,IConvertible
{...}

您可以为您的类使用的每个一般类型参数提供约束,例如:

public class LinkedList where K : IComparable
                             where T : ICloneable 
{...}

您可以具有一个基类约束,这意味着规定一般类型参数派生自特定的基类:

public class MyBaseClass
{...}
public class LinkedList where K : MyBaseClass
{...}

但是,在一个约束中最多只能使用一个基类,这是因为 C# 不支持实现的多重继承。显然,您约束的基类不能是密封类或静态类,并且由编译器实施这一限制。此外,您不能将 System.DelegateSystem.Array 约束为基类。

您可以同时约束一个基类以及一个或多个接口,但是该基类必须首先出现在派生约束列表中:

public class LinkedList where K : MyBaseClass, IComparable
{...}

C# 确实允许您将另一个一般类型参数指定为约束:

public class MyClass where T : U 
{...}

在处理派生约束时,您可以通过使用基类型本身来满足该约束,而不必非要使用它的严格子类。例如:

public interface IMyInterface
{...}
public class MyClass where T : IMyInterface
{...}
MyClass obj = new MyClass();

或者,您甚至可以:

public class MyOtherClass
{...}

public class MyClass where T : MyOtherClass 
{...}

MyClass obj = new MyClass();

最后,请注意,在提供派生约束时,您约束的基类型(接口或基类)必须与您定义的一般类型参数具有一致的可见性。例如,以下约束是有效的,因为内部类型可以使用公共类型:

public class MyBaseClass
{}
internal class MySubClass where T : MyBaseClass
{}
但是,如果这两个类的可见性被颠倒,例如:
internal class MyBaseClass
{}
public class MySubClass where T : MyBaseClass
{}

则编译器会发出错误,因为程序集外部的任何客户端都无法使用一般类型 MySubClass,从而使得 MySubClass 实际上成为内部类型而不是公共类型。外部客户端无法使用 MySubClass 的原因是,要声明 MySubClass 类型的变量,它们需要使用派生自内部类型 MyBaseClass 的类型。

构造函数约束

假设您要在一般类的内部实例化一个新的一般对象。问题在于,C# 编译器不知道客户端将使用的类型实参是否具有匹配的构造函数,因而它将拒绝编译实例化行。

为了解决该问题,C# 允许约束一般类型参数,以使其必须支持公共默认构造函数。这是使用 new() 约束完成的。例如,以下是一种实现代码块 3 中的一般 Node 的默认构造函数的不同方式。

class Node where T : new() 
{
   public K Key;
   public T Item;
   public Node NextNode;
   public Node()
   {
      Key      = default(K);
      Item     = new T();
      NextNode = null;
   }
}

可以将构造函数约束与派生约束组合起来,前提是构造函数约束出现在约束列表中的最后:

public class LinkedList where K : IComparable,new() 
{...}

引用/值类型约束

可以使用 struct 约束将一般类型参数约束为值类型(例如,int、bool 和 enum),或任何自定义结构:

public class MyClass where T : struct 

{...}

同样,可以使用 class 约束将一般类型参数约束为引用类型(类):

public class MyClass where T : class 

{...}

不能将引用/值类型约束与基类约束一起使用,因为基类约束涉及到类。同样,不能使用结构和默认构造函数约束,因为默认构造函数约束也涉及到类。虽然您可以使用类和默认构造函数约束,但这样做没有任何价值。可以将引用/值类型约束与接口约束组合起来,前提是引用/值类型约束出现在约束列表的开头。

泛型和强制类型转换

C# 编译器只允许将一般类型参数隐式强制转换到 Object 或约束指定的类型,如代码块 5 所示。这样的隐式强制类型转换是类型安全的,因为可以在编译时发现任何不兼容性。

代码块 5. 一般类型参数的隐式强制类型转换

interface ISomeInterface
{...}
class BaseClass
{...}
class MyClass where T : BaseClass,ISomeInterface
{
   void SomeMethod(T t)
   {
      ISomeInterface obj1 = t;
      BaseClass      obj2 = t;
      object         obj3 = t;
   }
}

编译器允许您将一般类型参数显式强制转换到其他任何接口,但不能将其转换到类:

interface ISomeInterface
{...}
class SomeClass
{...}
class MyClass 
{
   void SomeMethod(T t)
   {
      ISomeInterface obj1 = (ISomeInterface)t;//Compiles
      SomeClass      obj2 = (SomeClass)t;     //Does not compile
   }
}

但是,您可以使用临时的 Object 变量,将一般类型参数强制转换到其他任何类型:

class SomeClass
{...}

class MyClass 
{
   
   void SomeMethod(T t)
   
   {
      object temp = t;
      SomeClass obj = (SomeClass)temp;
   
   }
}

不用说,这样的显式强制类型转换是危险的,因为如果为取代一般类型参数而使用的类型实参不是派生自您要显式强制转换到的类型,则可能在运行时引发异常。要想不冒引发强制类型转换异常的危险,一种更好的办法是使用 is 和 as 运算符,如代码块 6 所示。如果一般类型参数的类型是所查询的类型,则 is 运算符返回 true;如果这些类型兼容,则 as 将执行强制类型转换,否则将返回 null。您可以对一般类型参数以及带有特定类型实参的一般类使用 is 和 as。

代码块 6. 对一般类型参数使用“is”和“as”运算符

public class MyClass 
{
   public void SomeMethod(T t)
   {
      if(t is int)
      {...} 

      if(t is LinkedList)
      {...}

      string str = t as string;
      if(str != null)
      {...}

      LinkedList list = t as LinkedList;
      if(list != null)
      {...}
   }
}

抱歉!评论已关闭.