总结:C#中的泛型原理及应用

本文深入探讨C#中的泛型使用,包括泛型方法、类、接口和委托的定义与调用,以及协变、逆变的概念与应用。通过实例讲解如何利用泛型提高代码复用性和灵活性,理解装箱、拆箱过程,掌握泛型约束的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引:

先举如下例子:
三个方法,都是用格式化的方式将传入值打印出来,但是每个方法传入的参数,类型不同,
如下:第一个传入 int型,第二个传入 string型,第三个传入Datetime类型。如果分别来写很不方便,而且有代码重复

public static void ShowInt(int iParameter)
{
    Console.WriteLine("This is {0},parameter={1},type={2}",
    typeof(CommonMethod).Name, iParameter.GetType().Name, iParameter);
}
/// <summary>
/// 打印个string值
/// </summary>
/// <param name="sParameter"></param>
public static void ShowString(string sParameter)
{
    Console.WriteLine("This is {0},parameter={1},type={2}",
    typeof(CommonMethod).Name, sParameter.GetType().Name, sParameter);

//typeof(string)
//typeof(Type)
}
/// <summary>
/// 打印个DateTime值
/// </summary>
/// <param name="oParameter"></param>
public static void ShowDateTime(DateTime dtParameter)
{
    Console.WriteLine("This is {0},parameter={1},type={2}",
    typeof(CommonMethod).Name, dtParameter.GetType().Name, dtParameter);
}

于是可以定义一个方法,传入object类型,如下:

 public static void ShowObject(object oParameter)
  {
       Console.WriteLine("This is {0},parameter={1},type={2}",
           typeof(CommonMethod), oParameter.GetType().Name, oParameter);
   }

调用时,使用传入object类型参数的方法,都适用。

 int iValue = 123;
 string sValue = "456";
 DateTime dtValue = DateTime.Now;
 object oValue = "678";
CommonMethod.ShowObject(oValue);
CommonMethod.ShowObject(iValue);
CommonMethod.ShowObject(sValue);
CommonMethod.ShowObject(dtValue);

== 原因:a. 通过继承,子类可以拥有父类的一切属性和行为,任何父类出现的地方,都可以用子类来代替
b.object是一切类型的父类
c.这种方式有一个缺点,就是在传入引用类型的时候需要进行装箱和拆箱的操作 ==

一 装箱和拆箱

(一) 值类型和引用类型在栈和堆中的分配

这儿有两个原则:

1.创建引用类型时(创建类的实例时),

runtime会为其分配两个空间,一块空间分配在堆上,存储引用类型本身的数据,另一个块空间分配在栈上,存储对堆上数据的引用(堆上内存地址的指针)。

2.创建值类型时,
runtime会为其分配一个空间,这个空间分配在变量创建的地方,如:
如果值类型是在方法内部创建,则跟随方法入栈,分配到栈上存储。
如果值类型是引用类型的成员变量,则跟随引用类型,存储在堆上。(对象的成员变量)

此处具体可以看下面这篇文章:https://www.cnblogs.com/cjm123/p/8056239.html

(二)装箱和拆箱的过程

== 装箱:== 将值类型(如 int ,或自定义的值类型等)转换成 object 或者接口类型的一个过程。当 CLR 对值类型进行装箱时,会将该值包装为 System.Object 类型,再将包装后的对象存储在堆上。
== 拆箱: == 就是从对象中提取对应的值类型的一个过程。
装箱是隐式的;拆箱必定是显式的。

二.引入泛型

泛型的思想,表现了一个很重要的架构思想: == 延迟思想,推迟一切可以推迟的 使程序有更多的灵活性和扩展性==
== 泛型就是用来解决,方法中是相同的操作,但是传入参数是不同类型的问题。==

(一) 泛型方法:

1.泛型方法的声明:

(1) 方法名字后面带上尖括号 < 类型参数列表 >:(可以带有多个类型参数用逗号分隔)
如: public static void Show<T,W,V>(T tParameter)
其中< >中的T称为== 类型参数 ==, 就表示这个方法是个泛型方法,参数列表中的T,就表示可以传入的类型是任意类型。

== 看上去有一个传入参数,其实有两个参数,一个是类型参数,一个是参数列表的参数,后面调用的时候,不但要传入参数列表的参数,还要指定具体的类型参数。==
方法的返回值也可以用类型参数
如:

public T GetMy < T >()
{
}
		
