100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > (04)基础强化:接口 类型转换cast/convert 异常处理 传参params/ref/out 判断同一对象

(04)基础强化:接口 类型转换cast/convert 异常处理 传参params/ref/out 判断同一对象

时间:2023-01-24 12:13:36

相关推荐

(04)基础强化:接口 类型转换cast/convert 异常处理 传参params/ref/out 判断同一对象

一、复习

1、New的截断是指什么?

new除了新开空间创建初始化对象外,还有一个隐藏父类同名方法的作用。

当子类想要隐藏父类同名的方法时用new,用了new后父类同名方法将到此为止,后面

继承的子类,将再也继承不到父类的同名方法,相当于由此截断,断子绝孙。

2、参数传递有几种,有什么区别?

参数传递有两种:值传递与引用传递

值传递:栈中内容的副本拷贝。

引用传递:传递的是栈本身的地址,相当于给变量起了一个别名。都表示同一个变量。

注意:

out比较特别,它只能传出不能传入。传出用的引用传递。

3、把接口当作参数传递 是什么个回事?

后面马上讲

4、方法重载overload与方法重写override,及以隐藏new的区别是什么?

重载overload: 至少两个及以上方法,方法名相同,但参数个数或类型或顺序等不同。

根据同名的不同函数签名调用对应不同的同名函数。

在程序编译的时候已经确定它一定调用对应的同名方法。

重写override: 改写父类继承过来的同名函数。必须与abstract与virtual成双出现。

主要用在多态上。

在程序编译的时候无法确定它到底调用那个,因为父类由动态的子类来

赋值,子类在运行时才能确定到底是哪一个子类。

隐藏new: 隐藏父类继承过来的同名函数,从此截断,不再继承下去。

new可以和abstract或virtual进行联合修饰,但不能和override联合

判断:方法重载与重写都是实现多态的有效方式?

有些人认为:重载是编译器多态,重写是运行时多态。

有些人认为:多态是面向对象的概念,所以重写是多态,而重载不能算是多态。

二、怎么实现多态2-接口

1、什么是接口?

接口就是一种规范,协议(*),约定好遵守某种规范就可以写通用的代码。

定义了一组具有各种功能的方法。

(只是一种能力,没有具体实现,像抽象方法一样,“光说不做”)

接口既是代码的规范,也是人力资源的规范。

在代码上,对功能进行修改封闭,大家都知道有这个接口,具体怎么实现不知道。

反正简便使用这个接口功能即可,无需了解很多。而修改的人反正有了接口,放

心进行功能修改和优化即可。接口通过约束与规范,就把写与用两方面的人有机

结合起来。

另一方法是人力资源的规范。分配工作时,先确定有这个接口,然后分配人力,

A组用写好的接口去实现他们的具体功能,而B组则去写那些写好但没有具体的代

码的接口,可以提高工作效率。同时底层与应用层逻辑界限清晰。

简单地说就是:对修改封闭,对扩展开发。

当买优盘时,不用担心大小和能否能用?

因为所有电脑都留下了一个USB接口,电脑只管这个大小的USB口和读写方法。而

U盘只须按对应的USB接口进行制作,至于存储的大小和速度可以随意,但接口尺

寸和读写方法是确定好的。

通过接口规范了电脑与U盘相互必须遵守,这样极大地方便使用。

同理,内存条也可以放心插入到电脑主板中,不用担心能否使用的问题。

思考:

那么上面U盘与内存条情况,谁是接口?谁是实现接口的类?如何实现了多态?

内存条:内存条规范如ddr3标准是接口。内存条是实现接口的类,每一根内存

条都在具体地实现这个接口。在电脑类使用这个接口(实现接口)进行

了多态,它只管统一的插口、电压、读写,至于什么样的具体品牌、

大小等的内存条(动态子类)无关,随便来个内存条都可以使用。

U盘:U盘规范标准如USB2.0是接口,U盘是实现接口的类,不同的U盘内部用不

同的实现。电脑类统一的插口来适应不同品牌、大小等以便多态,只要是。

U盘就可以接入使用。

总结:

接口光说不做(就是规范,就是标准,就是文档,抽象的)

多态:统一的接口,适应不同的优盘。

2、接口存在的意义:多态。

多态的意义: 程序可扩展性。最终->节省成本,提高效率。

接口解决了类的多继承的问题

接口解决了类继承以后体积庞大的问题,

接口之间可以实现多继承

先从语法角度看一下接口,与抽象类类似。

3、定义接口

interface,一般以I开头命名,以示这是一个接口。

以able结尾,以示一种能力、方法。

接口里面只能包含方法。接口中可以有属性、方法、索引器等 (其实都是方法) ,

但不能有字段。

属性也是方法(get,set)

索引器(名叫Item的属性,也是方法)

事件也是属性,也是一个方法。

