C#泛型和泛型约束之追根溯源

关于C#泛型约束

你好! 这是 泛型约束 所展示的欢迎页。

1.这得从类说起

1>类:

定义新的数据类型以及这些新的数据类型进行相互操作的方法

C#中所有的类都是默认由object类派生来的,显示指定或者省略效果是一样的,所以上面的两个例子完全相同。

在类的定义中:
申明时定义的类叫申明类,执行实例化时候定义的类叫实例类。
例如:

BaseUnit b = new hero();

其中BaseUnit叫做申明类,而hero则是实例类。

1.1>.类的定义
class A
{
}
class A:object
{
}
1.2> C#中的类【3种:抽象、密封、非抽象】

1>抽象类

abstract:表示修饰的类不完整,也就是抽象类,只能用做基类。 在使用是不能直接实例化,不能使用new运算符。

2>密封类

sealed:表示修饰的类不可派生,也就是密封类。

3>非抽象类

普通的类
另外提一下虚基类, virtual不能在c#中修饰类 在c++中virtual修饰的基类叫做其派生类的虚基类,是为了避免多重继承的二义性产生的概念,基类定义了virtual,子类重写可以不加virtual关键字,
有关于虚基类的描述1
https://www.cnblogs.com/hulichao/p/9656694.html
有关于虚基类的描述

1.3> C#中的类的赋值

1.父类可以引用子类对象。
2.父类引用只能调用子类继承父类的方法,父类引用不能调用子类独有的方法。

parent newchild = new children1("c11"); 
        newchild.father(); 
        newchild.mother(); 
        Console.WriteLine("newchild 的名字为:"+newchild.getname()); 

3.子类引用不能直接赋值父类对象,即使将父类对象进行强制转换,也不可以。(编译虽通过,但运行异常)。

1.4> base的使用

base:访问最近的基类,也就是当前类继承的类

class A:AA
{
  public void AFun()
  {
    base.AAFun();
  }
}

以上例子中base代表AA。 注意:base只能在类的内部使用。

1.5> 类与继承的一个表格

先来看一下C# 中一个类对于多重继承的支持

基类【类型】是否支持多重继承
普通类不支持
抽象类不支持
接口类支持
1.6> 类与方法的三个表格

再来看一下C#中基类对于三种方法写法的支持 和派生类对于基类方法调用的权限

基类【类型】普通方法虚方法抽象方法
普通类支持(可调用/隐藏)支持(可调用/重写)不支持
抽象类支持(可调用/隐藏)支持(可调用/重写)支持(可重写)
接口类支持(无意义/不可调用)支持(无意义/不可调用)支持(可重写)

三种方法在不同类中的定义

基类普通方法 public虚方法 public抽象方法(重写)
普通类/virtual void fun(){ }/
抽象类void fun(){}virtual void fun(){}public abstract void fun();
接口类//void fun();

三种方法在不同派生类中的重写

基类普通方法(隐藏)虚方法(重写/隐藏) +(public override/new)抽象方法(重写)
普通类/void fun(){ }/
抽象类(new void) / void fun(){ }void fun(){ }public override void fun(){ }
接口类//public void fun() { }

2>一些名词解释:【若有不足,移驾百度】

1.索引器【{ get{}; set{}; }】:

可以让你写更少的代码.

2.委托【delegate】/事件【event】:

c#新名字委托. 在C中就是函数指针.
事件是对于委托的一种封装

3.虚函数【virtual】:

面向对象时引入, 主要为了实现多态. 让方法可以继承。
virtual 修饰方法、属性、索引器或事件声明,允许在派生类中重写这些对象。
虚方法可以被子类重写,如果子类重写了虚方法,那么运行时将使用重写后的逻辑,如果没有重写,则使用父类中虚方法的逻辑;

4.抽象函数【abstract】:(等同于C++中的纯虚函数)

和虚方法相似, 不同的是虚方法在父类可以提供实现, 而抽象函数只有声明
抽象函数没有执行代码,必须在非抽象的派生类中重写,
抽象类不能被实例化;

4.3 虚方法与抽象方法的区别:

虚拟方法有一个实现部分,并为派生类提供了覆盖该方法的选项。相反,抽象方法没有提供实现部分,强制派生类覆盖方法(否则 派生类不能成为具体类)。
~抽象方法只能在抽象类中声明,虚方法则不是;
~抽象方法必须在派生类中重写,而虚方法不必;
~抽象方法不能声明方法实体,虚方法则可以。

5.接口【interface】:

面向对象时, 类描述是什么, 接口表达做什么(有人说是象什么…行为上).
接口就是协议,其声明的成员(属性,方法,事件和索引器)必须由其继承的类实现。接口不能直接被实例化。

6.抽象类【abstrtact】:(abstract修饰的类就是纯虚类)

1>使用关键字abstract声明的类为抽象类
2>抽象类不能直接实例化
3>抽象类中可以包含抽象成员,但是非抽象(普通)类中不行,不支持抽象成员! ——即如果类包含抽象函数,该类将也是抽象的,也必须声明为抽象的
4>不能把关键字abstract 和sealed 一起用在C#中,因为一个密封类不能够被抽象

2.OOP中,继承的两种方式

实现继承 和 接口继承

1>实现继承:

在Java中,所有函数默认都是virtual ~
而在C#中,所有函数并不默认为virtual ~
但可以在基类中通过声明关键字virtual,就可以在其派生类中通过关键字override重写该函数,重写后的virtual函数依旧是virtual函数。
由于virtual只对类中的实例函数成员有意义,所以成员字段和静态函数都不能声明为virtual,也不能与override和abstract一起用。
C#中可以设置virtual属性、索引器或事件

1.1>实现继承的重写/隐藏的方式

如果签名后的函数在基类中都进行了声明(遇到基类同名函数),却没有用virtual和override关键字

由于方法相同,在用子类新方法编译代码时候,程序在应该调用哪种方法上就会有潜在的冲突,此时编译器会发出警告,认为子类隐藏了父类的方法,此时可以重新命名子类,也可以通过关键字new隐藏此方法。

如果签名后的函数在基类中都进行了声明(遇到基类同名函数),用了virtual和override关键字

1、当调用函数时,系统会直接去检查申明类,看所调用的函数是否为虚函数;
2、如果不是,那么它就直接执行该函数。如果是virtual函数,则转去检查对象的实例类。
3.在实例类中,若有override的函数,则执行该函数,如果没有,则依次上溯,按照同样步骤对父类进行检查,知道找到第一个override了此函数的父类,然后执行该父类中的函数。

签名后的函数在基类中都进行了声明(遇到基类同名函数),用了abstract关键字

1、直接调用本类实现的抽象方法

1.2>继承非抽象类 - 实现继承案例
1.2.1>子类隐藏/重写基类方法
	class Program
	{
		static void Main(string[] args)
		{
			//基类实例 
			BaseUnit b = new BaseUnit();
			b.GetAttack();
			b.getDes();
		
			//里氏替换原则 父类不可以替换子类 子类能替换父类
			//实例包含父类的成员
			BaseUnit h = new hero();
			h.GetAttack();
			h.getDes();

			//hero类的实例 包含父类和自身的方法
			hero h1 = new hero();
			h1.GetAttack();
			h1.getDes();

			BaseUnit e = new enemy();
			e.GetAttack();
			e.getDes();
			
			enemy e1 = new enemy();
			e1.GetAttack();
			e1.getDes();
			
			// 不能替换的指针指向,即子类不能指向父类
			// 我们可以说 苹果是水果,却不能说水果是苹果!
			//hero hero_ = new BaseUnit<hero>();

			Console.ReadKey();
		}
	}

其中
hero类中 重写/隐藏 了父类的同名函数【拷贝了父类函数中的代码】
enemy类没有隐藏

猜一猜结果:
执行截图
结果分析:

继承之后,肯定是先执行父类的构造,再次本类构造。

对象b: 执行的本类方法
对象h: 重写了父类方法,调用本类方法
对象h1: 重写了父类方法,调用本类方法
对象e: 执行的父类方法,未重写/隐藏父类方法,直接调用
对象e1: 执行的父类方法,未重写/隐藏父类方法,直接调用

1.2.2>基类与派生类的代码

再来看基类与派生类的代码:

普通方法的重写 与 隐藏:

	/// <summary>
	/// 基本单位
	/// </summary>
	public class BaseUnit
	{
		protected int attack = 10;
		protected int defence = 10;
		protected string name = "基本单位";
		protected string description = "基本单位描述:";

		public BaseUnit()
		{
			Console.WriteLine("base constructor");
		}
		public void GetAttack()
		{
			Console.WriteLine("攻击力:"+attack);
		}
		public void getDes()
		{
			Console.WriteLine(description);
		}
	}
	
	class hero : BaseUnit
	{
		public hero() {
			Console.WriteLine("hero constructor");

			attack = 20;
			defence = 20;
			name = "hero";
			description = "我是一个英雄";
		}

		//这里默认隐藏/重写父类方法 hero类实例执行此方法 优先执行本类方法
		public void getDes()
		{
			Console.WriteLine(description);
		}

		/// <summary>
		/// 子类直接调用父类方法,这里如果注释掉不会执行本方法,执行父类方法
		/// 如果想重写,需要设置父类方法为虚拟(virtual)或者抽象(abstract)
		/// 使用override关键字进行重写
		/// 如果 想隐藏 使用new关键字 public new void GetAttack()
		/// 如果 没有new 关键字 默认是隐藏/重写了父类方法 
		/// 子类对象执行同名方法,执行子类方法【隐藏基类方法】
		/// </summary>
		/// <returns></returns>
		
		public new void GetAttack()
		{
			Console.WriteLine("攻击力:"+attack);
		}
	}

	class enemy : BaseUnit
	{
		public enemy()
		{
			Console.WriteLine("enemy constructor");

			attack = 30;
			defence = 30;
			name = "enemy";
			description = "我是一个反派";
		}
		
		//这里使用继承特性 实例直接执行父类方法
		//public void GetAttack()
		//{
			//Console.WriteLine("攻击力:"+attack);
		//}
		
		//public void getDes()
		//{
			//Console.WriteLine(description);
		//}
	}

虚方法的重写 与 隐藏:

public class BaseUnit
	{
		protected int attack = 10;
		protected int defence = 10;
		protected string name = "基本单位";
		protected string description = "基本单位描述:";

		public BaseUnit()
		{
			Console.WriteLine("base constructor");
		}
		public virtual void GetAttack()
		{
			Console.WriteLine("攻击力:"+attack);
		}
		public virtual  void getDes()
		{
			Console.WriteLine(description);
		}
	}
	
	class hero : BaseUnit
	{
		public hero() {
			attack = 20;
			defence = 20;
			name = "hero";
			description = "我是一个英雄";
		}

		//这里默认隐藏/重写父类方法 hero类实例执行此方法 优先执行本类方法
		public override void getDes()
		{
			Console.WriteLine(description);
		}

		public override void GetAttack()
		{
			Console.WriteLine("攻击力:"+attack);
		}
	}

	class enemy : BaseUnit
	{
		public enemy()
		{
			attack = 30;
			defence = 30;
			name = "enemy";
			description = "我是一个反派";
		}
		public new void getDes()
		{
			Console.WriteLine(description);
		}

		public new void GetAttack()
		{
			Console.WriteLine("攻击力:"+attack);
		}
	}

2>接口继承:

一般接口都是对应的功能:即插即用
关键字:interface
写法: interface 接口名{ 返回类型 抽象函数(); 其他抽象成员; }
接口内的方法,默认抽象、公开!

  1. 接口是一个引用类型,通过接口可以实现多重继承。
  2. C#中接口的成员不能有new、public、protected、internal、private等修饰符。
  3. 接口中只能声明"抽象"成员,所以不能直接 下一步对接口进行实例化
    (即不能使用new操作符声明一个接口的实例对象),而不能声明共有的域或者私有的成员变量。
  4. 接口声明不包括数据成员,
    接口包含的成员只有方法,属性, 索引器(有参属性),事件四种成员。
  5. 接口名称一般都以“I”作为首字母(当然不这样声明也可以),这也是接口和类的一个区别之一。
  6. 接口成员的访问级别是默认的(默认为public),所以在声明时不能再为接口成员指定任何访问修饰符,否则 编译器会报错。
  7. 接口成员不能有static、abstract、override、virtual修饰符,使用new修饰符不会报错,但会给出警告说不需要关键字new。
  8. 在声明接口成员的时候,不准为接口成员编写具体的可执行代码,也就是说,只要在对接口进行声明时指明接口的成员名称和参数就可以了。
  9. 接口一旦被实现,实现类必须实现接口中的所有成员,除非实现类本身是抽象类(通过具体的可执行代码实现接口抽象成员的操作)。
