技术图文:C#语言中的泛型 I

C#语言中的泛型 I

知识结构:

技术图文:C#语言中的泛型 I_第1张图片


1. 泛型概述

泛型广泛应用于容器(Collections)和对容器操作的方法中。

从 .NET Framework2.0 开始,微软提供了一个新的命名空间System.Collections.Generic,其中包含了一些新的基于泛型的容器类。当然,我们也可以自己创建泛型类、泛型接口、泛型方法以及泛型委托,通过这种类型安全且高效的方式来满足不同应用场景的需求。

下面的示例代码以一个简单的泛型链表类作为示范。(多数情况下,推荐使用 .NET Framework 类库提供的LinkedList,而不是创建自己的链表。)

单链表结构代码如下所示:

public class SNode<T> where T : IComparable<T>
{
     
    public T Data {
      get; set; }
    public SNode<T> Next {
      get; set; }
    public SNode(T data, SNode<T> next = null)
    {
     
        Data = data;
        Next = next;
    }
}

public class SLinkList<T> : IEnumerable<T>  where T : IComparable<T>
{
     
    public SNode<T> PHead {
      get; protected set; }
    public int Length {
      get; private set; }
    public SLinkList()
    {
     
        Length = 0;
        PHead = null;
    }
    private SNode<T> Locate(int index)
    {
     
        if (index < 0 || index > Length - 1)
            throw new IndexOutOfRangeException();

        SNode<T> temp = PHead;
        for (int i = 0; i < index; i++)
        {
     
            temp = temp.Next;
        }
        return temp;
    }
    public void InsertAtFirst(T data)
    {
     
        PHead = new SNode<T>(data, PHead);
        Length++;
    }
    public void InsertAtRear(T data)
    {
     
        if (PHead == null)
        {
     
            PHead = new SNode<T>(data);
        }
        else
        {
     
            Locate(Length - 1).Next = new SNode<T>(data);
        }
        Length++;
    }
    public void Insert(int index, T data)
    {
     
        if (index < 0 || index > Length)
            throw new IndexOutOfRangeException();
        if (index == 0)
        {
     
            InsertAtFirst(data);
        }
        else if (index == Length)
        {
     
            InsertAtRear(data);
        }
        else
        {
     
            Locate(index - 1).Next = new SNode<T>(data, Locate(index));
            Length++;
        }
    }
    public void Remove(int index)
    {
     
        if (index < 0 || index > Length - 1)
            throw new IndexOutOfRangeException();
        if (index == 0)
        {
     
            PHead = PHead.Next;
        }
        else
        {
     
            Locate(index - 1).Next = Locate(index).Next;
        }
        Length--;
    }
    public T this[int index]
    {
     
        get
        {
     
            if (index < 0 || index > Length - 1)
                throw new IndexOutOfRangeException();
            return Locate(index).Data;
        }
        set
        {
     
            if (index < 0 || index > Length - 1)
                throw new IndexOutOfRangeException();
            Locate(index).Data = value;
        }
    }
    public bool IsEmpty()
    {
     
        return Length == 0;
    }
    public void Clear()
    {
     
        PHead = null;
        Length = 0;
    }
    public IEnumerator<T> GetEnumerator()
    {
     
        SNode<T> current = PHead;
        while (current != null)
        {
     
            yield return current.Data;
            current = current.Next;
        }
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
     
        return GetEnumerator();
    }
}

客户端代码如下所示:

class Program
{
     
    static void Main(string[] args)
    {
     
        SLinkList<int> lst = new SLinkList<int>();
        for (int i = 0; i < 3; i++)
        {
     
            lst.InsertAtFirst(i);
        }
        foreach (int i in lst)
        {
     
            Console.WriteLine(i);
        }
        // 2
        // 1
        // 0
    }
}

上面实例代码演示了客户端代码如何使用泛型类SLinkList,来创建一个整数链表。通过简单地改变参数的类型,很容易改写上面的代码,以创建字符串或其它自定义类型的链表。

客户端代码如下所示:

class Program
{
     
    static void Main(string[] args)
    {
     
        SLinkList<string> lst = new SLinkList<string>();
        for (int i = 0; i < 3; i++)
        {
     
            lst.InsertAtFirst("第" + i + "个,进入链表.");
        }
        foreach (string s in lst)
        {
     
            Console.WriteLine(s);
        }
        // 第2个,进入链表.
        // 第1个,进入链表.
        // 第0个,进入链表.
    }
}