public interface IFlyable{void SayHi();//不能有修饰符和实现体string Name { get; set; }//只能写能"自动属性"样式//string ID//错误属性,不以有实现体//{// get { }// set { }//}}

注意:

属性只能写成“自动属性”样式,它不表示自动属性,只表示这是一个未实

现的属性

同样索引器:也只能写成简写方法,不能有实现体。

public interface IFlyable{void SayHi();//不能有修饰符和实现体string this[int index] { get; set; }//正确,索引器简写//string this[int index]//错误索引器。不能有实现体//{// get { }// set { }//}}

接口中的成员不能显式有访问修饰符(默认隐式公开public)

接口中的成员必须不能有实现,接口不能实例化。

(它是规范、标准,类似抽象类不能有实现)

接口中的所有成员"必须"被子类中全部实现。

除非子类是抽象类,把接口中的成员标记为抽象的。

4、接口的关键处:

同一段代码,只要能赋值到接口,那么接口就能调用它们。实现多态。

下面f.fly()一直不变,但被赋值的"子类"变化,结果也就不一样了,所以此句关键。

internal class Program{private static void Main(string[] args){IFlyable f = new Bird();f.Fly(); //关键,同一代码不同情况不同结果,多态。f = new Plane();f.Fly();Console.ReadKey();}}public interface IFlyable{void Fly();}public class Bird : IFlyable{public void Fly(){Console.WriteLine("鸟会飞"); ;}}public class Plane : IFlyable{public void Fly(){Console.WriteLine("飞机会飞");}}

5、抽象类与接口的区别

既然能用抽象类实现多态,为什么还要用接口来实现多态呢?两者相似度很高。

1)通过父类来多态时,必须继承父类。由于单根性,只能继承一个父类,如果有多

个“父类”需要继承时,就没有办法继承了。但如果通过接口,接口可以多继承,

可以随意实现n个接口继承,就突破了单根性的局限。

2)突破“面向对象”概念的限制。例如:飞的能力,鸟会飞,飞机会飞,风筝会飞,

它们不属于同一范畴的类,用共同的父类来强行归类很勉强。又如鱼会游,船

会游,船和鱼很难归为一类,很难找到共同父类等等。但是,如果用一种能力,

能容易就附着到别的类中,不必强行归类,不影响面向编程概念,逻辑又清晰。

简单地说:

接口可以“实现”多继承,多实现;

解决了不同类之间的多态问题。

上面两个类是做不到的、

两者解决的目的(多态)是一样的,但两者概念和实现过程不同。

抽象类的验证是通过类is a来验证。(鱼是动物,动物是父类)

接口的验证是通过can do来验证。 (鱼能游泳,游泳是接口)

接口可以实现“多继承”(接口一般称多实现,而不称多继承)

一个类只能继承一个父类,但可以实现多个接口。

子类继承抽象类,实现接口

三、案例分析

1、鸟-麻雀sparrow,鸵鸟ostrich,企鹅penguin,鹦鹉parrot,鸟能飞,鸵鸟,企鹅不能飞...

你怎么办?

分析:都继承了一个类:鸟,有些能飞,有些不能飞。飞是一种能力。在继承的同时

加入接口(能力)

internal class Program{private static void Main(string[] args){IFlyable f = new Sparrow();f.Fly();//f=new Penguin();//没有这个接口,所以写法是错误的Console.ReadKey();}}public interface IFlyable{void Fly();}public class Bird{public void Bark(){Console.WriteLine("鸟会叫");}}public class Sparrow : Bird, IFlyable{public void Fly(){Console.WriteLine("麻雀能飞");}}public class Ostrich : Bird{}public class Penguin : Bird{}public class Parrot : Bird, IFlyable{public void Fly(){Console.WriteLine("鹦鹉能飞");}}

注意:

继承的类必须写在第一个,后面跟接口,用逗号间隔各接口。

鹦鹉会说话可以写成接口,但这里无须说话成多态,故无须接口可写在鹦鹉类中。

因此,是否写接口,取决于是否要多态,多态则写成接口,否则直接写在本类中。

2、从学生,老师,校长类中抽象出人的类,学生和老师都有收作业的方法,但是校长不

会收作业

internal class Program{private static void Main(string[] args){ICollectable c = new Student();c.Collect();c = new Teacher();c.Collect();//c = new Master();//错误,无此接口Console.ReadKey();}}public interface ICollectable{void Collect();}public class Person{public string Name { get; set; }}public class Student : Person, ICollectable{public void Collect(){Console.WriteLine("学生收作业");}}public class Teacher : Person, ICollectable{public void Collect(){Console.WriteLine("老师收作业");}}public class Master : Person{}

3、海关登记:中国人,美国人,德国人等进行登记,这可以提炼出一个共同的人类来进行

登记。这个登记方法可以用一个方法,接收一个父类人类来写这个方法。但是,如果

是汽车,就相异于人类,如果是化学物品,也相异于人类,如果强制把它们写成继承

自人类,那么这个化学物品的年龄是多少?身份证是多少?身高是多少?无论逻辑还

是语意上不通。

因此,此时应把他们的登记信息作为一个接口。这样在共享的登记方法时,只需

要这个共同的接口参数即可,每个东西实现每个东西的信息,从而多态。

internal class Program{private static void Main(string[] args){IDengJiInfoable dj = new Chinese();DengJi(dj);DengJi(new Car());DengJi(new American());Console.ReadKey();}public static void DengJi(IDengJiInfoable dengJi){dengJi.Show();}}public interface IDengJiInfoable{void Show();}//public abstract class Person//不必用抽象父类,全部用接口//{// public string Name { get; set; }// public abstract void Show();//}public class Chinese : IDengJiInfoable{public void Show(){Console.WriteLine("中国人");}}public class American : IDengJiInfoable{public void Show(){Console.WriteLine("美国人");}}public class German : IDengJiInfoable{public void Show(){Console.WriteLine("德国人");}}public class Car : IDengJiInfoable{public void Show(){Console.WriteLine("轿车");}}

提示:反复说接口,并不是练接口怎么写。而是怎么从问题中找出接口、抽象类。

以及最终怎么实现多态。

技巧:

vs中上面代码行号的左侧有一个蓝色的小图标,同时附加了一个小箭头。

鼠标指向它,会提示“已继承...”,说明在接口中可以用“继承”这个用语。

右击蓝色图标,如果是接口,则会显示具体的哪些类实现了接口/成员。如果

是类则显示实现自哪个接口/成员。

4、橡皮rubber鸭子、木wood鸭子、真实的鸭子realduck。三个鸭子都会游泳,而橡皮鸭

子和真实的鸭子都会叫,只是叫声不一样。橡皮鸭子“唧唧”叫,真实地鸭子“嘎嘎”

叫,木鸭子不会叫.把抽象类变成接口。

internal class Program{private static void Main(string[] args){IBarkable[] b = new IBarkable[] { new RubberDuck(), new RealDuck() };b[0].Bark();b[1].Bark();Console.ReadKey();}}public interface IBarkable{void Bark();}public class Duck{public void Swim(){Console.WriteLine("会游泳");}}public class RubberDuck : Duck, IBarkable{public void Bark(){Console.WriteLine("橡皮鸭子唧唧叫...");}}public class WoodDuck : Duck{}public class RealDuck : Duck, IBarkable{public void Bark(){Console.WriteLine("真实鸭子嘎嘎叫...");}}

注意:

只有用到了多态,我们才写出对应的接口,否则没有必要写接口。

另外,接口的多态不能用虚方法或抽象类的多态。

各自的方法用各自的多态,这里接口所以用IBarkable接口类多态。

如果是抽象类,那就应用写成abstrack的抽象类来多态。

四、显式实现接口

1、为什么要显式实现接口?

方法重名后的解决办法。

假定一个类实现了两个接口,但每个接口都有一个Fly()的方法,那么方法名重名后

怎么实现,到底实现的是哪一个的接口Fly()方法呢?

internal class Program{private static void Main(string[] args){IFlyable1 f1 = new Student();f1.Fly();IFlyable2 f2 = new Student();f2.Fly(); //原意调用f2的,结果显示的是f1的Console.WriteLine("---------");Teacher t = new Teacher();t.Fly(); //正常的接口,1。显式无法调用为privateIFlyable1 t1 = new Teacher();t1.Fly(); //正常的接口,1IFlyable2 t2 = new Teacher();t2.Fly(); //显式的接口,不再是正常的接口,2.Console.WriteLine("---------");Master m = new Master();m.Fly();IFlyable1 m1 = new Master();//用两个不同的接口去访问m1.Fly();IFlyable2 m2 = new Master();m2.Fly();Console.ReadKey();}}public interface IFlyable1{void Fly();}internal interface IFlyable2{void Fly();}internal class Student : IFlyable1, IFlyable2{public void Fly(){Console.WriteLine("实现1中的Fly()");}}internal class Teacher : IFlyable1, IFlyable2{public void Fly(){Console.WriteLine("实现1中的Fly()");}void IFlyable2.Fly()//明确告诉用的是IFlyable2中的{//不能有访问修饰符Console.WriteLine("实现2中的Fly()");}}internal class Master : IFlyable1, IFlyable2{public void Fly() //只能Master对象调用{Console.WriteLine("正常接口成员Fly()");}void IFlyable1.Fly() //只能接口IFlayabel1中对象调用{Console.WriteLine("1中成员Fly()");}void IFlyable2.Fly() //只能接口IFlayabel2中对象调用{Console.WriteLine("2中成员fly()");}}

提示:

vs中,对于接口,智能提示时,它的小图标是两个圆圈(一大一小)用一根

线连接在一起,表示接口。这个小图标下面若有一个白色的心形形状,表示访问修饰

的是程序集内部(internal),若把接口改为public,则这个白色形状消失。

2、显式实现接口后,只能通过接口来调用。

不能通过类对象本身来调用(显式实现的接口,查看IL是private,防止通过类来调用)

尽管类中是private,无法通过这个类的对象来调用。但是,它是通过接口的对象来

调用,而接口的方法默认隐式公开public,所以是能够访问的。

t2.Fly();//上例中通过t2接口来访问

对于Master类中,public void Fly()能过本类的对象来访问。

而后面的两个显示的接口方法,只能仅限对应的接口对象来进行访问。

3、为什么要有显式实现接口?

可以解决重名方法的问题。

4、什么是显式实现接口?

实现接口中的方法时用:

接口名.方法名(),并且没有访问修饰符,默认为private,只能通过接口来调用。

5、显式实现接口后怎么调用?

只能通过接口变量来调用,因为显式实现接口在类中默认为private.

只有接口中默认public.

疑惑:

在输入要实现接口的类名后,按Alt+Shift+F10,会提示“实现接口”,这样创建

类时会直接把实现的接口一起写出。

但是,这个快捷键并不会提示“显式实现接口”。无论怎么折腾,就是不出现显式

实现接口。猜测原因有:

1)显式实现接口的情况比较少,所以不需要这样的快捷键。

2)一般不推荐使用显式接口实现???

6、接口小结

接口是一种规范。为了多态。

接口不能被实例化。

接口中的成员不能加“访问修饰符”。默认为public,不能修改。

接口的成员不能实现->光说不做。

接口不能用字段,只能是方法:方法,属性,索引器,事件。

不能有委托,委托就是字段了。

因为接口是抽象的、规范的,不能有具体或实现,而字段是具体实现。

接口与接口之间可以继承,并且多继承。

实现接口的子类必须实现该接口的全部成员。

一个类可同时继承一个类并实现多个接口。此时类必须写在最前,因为类为单继承。

当一个抽象类实现接口的时候,若不想把接口的成员实现,可以把该成员实现

为abstract。(抽象类也能实现接口,用abstract标记)

显式实现的接口,只能通过接口变量来调用(因为显示实现接口的成员这private)

下面说明接口多继承:

internal class Program{private static void Main(string[] args){ISuperMan s = new SharpMan();s.Fly();SharpMan s1 = new SharpMan();s1.Fly();Console.ReadKey();}}internal interface IF1{void Swim();}internal interface IF2{void Fly();}internal interface IF3{void Jump();}internal interface ISuperMan : IF1, IF2, IF3 //超人继承前面三个接口{void Fly(string s);}internal class SharpMan : ISuperMan{public void Fly(){Console.WriteLine("能飞");}public void Fly(string s){Console.WriteLine("字串能飞");}public void Jump(){Console.WriteLine("能跳");}public void Swim(){Console.WriteLine("能游");}}

说明:

接口ISuperMan多继承前面三个接口。加上本接口,共有四个方法,因此后面类

SharpMan必须全部实现接口的四个方法。(fly实现了重载)

同时还说明了SharpMan类的fly,jump,swim可以由类来访问也可由接口来访问。

五、使用接口的建议

1、面向抽象编程,使用抽象(父类,抽象类,接口)不使用具体。

简言之:向上转型。(尽量向上、向父类、向抽象方向进行考虑)

2、在编程时:

接口->抽象类->父类->具体类(接口最优先,抽象类其次,具体类最后)

(在定义方法参数、返回值、声明变量的时候,能用抽象就不要用具体。)

能使用接口就不用抽象类,能使用抽象类就不用类,能用父类就不用子类。

避免定义“体积庞大的接口”,“多功能接口”,会造成“接口污染”。

只把相关联的一组成员定义到一个接口中(尽量在接口中少定义成员)。

单一职责原则:

定义多个职责单一的接口(小接口)(组合使用)。

印刷术与活字印刷术:古代最开始印刷是把整个版本刻成文字,若这一版有一个

文字出错,则整个版本报废。活字印刷就是把整个版本分成一个一个单一的

文字,每个文字可以取下、可以安装上。这样如果有一个错误,不需要整个

版本报废,只需要把这个错误字取下,用正确的字代替即可。

接口也一样,不要大而全。按照单一职责原则,分成多个单一职责的小接口,调

试与维护方便,编写逻辑清晰。

3、如果父类已经实现了接口,子类是否还实现接口?(书籍《改善程序的50个建议》)

不必了,子类将继承父类接口的实现。所以子类不必再加上接口再实现。

但是,微软类库文档中,子类继承已实现接口的父类,往往会再一次加上接口,但

是它并不在子类内再写一次。这是因为转型效率原因,子类接口会直接调用实现,如果

子类不写接口,在内部它会再较型到父类,再调用父类实现的接口方法,多了一次转换。

internal class Program{private static void Main(string[] args){IFlyable s = new Student();//子类用news.Fly();//1Console.ReadKey();}}internal interface IFlyable{void Fly();}internal class Person : IFlyable{public string Name { get; set; }public void Fly() // 2{Console.WriteLine("父类接口飞...");}}internal class Student : Person, IFlyable //3{//public new void Fly()// 4 正确,有意隐藏父类飞//{//父类接口被隐藏。此Fly可由Student及其对应接口访问,但不是父类继承来的接口// Console.WriteLine("子类接口飞...");// base.Fly();// 6 此处是父类接口//}//public override void Fly()// 5 错误,override只能与abstract,virtual配对//{// Console.WriteLine("错误飞...");//}}

说明:

2处父类Person实现了接口。继承到子类Student时,3处无须再写IFlyable,子

类也将继承由父类实现的接口方法Fly()。但为了效率,3处可以直接调用方法Fly(),

不必内部再转Person再调用Fly(),故3处这里再次写上IFlyable。

5处是错误的,原意要重写由父类过来的接口实现Fly(),但override不能直接单

独使用。

4处去注释后是正确的。它隐藏父类过来的Fly,会有警告,正式的写法是前面

应加new起到显式隐藏父类同名方法。这样子类类外无再这样调用父类同名方法,但

子类Student类内仍然可以用6处的base.Fly()进行调用父类Fly().

1处运行时,会显示父类接口飞。如果把4处的注释去掉,运行时,4处将重写父

类Fly,而显示子类接口飞。

注意:

3处的接口IFlyable可以去掉,1处的去处结果不会发生变化。3处加上这个接口

一是为了效率。二是为了显式多态,父类群诸如Person等可以通过IFlyable多态,子

类群诸如Student等也可以多态,但查看时需要从Student向上再去看父类,不方便,

同时也不灵活。在子类中直接写上接口IFlyable而不必实现,一眼就可以看出可实现

多态,而且还可以直接在子类重写,实现同等级父类,与诸多子类的多态。

上面1处用子类Student多态时,显示是父类接口内容。但如果4处重写了,用子类

Student多态时,就显示的是子类接口内容。

提示:

父类中用了接口,就必须实现,不能等到子类进行实现。上面中Person必须实现,

否则出错。除非父类是一个抽象类,把这个接口在抽象类中写成抽象方法。

4、如果接口有AB两个方法,父类实现A方法,子类实现B方法,是否可以?

不可以!只要实现接口,就必须把这个接口全部实现。必须在父类中把AB两个方

法全部实现,否则出错。除非父类是一个抽象类。

抽象类中可以实现接口也可不实现接口,但不实现时须注明抽象方法。

internal class Program{private static void Main(string[] args){IFlyable s = new Student();s.Fly();s.Swim();Console.ReadKey();}}internal interface IFlyable{void Fly();void Swim();}internal abstract class Person : IFlyable{public string Name { get; set; }public void Fly()//实现接口中的一个方法{Console.WriteLine("父类接口飞...");}public abstract void Swim();//接口方法,不实现,须写成抽象方法}internal class Student : Person, IFlyable{public override void Swim()//子类中重写,并实现接口方法{Console.WriteLine("子类游...");}}

5、同一个接口能在一个类中写两次么?如下面:

Internal Class Student:IFlyable,IFlyable

不能!编译器会报错:已经实现接口。

六、复习

(一)抽象类复习、简单工厂设计模式复习

1、抽象类:

不能被实例化,需要被继承。多态

子类必须重写父类中的所有的抽象成员,除非: 子类也是一个抽象类

抽象成员在父类中不能有任何实现。

抽象类中可以有实例成员。

抽象成员的访问修饰符不能是private

抽象成员只能写在抽象类中。

2、作业: 通过案例笔记本电脑的选择。笔记本电脑父类NoteBook、不同品牌的笔记本产

品。(继承+简单工厂)

internal class Program{private static void Main(string[] args){string s = "联想";NoteBook b = GetCumputer(s);b.Show();Console.ReadKey();}private static NoteBook GetCumputer(string s){switch (s){case "联想":return new Lenovo();case "三星":return new Suming();default:return new Dell();}}}internal abstract class NoteBook{public abstract void Show();}internal class Dell : NoteBook{public override void Show(){Console.WriteLine("戴尔电脑");}}internal class Suming : NoteBook{public override void Show(){Console.WriteLine("三星电脑");}}internal class Lenovo : NoteBook{public override void Show(){Console.WriteLine("联想电脑");}}

(二)接口复习

定义接口的语法(interface)

接口中只能包含方法、属性、索引器、事件。不能包含字段。

见备注1 (貌似事件像一个字段?其实是两个方法。reflector查看源码)

接口中的成员不能有任何的实现

(真正的“光说不做”。思考这样做的意义。联想抽象类中的抽象方法。)

接口中的成员不能写访问修饰符。

使用接口的语法

一个类可以实现多个接口。实现接口的类,必须把接口中的所有成员都实现。

子类实现接口中的成员时,不能修改成员的访问修饰符、参数列表、方法名等。

(与方法重写一样)

七、面试题

提示:除了回答正确外,表达是否清晰,语气是否紧张也是加分项。

回答是对本类问题尽量多说,但外展尽量少说(会无穷追问无关项,累)。

把面试当作一个平等的交流,不是一问一答。

1.如何使用virtual和override ?

Person per = new Student();

per.SayHI();//调用的子类重写的SayHi方法(语法、应用-多态)

答:virtual与override是虚方法中的配对使用。虚方法是父类中必须实现,子类

中可以不实现。父类中方法用虚方法时加virtual,对应的子类同名方法重写时用

override。有virtual时不一定有override,有overide时父类必须有abstract或

virtual。虚方法主要用于多态。

2.如何使用abstract和override?

答:abstract与override是抽象方法配对使用。父类中的方法前加abstract时,父

类必须也是abstract(抽象方法只能存于抽象类中),同时抽象方法在父类中不允许

实现,只能在子类(除非子类又是抽象类)中实现。有abstract必须有override,

有override则必须有abstract或virtual出现。抽象方法主要用于多态,比虚方法更

常见。

3.“方法重载overload”、“方法重写override"、"隐藏new"是同个概念吗?

答:重载是同名但函数签名不同的多个方法共存时,依据不同签名选择对应方法,

它相当于编译器多态。重写是父类同名方法在子类中改写的情况,override只能是

有virtual或abstract时进行重写父类同名方法。重写override父类必须有virtual

或abstract,主要用于多态。隐藏new,用于子类中要隐藏父类类继承过来的同名

方法,使用new后,父类方法隐藏,且不再继续向下继承,意同sealed.

4.抽象类和接口的区别?

答:抽象类适用于同一系列,并且有需要继承的成员。有清晰的群类关系。

接口适用于不同系列的类具有相同的动作(行为、动作、方法)。

对于不是相同的系列,但具有相同的行为,这个就考虑使用接口。

接口解决了类不能多继承问题。

八、类型转换

(一)类型转换:CAST

1、隐式类型转换

数字类由小到大可以直接赋值,称之为隐式转换。如:byte->int->float->double

byte b = 23;int n = b;float f = n;double d = f;Console.WriteLine(d);

注意下面:

char c = '王';//c='z';int n = c;Console.WriteLine(c);

char在内存显示的数字ASC码,在显示时转为字符。它是2个字节,int是4个字节,

因此int可以隐式容纳char.

Console.WriteLine(sizeof(byte));//1Console.WriteLine(sizeof(bool));//1Console.WriteLine(sizeof(char));//2Console.WriteLine(sizeof(short));//2Console.WriteLine(sizeof(long));// 8Console.WriteLine(sizeof(int));//4Console.WriteLine(sizeof(float));//4Console.WriteLine(sizeof(double));//8Console.WriteLine(sizeof(decimal));//16

注意:

sizeof():确定给定类型的内存需求(占用的字节数)。

由于sizeof参数是一个非托管类型的名称,因此需要在不安全的上下文中运行。

但微软确认下面不需要“不安全”:

byte,short,int,long,char,float,double,decimal,bool

包括枚举与结构,都不需要在不安全的上下文运行,但指针需要:

internal enum Person{name1,name2,name3,name4}private static void Main(string[] args){unsafe{//指针只能在unsafe括号内运行,否则出错。Console.WriteLine(sizeof(byte*));//4Console.WriteLine(sizeof(int*));//4}Console.WriteLine(sizeof(Person));//4Console.ReadKey();}

上述代码需要设置允许不安全代码:右击当前项目->点击属性->点击左侧生

成->勾选右侧"允许不安全代码"。(vs)

2、显示类型转换

由大到小转换可能有误,需要显式转换以确认转换。double->float->int->byte

double d = 23;float f = (float)d;int n = (int)f;byte b = (byte)n;Console.WriteLine(b);

3、引用类型

学生类Student继承于父类Person.

把学生转换为人是隐式转换,把人转换为学生则是显式转换(强制转换)

Student s = new Student();Person p =s;//隐式类型转换Student stu =(Student)p;//显式类型转换

obj as 类型 //成功返回类型,失败返回null.

只有在内存存储上存在交集的类型之间才能进行隐式转换。

不能用Cast转换string->int,只能用Convert。

Convert.Tolnt32/Convert.ToString

4、类型转换Cast是在内存级别上的转换。内存中的数据没有变化,只是观看的视角不

同而已。

5、什么情况下会发生隐式类型转换?

1)把子类类型赋值给父类类型的时候,会发生隐式类型转换。

2)将占用字节数小的数据类型,赋值给占用字节数大的数据类型,

可以发生隐式类型转换(前提是这两种数据类型兼容,在内存的同一个区域)

注意:

Math.Round();/四舍五入Convert.ToInt32();//四舍五入

6、结构类型占内存多大?

结构类型不是微软预定义的类型,只能在不安全代码中运行。

它的大小由内部成员确定,并根据字节对齐或优化的情况进行计算。

特别的:string的大小不固定,sizeof表示它的指针大小。

private struct Person{private string Name ;//1private int age;//2private string ID;//3private char BloodType;//4}private static void Main(string[] args){unsafe{//结构大小只能在不安全上运行Console.WriteLine(sizeof(Person));//16Console.WriteLine(sizeof(String));//4 没有预定义不能在括号外运行。}Console.ReadKey();}

说明:

sizeof(String)是4个字节,只能在unsafe中运行。

结构按最大字节4的成员进行对齐,每个都是4字节,故为4*4=16字节。

注释掉1处,结果12;

注释掉1、2处,结果8; 都按最大4字节对齐

注释掉1、2、3处,结果2; 只有一个成员无须再对齐,就是char本身2个字节。

注释掉1、2、3、4处,结果1.

如果把age的int改为double,结果为20。说明按4*3+8=20进行对齐和优化。

(二)类型转换:Convert

1、Convert考虑数据意义的转换。Convert是一个加工、改造的过程。

若要进行其它类型的转换可以使用Convert.Tolnt32,Convert.ToString等。

Convert可以把object类型转换为其它类型

string str = null;int num;num = Convert.ToInt32(str);Console.Write(num + "\r\n");//num = Int32.Parse(str);//不能为null,否则异常//Console.Write(num + "\r\n");Int32.TryParse(str, out num);Console.Write(num + "\r\n");

2、(int),Int32.Parse(),Int32.TryParse(),Convert.ToInt32()的区别

3、将字符串转换成“数值类型”(int、foat、double)

int.Parse(string str);int.TryParse(string str,out int n);//很常用,推荐。double.Parse(string str);double.TryParse(string str,out double d)......

Parse()转换失败报异常,

TryParse()转换失败不报异常。

4、再说as与直接类型转换: (*)

如果用is a来进行类型判断后,再进行类型转换:

if(p is Student)

{

Student stu=(Student)p;

}

那么,CLR会进行两次类型检查:

if(检查一次)

{

//再检查一次

}

所这种情况效率比较低,推荐直接用as,成功返回对象,失败返回null。

Student stu=p as student;

//推荐,效率高于第一种,如果转换失败返回null,而不会报异常。

5、类型提取

GetType():获取当前实例的 Type。 GetType()不允许重写。

BaseType:获取当前 Type 直接从中继承的类型

所有数组类型继承于Array,所有类型继承于Object。

Object没有基类(父类),再次提取BaseType时为null.

string[] s = new string[] { "李世明", "雍正", "孙中山" };Console.WriteLine(s.GetType().ToString()); //System.String[]Console.WriteLine(s.GetType().BaseType.ToString());//System.ArrayConsole.WriteLine(s.GetType().BaseType.BaseType.ToString());//System.ObjectConsole.WriteLine(s.GetType().BaseType.BaseType.BaseType.ToString()); //对象为null,异常

技巧:

一般中断或异常后,鼠标指向某变量或对象或数组等,会有值的提示。

但是如果用点号取成员很长时,不会有提示,不知道是哪一级成员出问题。

可以使用快速监视或添加监视来看:

在指定可能问题处中断,或异常时,在多级成员处,先选择短的成员,例如

s.GetType(),然后右击选择快速监视(Ctrl+F9),或者添加监视,这样就可查看

值的情况。以此类推,选择s.GetType().BaseType再监视,如此,直到查看到问

题所在。

6、将任意类型转换成字符串:ToString()

7、技巧:

当遇到类型转换的时候不知道该怎么转,可以去Convert中找找

九、异常处理

1、什么是异常?

程序运行时发生的错误。

错误的出现并不总是程序员人的原因,有时应用程序会因为最终用户或运行代码

的环境改变而发生错误。比如:

1)连接数据库时数据库服务器停电了,

2)操作文件时文件没了、权限不足等,

3)计算器用户输入的被除数是0;

4)使用对象时对象为null,等等。

.net为我们把“发现错误(try)”的代码与“处理错误(catch)”的代码分离开来。

2、异常处理的一般代码模式:

try{//1可能发生异常的代码}catch (Exception){//2对异常的处理}finally{//3无论是否发生异常、是否捕获异常都会执行的代码}

try块: 可能出问题的代码。当遇到异常时,后续代码不执行。

catch块: 对异常的处理。记录日志(log4net),继续向上抛出等操作。

(只有发生了异常,才会执行。)

finally块: 代码清理、资源释放等。无论是否发生异常都会执行。

重要:finally可以省略。catch块可能有多个,以便捕获不同异常。

注意:

除非必须用try...catch...,一般尽量不要用。

因为try...catch会监视try执行的代码,影响程序执行的效率。

技巧

vs中,由于当前解决方案有多个项目,如何一次性关闭它们?

右击(主IDE界面中)代码窗上面的标签,选择“关闭所有选项卡”,或者选择

“除此之外全部关闭”,可快速关闭其它项目或全部项目的标签。

也可以在菜单上的“窗口”菜单里进行操作,只是有点不习惯。

3、案例:

try{int x = 5;int y = 0;int z = x / y;}catch{Console.WriteLine("除数不能为0");}

程序运行运行时出错,后续的内容无法运行程序一旦有一个功能发生异常,整

个程序崩溃其它功能也无法正常运行

技巧:

vs中,如何快速添加try语句块?

1)选中可能出问题的语句块,按Ctrl+k,Ctrl+s后,在弹出小窗口中选择try。

2)选中可能出问题的语句块,右击->片段->外侧代码->选择try

提示:

catch(Exception ex)后面的参数可加可不加。

加上参数后,可以看出问题的相关信息。例如:

internal class Program{private static void Main(string[] args){Person p = new Person();p = null; //p被释放,不再指向任何对象try{p.Name = "Test";//1Console.WriteLine(p.Name);}catch (Exception ex){Console.WriteLine(ex.Message);}Console.ReadKey();}}internal class Person{public string Name { get; set; }}

注意:

尽管上面用了try,但并不能捕获。仍然会在1处抛出异常。

原因:Debug与Release两个模式中的try-catch运行情况是不同的。Debug模式

并不能捕捉此类异常。(Debug调试模式,Release发布模式)

解决方法:运行上面代码,抛出异常后,在异常小窗口中选择最下面“打开异

常设置”,在“异常设置”小窗体中的右上输入null进行搜索,去掉System.NullRefe

renceException的勾选即可。

再次运行,就会捕捉到,将异常信息ex.Message显示:

未将对象引用设置到对象实例。

同理前一个案例除以0的,若带异常参数信息,则显示:

尝试除以零。

4、异常处理代码的其他几种形式:

1)一个catch,捕获所有异常:不带参

private static void Main(string[] args){int x = 5, y = 0;try{int z = x / y;}catch //无参,捕获所有异常{Console.WriteLine("发生异常了");}finally{Console.WriteLine("finally块");}Console.ReadKey();}

上面可捕获所有异常,但无法获取异常信息。

2)一个catch,捕获所有异常:带参(可获取异常信息)

private static void Main(string[] args){int x = 5, y = 0;try{int z = x / y;}catch (Exception e)//带参,可获取异常相关信息{Console.WriteLine(e.Message);//异常信息Console.WriteLine(e.Source);//异常源(程序或对象)Console.WriteLine(e.StackTrace);//栈上跟踪信息Console.WriteLine(e.TargetSite);//引发异常所在的方法(地点)}finally{Console.WriteLine("finally块");}Console.ReadKey();}

3)多个catch块单独针对可能异常捕获,只要有一个捕获,其余catch将不再捕获。

一般最后写一个总的捕获,这样前面捕获不到时,由最后的总的捕获处理。

private static void Main(string[] args){int x = 5, y = 0;try{int z = x / y;}catch (NullReferenceException e)//1空指针异常{Console.WriteLine("空指针异常,{0}", e.Message);}catch (ArgumentException e)//2参数异常{Console.WriteLine($"参数异常,{e.StackTrace}");}catch (DivideByZeroException e)//3除数为零异常{Console.WriteLine($"除数为零,{e.StackTrace}");}catch (Exception e)//4其余异常,不能写在最前面{Console.WriteLine($"异常信息{e.Message}");}Console.ReadKey();}

注意:

怎么知道分别出现哪些异常种类,以便分别写出catch?

1)靠经验与推测,每次出现异常时,看看异常小窗体中的信息,有印象

2)注释掉上面1到4的信息,运行则报出异常的种类。

3)可以在.Net Reflector中搜索System.Excepton,查看异常信息.

为什么要分别catch处理?

主要是编程上的逻辑清晰,功能分类。也可以不分别处理,直接在一个总

的异常中用switch或if进行判断处理。

4)没有catch块,只有try-finally。(catch与finally可以两者现,也可现其一)

由于没有catch所以不会捕获,同平时一样会抛出异常。

不同的是,有了一个finally可以最终处理一下。

5、强调

1)既然finally最后都是执行,那直接把finally去掉行不行?

不行。finally是无论异常否,都必须执行。哪怕try或catch中有return,

这个语句块也必须执行。

另外,finally后面代码在异常后是不能执行的,那么一些无论异常否都

得处理的后尾问题,就可以放在finally中进行扫尾工作。比如:catch没有

捕获到,那么finally后面代码是不能执行的,程序可能崩溃,而finally必

须执行就可以在里面添加一些处理代码。

或者catch块中又有异常,finally就是最终应对方式。

由此可见finally并不是可有可无的。

因此使用finally时应注意:

如果希望代码无论如何都要被执行,则一定要将代码放在finally块中。

1)当catch有无法捕获到的异常时,程序崩溃,但在程序崩溃前会执行

finally中的代码,而finally块后的代码则由于程序崩溃了无法执

行.

2)如果在catch块中又引发了异常,则finally块中的代码也会在继续