public static void Show<T>(T tParameter)
{
	   Console.WriteLine("This is {0},parameter={1},type={2}",
	      typeof(GenericConstraint), tParameter.GetType().Name, tParameter);
	
	   Console.WriteLine($"{tParameter.Id}  {tParameter.Name}");
	   tParameter.Hi();
	   //tParameter.Pingpang();
}

2.泛型方法的调用:

调用的时候尖括号中的类型,就可以具体指定一个任意的类型

 int iValue = 123;
 string sValue = "456";
 DateTime dtValue = DateTime.Now;
 object oValue = "678";
 
CommonMethod.Show<int>(iValue);//调用泛型,需要指定类型参数
CommonMethod.Show(iValue);//如果可以从参数类型推断,可以省略类型参数---语法糖(编译器提供的功能)
CommonMethod.Show<string>(sValue);
//CommonMethod.Show<int>(sValue);//类型错了
CommonMethod.Show<DateTime>(dtValue);
CommonMethod.Show<object>(oValue);

3.泛型的编译:

泛型在编译的时候,类型是不明确的,类型参数会被系统编译成占位符 (~),在运行时,Jit即时编译会根据程序中调用泛型方法时候给它指定的类型参数替换过来。

4.为什么要有泛型:

类型不同的参数,在执行相同的逻辑的时候,代码可以重用尽量不重复,

(二) 泛型类、泛型接口、委托

1 泛型类的定义

跟泛型方法的定义差不多,类名后面跟尖括号 类型参数列表
如:

public class GenericClass<W,T,V>
	{
			public void Show(W w)
			{
			}
			public V Get()//返回值也可以带类型参数
			{
					return default(V);
			}
	}

2. 泛型接口的定义

 public interface GenericInterface < S>
    {
    }

3. 泛型委托

public delegate void Do< T>()

注意:普通类不能继承泛型类

(三)泛型约束

1.语法:

public static void Show< T>(T tParameter)
== //where T : String//密封类约束的不行,因为没有意义
//where T : People //基类约束
//where T : ISports //接口约束
where T : People, ISports, IWork, new()//可以多个约束,而且关系
where T:class //引用类型约束,保证类型参数是一个引用类型的类型
where T:struct //值类型约束 保证类型参数是一个值类型的类型
where T: new() //无参数构造函数约束==

2.约束种类:

(1)基类约束:

== 基类约束,内部就可以访问基类的属性和方法
指定的类型参数必须是基类或者其子类 ==

(2)接口约束

== 指定的类型参数必须是实现接口的类 ==

(3)引用类型约束
(4)值类型约束
(5)无参数构造函数约束

可以在方法体中直接用类型参数实例化对象

public static void Show< T>(T tParameter)
  where T:  new()
  {
   			T t = new T();
  }

三.协变(out)和逆变(in)(只能运用到接口或者委托)

协变和逆变只能出现在接口或者委托中
协变和逆变是在.NET 4.0的时候出现的,只能放在接口或者委托的泛型参数前面,out 协变covariant,用来修饰返回值;in:逆变contravariant,用来修饰传入参数。

(一)协变:

先看下面的一个例子:定义一个Animal类:

namespace MyGeneric
{
    public class Animal
    {
        public int Id { get; set; }
    }
}

然后在定义一个Cat类继承自Animal类

namespace MyGeneric
{
    public class Cat :Animal
    {
        public string Name { get; set; }
    }
}

在Main()方法可以这样调用:

== 父类出现的地方子类就可以出现==
// 直接声明Animal类
Animal animal = new Animal();
// 直接声明Cat类
Cat cat = new Cat();
// 声明子类对象指向父类
Animal animal2 = new Cat();
// 声明Animal类的集合
List listAnimal = new List();
// 声明Cat类的集合
List listCat = new List();

而:List< Animal> list = new List< Cat>();这样是错误的,原因是程序认同的就是继承关系,而List< Animal>和 List< Cat>都是List,但是没有继承关系。
这以下两个都可以,

IEnumerable< Animal> List1 = new List< Animal>();//协变
IEnumerable< Animal> List2 = new List< Cat>();

原因是IEnumerable定义的时候,在泛型接口的T前面有一个out关键字修饰,而且T只能是返回值类型,不能作为参数类型,这就是协变。使用了协变以后,左边声明的是基类,右边可以声明基类或者基类的子类。如下图所示:
在这里插入图片描述
== 普通的泛型类或者泛型接口或委托,对于它的类型参数,在实例化的时候,是不可以左边父类,右边实例化出子类来的,如果想要达成这点,就必须定义协变,就是在返回值的时候加 Out==

