Keep on going never give up.

Let's Go

C# 学习笔记(55)全面解析泛型

C#Lonely2019-06-10 18:28:1987次0条

类型参数

根据泛型类型参数是否提供实际数据,又可把泛型分为两类:未绑定的泛型已构造的泛型如果没有为类型参数提供实际数据,此时的泛型被称为未绑定的泛型;而如果已指定了实际数据类型作为参数,则此时的泛型被称为已构造的泛型。

已构造的泛型又可分为开放类型密封类型。其中,开放类型是指包含类型参数的泛型,而封闭类型则是指那些已经为每一个类型参数都传递了实际数据类型的泛型。

//示例:
//Dictionary<,> 开放类型
Type t = typeof(Dictionary<,>);
Console.WriteLine(t.ContainsGenericParameters); //true
//Dictionary<int,string> 封闭类型
t = typeof(Dictionary<int,string>);
Console.WriteLine(t.ContainsGenericParameters); //false
Console.ReadKey();

以上代码首先用typeof操作符获得了类型声明,然后通过Type.ContainsGenericParameters属性来判断类型对象是否包含未被实际类型替代的类型参数。如果存在未替代的类型参数,则返回true,表明此时泛型类型为开放类型;否则为封闭类型。


泛型类型的静态字段和静态函数问题

静态数据类型是属于类型的,对于静态字段来说,如果在每个MyClass类中定义了一个静态字段x,则不管之后还创建了多少个该类的实例,也不管从该类派生出多少个实例,都只存在一个MyClass.x字段。但泛型类型却并非如此,每个封闭的泛型类型中都有仅属于它自己的静态数据。

//泛型类
public static class Test<T>
{
    public static string field; //静态字段
    public static void Print() //静态函数
    {
        Console.WriteLine(field+" :"+typeof(T).Name);
    }
}

//非泛型类
public static class MyClass
{
    public static string field; //静态字段
    public static void Print() //静态函数
    {
        Console.WriteLine(field);
    }
}

class Program
{
    static void Main(string[] args)
    {
        //示例:
        //非泛型类
        MyClass.field = "张三";
        MyClass.field = "李四";
        MyClass.field = "钻石王老五";
        MyClass.Print();

        //泛型类
        Test<int>.field= "一";
        Test<int>.Print();

        Test<string>.field = "二";
        Test<string>.Print();

        Test<object>.field = "二";
        Test<object>.Print();

        Console.ReadKey();
    }
}

运行结果:

image.png

从以上结果可以看出,每个封闭的泛型类型都有属于它自己的静态字段。这是因为,在使用实际类型参数代替泛型参数时,编译器会根据不同的类型实参,重新生成类型。对于编译器来说,每个封闭泛型类型都是一个不一样的类型,所以它们都有属于它们自己的静态字段。对于静态构造函数,道理也是如此,每个封闭的泛型类型都会有一个静态构造函数。每个封闭泛型类型都会调用其构造函数,对于非泛型类型,其构造函数只会调用一次。


类型参数的推断

由于使用泛型时都需要写“<”和“>”等符号,在阅读代码时,一旦代码变多,难免令开发人员感觉头晕。通过使用编译器的类型推断,你便可以在写泛型代码时省略掉这些符号,具体的实际类型则交由编译器自行推断。

class Program
{
    static void Main(string[] args)
    {
        //示例:
        int n1 = 10;
        int n2 = 20;

        //不使用类型推断的代码
        Tset<int>(ref n1, ref n2);
        //使用类型推断的代码
        Tset(ref n1, ref n2);
        Console.WriteLine("n1值:"+ n1);
        Console.WriteLine("n2值:" + n2);

        //无法推断出方法的类型参数
        string str = "string";
        object obj = "object";
        Tset(ref str, ref obj);

        Console.ReadKey();
    }

    public static void Tset<T>(ref T t1, ref T t2)
    {
        T temp = t1;
        t1 = t2;
        t2 = temp;
    }
}