引发异常之前执行,但是finally块后的代码则不会.

3)当catch块中有return语句时,finally块中的代码会在return之前

执行,但finally块后的代码不会执行。

4)finally中不能用return

private static void Main(string[] args){try{string s = null;ProcessString(s);}catch (ArgumentNullException e){Console.WriteLine("{0}Fist exception caught.", e);return;}catch (Exception e){Console.WriteLine("{0}Second exception caught.", e);return;}finally{Console.WriteLine("必须执行");//return;//错误Console.ReadKey();}Console.WriteLine("末尾");Console.ReadKey();}private static void ProcessString(string s){if (s == null){throw new ArgumentNullException();}}

为什么finally里面不能有return?

因为finally块无论如何里面代码都必须执行。如果里面有了return,

那么有可能直接返回,有些代码就执行不了。所以不能有return.

上面代码还可以看出,方法体写在try中,对整个方法也会捕捉。

2)throw

除了电脑抛出异常,也可以人为手工抛出异常。

string s = "k";if (s == "k"){throw new Exception("异常");}

Exception是所有异常的基类。new Exception("")创建一个新异常对象.

程序一般不人为抛出异常,因为它浪费资源。上述代码,一般判断后直接

给出提示或者处理办法。

有时直接使用throw; 后面不加参数直接分号。

它仅限于在catch块中使用。表示将当前的异常继续向上抛出。

类似低级人员逐级上报给上一级的领导,有一个throw;就报上报一次。

private static void Main(string[] args){try{Console.WriteLine("9999");M1();Console.WriteLine("aaaa");}catch{Console.WriteLine("bbbb");throw;//3}Console.ReadKey();}private static void M1(){try{Console.WriteLine("1111");M2();Console.WriteLine("2222");}catch (Exception){Console.WriteLine("8888");throw;//2}}private static void M2(){int x = 5, y = 0;try{Console.WriteLine("3333");int n = x / y; // 4Console.WriteLine("4444");}catch{Console.WriteLine("5555");//n = 3;// 3 try块中n是局部变量,不能在catch块中使用throw;//1Console.WriteLine("6666");//throw后面的代码不再执行}finally{Console.WriteLine("7777");}Console.WriteLine("xxxx");//5}

注意:

1)各块中局部变量不能跨越使用。例如上面4处的变量n,不能在3处使用.

2)throw;仅在catch中使用,且逐级上报。1处的异常来自于4处的同一个

异常,4处把异常转交给1处,1处throw向上抛给上级M1报告,在M1

中捕获后,在2处继续向上级Main()上报,主函数捕获后,在catch中

继续上报(谁呢?但程序这里没出错),到此时,这个异常就抛出来

了,直接由这个“报告”查找到异常的原产地1处。

(这里本身的n=x/y异常已经转交给了1处的throw)

所以执行顺序9->1->3->5->7->8->b

3)throw是手工抛出,所以最终还是显示为同平常抛出异常一致。

