——委托
可以理解为:委托是一个包含有序方法列表的对象,这些方法拥有相同类型的参数列表和返回值。
委托既可以卸载类外,也可以写在类内。
委托可以被继承
委托既可以添加普通方法,也可以添加静态方法。
delegate string Delegate1(string str); //声明在类的外部
class Base
{
public delegate int Delegate2(int val); //声明在类的内部
public void Fun()
{
Delegate1 de1 = new Delegate1(Fun3); //创建类外部的委托的变量
Console.WriteLine(de1("Delagate1"));
Delegate2 de2 = new Delegate2(Fun1); //创建类内部的委托的变量
de2 += Fun2; //添加静态方法
Console.WriteLine(de2(10)); //打印15,因为委托返回值会返回最后一个方法的返回值
}
public int Fun1(int val)
{
return val * 2;
}
static int Fun2(int val)
{
return val + 5;
}
string Fun3(string str)
{
return str;
}
}
class Child : Base
{
public void Method()
{
Delegate2 de = new Delegate2(Method1); //使用基类内部创建的委托
}
public int Method1(int val)
{
return val + 10;
}
}
class Program
{
static void Main(string[] args)
{
Base b = new Base();
b.Fun();
Console.ReadLine();
}
}
如果委托使用引用参数,那么这个参数在委托中第一个方法使用完后,得到的数值再传入下一个方法。
匿名方法
所谓匿名方法,就是没有名字的方法。
当某些方法,只需要在委托中使用一次,其它地方不会调用这个方法,那么可以不声明这个方法,在委托使用时以匿名方法添加上就ok。匿名函数不需要写返回值。
比如:
class Base
{
public delegate int MyDel(int val); //委托
public void Fun()
{
MyDel myDel1 = delegate (int val) { return val * 2; };//委托初始化时使用匿名方法
myDel1 += delegate (int val) { return val + 5; };//添加方法时使用匿名放啊
}
}
Lambda表达式
Lambda表达式是就是用于进一步简化匿名方法的(Lambda是在匿名方法之后才有的,如果先有Lambda,那么可能就不会有匿名方法了)。因此,所有可以用匿名函数的地方都可以使用Lambda表达式。下面的例子展示Lambda的用法。
class Base
{
public delegate int MyDel1(int x, int y);//两个参数的委托
public delegate int MyDel2(int val);//一个参数的委托
public delegate void MyDel3();//没有参数,没有返回值的委托
public void Fun()
{
MyDel1 myDel0 = delegate (int x,int y) { return x + y; };//匿名方法
MyDel1 mydel1 = (int x, int y) => { return x + y; };//有两个参数的Lambda表达式
MyDel2 myDel2 = (val) => { return val * 2; };//如果只有一个参数,那么可以不写参数的类型
MyDel2 myDel3 = val => { return val * 2; };//甚至括号都可以省略
MyDel2 myDel4 = val => val * 2;//如果出了返回一个值,没有其他语句,可以去掉大括号和return。
MyDel3 myDel5 = () => { };//委托不需要返回值,也不需要参数,如果没有要执行的方法体,可以简化到这样
}
}
——事件
事件其实就是使用委托,只不过事件是专门用于某种特殊用途的委托。也就是发布者/订阅者订阅者模式。
发布者定义一系列的程序,订阅者在发布者中注册(也就是往发布者的委托中加入方法),以便在发布者发生事件时通知订阅者(调用事件委托)。
事件对委托做了一些限制,
1、事件将内部委托私有化,你无法在其他类中访问委托
2、事件的操作非常少,只有添加,移除,调用。
3、事件触发时,让内部委托依次调用方法列表中的方法。
4、事件是直接调用使用的,不可以实例化对象。
事件和委托其实就是下面这种结构
一个普通的事件的使用:
class Base
{
public delegate void Del();//定义事件用到的委托
public event Del Eve; //定义事件
public void Fun()
{
Eve(); //调用事件
}
}
class Other
{
Base b = new Base();
public void Method()
{
b.Eve += Fun1; //在事件中注册
}
public void Fun1()
{
Console.WriteLine("this is Fun1");
}
}
——接口
什么时候是必须用到接口,而单纯靠继承是无法完成的呢?
比如下面的代码,我们无法让PrintInfo的参数可以接收任意类。
class Ca
{
public void Fun()
{
Console.WriteLine("this is ca");
}
}
class Cb
{
public void Fun()
{
Console.WriteLine("this is cb");
}
}
class Program
{
static void Main(string[] args)
{
Ca ca = new Ca();
Cb cb = new Cb();
PrintInfo(ca);//打印 tihs is ca
PrintInfo(cb);//错误
Console.ReadLine();
}
static void PrintInfo(Ca ca)
{
ca.Fun();
}
}
这个时候我们就可以使用接口,让两个类都实现这个接口,而PrintInfo函数的参数就是这个接口类型,这时,由于两个类都实现了IFun接口,所以它们都可以作为参数传递进去
interface IFun
{
void Fun();
}
class Ca : IFun
{
public void Fun()
{
Console.WriteLine("this is ca");
}
}
class Cb : IFun
{
public void Fun()
{
Console.WriteLine("this is cb");
}
}
class Program
{
static void Main(string[] args)
{
Ca ca = new Ca();
Cb cb = new Cb();
PrintInfo(ca);//打印this is ca
PrintInfo(cb);//打印this is cb
Console.ReadLine();
}
static void PrintInfo(IFun c)
{
c.Fun();
}
}
关于Array.Sort();方法
它其实不是可以排序int类型,而是可以排序继承自IComparable接口的类型,在这个接口中包含唯一一个方法CompareTo(),这个方法规定,在调用它时,需要返回一下几个值:
负数值:当前对象小于用于比较的参数对象
正数值:当前对象大于用于比较的参数对象
0:当前对象等与用于比较的参数对象
因为int实现了IComparable接口的CompareTo()函数,所以int类型可以排序,而Ca没有实现这个接口,所以不能排序
class Ca
{
public int value = 0;
}
class Program
{
static void Main(string[] args)
{
//排序一个int类型数组
int[] arrs = new int[] { 5, 1, 3, 2, 4 };
Array.Sort(arrs);
foreach (var item in arrs)
{
Console.Write(item + " ");
}
//排序Ca类型数组
Ca[] cas = new Ca[5];
for (int i = 0; i < cas.Length; i++)
{
cas[i] = new Ca();
cas[i].value = i + 1;
}
Array.Sort(cas);//这段可以正常写,但是运行会报错
foreach (var item in cas)
{
Console.Write(item.value + " ");
}
Console.ReadLine();
}
}
所以需要让Ca类实现IComparable接口,就可以排序了。
class Ca : IComparable
{
public int value = 0;
public int CompareTo(object obj)
{
Ca ca = (Ca)obj;
if (value < ca.value)
{
return -1;
}
if (value > ca.value)
{
return 1;
}
return 0;
}
}
class Program
{
static void Main(string[] args)
{
//排序Ca类型数组
Ca[] cas = new Ca[5];
for (int i = 0; i < cas.Length; i++)
{
cas[i] = new Ca();
cas[i].value = i + 1;
}
Array.Sort(cas);//打印1 2 3 4 5
foreach (var item in cas)
{
Console.Write(item.value + " ");
}
Console.ReadLine();
}
}
接口的成员
接口的成员只能是非静态的函数成员,如方法,属性,索引器,事件。
接口的实现
类和结构体都可以实现接口
接口是引用类型
接口是引用类型,它虽然不可以实例化,但是可以用接口声明引用,实现类去实例,就像父类引用,子类实例一样,代码如下:
interface IFun
{
void Fun();
}
class Ca : IFun
{
public void Fun()
{
Console.WriteLine("this is fun");
}
}
class Cb : Ca
{
}
class Program
{
static void Main(string[] args)
{
Cb cb = new Cb();//子类对象
cb.Fun();
Ca ca = cb;//父类引用,子类实例
ca.Fun();
IFun ifun = ca;//接口引用,实现类实例
ifun.Fun();
Console.ReadLine();
}
}
在堆中的形式如下:
接口使用 as 运算符
如果类没有实现接口而去强转为接口,那么会报错,这时,可以使用 as 运算符强转,它可以让无法发生的强转返回null,而不是报错,代码如下:
interface IFun
{
void Fun();
}
class Ca
{
public void Fun()
{
Console.WriteLine("this is fun");
}
}
class Program
{
static void Main(string[] args)
{
Ca ca = new Ca();
IFun ifun = ca;//报错,因为Ca类没有实现该接口
IFun ifun = ca as IFun;//返回null而不是异常
ifun.Fun();
Console.ReadLine();
}
}
多个接口的函数成员签名相同
如果一个类实现多个接口,并且多个接口定义的函数成员签名相同,那么类可以实现单个函数满足所有接口
interface IFun1//接口1
{
string Fun(string s);
}
interface IFun2//接口2
{
string Fun(string s);
}
class Ca : IFun1,IFun2//实现接口1和接口2
{
public string Fun(string s)//实现一个函数满足两个接口
{
return s;
}
}
显示实现接口成员
如果类继承了两个函数成员签名一样的接口,但是这两个接口的函数成员实现不一样的功能,那么需要分别实现,可以使用显示实现接口成员的方式,但是要注意,显示实现接口成员只能私有,不能添加访问修饰符,并且调用也只能通过接口引用才可以调用,如下例子:
interface IFun1//接口1
{
string Fun(string s);
}
interface IFun2//接口2
{
string Fun(string s);
}
class Ca : IFun1, IFun2//实现接口1和接口2
{
string IFun1.Fun(string s)
{
return "this is IFun1";
}
string IFun2.Fun(string s)
{
return "this is IFun2";
}
}
class Program
{
static void Main(string[] args)
{
IFun1 fun1 = new Ca();
fun1.Fun("Fun1");//只能通过接口引用调用
IFun2 fun2 = new Ca();
fun2.Fun("Fun2");//只能通过接口引用调用
Console.ReadLine();
}
}
接口的继承
interface IFun
{
void Fun();
}
class Ca//没有继承接口,但是有满足接口的函数
{
public void Fun()
{
Console.WriteLine("this is fun");
}
}
class Cb : Ca, IFun//继承Ca,又继承接口,间接使用Ca的Fun函数实现接口
{
}
class Cc : Cb//继承Cb
{
}
class Program
{
static void Main(string[] args)
{
IFun ifun = new Cc();//使用Cc实例接口的引用
ifun.Fun();
Console.ReadLine();
}
}
接口可以继承其他接口
直接看例子就能明白
interface IFun1//接口1
{
string Fun(string s);
}
interface IFun2//接口2
{
string Fun(string s);
}
interface IFun : IFun1, IFun2
{
}
class Ca : IFun//实现接口1和接口2
{
string IFun1.Fun(string s)
{
return "this is IFun1";
}
string IFun2.Fun(string s)
{
return "this is IFun2";
}
}
——转换
checked和unchecked
checked和unchecked可以用来检测溢出。
如果用于表达式,那么溢出时,checked会抛出异常,unchecked会继续执行。
int i = 100000;
byte b = unchecked((byte)i);//打印b
byte c = checked((byte)i);//抛出异常
也可以用作语句,如果用于语句,效果也是一样的
int i = 100000;
checked
{
byte b = (byte)i;//抛出异常
}
unchecked
{
byte b = (byte)i;//继续执行
}
浮点型转换为整形
浮点型会舍去小数部分
double转float
flaot占32位,double占64位,double 类型的值会舍入到最接近float类型的值。之后,如果值太小导致无法转为float表示,那么值会被设置为正或负0.。如果值太大导致无法转为float表示,那么值会被设置为无穷大或负无穷大。
关于引用类型的转换
引用类型的转换,其实是改变引用指向堆中的内存。
显示的引用类型转换
之前说过,可以用父类声明,子类构造,但不可以子类声明,父类构造。之前解释是会报错,其实是抛出InvalidCastException异常,不会导致编译错误,需要特别注意。有两种情况是可以子类声明,父类构造的
1、父类对象为空
class A
{
}
class B : A
{
}
class Program
{
static void Main(string[] args)
{
A a = null;
B b = (B)a;
Console.ReadLine();
}
}
2、父类已经是子类对象的引用
class A
{
}
class B : A
{
}
class Program
{
static void Main(string[] args)
{
B b = new B();
A a = b;
B b2 = (B)a;
Console.ReadLine();
}
}
装箱
值类型默认在堆上不包括它们的对象组件,然而,如果需要对象组件,我们可以使用装箱。装箱是一种隐式转换,他接受值类型的值,根据这个值在堆上的创建一个完整的引用类型对象,并返回对象引用。也就是说,如果想把值类型转为引用类型,可以通过装箱。
如上面的例子,装箱一个值类型。
上述例子存在一个知识点,装箱并不是在被装箱的项上发生了操作,而是对值的副本进行操作,装箱之后会有原值和引用类型副本两个,都可以独立操作。
拆箱
拆箱是将引用类型转换为值类型显示转换,在拆箱时,系统会进行以下操作:
检测要拆箱的对象实际的装箱值。
把对象的值复制到变量里。
因此,拆箱后的变量和拆箱前的引用对象也都是独立的。如果拆箱为一个非它的原始类型,会抛出InvalidCastException异常。
自定义转换
之前介绍过自定义转换,这里再说几点约束:
1、只能为类或结构体定义自定义转换,不能重新定义标准的隐式或显示转换
2、只能定义不同类型的转换,不能是继承关系。
is运算符和as运算符
is运算符可以在转换之前判断是否可以转换,返回true或false。
class A
{
}
class B : A
{
}
class Program
{
static void Main(string[] args)
{
B b = new B();
A a = null;
if (a is B)//判断是否可以转换
{
a = b;
}
Console.ReadLine();
}
}
as运算符进行强制转换,即使无法强制转换也不会抛出异常,只会返回null。
——泛型
泛型的约束
使用where关键字
where T : struct 那么T只能是指类型
where T : class 那么T只能是引用类型(比如类,数组,委托,接口)
where T : 类型 那么T只能是这个类,或者是派生这类类
where T : 接口名 那么T只能是这个接口,或者是实现这个接口的类型
where T : class,new() 那么T必须包含公共的无参的构造函数,不然就会报错
——枚举器与迭代器
对于数组可以使用forea遍历,是因为数组是一个可枚举类型,可枚举类型通过枚举器访问每一个项。IEnumerator(枚举器接口)
枚举器必须实现该接口,该接口包括3个重要的函数成员:
1、Current:它是一个属性,是只读的,用来返回当前索引的项,返回值是一个object,所以可以返回所有类型。
2、MoveNext:它是一个方法,用来把当前索引移到下一个索引位置。返回一个bool值。
3、Reset:它是一个方法,用来把位置重置为原始状态的方法。
IEnumerable(可枚举类型)
可枚举类型接口有一个函数,就是GetEnumerator(),它返回一个IEnumerator,也即是枚举器,foreach其实可以理解给我就是调用可枚举类型中的GetEnumerator()获取枚举器,然后通过这个枚举器调用MoveNext和Current来遍历所有项。
下面演示一个简单的的枚举器和可枚举类型:
//枚举器
class MyColor : IEnumerator
{
string[] color;
int index = -1;
public MyColor(string[] color)
{
this.color = new string[color.Length];
for (int i = 0; i < color.Length; i++)
this.color[i] = color[i];
}
public object Current
{
get
{
if (index == -1)
throw new InvalidOperationException();
else if (index > color.Length - 1)
throw new InvalidOperationException();
return color[index];
}
}
public bool MoveNext()
{
if (index < color.Length - 1)
{
index++;
return true;
}
return false;
}
public void Reset()
{
index = -1;
}
}
//Spectrum类定义为可枚举类型
class Spectrum : IEnumerable
{
string[] color = { "White", "Red", "Green", "Pink" };
//使用MyColor枚举器枚举
public IEnumerator GetEnumerator()
{
return new MyColor(color);
}
}
class Program
{
static void Main(string[] args)
{
Spectrum spe = new Spectrum();
foreach (var item in spe)//foreach遍历
{
Console.WriteLine(item);
}
Console.ReadLine();
}
}
上述代码Spectrum类为可枚举类型,它使用MyColor枚举器进行枚举,所以它可以被foreach遍历。
其实主函数的foreach就相当于如下代码:
class Program
{
static void Main(string[] args)
{
Spectrum spe = new Spectrum();
IEnumerator enumerator = spe.GetEnumerator();//获取枚举器
while (enumerator.MoveNext())//索引写一个
{
object obj = enumerator.Current;//当前索引的值
string str = (string)obj;
Console.WriteLine(str);
}
enumerator.Reset();//恢复
Console.ReadLine();
}
}
迭代器
迭代器其实就是对枚举器的简化,使用迭代器,程序在编译时自动为我们创建枚举器,枚举器用到 yield return 或者 yield break
1、迭代器返回枚举器
class MyClass : IEnumerable
{
public IEnumerator GetEnumerator()//在GetEnumerator方法中,返回枚举器
{
return GetColor();//返回GetColor迭代器返回的枚举器
}
public IEnumerator GetColor()//返回枚举器
{
yield return "Red";
yield return "Black";
yield return "Green";
yield return "Pink";
yield return "Yellow";
}
}
class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
foreach (var item in mc)
{
Console.Write(item + " ");
}
Console.ReadLine();
}
}
2、迭代器返回可枚举类型
class MyClass : IEnumerable
{
public IEnumerator GetEnumerator()//在GetEnumerator方法中,返回枚举器
{
return GetColor().GetEnumerator();//通过调用GetColor返回的IEnumerable(可枚举类型)的GetEnumerator方法,返回枚举器
}
public IEnumerable GetColor()//返回可枚举类型
{
yield return "Red";
yield return "Black";
yield return "Green";
yield return "Pink";
yield return "Yellow";
}
}
class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
foreach (var item in mc)//直接调用MyClass中的GetEnumerator方法迭代
{
Console.Write(item + " ");
}
Console.WriteLine();
foreach (var item in mc.GetColor())//调用MyClass中的GetColor方法返回的IEnumerable中的GetEnumerator方法迭代
{
Console.Write(item + " ");
}
Console.ReadLine();
}
}
迭代器作为属性
class MyClass : IEnumerable
{
bool b;
public MyClass(bool b)
{
this.b = b;
}
public IEnumerator GetEnumerator()
{
return b ? GetColor1.GetEnumerator() : GetColor2.GetEnumerator();//通过b的位true或者false判断调用哪个迭代器属性
}
private IEnumerable GetColor1//迭代器属性1
{
get
{
yield return "Red";
yield return "Black";
yield return "Green";
yield return "Pink";
yield return "Yellow";
}
}
private IEnumerable GetColor2//迭代器属性2
{
get
{
yield return "Yellow";
yield return "Pink";
yield return "Green";
yield return "Black";
yield return "Red";
}
}
}
class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass(true);
MyClass mc2 = new MyClass(false);
foreach (var item in mc)
{
Console.Write(item + " ");
}
Console.WriteLine();
foreach (var item in mc2)
{
Console.Write(item + " ");
}
Console.ReadLine();
}
}
迭代器的实质
1、使用迭代器,需要引入System.Collections.Generic命名空间。
2、迭代器没有实现枚举器中的Resset方法,因此接口调用该方法时会抛出异常。
3、在后台,编译器产生的枚举器是包含4个状态的状态机
Befor:首次调用MoveNext的初始状态
Running:调用MoveNext会进入这个状态,在这个状态中,枚举器检测并设置下一项的位置。在遇到yield return或者yield break或迭代块结束时,退出状态。
Suspende状态机等待下次调用MoveNext状态
After:没有更多项可以枚举。