该文章是本人自己阅读并学习了《Visual C#从入门到精通》(第九版)之后进行的整理和总结
对个人认为重点的知识点进行了概括总结,帮助自己学习C#,分享出来希望能对读者有所帮助,也让自己进步
一定会存在各种漏洞欢迎指出!
目录:
第七章:类和对象
第八章:值和引用
第九章:枚举和结构
第十章:数组
第十一章:理解参数数组
第十二章:继承
第十三章:接口和抽象类
第十四章:垃圾回收和资源管理
第十五章:属性
第十六章:索引器
第十七章:泛型
未完待续。。。
类和对象
7.3定义和使用类
定义:
Eg.class A
{
………;
}
实例化:
A a=new A();
相同类的实例的赋值:
A a1=new A();
a1=a;
7.4控制可访问类型
private 私有的
public 公共的
eg.private int a;
构造器
构造器是在创建类的实例时自动运行的方法;
可以获取参数,但不能有返回值;
每个类都有一个构造器,不写会自动添加默认构造器,如果写了非默认构造器就不会自动生成默认构造器了,如果还需要默认构造器就需要手动写一个;
默认private,只能在类内创建该类实例,需要手动加public;
定义构造器:
class A
{
private int a;
public A()
{
a = 0;
}
}
可以写构造器的重载,来获取参数以初始化类的数据:
class Circle
{
private int radius;
public Circle(int r)
{
radius = r;
}
}
调用:Circle c = new Circle(45);
分部类:一个类分成几个部分写,用关键词partial
partial class Circle
{
private int radius;
public Circle(int r)
{
radius = r;
}
}
......
partial class Circle
{
private int b;
}
注意:类的字段的private修饰也可以在同一个类的不同实例中相互访问,字段的private指的是级别上的私有,而非对象级的私有;
解构器:
作用:检查对象并提取字段的值
class Point
{
private int x, y;
public void Deconstruct(out int x, out int y)
{
x = this.x;
y = this.y;
}
}
注意:
1.必须命名为Decontruct
2. 必须是void
3.必须获取一个或多个参数来填充
4.用out标记
5.方法主体代码向参数赋值
调用:用元组的方式获取
Point origin = new Point();
(int x, int y) = origin;
7.5理解静态方法和数据
const 常量字段,是特殊的静态字段,值不会变,不用写static
静态类 只能包含静态成员 作为工具方法和字段容器使用 不能包含任何实例数据或方法 初始化可以写静态构造器,必须是默认构造器
public static class Math
{
public static double Sin(double x) {…}
public static double Cos(double x) {…}
public static double Sqrt(double x) {…}
}
匿名类——没有名字的类
定义方法:var myAnonymousObject = new {Name ="John ",Age = 54 } ;
Var:对变量初始化的表达式为什么类型,就用这个类型创建变量
C#根据匿名类定义时里面的字段名,值类型,顺序是否相同自动归类
只能包含public字段且必须都初始化 不能是static 不能定义方法
值和引用
值类型和引用类型的区别:
值类型: 包含int,long,double,char,结构等,除string外
赋值过程:开辟空间(int i=42)->将值赋进去
引用类型: 包含string,类,等
赋值过程:开辟地址空间->将引用对象(类的实例)的地址存入
如何复制一个引用类型:
Circle c = new Circle();
Circle refc = c;->若出现了引用,只复制了引用,没有复制引用的对象(指向了同一个对象)(浅拷贝)
解决浅拷贝办法:使用Clone方法返回自己的新实例,并填充相同的数据(拆分成两个对象)(深拷贝)
定义Clone方法:
class Circle
{
private int radius;
//省略构造器和其他方法
public Circle Clone()
{
Circle clone = new Circle();
clone.radius = this.radius;
return clone;
}
}
Clone方法可以访问私有数据;
注意:如果Circle类里的引用类型要Clone,则引用类型也需要有Clone方法
私有指“类级别上私有”而非“对象级别上私有”=>相同类的实例可以访问互相的private
8.2 理解null值和可空类型
null
null 用于初始化引用类型的实例
空条件操作符 ? 作用:用于判断变量是否为空,如果为空,就不返回值
Eg.Console.WriteLine($"{c?.Area}");
可空类型:引用类型
定义: int? i = null; 值类型可以赋给可空类型,可空理性不可以赋给值类型
可空类型的两个属性:
变量名.HasValue
变量名.Value
8.3 使用ref和out参数
ref—使实参传递给形参时传递实参的引用而非拷贝(可以直接改变实参)
Eg. static void doIncrement(ref int param)
{
param++;
}
static void Main(string[] args)
{
int arg = 42;//必须要赋初值
doIncrement(ref arg);
Console.WriteLine(arg);//输出为43
}
out—ref的功能+可以不初始化变量,到方法内初始化(使用规则同ref)
Eg. static void doIncrement(out int param)
{
param = 42;//方法内初始化
param++;
}
static void Main(string[] args)
{
int arg;//未初始化
doIncrement(out arg);
Console.WriteLine(arg);//输出为43
}
8.4 计算机内存的组织方式
栈存放:参数,局部变量(即所有值类型) 生存期良好
注意:对对象的引用(地址)在栈中
堆存放:可空类型,对象(即引用类型的实例)
生存期差,最后一个引用消失时可被重用
堆内存有限
8.5 System.Object类
System.Object类->可直接写为object
- 所有类都是System.Object类的派生类
- System.Object可以引用任何对象
Eg. Circle c;
c = new Circle(42);
object o;
o = c;
8.6 8.7 装箱和拆箱
装箱:将数据项从栈自动复制到堆的行为称为装箱
Eg. int i = 42;
object o = i;//装箱
拆箱:从装箱中提取出值得过程叫拆箱
需要用到强制类型转换
强制类型转换会检查已装箱的类型是否正确
Eg. int i = 42;
object o = i;//装箱
i = (int)o;//拆箱
装/拆箱会产生大量开销,因为有许多检查工作且需要分配额外的堆内存,不能滥用
8.8 数据的安全类型
is操作符:判断对象类型是否时所期望的类型
if(对象 is 类型)/if(对象 is 类型 对象名)
Eg. if(o is Circle)/if(o is Circle myCircle) 确保转型安全
as操作符:尝试将对象转换成类型
成功->返回转换成功的结果 失败->返回null
Eg. Circle temp = o as Circle;
C#可以使用指针:需要用unsafe修饰,并生成项目时指定“允许不安全代码”
枚举和结构
9.1使用枚举
定义:enum 枚举名 {元素名,…} 使用:枚举名 枚举实例名;枚举名.元素名;
Eg. enum Season { Spring,Summer,...}
Season param;
param=Season.Spring;
枚举可以做参数,局部变量,字段
可空枚举变量 Eg.Season?param=null; //赋初值为空
枚举类型的字面值:默认从0开始对应
可以设置字面值:enum Season {Spring=1,Summer,…}(此时Summer字面值为2)
可以设置两个元素相同:…{Spring,Summer,Fall=Autumn}
可以输出字面值:int i=(int)param(param=Season.Spring)
选择枚举字面值基础类型:enum Season:short {Spring,…}
param++;=>指下一个元素(若溢出,则返回溢出字面值)(param=Season.Spring)
9.2 使用结构
结构:值类型,在栈上存储 包含字段,方法和构造器 不能人为声明默认构造器
==或!=不能自动应用于结构变量,可以使用Equals()方法
重点:若一个概念重点在于值而不是功能,就用结构实现
结构和类的区别:
结构 | 类 |
值类型 | 引用类型 |
储存在栈上 | 储存在堆上 |
不能声明默认构造器 | 可以声明默认构造器 |
(已写自定义构造器后)能自动添加默认构造器 | (已写自定义构造器后)不能自动添加默认构造器 |
构造器参数字段不会自动初始化 | 构造器参数字段会自动初始化 |
字段声明时不能初始化 | 字段声明时可以初始化 |
声明结构变量:
Eg. Time parameter;(不会初始化)/Time parameter = new Time();(会初始化)
可空:Time? parameter = null;
复制结构变量:
Eg. Time a = parameter;(parameter必须要初始化)
托管代码: .NET框架下:C#->编译器->CIL->CLR->机器指令
C++虽然支持结构,但不支持其中的成员函数(即成员方法),不能包含实例,不能有静态方法
数组
10.1声明和创建数组
声明数组:int[] pins = new int[4];//数组是引用类型,储存方法像类
int[] pins;
int[] pins = new int[4] { 1, 2, 3, 4 };
int[] pins = { 1, 2, 3, 4 };
隐式类型的数组:var names = new[] { "a", "b" };
遍历:for/foreach(首选foreach)
用for的情况:
- foreach只能遍历整个数组,不能遍历部分
- foreach不能反向遍历
- foreach稚嫩知道元素值,没有索引
- 不能修改数组值
foreach遍历长度为0的数组是安全的
数组作为参数和返回值
Eg. public int[] ReadData(int[])
{
int[] data;
return data;
}
Main方法中的数组参数:static void Main(string[] args)
可以在命令行(cmd)启动程序时,指定附加的命令行参数,将这些参数传给CLR,后者将他们作为实参转给Main,每个命令参数都以空格分隔
复制数组
Eg. int[] pins = { 1, 2, 3, 4 };
int[] copy;
//1
pins.CopyTo(copy,0);//从索引0开始复制进去
//2
Array.Copy(pins, copy, copy.Length);
//3
int[] copy = (int[])pins.Clone();//Clone方法返回object类,因此要转型
三个方法都是浅拷贝,要深拷贝需要for虚幻中增加合适的代码
多维数组:int[,] items = new int[4, 6];//方括号内代表数组大小
items[2, 3]= 6;//方括号内代表元素位置
交错数组—即不规则数组,其每列长度可以不同
Eg. int[][] items = new int[4][];
int[] col0 = new int[3];
int[] col1 = new int[10];
items[0] = col0;
items[1] = col1;
items[0][1] = 99;
col0[1] = 99;//与上一行等价
每个类都有ToString方法,可以重写ToString方法使其输出的字符有意义(对于自定义类的数组的输出很重要)
Eg. public override string ToString()
{
string result = "1";
return result;
}
Console.Writeline()会自动调用需要显示的变量的ToString
随机数:Random random = new Random;
int a = random.Next(4);//返回随机数,范围0~3
换行符:$"{Environment.NewLine}"
从方法中返回引用数据:
Eg. Person[] family = new Person[4];
ref Person FindYoungest()
{
int i = 2;
return ref family[i];//返回的必须是方法结束时还存在的变量,不是局部变量
}
理解参数数组
11.2使用参数数组
参数数组—允许将数量可变的实参传给方法 使用params关键字
Min(int[] a)和Min(params int[] a) 的区别:
前者需要先定义一个项数确定的数组int[] a={1,2},再Min(a)调用方法,后者可以直接Min(1,2)调用不用提前写数组
定义:
Eg. public static int Min(params int[] paramlist)
{
//省略内容
}
调用方法时可以直接 int a=Min(b,c,d);
注意:
- 只有一维数组能用params
- 不能只依靠params来重载方法
Eg. public static int Min(int[] a)
public static int Min(params int[] a)//出错
- Params不能用ref out修饰
- Params只能是最后一个参数,只能有唯一的params
- 非params方法优先于params方法
- 为方法声明无params(无参数数组)的版本能优化性能
params object[]—让方法接收object类型的一个参数数组,从而接受任意数量,任意类型的参数
Eg. class Black
{
public static void Hole(params object[] paramlist)
{
//省略内容
}
}
可以不传参—Black.Hole()
可以传null—Black.Hole(null)
可以传递一个实际数组作为参数—Black.Hole(array)
可以传递不同类型的实参—Black.Hole(new object[]{“a”,1})
可选参数:
Eg. void optMethod(int a,double b=0.0,string c = "hi")
{
//省略内容
}
A必须要指定;b,c可以不指定值
Eg.optMethod(1);/optMethod(1,1,1);
参数数组和可选参数分别由同一个方法的重载时,优先使用可选参数
继承
12.2使用继承
Eg.class Derivedlass:Baseclass//C#只允许从一个类派生
{
//省略内容
}
继承只适用于类,不适用于结构!
调用基类构造器
Eg. class Mammal
{
public Mammal(string name)//Mammal构造器必须public
{
//省略内容
}
}
class Horse:Mammal
{
public Horse(string name):base(name)//调用基类构造器
{
//省略内容
}
}
如果不在派生类里调用基类构造器,则会自动添加
类的赋值
Horse horse = new Horse(...);
Mammal mammal=new Horse(...);//合法(子赋父)
Horse horse = new Mammal(...);//非法(父赋子)
Mammal.Breath();//合法(Breath是父类里定义的方法)
Mammal.Trot();//非法(Trot是子类里定义的方法)
可以使用is,as来检查是否可以赋值
方法签名:包括方法名,参数数量和参数类型,这三个相同就叫方法的签名相同(返回值不计入签名)
虚方法:故意设计成要被重写的方法称为虚方法
Eg.System.object下的ToString
class Object
{
public virtual string ToString()
{
//省略内容
}
}
注意:virtual不能与static,abstract,override同时使用
重写(override):提供同一个方法的不同实现
Eg. public override string ToString()
{
string temp = base.ToString();//派生类中,base关键字用来调用方法的基类版本
//省略内容(虚方法的内容和普通方法没有区别)
}
注意:
- 虚方法不能私有
- 虚方法和重写方法的签名必须一致
- 只能重写虚方法
- 派生类里面有和基类重名的方法并且没有用override,就不是重写基类方法,而是隐藏方法,需要用new消除警告
- 重写方法隐式成为虚方法,可在派生类中被重写,但不允许用virtual关键字将重写方法显示声明为虚方法(?不是很理解)
- override不能和static,new,virtual同时使用
虚方法和多态:
多态:写法一样的语句,却能依据上下文调用不同的方法
Eg. class Mammal
{
//省略内容
public virtual string GetTypeName()
{
return "mammal";
}
}
class Horse : Mammal
{
//省略内容
public override string GetTypeName()
{
return "horse";
}
}
class Whale:Mammal
{
//省略内容
public override string GetTypeName()
{
return "whale";
}
}
class Ardvark : Mammal
{
//省略内容
}
//以下在Main方法内
Mammal mammal = new Horse();
Console.WriteLine(mammal.GetTypeName());//输出horse
mammal = new Whale();
Console.WriteLine(mammal.GetTypeName());//输出whale
mammal = new Ardvark();
Console.WriteLine(mammal.GetTypeName());//输出mammal,体现了多态
Protected关键字:只有在类的派生类层级下可以访问protected
12.3 创建扩展方法
用于快速扩展已有类型,允许添加静态方法来扩展现有类型(无论是类还是结构)
引用被扩展类型的数据,即可调用扩展方法
编译器会自动识别当前作用域的所有静态类,找出给定类型定义的所有扩展方法
扩展方法在一个静态类中定义,被扩展类型必须是方法的第一个参数,而且必须附加 this关键字
Eg. namespace Extensions
{
static class Util//定义扩展方法
{
public static int ConvertToBase(this int i,int baseToConvertTo)
//被扩展类型必须是方法的第一个参数,this后面的类型就是扩展的类型,后面的都是方法的参数
{
//省略内容
return result;
}
}
}
using System;
using Extensions;//引入定义了扩展方法的命名空间
//省略固定结构
Console.WriteLine($"{x} in base {i} is {x.ConvertToBase(i)}");
在调用扩展方法时需要将静态类进入当前作用域(使用using 命名空间)
接口和抽象类
13.1理解接口
接口:描述了类提供的功能,不描述功能具体如何实现
接口约等于协议=>实现了接口的类必然包含接口规定的所有方法
接口能够定义方法和属性,不能有字段
定义: interface IComparable
{
int CompareTo(object o);//接口的成员默认public,不用加访问修饰符
}
实现:class Horse:IComparable
{
public int CompareTo(object o)//这种方法又叫隐式实现接口
{
//省略实现内容
}
}
注意:
- 方法名和返回类型完全匹配
- 所有参数(包括ref,out)都要完全匹配
- 实现接口方法需要public(但若使用显示接口实现【即实现时附加接口名前缀】,则不能加访问修饰符)
同时继承类,实现接口:(先写基类,再写接口)
class Horse:Mammal,IComparable
{
//省略内容
}
接口扩展:interface IA:IB
通过接口引用类:
Eg.Horse horse = new Horse();
ILandBound ihorse = horse;//合法,含有该接口的类可以直接赋给接口
接口作为参数:
Eg. int FindLandSpeed(ILandBound landBoundMammal)
{
//省略内容
}
多个接口:
Eg. class Horse:Mammal,ILandBound,IGrazable
{
//省略内容
}
显式实现接口:
解决不同接口中同名的方法的区分
Eg. interface IJourney
{
int LegsNumber();
}
interface ILandBound
{
int LegsNumber();
}
//定义两个有同名方法的接口
class Horse:Mammal,IJourney,ILandBound
{
int IJourney.LegsNumber()
{
//省略内容
}
int ILandBound.LegsNumber()//能够区分不同接口的同名方法
{
//省略内容
}
}
注意:显式实现接口不用写public
调用同一个类中不同接口的同名方法:
IJourney iJourney = new Horse();//注意这一步
int i = iJourney.LegsNumber();
注意:尽量显式实现接口
接口的限制:
- 接口不能有字段
- 接口不能有构造器,析构器
- 接口方法不能有访问修饰符
- 不能嵌套任何类型
- 接口不能从类,结构继承
13.2 抽象类
抽象类:为明确声明不允许创建某个类的实例(因为一般都为了提供通用的默认实现而创建出来,因此创建出来没有意义),必须将那个类显式声明为抽象类,用abstract关键字
抽象方法:与虚方法类似,只是不含方法主体,不能私有
Eg. abstract class GrazingMammal:Mammal,IGrazable
{
public abstract void DigestGrass();//抽象方法
}
13.3密封类
不能作为基类的类可以声明为密封类
Eg. sealed class Horse
{
//省略内容
}
注意:不能包含虚方法 抽象类不能密封
密封方法:
是方法的最后一个实现,意味着派生类不能重写该方法
方法要声明为sealed override(只有有override才能写成密封方法)
垃圾回收和资源管理
14.1对象生存期
垃圾回收:销毁对象并将内存归还给堆的过程
托管对象:类的对象,作用域内的变量等
非托管对象:文件,数据库等
析构器:在对象被垃圾回收时执行必要清理(大型的托管资源或者非托管资源需要析构器)
定义:先写一个~,再写类名
Eg. class FileProcessor
{
~FileProcessor()
{
//省略内容
}
}
注意:
- 析构器只适合引用类型
- 析构器不能有访问修饰符
- 析构器不能有参数
慎用析构器,以提升效率
析构器什么时候运行是无法精确预判的,所以不要让析构器相互依赖
14.2资源清理
析构器只能等待垃圾回收器在某个不确定的时间来释放内存,因此需要资源清理来控制释放资源的时机
使用using检查运行是否错误,且不管是否出错都执行Dispose()语句
定义:using(TextReader reader=new StreamReader(filename))//异常安全的
{
//省略内容
}
//上述代码等价于
TextReader reader = new StreamReader(filename);
try
{
//省略内容
}
finally
{
if (reader != null)
{
((IDisposable)reader).Dispose();
}
}
using语句声明的变量类型必须实现IDisposable接口,类中要具体化Dispose()方法
在析构器中调用Dispose()方法
保证Dispose()方法一定运行,多一层保障(可以看14.2.4示例代码)
GC.SuppressFinalize(this);//告诉“运行时”不要调用this的析构器
属性
15.2 什么是属性
属性是字段和方法的交集,既能够维持封装性,又能够使用字段风格的语法
定义属性:
Eg. class Class1
{
private int _x;
public int X
{
get { return this._x; }//get=>this._x;
set { this._x = value; }//set=>this._x=value;
}
}
value:隐藏的、内建的参数来传递要写入的数据
使用属性:
Class1.X=20;
只读:只有get 只写:只有set
get,set可以设置访问性:private get=>……
注意:
- 只能改变一个的可访问性
- 属性为private,访问器(get,set代码块)就不能public
15.3 属性的局限
1.只有在结构或类初始化过后,才能用属性来赋值
2.不能将属性作为ref,out参数传递给方法
Eg.MyMethod(ref location.X)//X为属性,编译出错
3.属性内只能包含get,set,不能有其他方法,字段等
4.get,set不能获取参数,只能通过value传给set
5.不能在属性内声明const属性
15.4接口中声明属性
Eg. interface IA
{
int X { get; set; }
}
要在类中实现属性
可以在类中实现属性时声明属性为virtual,允许派生类重写实现
通过属性就可以代替原来通过方法来取得值
15.5 自动生成属性
C#编译器能够自动为属性生成代码,该技术适合用来创建不可变属性(即属性在对象构造好后就不更改)
Eg. public int X { get; set; }
//等价于以下代码
private int _x;
public int X
{
get { return this._x; }
set { this._x = value; }
}
可以创建自动只读属性,但不能创建自动可写属性
自动只读属性要么构造器初始化,要么声明时初始化
构造器初始化:
Eg. class Class1
{
public Class1()
{
X = 0;
}
public int X { get; }
}
声明时初始化:
Eg. class Class1
{
public int X { get; } = 0;
}
15.6 属性初始化对象
Eg. //Side1Length是Triangle类下的属性
Triangle tri1 = new Triangle { Side1Length = 20 };//大括号内是对象初始化器
使用构造器的情况:
Eg. //Side1Length是Triangle类下的属性
//Triangle类有一个参数为string的构造器
Triangle tri1 = new Triangle ("Equal"){ Side1Length = 20 };
索引器
16.1 什么是索引器
属性->智能字段 索引器->智能数组
定义:
Eg. class Class1
{
private int[] data;
public int this[int i]//用this替代名称,[]内是下标类型
{
get => this.data[i];
set => this.data[i] = value;
}
}
注意:
- 每个类或结构只允许定义一个索引器,且总命令为this
- 索引器和数组的区别
- 数组只能整数下标
- 索引器可以重载【不是重写!!】(索引器参数类型,即[]内的参数类型必须不一样)
Eg. public int this[string i]
{
//省略内容
}
public string this[int i]
{
//省略内容
}
-
- 索引器不能作为ref,out参数
数组作为属性和索引器的区别
数组作为属性会直接修改该类/结构中数组属性的值,因此不管实例化几个值都相同
索引器就不会产生这种问题,每一次修改都是修改实例化的对象内的数组值
16.2 接口中的索引器
Eg. interface IA
{
int this [int i] { get;set; }//在类中具体实现
}
索引器加virtual->让派生类可以重写
Eg. public virtual int this[int i]
索引器的显式实现:
Eg. class A:IA
{
private int a;
int IA.this[int i]
//索引器的显示实现是非公共(不用加访问修饰符,默认private)和非虚的(不能重写)
{
get => this.a;
set => this.a = value;
}
}
泛型
17.2 泛型解决方案
用于创建常规化的类和方法(可以适用于任意类)
类型参数:<T>
定义:
Eg. class Queue<T>
{
private T[] data;
}
不同的T会生成不用的类
泛型类与常规类(object)的区别:
object在使用时任何情况下都是同一类实例
使用泛型每次指定一个新的类型都会自动生成一个新的类
泛型类的具体版本成为已构造类
泛型的约束
确保泛型类使用的类型参数是提供了特定方法的类型
利用接口约束:
Eg. public class A<T> where T:IA//约束T必须要实现接口IA
17.3 创建泛型类
类库:是已经编译好的多个类的集合,所有类型都存储在程序集(.dll文件)
构造器名称不能包含<T>
Eg. public class A<T>
{
int i;
public A()//public A<T>()是错误的
{
this.i = 0;
}
}
17.4 泛型方法
Eg. public void MyMethod<T>(int i)
{
T a;
//省略内容
}
//调用
int i = 0;
MyMethod<int>(i);
17.5 可变性和泛型接口
协变性:泛型类型参数T可以从派生类隐式转换为基类,用<out T>标记,协变的接口的方法只能有返回值(结合例子理解)
Eg. interface IRetrieveWrapper<T>
{
T GetData();
}
interface IStoreWrapper<T>
{
void SetData(T data);
}
class Wrapper<T> : IRetrieveWrapper<T>,IStoreWrapper<T>
{
private T Data;
void IStoreWrapper<T>.SetData(T data)
{
this.Data = data;
}
T IRetrieveWrapper<T>.GetData()
{
return Data;
}
}
//以上是类的定义,接下来的写在Main函数中
Wrapper<string> wrapper = new Wrapper<string>();
IStoreWrapper<string> storeWrapper = wrapper;
storeWrapper.SetData("Hello");
IRetrieveWrapper<string> retrieveWrapper = wrapper;
Console.WriteLine($"{retrieveWrapper.GetData()}");
//将类型参数转换为string的父类object
IRetrieveWrapper<object> retrieveWrapper1 = wrapper;
//上述语句不合法,不能将子类接口对象赋给父类接口对象,编译器会报错,这种接口称为不变量
//将该接口修改为协变接口就能让子类接口对象赋给父类接口对象,接口的类型参数前加out
//将接口定义修改为:interface IRetrieveWrapper<out T>
IRetrieveWrapper<object> retrieveWrapper1 = wrapper;//合法了
注意:
- 只有接口内的方法返回类型是类型参数才能使用out,如果类型参数作为接口内方法的传入参数,添加out就是非法的
- 协变性只适合引用类型,因为值类型没有继承
- 只有接口和委托类型才能使用out修饰符,泛型类不能使用out
逆变性:与协变性正好相反,泛型类型参数可以从基类隐式转换为派生类,用<in T>标记,逆变的接口的方法只能有传入的参数
Eg. public interface ICustomContravariant<in T>
{
void Get(T t);
}
public class CustomContravariant<T>:ICustomContravariant<T>
{
public void Get(T t)
{
//省略实现内容
}
}
//上面是类的定义,接下来的写在Main函数内
ICustomContravariant<string> custom = new CustomContravariant<object>();//合法