可见,泛型是由编译器提供的语法糖,在调用的时候,编译器针对客户端提供的类型参数生成不同的副本,以方便开发人员编码,不必为每种类型编写那些逻辑相同的代码。


2. C#泛型的优点

以前类型泛化(Generalization)是靠类型与全局基类System.Object的相互转换来实现的(里氏代换原则)。如 .NET Framework 基础类库中的ArrayList容器类。

ArrayList是一个很方便的容器类,使用中无需更改就可以存储任何引用类型或值类型。

ArrayList list1 = new ArrayList();
list1.Add(3);
list1.Add(105);
//...

ArrayList list2 = new ArrayList();
list2.Add("Tom");
list2.Add("John");
//...

但是这种便利是有代价的,这需要把任何一个加入ArrayList的引用类型或值类型都隐式地向上转换成System.Object。如果这些元素是值类型,那么当加入到ArrayList中时,它们必须被装箱,当重新取回它们时,要拆箱。类型转换和装箱、拆箱的操作都降低了性能,在迭代(Iterator)大容器的情况下,装箱和拆箱对性能的影响可能十分显著。

另一个局限是缺乏编译时的类型检查,引起类型安全问题,当一个ArrayList把任意类型都转换为Object,就无法在编译时预防客户代码类似这样的操作:

ArrayList list = new ArrayList();
list.Add(3);
list.Add("Tom");
int t = 0;
foreach (int x in list)
{
     
    t += x;
}

虽然这样完全合法,但这个错误直到运行的时候才会被发现。ArrayList和其它相似的类真正需要的是一种途径,能让客户端代码在实例化之前指定所需的特定数据类型。这样就不需要向上类型转换为Object,而且编译器可以同时进行类型检查。换句话来说,ArrayList需要一个类型参数。这正是泛型所提供的,泛型即参数化类型,在编码时通过指定类型参数就不会存在装箱,拆箱问题,避免性能损失,且保证类型安全。

System.Collections.Generic命名空间中的泛型List容器里,同样把元素加入容器的操作,类似这样:

List<int> list = new List<int>();
list.Add(3);
//list.Add("Tom"); //编译错误
//错误 CS1503  参数 1: 无法从“string”转换为“int”

ArrayList相比,在客户端代码中唯一增加的List语法是声明和实例化中的类型参数。代码略微复杂,但创建的容器类不仅比ArrayList更安全,而且明显地更加快速,尤其当容器中的元素是值类型的时候。


3. C#泛型之类型参数

在泛型类、泛型接口、泛型方法或泛型委托的定义中,类型参数是一个占位符(Place Holder),通常为一个大写字母,如T。在客户端通过代码声明或实例化该类型的变量时,把T替换为客户端所指定的数据类型。

如果要使用泛型类SLinkList,客户端代码必须在尖括号内指定一个类型参数。同样,也可以使用不同的类型参数来创建不同的对象,如下所示:

SLinkList<int> list1 = new SLinkList<int>();
SLinkList<double> list2 = new SLinkList<double>();
SLinkList<string> list3 = new SLinkList<string>();

对于这些SLinkList的实例,泛型类中出现的每个T都将在编译时被类型参数指定的具体类型所取代。依靠这样的取代,我们仅用一份泛型类的代码,就创建了三个独立的,类型安全且高效的对象。


4. C#泛型之类型参数约束

通常情况下一个泛型类写成如下形式:

public class MyClass<T>
{
     
    //…
}

以上泛型类中的T,被称为未绑定类型参数(Unbounded type parameters)。

未绑定类型参数必须遵守以下规则:

  • 不能使用!===运算符,因为无法保证具体的类型参数是否能够支持这些运算符。
  • 它们可以与System.Object相互转换,也可显式地转换成任何类型。
  • 它们可以与null进行比较。如果一个未绑定类型参数与null比较,当此类型参数为值类型时,比较的结果总为false。

在设计泛型类或泛型方法时,如果对泛型成员执行任何赋值以外的操作或者调用System.Object中所没有的方法(编译器默认类型参数是直接继承System.Object),就需要在类型参数上使用约束。

利用where子句,可以为类型参数指定一个或多个约束,从而增加了类型参数可调用的属性和方法的数量。这些属性和方法受约束类型及其派生层次中的类型的支持。如果客户端代码尝试使用某个约束不允许的类型来进行实例化,则会产生编译时错误。下表列出了六类约束:

技术图文:C#语言中的泛型 I_第2张图片

(1)值约束和引用约束举例

public class MyClass<T, U> where T : class where U : struct
{
     
    //对于多个类型参数,每个类型参数都使用一个where子句
}

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

