泛型(一)

泛型

泛型是2.0C#语言和公共语言运行库(CLR)中的一个新功能。泛型将类型参数的概念引入.NET Framework,类型参数使得设计如下类和方法成为可能:这些类和方法将一个或多个类型的指定推迟到客户端代码声明并实例化该类或方法的时候。例如,通过使用泛型类型参数T,您可以编写其他客户端代码能够使用的单个类,而不致引入运行时强制转换或装箱操作的成本或风险,如下所示:

// Declare the generic class
public class GenericList<T>
{
    
void Add(T input) { }
}

class TestGenericList
{
    
private class ExampleClass { }
    
static void Main()
    
{
        
// Declare a list of type int
        GenericList<int> list1 = new GenericList<int>();

        
// Declare a list of type string
        GenericList<string> list2 = new GenericList<string>();

        
// Declare a list of type ExampleClass
        GenericList<ExampleClass> list3 = new GenericList<ExampleClass>();
    }

}


泛型概述

l         使用泛型类型可以最大限度地重用代码、保护类型的安全以及提高性能

l         泛型最常见的用途是创建集合类

l         .NET Framework类库在System.Collections.Generic命名空间中包含几个新的泛型集合类。应尽可能地使用这些类来代替普通的类,如System.Collections命名空间中的ArrayList

l         可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托

l         可以对泛型类进行约束以访问特定数据类型的方法

l         关于泛型数据类型中使用的类型的信息可在运行时通过反射获取

 

泛型介绍

泛型类和泛型方法同时具备可重用性、类型安全和效率,这是非泛型类和非泛型方法无法具备的。泛型通常用在集合和在集合上运行的方法中。.NET Framework 2.0版类库提供一个新的命名空间System.Collections.Generic,其中包含几个新的基于泛型的集合类。建议面向2.0版的所有应用程序都使用新的泛型集合类,而不要使用旧的非泛型集合类,如ArrayList

当然,也可以创建自定义泛型类型和方法,以提供自己的通用解决方案,设计类型安全的高效模式。下面的代码示例演示一个用于演示用途的简单泛型链接列表类。(大多数情况下,建议使用.NET Framework类库提供的List<T>类,而不要自行创建类。)在通常使用具体类型来指示列表中所存储项的类型时,可使用类型参数T。其使用方法如下:

l         AddHead方法中作为方法参数的类型

l         Node嵌套类中作为公共方法GetNextData属性的返回类型

l         在嵌套类中作为私有成员数据的类型

注意,T可用于Node嵌套类。如果使用具体类型实例化GenericList<T>(例如,作为GenericList<int>),则所有的T都将被替换为int

using System.Collections.Generic;

// type parameter T in angle brackets
public class GenericList<T> {
    
// The nested class is also generic on T
    private class Node{
        
// T used in non-generic constructor
        public Node(T t){
            next 
= null;
            data 
= t;
            }


        
private Node next;
        
public Node Next{
            
get return next; }
            
set { next = value; }
            }

        
        
// T as private member data type
        private T data;

        
// T as return type of property
        public T Data  {
            
get return data; }
            
set { data = value; }
            }

        }


    
private Node head;
    
    
// constructor
    public GenericList() {
        head 
= null;
        }


    
// T as method parameter type:
    public void AddHead(T t){
        Node n 
= new Node(t);
        n.Next 
= head;
        head 
= n;
        }


    
public IEnumerator<T> GetEnumerator(){
        Node current 
= head;

        
while (current != null){
            
yield return current.Data;
            current 
= current.Next;
            }

        }

    }


class TestGenericList{
        
static void Main(){
                
//int is the type argument
                GenericList<int> list = new GenericList<int>();
                
                
for (int x = 0; x < 10; x++){
                        list.AddHead(x);
                    }

                
                
foreach (int i in list){
                        System.Console.Write(i 
+ " ");
                    }

                System.Console.WriteLine(
" Done");
            }

    }


泛型的优点

在公共语言运行库和C#语言的早期版本中,通用化是通过在类型与通用基类型Object之间进行强制转换来实现的,泛型提供了针对这种限制的解决方案。通过创建泛型类,可以创建一个在编译时类型安全的集合。

使用非泛型集合类的限制可以通过编写一小段程序来演示,这程序利用.NET Framework基类库中的ArrayList集合类。ArrayList是一个使用起来非常方便的集合类,无需进行修改即可用来存储任何引用或值类型。

// The .NET Framework 1.1 way to create a list:

System.Collections.ArrayList list1 = new System.Collections.ArrayList();

list1.Add(3);

list1.Add(105);

 

System.Collections.ArrayList list2 = new System.Collections.ArrayList();

list2.Add("It is raining in Redmond.");

list2.Add("It is snowing in the mountains.");

