• C#泛型基础


    1.基本概念

    .NET2.0新增的最大的特性是泛型。

    我们先来看下定义在System.Collections.Generic下的List<T>:

    public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable

    List类后边的紧跟着一个<T>,T被称为类型参数(type parameter),是一个真实实参的占位符,表明该类未定义实际的数据类型。
    实际使用的时候,需要指定T的具体类型,如:List<string>,此处的string被称为类型实参(type argument),List<T>下所有的T都会被替换成string类型。

    2.泛型的优点:实现了编译时的类型安全和算法重用

    在泛型出现以前,只能通过传递object类型的参数或者返回object类型来实现通用性的方法。
    比如IComparable接口的CompareTo方法签名如下:

    int CompareTo(object obj)

    由于System.Object是所有对象的基类,存在任意类型到object的隐式转换,所以可以向CompareTo方法传递任意类型的参数。
    以下是int的IComparable接口实现:

    public int CompareTo(object value)
    {
        if (value == null)
        {
            return 1;
        }
        if (!(value is int))
        {
            throw new ArgumentException(Environment.GetResourceString("Arg_MustBeInt32"));
        }
        int num = (int)value;
        if (this < num)
        {
            return -1;
        }
        if (this > num)
        {
            return 1;
        }
        return 0;
    }


    通过分析代码存在以下的问题:
    1.编译时可以传递任意参数,但是实际上只有传递一个int类型的参数才有实际意义。假如传递一个非int类型的参数,会抛出异常。
    2.当传递一个int类型的实参时,需要进行装箱和拆箱操作,会影响性能。

    再看int对泛型接口IComparable<T>的实现:

    public int CompareTo(int value)
    {
        if (this < value)
        {
            return -1;
        }
        if (this > value)
        {
            return 1;
        }
        return 0;
    }

    只能向方法传递一个int类型,实现了编译时的安全同时避免了拆箱和装箱的操作。
    而其他类型通过实现各自的IComparable<T>接口,同样实现了算法的重用。

    再来举一个例子。

    场景:向列表添加若干数字,计算数字的和。
    先来看使用ArrayList的实现:

    ArrayList list = new ArrayList();
    list.Add(1);
    list.Add(2);
    list.Add(3.0);
    int count = 0;
    for (int i = 0; i < list.Count; i++)
    {
        count += (int)list[i];
    }

    这段代码编译的时候能通过,运行时会报InvalidCastException异常,类型转换失败。

    ArrayList的Add方法的签名为:Add(object value),所以每次使用Add方法向列表添加int对象时,先要进行装箱操作。
    此处故意使用了for循环而不是foreach循环,就是为了清晰的体现其中的拆箱操作以及类型转换。

    再来看使用List<T>的实现:

    List<int> list = new List<int>();
    list.Add(1);
    list.Add(2);
    //list.Add(3.0);
    int count = 0;
    for (int i = 0; i < list.Count; i++)
    {
        count += list[i];
    }

    注释掉的那行在编译时报错,方法参数不匹配。

    我们使用int来替换T,表示list中的所有T都替换为int类型。
    List<T>的Add方法签名为:void Add(T item),鼠标移动到Add方法上,可以明显的看到方法的参数属性类型以及被替换为int,同理list[i]也是int类型。

    3.泛型约束

    假如你编译以下代码:

    T Foo<T>()
    {
        return new T();
    }

    编译的时候会提示错误:“变量类型“T”没有 new() 约束,因此无法创建该类型的实例 ”。
    将代码修改如下可以通过编译:

    T Foo<T>() where T : new()
    {
        return new T();
    }

    此处通过where对泛型添加了约束。以下为可以添加的约束类型(MSDN):

    约束

    说明

    T:struct

    类型参数必须是值类型。Nullable can be specified.' data-guid="6eb132c88993822c7977723af20162ad">Nullable can be specified.'>Nullable can be specified.' data-guid="6eb132c88993822c7977723af20162ad">可以指定除 Nullable <T>以外的任何值类型。

    T:class

    类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。

    T:new()

    类型参数必须具有无参数的公共构造函数。new() constraint must be specified last.' data-guid="012d33600c2a5348f794e49b607a570e">new() constraint must be specified last.'>new() constraint must be specified last.' data-guid="012d33600c2a5348f794e49b607a570e">当与其他约束一起使用时,new() 约束必须最后指定。

    T:<基类>

    类型参数必须是指定的基类或派生自指定的基类(非密封类)。不能指定以下的类型:Object,Array,Delegate,MulticastDelegate,ValueType,Enum和Void

    T:<接口>

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

    T:U

    为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。


    假如没有泛型约束功能,只能对泛型实例赋值或者使用object定义的方法。
    通过使用泛型约束限制T的类型范围,才能在T的类型或者实例上调用相应的方法。

    示例:

    T Foo<T>(T t1,T t2) where T :IComparable<T>
    {
        if (t1.CompareTo(t2) > 0)
        {
            return t1;
        }
        return t2;
    }
    很显然的只有对象实现了IComparable<T>接口,才能在对象的实例上调用CompareTo方法。

    4.泛型方法

    泛型类中的方法定义了自己的类型参数时,才能作为一个泛型方法。
    如下代码:只有方法二才是一个泛型方法,方法一不过是使用了泛型类的类型参数作为形参或者返回结果而已。

    public class Test<T>
    {
        public T GetInster()
        {
            return default(T);
        }
    
        public TValue GetFirstItem<TValue>(TValue[] list)
        {
            if (list == null)
            {
                throw new ArgumentNullException();
            }
            if (!list.Any())
            {
                throw new ArgumentException();
            }
            return list[0];
        }
    }

    编译器在使用泛型方法时,能够通过变量的数据类型自动推断要使用的类型,使用上面的GetFirstItem作为示例:

    int[] list = new int[] { 1, 2, 3 };
    GetFirstItem<int>(list);
    GetFirstItem(list);

    第三行的方法根据参数的类型,自动的推导出类型参数为int。

    5.泛型的协变和逆变

    泛型委托和泛型接口的类型参数可以标记为协变性量或者逆变量

    逆变量:泛型类型参数可以从一个基类变为该类的派生类,使用in关键字标记类型参数,逆变量只能出现在输入位置:方法的参数或者set访问器中的方法。

    协变量:泛型类型参数可以从一个派生类变为该类的基类,使用out关键字标记类型参数,协变量只能出现在输出位置:方法的返回值。

    对以下的泛型委托类型定义:

    delegate TResout Func<in T, out TResout>(T t);

    T被申明为逆变量,TResout被申明为协变量。

    MyFunc<object, string> fn1 = s => s.ToString();
    MyFunc<string, object> f2 = fn1;

    所以在需要一个object对象参数时,可以传递一个string类型对象。
    返回一个string类型的,可以被视为object类型

    6.其他

    6.1泛型反射

    查看

    typeof(List<int>).ToString()

    可以发现泛型的类型名为“System.Collections.Generic.List`1[System.Int32]”。
    前面是List类的类型名,紧跟着一个`和一个数字,数字表示类型的参数个数,再接着参数的类型。

    6.2泛型类的构造函数

    针对不同的类型参数,泛型类只在第一次调用的时候实时编译,所以第一次调用一个静态类会造成性能损失,以后再调用这个参数就能直接获取了。同时所有的引用类型实参都只会编译一次,因为所有的引用类型都是指向托管堆的指针。假如在某个泛型类中定义了静态字段或者静态构造器,针对不同类型的泛型实现,都拥有各种的静态字段,

    6.3泛型实例的默认值

    可以通过default(T)来设置泛型实例的默认值,类似逻辑为T是typeof(T).IsValueType ? 0 : null(此处的0是指将值类型的所有位设置为0)。

  • 相关阅读:
    MVC思想-程序的控制流程-Struts2和SpringMVC黑马流程图
    代理模式
    显卡
    感悟:Java新手一点想法
    java企业级开发的实质就是前台后台如何交互的-各个对象之间如何交互,通信的-程序执行的流程是怎样的
    $.ajax()方法详解--极快瑞中的阿贾克斯函数
    初学者必读之AJAX简单实例2
    初学者必读原生AJAX-异步的javaScript和XML
    c#输入方法名来调用方法(反射)
    unity接入讯飞教程
  • 原文地址:https://www.cnblogs.com/ylws/p/2592304.html
Copyright © 2020-2023  润新知