如果注释掉3处,则相当于高层处理了这个异常“报告”,不再抛出。

如果注释掉3处和2处,同上。只是最后有a无b,因为没有异常了。

如果只注释掉2处,则3处异常不会抛出,相当于中级官员已经把上报的

异常“报告”处理了,所以顺序为9->1->3->5->7->8->a(无异常无b)

throw是抛异常,所以如果M1()与M2()的finally块的后面有代码,将不

执行。(例如5处的x不会显示)

3)异常信息

Exception 类主要属性: Message、StackTrace、InnerException (当前异常的实例)

扔出自己的异常。扔: throw,抓住: catch

建议:

通过逻辑判断(if-else)减少异常发生的可能性!

尽量避免使用“异常处理”。

在多级方法嵌套调用的时候,如果发生了异常,则会终止所有相关方法的调用,

并释放相关的资源

十、代码观察

1、下面try块中发生异常与不发生异常时的输出结果分别是什么?

private static void Main(string[] args){T1();Console.ReadKey();}private static void T1(){try{Console.WriteLine("1111");---引发异常代码开始---//int x = 10, y = 0;//Console.WriteLine(x / y);---引发异常代码结束---Console.WriteLine("2222");return;Console.WriteLine("3333");}catch (Exception){Console.WriteLine("4444");}finally{Console.WriteLine("5555");}}

1)不引发异常时:

1->2->5

因不异常catch不执行,到return时退出方法,3执行不到。

2)引发异常时(去掉注释):

