①操作值类型:将内存中的值复制一份“副本”传递给形参,所以形参改变不会影响实参。
②操作引用类型:将对象在内存中的地址复制一份“副本”传给形参,虽然是副本,但是指向一样,所以形参也指向了相同的地址,故而形参改变好像是改变了实际参数。
形参与实参之前,都要加ref关键字。需要注意的是,实参在使用之前必须赋值,否则编译器会报错。此时不再复制实参在栈中的副本,而是将实参在栈中的地址传给形参,也就是实参与形参共用栈中的值。此时在方法中对形参所做的任何操作,都会影响实参。
①值类型:由于a与x,b与y指向了栈中相同的地址,在Add方法中对形参x与y的操作,会影响实参a与b的值。
class Program { static void Main(String[] args) { int a = 4; int b = 5; Add(ref a, ref b); Console.WriteLine($"a={a},b={b}"); //输出a=5,b=6 } public static void Add(ref int x, ref int y) { x += 1; y += 1; } }
②引用类型:这时Add方法中形参animal指向的不再是实参a在栈中的副本的地址,而是直接与a指向了栈中相同的位置。
class Program { static void Main(String[] args) { Animal a = new Animal() { Age = 1 }; Add(ref a); Console.WriteLine($"a.Age={a.Age}"); //输出animal.age=11 } public static void Add(ref Animal animal) { animal.Age += 1; } } class Animal { public int Age { get; set; } }
用法:
(1)形参与实参之前,都要加out关键字。 (2)输出参数主要是用于函数需要有多个返回值时,作为返回值使用。
与引用变量相同的是:
(1)输出参数也不复制实参在栈中的副本,而是将实参在栈中的地址传给形参。在这点上,输出参数与引用参数相同。 (2)实参必须是可以赋值的变量,而不能是常亮。
与引用参数不同的是:
(1)实参在使用之前不必赋值。 (2)事实上,在使用之前对实参的赋值没有任何意义,因为在调用方法的最开始,便会将其值抹去,使其成为未赋值的变量。 (3)在方法返回之前,必须对out参数进行赋值。 由以上特点所决定的是,输出参数无法向方法中传递信息,其唯一作用便是,当一个参数需要返回多个值时,作为返回值返回。
class Program { static void Main(String[] args) { int a; Add(1, 2, out a); Console.WriteLine(a); //输出3 } public static bool Add(int x, int y, out int result) { result = x + y; return true; } }
在C艹中与java一样有两种数据类型转换,即自动类型转换和强制类型转换。此外C#提供了Covert类,其中各种方法用于转换不同的数据类型。
Convert.ToInt32() 转换为整型(int) Convert.ToChar() 转换为字符型(char) Convert.ToString() 转换为字符串型(string) Convert.ToDateTime() 转换为日期型(datetime) Convert.ToDouble() 转换为双精度浮点型(double) Conert.ToSingle() 转换为单精度浮点型(float)
多维数组
多维数组声明,在声明时必须指定数组的长度, type [lenght ,lenght ,lengh, ... ]
int [,] test1 = new int [3,3];
或者声明时赋值,系统判断长度:
int [,] test1 = { {1,2,3}, {1,2,3}, {1,2,3}, };
交错数组
声明时,至少要指定第一维的长度,格式为 type [ ] [ ] [ ] ...
int [][] test1 = new int[5][]; int [][] test1 = new int[][]; //注意,此的声明方式是错的
或者声明时即赋值,由系统推断长度
int [][] test1 = { new int[] {1,2,3,4}, new int[] {1,2,3}, new int[] {1,2} };
多维数组与交错数组 二者的相同、区别
两者声明时,都必须指定长度,多维数组必须指定每一维的长度,而交错数组需要至少需要指定第一维的长度。
多维数组声明时,符号是这样的 [ , , , , ],逗号在 方括号 [ ] 中,每一维长度用逗号分隔。而交错数组每一维独立在 [ ]中
当你想指定数组长度时,只能在等号右侧指定,int [,] test1 = new int [3,3] 是正确的 ;int [6,4] test1 = new int [6,4] 是错误的;
下面以代码形式说明
int [,] test1 = { {1,2,3,4}, {1,2,3}, {1,2} }; //这样是错的,长度必须一致 int [,] test1 = new int [4,5] { {1,2,3,4,5}, {1,2,3}, {1,2,3} }; //这样也是错误的,长度必须一致,必须为每一个位置赋值
这一点C#与C语言有所区别,C语言可以不全赋值,没有赋值的位置系统默认为0。
下面的方法是正确的
//多维数组 int [,] numbers = new int[3,2] {{1,2},{3,4},{5,6}}; int [,] numbers = new int[,] {{1,2},{3,4},{5,6}}; //交错数组(数组的数组) int [][] numbers = new int[2][] {new int{2,3,4},new int{5,6,7,8,9}}; int [][] numbers = new int[][] {new int{2,3,4},new int{5,6,7,8,9}}; int [][] numbers = {new int{2,3,4},new int{5,6,7,8,9}};
重载是函数的重载,两个同名函数,参数类型或个数不同,构成重载关系,需注意,返回值不作为函数重载的判断条件。多用于子类继承了父类的函数,需要不同的实现。
当子类与父类有完全一样的方法时,称“子类隐藏了父类的同名方法”,如下:
class Parent{ public void HideF(){ Console.WriteLine("Praent.HideF"); } } class Child : Parent{ public void HideF(){ Console.WriteLine("Child.Hidef"); } }
当重名时调用哪个方法取决于谁(子类还是父类对象)调用:
Child c = new Child(); c.Hidef(); //Child.Hidef Parent p = new Parent(); p.Hidef(); //Praent.HideF
下面调用的是子类还是父类?
Child c = new Child(); Parent p; p = c; p.HideF(); //Parent.Hidef //如果需要调用子类方法则需要转换类型 ((Child)p).HideF(); //Child.Hidef
虽然能输出结果,但是不符合C#的语法规范,修改后如下:
class Child : Parent{ public new void HideF(){ Console.WirteLine("Child.Hidef"); } }
new告诉编译器,子类隐藏了父类的同名方法,提供自己的新版本,如果还想调用父类的方法,可以使用base关键字:
base.HideF(); //Parent.Hidef
为了达成“如果父类变量引用了子类对象则调用子类的方法,父类变量引用父类对象调用父类方法”,可以在父类的同名方法前面加上关键字virtual,表示这是虚方法,子类可以重写这个:即在子类同名方法前面加上override,表示进行了重写。
class Parent{ public virtual void OverrideF(){ Console.WirteLine("Parent.OverrideF"); } } class Child{ public override void OverrideF(){ Console.WirteLine("Child.OverrideF"); } }
解决引用的问题:
Child c = new Child(); Parent p; p = c; p.OverrideF(); //Child.OverrideF
以上示例说明,当父类的方法是虚方法时,子类重写了同名方法,通过父类变量去调用,调用谁的取决于父类变量是哪个对象的真实引用。
多态有两个必要条件,一是有继承,二是父类引用指向子类的对象。
class Animal{ //参数是一个父类类型,但能接收传过来的子类对象,也就是“父类引用指向子类的对象” public void shower(Animal a){ } public void eat(){ System.out.println("吃东西"); } public void helpEat(Animal a){ //调用具体对象的eat方法,但是每个动物不同,就需要自身继承了eat方法后进行拓展,也就是重写 a.eat(); } }
委托类型的变量可以接受一个函数的地址,也就是将方法以变量的形式传递,以方法的形式执行。可以看成一个容器,将某个具体的函数装入,就可以把这个容器当函数用。但是委托实际上不是函数的容器,是一个派生自Delegate的类。
课本示例:
public class Math{ public int Add(int x , int y){ return x + y; } } //定义一个委托类型 public delegate int MathDelegateAdd(int value1 , int value2); MathDelegateAdd dgt; Math obj = new Math(); dgt = obj.Add; Console.WirteLine(dgt(1,2)); //输出3
上述例子不是所有函数都能委托给agt对象,只有有两个int类型参数的,且返回值也是int的才可以(废话),一个委托对应着一种函数的signature。
委托不仅可以代表一个函数,更可以代表一堆函数批量执行(委托的组合和分解):
//参数为string,无返回的委托类型 delegate void MyDelegate(string s); //定义类 class MyClass{ public static void Hello(){ Console.WriteLine("你好,{0}",s); } public static void GoodBye(){ Console.WriteLine("再见,{0}",s); } } class Program{ static void Main(string args[]){ MyDelegate a,b,c,d; a = MyClass.Hello; Console.WriteLine("调用委托变量 a:"); a("a"); b = MyClass.GoodBye; Console.WriteLine("调用委托变量 b:"); b("b"); c= a + b; Console.WriteLine("调用委托变量 c:"); c("c=a+b"); //顺序调用两个方法 d = c - a; Console.WriteLine("调用委托变量 d:");\ d("d=a-c") //只调用GoodBye } }
事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理程序关联。
主要特点:一对多关联,即一个事件源,多个响应者。.NET Framework的事件处理机制是基于多路委托。
组成一个事件的五个部分:事件的拥有者、事件拥有者的某个事件成员、事件订阅器、事件响应者以及事件响应者的成员。
简单来说事件实质就是:事件的拥有者(Event source)的某个事件成员(Event,成员)被调用导致订阅该事件的事件响应者(Event Subcriber)调用事件处理成员(Event Handler,成员)被触发(本质上就是一个回调函数被调用)。
事件的作用:事件与属性类似本质是一个包装器,目的是为了更好的保护类内部的委托类型成员,事件只能被+=或-=,在这一过程中实际上操作的是事件拥有者的委托类型成员,从而保护类内部的委托类型成员不被外部随意修改。
//定义一个委托 public delegate void MyMultiDelegate(int value); //事件发布者 public class Publisher{ public MyMultiDelegate handers; //事件响应者清单 } //事件响应类 public class Subscriber{ //事件处理函数 public void MyMethod(int i){ Console.WriteLine(i); } }
事件响应代码:
static void Main(string args[]){ Publisher p = new Publisher(); //两个事件响应者 Subscriber s1 = new Subscriber(); Subscriber s2 = new Subscriber(); //直接调用Delegate类的静态方法组合成多个委托 p.handlers = System.Delegate.Combine(p.handlers,new MyMultiDelegate(s1.MyMethod)) as MyMultiDelegate; p.handlers = System.Delegate.Combine(p.handlers,new MyMultiDelegate(s2.MyMethod)) as MyMultiDelegate; //调用+=运算符组合委托 p.handlers += new MyMultiDelegate(s1.MyMethod)); p.handlers += new MyMultiDelegate(s2.MyMethod)); //最简单的写法 p.handlers += s1.MyMethod; p.handlers += s2.MyMethod; p.handlers(10); }
执行最后一句,调用两个事件响应s1和s2的事件响应函数,输出两个整数。
为了限制事件的激发只能由事件源对象引发,引入event关键字
public delegate void MyMultiDelegate(int value); public class Publisher{ //事件发布者类 public event MyMultiDelegate handlers; public void FireEvent(){ //激发事件 handlers(10); } } public class Subscriber{ //事件响应者 public void MyMethod(int i){ //事件处理函数 Console.WirteLine(i); } }
模拟事件响应:
static void Main(string args[]){ Publisher p = new Publisher(); //两个事件响应者 Subscriber s1 = new Subscriber(); Subscriber s2 = new Subscriber(); //声明为事件的委托无法直接调用Combine方法 //这两句不能用 //p.handlers = System.Delegate.Combine(p.handlers,new MyMultiDelegate(s1.MyMethod)) as MyMultiDelegate; //p.handlers = System.Delegate.Combine(p.handlers,new MyMultiDelegate(s2.MyMethod)) as MyMultiDelegate; //必须用+=来追加委托 p.handlers += new MyMultiDelegate(s1.MyMethod)); p.handlers += new MyMultiDelegate(s2.MyMethod)); //声明为事件的委托方法也不能直接调用,无法编译 //p.handlers += s1.MyMethod; //p.handlers += s2.MyMethod; 只能通过类的公有方法间接引发事件 p.FireEvent(); Console.ReadKey(); } //10 //10
结论:多路委托允许在事件源对象之外激发事件
Console.readkey():监听键盘事件,可以理解为按任意键执行。 Console.read():读取键盘输入的第一个字符,返回ASCII值。回车退出 Console.readline():读取所有字符,返回字符串。回车退出 Console.Write():控制台输出,不换行。 Console.Writeline():控制台输出,换行。