一、复习
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("轿车");
}
}
提示:反复说接口,并不是练接口怎么写。而是怎么从问题中找出接口、抽象类。
以及最终怎么实现多态。
技巧:
vs2022中上面代码行号的左侧有一个蓝色的小图标,同时附加了一个小箭头。
鼠标指向它,会提示“已继承...”,说明在接口中可以用“继承”这个用语。
右击蓝色图标,如果是接口,则会显示具体的哪些类实现了接口/成员。如果
是类则显示实现自哪个接口/成员。
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。显式无法调用为private
IFlyable1 t1 = new Teacher();
t1.Fly(); //正常的接口,1
IFlyable2 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()");
}
}
提示:
vs2022中,对于接口,智能提示时,它的小图标是两个圆圈(一大一小)用一根
线连接在一起,表示接口。这个小图标下面若有一个白色的心形形状,表示访问修饰
的是程序集内部(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();//子类用new
s.Fly();//1
Console.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));//1
Console.WriteLine(sizeof(bool));//1
Console.WriteLine(sizeof(char));//2
Console.WriteLine(sizeof(short));//2
Console.WriteLine(sizeof(long));// 8
Console.WriteLine(sizeof(int));//4
Console.WriteLine(sizeof(float));//4
Console.WriteLine(sizeof(double));//8
Console.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*));//4
Console.WriteLine(sizeof(int*));//4
}
Console.WriteLine(sizeof(Person));//4
Console.ReadKey();
}
上述代码需要设置允许不安全代码:右击当前项目->点击属性->点击左侧生
成->勾选右侧"允许不安全代码"。(vs2022)
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 ;//1
private int age;//2
private string ID;//3
private char BloodType;//4
}
private static void Main(string[] args)
{
unsafe
{//结构大小只能在不安全上运行
Console.WriteLine(sizeof(Person));//16
Console.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.Array
Console.WriteLine(s.GetType().BaseType.BaseType.ToString());//System.Object
Console.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执行的代码,影响程序执行的效率。
技巧:
vs2022中,由于当前解决方案有多个项目,如何一次性关闭它们?
右击(主IDE界面中)代码窗上面的标签,选择“关闭所有选项卡”,或者选择
“除此之外全部关闭”,可快速关闭其它项目或全部项目的标签。
也可以在菜单上的“窗口”菜单里进行操作,只是有点不习惯。
3、案例:
try
{
int x = 5;
int y = 0;
int z = x / y;
}
catch
{
Console.WriteLine("除数不能为0");
}
程序运行运行时出错,后续的内容无法运行程序一旦有一个功能发生异常,整
个程序崩溃其它功能也无法正常运行
技巧:
vs2022中,如何快速添加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";//1
Console.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; // 4
Console.WriteLine("4444");
}
catch
{
Console.WriteLine("5555");
//n = 3;// 3 try块中n是局部变量,不能在catch块中使用
throw;//1
Console.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);//a
Console.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操作比较艰深,有反汇编基础的可以了解一下。参考:
https://www.cnblogs.com/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-4
GetLength("bbbb", null);//空对象 bbbb
GetLength("cccc"); //有对象长度为0,cccc-0
int[] 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);//101
Console.WriteLine(y);//101
Console.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中的out
string 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存储在栈的地址,但没有显示这个栈中地址内容,这个内
容就应该是堆中对象的地址,可惜vs2022加强了托管,不再显示看不到了。
同理再输入&p2,回车,&p3回车.
这样可以看到&p1,&p2,&p3的值,实际上就是它们入栈后的地址,相隔4个字节。
本想用GetHashCode来验证。但msdn上说:
对象相同则哈希码一样,但反推不一定。
也就是说哈希码一样,却不能证明是同一个对象,那你微软创造这个函数做
毛线?
网上查了一下,有一个用代码取得对象地址的方法,可以去尝试一下:
https://www.cnblogs.com/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));//false
Console.WriteLine(object.ReferenceEquals(p1, p3));//true
Console.WriteLine(p1.Equals(p2));//false
Console.WriteLine(p1.Equals(p3));//true
Console.WriteLine(p1 == p2);//false
Console.WriteLine(p1 == p3);//true
Console.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' }); //TTF
Console.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方法还会判断字符串的内容是否相同,相同也为同一对象。