== 在泛型接口的T前面有一个out关键字修饰,而且T只能是返回值类型,不能作为参数类型,这就是协变。使用了协变以后,左边声明的是基类,右边可以声明基类或者基类的子类。==
协变除了可以用在接口上面,也可以用在委托上面:
Func< Animal> func = new Func< Cat>(() => null);
下面自定义一个协变泛型接口:
public interface ICustomerListOut< out T>
{
T Get();//注意:因为接口的类型变量,前面用的是out,代表是协变,== 所以T只能作为返回值的类型==
Set(T t);//这个是错误的~!
}

==public class CustomerListOut< out T> 这样写是错误的!,类不能有协变逆变,所以类泛型的类型参数前不能加 out in ==
public class CustomerListOut< T> : ICustomerListOut< T>
{
public T Get()
{
return defalt(T);
}
}

使用自定义的协变:
// 使用自定义协变

ICustomerListOut<Animal> customerList1 = new CustomerListOut<Animal>();
ICustomerListOut<Animal> customerList2 = new CustomerListOut<Cat>();//协变(返回值),左边声明父类,右边可以实例化子类,因为T是返回值,传出是子类则一定属于父类

(二) 逆变

在泛型接口的T前面有一个In关键字修饰,而且T只能方法参数,不能作为返回值类型,这就是逆变。
public interface ICustomerListIn< in T>
{
void Show(T t);
}
public class CustomerListIn< T> : ICustomerListIn< T>
{
public void Show(T t)
{
}
}

// 使用自定义逆变
ICustomerListIn< Cat> customerListCat1 = new CustomerListIn< Cat>();
ICustomerListIn< Cat> customerListCat2 = new CustomerListIn< Animal>();//逆变(传入参数),左边声明子类,右边可以实例化父类,因为T是传入参数,传入的必须是子类,既然它是子类了,怎肯定是父类,
协变和逆变可以同时使用,看下面的示例:

public interface IMyList<in inT, out outT>
{
     void Show(inT t);
     outT Get();
     outT Do(inT t);
}
public class MyList<T1, T2> : IMyList<T1, T2>
{
	public void Show(T1 t)
     {
          Console.WriteLine(t.GetType().Name);
     }
      public T2 Get()
     {
          Console.WriteLine(typeof(T2).Name);
          return default(T2);
      }
       public T2 Do(T1 t)
      {
           Console.WriteLine(t.GetType().Name);
           Console.WriteLine(typeof(T2).Name);
           return default(T2);
       }
}
//使用
void main()
{
 IMyList<Cat, Animal> myList1 = new MyList<Cat, Animal>();
 IMyList<Cat, Animal> myList2 = new MyList<Cat, Cat>();//协变
 IMyList<Cat, Animal> myList3 = new MyList<Animal, Animal>();//逆变
 IMyList<Cat, Animal> myList4 = new MyList<Animal, Cat>();//逆变+协变
 }

四 使用泛型类做缓存

在前面我们学习过,类中的静态类型有一个特点,无论类被实例化多少次,它的静态类型在内存中只会有一个。静态构造函数只会执行一次。
而在泛型类中,泛型类在编译的时候,类型参数T被编译成一个占位符,在运行时,随着T类型不同,每个不同的T类型,都会产生一个不同的副本,对于类中的静态方法和属性也是一个,每一个不同的T类型会产生不同的静态属性、不同的静态构造函数,请看下面的例子:

namespace MyGeneric
{
 public class GenericCache< T>
    {
    	static GenericCache()
        {
            Console.WriteLine("This is GenericCache 静态构造函数");
            _TypeTime = string.Format("{0}_{1}", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff"));
        }
        private static string _TypeTime = "";
         public static string GetCache()
        {
            return _TypeTime;
        }
    }
}

然后新建一个测试类,用来测试GenericCache类的执行顺序:

namespace MyGeneric
{
 public class GenericCacheTest
    {
        public static void Show()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine(GenericCache< int>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache< long>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache< DateTime>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache< string>.GetCache());
                Thread.Sleep(10);
                Console.WriteLine(GenericCache< GenericCacheTest>.GetCache());
                Thread.Sleep(10);
            }
        }
    }
}

Main()方法里面调用:
;GenericCacheTest.Show()

结果
在这里插入图片描述
缺点:建立的缓存不能被清除,每种数据类型只能有一份数据
优点:读取速度快。

适用场景:不需要被清除,并且每个数据类型有一份数据要存在内存中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值