一、静态成员
1、方法重写注意事项
1)子类重写父类方法时,必须与父类保持一致的方法签名与返回值类型。即: 方
法名、返回值类型、参数列表都必须保持一致。[访问修饰符也得一致]
2)“方法签名”:一般是指方法的[名称] +方法的[参数列表],不包含方法返回值
类型。
3)基类可以有多个virtual虚方法,在子类中可以不实现重写,或者部分重写,或
者全部重写,或者在不同的下代子类中各自部分重写或不重写。
4)子类的override重写只能在父类中有abstract,virtual,override才能。
即,对于虚方法,它的子类、孙子类...可以一直重写下去,或不重写。
但是一旦用了new隐藏上代的方法,从此断子绝孙,不得再向下重写。
internal class Program
{
private static void Main(string[] args)
{
Z a = new A();
a.M1(); //1 爷
Console.WriteLine("----");
((A)a).M1();//2 父亲
Console.WriteLine("----");
((A)a).M2(); //3 父 爷
Console.WriteLine("----");
A b = new B();
b.M1(); //4
Console.WriteLine("----");
((A)b).M1();//5
Console.WriteLine("----");
Z c = new B();
b.M1();//6
Console.ReadKey();
}
}
internal class Z
{
public virtual void M1()//7
{
Console.WriteLine("爷爷类");
}
}
internal class A : Z
{
public new void M1()//8
{
Console.WriteLine("父亲类");
}
//public override void M1()//9
//{
// Console.WriteLine("重写的父亲类");
//}
public void M2()//10
{
this.M1();
base.M1();
}
}
internal class B : A
{
//public override void M1()//11
//{
// Console.WriteLine("儿子类");
//}
}
说明:
1处调用8处(隐藏Z类M1),无重写不能“父亲”,只能根据a调用Z的M1(爷),因其
可转A类,2处即为A类指针调用M1时为父。3处M2就显示了this与base的不同:父、爷。
4处调用实则是A类继承来的new M1,为父。同理,5、6处均为父。
若此时把11处注释去掉,则出错。因为上级A类中M1用了new,隐藏了Z类过来的
virtual,所以不再有虚方法继承到B类,B类也就无法override重写,从此断子绝孙。
若把11处、9处注释去掉,同时注释8处,这样是可以的。此时1处按虚方法重写
显示“重父”,2处直接调用自身本类方法显示“重父”,3处显示this与base的不同分别
是“重父”、“爷爷”。4处对A类中由Z而来的虚方法,由B再次重写,因此显示“儿子"。
同理5和6处都是调用B类自身的M1,显示“儿子"。
这种情况可以看出两点:
1)虚方法可以分别在不同的下代中重写(9处与11处)。
2)虚方法外表用“父类”,显示结果由本身实际重写类来决定。
1处,父类为Z,实际重写类是A,所以调用实际重写类的M1(重父).
4处,父类为A,实际重写类是B,所以调用实际重写类的M1(儿子).
3)重写反映:用父类对象调用子类对象的方法,其方法重写的“穿透”性,穿透
到子类(被重写)。若未被重写,只能在父类本类执行。
因此,把父类对象转为子类对象(a),说明是子类对象了,所以子类向下若
没有重写,那么此时子类对象(a)指向的对象就是A中的对象,否则,(a)就
继续重写下去指向B类的同名重写方法。
是不是有点晕了?
1)override不能单独出现,前面必须有virtual或abstact.
2)overrid与new不能同时修饰。
overrid是重写,原父类的虚方法在存在的,可以继续继承到子类。
new是隐藏,原父类的方法到此类时彻底隐藏封存,不能再继承,意同sealed.
注意此时类内仍可用base调用父类同名方法(this与base的同名方法不同)
3)virtual与new可以同时修饰。
表示有同名的virtual,但因new的关系,原父类的virtual到此结束(被隐藏),
新的virtual由new此处开始,因此后面继承的virtual是本类的虚方法,不再
是父类的虚方法。
没有晕的话看下面:
还是上面的例子:把8处方法注释掉。把9处、10处、11处方法去注释。
1处因没有重写,只能调用本类Z的方法M1(而不是子类A),显示"爷".
2处被转为本身类A类,当然本类A的方法M1,显示"重父".
3处实际调用本类A的this与base,故显示"重父","爷".
4处是重写显示“儿子”,5处与6处调用本类B的M1显示“儿子”。
2、静态成员
什么叫静态类?
平时遇到的都是非静态类,也称实例类。实例类前加static则变成静态类。
静态类时的所有成员都必须都是静态成员(加static)。
反过来,不是所有静态成员都写在静态类中。即非静态类中也可以拥有静态成员。
因此含有静态成员的类是静态类也可能是非静态类。
静态类与实例类的区别
实例属性:实例类中非静态属性。
该属性只能属于实例化后的属性,是属于具体的某一个对象。
静态成员是属于类的,而不是属于具体的某一个对象,故不能通过对象访问。
静态成员:通过类名访问;
实例成员:通过对象访问。
静态字段独立于任何实例,在使用之前就已经初始化(编译器完成时)
任何实例访问它都是同一内在地址,程序结束时释放该内存。
静态成员常用在哪些地方?
成员登记时,统计全部成员。这样所有实例都可以知道当前的总人数。
存款人员时,人名、金钱、电话不一样,但利息公用的可以考虑静态字段。
因此对于各个具体对象的公共的、共同访问、不具特异性的,考虑静态。
能否所有方法都写成静态方法?
不能!因为静态方法属于类,它不能访问到具体的对象,静态时具体对象就不能
调用本身的具体方法了。比如:计算具体存款用户的总金额。它需要访问到具体
对象的存款金钱及利率,前者无法用静态方法访问。
当然,如果该方法与具体对象没有关系,可以写成静态。
C#中声明的所有的变量都需要在类型中,不能在类型以外直接声明全局变量,与
c或c++不同。没有全局变量的概念。
实例类的构造函数能否私有化?private
可以!虽然私有化能不能直接调用构造函数创建对象。但是我们可能利用类的
静态成员在类内调用私有的构造函数达到创建对象的目的。
internal class Program
{
private static void Main(string[] args)
{
//Person p = new Person();//错误。私有构造函数不能调用。
Person p = Person.Create("康熙");
Console.ReadKey();
}
}
internal class Person
{
public string Name { get; set; }
private Person(string str)//私有构造函数,不能在类外调用。
{
this.Name = str;
}
public static Person Create(string str)
{
return new Person(str);
}
}
什么情况写成静态类?
静态类与实例类最大的区别是存储的不同:
静态类,只存储一份,在一个内在地址,所以可以直接用类名来访问。
实例化,每个对象都是一份,每份存储在不同内存地址。
所以一般公共的、不具有特异性的,不存储某一具体对象数据的,这些字段与方
法,归类写成静态类。比如工具类Math,所以一般静态类很少有字段与属性,
一般都是方法,调用起来特别方便快捷。例如:
Convert.ToInt32(); Math.Abs()
注意:静态类中只能包括静态成员。
静态成员只能访问外部的静态成员,内部可以定义局部变量。
在实例方法中可以直接调用静态方法,在静态方法中不可以直接调用实例方法
静态方法和静态变量创建后始终使用同一块内存(静态存储区,而使用实例的
方式会创建多个内存
少使用静态类,因为静态类、静态成员所分配的内存在程序退出时才会释放.
类与结构的区别
两者写法类似,但是,类是引用类型,在堆在。结构是值类型在栈上。
结构也可以有静态成员。
二、静态构造函数
1、类中的静态成员,在第一次使用类的时候静态成员就进行初始化了。
无论第一次类使用创建对象,还是使用静态成员,在这之前就会使用静态构造函数。
2、静态构造函数不能有参数、也不能有访问修饰符(public/private),默认是private。
1)静态构造函数在第一次使用该类的时候执行,只执行一次。
2)它由.Net自动调用,所以修饰符public/private对它而言,没有意义,反正自动
执行。
3)由于是自动调用,人为手工无法调用并加入参数,无法控制什么时候执行静态构造
函数,所以参数无法参与进来,不能有参数。
3、一个类最多只能有一个静态构造函数(多了无法人工控制重载)
4、无参构造函数与静态无参构造函数可以共存,因为一个属于类,一个属于实例.
静态构造函数不可以被继承。
注意:
如果没有写静态构造函数,而类中却包含带有初始值设定的静态成员,那么编译器会
自动生成默认的静态构造函数。(如果没有初始值设定,则没有)
internal class Program
{
private static void Main(string[] args)
{
Person p = new Person();//1
Person.Name = "雍正";
p.Show();//2
Console.ReadKey();
}
}
internal class Person
{
public static string Name="OK";//3
public Person()//4
{
Console.WriteLine("无参构造函数");
}
static Person() //5 不允许出现private/public等修饰符
{
Console.WriteLine("无参静态构造函数");
}
public void Show()//6
{
Console.WriteLine(Person.Name);//不能this.Name
}
}
说明:
在调用1处之前,执行静态构造函数(5处),然后才调用本身构造函数(4处),最后
由2处调用6处,6处中因为是静态成员所以只能由类名引出。
5、静态类为什么不能New?
查看静态类,发现它的类型变成了abstact和sealed,即是抽象与密封的。不能被实例
化和继承。
同时静态类,只会在类出现时执行一次初始化且整个程序只会执行一次。
而实例类则随时随地可以new,两者矛盾。
三、多态目的
1、什么是多态?
同一段代码,在不同的情况运行结果不一样。因为里面实质包含的对象不同,表现出
的(执行)情况就不一样。
多态就是指不同对象收到相同消息时,会产生不同行为,同一个类在不同的场合下表
现出不同的行为特征。
2、多态的作用是什么?
把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的
代码,做出通用的编程,以适应需求的不断变化。 多态: 为了程序的可扩展性
internal class Program
{
private static void Main(string[] args)
{
object o = new object();
Console.WriteLine(o.ToString());//1
Person person = new Person();
Console.WriteLine(person.ToString());//2
string s = "OK";
Console.WriteLine(s.ToString());//3
Console.ReadKey();
}
}
internal class Person
{
public void Show()
{
Console.WriteLine("k");
}
}
说明:
ToString()本身是虚方法,输出的是命名空间+类名。所以1和2处是命名空间+
类名。后面3处进行了重写(F12查看),返回的是this本身("OK").
public override string ToString()
{
return this;
}
开放封闭原则(对修改封闭,对扩展开放。)
3、里氏替换原则
父类引用指向子类对象
Person p=new Chinese();(隐式类型转换)
父类对象不能够替换子类
Chinese c=(Chinese)new Person();//错误
4、判断关系
is-a:可以用来验证继承关系中是否合理。 父子关系
can do,验证实现接口是否合理。 接口关系
if(obj is 类型A)//obj是父类类型对象,”类型A”是子类类型
关键字as (类型转换)、is(通常类型转换前需要通过is来判断一下类型
is类型转换 返回的是bool,成功true,失败false. 常用于判断中
as类型转换 成功返回对象,失败返回null. 常用于赋值语句。
因此,常用as进行高效转换。
internal class Program
{
private static void Main(string[] args)
{
Person p = new Teacher();
Teacher t = (Teacher)p;//正确
Person p1 = new Person();
//Teacher t1 = (Teacher)p1;//错误
if (p1 is Teacher)
{
Console.WriteLine("是教师类");
}
else
{
Console.WriteLine("不是教师类");
}
Teacher t2 = p as Teacher;
if (t2 == null)
{
Console.WriteLine("as转换失败");
}
else
{
Console.WriteLine("as转换成功");
}
Console.ReadKey();
}
}
internal class Person
{
public virtual void Show()
{
Console.WriteLine("人类");
}
}
internal class Teacher : Person
{
public override void Show()
{
Console.WriteLine("教师");
}
}
四、多态的方式
三种: virtual,abstract,interface
1、怎么实现多态1-抽象类abstract
抽象类不能被实例化(不能用new)。
抽象类中不一定必须有抽象成员(子类也就无法override).它还可以有普通成员。
抽象成员在父类中不能有实现(不能有方法体)
抽象成员必须包含在抽象类中。
2、抽象类存在的意义:
1)抽象类不能被实例化,只能被其他类继承,也就是为了多态。
2)继承抽象类的子类必须把抽象类中的所有抽象成员都重写 (实现)
(除非子类也是抽象类。)
3)抽象类就是为了重写->多态(代码重用)。
4)抽象类中可以有实例成员也可以有抽象成员
2、什么是抽象类(光说不做)
不能被实例化的类(不能new)抽象类的特点
技巧:
开发中尽量用抽象,不用具体;
能用父类,就不要用子类;
能用抽象的父类,就不要要用实例的父类;
能用接口就不要用抽象的类。
原则:尽量向上转移。
所以比较抽象类与虚方法,尽量用抽象类。除非父类必须实例化(要实现)这时
候就要用虚方法。例如,员工派生经理,CEO等,打卡这个事,因员工实例化,用虚
方法。
因此是否虚方法:1是否实例化;2是否行为要默认实现。
下面案例中,写一个方法中参数时,尽量用Person,不要用具体的子类比如student
与teacher.它可以屏蔽子类sttudent与teacher的差异化,实现多态。方法中的返回
值一样尽量用Person
3、案例: 学生类和老师类中抽象出父类(Person),并让学生和老师都要具有SayHello和
起立Standup两个方法
internal class Program
{
private static void Main(string[] args)
{
Person[] p = new Person[] { new Student(), new Teacher() };
p[0].SayHello();
p[1].StandUp();
Console.ReadKey();
}
internal abstract class Person
{
public abstract void SayHello();
public abstract void StandUp();
}
internal class Student : Person
{
public override void SayHello()
{
Console.WriteLine("学生说");
}
public override void StandUp()
{
Console.WriteLine("学生起立");
}
}
internal class Teacher : Person
{
public override void SayHello()
{
Console.WriteLine("教师说");
}
public override void StandUp()
{
Console.WriteLine("教师起立");
}
}
}
4、练习
练习1:动物Animal 都有吃Eat和叫Bark的方法,狗Dog和猫Cat叫的方法不一样.父类
中没有默认的实现所哟考虑用抽象方法。
internal class Program
{
private static void Main(string[] args)
{
Animal[] a = new Animal[] { new Dog(), new Cat() };
a[0].Eat();
a[1].Bark();
Console.ReadKey();
}
}
internal abstract class Animal
{
public abstract void Eat();
public abstract void Bark();
}
internal class Dog : Animal
{
public override void Bark()
{
Console.WriteLine("旺旺");
}
public override void Eat()
{
Console.WriteLine("抢骨头");
}
}
internal class Cat : Animal
{
public override void Bark()
{
Console.WriteLine("咪咪");
}
public override void Eat()
{
Console.WriteLine("抢鱼儿");
}
}
练习2: 计算形状Shape(圆Circle,矩形Rectangle ,正方形Square)的面积、周长
internal class Program
{
private static void Main(string[] args)
{
Shape[] s = new Shape[] { new Circle(3.0), new Rectangel(3, 4), new Square(4) };
Console.WriteLine(s[0].Area());
Console.WriteLine(s[1].Perimeter());
Console.ReadKey();
}
}
internal abstract class Shape
{
public abstract double Area();
public abstract double Perimeter();
}
internal class Circle : Shape
{
private double _r;
public double R
{
get { return _r; }
set
{
if (value < 0) value = 0;
_r = value;
}
}
public Circle(double r)
{
R = r;
}
public override double Area()
{
return Math.Round(Math.Pow(R, 2), 2);
}
public override double Perimeter()
{
return Math.Round(2 * Math.PI * R, 2);
}
}
internal class Rectangel : Shape
{
private double _height;
private double _width;
public double Height
{
get { return _height; }
set
{
if (value < 0) value = 0;
_height = value;
}
}
public double Width
{
get { return _width; }
set
{
if (value < 0) value = 0;
_width = value;
}
}
public Rectangel(double height, double width)
{
Height = height;
Width = width;
}
public override double Area()
{
return Math.Round(Height * Width, 2);
}
public override double Perimeter()
{
return Math.Round(2 * Height * Width, 2);
}
}
internal class Square : Shape
{
private double _side;
public double Side
{
get { return _side; }
set
{
if (value < 0) value = 0;
_side = value;
}
}
public Square(double side)
{
Side = _side;
}
public override double Area()
{
return Math.Round(Side * Side, 2);
}
public override double Perimeter()
{
return Math.Round(4 * Side, 2);
}
}
五、抽象类练习
1、要实现U盘、MP3播放器、移动硬盘三种移动存储设备,要求计算机能同这三种设备进
行数据交换,并且以后可能会有新的第三方的移动存储设备,所以计算机必须有扩展
性,能与目前未知而以后可能会出现的存储设备进行数据交换。各个存储设备间读、
写的实现方法不同,U盘和移动硬盘只有这两个方法,MP3Player还有一个PlayMusic
方法
分析:计算机类有属性抽象设备(来自U盘,MP3,移动硬盘,抽象出读写方法)
计算机类写与读时,用抽象类中的读写在子类(U盘,MP3,移动硬盘)中具体实现。
internal class Program
{
private static void Main(string[] args)
{
MobileDev[] dev = new MobileDev[] { new MP3(), new MobileDisk(), new UDisk() };
Computer c = new Computer(dev[0]);
c.Read();
c.Dev = dev[2];
c.Write();
c.Dev = new MobileDisk();
c.Read();
Console.ReadKey();
}
}
internal abstract class MobileDev
{
public abstract void Read();
public abstract void Write();
}
internal class UDisk : MobileDev
{
public override void Read()
{
Console.WriteLine("U盘读取...");
}
public override void Write()
{
Console.WriteLine("U盘写入...");
}
}
internal class MP3 : MobileDev
{
public override void Read()
{
Console.WriteLine("MP3读取...");
}
public override void Write()
{
Console.WriteLine("MP3写入...");
}
public void PlayMusic()
{
Console.WriteLine("Mp3播放音乐...");
}
}
internal class MobileDisk : MobileDev
{
public override void Read()
{
Console.WriteLine("移动硬盘读取...");
}
public override void Write()
{
Console.WriteLine("移动硬盘写入...");
}
}
internal class Computer
{
public MobileDev Dev { get; set; }
public Computer(MobileDev dev)
{
this.Dev = dev;
}
public void Write()
{
this.Dev.Write();
}
public void Read()
{
this.Dev.Read();
}
}
2、橡皮鸭子(RubberDuck)、真实的鸭子(RealDuck)。两个鸭子都会游泳,而橡皮鸭子和
真实的鸭子都会叫,只是叫声不一样,橡皮鸭子“唧唧”叫,真实地鸭子“嘎嘎”叫
internal class Program
{
private static void Main(string[] args)
{
Duck[] d = new Duck[] { new RubberDuck(), new RealDuck() };
d[0].Swim();
d[1].Bark();
Console.ReadKey();
}
}
public abstract class Duck
{
public void Swim()
{
Console.WriteLine("鸭子水上漂...");
}
public abstract void Bark();
}
internal class RubberDuck : Duck
{
public override void Bark()
{
Console.WriteLine("唧唧唧...");
}
}
internal class RealDuck : Duck
{
public override void Bark()
{
Console.WriteLine("嘎嘎嘎...");
}
}
3、抽象类中的new
new在类中方法中主要用于隐藏父类同名方法,且不能再继续继承下去。如果用了new
那么,抽象类中的抽象方法无法在子类中实现,也无法继承下去实现。会出错。
如果一个方法在子类中被重写
Duck duck = new RubberDuck():
duck.Bark()
调用子类的方法,因为被重写抽象方法子类必须重写,所以不能用new.
注意:
使用第三方dll的时候,原来没有SayHi方法,自己继承后加了个SayHi()。后来
第三方dll更新,也加了个SayHi()。继承后的类中现在就得用new了。以隐藏dll中
的同名SayHi(),保留现在使用的SayHi().
六、案例:面向对象计算器
前面做过计算器,有一个缺点:每增加一个新运算符,就必须打开源代码进行修改。
1、现在尝试进行优化,设计思路是:
最开始只有加减,然后给它扩展乘除法,或者添加新的其它运算法。要求扩展或新
添加运算符时,不能修改原来的源代码。那么应该怎么做呢?
2、分析:
计算器变化的是运算符,或者说运算(计算)方法,扩展也就是这一部分。
封装,就是要封闭变化。把变化的地方抽象出来,进行封装,以便多态。
对于变化,比如人有多种:中国人,美国人,英国人等等,人老是在变。这时可以
用一个父来“人”来表示,然后给它赋值为不同的子类对象(中国人,美国人等),
这样就把变化封装起来了,无论什么人来都可以处理。
同样,计算器不断变化的是运算符,现在加减,新添加乘除,后面说不定哪天还要
乘方开方,变化种类很多,一堆运算等着要用添加...不能老是打开源代码,然后
在源码中添加。
因此我们把运算符(作父类)进行封装,在子类实现不同的具体的+-*/等运算
符。
3、为了统一管理存储新的方案,来管理多个项目,可以新建一个新的解决方案文件夹:
右击解决方案->添加->新建解决方案文件夹(重命名为“计算器”)
4、对新建的"计算器"文件夹右击->添加->新建项目->c#类库(命名为CalculatorDll)
(vs2022添加新项目的弹出窗体中,右侧最上填入“类库”进行筛选,第二排最右侧中
选择“所有项目类型”。这样所有含"类库"的项就出来了,最上面就是C#类库,选择它
即可。类库框架选择6.0)
类库是一个程序集(dll),本身不能直接运行的,只能靠别的程序来调用它。
这样解决方案下有一个文件夹"计算器",里面有一个项目(类库)Calculator,展开它
里面有一个默认的Class1.cs文件,重命名为Calculator.cs,会提示是否更改类名,
选择是,这样源代码中的类名也同cs文件保持一致的名称,便于识别。
5、这个Calculator就是变化的运算符抽象父类。抽象便于后面子类变化。里面运算为
抽象,带着两个参与数。
namespace CalculatorDll
{
public abstract class Calculator
{
public double Number1 { get; set; }
public double Number2 { get; set; }
public Calculator()
{
}
public Calculator(double d1, double d2)
{
this.Number1 = d1;
this.Number2 = d2;
}
public abstract double JiSuan();
}
}
6、按照题意,还应该有两个方法:加法、减法。分别用两个类来进行重写:
右击CalculatorDll项目,添加一个加法类:JaFaClass.cs
注意里面修饰符要改成public,以便这个dll被其它程序集使用。
namespace CalculatorDll
{
public class JiaFaClass : Calculator
{
public JiaFaClass()
{
}
public JiaFaClass(double d1, double d2) : base(d1, d2)
{
}
public override double JiSuan()
{
return Number1 + Number2;
}
}
}
同样,再添加一个减法类:JianFaClass.cs
namespace CalculatorDll
{
public class JianFaClass : Calculator
{
public JianFaClass()
{
}
public JianFaClass(double d1, double d2) : base(d1, d2)
{
}
public override double JiSuan()
{
return Number1 - Number2;
}
}
}
7、下面进行测试上面dll是否正确。
右击"计算器"文件夹,添加一个新项目:控制台应用程序(注意是6.0版本,选中那
个不使用顶级语句,命名"CalTest"项目)
为了使用运算符,应先引用另一项目中的Calculator类。
因此,右击本项目CalTest->添加->项目引用,在窗体左侧选择项目,复选
CalculatorDll。在代码首端添加命名空间:using CalculatorDll;
由于原dll只有加减,现在添加一个乘法类:ChengFaClass.cs,一同参加测试
namespace CalTest
{
internal class ChengFaClass : CalculatorDll.Calculator
{
public ChengFaClass()
{
}
public ChengFaClass(double d1, double d2) : base(d1, d2)
{
}
public override double JiSuan()
{
return this.Number1 * this.Number2;
}
}
}
主测试程序:
using CalculatorDll;
namespace CalTest
{
internal class Program
{
private static void Main(string[] args)
{
Console.WriteLine("请输入第一个数:");
double d1 = Convert.ToDouble(Console.ReadLine());
Console.WriteLine("输入运算符:");
string op = Console.ReadLine();
Console.WriteLine("请输入第二个数:");
double d2 = Convert.ToDouble(Console.ReadLine());
Calculator c = null;
switch (op)
{
case "+":
c = new JiaFaClass(d1, d2);
break;
case "-":
c = new JianFaClass(d1, d2);
break;
case "*":
c = new ChengFaClass(d1, d2);
break;
default:
break;
}
if (c != null)
{
Console.WriteLine(c.JiSuan());
}
else
{
Console.WriteLine("没有该运算符!");
}
Console.ReadKey();
}
}
}
说明:
上面原来的代码dll没有改变,在新程序中进行添加和扩展新的运算符时,只须重新
加一个即可,不影响原来的dll.
8、也可以改良一下上面的测试代码:
用简单工厂模式,把里面的转换包装成方法。
using CalculatorDll;
namespace CalTest
{
internal class Program
{
private static void Main(string[] args)
{
Console.WriteLine("请输入第一个数:");
double d1 = Convert.ToDouble(Console.ReadLine());
Console.WriteLine("输入运算符:");
string op = Console.ReadLine();
Console.WriteLine("请输入第二个数:");
double d2 = Convert.ToDouble(Console.ReadLine());
Calculator c = GetObject(op, d1, d2);
if (c != null)
{
Console.WriteLine(c.JiSuan());
}
else
{
Console.WriteLine("没有该运算符!");
}
Console.ReadKey();
}
public static Calculator GetObject(string op, double d1, double d2)
{
Calculator result = null;
switch (op)
{
case "+":
result = new JiaFaClass(d1, d2);
break;
case "-":
result = new JianFaClass(d1, d2);
break;
case "*":
result = new ChengFaClass(d1, d2);
break;
default:
break;
}
return result;
}
}
}
上面GetObject(op, d1, d2)方法就是简单工厂设计模式。它相当一个工厂,无论你
想什么材料(加,减等,动态不同的子类),都给你加工成一个统一的父类产品Calculator
出来。而在使用这个产品时,又因重载,调用同一个JiSuan,出现不同的结果(多态)。
设计模式实质是对各种特性的巧妙使用技巧,若没有这些特性,如里氏转换,多态等
也就无从谈设计模式。
9、为什么在转换里面还要重写一下?
目前只能每增加一下用一这个转换方法。后面也可以写成活的,要用反射。
10、设计模式 (GOF23种设计模式)
世上本没路,走的人多了也就成了路;
设计本没模式,程序写多了也就有了模式;
总结前人的思想,总结出的解决某一类问题的通用方法;
上面的计算器就是设计模式中简单工厂设计模式
各种设计模式的本质都是: 多态。
充分理解了多态,再看设计模式就会觉得轻松很多
11、小结
使用面向对象的方式实现+、-、*、/的计算器流程:
1)找对象。
2)抽象出父类,让子类都有计算能力。
3)实现子类。
4)产生子类对象的工厂。
5)用哪部分是可能扩展的就尝试将该部分抽象。
注意:封装变化,将变化的地方抽象出来,以便多态。
七、值类型与引用类型
1、什么是值类型?什么是引用类型?
1)值类型有:
int,char,double,float,long,short,byte,bool,num,struct,decimal等等
它们类型的大小值是固定的,int四个字节,char占2个,double8,float4,long8,
short2,byte1,bool1,enum4,struct,decimal16
可以看到decimal占16是比较费内存的,其中struct根据内部计算(同时还有对齐
的情况,按2字节或4字节对齐时统计时,内存可能有浪费情况)
值类型均隐式派生自System.ValueType
值类型不能继承,只能实现接口。
栈是动态的,是连接存储的空间。
堆是不连接的大块存储空间,所以必须有回收GC机制,不然浪费内存。
由于大小固定所以存储时都是在栈上,这样对确定它们的存储位置,便于连续排版
,也方便定位。
而引用类型大小不固定,无法连续排版,也许下一时刻引用类型大小变大,也许变
小。所以只有把引用类型存储在另一单独的空间(堆)上,这块空间无限大,但不
能保证是连续的,所以查找起来比较慢。所以引用类型实质存储的是这个类型存储
空间的一个内存地址。
2)引用类型有:
string、数组、类(自定义数据类型)、接口、委托。
int[] n={1,2,33://引用类型。
引用类型都派生自: Object
引用类型可以继承(类之间可以继承)
2、赋值情况
栈中的内容进行数据拷贝的时候都是复制了一个数据的副本(将数据又拷贝一份)
引用类型变量的赋值,是将栈中的的地址拷贝了一个副本。
internal class Program
{
private static void Main(string[] args)
{
int n = 100;
M1(n);
Console.WriteLine(n);//1
int[] a = new int[] { 5, 6, 7 };
M2(a);
Console.WriteLine(a[0]);//2
int[] b = new int[] { 5, 6, 7 };
M3(b);
Console.WriteLine(b[0]);//3
Person zf = new Person();
zf.Name = "张飞";
M4(zf);
Console.WriteLine(zf.Name);//4
Person zdd = new Person();
zdd.Name = "郑丹丹";
M5(zdd);
Console.WriteLine(zdd.Name);//5
Console.ReadKey();
}
private static void M1(int n)
{
n += 1;
}
private static void M2(int[] a)
{
for (int i = 0; i < a.Length; i++)
{
a[i] *= 2;
}
}
private static void M3(int[] b1)
{
b1 = new int[] { 9, 10, 11 };
}
private static void M4(Person p)
{
Person px = new Person();
px.Name = "刘备";
p = px;
}
private static void M5(Person p1)
{
p1.Name = "苏坤";
p1 = new Person();
p1.Name = "许正龙";
}
}
internal class Person
{
public string Name { get; set; }
}
说明:
1处因为是值类赋值是栈上副本,不影响原值,为100。
2处是引用类型,传参仍然指向的是原堆中地址,所以为10。
3处是引用类型,开始b与b1的指向地址一样,但是M3中把新分配的地址分配给
了传参b1,此时传参地址b1与原堆中b不一致。所以不影响原b,为5.
因此:b指向堆中的原来的5,6,7,而b1指向堆中的是9,10,11。
提示:目前方法传参都是把实参"值传递"给形参,所以b和b1都在栈上,它们
分别在不同内存地址上,进行了值拷贝。其内保存着堆中的内存地址。
3处一定理解栈中传参值传递过程。实际是一个入栈出栈问题,可百度.
引用传递见第9部分。
如果是用ref引用传递,相当于把b1与b进行捆绑指向同一地址(可栈可堆)
改变任意b或b1都会一起更改,所以用ref时b1与b始终指向同一地址。这
大概就是双宿双飞的夫妻吧。
4处同3处一样,开始zf与p在栈中不同地址,但存储指向同一堆中地址。但后
面p变化指向另一px,不影响原来的zf指向,zf结果为张飞。
5处传参时zdd与p1在栈上不同地址,但指向堆中同一地址为郑丹丹,更名为苏
坤后,两者均变为苏坤。后面p1被更改为堆中新的地址(许正龙),所以不
再影响zdd指向的“苏坤”。结果为苏坤。
八、引用传递ref
1、引用传递:传递是栈本身内存的地址;同一变量的两个别名。
值传递:传递的是栈中内容的副本。
private static void Main(string[] args)
{
int m = 100;
M1(ref m);
Console.WriteLine(m);//101
Console.ReadKey();
}
private static void M1(ref int n)
{
n += 1;
}
说明:
实际上m和n都指向栈中同一块内存地址,m与n相当于这个变量的两个别名。
如同“曹操"与"小曹"大名与小名都指同个人。故改变其一另一指向随同变化。
对于ref值传递,调用方法时会创建指针,然后入栈,执行完后出栈,不会
提高值传递的效率。同样ref引用传递时,本身引用传递就是指针,ref还要进
行一次解引用,影响速度。加上ref会对编译器优化造成干扰,本可以使用内联
的函数可能因ref而放弃内联。所以用ref并不会提高性能,加上ref可能造成数
据被修改的风险,非必要时不要使用ref。
2、ref引用传递
internal class Program
{
private static void Main(string[] args)
{
Person p1 = new Person();
p1.Name = "黄林";
M2(ref p1);
Console.WriteLine(p1.Name);//1
Person my = new Person();
my.Name = "马毅";
M3(ref my);
Console.WriteLine(my.Name);//2
Console.ReadKey();
}
private static void M3(ref Person p)
{
p.Name = "石国庆";
}
private static void M2(ref Person p2)
{
p2 = new Person();
p2.Name = "许正龙";
}
}
internal class Person
{
public string Name { get; set; }
}
说明:
1处,因ref传参,栈上同一变量的两个别名p1与p2,存储指向堆中黄林的内存
地址0x550。p2新创建对象后的内存地址变化,则栈上的0x550被更改为新的0x558
存储“许正龙”。而此时p1也是通过栈上0x123里存储的0x558指向对象“许正龙”。所
以结果是许正龙。2处同理分析为石国庆。
结论:
对于ref经典的说法:同一个变量的两个别名。
若要画图进而涉及指针时的指针,没必须这样去详究。
傻瓜记法,ref的两个别名就是一个变量。两个别名同甘共苦,双宿双飞。