在以上代码中,编译器会根据传递的方法实参来判断传入的实际类型参数。如果编译器根据传入的参数不能推断出实际参数类型,就会出现编译时错误。使用类型推断后,可以省略“<”和“>”,从而在泛型代码变多时,增强代码可读性。类型推断只能用于泛型方法,它对泛型类则并不适用,因为编译器不能通过泛型类的构造函数推断出实际的类型参数。


类型参数约束

类型参数约束就是限制类型参数只能代表某些符合要求的类型。类型约束用where关键字来限制某个类型实参的类型。如以下代码中的where T: IComparable<T>语句就使类型参数继承于IComparable<T>接口,这样就可以保证传入的类型实参都具有CompareTo方法了。

//比较两个值的大小,返回大的那个
public static T GetMax<T>(T obj1, T obj2) where T:IComparable<T>
{
    if (obj1.CompareTo(obj2) > 0)
    {
        return obj1;
    }
    return obj2;
}

C#中有4种约束可以使用,它们的语法类似:约束要放在泛型方法或类型声明的结尾,并且使用where关键字。


引用类型约束

引用类型约束表示形式为T:class,它确保传递的类型实参必须是引用类型。注意:约束的类型参数和类型本身没有关系,即在定义一个泛型结构体时,泛型类型一样可以被约束为引用类型。此时,结构体类型本身是值类型,而类型参数约束为引用类型,它可以为任何的类、接口、委托或数组等,但不能指定以下这些特殊的引用类型:System.Object、System.Array、System.Delegate、System.MulticastDelegate、System.ValueType、System.Enum和System.Void。

代码示例:

public class SampleReference<T> where T : Stream
{
    public void Test(T stream)
    {
        stream.Close();
    }
}

在以上代码中,类型参数T设置了引用类型约束。where T:Stream的意思就是告诉编译器:传入的类型必须是System.IO.Stream,或者是从Stream派生的一个类型。如果一个类型参数没有指定约束,则默认T为System.Object类型。但若你在代码中显式地指定了System.Object约束,则编译器会报错:约束类型不能是特殊类object。


值类型约束

值类型约束的表示形式为T : struct它确保传递的类型实参是值类型(包括枚举),但这里的值类型不包括可空类型。

代码示例:

public class SampleReference<T> where T : struct
{
    public static T Test()
    {
         return new T();
    }
}

在上面的代码中,new T()是可以通过编译的,因为T是一个值类型,而所有值类型都有一个公共的无参构造函数。但如果不对T进行约束,或约束为引用类型,则上面的代码就会报错,因为有的引用类型是没有公共的无参数构造函数的。


构造函数类型约束

构造函数类型约束的表示形式为T:new(),如果类型参数有多个约束,则此约束必须最后指定。构造函数类型约束确保指定的类型实参有一个公共无参构造函数的非抽象类型。这适用于所有值类型,所有非静态、非抽象、没有显式声明构造函数的类,以及显式声明了一个公共无参构造函数的所有非抽象类。这里需要注意,如果同事声明指定构造器约束和struct约束,C#编译器会认为这是一个错误。因为这样指定是多余的,所有值类型都会隐式地提供了一个无参公共构造函数。就如同定义接口时指定访问类型为public一样,编译器会报错,因为接口一定是public的,编译器认为这样做是多余的。


转换类型约束

转换类型约束的表示形式为T:基类名、T:接口名、T:U。T:基类名确保指定的类型实参必须是基类或派生自基类的之类;T:接口名确保指定的类型实参必须是接口或实现了该接口的类;而T:U则确保T提供的类型实参必须是U提供的类型实参或派生于U提供的类型实参,即前面一个类型实参必须是后面的类型实参或后面类型实参子类。

转换约束示例

image.png


组合约束

组合约束是将多个不同种类的约束合并在一起的情况。这里需要注意,没有任何一种类型即是引用类型又是值类型,所以引用约束和值约束不能同时使用如果存在多个转换类型约束,且其中一个是类,则类必须放在接口前面。不同的类型参数可以有不同的约束,但每种类型参数必须分别使用一个单独的where关键字。

组合约束示例

image.png

暗锚,解决锚点偏移

文章评论

    嘿,来试试登录吧!