但这种方便是需要付出代价的。添加到ArrayList中的任何引用或值类型都将隐式地向上强制转换为Object。如果项是值类型,则必须在将其添加到列表中时进行装箱操作,在检索时进行取消装箱操作。强制转换以及装箱和取消装箱操作都会降低性能;在必须对大型集合进行循环访问的情况下,装箱和取消装箱的影响非常明显。

另一个限制是缺少编译时类型检查;因为ArrayList将把所有项都强制转换为Object,所以在编译时无法防止客户端代码执行以下操作:

System.Collections.ArrayList list = new System.Collections.ArrayList();

// Add an integer to the list.

list.Add(3);

// Add a string to the list. This will compile, but may cause an error later.

list.Add("It is raining in Redmond.");

 

int t = 0;

// This causes an InvalidCastException to be returned.

foreach (int x in list)

{

    t += x;

}

尽管将字符串和ints组合在一个ArrayList中的做法在创建异类集合时是完全合法的,有时是有意图的,但这种做法更可能产生编程错误,并直到运行时才能检测到此错误。

C#语言的1.01.1版本中,只能通过编写自己的特定于类型的集合来避免.NET Framework基类库集合类中的通用代码的危险。当然,由于此类不可对多个数据类型重用,因此将丧失通用化的优点,并且您必须对要存储的每个类型重新编写该类。

ArrayList和其他相似类真正需要的是:客户端代码基于每个实例指定这些类要使用的具体数据类型的方式。这样将不再需要向上强制转换为T:System.Object,同时,也使得编译器可以进行类型检查。换句话说,ArrayList需要一个type parameter。这正是泛型所能提供的。在N:System.Collections.Generic命名空间的泛型List<T>集合中,向该集合添加项的操作类似于以下形式:

// The .NET Framework 2.0 way to create a list

List<int> list1 = new List<int>();

 

// No boxing, no casting:

list1.Add(3);

 

// Compile-time error:

// list1.Add("It is raining in Redmond.");

对于客户端代码,与ArrayList相比,使用List<T>时添加的唯一语法是声明和实例化只能够的类型参数。虽然这稍微增加了些编码的复杂性,但好处是可以创建一个比ArrayList更安全并且速度更块的列表,特别适用于列表项是值类型的情况。

 

泛型类型参数

在泛型类型或方法定义中,类型参数是客户端在实例化泛型类型的变量时指定的特定类型的占位符。泛型类(如泛型介绍中列出的GenericList<T>)不可以像这样使用,因为它实际上并不是一个类型,而更像是一个类型的蓝图。若要使用GenericList<T>,客户端代码必须通过指定尖括号中的类型参数来声明和实例化构造类型。此特定类的类型参数可以是编译器识别的任何类型。可以创建任意数目的构造类型实例,每个实例使用不同的类型参数,如下所示:

GenericList<float> list1 = new GenericList<float>();

GenericList<ExampleClass> list2 = new GenericList<ExampleClass>();

GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>();

在每个GenericList<T>实例中,类中出现的每个T都会在运行时替换为相应的类型参数。通过这种替换方式,使用一个类定义创建了三个独立的类型安全的有效对象。

类型参数命名准则

l         务必使用描述性名称命名泛型类型参数,除非单个字母名称完全可以让人了解它表示的含义,而描述性名称不会有更多的意义。

public interface ISessionChannel<TSession> { /*...*/ }

public delegate TOutput Converter<TInput, TOutput>(TInput from);

public class List<T> { /*...*/ }

l         考虑使用T作为具有单个字母类型参数的类型的类型参数名。

public int IComparer<T>() { return 0; }

public delegate bool Predicate<T>(T item);

public struct Nullable<T> where T : struct { /*...*/ }

l         务必将“T”作为描述性类型参数名的前缀。

public interface ISessionChannel<TSession>

{

    TSession Session { get; }

}

l         考虑在参数名中指示对此类型参数的约束。例如,可以将带有ISession约束的参数命名为TSession

 

类型参数的约束

在定义泛型类时,可以对客户端代码能够在实例化类时用于类型参数的类型种类施加限制。如果客户端代码尝试使用某个约束所不允许的类型来实例化类,则会产生编译时错误。这些限制称为约束。约束是使用where上下文关键字指定的。下表列出了六种类型的约束:

约束

说明

T:结构

类型参数必须是值类型。可以指定除Nullable以外的任何值类型。

T:类

类型参数必须是引用类型,包括任何类、接口、委托或数组类型。

Tnew()

类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new()约束必须最后指定。

T<基类名>

类型参数必须是指定的基类或派生自指定的基类。

T<接口名称>

类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。

TU

T提供的类型参数必须是为U提供的参数或派生为U提供的参数。这称为裸类型约束。

使用约束的原因

