第十二章 第十三章 泛型与接口

泛型部分

泛型 :

  • 定义 :泛型是CLR和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即算法重用
  • 实际形式 :定义算法的开发人员并不设定该算法要操作什么数据类型,该算法可广泛地应用于不同类型的对象。
  • 适用对象
    1. 引用类型
    2. 值类型 (但是并不支持枚举类型。)
    3. 接口
    4. 委托
    5. 方法
代码演示:
private static void SomeMethod()
{
  //构造一个List来操作DateTime对象
  List<DateTime> dtList = new List<DateTime>();
  //向列表添加一个DateTime对象
  dtList.Add(DateTime.Now);    //不进行装箱
  //向列表添加一个DateTime对象
  dtList.Add(DateTime.MinValue);   //不进行装箱
  //尝试向列表中添加一个String对象
  dtList.Add("1/1/2004");    //编译时错误
  //从列表提取一个DataTime对象
  DateTime dt = dtList[0];    //不需要转型
}

形如:List<DateTime>,DateTime这种可以被指定为任何变量的都称为类型实参。

泛型的优势所在:
  • 源代码保护 :因为使用者不需要访问算法的源代码(使用 C++ 模板的泛型技术时,算法的源代码必须提供给使用者)
  • 类型安全 :编译器和CLR保证只有与指定类型数据兼容的对象才能用于算法。试图使用不兼容的对象会报错(编译时错误或者运行时抛出异常)
  • 更清晰的代码
  • 更佳的性能 :由于能创建一个泛型算法来操作一个具体的值类型,所以值类型的实例能以传值方式传递,CLR不再需要执行任何装箱操作。(装箱操作会在托管堆上进行内存分配,造成更频繁的垃圾回收,从而损害应用程序的性能。)在操作值类型时候,泛型比非泛型算法速度要快得多
ref 和 out 修饰的值参数,类似于传引用,但不用装箱,只是传递值类型的地址。
FCL中的泛型:
Microsoft建议使用泛型集合类,不建议使用非泛型集合类。
集合类中实现了许多接口,放入集合中的对象可实现接口来执行排序和搜索等操作。常用接口在 System.Collections.Generic 命名空间中提供
System.Array类(所有数组类型的基类)提供了大量静态泛型方法:AsReadOnly,BinarySearch,ConvertAll,Exists,Find,FindAll,FindIndex,FindLast,FindLastIndex,ForEach,IndexOf,LastIndexOf,Resize,Sort和TrueForAll等,部分代码展示如下:
public abstract class Array : ICloneable,IList,ICollection,IEnumerable,ISructuralComparable,IStructuralEquatable
{
  public static void Sort<T>(T[] array);
  public static void Sort<T>(T[] array,IComparer<T> comparer);
  public static Int32 BinarySearch<T>(T[] array,T value);
  public static Int32 BinarySearch<T>(T[] array,T value,IComparer<T> comparer);

  public static void Main()
  {
    //创建并初始化字节数组
    Byte[] byteArray = new Byte[] {5,1,4,2,3};
    //调用Byte[]排序算法
    Array.Sort<Byte>(byteArray);
    //调用Byte[]二分搜索算法
    Int32 i = Array.BinarySearch<Byte>(byteArray,1);
    Console.WriteLine(i);    //显示“0”
  }
}
具有泛型类型参数的类型称为开放类型。CLR禁止构造开放类型的任何实例。
只有为所有的类型参数都传递了实际的数据类型,类型才成为封闭类型。CLR允许构造封闭类型的实例。
演示代码如下:
//一个部分指定的开放类型
    internal sealed class DictionaryStringKey<TValue> : Dictionary<String,TValue>
    {

    }

    public static class Program
    {
        public static void Main(string[] args)
        {
            Object o = null;

            //Dictionary<,> 是开放类型,有2个类型参数
            Type t = typeof(Dictionary<,>);

            //尝试创建该类型的实例(失败)
            o = CreateInstance(t);
            Console.WriteLine();

            //DictionaryStringKey<>是开放类型,有一个类型参数
            t = typeof(DictionaryStringKey<>);

            //尝试创建该类型的实例(失败)
            o = CreateInstance(t);
            Console.WriteLine();

            //DictionaryStringKey<Guid>是封闭类型
            t = typeof(DictionaryStringKey<Guid>);

            //尝试创建该类型的一个实例(成功)
            o = CreateInstance(t);

            //证明它确实能够工作
            Console.WriteLine("对象类型 = "+o.GetType());
        }

        private static Object CreateInstance(Type t)
        {
            Object o = null;

            try
            {
                o = Activator.CreateInstance(t);        
                Console.Write("已创建 {0} 的实例。", t.ToString());
            }
           catch(ArgumentException e)
            {
                 Console.WriteLine(e.Message);
            }
            return o;
        }
    }