class Program
{
     
    private static void OpTest<T>(T s, T t) where T : class
    {
     
        Console.WriteLine(s == t);
    }
    static void Main(string[] args)
    {
     
        string s1 = "target";
        StringBuilder sb = new StringBuilder("target");
        string s2 = sb.ToString();
        OpTest(s1, s2); // False
        Console.WriteLine(s1 == s2); // True
    }
}

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

(2)接口约束举例

public class MyGenericClass<T> where T : IComparable<T>
{
     
    //...
}

public interface IMyInterface
{
     
    //...
}

public class Test
{
     
    public bool MyMethod<T>(T t) where T : IMyInterface
    {
     
        //...
        return false;
    }
}

(3)构造约束举例

public class ItemFactory<T> where T : new()
{
     
    public T CreateItem()
    {
     
        return new T();
    }
}
public delegate T MyDelegate<T>() where T : new();

public class ItemFactory2<T> where T : IComparable, new()
{
     
    //当与其它约束一起使用时,new()约束必须最后指定。
}

(4)类型参数约束举例

internal class MyList<T>
{
     
    // T为未绑定类型参数
    private void Add<TU>(MyList<TU> items) where TU : T
    {
     
        //此处为类型参数约束
    }
}

类型参数也可以用在泛型类的定义中,但这种约束用途十分有限。只有希望强制两个类型参数具有继承关系的时候,才考虑使用。

public class MyClass<T, TU, TV> where T : TV
{
     
    // ...
}

下面提供一个完整的,有关泛型约束的例子。

Employee代码如下:

public class Employee : IComparable<Employee>
{
     
    public string Name {
      get; set; }
    public int Age {
      get; set; }
    public Employee(string name, int age)
    {
     
        Name = name;
        Age = age;
    }
    public Employee()
    {
     
        ;
    }
    public int CompareTo(Employee other)
    {
     
        if (other == null)
            throw new ArgumentNullException();
        return Age - other.Age;
    }
    public override string ToString()
    {
     
        return Name + ":" + Age;
    }
}

public class MyList<T> : SLinkList<T>
    where T : Employee, IComparable<T>, new()
{
     
    public T FindFirstOccurence(string str)
    {
     
        T result = null;
        SNode<T> current = PHead;
        while (current != null)
        {
     
            if (current.Data.Name == str)
            {
     
                result = current.Data;
                break;
            }
            current = current.Next;
        }
        return result;
    }
}

客户端代码如下:

class Program
{
     
    static void Main(string[] args)
    {
     
        // MyList lst = new MyList();
        // 编译错误                                          
        // 错误  CS0310  
        // “string”必须是具有公共的无参数构造函数的非抽象类型,
        // 才能用作泛型类型或方法“MyList < T >”中的参数“T”	

        MyList<Employee> list = new MyList<Employee>();
        string[] names = {
      "Tom", "John", "Patrick", "Maya", "Smith" };
        int[] ages = {
      45, 19, 28, 23, 35 };
        for (int i = 0; i < names.Length; i++)
        {
     
            list.InsertAtFirst(new Employee(names[i], ages[i]));
        }
        foreach (Employee e in list)
        {
     
            Console.WriteLine(e.ToString());
        }
        // Smith:35
        // Maya:23
        // Patrick:28
        // John:19
        // Tom:45
        Console.WriteLine(list[0] == list[1] ? "True" : "False");
        // False
    }
}

5. C#中的泛型类

泛型类封装了不针对任何特定数据类型的操作。常用于容器类,如 .NET Framework 类库中的动态数组List、双向链表LinkedList、栈Stack、队列Queue等等。

  • 封闭构造类型的泛型类(closed constructed type):给类型参数指定类型后的泛型类,如List
  • 开放构造类型的泛型类(open constructed type):未给类型参数指定类型的泛型类,如List

在进行泛化的设计时,应该注意以下问题:

(1)泛型类可以继承自实体类、封闭构造类型的基类、开放构造类型的基类,如下所示。

public class BaseNode
{
     
    // 实体类
}

public class BaseNodeGeneric<T>
{
     
    // 普通泛型类
}

public class BaseNodeMultiple<T, TU>
{
     
    // 普通泛型类
}

public class NodeConcrete<T> : BaseNode
{
     
    // 泛型类继承实体类
}

public class NodeClosed<T> : BaseNodeGeneric<int>
{
     
    //泛型类继承封闭构造类型的基类
}


public class NodeOpen<T> : BaseNodeGeneric<T>
{
     
    //泛型类继承开放构造类型的基类
}

