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

news2024/11/15 4:19:10

    
    
    
一、复习


    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方法还会判断字符串的内容是否相同,相同也为同一对象。
        
        
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/464635.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

electron+vue3全家桶+vite项目搭建【15】vue3+sass实现多主题一键切换,支持electron多窗口同步更新

文章目录 引入实现效果展示实现思路整理实现步骤1.定义全局主题样式变量2.定义主题模板3.封装颜色工具类4.初始化主题色5.主进程监听颜色修改6.补充主题状态管理7.主题一键切换组件8.测试案例 引入 我们之前在这篇文章中集成了 sass,接下来我们结合sass的变量定义&…

银行数字化转型导师坚鹏:宏观经济形势分析与银行发展模式创新

宏观经济形势分析与银行发展模式创新 课程背景: 很多学员存在以下问题: 不知道我国目前的宏观经济形势? 不清楚宏观环境对我国经济的影响? 不知道银行未来主要的发展模式? 课程特色: 精彩解读宏…

最新:机器学习在生态、环境经济学中的实践技术应用及论文写作

查看原文>>>最新:机器学习在生态、环境经济学中的实践技术应用及论文写作 目录 专题一、理论基础与软件介绍 专题二、数据的获取与整理 专题三、常用评价方法与相关软件详细教学(案例详解) 专题四、写作要点与案例的讲解 近年来…

Redis数据库常用语句

Redis数据库常用语句 前言1. 键(Key)的基本操作1.1 增加新的键值对1.2 访问键的值1.3 修改键值对1.4 键值对的删除1.5 判断键值对是否存在1.6 获取所有键1.7 删除所有的键: 2. Redis 中的列表2.1 列表加入新元素2.2 获取列表长度2.3 获取指定下标的元素2.4 获取指定…

Android App 架构 面试专题,你可能会被问到的 20 个问题

iveData 是否已经被弃用? 没有被弃用。在可以预见的未来也没有废弃的计划。 LiveData 可以使用简单的方式获取一个易于观察、状态安全的对象。虽然其缺少一些丰富的操作符,但是对于一些简单的 UI 业务场景已经足够。 Flow 有 LiveData 相同的功能,其…

1.栈的介绍-C语言调用函数(二)

1.栈的介绍-C语言调用函数(一)_双层小牛堡的博客-CSDN博客 接着上面 函数调用的约定 在栈帧中 主要的是主调函数如何存入实参 让被调用函数能够访问 这种是通过函数见的调用规定来规范的 并且 调用规定还规范了 函数执行完后应该由主函数实现 清除参…

[测试猿课堂]小白怎么学测试?史上最全《软件测试》学习路线

熬夜3天,联合3位猿计划教育的总监级授课老师,整理了这份《软件测试小白学习路线》,全文接近6000字,请大家耐心看完! 对于很多想通过自学转行软件测试的同学,痛点并不是学习动力,而是找不到清晰…

Apache SeaTunnel 3 分钟入门指南

简介 新一代分布式超高性能云原生数据同步工具 - Apache SeaTunnel 已经在B站、腾讯云、字节等数百家公司使用。 SeaTunnel 是 Apache 软件基金会下的一个高性能开源大数据集成工具,为数据集成场景提供灵活易用、易扩展并支持千亿级数据集成的解决方案。SeaTunnel …

《计算机网络--自顶向下方法》第三章--运输层

3.1概述和运输层服务 运输层协议为运行再不同主机上的应用进程之间提供了逻辑通信(logic communication)功能 运输层协议是在端系统中而不是在路由器中实现的 3.1.1运输层和网络层的关系 运输层协议至工作在端系统中 在端系统中,运输层…

基于Mybatis使用MySql存储过程,实现数据统计功能

1、前言 作为一个工作了很多年的程序员来说,没有在实际工作中真正使用过存储过程,其实对存储过程本身有过了解和学习,在日常的学习中,也会看过一些存储过程的相关介绍,不过“纸上得来终是浅”,正好这次做统…

Linux 利用网络同步时间

yum -y install ntp ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime ntpdate ntp1.aliyun.com 创建加入crontab echo "*/20 * * * * /usr/sbin/ntpdate -u ntp.api.bz >/dev/null &" >> /var/spool/cron/rootntp常用服务器 中国国家授…

力扣sql中等篇练习(十三)

力扣sql中等篇练习(十三) 1 每位学生的最高成绩 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 #先找到最大的元素 然后分组即可,不用管某些字段(grade)是不是聚合字段 SELECT e1.student_id,min(e1.course_id) course_id,e1.grade FROM Enrollment…

setup.py方式打包自己的python代码并可以用pip install安装

setup.py方式打包自己的python代码并可以用pip install安装 所需文件及目录规范示例演示引用自己打的包 所需文件及目录规范 注意setup.py文件和MANIFEST.in文件需要放在和你需要打包的目录同一级下,例如我这里需要打包的就是webconsole文件夹(这里webc…

gl-opendrive插件(车俩3D仿真模拟自动驾驶)

简介 本插件基于免费opendrive开源插件、Threejs和Webgl三维技术、vue前端框架,blender开源建模工具等进行二次开发。该插件由本人独立开发以及负责,目前处于demo阶段,功能还需待完善,由于开发仓促代码还需优化。 因此&#xff…

35岁测试人,面临职场危机,打了一场漂亮的翻身仗...

“夜深知雪重,时闻折竹声”。雪折,一种在雪的载荷下,植物(多指树)的躯干或枝条被不断堆积的雪花压断的现象。我的刚刚经历了人生的第一次“雪折”。 我是一个有点聪明且勤奋好学的人,从考入省重点大学起&a…

Windows环境下C++ 安装OpenSSL库 源码编译及使用(VS2019)

参考文章https://blog.csdn.net/xray2/article/details/120497146 之所以多次一举自己写多一篇文章,主要是因为原文内容还是不够详细。而且我安装的时候碰到额外的问题。 1.首先确认一下自己的代码是Win32的还是Win64的,我操作系统是64的,忘…

java websocket实现聊天室 附源码

目录 1.Socket基础知识 2.socket代码实现 2.1 引入依赖 2.2 配置websocket 2.3 websocket的使用 2.4 webSocket服务端模块 2.5 前端代码 3.测试发送消息 4.websocket源码地址 1.Socket基础知识 Socket(套接字)用于描述IP地址和端口&#xff0c…

4年测试工作经验,跳槽之后面试20余家公司的总结

先说一下自己的个人情况,普通二本计算机专业毕业,懂python,会写脚本,会selenium,会性能,然而离职后到今天都没有收到一份offer!一直在待业中,从离职第一天就开始准备简历&#xff0c…

【Vue 基础】尚品汇项目-02-路由组件的搭建

项目路由说明: 前端的路由:Key-Value键值对 Key:URL(地址栏中的路径) Value:相应的路由组件 作用:设定访问路径,并将路径和组件映射起来(就是用于局部刷新页面&#xff0…

Vue+Openlayers+proj4实现坐标系转换

场景 Vue中使用Openlayers加载Geoserver发布的TileWMS: Vue中使用Openlayers加载Geoserver发布的TileWMS_霸道流氓气质的博客-CSDN博客 在上面的基础上实现不同坐标系坐标数据的转换。 Openlayers中默认的坐标系是EPSG:900913 EPSG:900913等效于EPSG:3857 可在…