1.1>接口继承的实现的方式

签名后的函数在基类中都进行了声明(遇到基类同名函数),用abstract关键字
1、直接调用本类实现的抽象方法

1.2>继承接口类 - 接口继承案例
	interface IFly
	{
		void Fly();
	}
	interface ISwim
	{
		void Swim();
	}
	
	class hero : BaseUnit, IFly
	{
		public hero() {
		}
		public void Fly()
		{
			Console.WriteLine("我的能力:飞行");
		}
	}
	class enemy : BaseUnit, ISwim
	{
		public enemy()
		{
		}
		public void Swim()
		{
			Console.WriteLine("我的能力:游泳");
		}
	}
	
	class Program
	{
		static void Main(string[] args)
		{
			//基类实例 
			BaseUnit b = new BaseUnit();
			b.GetAttack();
			b.getDes();

			//里氏替换原则 父类不可以替换子类 子类能替换父类
			//实例包含父类的成员
			BaseUnit h = new hero();
			h.GetAttack();
			h.getDes();
			//这里父类没有继承实现Fly方法

			//hero类的实例 包含父类和自身的方法
			hero h1 = new hero();
			h1.GetAttack();
			h1.getDes(); 
			h1.Fly();

			BaseUnit e = new enemy();
			e.GetAttack();
			e.getDes();
			//这里父类没有继承实现Swim方法

			enemy e1 = new enemy();
			e1.GetAttack();
			e1.getDes();
			e1.Swim();
		}
	}

执行结果:

只有实现了接口类型的实例可以调用接口的方法

结果图片

3.类型的复用 -> 泛型【gengric】

1.0>属性 和 泛型全名:泛化数据类型
//打上prop 按下tab键,会有属性的完整提示显示,如下,进行修改即可
public int MyProperty { get; set; }

泛化就是不去确定具体的类型,相反,特化就是类型的具体化 ~
比如,我喜欢听音乐,是泛泛而谈,类型未指定,这就是泛化 ~
比如,我喜欢周杰伦的彩虹,类型指定,这就是特化,具体化 ~

1.1>泛型的类型

这里我说的类型,分为两种:
一种是值类型
比如 int bool float double 这些
一种是引用类型
比如 string 数组 自定义的类 接口 委托 这些

1.2>泛型的优点

为啥要用泛型?

避免成员膨胀和类型膨胀

正交性:与类型和成员交集产生新的泛型类型

泛型类型(+类=泛型类 +接口=泛型接口 +委托=泛型委托 …)
泛型成员(+属性=泛型属性,+方法=泛型方法,+字段=泛型字段…)
泛型定义:【my understand】
泛型可以表示某种类型的复用,一般用T表示类型的泛指。
在实例化时,确定类型,进行类型的强制转换。

1.3>泛型的引入

n个盒子装苹果等物品案例,4个类,非泛型类型,较为复杂,类型膨胀
2个盒子类 一个苹果类,一个书籍类

namespace XGeneric
{
	class Apple
	{
		public string Color { get; set; }
	}
	class Book
	{
		public string Name { get; set; }
	}

	class AppleBox
	{
		public Apple appleBox { get; set; }
	}

	class BookBox
	{
		public Book bookBox { get; set; }
	}
	class Program
	{
		static void Main(string[] args)
		{
			Apple apple = new Apple() { Color = "red" };
			Book book = new Book() { Name = "Fly Wave CSDN" };

			AppleBox applebox = new AppleBox() { appleBox = apple };
			BookBox bookBox = new BookBox() { bookBox = book };

			Console.WriteLine(applebox.appleBox.Color);
			Console.WriteLine(bookBox.bookBox.Name);
		}
	}
}