public class Node5<T, TU> : BaseNodeMultiple<T, TU>
{
     
    //泛型类继承开放构造类型的基类
}

public class Node4<T> : BaseNodeMultiple<T, int>
{
     
    //除了与子类共用的类型参数外,必须为所有的其它类型参数指定类型
}

//public class Node6 : BaseNodeMultiple
//{
     
//    //Generates an Error
//}

(2)非泛型的具体类可以继承自封闭构造类型的基类,但不能继承自开放构造类型的基类。这是因为客户代码无法提供基类所需的类型参数,如下所示。

public class BaseNodeGeneric<T>
{
     
    // 普通泛型类
}

public class Node1 : BaseNodeGeneric<int>
{
     
    // No Error
}

//public class Node2 : BaseNodeGeneric
//{
     
//    // Generates an Error
//}

//public class Node3 : T
//{
     
//    // Generates an Error
//}

(3)从开放式构造类型继承的泛型类,如果基类指定约束,那么子类必须指定约束,这些约束是基类约束的超集或等于基类型约束,如下所示。

public class NodeItem<T> where T : IComparable<T>, new()
{
     
    //...
}

public class SpecialNodeItem<T> : NodeItem<T>
    where T : IComparable<T>, new()
{
     
    //...
}

(4)开放结构类型和封闭结构类型可以用作方法的参数,如下所示:

class Program
{
     
    private void Swap<T>(List<T> list1, List<T> list2)
    {
     
        //...
    }

    private void Swap(List<int> list1, List<int> list2)
    {
     
        //...
    }
}

我们来看一个综合的例子,构造SortList结构。

public class SortList<T> : SLinkList<T> where T : IComparable<T>
{
     
    public void BubbleSort()
    {
     
        if (PHead == null || PHead.Next == null)
            return;
        bool swapped;
        do
        {
     
            swapped = false;
            SNode<T> previous = null;
            SNode<T> current = PHead;
            while (current.Next != null)
            {
     
                if (current.Data.CompareTo(current.Next.Data) > 0)
                {
     
                    SNode<T> tmp = current.Next;
                    current.Next = tmp.Next;
                    tmp.Next = current;
                    if (previous == null)
                    {
     
                        PHead = tmp;
                    }
                    else
                    {
     
                        previous.Next = tmp;
                    }
                    previous = tmp;
                    swapped = true;
                }
                else
                {
     
                    previous = current;
                    current = current.Next;
                }
            }
        } while (swapped);
    }
}

客户端代码如下:

class Program
{
     
    static void Main(string[] args)
    {
     
        SortList<Employee> list = new SortList<Employee>();
        string[] names = {
      "Tom", "John", "Patrick", "Maya", "Smith" };
        int[] ages = {
      45, 19, 28, 23, 35 };
        for (int i = 0; i < names.Length; i++)
        {
     
            list.InsertAtFirst(new Employee(names[i], ages[i]));
        }
        foreach (Employee e in list)
        {
     
            Console.WriteLine(e);
        }
        // Smith:35
        // Maya:23
        // Patrick:28
        // John:19
        // Tom:45

        list.BubbleSort();
        foreach (Employee e in list)
        {
     
            Console.WriteLine(e);
        }
        // John:19
        // Maya:23
        // Patrick:28
        // Smith:35
        // Tom:45
    }
}

通常,我们设计泛型类是从一个已有的具体类开始的,每次把一个类型改为类型参数,直至达到一般性和可用性的最佳平衡。在设计泛型类时,需要重点考虑的事项有:

(1)考虑要把哪些类型泛化为类型参数。一般的规律是,用参数表示的类型越多,代码的灵活性和复用性也越大。但过多的泛化会导致代码难以被其它开发人员理解,需要权衡。

(2)考虑这些类型参数需要什么样的约束。一个良好的习惯是,尽可能使用最大的约束,同时保证可以处理所有需要处理的类型。例如,如果你知道你的泛型类只打算使用引用类型,那么就应用使用引用约束。这样可以防止无意中使用值类型,同时可以对T使用as运算符,并且检查空引用。

(3)考虑泛型类的继承关系,以及泛型行为应该放在基类中还是子类中。

(4)考虑泛型类是否要实现一个或多个泛型接口。例如,要设计一个泛型容器,就要考虑是否需要实现类似IEnumerableIComparable这样的接口,以方便容器元素的遍历和查找。


后台回复「搜搜搜」,随机获取电子资源!
欢迎关注,请扫描二维码:



你可能感兴趣的:(C#学习)