1->4->5

因异常try块后续代码不执行,直接到4,最后必须finally里的5.

2、下面try块中发生异常与不发生异常时的输出结果以及方法的返回值是什么?

private static void Main(string[] args){int r = GetNumber();Console.WriteLine(r);Console.ReadKey();}private static int GetNumber(){try{int n1 = 10, n2 = 0;---引发异常代码---// int n3 = n1 / n2;---引发异常代码---return 100;}catch (Exception ex){Console.WriteLine("1111");return 200;}finally{Console.WriteLine("2222");}}

1)不异常时:

输出2,返回100.主程序r输出100

2)异常时(去掉注释):

输出1->2,返回200.主程序r输出200

3、下面try块中发生异常与不发生异常时fnally块中的代码是否被执行了? 该方法

的返回值又分别是多少?

private static void Main(string[] args){int n = M1();Console.WriteLine(n);//aConsole.ReadKey();}private static int M1(){int result = 100;try{result += 1;---引发异常代码---//int x = 10, y = 0;//Console.WriteLine(x / y);---引发异常代码---return result;}catch (Exception ex){result += 1;return result;}finally{result += 1;}}

1)不异常时:

返回result是101,主程序a处输出101.

原因:先生成执行文件,用.Net Reflector反编译查看,上面的M1方法:

可以看到在方法M1中另外生成了一个变量num2,专用于返回值。就类似

我们传参数到另一个方法时,会创建形参来保存传过来的实参。同样,在返

回时,会同样单独另外创建一个参数(num2)来保存返回值.

因此,不异常时,num2保存101后,尽管在finally里num++成102,但为

返回值创建的变量num2仍然是101,所以返回值仍然是101.

是不是只有try-catch才单独创建一个变量用于返回值呢?

不是!!只要方法有返回值,就会单独创建一个用于返回值的临时变化,

没有返回值的方法是不会创建的。下面测试一下:

为上面代码添加两个方法:

private static int T1(){int n = 100;n++;return n;}private static int T2(){return 1;}

再次生成反编译查看T1:

发现并没有另一个变量出现。切换到IL(中间语言)查看:

可以看一T1()方法出现了另一个变量num2,并且在返回之前加载了索引为1的变量

也就是num2。

同样的对于T2()在C#反编译时没有看到变量,但切换到IL可以看到:

T2()进去后就单独生成了一个变量num(索引为0),在返回之前加载索引为0的

变量即num。

这里面涉及IL操作比较艰深,有反汇编基础的可以了解一下。参考:

/cc299/p/14539782.html

结论:

进入任何有返回值的方法后,都会为返回值创建一个单独的临时变量,用它来

