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

    
    
    
一、复习


    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.mfbz.cn/a/15442.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Java基础 1】Java 环境搭建

🍊 欢迎加入社区,寒冬更应该抱团学习:Java社区 📆 最近更新:2023年4月22日 文章目录 1 java发展史及特点1.1 发展史1.2 Java 特点1.2.1 可以做什么?1.2.2 特性 2 Java 跨平台原理2.1 两种核心机制2.2 JVM…

阳光开朗孔乙己,会否奔向大泽乡

前言 🔥学历对职业关系到底有什么影响呢?🔥学历给我们带来了优势吗?🔥到底是什么造成了"孔乙己的长衫"? 孔乙己是中国清代作家鲁迅创作的一篇短篇小说,发表于1919年。这部作品被认为是…

跌倒检测和识别2:YOLOv5实现跌倒检测(含跌倒检测数据集和训练代码)

跌倒检测和识别2:YOLOv5实现跌倒检测(含跌倒检测数据集和训练代码) 目录 跌倒检测和识别2:YOLOv5实现跌倒检测(含跌倒检测数据集和训练代码) 1. 前言 2. 跌倒检测数据集说明 (1)跌倒检测数据集 (2)自定…

初学Python来用它制作一个简单的界面

前言 很多刚开始学习python的宝子,就想着自己开始琢磨一些界面,但是吧很多都是有点难度的,自己又琢磨不透,只能把代码复制粘贴运行 现在就带你们来了解一个制作简单界面的代码 ttkbootstrap 是一个基于 tkinter 的界面美化库&am…

Spring RabbitMQ 实现消息队列延迟

1.概述 要实现RabbitMQ的消息队列延迟功能,一般采用官方提供的 rabbitmq_delayed_message_exchange插件。但RabbitMQ版本必须是3.5.8以上才支持该插件,否则得用其死信队列功能。 2.安装RabbitMQ延迟插件 检查插件 使用rabbitmq-plugins list命令用于查看…

workerman开发者必须知道的几个问题

1、windows环境限制 windows系统下workerman单个进程仅支持200个连接。 windows系统下无法使用count参数设置多进程。 windows系统下无法使用status、stop、reload、restart等命令。 windows系统下无法守护进程,cmd窗口关掉后服务即停止。 windows系统下无法在一个…

appuploader 常规使用登录方法

转载:登录appuploader 登录appuploader 常规使用登录方法 双击appuploader.exe 启动appuploader 点击底部的未登录,弹出登录框 在登录框内输入apple开发者账号 如果没有apple开发者账号,只是普通的apple账号,请勾选上未支付688…

本地运行 minigpt-4

1.环境部署 参考官方自带的README.MD,如果不想看官方的,也可参考MiniGPT-4|开源免费可本地进行图像对话交互的国产高级大语言增强视觉语言理解模型安装部署教程 - openAI 当然,所有的都要按照作者说明来,特别是版本号…

什么是3D渲染,3D渲染在CG项目中为何如此重要?

随着科技的发展,现如今任何人都可以使用免费软件在个人计算机上创作 3D 图像,当然也有人对于专业 3D 艺术的创作方式及其相关工作流程存在一些误解,认为创建一个模型后,在上面放上材料和纹理,就可以立马得到一个漂亮的…

SpringCloud源码之OpenFeign

OpenFeign 基于 OpenFeign 2.2.6.RELEASE版本进行源码阅读 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId><version>2.2.6.RELEASE</version> </dependen…

【细读Spring Boot源码】监听器合集-持续更新中

前言 监听器汇总 归属监听器名称作用cloudBootstrapApplicationListenercloudLoggingSystemShutdownListenercloudRestartListenercloudLoggingSystemShutdownListenerspringbootEnvironmentPostProcessorApplicationListener用于触发在spring.factories文件中注册的Environm…

市级大数据中心大数据资源平台概要设计方案(ppt可编辑)

本资料来源公开网络&#xff0c;仅供个人学习&#xff0c;请勿商用&#xff0c;如有侵权请联系删除。 大数据管理中心发展背景 为建设卓越全球城市&#xff0c;实现政府治理能力现代化目标&#xff0c;由市大数据中心牵头&#xff0c;在政务公共数据管理和互联网政务服务方面…

numpy的下载、数据类型、属性、数组创建

下载numpy 因为numpy不依赖于任何一个包所以numpy可以直接使用pip命令直接下载 下载命令&#xff1a; pip install numpy # 默认从https://pypi.org/simple 下载 pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple/ # 从清华大学资源站点下载 pip install nump…

UG NX二次开发(C#)-显示-更改对象颜色

文章目录 1、前言2、UG NX中的更换对象颜色的功能3、采用UG NX二次开发实现颜色修改3.1 采用直接赋值对象颜色不能直接更改对象颜色3.2 采用NewDisplayModification的方法如下:1、前言 当一个三维模型展现在我们面前时,总会有颜色赋予三维模型的对象上,比如红色、蓝色、银灰…

if条件语句

if条件语句 条件测试 test 测试表达式是否成立&#xff0c;若成立返回0&#xff0c;否则返回其他数值 格式1 &#xff1a;test 条件表达式&#xff1b;格式2 &#xff1a;[ 条件表达式 ] echo $?参数作用-d测试是否为目录 (Directory)-e测试目录或文件是否存在(Exist)-f测…

直线导轨水平仪零位调整方法

对于直线导轨的使用&#xff0c;相信很多人都知道&#xff0c;这主要是因为直线导轨的使用范围非常广泛&#xff0c;小到抽屉&#xff0c;大到机械设备&#xff0c;我们都能看到他的身影&#xff0c;接触得多自然就熟悉了。 事实上&#xff0c;大家对直线导轨的了解可能就仅限于…

Cortex-A7中断详解(一)

STM32中断系统回顾 中断向量表NVIC&#xff08;内嵌向量中断控制器&#xff09;中断使能中断服务函数 中断向量表 中断向量表是一个表&#xff0c;表里面存放的是中断向量。 中断服务程序的入口地址或存放中断服务程序的首地址成为中断向量&#xff0c;因此中断向量表是一系…

【Linux入门】linux指令(1)

【Linux入门】linux指令&#xff08;1&#xff09; 目录 【Linux入门】linux指令&#xff08;1&#xff09;操作系统登录服务器Linux下的基本指令ls指令pwd指令Linux路径分割符 /cd指令touch指令mkdir指令&#xff08;重要&#xff09;rmdir指令&&rm指令&#xff08;重…

linux实现网络程序

1️⃣ 在linux下&#xff0c;通过套接字实现服务器和客户端的通信。 2️⃣ 实现单线程、多线程通信。或者实现线程池来通信。 3️⃣ 优化通信&#xff0c;增加守护进程。 有情提醒&#xff0c;类里面默认的函数是内联。内联函数在调用的地方展开&#xff0c;没有函数地址&…

Mac使用命令行工具解压和压缩rar文件

目前在Mac电脑里支持解压缩的格式主要有&#xff1a;zip、gz等&#xff0c;但是还不支持rar格式的文件&#xff0c;接下来带着大家学习一下如何解压缩rar格式文件。 1.下载rar工具 打开&#xff1a;https://www.rarlab.com/download.htm 根据自己电脑的芯片要求选择自己的安装…
最新文章