C# 泛型简介
发布日期: 5/30/2005 | 更新日期: 5/30/2005
Juval Lowy
IDesign
摘要:本文讨论泛型处理的问题空间、它们的实现方式、该编程模型的好处,以及独特的创新(例如,约束、一般方法和委托以及一般继承)。此外,本文还讨论 .NET Framework 如何利用泛型。
简介
泛型是 C# 2.0 的最强大的功能。通过泛型可以定义类型安全的数据结构,而无须使用实际的数据类型。这能够显著提高性能并得到更高质量的代码,因为您可以重用数据处理算法,而无须复制类型特定的代码。在概念上,泛型类似于 C++ 模板,但是在实现和功能方面存在明显差异。本文讨论泛型处理的问题空间、它们的实现方式、该编程模型的好处,以及独特的创新(例如,约束、一般方法和委托以及一般继承)。您还将了解在 .NET Framework 的其他领域(例如,反射、数组、集合、序列化和远程处理)中如何利用泛型,以及如何在所提供的基本功能的基础上进行改进。
泛型问题陈述
考虑一种普通的、提供传统Push()和Pop()方法的数据结构(例如,堆栈)。在开发通用堆栈时,您可能愿意使用它来存储各种类型的实例。在 C# 1.1 下,您必须使用基于 Object 的堆栈,这意味着,在该堆栈中使用的内部数据类型是难以归类的 Object,并且堆栈方法与 Object 交互:
public class Stack
{
object[] m_Items;
public void Push(object item)
{...}
public object Pop()
{...}
}
代码块 1显示基于 Object 的堆栈的完整实现。因为 Object 是规范的 .NET 基类型,所以您可以使用基于 Object 的堆栈来保持任何类型的项(例如,整数):
Stack stack = new Stack();
stack.Push(1);
stack.Push(2);
int number = (int)stack.Pop();
代码块 1. 基于 Object 的堆栈
public class Stack
{
readonly int m_Size;
int m_StackPointer = 0;
object[] m_Items;
public Stack():this(100)
{}
public Stack(int size)
{
m_Size = size;
m_Items = new object[m_Size];
}
public void Push(object item)
{
if(m_StackPointer >= m_Size)
throw new StackOverflowException();
m_Items[m_StackPointer] = item;
m_StackPointer++;
}
public object Pop()
{
m_StackPointer--;
if(m_StackPointer >= 0)
{
return m_Items[m_StackPointer];
}
else
{
m_StackPointer = 0;
throw new InvalidOperationException("Cannot pop an empty stack");
}
}
}
但是,基于 Object 的解决方案存在两个问题。第一个问题是性能。在使用值类型时,必须将它们装箱以便推送和存储它们,并且在将值类型弹出堆栈时将其取消装箱。装箱和取消装箱都会根据它们自己的权限造成重大的性能损失,但是它还会增加托管堆上的压力,导致更多的垃圾收集工作,而这对于性能而言也不太好。即使是在使用引用类型而不是值类型时,仍然存在性能损失,这是因为必须从 Object 向您要与之交互的实际类型进行强制类型转换,从而造成强制类型转换开销:
Stack stack = new Stack();
stack.Push("1");
string number = (string)stack.Pop();
基于 Object 的解决方案的第二个问题(通常更为严重)是类型安全。因为编译器允许在任何类型和 Object 之间进行强制类型转换,所以您将丢失编译时类型安全。例如,以下代码可以正确编译,但是在运行时将引发无效强制类型转换异常:
Stack stack = new Stack();
stack.Push(1);
//This compiles, but is not type safe, and will throw an exception: string number = (string)stack.Pop();
您可以通过提供类型特定的(因而是类型安全的)高性能堆栈来克服上述两个问题。对于整型,可以实现并使用IntStack:
public class IntStack
{
int[] m_Items;
public void Push(int item){...}
public int Pop(){...}
}
IntStack stack = new IntStack();
stack.Push(1);
int number = stack.Pop();
对于字符串,可以实现StringStack:
public class StringStack
{
string[] m_Items;
public void Push(string item){...}
public string Pop(){...}
}
StringStack stack = new StringStack();
stack.Push("1");
string number = stack.Pop();
等等。遗憾的是,以这种方式解决性能和类型安全问题,会引起第三个同样严重的问题—影响工作效率。编写类型特定的数据结构是一项乏味的、重复性的且易于出错的任务。在修复该数据结构中的缺陷时,您不能只在一个位置修复该缺陷,而必须在实质上是同一数据结构的类型特定的副本所出现的每个位置进行修复。此外,没有办法预知未知的或尚未定义的将来类型的使用情况,因此还必须保持基于 Object 的数据结构。结果,大多数 C# 1.1 开发人员发现类型特定的数据结构不实用,并且选择使用基于 Object 的数据结构,尽管它们存在缺点。
什么是泛型
通过泛型可以定义类型安全类,而不会损害类型安全、性能或工作效率。您只须一次性地将服务器实现为一般服务器,同时可以用任何类型来声明和使用它。为此,需要使用<和>括号,以便将一般类型参数括起来。例如,可以按如下方式定义和使用一般堆栈:
public class Stack
{
T[] m_Items;
public void Push(T item)
{...}
public T Pop()
{...}
}
Stack stack = new Stack();
stack.Push(1);
stack.Push(2);
int number = stack.Pop();
代码块 2显示一般堆栈的完整实现。将代码块 1与代码块 2进行比较,您会看到,好像代码块 1中每个使用 Object 的地方在代码块 2中都被替换成了T,除了使用一般类型参数 T 定义 Stack 以外:
public class Stack
{...}
在使用一般堆栈时,必须通知编译器使用哪个类型来代替一般类型参数 T(无论是在声明变量时,还是在实例化变量时):
Stack stack = new Stack();
编译器和运行库负责完成其余工作。所有接受或返回 T 的方法(或属性)都将改为使用指定的类型(在上述示例中为整型)。
代码块 2. 一般堆栈
public class Stack
{
readonly int m_Size;
int m_StackPointer = 0;
T[] m_Items;
public Stack():this(100)
{}
public Stack(int size)
{
m_Size = size;
m_Items = new T[m_Size];
}
public void Push(T item)
{
if(m_StackPointer >= m_Size)
throw new StackOverflowException();
m_Items[m_StackPointer] = item;
m_StackPointer++;
}
public T Pop()
{
m_StackPointer--;
if(m_StackPointer >= 0)
{
return m_Items[m_StackPointer];
}
else
{
m_StackPointer = 0;
throw new InvalidOperationException("Cannot pop an empty stack");
}
}
}
注T 是一般类型参数(或类型参数),而一般类型为 Stack。Stack 中的 int 为类型实参。
该编程模型的优点在于,内部算法和数据操作保持不变,而实际数据类型可以基于客户端使用服务器代码的方式进行更改。
表面上,C# 泛型的语法看起来与 C++ 模板类似,但是编译器实现和支持它们的方式存在重要差异。正如您将在后文中看到的那样,这对于泛型的使用方式具有重大意义。
注在本文中,当提到 C++ 时,指的是传统 C++,而不是带有托管扩展的Microsoft C++。
与 C++ 模板相比,C# 泛型可以提供增强的安全性,但是在功能方面也受到某种程度的限制。
在一些 C++ 编译器中,在您通过特定类型使用模板类之前,编译器甚至不会编译模板代码。当您确实指定了类型时,编译器会以内联方式插入代码,并且将每个出现一般类型参数的地方替换为指定的类型。此外,每当您使用特定类型时,编译器都会插入特定于该类型的代码,而不管您是否已经在应用程序中的其他某个位置为模板类指定了该类型。C++ 链接器负责解决该问题,并且并不总是有效。这可能会导致代码膨胀,从而增加加载时间和内存足迹。
在 .NET 2.0 中,泛型在 IL(中间语言)和 CLR 本身中具有本机支持。在编译一般 C# 服务器端代码时,编译器会将其编译为 IL,就像其他任何类型一样。但是,IL 只包含实际特定类型的参数或占位符。此外,一般服务器的元数据包含一般信息。
客户端编译器使用该一般元数据来支持类型安全。当客户端提供特定类型而不是一般类型参数时,客户端的编译器将用指定的类型实参来替换服务器元数据中的一般类型参数。这会向客户端的编译器提供类型特定的服务器定义,就好像从未涉及到泛型一样。这样,客户端编译器就可以确保方法参数的正确性,实施类型安全检查,甚至执行类型特定的 IntelliSense。
有趣的问题是,.NET 如何将服务器的一般 IL 编译为机器码。原来,所产生的实际机器码取决于指定的类型是值类型还是引用类型。如果客户端指定值类型,则 JIT 编译器将 IL 中的一般类型参数替换为特定的值类型,并且将其编译为本机代码。但是,JIT 编译器会跟踪它已经生成的类型特定的服务器代码。如果请求 JIT 编译器用它已经编译为机器码的值类型编译一般服务器,则它只是返回对该服务器代码的引用。因为 JIT 编译器在以后的所有场合中都将使用相同的值类型特定的服务器代码,所以不存在代码膨胀问题。
如果客户端指定引用类型,则 JIT 编译器将服务器 IL 中的一般参数替换为Object,并将其编译为本机代码。在以后的任何针对引用类型而不是一般类型参数的请求中,都将使用该代码。请注意,采用这种方式,JIT 编译器只会重新使用实际代码。实例仍然按照它们离开托管堆的大小分配空间,并且没有强制类型转换。
.NET 中的泛型使您可以重用代码以及在实现它时付出的努力。类型和内部数据可以在不导致代码膨胀的情况下更改,而不管您使用的是值类型还是引用类型。您可以一次性地开发、测试和部署代码,通过任何类型(包括将来的类型)来重用它,并且全部具有编译器支持和类型安全。因为一般代码不会强行对值类型进行装箱和取消装箱,或者对引用类型进行向下强制类型转换,所以性能得到显著提高。对于值类型,性能通常会提高 200%;对于引用类型,在访问该类型时,可以预期性能最多提高 100%(当然,整个应用程序的性能可能会提高,也可能不会提高)。本文随附的源代码包含一个微型基准应用程序,它在紧密循环中执行堆栈。该应用程序使您可以在基于 Object 的堆栈和一般堆栈上试验值类型和引用类型,以及更改循环迭代的次数以查看泛型对性能产生的影响。
应用泛型
因为 IL 和 CLR 为泛型提供本机支持,所以大多数符合 CLR 的语言都可以利用一般类型。例如,下面这段 Visual Basic .NET 代码使用代码块 2的一般堆栈:
Dim stack As Stack(Of Integer)
stack = new Stack(Of Integer)
stack.Push(3)
Dim number As Integer
number = stack.Pop()
您可以在类和结构中使用泛型。以下是一个有用的一般点结构:
public struct Point
{
public T X;
public T Y;
}
可以使用该一般点来表示整数坐标,例如:
Point point;
point.X = 1;
point.Y = 2;
或者,可以使用它来表示要求浮点精度的图表坐标:
Point point;
point.X = 1.2;
point.Y = 3.4;
除了到目前为止介绍的基本泛型语法以外,C# 2.0 还具有一些泛型特定的语法。例如,请考虑代码块 2的Pop()方法。假设您不希望在堆栈为空时引发异常,而是希望返回堆栈中存储的类型的默认值。如果您使用基于 Object 的堆栈,则可以简单地返回 null,但是您还可以通过值类型来使用一般堆栈。为了解决该问题,您可以使用default()运算符,它返回类型的默认值。
下面说明如何在Pop()方法的实现中使用默认值:
public T Pop()
{
m_StackPointer--;
if(m_StackPointer >= 0)
{
return m_Items[m_StackPointer];
}
else
{
m_StackPointer = 0;
return default(T);
}
}
引用类型的默认值为 null,而值类型(例如,整型、枚举和结构)的默认值为全零(用零填充相应的结构)。因此,如果堆栈是用字符串构建的,则Pop()方法在堆栈为空时返回 null;如果堆栈是用整数构建的,则Pop()方法在堆栈为空时返回零。
多个一般类型
单个类型可以定义多个一般类型参数。例如,请考虑代码块 3中显示的一般链表。
代码块 3. 一般链表
class Node
{
public K Key;
public T Item;
public Node NextNode;
public Node()
{
Key = default(K);
Item = defualt(T);
NextNode = null;
}
public Node(K key,T item,Node nextNode)
{
Key = key;
Item = item;
NextNode = nextNode;
}
}
public class LinkedList
{
Node m_Head;
public LinkedList()
{
m_Head = new Node();
}
public void AddHead(K key,T item)
{
Node newNode = new Node(key,item,m_Head.NextNode);
m_Head.NextNode = newNode;
}
}
该链表存储节点:
class Node
{...}
每个节点都包含一个键(属于一般类型参数 K)和一个值(属于一般类型参数 T)。每个节点还具有对该列表中下一个节点的引用。链表本身根据一般类型参数 K 和 T 进行定义:
public class LinkedList
{...}
这使该列表可以公开像AddHead()一样的一般方法:
public void AddHead(K key,T item);
每当您声明使用泛型的类型的变量时,都必须指定要使用的类型。但是,指定的类型实参本身可以为一般类型参数。例如,该链表具有一个名为 m_Head 的 Node 类型的成员变量,用于引用该列表中的第一个项。m_Head 是使用该列表自己的一般类型参数 K 和 T 声明的。
Node m_Head;
您需要在实例化节点时提供类型实参;同样,您可以使用该链表自己的一般类型参数:
public void AddHead(K key,T item)
{
Node newNode = new Node
m_Head.NextNode = newNode;
}
请注意,该列表使用与节点相同的名称来表示一般类型参数完全是为了提高可读性;它也可以使用其他名称,例如:
public class LinkedList
{...}
或:
public class LinkedList
{...}
在这种情况下,将 m_Head 声明为:
Node m_Head;
当客户端使用该链表时,该客户端必须提供类型实参。该客户端可以选择整数作为键,并且选择字符串作为数据项:
LinkedList list = new LinkedList();
list.AddHead(123,"AAA");
但是,该客户端可以选择其他任何组合(例如,时间戳)来表示键:
LinkedList list = new LinkedList();
list.AddHead(DateTime.Now,"AAA");
有时,为特定类型的特殊组合起别名是有用的。可以通过 using 语句完成该操作,如代码块 4中所示。请注意,别名的作用范围是文件的作用范围,因此您必须按照与使用 using 命名空间相同的方式,在项目文件中反复起别名。
代码块 4. 一般类型别名
using List = LinkedList;
class ListClient
{
static void Main(string[] args)
{
List list = new List();
list.AddHead(123,"AAA");
}
}
一般约束
使用 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(https://www.doczj.com/doc/1b15959974.html,pareTo(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(https://www.doczj.com/doc/1b15959974.html,pareTo(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
}
您可以在同一个一般类型参数上约束多个接口(彼此用逗号分隔)。例如:
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.Delegate或System.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)
{...}
}
}
继承和泛型
在从一般基类派生时,必须提供类型实参,而不是该基类的一般类型参数:
public class BaseClass
{...}
public class SubClass : BaseClass
{...}
如果子类是一般的而非具体的类型实参,则可以使用子类一般类型参数作为一般基类的指定类型:
public class SubClass : BaseClass
{...}
在使用子类一般类型参数时,必须在子类级别重复在基类级别规定的任何约束。例如,派生约束:
public class BaseClass where T : ISomeInterface
{...}
public class SubClass : BaseClass where T : ISomeInterface
{...}
或构造函数约束:
public class BaseClass where T : new()
{
public T SomeMethod()
{
return new T();
}
}
public class SubClass : BaseClass where T : new()
{...}
基类可以定义其签名使用一般类型参数的虚拟方法。在重写它们时,子类必须在方法签名中提供相应的类型:
public class BaseClass
{
public virtual T SomeMethod()
{...}
}
public class SubClass: BaseClass
{
public override int SomeMethod()
{...}
}
如果该子类是一般类型,则它还可以在重写时使用它自己的一般类型参数:
public class SubClass: BaseClass
{
public override T SomeMethod()
{...}
}
您可以定义一般接口、一般抽象类,甚至一般抽象方法。这些类型的行为像其他任何一般基类型一样:
public interface ISomeInterface
{
T SomeMethod(T t);
}
public abstract class BaseClass
{
public abstract T SomeMethod(T t);
}
public class SubClass : BaseClass
C#中所谓泛型:即通过参数化类型来实现在同一份代码上操作多种数据类型。泛型编程是一种编程范式,它利用“参数化类型”将类型抽象化,从而实现更为灵活的复用。 C#泛型赋予了代码更强的类型安全,更好的复用,更高的效率,更清晰的约束。 C#泛型机制简介 C#泛型能力由CLR在运行时支持,区别于C++的编译时模板机制,和java 的编译时的“搽拭法”。这使得泛型能力可以在各个支持CLR的语言之间进行无缝的互操作。 C#泛型代码在被编译为IL和元数据时,采用特殊的占位符来表示泛型类型,并用专有的IL指令支持泛型操作。而真正的泛型实例化工作以“on-demand”的方式,发生在JIT编译时。 C#泛型编译机制 第一轮编译时,编译器只为Stack类型产生“泛型版”的IL代码和元数据,并不进行泛型类型的实例化,T在中间只充当占位符。 JIT编译时,当JIT编译器第一次遇到Stack时,将用int类型替换“泛型版”IL代码与元数据中的T -- 进行泛型类型的实例化。 CLR为所有类型参数为“引用类型”的泛型类型产生同一份代码,但如果类型参数为“值类型”,对每一个不同的“值类型”,CLR将为其产生一份独立的代码。 C#泛型的几个特点 如果实例化泛型类型的参数相同,那么JIT编译器会重复使用该类型,因此C#的动态泛型能力避免了C++静态模板可能导致的代码膨胀的问题。 C#泛型类型携带有丰富的元数据,因此C#的泛型类型可以应用于强大的反射技术。 C#的泛型采用“基类、接口、构造器、值类型/引用类型”的约束方式来实现对类型参数的“显示约束”,提高了类型安全的同时,也丧失了C++模板基于“签名”的隐式约束所具有的高灵活性。 C#泛型类与结构 class C{} //合法 class D:C{} //合法
《面向对象程序设计》习题 第8章泛型编程 一、选择题(共40分,每题2分) 二、填空题(共20分,每空2分) 1. 逻辑数据类型 2. 函数类 3. 函数模板类模板 4.函数类型形参类型 5.2 6. 尖括号 三、下列程序有2个错,找出并修改(共6分) 错1:public: A(T a, b) // A(T a,T b) {x=a,y=b;s=x+y;} 错2: int main() { A add(10,100); // A
五、编程题(22分) 1.设计一个函数模板,实现两数的交换,并用int、float、double、char类型的数据进行测试。 #include
29 { 30return arr[index]; 31 } 32 } 33set 34 { 35if (index >= arr.Length) 36 { 37throw new System.Exception("数组下标越界 "); 38 } 39else 40 { 41 arr[index]=value; 42 } 43 } 44 } 45 } 调用: 1protected void Page_Load(object sender, EventArgs e) 2 { 3 MyList
前言 前几日同学告诉我他老师告诉他,泛型是定义一种通用的方法,至于为什么用泛型他的答 案还是定义一种通用的方法,当时我也没有学,还不懂啦,毕竟人家是计算机专业的科班生,我当 然没有发言权,只是怀疑的记下了这个错误的结论打算以后看看是不是对的,这两天算是把泛型学 了一下,其实它定义的不只是方法,自己感觉他就是一种规范代码执行效率的模版,这样极不容易 出错,并把我们常用的箱拆箱操作过程给省了,提高了编译器的执行效率,。至于为什么用泛型这 个我貌似已经说明白了,泛型多用于集合类操作,我在这里随意的拿出C#定义的ArrayList的 Add方法的定义我们一起看看: public virtual int Add( Object value ) 可以看出我们可以往ArrayList里添加任何类型的元素,假如有一个string类型的str字符 串它在添加到ArrayList lib的过程中实际进行了装箱操作,即object value=str;那么我 们在获取这个值得时候必须这样使用string s=(string)lib[0];不能遗漏(string)否则会提示你, 错误1无法将类型“object”隐式转换为“string”。存在一个显式转换(是否缺少强制转换?) 但是你假如这样写:int a=(int)lib[0];程序在运行前是无法报错的,只要一旦运行就会出 现异常,而这些错误都可以利用泛型轻松搞定,让你在编译时就可以发现错误,具体的就看你怎么 领悟了,我在文章里没有写任何定义的语法格式,因为这个既没必要说,这是常识,出来混这些迟 早是要明白的,呵呵。玩笑啦......欢迎新手同学来我的博客交流学习,我也希望你们可以为我敞 开心怀,因为我既不是计算机专业的学生,甚至上了大学我已经不再是理工类的学生。我无意扎进了 文科专业去叱咤风云啦,哎,这个文科专业名字太有点科学技术的味道了。我喜欢 ........ 我的博客地址是:https://www.doczj.com/doc/1b15959974.html,
c代码规范(总7页) -CAL-FENGHAI.-(YICAI)-Company One1 -CAL-本页仅作为文档封面,使用请直接删除
C# 代码规范 1、前言 本文档定义了一些通用的代码规范和准则,一般情况下本文档适用于项目组所有项目,特殊情况下,如果客户有自己的代码规范,以客户的代码优先。 2、大小写约定 2.1、大小写样式,本文中将出现两种大小写样式,这里先分别定义: Pascal大小写 将标识符的首字母和后面连接的每个单词的首字母都大写。可以对三字符或更多字符的标识符使用 Pascal 大小写。例如:BackColor Camel大小写 标识符的首字母小写,而每个后面连接的单词的首字母都大写。例如:backColor 匈牙利命名法 基本原则是:变量名=属性+类型+对象描述。例如:lblName 2.2、标识符大小写规则 2.2.1、下表中列出常见的代码元素的样式规范和示例 标识符规则示例 类Pascal AppDomain 枚举类型Pascal ErrorLevel 枚举值Pascal Warning 事件Pascal ValueChanging, ValueChanged 异常类Pascal WebException 只读的静态字段Pascal CurrentUser 接口Pascal IDisposable 方法Pascal ToString 命名空间Pascal 参数Camel typeName 属性Pascal Name 常量全大写MAXLENGTH, LENGTH_MAX Web或Win控件匈牙利txtName 2.2.2、除了遵循以上大小写约定外还需注意以下约定(除常量为特例): 如果标识符由多个单词组成,请不要在各单词之间使用分隔符,如下划线(“_”)或连字符(“-”)等。而应使用大小写来指示每个单词的开头。 所有公共的成员如:方法、属性,都应使用Pascal大小写样式 2.3、首缩写词的大小写规则 2.3.1、首字母缩写词 首字母缩写词是由术语或短语中各单词的首字母构成的单词。 例如,HTML 是 Hypertext Markup Language 的首字母缩写。为了方便编码规范的实施,本文规定受字母缩写词必须至少为两个单词,正好为两个单词的首字母缩写词称其为“短型首字母缩写词”,两个单词以上的称其为“长型首字母缩写词” 2.3.2、单缩写词
C/C++编程注意问题总结-模板和泛型编程-结构体注意-中文字符读 取-产生随机数 1.1.1 C++模板和泛型编程总结 ?函数模板:函数中有不确定的类型T(或成为模板类型) template
Java中的泛型 JDK1.5令我们期待很久,可是当他发布的时候却更换版本号为5.0。这说明Java已经有大幅度的变化。本文将讲解JDK5.0支持的新功能-----Java的泛型. 1、Java泛型 其实Java的泛型就是创建一个用类型作为参数的类。就象我们写类的方法一样,方法是这样的method(String str1,String str2 ),方法中参数str1、str2的值是可变的。而泛型也是一样的,这样写class Java_Generics<K,V>,这里边的K和V就象方法中的参数str1和str2,也是可变。下面看看例子: 正确输出:value 这只是个例子(Java中集合框架都泛型化了,这里费了2遍事.),不过看看是不是创建一个用类型作为参数的类,参数是K,V,传入的“值”是String类型。这个类他没有特定的待处理型别,以前我们定义好了一个类,在输入输入参数有所固定,是什么型别的有要求,但是现在编写程序,完全可以不制定参数的类型,具体用的时候来确定,增加了程序的通用性,像是一个模板。 呵呵,类似C++的模板(类似)。 1.1. 泛型通配符 下面我们先看看这些程序:
看看这个方法有没有异议,这个方法会通过编译的,假如你传入String,就是这样List <String>。 接着我们调用它,问题就出现了,我们将一个List<String>当作List传给了方法,JVM会给我们一个警告,说这个破坏了类型安全,因为从List中返回的都是Object类型的,而让我们再看看下面的方法。 因为这里的List<String>不是List<Object>的子类,不是String与Object的关系,就是说List<String>不隶属于list<Object>,他们不是继承关系,所以是不行的,这里的extends是表示限制的。 类型通配符是很神奇的,List<?>这个你能为他做什么呢?怎么都是“?”,它似乎不确定,他总不能返回一个?作为类型的数据吧,是啊他是不会返回一个“?”来问程序员的?JVM会做简单的思考的,看看代码吧,更直观些。 这段代码没问题的,l1.get(0)将返回一个Object。 1.2. 编写泛型类要注意: 1) 在定义一个泛型类的时候,在“<>”之间定义形式类型参数,例如:“class TestGen <K,V>”,其中“K” , “V”不代表值,而是表示类型。 2) 实例化泛型对象的时候,一定要在类名后面指定类型参数的值(类型),一共要有两次书写。例如: TestGen<String,String> t=new TestGen<String,String>();
实验七泛型 实验学时:2学时 实验类型:验证 实验要求:必做 一、实验目的 1.掌握泛型的基本使用; 2.声明和使用泛型方法 二、实验内容 实验1使用泛型List
Console.WriteLine("字符串型数组列表ArrayList2的内容如下:"); foreach (string s in list2) { Console.WriteLine(s); } Console.ReadLine(); } } } 运行结果: 实验2 声明和使用泛型方法 实验要求:参照课本例11.5,声明泛型方法Swap
基于泛型DAO的spring和hibernate的集成(转) 编写Spring+Hibernate框架下的应用,总是离不了编写一个通用的泛型GenericHibernateDao。查阅了网上不少的GenericHibernateDao实现,归纳整理为如下实现,供后续编码参考。 首先定义接口泛型DAO接口GenericDao package com.th.huz; import java.io.Serializable; import java.util.Collection; import java.util.Iterator; import java.util.List; import org.hibernate.Criteria; import org.hibernate.LockMode; import org.hibernate.criterion.DetachedCriteria; public interface GenericDao
C++模板函数 #include "iostream" using namespace std; intint_ab(inta,int b) { returna+a*b; } doubledouble_ab(double a,double b) { returna+a*b; } int main(){ cout<
泛型 1. 介绍 下面是那种典型用法: List myIntList = new ArrayList();// 1 myIntList.add(new Integer(0));// 2 Integer x = (Integer) myIntList.iterator().next();// 3 第3 行的类型转换有些烦人。通常情况下,程序员知道一个特定的list 里边放的是什么类型的数据。但是,这个类型转换是必须的(essential)。编 译器只能保证iterator 返回的是Object 类型。为了保证对Integer 类型变量赋值的类型安全,必须进行类型转换。 当然,这个类型转换不仅仅带来了混乱,它还可能产生一个运行时错误(run time error),因为程序员可能会犯错。 程序员如何才能明确表示他们的意图,把一个list(集合) 中的内容限制 为一个特定的数据类型呢?这就是generics 背后的核心思想。这是上面程序片断的一个泛型版本: List
学号11710115 天津城建大学 Java 语言程序设计C 实验报告 实验3:泛型实现链表 学生姓名路江飞 班级11卓越七班
一、实验内容 1.掌握使用Java语言进行结构化程序设计; 2.熟悉Java泛型。 3.熟悉Eclipse开发环境,编写简单的Application程序,并编译和执行。 二、实验要求 1.调试程序、编译,运行后得到正确的结果; 2.写出实验报告,要求记录编译和执行Java程序当中的系统错误信息提示,并给出解决办法。 三、实验结果 文件1: package _List; class Node
System.out.println("* 8.查看链表长度*"); System.out.println("* 9.退出*"); System.out.println("*************************************"); } } 文件3: package _List; import java.util.Scanner; public class List
局部变量在栈里面,全局变量在堆里面。 void*(*start_rtn)(void)怎么理解,返回值是什么类型,求高手指点 这是声明一个函数指针,该函数指针指向一个无参,返回值为void *指针的函数。 泛型指针 通常情况下,C只允许相同类型的指针之间进行转换。例如:一个字符型指针sptr(一个字符串)和一个整型指针iptr,我们不允许把sptr转换为iptr或把iptr转换为sptr。但是,泛型指针能够转换为任何类型的指针,反之亦然。因此,如果有一个泛型指针gptr,就可以把sptr转换为gptr或者把gptr转换为sptr。在C语言中,通常声明一个void指针来表示泛型指针。 很多情况下void指针都是非常有用的。例如:C标准函数库中的memcpy函数,它将一段数据从内存中的一个地方复制到另一个地方。由于memcpy可能用来复制任何类型的数据,因此将它的指针参数设定为void指针是非常合理的。void指针同样也可以用到其他普通的函数中。例如:之前提到过的交换函数swap2,可以把函数参数改为void指针,这样swap2就变成一个可以交换任何类型数据的通用交换函数,代码如下: 1#include
大局观:泛型程序设计( Generic Programming)与 STL 侯捷 jjhou@https://www.doczj.com/doc/1b15959974.html,.tw https://www.doczj.com/doc/1b15959974.html, => 1. 大局观:泛型程序设计与 STL 2. 泛型指标(Iterators)与 Traits 技术 3. 泛型容器(Containers)的应用与实作 4. 泛型算法( Generic Algorithms)与 Function Objects 5. 各色各样的 Adaptors 2000.02 2000.03 2000.04 2000.05 2000.06 十年前赶上 OO(Object Oriented,面向对象)第一波工业浪潮的朋友们,想必今日有一番顾盼豪情,以及一份「好加在」的惊悚:「好加在」搭上了 OO 的早班列车,不然今天就玩不下去了! 面对 Generic Programming(泛型程序设计),我有相彷的感觉。 我将撰写为期五次的系列文章,为各位介绍这个不算新却又有点新的观念与技术。 ●C++ Template, Generic programming, STL 1968 Doug McIlroy发表其著名论文 "Mass Produced Software Components",揭橥了以「可重用软件组件(又称软件积木或软件 IC)」构筑标准链接库的愿景。如今三分之一个世纪已逝,components software -- 以组件(组件)为本的软件 --大行其道。然而在许多领域中,标准化仍未建立。甚至连任何软件一定会用到的基本数据结构和基本算法,对此亦付阙如。于是大量的程序员,被迫进行大量重复的工作,为的竟只是重复完成前人早已完成而自己手上并未拥有的码。这不但是人力资源的浪费,也是挫折与错误的来源。 撇开追求技术的热诚不谈(也许有人确实喜欢什么都自己来),商用链接库可以拯救这些程序员于水深火热之中。只要你的老板愿意花一点点钱,就可以在人力资源与软件开发效率上取得一个绝佳平衡。但是金钱并非万能,商用链接库彼此之间并无接口上的标准!换句话说你无法在发现了 另一套更好、更丰富、效率更高的链接库后,改弦更张地弃此就彼--那可能会使你付出惨烈的代价。这或许是封闭型技术与观念带给软件组件公司的一种小小的、短暂的利益保护。 除了「接口无标准」这一问题,另一个问题是,目前绝大部份软件组件都使用面向对象技术,大量运用继承与虚拟函式,导致执行成本增加。此外,一旦运用面向对象技术,我们此刻所说的基础数据结构及各种基础算法,便皆需以 container(容器,放置数据的某种特殊结构)为本。资料,放在 container classes 内(形成其data members);操作行为,亦定义在 container classes 内(形成其 member functions);这使得耦合(coupling,两物过度相依而不独立)的情况依然存在,妨碍了组件之所以为组件的独立性、弹性、互操作性(相互合作性,interoperability)。 换句话说,要解决这些困境,第一需要标准接口的建立,第二需要 OO 以外的新技术,俾能够比 OO更高效率地 完成工作。 C++ template是新技术的曙光。 一言以蔽之,所谓 template机制就是,将目标物的数据型别参数化。一旦程序以指定自变量的方式,确定了这个(或这些)型别,编译程序便自动针对这个(或这些)型别产生出一份实体。这里所说的实体,可以是一个 function body,也可以是一个 class body。而「由编译程序产生出一份实体」的动作,我们称之为具现化(instantiation)。 针对目标物之不同,C++ 支持 function templates 和 class templates两大类型。后者的 members又可以是 templates(所谓 member templates),形成深 一
C# 自定义泛型集合Generic Collection类using System; using System.Collections.Generic; using System.Text; namespace CustomGenericCollection { #region Automobile definitions public class Car { public string PetName; public int Speed; public Car(string name, int currentSpeed) { PetName = name; Speed = currentSpeed; } public Car() { } } public class SportsCar : Car { public SportsCar(string p, int s) : base(p, s) { } // Assume additional SportsCar methods. } public class MiniVan : Car { public MiniVan(string p, int s) : base(p, s) { } // Assume additional MiniVan methods. } #endregion #region Custom Generic Collection //下面的泛型集合类的项目必须是Car 或Car的继承类 public class CarCollection
泛型实验 实验时间:2009年3月2 实验目的: 掌握泛型与其他集合相比的优势,创建泛型类,泛型方法,可空类型的运用,编译程序集的方法。 实验准备: 打开VS2005,新建一个空白解决方案,命名为lab2,存储在d盘根目录。 实验内容: 一,泛型与集合安全的比较 1,在解决方案里面,新建一个控制台应用程序命名为ListArray 2, 在Program.cs的入口函数中,创建一个ArrayList集合,往集合里面添加2个Int数字,再添加一个double数字 3, 再定义一个int变量total初始值为0 4, 利用循环语句foreach把集合里面的int变量相加的和存入total
5, 输出total的值,预览下效果 思考:如果出错,出错的原因在哪,如何更改 6,注释掉上面代码,通过同样的方法,定义一个泛型List,实现同样的目的,测试调试,比较下2种方法有什么区别和进步的地方。 二,创建泛型类 1,在解决方案里面再新建一个控制台应用程序命名为ListClass,设置为启动项 2, 新建一个类,命名为ListTest,在类中创建一个名称称为MyList 泛型类,为把它参数化,插入一个尖括号。在<>内的添加T代表使用该类时要指定的类型 3, 在MyList类中定义一个静态字段ObjCount,初始值为0,在构造器中实现自加,实现功能:计算用户实例化了多少个不同类型的对象。再创建一个属性字段getCount用于取得静态字段ObjCount 的值 4, 在Program.cs的入口函数Main中,实例2个int类型的MyList 类,1个double类型的Mylist类,用控制台输出每个实例的getCount属性。 思考:getCount的值有什么特性?泛型类的优势