C#中泛型<T>(特殊的占位符)的使用详解

73 篇文章 3 订阅
46 篇文章 0 订阅

泛型(Generic),是将不确定的类型预先定义下来的一种C#高级语法,我们在使用一个类,接口或者方法前,不知道用户将来传什么类型,或者我们写的类,接口或方法相同的代码可以服务不同的类型,就可以定义为泛型。这会大大简化我们的代码结构,同时让后期维护变得容易。

泛型很适用于集合,我们常见的泛型集合有:List<T>,Dictionary<K,V>等等(T,K,V就代表不确定的类型,它是一种类型占位符),无一不是利用的泛型这一特性,若没有泛型,我们会多出很多重载方法,以解决类型不同,但是执行逻辑相同的情况。

看看上面这一堆介绍看着都想睡觉了,没办法,CSDN要求文字个数不够就不给推荐,估计这些简介也没几个人会去仔细看的哈,哈哈哈。

废话一大堆,那么泛型到底长啥样?,看到下面代码中的 “T” 没有,那就是泛型。

namespace 泛型
{
    class Program
    {
        static void Main(string[] args)
        {
            Test<string>();
 
            Console.ReadKey();
        }
 
        static void Test<T>()
        {
            Console.WriteLine(typeof(T).FullName);
        }
    }
}
输出:System.String

你可能会问,这个 “T” 就是泛型啊?把 “T” 改成鸡,那鸡不也是泛型了?

namespace 泛型
{
    class Program
    {
        static void Main(string[] args)
        {
            Test<string>();
            Console.ReadKey();
        }
 
        static void Test<鸡>()
        {
            Console.WriteLine("鸡");
        }
    }
}
是的,没错,输出:鸡