1个盒子装苹果等物品案例,代码沉淀,成员碰撞
一个盒子类 一个苹果类,一个书籍类

	class GenBox
	{
		public Apple appleBox { get; set; }
		public Book bookBox { get; set; }
	}
	class Program
	{
		static void Main(string[] args)
		{
			Apple apple = new Apple() { Color = "red" };
			Book book = new Book() { Name = "Fly Wave CSDN" };

			GenBox box1 = new GenBox() { appleBox = apple };
			GenBox box2 = new GenBox() { bookBox = book };

			Console.WriteLine(box1.appleBox.Color);
			Console.WriteLine(box2.bookBox.Name);
		}
	}

1个盒子装苹果等物品案例,使用Object类型的box类的成员类型,类型转换和访问成员都较为复杂,这时,引入泛型,泛指一下物品,物品时,进行类型指定,特化!
一个盒子泛型类 一个苹果类,一个书籍类

	class ObjBox
	{
		public Object GenBox { get; set; }
	}
	class Program
	{
		static void Main(string[] args)
		{
			Apple apple = new Apple() { Color = "red" };
			Book book = new Book() { Name = "Fly Wave CSDN" };

			//GenBox 是object类型,无法直接访问color属性,需要类型转换
			ObjBox objbox1 = new ObjBox() { GenBox = apple };
			Console.WriteLine((objbox1.GenBox as Apple)?.Color);
		}
	}
	

使用盒子泛型,当装苹果时,T就是苹果类型,t就是苹果的实例,自然可以访问苹果对象的成员Color。

	class Box<T>
	{
		public T t { get; set; }
	}
	class Program
	{
		static void Main(string[] args)
		{
			Apple apple = new Apple() { Color = "red" };
			Book book = new Book() { Name = "Fly Wave CSDN" };

			Box<Apple> box1 = new Box<Apple>() { t = apple };
			Box<Book> box2 = new Box<Book>() { t = book };

			Console.WriteLine(box1.t.Color);
			Console.WriteLine(box2.t.Name);
		}
	}

结果图:
结果3

1.4>泛型接口
1.4.1>泛型类实现泛化接口
	/// <summary>
	/// 泛型接口
	/// </summary>
	/// <typeparam name="TId"></typeparam>
	interface IUnique<TId>
	{
		TId ID { get; set; }
	}

	/// <summary>
	/// 如果一个类实现的时泛型接口,那么这个类本身就是泛型的类
	/// </summary>
	/// <typeparam name="TId"></typeparam>
	class Student<TId>:IUnique<TId>
	{
		public TId ID { get; set; }
		public string Name { get; set; }
	}
	class Program
	{
		static void Main(string[] args)
		{
			//泛型 特化为int类型
			Student<int> stu = new Student<int>();
			stu.ID = 101;
			stu.Name = "Flyer";
			//泛型特化为ulong类型
			//声明student类的实例时 实现泛型接口,就是实现了Student这个泛型类
			Student<ulong> student = new Student<ulong>();
			stu.ID = 1111111111;
			stu.Name = "infinity";
		}
	}
1.4.2>普通类实现特化接口
	/// <summary>
	/// 直接将接口特化,这个类将不再是泛型类
	/// </summary>
	class Teacher : IUnique<ulong>
	{
		public ulong ID { get; set; }
		public string Name { get; set; }
	}
	class Program
	{
		static void Main(string[] args)
		{
			//可以继承 特化的接口 ,这时老师类不再是泛型类
			Teacher teacher = new Teacher();
			teacher.ID = 123123123;
			teacher.Name = "Waver";
		}
	}
1.4.3>系统内置泛型类实现泛化接口 - Dictionary

系统内置的几个泛型类型,并且实现的放在一下命名空间:

using System.Collections.Generic;

举例,Dictionary 字典类实现了 IDictionary 接口,所以实例化的写法应该为:

用接口类声明:只能调用实现的接口的方法
IDictionary<TKey,TValue> 变量名 = new Dictionary<TKey,TValue>();
或者
用字典类声明:可以调用字典类实现的所有方法,包括字典接口,迭代接口等...
Dictionary<TKey,TValue> 变量名 = new Dictionary<TKey,TValue>();