如果要检查泛型列表中的某个项以确定它是否有效,或者将它与其他某个项进行比较,则编译器必须在一定程度上保证它需要调用的运算符或方法将受到客户端代码可能指定的任何类型参数的支持。这种保证是通过对泛型类定义应用一个或多个约束获得的。例如,基类约束告诉编译器:仅此类型的对象或从此类型派生的对象才可用作类型参数。一旦编译器有了这个保证,它就能够允许在泛型类中调用类型的方法。约束是使用上下文关键字where应用的。下面的代码示例演示可通过应用基类约束添加到GenericList<T>类的功能。

public class Employee
{
    
private string name;
    
private int id;

    
public Employee(string s, int i)
    
{
        name 
= s;
        id 
= i;
    }


    
public string Name
    
{
        
get return name; }
        
set { name = value; }
    }


    
public int ID
    
{
        
get return id; }
        
set { id = value; }
    }

}


public class GenericList<T> where T : Employee
{
    
private class Node
    
{
        
private Node next;
        
private T data;

        
public Node(T t)
        
{
            next 
= null;
            data 
= t;
        }


        
public Node Next
        
{
            
get return next; }
            
set { next = value; }
        }


        
public T Data
        
{
            
get return data; }
            
set { data = value; }
        }

    }


    
private Node head;

    
public GenericList() //constructor
    {
        head 
= null;
    }


    
public void AddHead(T t)
    
{
        Node n 
= new Node(t);
        n.Next 
= head;
        head 
= n;
    }


    
public IEnumerator<T> GetEnumerator()
    
{
        Node current 
= head;

        
while (current != null)
        
{
            
yield return current.Data;
            current 
= current.Next;
        }

    }


    
public T FindFirstOccurrence(string s)
    
{
        Node current 
= head;
        T t 
= null;

        
while (current != null)
        
{
            
//The constraint enables access to the Name property.
            if (current.Data.Name == s)
            
{
                t 
= current.Data;
                
break;
            }

            
else
            
{
                current 
= current.Next;
            }

        }

        
return t;
    }

}


约束使得泛型能够使用Employee.Name属性,因为类型为T的所有项都保证是Employee对象或从Employee继承的对象。

可以对同一类型参数应用多个约束,并约束自身可以是泛型类型,如下所示:

class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()

{

    // ...

}

通过约束类型参数,可以增加约束类型及其继承层次结构中的所有类型所支持的允许操作和方法调用的数量。因此,在设计泛型类或方法时,如果要对泛型成员执行除简单赋值之外的任何操作或调用System.Object不支持的任何方法,将需要对该类型参数应用约束。

在应用where T : class约束时,建议不要对类型参数使用==!=运算符,因为这些运算符仅测试引用同一性而不测试值相等性。即使在用作参数的类型中重载这些运算符也是如此。下面的代码说明了这一点;即使String类重载==运算符,输出也为false

public static void OpTest<T>(T s, T t) where T : class
{
    System.Console.WriteLine(s 
== t);
}

static void Main()
{
    
string s1 = "foo";
    System.Text.StringBuilder sb 
= new System.Text.StringBuilder("foo");
    
string s2 = sb.ToString();
    OpTest
<string>(s1, s2);
}


这种情况的原因在于,编译器在编译时仅知道T是引用类型,因此必须使用对所有引用类型都有效的默认运算符。如果需要测试值相等性,建议的方法是同时应用where T : IComparable<T>约束,并在将用于构造泛型类的任何类中实现该接口。

 

未绑定的类型参数

没有约束的类型参数(如公共类SampleClass<T>{}中的T)称为未绑定的类型参数。未绑定的类型参数具有以下规则:

l         不能使用!===运算符,因为无法保证具体类型参数能支持这些运算符

l         可以在它们与System.Object之间来回转换,或将它们显示转换为任何接口类型

l         可以将它们与null进行比较。将未绑定的参数与null进行比较时,如果类型参数为值类型,则该比较将始终返回false

 

裸类型约束

用作约束的泛型类型参数称为裸类型约束。当具有自己的类型参数的成员函数需要将该参数约束为包含类型的类型参数时,裸类型约束很有用,如下面的示例所示:

class List<T>

{

    void Add<U>(List<U> items) where U : T {/*...*/}

}

在上面的示例中,TAdd方法的上下文中是一个裸类型约束,而在List类的上下文中是一个未绑定的类型参数。

裸类型约束还可以在泛型类定义中使用。注意,还必须已经和其他任何类型参数一起在尖括号中声明了裸类型约束:

//naked type constraint

public class SampleClass<T, U, V> where T : V { }

泛型类的裸类型约束的作用非常有限,因为编译器除了假设某个裸类型约束派生自System.Object以外,不会做其他任何假设。在希望强制两个类型参数之间的继承关系的情况下,可对泛型使用裸类型约束。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值