一、面向对象概念复习
1、什么是面向对象?OOP(Object-Oriented Programming)
一种分析问题的方式,增强了程序的可扩展性。
OOP面向对象编程
OOA面向对象分析
OOAD面向对象分析与设计(Object Orient Analysis Design)面向对象的分析和设计
面向对象技术的优点:为能够构建与现实世界相对应的问题模型,并保持他们的结果,
关系和行为模式。
2、面向对象的三大特性:
封闭,继承,多态。
面向对象起源于生活,把生活中相同或相似的进行归类,总结,抽象出相同的特征,
这样就形成了类,是人类分析问题和解决问题的一种方法。因此是先有实例后有类
别。
也即:先有对象后有类。
因此解决问题时,也是先根据实际问题,分析异同,是否扩展,创建一个类,来解
决问题中的实际问题(对象)
创建了类和对象,会使得相关的数据、功能集中,它集合了字段属性方法。使得分
析问题脉络清晰,同时还增强了问题的可扩展性。
类是模具,创建对象的模具,抽象的。
类是一种数据类型,用户自定义的数据类型。
类的组成:字段,属性,方法,构造函数等
对象是具体的,是类的具体实例。
对象具有属性(特征),和方法(行为)
类中包含了数据(用字段表示)和行为(用方法,函数,功能)来表示。
方法为一块具有名称的代码。
技巧:
添加类文件(.cs)的方式:
可以直接在主程序Program.cs中进行添加类,但不推荐,容易混乱,推荐单独建立一
个类文件(.cs).
vs2022的几种方式:
1)右击当前项目->添加->新建项,选择“类”,填入名称即可创建。也可以在右上侧
搜索中填入“类”,或窗体的左侧中选择“代码”,更快地过滤出类文件,便于
选择。
2)右击当前项目->添加->现有项,然后选择自己已经建立好的类文件。
3)右击当前项目->添加->类,直接添加。
技巧:关于类的代码片段(写上关键字后按Tab)
class 加上Tab,创建类的代码(默认internal)
ctor 类中创建构造函数
prop 类中创建自动属性
propfull 类中创建字段与属性的代码
indexer 类中创建索引器
3、构造函数:
类中默认有一个隐形的无参构造函数。
当手动创建一个构造函数后,原隐形无参函数会被覆盖而消失。
构造函数一般为public否则不能创建对象,也有特殊情况时用private,通过曲线
方式来创建对象。(比如类中方法可以调用私有的构造函数)
练习:
学生类的输入(结合上面的代码片段)
internal class Student//输入class后按Tab自动创建类,修改类名后按回车,光标进入类中
{
public string Name { get; set; }//输入prop按Tab自动创建
public int Age { get; set; }
public string Gender { get; set; }
public string SId { get; set; }
public Student(string name, int age, string gender, string sid)//输入ctor按Tab,自动创建构造函数
{
this.Name = name;
this.Age = age;
this.Gender = gender;
this.SId = sid;
}
public void SayHi()//输入"SayHi("后,按Shift+Enter,自动添加花括号并进入代码内
{
Console.WriteLine($"{Name}{SId}");//输入cw后按Tab自动输出命令
}
}
4、自动属性与手动字段+对应属性的区别:
两者基本相同。只不过自动属性的字段(隐形)由编译器自动生成,例如:
private string <Name>k__BackingField;(用.Net Reflector反编译查看)
每次编译器生成的字段可能不一致。
因此,一般情况下两者可互换,但在序列化时,由于自动属性的字段由编译器自动
生成,每次不一样,有可能产生问题。所以推荐传统老式写法。
5、Convert.ToInt32()与int.Parse(),int.TryParse()的区别
对Convert.ToInt32()按F12查看反编译,可以里面有int.Parse():
public static int ToInt32(string value)
{
if (value == null)
{
return 0;
}
return int.Parse(value, CultureInfo.CurrentCulture);
}
说明Convert.ToInt32()内部调用了int.Parse(),因此比int.Parse()更完善。
Convert.ToInt32()参数为null时,返回0;
int.Parse()参数为null时,抛出异常。
若参数为""时,上面两个都抛出异常。
Convert.ToInt32()参数可以是字串、字节、字符等类型转多。
int.Parse()只能转换数字类型的字符串。
简单地说Convert.ToInt32()适应性更强。
由于int.Parse()抛出异常时费资源,因此进行了改进为int.TryParse()
这样就不会抛出异常。
int.TryParse(s,out int result)成功返回true,结果在result中
失败返回false,结果在result为0
注意:result是一个"外部"变量,在语句外生效。
string s = "123";
bool n = int.TryParse(s, out int result);
Console.WriteLine(Convert.ToInt32(n));//true,1
例:
int.TryParse("",out int result)//false,result=0
int.TryParse(null,out int result)//false,result=0
Convert.ToInt32("")//异常
Convert.ToInt32(null)//0
(int)属于cast转换,只能将其它数字转为int,比如double,float等,而不能
转换字符串。如:
int n = (int)2.4; //正确
int n1 = (int)"231";//错误,无法将string转为int
6、bool转为整形是多少?
Convert.ToInt32(true);//为1
Convert.ToInt32(false);//为0
二、案例:计算器
上课的计算器用的是winform,本次用控制台。无论哪一种,都会重写一遍代码。
如果能够在winform与控制台,都能用。尽可能把功能进行封装,实现代码重用。
因此使用类来进行封装计算器。
创建一个计算类。先只添加一个+
internal class Calculator
{
public Calculator(double n1, double n2)
{
this._number1 = n1;
this._number2 = n2;
}
private double _number1;
private double _number2;
public double Number2
{
get { return _number2; }
set { _number2 = value; }
}
public double Number1
{
get { return _number1; }
set { _number1 = value; }
}
public double Add()
{
return _number1 + _number2;
}
}
然后在winForm中可以调用这个对象的实例,用Add加法实现:
private void button1_Click(object sender, EventArgs e)
{
double n1 = Convert.ToDouble(textBox1.Text);
double n2 = Convert.ToDouble(textBox2.Text);
Calculator c = new Calculator(n1, n2);
switch (comboBox1.Text)
{
case "+":
label1.Text = c.Add().ToString();
break;
default:
break;
}
}
说明:
每次添加一个运算,比如再添加一个减法,就会再次打开Calculator进行方法
更新,再生成可执行文件。这样,每次操作源代码繁冗,是不可取的。
一般,会将运算生成dll,更新时只须要更新dll,这样不会全部源代码打开进
行重新生成可执行exe,即方便了更新升级程序,也方便功能的拆分,使得分工更
为恰当。
三、案例:猜拳游戏
问题:人和电脑分别出剪刀石头布,进行比较胜败。
1、分析
对猜拳进行数字化处理,设置布1,剪刀2,石头3
两者相差的数进行归类总结:
败:1-2=-1,2-3=-1,3-1=2,有两种-1和2为败.
平:0为平
胜:其余为败
2、OOP分析:
创建三个类。玩家,有拳(字串数据)和出拳(动作,返回int)
电脑,有拳(字串和整形)和出拳。
裁判,用一个静态方法进行判断
细化:玩家与电脑类有构造函数,电脑构造时添加随机出拳。
对于属性只取Get,所以不用写set
界面:(5个label,3个button)
label1:玩家,label2:玩家拳名,label3电脑,label4电脑拳,label5胜负平
button1玩家布,button2玩剪刀,button3玩家石头
玩家类:
internal class Player
{
private string _fist;
public string Fist
{
get { return _fist; }
}
public Player(string fist)
{
this._fist = fist;
}
public int ShowFist()
{
switch (_fist)
{
case "布":
return 1;
case "剪刀":
return 2;
default:
return 3;//石头
}
}
}
电脑类:
internal class Computer
{
private string _fist;
private int _n;
public string Fist
{
get { return _fist; }
}
public Computer()
{
Random r = new Random();
_n = r.Next(1, 4);//随机出拳
switch (_n)
{
case 1:
_fist = "布";
break;
case 2:
_fist = "剪刀";
break;
default:
_fist = "石头";
break;
}
}
public int ShowFist()
{
return _n;
}
}
裁判类(判断胜负,静态)
internal class Judgment
{
public static string Judge(Player player, Computer computer)
{
if (player.ShowFist() == computer.ShowFist())
{
return "平";
}
else if (player.ShowFist() - computer.ShowFist() == -1 || player.ShowFist() - computer.ShowFist() == 2)
{
return "败";
}
else
{
return "胜";
}
}
}
事件,主要是三个按钮,由于代码类似,写成一个方法:
private void button1_Click(object sender, EventArgs e)
{
GetJudge("布");
}
private void button2_Click(object sender, EventArgs e)
{
GetJudge("剪刀");
}
private void button3_Click(object sender, EventArgs e)
{
GetJudge("石头");
}
private void GetJudge(string v)
{
Player p = new Player(v);
Computer c = new Computer();
label2.Text = v;//玩家拳
label4.Text = c.Fist;//电脑拳
label5.Text = Judgment.Judge(p, c);//结果
}
3、补充说明
1)sender与e是什么意思?
很多控件的事件中都带有这两个参数,它们是什么意思?例如:
private void button2_Click(object sender, EventArgs e)
object sender 表示触发事件的对象(事件的激发控件),由什么控件被激发。
比如点击button1,button1监测到被点击了,它就触发点击事件,这个
sender就是button1
可以理解为委托:
Delegate void Event(object o,EventArgs e)
为什么事件类型用Object?
因为Object是所有类型的基类。事件激发可能是Button类,可能是Label类
可能是Picture类等,甚至还有可能是一个变量(后面委托监测激发),因此
类型多样,所以用Object来代替.
eventArge e 表示触发事件的相关信息,对象中的数据(包含事件参数)。比如:
如果是单击事件,那么这个事件是左击还是右击,都包含在e中.
如果是鼠标移动事件,那么鼠标移动的坐标包含在e中.
如果是键盘按下事件,那么按的是哪个键包含在e中.
2)DRY原则:(don't repeat yourself)不要重复你自己。
如果有代码老是用Ctr+C和Ctr+V,说明代码重复量比较大,说明软件设计有问
题。因为应该进行封装,或者再写一个方法传入参数,来减少代码的重复量。
3)根据上面1)2),还可以优化一下:
将三个button的事件关联到一起。
选择button2,按F4调出属性,切换到事件,选择Click事件,在其右侧的下拉
菜单中选择button1。同理,button3的点击也关联到button1中.
这样三个键的事件就可以由button1一键激活。
但事件得判断object sender是谁激活。因此改写button1的事件:
private void button1_Click(object sender, EventArgs e)
{
Button b = (Button)sender;//里氏转换,父转子以以便后面取Text
GetJudge(b.Text);
}
同时,屏蔽(注释)掉原来button2与button3的点击事件。
这样button2与的button3的事件由于关联到button1中,当点击button2时,它
会自动去激活button1的Click事件,其中的sender就是激发的控件(button2),通
过转换,取得真实的控件b,从而将其属性Text转给方法。
问题引申:
不通过设置,而通过代码将button2与button3的点击事件关联到button1的点击
事件?
技巧:
将重复的代码封装成一个方法:
选中这些代码,右击选择->快速操作和重构(或者Alt+Shift+F10),然后回车
修改方法名即可。
四、命名空间-添加引用-变量作用域
1、命名空间
添加引用(前提):添加程序集
导入命名空间: namespace(快捷键Alt+Shift+F10)
为什么在另一个项目中建的类,添加引用后还是不能使用?
类的访问修饰符默认:internal,改成public。
参数与返回值
参数的个数、类型与返回值没有任何半毛线关系。
控制台应用程序中不要新建Form
再次说明this的使用,通过this访问类的属性。this.Fist
表示类中当前的这个对象。
2、变量作用域
变量作用域:离声明该变量最近的那对包含声明语句的大括号内部
成员变量--直接属于某个类,作用域在该类内部。
成员变量:在类中直接声明的变量叫做类的成员变量,类的成员变量声明以后
可以不赋初值,因为会有默认值
成员变量使用前如果不赋值,默认会有一个初始值,
string->null,
int->0,
bool->false
局部变量:在方法/函数体(如主函数)、或语句块(如For)中声明的变量。
局部变量必须声明并赋值后,才能使用,否则报错。
private static void Main(string[] args)
{
int x;
x++; //错误 x=x+1,使用了x
x=10+1;//正确, x=11,没有使用x,只是赋值
for (int i = 0; i < 10; i++)
{
int n;
n++;//错误
}
}
public void Show()
{
int n = 10;
if (n > 5)
{
int n =100;//已经声明n,不能重复声明
}
}
类成员变量与局部变量是可以重名的,而且局变量优先成员变量。
但局部变量不能重名如上例。
判断下面是否错误:
1)
public int n;
public static int m;
private static void Main(string[] args)
{
Console.WriteLine(n);//错误,
}
非静态字段,必须赋初值。
由于静态成员只能调用本范围外的静态成员。本范围内的不限于静态成员和局部
变量。静态Main中的n,对其外是非静态成员,对其内是局部变量(未声明赋值)
调用时会出错。
详细如下:
internal class Program
{
private static void Main(string[] args)
{
Person p = new Person();
p.Show();//0
Console.ReadKey();
}
}
internal class Person
{
public int n;
public void Show()
{
Console.WriteLine(n);
}
}
2)
public int n;
public static int m;
private static void Main(string[] args)
{
Console.WriteLine(m);//0
}
正确,静态字段int默认为0
3)
public int n;
public static int m;
private static void Main(string[] args)
{
int m = 3;
Console.WriteLine(m);//3 局部变量优先
}
正确,局部变量优先成员变量,为3.
4)
public int n;
public static int m;
private static void Main(string[] args)
{
Console.WriteLine(m);
int m = 3;
Console.WriteLine(m);//3 局部变量优先
}
错误,因为声明了局部变量,则花括号内应是局部变量的范围,但在局部变量声明前
又使用了m,所以报错。
五、面向对象(封装)
1、什么是封装?
遥控器刚出来的时候很神奇,点个按钮就能换台、改变音量、关电视。而我们使用
遥控器的人不需要知道他是怎么实现的(只须知道每个按钮的功能即可)
和遥控器类似,面向对象的封装就是把事件的状态和行为封装在类中。
使用类的人不需要知道类内部是怎么实现的,只要调用其中的属性和方法实现功能
就行。就如同使用遥控器,不需知道怎么控制,只要知道按钮能换台即可。
2、为什么要封装?
方便用户使用,代码重用,增加安全性,可扩展性。
3、类和对象本身就是封装的体现。
1)属性封装了字段(清洗数据);
2)方法的多个参数封装成了一个对象;
3)将一堆代码封闭到了一个方法中;
4)将一些功能封装到了几个类中;
5)将一些具有相同功能的代码封装到了一个程序集中(dll,exe),并且对外提供
统一的访问接口(属性名,方法名等)。
Person person = new Person("张三", 23) { Name = "曹操", Age = 40 };//对象的初始化器
在调用初始化器时,会先调用构造函数,然后才执行初始化器。故Name最终为曹操
六、面向对象 继承
1、继承是指类与类之间的关系。具体的继承于更抽象的。
例如:车Vehicle
卡车Truck
轻型卡车
重型卡车
轿车Car
小轿车
面包车
2、如何判断一个继承关系是否合理?
子类 is a 父类
卡车和轿车都是车,都有轮子、发动机。但卡车又能拉货,轿车能拉人
3、Base Class 基类
Parent Class 父类
Derived Class 派生类
Child Class 子类
父类越向上越抽象,子类越向下越具体。
所有的类都直接或间接的继承自object,查看IL代码(.Net Reflector)。
任何类未显式写明继承时,都继承于Object。当A显式写明父类B时,则继承于当前
的父类B,A不再直接继承于B。但是,当前的父类B又没显示写明继承,同理它也继承
于object,所以实际上A再次继承了object的一些成员。object的显隐。
类的单根继承性、传递性。
4、为什么要继承?继承带给我们的好处?
1)代码重用;
2)多态
子类继承父类的属性和方法,使创建子类变得很简单,实现了代码重用以及多态。
3)LSP里氏替换原则: 子类可以赋值给父类。
已经存储子类A的父类,可以强制转为子类A。
(但已经存储子类A的父类,不可以强制转为子类B)
例如:Student继承Person,Teacher继承Person.
Person ps=new Studendt();
Person pt=new Teacher();
Student s1=(Student)ps;//正确
Student s2=(Student)pt;//错误。存储同子类(student)的父类才能强制转换到student
Person p=new Person();
Student s3=(Student)p; //错误,此时p实际存储的是父类,而不是子类,这种情况
不能父转子。
里氏替换原则主要为了多态。 多态就是为了增加程序的可扩展性、灵活性。
判断一个对象(实例)属于某个类:object is Class
Student s=new Student()
if(s is Student){...}
代码重用与代码冗余的区别:
冗余是功能相同,把代码再次进行复制粘贴的过程,叫代码冗余。多了不好!
重用是功能相同,不须复制粘贴直接再次用别人的代码,例如,老师继承于
学生类,这样学生类中的姓名、年龄、ID,不必在老师类再进入输入,直接由继承
实现代码重用。精简了很好!
方法的重写override
虚方法原理:虚方法表
5、继承时构造函数的问题。
构造函数不能被继承。所以有下面情况:
子类调用本身构造函数时,一般情况下会默认先调用父类的无参构造函数。若
父此时没有无参构造函数,则报错。解决方法:
1)父类添加无参数构造函数
2)子类构造函数指定父类的某一有参构造函数base.
特殊情况两种:1)用this,表示调用本类其它构造函数。
2)用base,表示明确指明调用父类存在的构造函数。
internal class Studend : Person
{
public string SId { get; set; }
public Studend(string name, int age, string sid) : base(name, age)
{
this.SId = sid;
}
}
上面Student构造函数中参数可以乱序,但在base(name,age)中参数必须严格按照
父类指明的构造函数中的参数顺序来写。
Student构造函数后面无base时,会先调用父类Person的无参构造函数(若无此
函数则报错),添加base后,会先调用指定的父类Person对应的构造函数。
所以,一般情况下子类是先调用了父类的base(),即无参构造函数。
internal class Studend : Person
{
public string SId { get; set; }
public Studend(string name, int age, string sid) : base()
{//父类有无参构造函数时,也可以“刻意”调用base(),不会报错
this.SId = sid;//Name与Age没有赋值,不会报错
}
}
6、关键字
1)protected
只能在当前类的内部,以及所有子类的内部中使用;
可以在类内部以及所有子类中 (内部) 访问。
五种修饰符:internal,public,private,protected,internal protected
internal 程序集,只要在同一个程序集(同一项目,同一exe,同一dll)中都
可以访问,与namespace没有关系。
同一程序集(项目)中可能有不同的命名空间,只要引用,就可使用访问
到成员。
但是,在不同的程序集(项目)中,既使引用了另一个项目,若其中
的成员用的是interanl,那么引用后仍然无法使用到该成员。
类中成员默认为private,同样结构内部成员默认也为private.
接口内的成员默认是public,且不能添加任何访问修饰符。
类本身不写修饰符则默认为internal,同样结构与接口也默认为internal.
命名空间下的成员修饰符只能是internal或public,比如类。
注意微软自己可以用private,与我们程序员无关,可查看IL。
2)this
1.作为当前类的对象,可以调用类中的成员。this.成员(调用成员,自己)
2.调用本类的其他构造函数。this()(调用构造函数,自己) reflector查看
internal class Studend : Person
{
public string SId { get; set; }
public Studend(string name, int age, string sid) : base(name, age)///3
{
this.SId = sid;
}
public Studend(string name) : this(name, 22, "1111")//2
{
}
public Studend() : this("秦始皇")//1
{
}
}
this调用本类的构造函数时,应比当前的构造函数更为全面。例如上面2处比1
更全面,所以this调用的是2处。同理3比2更全面,所以this调用的是3处。
因此上面若调用:
Studend s = new Studend();
那么先执行1处,再跳到2处执行,但2处又要调用3处,3处又要调用父类构造
这样,回来时从父类->3->2->1,完成了上面的过程。所以Age的值是22。
结论:
用this调用本类构造函数,应是更全面的构造函数。
但是,不是把所有字段参数全部写完,可以写部分,如2处。
3)base
1.调用父类中的成员(在子类重写父类成员,或者子类使用new关键字隐藏了父
类成员时,调用父类成员) ,base点不出子类独有成员。
2.调用父类中的构造函数(调用构造函数,父类)
当调用从父类中继承过来的成员的时候,如果子类没有重写则:
this.成员 与 base.成员: 两者没有区别。
如果子类重写了父类成员,则:
this.成员:调用的是子类重写以后的。
base.成员;调用的依然是父类的成员。
internal class Program
{
private static void Main(string[] args)
{
Student s = new Student("宋江", 52);//6
s.Show();//7
Console.ReadKey();
}
}
internal class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person()//1
{
this.Name = "李世明";
}
}
internal class Student : Person
{
public string SId { get; set; }
public Student(string name, int age)//2
{
this.Name = name;//3
this.Age = age;
}
public void Show()
{
Console.WriteLine(this.Name);//4
Console.WriteLine(base.Name);//5
}
}
说明:主函数6处创建对象时,调用Student中2处构造函数,2处会先调用父类Person
中1处的构造函数,给Name赋值“李世明”,由于继承,此时父类的base.Name与子
类Student中的this.Name都是“李世明”。
父类构造函数调用完后,回到子类Student中的构造函数2处,执行方法体内
3处的赋值,即this.Name="宋江",同时这个this.Name与base.Name是相同的。所
以,子类与父类的Name再次赋值为宋江。所以调用主函数中7处时,其对应Student
类中方法Show()内,4处与5处显示宋江。
实际上,子类内部部分根据父类模板,在子类内部创建了父类的成员(继承):
因此主函数6处实际上只是创建了一个对象(不是两个),其中子类调用的是复制
到子类内部的父类成员,所有这些都是在子类内部。
因此this是所有子类的成员,包括上部分的父类模板创建的成员。
而base则只能是由父类模板在子类内部创建的成员,上部分黑色部分。
此时,原父类中的成员用this与base都是指向同一个。因此两者相等。
但是,如果子类中有意用new隐藏父类中成员,使得原this指向上部分base的转向
改为指向子类“自己”的下部分中同名成员,这时base与this的指向就变得不一样
了。如图:
internal class Program
{
private static void Main(string[] args)
{
Student s = new Student("宋江", 52);//6
s.Show();//7
Console.ReadKey();
}
}
internal class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person()//1
{
this.Name = "李世明";
}
}
internal class Student : Person
{
public string SId { get; set; }
public new string Name { get; set; }//8
public Student(string name, int age)//2
{
this.Name = name;//3
this.Age = age;
}
public void Show()
{
Console.WriteLine(this.Name);//4
Console.WriteLine(base.Name);//5
}
}
主函数执行到6处时转到2处,又会调用父类1处,因此base.Name就成了李世明
本来此时子类中this.Name也应该是李世明,但因为New string Name刻意隐藏父类
成员,也即this.Name原来指向base.Name,却改指向了New string Name了。
由1处返回到子类3处执行,这时的Name就New Name即宋江了。
可以看到,一旦用New进行改写有意隐藏父类同名成员,那么对于原来父类模板
创建的成员this与base指向了不同。
因此:this.Name是宋江,base.Name是李世明。
4)子类构造函数必须指明调用父类哪个构造函数
7、练习:
1)定义父亲类Father(姓lastName,财产property,血型bloodType),儿子Son类(玩
游戏PlayGame方法),女儿Daughter类(跳舞Dance方法),调用父类构造函数(:base()
)给子类字段赋值
internal class Program
{
private static void Main(string[] args)
{
Son s = new Son("李", 2345678.2, "A");
s.PlayGame();
Daughter d = new Daughter("张", 2345511, "O");
d.Dance();
Console.ReadKey();
}
}
internal class Father
{
public string LastName { get; set; }
public double Property { get; set; }
public string BloodType { get; set; }
public Father(string lastname, double property, string bloodtype)
{
this.LastName = lastname;
this.Property = property;
this.BloodType = bloodtype;
}
}
internal class Son : Father
{
public Son(string lastname, double property, string bloodtype) : base(lastname, property, bloodtype)
{
}
public void PlayGame()
{
Console.WriteLine("game");
}
}
internal class Daughter : Father
{
public Daughter(string lastname, double property, string bloodtype) : base(lastname, property, bloodtype)
{
}
public void Dance()
{
Console.WriteLine("dance");
}
}
2)定义汽车类Vehicle属性 (brand(品牌),color(颜色) ) 方法run,子类卡车
(Truck) 属性weight载重 方法拉货,轿车(Car)属性passenger载客数量 方法载
客
internal class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public void Run()
{
Console.WriteLine("running");
}
}
internal class Truck : Vehicle
{
public double Weight { get; set; }
public void LaHuo()
{
Console.WriteLine("拉货");
}
}
internal class Car : Vehicle
{
public int Passenger { get; set; }
public void ZhaiKe()
{
Console.WriteLine("载客");
}
}
3)升级猜拳游戏(加入父类[继承] )
在玩家与电脑出拳上进行归纳总结。总结到父类有拳名(fistname,剪刀石头布)和
拳的数据化(intfist)。字段只需protected,属性只须Get。
因此从玩家与电脑中提炼父类,其它的都不变化。
internal class Fist
{
protected int _intfist;
protected string _fistname;
public int IntFist
{
get { return _intfist; }
}
public string FistName
{
get { return _fistname; }
}
}
internal class Player : Fist
{
public Player(string fistname)
{
switch (fistname)
{
case "布":
_intfist = 1;
break;
case "剪刀":
_intfist = 2;
break;
default:
_intfist = 3;
break;
}
_fistname = fistname;
}
public int ShowFist()
{
return _intfist;
}
}
internal class Computer : Fist
{
public Computer()
{
Random r = new Random();
_intfist = r.Next(1, 4);
switch (_intfist)
{
case 1:
_fistname = "布";
break;
case 2:
_fistname = "剪刀";
break;
default:
_fistname = "石头";
break;
}
}
public int ShowFist()
{
return _intfist;
}
}
注意:
当this与base作为调用构造函数的语法的时候,参数的传递可能会有的疑惑。
:base(参数,参数)对应指定的父类构造函数时的参数须类型、顺序一致。
七、可访问性不一致问题(修改成员的访问修饰符)
1、访问级别约束
1)子类的访问级别不能比父类的高。 (会暴露父类的成员)
internal class Fist
{
}
public class Player : Fist//错误,
{
}
上面发生错误:可访问性不一致:基类Fist的可访问性低于子类Player.
原因:父类原意限制自己成员仅在同一程序集才能访问,子类Player继承父类这些
成员后,因其为public,所以在任意程序集都可“使用展现”原来父类的成员,
导致父类成员“暴露”在另一程序集中。
2)类中属性或字段的访问级别不能比所对应的类型访问级别高。
internal class Person
{
public string Name { get; set; }
}
public class Myclass
{
public void SayHi(Person person) //错误
{
Console.WriteLine(person.Name);
}
}
提示:可访问性不一致,参数类型Person的可访问性低于(public)SayHi().
原因:(public)SayHi()可以在任意程序集中进行访问,但是它的参数person是
internal也即只能在程序集内进行访问,两者访问性不一致,person拖了这
个方法的“后脚”,这样导致,在程序集外访问时可能因person而发生错误。
方法的访问修饰符需要与,方法的参数类型的访问修饰符一致。
注意:即使上面方法public void SayHi(Person person)把public改protected一样
会报错。这和1)的原因一样,protected原意思只想类及子类中用,但参数person
的public,有可能使得原来protected中的“数据”,通过public而泄露。
因此,必须保持一致!是一模一样!!!
对比下面:
internal class Person
{
public string Name { get; set; }
}
internal class Myclass
{
public void SayHi(Person person)
{
Console.WriteLine(person.Name);
}
}
提示:这里person是internal,其方法前面是public,两者类型不一致,似乎应该是
错误的。但是,因为SayHI()方法的类是internal,也即SayHI虽写public,但
因类internal的限制,始终跳不出如来佛的手心internal,public实际不起作用,
相当于是internal,与参数person的internal是一致的,所以正确!!!
3)方法的访问级别不能比方法的参数和返回值的访问级别高。
internal class Person
{
}
public class Myclass
{
public Person GetPerson() //error
{
return new Person();
}
}
提示:可访问性不一致,返回类型Person的可访问性低于方法(Public)GetPerson()
原因:同2)原因一样,返回参数Person是Internal拖了方法GetPerson中public的后
脚,导致在程序集外该public的方法却不能执行。
internal class Person
{
}
public class Myclass
{
public Person Friend { get; set; }
}
因为在编译器最终属性也是编译成方法,因此上面实际上也是public方法却返回了一
个internal的返回值。因此也是错误的。
结论:
方法的参数与方法的返回值都必须得和方法保持一致的访问修饰符。门当户对!!
2、用于解决“可访问性不一致”的错误
修改访问修饰符,保持两者一致。门当户对即可。
八、实现多态的手段
1、通过虚方法virtual,实现方法重写-多态
多态:同样的方法,因里面实际对象的不同而调用不同的方法出现不同结果。
Person-Chinese-American-Korean,每个国家的人都有一个说出自己国籍的方法。
当有一个Person[]的时候,循环现实每个国家的人。
internal class Program
{
private static void Main(string[] args)
{
Person[] p = new Person[] { new Chinese(), new American(), new Japanese() };
for (int i = 0; i < p.Length; i++)
{
p[i].SayNationality();
}
Console.ReadKey();
}
}
internal class Person
{
public string Name { get; set; }
public virtual void SayNationality()
{
Console.WriteLine("person");
}
}
internal class Chinese : Person
{
public override void SayNationality()
{
Console.WriteLine("CN");
}
}
internal class American : Person
{
public override void SayNationality()
{
Console.WriteLine("USA");
}
}
internal class Japanese : Person
{
public override void SayNationality()
{
Console.WriteLine("JP");
}
}
创建一个对象,其实对象的内部有很多内容:同步索引块,方法表,元(元数据)
虚方法大概情况:
所有类,都有字段与方法,创建实例的内存中的方法指向一个方法表,表中对应
着每一个应执行的方法。
当虚方法继承后,子类要进行重写。子类同样对应方法表,除了父类的还有
子类本身的。对于已经重写的虚方法,后面会再嵌入一个Method Slot Table方法
槽表。
原本指向SayNationality(父类的方法)因为重写,会在后面槽表中记录重
写后的方法。因此,它不会按棕色箭头执行,再是按红色箭头执行方法槽表中的
方法。
没有重写的,则按前面的方法表对应进行执行。
因此,重写后由父类继承来的方法,若在子类用base指定方法(父类的)与用
this指定的(已经重写,此时this发生改变指向子类重写的),两者不再一样。
属性也可以重写(因为属性也方法set,get),故可以用虚属性。
虚方法可以给父类中的方法一个实现,比如ToString()方法
虚方法必须有实现部分,哪怕是空实现
2、练习
1)员工类、部门经理类(部门经理也是员工,所以要继承自员工类)。员工有上班
打卡的方法。用类来模拟。
internal class Program
{
private static void Main(string[] args)
{
Employee emp = new Manager() { Name = "黄勃" };
emp.Daka();
Console.ReadKey();
}
}
internal class Employee
{
public string Name { get; set; }
public virtual void Daka()
{
Console.WriteLine("9点打卡");
}
}
internal class Manager : Employee
{
public override void Daka()
{
Console.WriteLine("不用打卡");
}
}
2)把Person类中的SayHello改为虚方法让其默认为学生的,增加老师、司机类。
internal class Program
{
private static void Main(string[] args)
{
Person[] p = new Person[] { new Teacher(), new Driver() };
p[0].SayHello();
p[1].SayHello();
Console.ReadKey();
}
}
internal class Person
{
public virtual void SayHello()
{
Console.WriteLine("人类");
}
}
internal class Teacher : Person
{
public override void SayHello()
{
Console.WriteLine("教师");
}
}
internal class Driver : Person
{
public override void SayHello()
{
Console.WriteLine("司机");
}
}
3、虚方法和抽象方法的区别。
虚方法必须有实现,抽象方法必须没有实现;
抽象方法必须在抽象类中声明,虚方法可以出现在抽象类中;
抽象方法必须在子类中重写,虚方法可以被重写。