Dictionary<int, string> dic = new Dictionary<int, string>();

dic[0] = "dic01";
dic[1] = "dic02";

Console.WriteLine(dic[0] + " " + dic[1]);
// 输出 dic01 dic02

4.泛型约束

4.0泛型约束名词

4.0.1.类型形参
T t { get; set; }  T 是类型形参
4.0.2.类型实参
T t { get; set; }  t 是类型实参

4.1泛型约束种类

4.1.1.基类约束
基类约束写法:
child<T> where T : 基类名{  }
继承写法:
child: 基类名{  }

这里和继承非常像:
1.子类可以调用父类方法,属性。
2.通过提高基类约束,编辑器将知道所有的类型实参将拥有指定的基类定义的成员。
确保只使用指定基类类型实参,这意味着:类型实参必须是基类本身,或者派生于该基类的类。
这句话的意思就是,声明对象【hero h】时,基类既可以时BaseUnit,也可以是hero,new出来的对象都是 hero类型的实例,而对象 h 也可以调用基类的方法。
3.只能指定一个基类,单一继承。

4.1.2.接口约束 【常用】
接口约束写法:
child<T> where T : 接口名{  }
继承写法:
child: 基类名{  }

某个类型实参必须实现的接口
这里和继承非常像:
2个主要功能与积累约束一样,允许调用接口成员。
类型实参必须是接口本身,或者实现该接口的类。
这句话的意思就是,声明对象【dic】时,基类既可以时IDictionary,也可以是Dictionary,new出来的对象都是 Dictionary类型的实例,而对象 dic 也可以调用实现的接口的方法。
但是不完全一样:
1.接口内,只能存在抽象成员,类型有限,且都是public abstract

4.1.3.构造函数约束 【常用】

通常用来做单例模式,可以参考上一篇文章
C#泛型与单例
简易的普通单例类写法

	class Singleton
	{
		private static Singleton ins;
		//方法1 使用静态方法获取单例
		public static Singleton GetIns()
		{
			if(ins == null)
			{
				ins = new Singleton();
			}
			return ins;
		}
		//方法2 使用静态属性获取单例
		public static Singleton Ins
		{
			get
			{
				if (ins == null)
				{
					ins = new Singleton();
				}
				return ins;
			}
		} 
	}
	用法:
	Singleton.GetIns().其他方法调用();
	Singleton.Ins.其他方法调用();

简易的泛型单例类写法,将上一个类加上new约束,Singleton类型换成T即可

class Singleton<T> where T:new()
	{
		private static T ins;

		public static T GetIns()
		{
			if(ins == null)
			{
				ins = new T();
			}
			return ins;
		}

		public static T Ins
		{
			get
			{
				if (ins == null)
				{
					ins = new T();
				}
				return ins;
			}
		} 
	}
	用法:调用基类泛型的获取实例方法,获取本类的
	Hero:Singleton<Hero>{
	  public Hero(){  }
	  public void fun(){ }  
	}
	Hero.GetIns().fun();
	Hero.Ins.fun();
构造函数约束写法:
child<T> where T : new() {  }
继承写法:
child: 基类名{  }

构造函数约束允许实例化一个泛型类型对象。
1.要求类型实参必须提供一个无参的公有构造函数,
-----意思是:子类继承此泛型之后,必须写入一个公开的无参的构造函数。
2.可以通过调用无参的构造函数创建对象,
3.有多个约束时,放在末端,看下约束顺序
4.不允许再new()的括号中传递参数
5.不允许与值类型约束同时使用,值类型 隐式提供了无参构造函数,相当于已经new过了。

4.1.4.值类型约束

where T : struct

4.1.5.引用类型约束

where T : class

4.1.6.混合约束

where T : 以上五种约束按照一定顺序和规则排列

4.2泛型约束写法顺序

前部中部尾部
基类/值类型/引用类型约束接口约束 - 多个约束使用,分隔new()

4.3泛型约束对象

泛型约束对象是T

——————————————————————————————
未完待续…
——————————————————————————————

5.泛型方法,泛型委托

6.泛型 协变 逆变

7.泛型缓存

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值