存储返回值。即:

临时变量=返回值;

return 临时变量;

若无返回值,这个变量不会单独创建(可自行试验)

注意:

平时无须关心有返回值时,单独创建的另一个变量。只有finally强制必须执

行时,前面try或catch有return时,才考虑返回值发生变化的情况。

2)有异常时(去掉注释):

返回值是102,a处输出为102.

由1)知道finally块中并不能影响try与catch块中的return的值,故为102.

4、下面当调用该方法时,返回的Person对象的Age属性在try块中发生异常与不发生

异常时输出结果分别是多少?

internal class Program{private static void Main(string[] args){Person p = GetPerson();Console.WriteLine(p.Age);Console.ReadKey();}private static Person GetPerson(){Person p = new Person();p.Age = 100;try{p.Age += 1;---引发异常代码---//int x = 10, y = 0;//Console.WriteLine(x / y);---引发异常代码---return p;}catch (Exception){p.Age++;return p;}finally{p.Age++;}}}internal class Person{public int Age { get; set; }}

1)不引发异常时:

102

原因:实际与上面一样,会为返回值创建一个单独的临时变量Person2:

仍然一样进行了赋值person2=person,finally也进行了person.Age++,但

是person2与person是引用类型,指向同一个对象,任何一个更改时,另一

个同样随之改变。所以person.Age++同样影响person2,再次加1,结果102.

2)引发异常时(去掉注释):

103

由于是引用类型,连续三次加1都会影响person,故为103.

十一、函数返回值(函数参数前的修饰符)

1、params 可变参数

1)无论有几个参数,必须出现在参数列表的最后。

2)可以为可变参数直接传递一个对应类型的数组

3)可变参数可以传递参数,甚至可以为null(无对象)。

也可以不传递参数,则形参为长度为0的数组。

private static void Main(string[] args){GetLength("aaaa", 1, 2, 3, 4);//aaaa-4GetLength("bbbb", null);//空对象 bbbbGetLength("cccc"); //有对象长度为0,cccc-0int[] n = { 1, 2, 3 };GetLength("dddd", n); //dddd-3//GetLength("eeee", 1, n);//错误 无法转换//GetLength("ffff", n, 1);//错误 无法转换Console.ReadKey();}private static void GetLength(string s, params int[] n){if (n != null){Console.WriteLine(s + "----" + n.Length);}else{Console.WriteLine(s);}}

2、ref 引用传递

仅仅是一个地址,,可以把值传递强制改为引用传递

3、out 让函数可以输出多个值

1.在方法中必须为out参数赋值(才能使用)

2.out参数的变量在传递之前不需要赋值,即使赋值了也不能在方法中使用。

(赋值没意义,甚至只须在参数中声明类型即可)

out参数如同布施乞丐一样,钱只能付出去,不能从乞丐碗中取出。

而且必须把钱布施出去。所以见到out就当做功德吧。

private static void Main(string[] args){int m = 200;int n, z;T1(out m);T1(out int y);//此处int y作用区与T1的作用区一样,所以最后可以输出y=101//T2(out n);//n传入前不必赋值//T3(out m);//T(ref int a);//不能象out一样在实参时声明//T(ref 222);//错误,因为要用变量别名,传地址,不能用常量//T(ref z);//错误,不能象out一样使用前不赋值z = 1;T(ref z);Console.WriteLine(z);//101Console.WriteLine(y);//101Console.ReadKey();}private static void T(ref int x){x = 100;x++;}private static void T1(out int x){x = 100;x++;}//private static void T2(out int x)//{//错误out必须带出,不能带入,此方法内x未同赋值,语法错误// Console.WriteLine(x);//错误// x++;//}//private static void T3(out int x)//错误。空方法也必须赋值x。必须布施//{//}

上面基本覆盖了out与ref的使用情况。

ref与out的区别:

ref:参数在传递之前必须赋值

在方法中可以不为ref参数赋值,可以直接使用

4、既然有了ref可以引用传递,为什么还要设置一个out来作为参数呢?

ref应用场景用于内部对外部的值进行改变,

out则是内部为外部变量赋值,out一般用在函数有多个返回值的场所。

这样要需要多个返回值时,可以考虑out

private static void Main(string[] args){int m = 1000;JianJin(ref m);KouKuan(ref m);Console.WriteLine(m);//获取年龄的同时,传回多个参数:姓名,身高int age = GetAge(out string name, out int height);Console.WriteLine(age + "---" + name + ":" + height);//int.TryParse中的outstring s = "abc";int result;bool b = int.TryParse(s, out result);if (b){Console.WriteLine("成功:" + result);}else{Console.WriteLine("失败:" + result);}Console.ReadKey();}private static int GetAge(out string n, out int h){n = "黄林";h = 180;return 1000;}private static void JianJin(ref int m){m += 300;}private static void KouKuan(ref int m){m -= 30;}

5、out参数方法中能否用params?

不能!

params主要的应对场景是传参前的多个不定数目的同类参数。针对传参前的变化。

而out跟传参前基本无关,只须声明甚至连赋值都省略了。重点是传参后的返回。

所以应用的场景不同,不能连用。

十二、ref与out的案例练习

1、案例1: 两个int变量的交换,用方法做。ref ? out

private static void Main(string[] args){int m = 10, n = 20;//Swap(ref m, ref n);//方法一Swap1(m, n, out m, out n);//方法二Console.WriteLine(m + "---" + n);Console.ReadKey();}private static void Swap1(int m1, int n1, out int m2, out int n2){m2 = n1;n2 = m1;}private static void Swap(ref int m, ref int n){int temp = m;m = n;n = temp;}

2、案例2: 模拟登陆,返回登陆是否成功(bool),如果登陆失败,提示用户是用户名

错误还是密码错误。admin、888888

[两个返回值,一个bool,一个string] ref ? out

private static void Main(string[] args){Console.WriteLine("请输入用户名:");string name = Console.ReadLine();Console.WriteLine("请输入密码:");string password = Console.ReadLine();if (Login(name, password, out string result)){Console.WriteLine("登陆成功!");}else{Console.WriteLine(result);}Console.ReadKey();}private static bool Login(string name, string password, out string result){if (name != "admin"){result = "用户名错误";return false;}if (password != "888888"){result = "密码错误";return false;}else{result = "用户名密码正确";return true;}}

上面用否定判断,只要三种情况。

如果用肯定就多了:

1)name="admin" && password="888888" //完全正确

2)name="admin" //密码错误

3)password="888888" //用户名错误

4)else //两者都错