/************************************************

一、简介
简单记录一下C#中泛型类,泛型方法,泛型委托等泛型的使用,从.NetFramework2.0开始,支持泛型,通过反编译可以看到使用泛型的地方,都是使用了占位符 `1,当运行的时候,会将占位符替换成对应的类型

二、泛型方法
泛型方法就是为了解决不同类型要使用同样方法的问题,泛型方法需要在方法名的后面带一个<T>,T是类型参数,只是一个占位符,也可以用别的名字代替

2.1 泛型方法的声明

//实现string 转换为其他类型
public static T TypeConvert<T>(string val)
{
    if (String.IsNullOrWhiteSpace(val))
        return default(T);
    if (typeof(T).IsEnum)
         return (T)Enum.Parse(typeof(T), val, true);
    return (T)Convert.ChangeType(val, typeof(T));
}

2.2 泛型方法的调用
int num = TypeConvert<int>("1");//将字符l类型的1转换成int 类型的1
三、泛型类和泛型接口
3.1 泛型类的声明
泛型类就是一个类可以实现多个类型的需求

我们常用的集合List<T> 就是一个泛型类,可以转到定义看到其声明 public class List<T> 

当然我们进行实例化的时候,必须要指定类型,List<string> names = new List<string>(); 

3.2 泛型接口
泛型接口,就是一个接口 满足多个多个类型的需求

//声明泛型接口
public interface GenericInterface<T>
{
}
四、泛型委托
泛型委托与普通的委托相比,它可以适应更多的类型

微软官方提供了两种泛型委托,一个是Action,无返回值,另一个是Func,带返回值

我们可以自定义泛型委托,如下所示

public delegate void GenericDelegate <T>(T para);

五、泛型缓存
泛型缓存: 会根据传入的不同类型,分别生成不同的副本 。主要针对不同类型,每次都会调用都会产生与之对应的相同的数据的现象,可以使用泛型缓存,将第一次生成的数据存放在缓存中,再次调用时直接返回该数据。

5.1 泛型缓存声明

public class GenericCache<T>
{
  static GenericCache()
  {
    _Sql = GetSql(T);
  }

  private static string _Sql = "";

  public static string GetCache()
  {
    return _Sql;
  }
}

5.2 泛型缓存调用
//对于int 和 string 这两种类型,会生成不同的副本,生成的sql不一样,当第二次调用时,则不需要再重新生成sql,可直接返回第一次生成的sql,对于不同的类型,缓存的数据也都不同
GenericCache<int>.GetCache();
GenericCache<string>.GetCache();
六、泛型约束
泛型约束,顾名思义,就是传入的类型进行约束,防止任何类型都可以传进去导致出现异常


public static void GenericMethod<T>(T t)
        where T : class // 引用类型约束  就只能传入引用类型的参数
{
}

public static void GenericMethod1<T>(T t)
        where T : struct // 结构体类型   int float ...
{
}

public static void GenericMethod2<T>(T t)
        where T : new() // 无参数公共构造函数约束
{
}

public static void GenericMethod3<T>(T t)
        where T : P //基类约束,传入类型必须是P或P的派生类
{
}
public static void GenericMethod4<T>(T t)
        where T : IP //接口约束,传入类型必须实现接口IP
{
}

七、协变与逆变
7.1 协变跟逆变是对泛型类的继承关系的表述
在使用泛型的时候,会存在一些不和谐的地方,首先准备两个继承关系的类看一下


public class Animal
{
    public int TypeName { get; set; }
}

public class Cat: Animal
{
    public int Id { get; set; }
}

Cat继承于Animal,当使用集合List时,就会发现存在矛盾

Animal cat = new Cat();
List<Cat> cats = new List<Cat>();
List<Animal> animals = new List<Animal>();
List<Animal> animal_cats = new List<Cat>();//编译器报错
按理说,一只猫是一个动物,一群猫也应该是一群动物才对,这里创建时就会发现编译器会报错,原因就是List<Animal> 与 List<Cat>是两个类,不存在继承关系,在这里就需要使用到协变和逆变了

7.2 协变
使用IEnumerable实现

IEnumerable<Animal> cats = new List<Cat>();
也可自己定义模仿IEnumerable 定义接口实现,需要使用到out


public interface IMyListOut<out T>
{
    T Show();
}
public class MyListOut<T> : IMyListOut<T>
{
    public T Show()
    {
      return default;
    }
}

IMyListOut<Animal> myListOut = new MyListOut<Cat>();//使用
在使用协变时,需要使用out 修饰,并且左边只能传入基类,只能作为返回值而不能作为参数

7.3 逆变
逆变需要使用到关键字in


public interface IMyListIn<in T>
{
    void Show(T t);
}

public class MyListIn<T> : IMyListIn<T>
{
    public void Show(T t)
    {
         Console.WriteLine(t.GetType());
    }
}

IMyListIn<Cat> myListIn = new MyListIn<Animal>();
in用于修饰传入的子类,且只能为参数,不可为返回值

7.4 协变与逆变同时使用 

public interface IMyListOutAndIn<in Tin, out Tout>
{
    void ShowT(Tin t);
    Tout GetT();
    Tout Together(Tin t);
}
public class MyListOutAndIn<Tin, Tout> : IMyListOutAndIn<Tin, Tout>
{
    public Tout GetT()
    {
        return default;
    }

    public void ShowT(Tin t)
    {
        Console.WriteLine(t.GetType());
    }

    public Tout Together(Tin t)
    {
        Console.WriteLine(t.GetType());
        return default;
    }
}    

IMyListOutAndIn<Cat, Animal> myListOutAndIn = new MyListOutAndIn<Animal, Cat>();

/************************************************

C# 泛型详解(泛型类,方法,接口,委托,约束,反射 )

一、什么是泛型

二、为什么要用泛型

三、泛型和Object类型的区别

四、泛型类

五、泛型方法

六、泛型接口

七、泛型委托

八、泛型约束

九、泛型配合反射

结束

一、什么是泛型
先看一段介绍

泛型(Generic),是将不确定的类型预先定义下来的一种C#高级语法,我们在使用一个类,接口或者方法前,不知道用户将来传什么类型,或者我们写的类,接口或方法相同的代码可以服务不同的类型,就可以定义为泛型。这会大大简化我们的代码结构,同时让后期维护变得容易。

泛型很适用于集合,我们常见的泛型集合有:List<T>,Dictionary<K,V>等等(T,K,V就代表不确定的类型,它是一种类型占位符),无一不是利用的泛型这一特性,若没有泛型,我们会多出很多重载方法,以解决类型不同,但是执行逻辑相同的情况。

看看上面这一堆介绍看着都想睡觉了,没办法,CSDN要求文字个数不够就不给推荐,估计这些简介也没几个人会去仔细看的哈,哈哈哈。

废话一大堆,那么泛型到底长啥样?,看到下面代码中的 “T” 没有,那就是泛型。

namespace 泛型
{
    class Program
    {
        static void Main(string[] args)
        {
            Test<string>();
 
            Console.ReadKey();
        }
 
        static void Test<T>()
        {
            Console.WriteLine(typeof(T).FullName);
        }
    }
}
输出:System.String

你可能会问,这个 “T” 就是泛型啊?把 “T” 改成鸡,那鸡不也是泛型了?

namespace 泛型
{
    class Program
    {
        static void Main(string[] args)
        {
            Test<string>();
            Console.ReadKey();
        }
 
        static void Test<鸡>()
        {
            Console.WriteLine("鸡你太美");
        }
    }
}
是的,没错,输出:鸡你太美

二、为什么要用泛型
先看一个例子

namespace 泛型
{
    class Program
    {
        static void Main(string[] args)
        {
            int result = Test.Add(3, 4);
            Console.WriteLine(result);
 
            Console.ReadKey();
        }
 
    }
 
    public class Test
    {
        public static int Add(int x, int y)
        {
            return x + y;
        }
 
        public static int Add(string x, string y)
        {
            return int.Parse(x) + int.Parse(y);
        }
    }
}
比如,我们需要封装一个加法的算法,但是传入的类型,可能有 int 类型,也可能有 string 类型,还可能有其他的类型,但是每多一种类型,你就要多加一个方法,非常的麻烦,臃肿。

那么有没有方法来解决这个问题呢,于是后面C#2.0中,就有了泛型这个概念,它并不是语法糖,看字面意思像 “泛滥的类型” 的意思。

那么下面就用泛型的方式来封装一下这个加法运算。

注意一下写法,在 Add 后面要加上 <T> 这句,不然会报错。

正确方式

下面是封装加法的完整代码

public class Test
{
    public static int Add<T>(T x,T y)
    {
        try
        {
            return int.Parse(x.ToString()) + int.Parse(y.ToString());
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        return -1;
    }
}
测试第一种

class Program
{
    static void Main(string[] args)
    {
        int result = Test.Add("3", "4");
        Console.WriteLine(result);
 
        Console.ReadKey();
    }
}
输出:7

测试第二种

class Program
{
    static void Main(string[] args)
    {
        int result = Test.Add(4, 5);
        Console.WriteLine(result);
 
        Console.ReadKey();
    }
}
输出:9

三、泛型和Object类型的区别
看了上面的案例,你会觉得,泛型是不是 Object 类型不是也差不多?下面是一些介绍:

C# 中 Object 是一切类型的基类,可以用来表示所有类型,而泛型是指将类型参数化以达到代码复用提高软件开发工作效率的一种数据类型。

你可以将泛型理解成替换,在使用的时候将泛型参数替换成具体的类型,这个过程是在编译的时候进行的,使用泛型编译器依然能够检测出类型错误。泛型不用拆箱装箱。

而 Object 表示其他类型是通过类型转换来完成的,而所有类型转化为 Object 类型都是合法的,所以即使你先将 Object 对象赋值为一个整数再赋值为一个字符串,编译器都认为是合法的。

Object 类型

> 优点:
> 1. object类型可以用来引用任何类型的实例;
> 2. object类型可以存储任何类型的值;
> 3. 可以定义object类型的参数;
> 4. 可以把object作为返回类型。

> 缺点:
> 1. 会因为程序员没有记住使用的类型而出错,造成类型不兼容;
> 2. 值类型和引用类型的互化即装箱拆箱使系统性能下降。

泛型,Object 类型,var 类型,这三种类型,看着很类似,但其实泛型的作用更不止如此,还有泛型类,泛型方法,泛型接口,泛型约束等,下面分别来介绍这几个功能

四、泛型类
泛型类是指这个类的某些字段的类型是不确定的,只有在构造的时候才能确定下的类,泛型类一般在数据结构中用的比较多,比如 C# 自带的 List<T>,Dictionary<TKey, TValue> 等,我也写过自定义 List 相关的帖子,有兴趣的可以去看下

C# 自定义List_熊思宇的博客-CSDN博客_c#定义list

1.泛型类案例

下面看一个例子

namespace 泛型
{
    class Program
    {
        static void Main(string[] args)
        {
            Test<int> test = new Test<int>();
            test.Add(1);
            test.Add(3);
            Console.WriteLine(test.Count);
 
            Console.ReadKey();
        }
    }
 
    public class Test<T> 
    {
        private List<T> list = new List<T>();
 
        public int Count => list.Count;
 
        public void Add(T t)
        {
            if(t != null)
                list.Add(t);
        }
    }
}
运行后输出:2

Test 类此时并没有添加任何的约束,这个是不推荐的,至少应该加一个,如下

public class Test<T> where T : new()
{
    private List<T> list = new List<T>();
 
    public int Count => list.Count;
 
    public void Add(T t)
    {
        if(t != null)
            list.Add(t);
    }
}
关于泛型约束,在后面的章节中会有详细的介绍。

2.泛型类继承

泛型类型继承写法:

public class Test1<T>
{
  
}
 
public class Test2<T> : Test1<T>
{
 
}
指定基类的类型写法:

public class Test1<T>
{
  
}
 
public class Test2<T> : Test1<int>
{
 
}
关于泛型类的继承,后面我再单独写文章进行介绍吧

五、泛型方法
泛型方法是指通过泛型来约束方法中的参数类型,如果没有泛型,每次方法中的参数类型都是固定的,不能随意更改,在使用泛型后,方法中的数据类型则有指定的泛型来约束,即可以根据提供的泛型来传递不同类型的参数。

泛型方法的返回类型和参数类型都可以使用泛型。如下:

public static T GetT<T>(T a)
{
    return a;
}
泛型方法同样可以使用泛型约束,只是泛型方法中的 T 只能用在方法内部和返回值,而泛型类中的 T 可以应用于整个类的字段和方法。

public class Test
{
    public void Add<T>(T t) where T : new()
    {
        List<T> list = new List<T>();
        if (t != null) list.Add(t);
    }
}
如果要对 T 类型作一些操作,则需要用到反射。

六、泛型接口
泛型接口如下

public interface IFace<T>  
{  
    void SayHi(T msg);  
}  
同样的,泛型接口也可以使用泛型约束

public interface IFace<T> where T : new() 
{  
    void SayHi(T msg);  
}  
实现泛型接口有两种情况

/// <summary>  
/// 1.普通类实现泛型接口  
/// </summary>  
public class MyClass1 : IFace<string>  
{  
    public void SayHi(string msg)  
    {  
        Console.WriteLine(msg);  
    }  
}  
 
/// <summary>  
/// 2.泛型类继承泛型接口  
/// </summary>  
/// <typeparam name="T"></typeparam>  
public class MyClass2<T> : IFace<T>  
{  
    public void SayHi(T msg)  
    {  
        Console.WriteLine(msg);  
    }  

案例

namespace 泛型
{
    class Program
    {
        static void Main(string[] args)
        {
            IFace<Msg> face = new Test();
            face.SayHi(new Msg());
            Console.ReadKey();
        }
    }
 
    public interface IFace<T> where T : new()
    {
        void SayHi(T msg);
    }
 
    public class Test : IFace<Msg>
    {
        public void SayHi(Msg msg)
        {
            Console.WriteLine(msg.MyName);
        }
    }
 
    public class Msg
    {
        public string MyName = "张三";
    }
}
输出:张三

七、泛型委托
泛型委托和普通委托差别不大,只是参数由具体类型变成了 "T"

public delegate void MyDelegate<T>(T args); 
案例

namespace 泛型
{
    class Program
    {
        delegate void MyDelegate<T>(T args);
 
        static void Main(string[] args)
        {
            MyDelegate<string> myDelegate = SayHi;
            myDelegate("哈喽");
 
            Console.ReadKey();
        }
 
        static void SayHi(string msg)
        {
            Console.WriteLine(msg);
        }
    }
}
输出:哈喽

八、泛型约束
泛型约束就是对 “T” 类型的数据做一定的限制,防止在运用过程中,做一些违规的操作,来保证数据的安全。

六种类型的约束:

T:结构

类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型。有关更多信息,请参见使用可空类型(C# 编程指南)。

T:类

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

T:new()

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

T:<基类名>

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

T:<接口名称>

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

T:U

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

案例

namespace 泛型
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.ReadKey();
        }
    }
 
    /// <summary>  
    /// 泛型接口  
    /// </summary>  
    /// <typeparam name="T"></typeparam>  
    public interface IFace<T>
    {
        void SayHi(T msg);
    } 
 
    /// <summary>  
    /// 泛型约束  
    /// </summary>  
    public class MyClass1<T, K, V, W, X,Y>
        where T : struct        //约束 T 必须是值类型  
        where K : class         //约束 K 必须是引用类型   
        where V : IFace<T>      //约束 V 必须实现 IFace 接口  
        where W : K             //约束 W 必须是 K 类型,或者是 K 类型的子类  
        where X : class, new()  //约束 X 必须是引用类型,并且有一个无参数的构造函数,当有多个约束时,new()必须写在最后  
        where Y : MyClass2      //约束 Y 必须是 MyClass2 类型,或者继承于 MyClass2 类
    {
        public void Add(T num)
        {
            Console.WriteLine(num);
        }
    }
 
    public class MyClass2
    {
 
    }
}
这个案例,几乎介绍了所有的泛型约束的使用方法,有兴趣的可以亲自动手写一写。

九、泛型配合反射
现在有一个需求,用户修改个人资料,在提交这里必须要判断是否有修改内容,如果没有修改,点击提交按钮则不提交,那要怎么判断呢?

有人可能会说,用 if...else 去判断就好了,可以这么写没错,但资料如果有几百个地方改了,不会还用 if...else 吧,那整篇代码全是 if...else 了,就像我同事开玩笑一样:“我不会高数,我只会写 if...else...”

下面这个案例就教你如何来解决这个问题。

代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
 
namespace 泛型
{
    class Program
    {
        static void Main(string[] args)
        {
            Test test1 = new Test();
            test1.Names = "张三";
            test1.Age = 24;
            test1.Height = 179.9f;
            Test test2 = new Test();
            test2.Names = "李四";
            test2.Age = 31;
            test2.Height = 163.4f;
 
            bool result = CompareType(test1, test2);
            Console.WriteLine("是否相同:" + result);
 
            Console.ReadKey();
        }
 
        /// <summary>
        /// 比较--两个类型一样的实体类对象的值
        /// </summary>
        /// <param name="oneT"></param>
        /// <returns>返回true表示两个对象的数据相同,返回false表示不相同</returns>
        public static bool CompareType<T>(T oneT, T twoT)
        {
            bool result = true;//两个类型作比较时使用,如果有不一样的就false
            Type typeOne = oneT.GetType();
            Type typeTwo = twoT.GetType();
            //如果两个T类型不一样  就不作比较
            if (!typeOne.Equals(typeTwo)) { return false; }
            PropertyInfo[] pisOne = typeOne.GetProperties(); //获取所有公共属性(Public)
            PropertyInfo[] pisTwo = typeTwo.GetProperties();
            //如果长度为0返回false
            if (pisOne.Length <= 0 || pisTwo.Length <= 0)
            {
                return false;
            }
            //如果长度不一样,返回false
            if (!(pisOne.Length.Equals(pisTwo.Length))) { return false; }
            //遍历两个T类型,遍历属性,并作比较
            for (int i = 0; i < pisOne.Length; i++)
            {
                //获取属性名
                string oneName = pisOne[i].Name;
                string twoName = pisTwo[i].Name;
                //获取属性的值
                object oneValue = pisOne[i].GetValue(oneT, null);
                object twoValue = pisTwo[i].GetValue(twoT, null);
                //比较,只比较值类型
                if ((pisOne[i].PropertyType.IsValueType || pisOne[i].PropertyType.Name.StartsWith("String")) && (pisTwo[i].PropertyType.IsValueType || pisTwo[i].PropertyType.Name.StartsWith("String")))
                {
                    if (oneName.Equals(twoName))
                    {
                        if (oneValue == null)
                        {
                            if (twoValue != null)
                            {
                                result = false;
                                break; //如果有不一样的就退出循环
                            }
                        }
                        else if (oneValue != null)
                        {
                            if (twoValue != null)
                            {
                                if (!oneValue.Equals(twoValue))
                                {
                                    result = false;
                                    break; //如果有不一样的就退出循环
                                }
                            }
                            else if (twoValue == null)
                            {
                                result = false;
                                break; //如果有不一样的就退出循环
                            }
                        }
                    }
                    else
                    {
                        result = false;
                        break;
                    }
                }
                else
                {
                    //如果对象中的属性是实体类对象,递归遍历比较
                    bool b = CompareType(oneValue, twoValue);
                    if (!b) { result = b; break; }
                }
            }
            return result;
        }
    }
 
    public class Test
    {
        public string Names { get; set; }
        public int Age { get; set; }
        public float Height { get; set; }
    }
}
 
输出:是否相同:False

将这两个 Test 对象的值改为一样试试

static void Main(string[] args)
{
    Test test1 = new Test();
    test1.Names = "张三";
    test1.Age = 24;
    test1.Height = 179.9f;
    Test test2 = new Test();
    test2.Names = "张三";
    test2.Age = 24;
    test2.Height = 179.9f;
 
    bool result = CompareType(test1, test2);
    Console.WriteLine("是否相同:" + result);
 
    Console.ReadKey();
}
输出:是否相同:True

可以看到,输出结果是对的

 

  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值