关于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 接口名{ 返回类型 抽象函数(); 其他抽象成员; }
接口内的方法,默认抽象、公开!
- 接口是一个引用类型,通过接口可以实现多重继承。
- C#中接口的成员不能有new、public、protected、internal、private等修饰符。
- 接口中只能声明"抽象"成员,所以不能直接 下一步对接口进行实例化
(即不能使用new操作符声明一个接口的实例对象),而不能声明共有的域或者私有的成员变量。- 接口声明不包括数据成员,
接口包含的成员只有方法,属性, 索引器(有参属性),事件四种成员。- 接口名称一般都以“I”作为首字母(当然不这样声明也可以),这也是接口和类的一个区别之一。
- 接口成员的访问级别是默认的(默认为public),所以在声明时不能再为接口成员指定任何访问修饰符,否则 编译器会报错。
- 接口成员不能有static、abstract、override、virtual修饰符,使用new修饰符不会报错,但会给出警告说不需要关键字new。
- 在声明接口成员的时候,不准为接口成员编写具体的可执行代码,也就是说,只要在对接口进行声明时指明接口的成员名称和参数就可以了。
- 接口一旦被实现,实现类必须实现接口中的所有成员,除非实现类本身是抽象类(通过具体的可执行代码实现接口抽象成员的操作)。
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);
}
}
结果图:
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
——————————————————————————————
未完待续…
——————————————————————————————