(使用泛型类型并指定类型实参时,实际是在CLR中定义一个新的类型对象,新的类型对象从泛型类型派生自的那个类型派生(如果有的话),指定类型实参,不影响继承层次结构。)

模拟链表:
  • 代码如下 :
internal sealed class Node<T>
{
  public T m_data;
  public Node<T> m_next;
  
  public Node(T data) : this(data,null)
  {}
  public Node(T data,Node<T> next)
  {
    m_data = data;
    m_next = next;
  }
  
  public override  String ToString()
  {
    return m_data.ToString + ((m_next != null) ? m_next.ToString() : String.Empty);
  }

  public static void SameDataLinkedList()
  {
    Node<Char> head = new Node<Char>('C');
    head = new Node<Char>('B',head);
    head = new Node<Char>('A',head);
    Console.WriteLine(head.ToString());    //显示“ABC”
  }
}

以上代码具有一定的限制,限制就是该链表只能存储相同数据类型的链表结点。

  • 优化代码 :
internal class Node
{
  protected Node m_next;
  
  public Node(Node next)
  {
    m_next = next;
  }
}

internal sealed class TypedNode<T> : Node
{
  public T m_data;
  
  public TypedNode(T data) : this(data,null)
  {
  }
  public TypedNode(T data,Node next) : base(next)
  {
    m_data = data;
  }

  public override String ToString()
  {
    return m_data.ToString() + ((m_next != null) ? m_next.ToString() : String.Empty);
  }

  //该静态方法创建了一个链表,其中每个节点都是不同的数据类型。
  private  static void DifferentDataLinkedList()
  {
    Node head = new TypedNode<Char>('.');
    head = new TypedNode<DateTime>(DateTime.Now,head);
    head = new TypedNode<String>("Today is ",head);
    Console.WriteLine(head.ToString());
  }
}

以上优化代码,定义了非泛型的Node基类,在定义了泛型的TypedNode类(用Node类作为基类)。这样就可以创建一个链表,其中每个节点都可以是一种不同的具体的数据类型( 不能是Object)

泛型类型同一性:
  • 绝对不要单纯出于增强源码可读性的目的来定义一个新类。这样会丧失类型的同一性和相等性。如下代码所示:
    Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));

该部分的正确增强可读性简化语法:
1.在源文件顶部使用传统的using指令:
using DateTimeList = (typeof(List<DateTime>) == typeof(DateTimeList));
2.使用C#的"隐式类型局部变量"功能:
var dtl = new List<DateTime>();

代码爆炸:
使用泛型类型参数的方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码(这些代码为操作指定数据类型“量身定做”)。这样做有一个重大缺点:

CLR要为每种不同的方法/类型组合生成本机代码。(这样造成的现象就成为代码爆炸。)
(代码爆炸可能造成应用程序的工具集显著增大,从而损坏性能)

CLR内建了一些优化措施来缓解代码爆炸:
  1. 假定为特定的类型实参调用了一个方法,以后再用相同的类型实参调用这个方法,CLR只会为这个方法/类型组合编译一次代码
  2. CLR认为所有引用类型都完全相同,所以代码可以共享。例如List<String>的方法编译代码可直接用于List<Stream>的方法,因为String和Stream均为引用类型。

泛型接口:

引用类型或值类型可指定类型实参实现泛型接口,也可保持类型实参的未指定状态来实现泛型接口:
public interface IEnumerator<T> : IDisposable,IEnumerator
{
  T Current{ get; }
}

internal sealed class Triangle : IEnumerator<Point>
{
  private Point[] m_vertices;
  
  //IEnumerator<Point>的Current属性是Point类型
  public Point Current{get{...}}
  ...
}

