> 文章列表 > (3)基础强化:静态类静态成员,静态构造函数,抽象类抽象成员,值类型和引用类型,Ref

(3)基础强化:静态类静态成员,静态构造函数,抽象类抽象成员,值类型和引用类型,Ref

(3)基础强化:静态类静态成员,静态构造函数,抽象类抽象成员,值类型和引用类型,Ref

    
    
一、静态成员

    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();  //4Console.WriteLine("----");((A)b).M1();//5Console.WriteLine("----");Z c = new B();b.M1();//6Console.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();//1Person.Name = "雍正";p.Show();//2Console.ReadKey();}}internal class Person{public static string Name="OK";//3public 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());//1Person person = new Person();Console.WriteLine(person.ToString());//2string s = "OK";Console.WriteLine(s.ToString());//3Console.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);//1int[] a = new int[] { 5, 6, 7 };M2(a);Console.WriteLine(a[0]);//2int[] b = new int[] { 5, 6, 7 };M3(b);Console.WriteLine(b[0]);//3Person zf = new Person();zf.Name = "张飞";M4(zf);Console.WriteLine(zf.Name);//4Person zdd = new Person();zdd.Name = "郑丹丹";M5(zdd);Console.WriteLine(zdd.Name);//5Console.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);//101Console.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);//1Person my = new Person();my.Name = "马毅";M3(ref my);Console.WriteLine(my.Name);//2Console.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的两个别名就是一个变量。两个别名同甘共苦,双宿双飞。