十三、out与ref的方法重载。

1、方法重载要要点:

1)方法名称相同

2)方法签名不同

签名指

参数类型、个数、(顺序)

参数的修饰符 (ref、out、 params)

但不包含方法返回值。

2、out与ref

ref与out形成机制类似,都有点引用味道。但out更为特殊点一点,只输出。

相当于ref是一个成品,out是一个半成品。所以一般不说out是引用参数。

因此,在重载时,ref与out是不能相见的。

也即ref与out不能形成重载。

private static void M1(int n)//1{}private static void M1(string s)//2{}//static void M1(out int n)//3//{// n = 0;//}//static void M1(out string s)//4//{// s = "";//}private static void M1(ref int n)//5{}private static void M1(ref string s)//6{}

1)上面1,2,5或1,2,5,6可以形成重载。

2)上面1,2,3或1,2,3,4可以形成重载。

3)但是out与ref不能混入相见,即:

3,4之一或两者,不能与5,6之一或两者,混合不能形成重载,报错。

比如1,2,3,5错误。报错:不能定义在参数修饰符ref与out上存在区别重载。

说明:ref与out在重载时,若后面参数一样时,编译器不能区别出ref与out的

的区别。也即两者函数签名是一样的。所以不能混合在一起。

除非在后面参数上再变化一下,进行区别,编译器才认出两者重载:

private static void M1(out string s)//4{s = "";}private static void M1(ref string s, int a)//6{}

上面两者是可以重载的。

M1(out string s)与M1(ref string s)是不能重载,但在后面参数多一个,变化

一下,就可以重载了。

3、结论:

重载时,编译器会把out与ref看作同一个辨识符,不会区别。

十四、比较两个对象是否为同一个对象

1、判断两个对象是否为同一个对象的方法有哪些?

Equals、==、ReferenceEquals

2、什么是同一个对象?

如果两个对象指向堆中同一块内存地址,则这两个对象是同一个对象。

private static void Main(string[] args){Person p1 = new Person();p1.Name = "林则徐";Person p2 = new Person() { Name = "林则徐" };Person p3 = p1;Console.ReadKey();//a处}private class Person{public string Name { get; set; }}

上面p1,p2分别各自在堆中开辟空间创建对象,是不同对象。

p1与p3指向同一个对象,所以是同一个对象。

在a处下断点,运行(F5),中断后,打开即时窗口(菜单中调试->窗口->即时)

输入&p1,回车,显示p1存储在栈的地址,但没有显示这个栈中地址内容,这个内

容就应该是堆中对象的地址,可惜vs加强了托管,不再显示看不到了。

同理再输入&p2,回车,&p3回车.

这样可以看到&p1,&p2,&p3的值,实际上就是它们入栈后的地址,相隔4个字节。

本想用GetHashCode来验证。但msdn上说:

对象相同则哈希码一样,但反推不一定。

也就是说哈希码一样,却不能证明是同一个对象,那你微软创造这个函数做

毛线?

网上查了一下,有一个用代码取得对象地址的方法,可以去尝试一下:

/xiaoyaodijun/p/6605070.html

3、比较两个对象是否为同一个对象?

1)现象1:

private static void Main(string[] args){Person p1 = new Person();p1.Name = "林则徐";Person p2 = new Person() { Name = "林则徐" };Person p3 = p1;Console.WriteLine(object.ReferenceEquals(p1, p2));//falseConsole.WriteLine(object.ReferenceEquals(p1, p3));//trueConsole.WriteLine(p1.Equals(p2));//falseConsole.WriteLine(p1.Equals(p3));//trueConsole.WriteLine(p1 == p2);//falseConsole.WriteLine(p1 == p3);//trueConsole.ReadKey();}private class Person{public string Name { get; set; }}

上面对于类来说,比较它们的对象三个方法都得出一致的结果。

2)现象2:

private static void Main(string[] args){//string s1 = "abc", s2 = "abc";//TTT 同一对象string s1 = new string(new char[] { 'a', 'b', 'c' });string s2 = new string(new char[] { 'a', 'b', 'c' }); //TTFConsole.WriteLine(s1 == s2);Console.WriteLine(s1.Equals(s2));Console.WriteLine(object.ReferenceEquals(s1, s2));Console.ReadKey();}

ReferenceEquals()仍能准确判断是否是同一个对象。

当是字符串时,==与Equals的结果是一致的。

而ReferenceEquals()在常量确认是;

在new时则因在不同内存创建,是不同对象。这点与==与Equals结论相反。

因为此时==与Equal还要比较内容,只要内容一样也认为一样。

原因:

打开.Net reflector,搜索string,查看Equals(string)。如图:

在两对象ReferenceEquals()为真时,返回真。否则继续向下比较(并不

是直接返false),在遇到两者的内容一样时,它继续返回真,否则返回假。

注意里面的代码,说明Equals(string)有两种情况返回真:

1)确实是两个对象时[相当于ReferenceEquals()];

2)即使不是是同一对象,只要里面内容一样,也返回真。

再次查看一下==号操作符重载是怎么比较的:

operator ==(string a, string b)

发现它直接就调用了Equals(string),也就是说:

在参数是string时:==与Equals的比较方法是一样的。所以结论一致。

此时Equals(string)是重载。

另一个比较Equals(object)则是重写,它的比较方法与Equals(string)

相同。

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), __DynamicallyInvokable]public override bool Equals(object obj){if (this == null){throw new NullReferenceException();}string strB = obj as string;return ((strB != null) ? (!ReferenceEquals(this, obj) ? ((this.Length == strB.Length) ? EqualsHelper(this, strB) : false) : true) : false);}

Equals(object)来源于object类中的虚方法,在string类中进行了重写

也就是说,字符串的类中有两个判断方法:

1)重载Equals(string)

2)重写由object而来的Equals(object)

两者与重载的==,它们的比较方法都是一样的,结果也是一样的。

因此,这三个方法返回的结果都是真,就有以下两个情况:

1)确实是一个对象;

2)不是一个对象,但里面的内容一样。

3)结论:

比较两个对象推荐使用object.ReferenceEquase(object o1,object o2)。

不推荐用Equals()与==,因为他们在内容相同时也会认为是同一个对象。

4)问题:只要不是字符串,其它情况就可以用Equal与==来判断两者是否相等?

答:不推荐。

因为Equals或==可以被再次重载或重写,调用者还要花精力考虑它们

是否被重载或重写。

而object.ReferenceEquals(o1,o2)是静态方法,不能重写重载,非常保险。

internal class Program{private static void Main(string[] args){Person p1 = new Person() { Name = "武则天" };Person p2 = new Person() { Name = "武则天" };Console.WriteLine(p1 == p2);Console.WriteLine(p1.Equals(p2));Console.WriteLine(object.ReferenceEquals(p1, p2));Console.ReadKey();}}internal class Person{public string Name { get; set; }public override bool Equals(object obj){Person p = obj as Person;if (p == null) return false;if (this.Name == p.Name) return true;return object.ReferenceEquals(this, p);}}

原本判断不是同一个对象,三个皆为false.

但Person类中进行了重写Equals,导致结果变量:false,true,fasle

5)为什么字符串的Equals和别的不一样?

答: 原本object中的Equals方法是判断对象的地址是否相同。

但到了string类时,该方法被重写(见2)分析)

string类中Equals方法还会判断字符串的内容是否相同,相同也为同一对象。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。