//下例实现了相同的泛型接口,但保持类型实参的未指定状态:
internal sealed class ArrayEnumerator<T> : IEnumerator<T>
{
  private T[] m_array;
  
  //IEnumerator<T>的Current属性时T类型
  public T Current{get{...}}
  ...
}

泛型委托:

CLR支持泛型委托,目的是保证任何类型的对象都能以类型安全的方式传给回调方法。此外泛型委托允许值类型实例在传给回调方法时不进行任何装箱。(如果定义的委托类型指定了类型参数,编译器会定义委托类的方法,用指定的类型参数替换方法的参数类型和返回值类型。)
建议尽量使用FCL预定义的泛型Action和Func委托。
委托的每个泛型类型参数都可标记为 协变量逆变量

泛型类型参数可以是以下任何一种形式:(只有接口和委托类型的类型参数能设置为in或者out)

  • 不变量 :意味着泛型类型参数不能更改。(无修饰符修饰)
  • 逆变量 :意味着泛型类型参数可以从一个类更改为它的某个派生类。在C#中是用in关键字标记逆变量形式的泛型类型参数。逆变量泛型类型参数只出现在输入位置,比如作为方法的参数。
  • 协变量 :意味着泛型类型参数可以从一个类更改为它的某个基类。在C#中是用out关键字标记协变量形式的泛型类型参数。协变量泛型类型参数只能出现在输出位置,比如作为方法的返回类型。
泛型委托参数形式演示代码如下:
public delegate TResult Func<in T,out TResult>(T arg);

Func<Object,ArgumentException> fn1 = null;

Func<String,Exception> fn2 = fn1;  //不需要显式转型。
Exception e = fn2(" ");

上述代码的意义:fn1变量引用一个方法,获取一个Object,返回一个ArgumentException。而fn2变量引用另一个方法,获取一个String,返回一个Exception。由于可将一个String传给期待Object的方法(因为String从Object派生),而且由于可以获取返回ArgumentException的一个方法的结果,并将这个结果当成一个Exception(因为Exception是ArgumentException的基类),所以上述代码能正常编译,而且编译时能维持类型安全性。

使用要获取泛型参数和返回值的委托时,建议尽量为逆变性和协变性指定in和out关键字。这样做不会有不良反应,并使你的委托能在更多的情形中使用。
和委托类似,具有泛型类型参数的接口也可将类型参数标记为逆变量和协变量。
泛型接口参数形式演示代码如下:
public interface IEnumerator<in T> : IEnumerator
{
  Boolean MoveNext();
  T Current{get;}
}
//由于T是逆变量,所以以下代码可以顺利编译和运行:
//这个方法接受任意引用类型的一个IEnumerable
Int32 Count(IEnumerable<Object> collection) {...}
...
//以下调用向Count传递一个IEnumerable<String>
Int32 c = Count(new[] {"Grant"});
在声明泛型类型参数时,必须由使用者显式使用in或out来标记可变性。以后使用这个类型参数时,加入用法与声明时指定的不符,编译器就会报错,提醒使用者违反订立的协定。

泛型方法

定义泛型类、结构或接口时,类型中定义的任何方法都可引用类型指定的类型参数。类型参数可作为方法参数、方法返回值或方法内部定义的局部变量的类型使用。与此同时,CLR还允许方法指定它自己的类型参数。这些类型参数也可作为参数、返回值或局部变量的类型使用。
泛型方法参数形式演示代码如下:
internal sealed class GenericType<T>
{
  private T m_value;
  
  public GenericType(T value){m_value = value;}

  public TOutput Converter<TOutput>()
  {
    TOutput result = (TOutput)Convert.ChangeType(m_value,typeof(TOutput));
    return result;    //返回类型转换之后的结果  
  }
}

可验证性和约束

如果一个泛型没有提供任何约束,那么编译器不能保证该泛型类中的方法适用于所有类型,所以很容易报错。
编译器和CLR支持称为约束的机制。
约束 :
  • 作用 :限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型执行更多操作。
  • 关键字 :where
  • 约束可应用于泛型类型的类型参数,也可应用于泛型方法的类型参数。CLR不允许基于类型参数名称或约束来进行重载;只能基于元数(类型参数个数)对类型或方法进行重载。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容