• C#方法


        OOP的类型其实可以用这样的等式:数据 + 方法,数据决定类的属性,方法决定类的行为。方法在类型设计中至关重要,因为它决定了该类的功能。

        C#的方法除了拥有一般OOP语言都会有的构造器方法外,还具有C++的操作符重载方法,而且它本身也有自己的特有方法:转换操作符方法,扩展方法和分部方法。

        我们先来了解一下构造器方法。

    1.构造器方法

         C#的构造器和java是一样的,主要的作用就是初始化成员变量,就连构造器的加载顺序也是一样,只是有些地方的说法不同。

         我们都知道,抽象类的默认构造器访问权限是protected,而一般类都是public(记住,是默认构造器,如果是自定义构造器,请一定要写上public)。类的实例构造器在访问从基类继承来的字段前,必须调用基类的构造器来初始化这些字段。若派生类没有显式的调用基类的构造器,编译器也会自动生成对基类构造器的调用,这时就要求基类要有默认构造器。因为这个原因,所以我们千万不要在基类构造器中调用会影响所构造对象的任何虚方法,因为如果该方法在当前要实例化的派生类中进行了覆写,就会调用派生类的方法,但这时派生类的字段还没有被初始化(因为派生类的构造器还没有被调用),这就会出现问题。

         我们经常用内联(inline)的方式来初始化实例字段,所谓的内联其实就是直接赋值,因为在存储中是嵌入到该对象的内存中,所以说是内联。它们真正的初始化也是发生在实例构造器被调用的时候,编译器会把它们作为构造器中的代码执行。

         有时候,我们构造器可能会有很多重载版本(可能就是为了修改字段),这时最好的方法就不是用内联的方式初始化字段,而是放在一个默认构造器中,然后让其他构造器调用它,这样就可以减少代码量(当然,我们看不到它们,但是编译器会将这些字段的初始化动作都放在每个构造器中!)。像是这样:

    class People{
        private String _Name;
    
        People(){
           this._Name = "";
        }
    
       public People(String name) : this(){
           _Name = "男人";
       }
    }

         调用默认构造器的方法和java一样,都是this(),但位置不一样。

         值类型也有自己的构造器,就是结构。我很不想说结构,因为我认为我不会经常使用结构,但还是必须说明一下。C#不允许我们为值类型声明一个默认构造器,因为编译器不会为值类型自动调用构造器,除非我们显式的调用构造器。

    struct Pratice
        {
            private String _Name;
    
            public Pratice(String name)
            {
                this._Name = name;
            }
        }
        class Program
        {
            public static void Main(String[] args)
            {
                Pratice pratice = new Pratice("");
            }
        }

          因为结构不能有默认构造器,所以我们不能在结构中用内联的方式初始化字段(但静态字段可以)。但访问结构的字段前,我们必须保证字段都被正确初始化,所以我们需要自定义构造器来初始化这些字段。这样我们会很烦,因为有时候,我们只想要在结构中存储一些值类型,至于它的值,默认值就可以了,但我们还是要一一初始化。幸好我们还是有一个方法可以避免这种事情发生:

     struct Pratice
        {
            private String _Name;
    
            public Pratice(String name)
            {
                this = new Pratice();
            }
        }

          this表示结构的实例,使用new就可以自动价格字段初始化为默认值。但这种方法不适合类,因为this引用默认是只读的,不能被赋值。

          我们上面讨论的是实例构造器,C#还有类型构造器(type constructor),即静态构造器。使用关键字static,可应用于除了接口以外的类型,作用就是设置类型的初始化状态。实例构造器是佘竹实例的初始化状态,而类型构造器则是设置类型的初始化状态,这两者有何区别呢?答案已经从名字中看出来了,类型构造器用于初始化静态字段。一个类型,只有一个类型构造器,并且在类被首次访问时,先执行类型构造器,而且只执行一次(java的静态域同样是在类被首次加载的时候初始化,这是同样的道理)。类型构造器没有参数,默认是private,而且不能有任何修饰符,就算我们想要显式的指定private也不行。这样做就是为了防止任何程序员调用它,CLR会想尽所有办法来阻止这种事发生。值类型虽然也能定义静态构造器,但最好不要这样做,因为CLR无法保证它们一定会被调用,最普遍的情况就是永远都不会被调用。

          CLR保证类型构造器是线程安全的,所以它也很适合用于初始化单例对象(想知道什么是单例对象,请看设计模式---单例模式 )。单个线程中,绝对不能让两个类型构造器互相引用,那样会出现问题,有可能另一个构造器还没有执行完毕的时候就已经被人调用了。类型构造器最好不要调用基类的构造器,因为几乎不可能会从基类中继承静态字段。

          类型构造器对编译器来说,是个不小的麻烦,因为它除了决定是否要生成代码来调用它(大部分的程序很少会有类型构造器),还要决定应该将这个调用添加到哪个位置。有两种可能:

         1.编译器可以在创建类型的第一个实例前,或者在访问类的一个非继承字段或成员前生成这个调用。这种行为就称为精确(precise)语义,因为调用时机恰到好处。

         2.编译器可以在首次访问一个静态字段或者一个静态/实例方法前,或者在调用一个实例构造器前,随便找一个时间生成调用。这称为字段初始化前(before-field-init)语义,CLR只保证访问成员前会运行静态构造器,至于时间并不确定。这是首选的方式,因为这样CLR的速度能够更快。

         选择哪种方式并不是完全交给编译器,我们也可以由这种选择权。如果类中没有显式构造器,就采用字段初始化前语义。为什么显式的构造器需要精确语义呢?没有用构造器进行初始化的一般是静态字段,什么时候初始化都没有问题,但显式构造器可能包含具有副作用的代码(所谓的副作用,就像线程安全,异常等这些执行代码可能出现的附加状态),需要编译器精确的调用。

    2.操作符重载方法和转换操作符方法

          C#中的操作符并没有什么特别的东西,但有一个必须注意:^不是用来求幂,而是异或(XOR),C#的数学操作都已经封装成静态方法,求幂可以用Math.Pow()。重点肯定不是这个,而是操作符重载方法。

          C++就有操作符重载方法,这样我们就能将操作符应用于自定义类型,而java是没有的(其实java认为,对类型的操作是对字段的操作,所以它鼓励我们更多把重心放在对字段的操作上,像是获取/设置字段值等方法)。CLR规范要求我们把操作符重载方法设为public static,对于参数,C#要求至少有一个参数的类型与当前定义这个方法的类型相同。这是很重要的,毕竟我们会选择在该类型中重载该方法,就是因为我们想要对该类型的字段进行操作,而且也方便编译器快速找到要绑定的方法。

         一般的操作符重载方法像是这样:

     public sealed class People
        {
            private int number = 5;
            public static int operator +(People p1, People p2)
            {
                return p1.number + p2.number;
            }
        }
        class Program
        {
            public static void Main(String[] args)
            {
                People p1 = new People();
                People p2 = new People();
               Console.WriteLine(p1 + p2);
            }
        }

          操作符重载方法在C++中非常常见,因为我们经常需要覆写"="的操作符重载方法和拷贝构造函数(深复制和浅复制的问题)。但接下来就是新的东西:转换操作符方法。
         转换操作符设计到类型转换,是把一个类型转换为截然不同的类型。为什么要这样做呢?这是我们的第一个疑问,毕竟语言本身就提供了类型转换机制,而且相对安全。理由非常简单,像是XML文件的恶操作,就使得我们经常需要这样做,而且就算是数值类型,我们也会自定义一个有理数的类型来包含其他值类型,也需要与其他值类型的转换功能。

          我们来看一个例子:

     public sealed class People
        {
            private int number = 5;
            public People(int number)
            {
                this.number = number;
            }
    
            public int ToInt32()
            {
                return this.number;
            }
            
            //有一个int隐式构造并返回一个People
    public static implicit operator People(int number) { return new People(number); }
    //由一个People显式返回一个int
    public static explicit operator Int32(People people) { return people.ToInt32(); } } class Program { public static void Main(String[] args) { People people = 5; int number = (int)people; Console.WriteLine(number); } }

          现在不仅能将5赋值给People,而且还能将People转型为int!
          要实现这样的功能,我们需要一个构造器,该构造器接受想要转换的目标类型作为参数,接着是一个ToXXX()方法,该方法能够返回目标类型,然后就是重点戏:

          实现显式转换和隐式转换的方法,它们内部调用的就是我们刚才定义的方法。这就是转换操作符方法,implicit关键字告诉编译器生成代码来调用方法,不需显式转换,而explicit则在发现显式转型时,才调用方法。

          看到这里,不知大家是否会想起C#也有用于转型操作的is和as操作符,它们永远也不会调用这两个方法,而且也不会报错,类型不对只要返回false和null就行。

          仔细看这两个方法,我们就会发现一个问题:转换操作符方法其实都是围绕着我们自定义的类型:将自定义类型显式转换为其他类型,或将其他类型隐式的转换为自定义类型。我们无法将自定义类型隐式的转换为其他类型,或者将其他类型显式的转换为自定义类型。这是有道理的,如果允许其他类型,像是int转换为People,我们就必须在int的源码中修改,但这是不可能的,而且容易发生严重的错误。放宽限制是必须的,但不能完全放开。从这里例子我们还可以了解到,类型转换其实也是方法的调用,像是People people = 5,就是调用implicit operator People(int),想要进行转换的类型作为方法参数,而目标类型就是我们的返回值。

           如果说转换操作符还不能使我跌破眼镜的话,那么扩展方法就真的让我一下子反应不过来,对于我这个java人来说,从来没有想过我们也会需要用实例方法的方式调用静态方法,因为根本没有这样的需求!

     3.扩展方法

          扩展方法允许我们定义一个静态方法,并用实例方法的语法来调用它。这违背了我们过往的认识:静态方法根本不与对象实例挂钩。撇开这样做的原因,我们先来看看怎样实现这个古怪的用法:

     public static class People
        {
            public static void Show(this String name, int number)
            {
                Console.WriteLine(number);
            }
        }
        class Program
        {
            public static void Main(String[] args)
            {
                "".Show(5);
            }
        }

          请大家无视我这里奇怪的调用,我们来研究下里面的原理。编译器看到这样的代码,就会去寻找String类型有没有Show()方法,如果没有,才会去寻找静态类中是否也有Show()方法,而且该方法的第一个参数必须是调用该方法的类型一样,而且这个类型必须是用this关键字标识。
          问题来了,而且非常重要:我怎么知道String类型有这样一个方法?毕竟它根本就不在String的源码中。我们只能依靠Visual Studio的智能感知窗口,就是Eclipse的工具提示,它会在我们写下"."的时候,有一系列可调用的方法,然后在这些方法的说明中还会特别注明是扩展方法(请注意,它弹出的是扩展名,这是翻译错误的问题,应该是扩展方法)。

         扩展方法只能在非泛型的静态类中声明,参数也必须一个以上,静态类必须要有整个文件作用域,像是嵌套类就不适合了。扩展方法的查找是很慢的,所以编译器的建议是我们程序员主动导入该类,这样也是为了避免找错方法,毕竟编译器是在所有静态类中查找,很难保证其他静态类不会具有同名的扩展方法,事实就是多个静态类可以同时定义相同的扩展方法。事实上,这样的情况我们最好的办法就是指定该静态类,但这样就与静态方法的调用没有什么区别。更加可怕的是,大部分程序员是不知道这样的扩展方法,就算是C#程序员,也不会主动去找这样的方法。最可怕的事情就是,如果该类被派生的话,它的派生类都会有该扩展方法!像是System.Object要是拥有哪怕一个扩展方法,我们就会发现,在智能感知窗口有大量的垃圾信息!版本控制问题也是非常麻烦的问题:如果将来有人为我的People添加Show()实例方法,该扩展方法就完全废了!

          扩展方法具有这么多的弊端,但是很多类型,像是接口类型,委托类型和枚举类型都支持该特性,委托甚至能够委托扩展方法。下面就是委托和接口类型使用扩展方法的示例:

    public static void Show(this IEnumerable<T> collection){}
    
    Action action = "".Show(5);

          扩展方法违背了我们的使用直觉,我甚至不认为我需要用到这东西,因为它让我们的方法调用会变得麻烦,就算它根本就不会覆写同名方法。为什么我使用一个静态方法非要这么麻烦呢?C#的设计者是不会随便添加一个无用的特定,它自然在该语言中会有它的用处,可能我现在根本就没法发觉吧,还请大神指教。

     4.分部方法

          分部方法是本文最后一个出场的,它主要是为了解决一个问题:我们想要特化工具生成的源码文件(工具生成源码在C#中很常见,像是ASP.Net就是极佳的例子)。如果是过去的我们,就会这样做:继承自该源码然后覆写它的虚方法。但这样会有两个问题:

         1.该技术不能用于密封类,也不能用于值类型(值类型隐式密封);

        2.效率问题,定义一个类型,竟然只是为了覆写一个虚方法!你在开玩笑吧!!相信编译器一定会这样大喊的。

        分部方法就能解决这样的问题。分部方法只能在分部类或结构中声明。一般都是在基类中声明一个用partial标识的没有方法体的方法(就像abstract方法),然后在我们的类中实现该方法,当然也要用partial标识。分部方法返回值必须是void,因为我们无法确定该方法是否存在,可能永远不会有人去实现它。任何参数都不能用out修饰符,那时因为方法必须初始化它但方法的是否存在还是一个问题。但分部方法可以有ref参数,也可以是泛型方法,实例或静态方法,而且可以标识为unsafe。分部方法总是被视为private,但奇怪的是,C#编译器禁止我们显式声明private。

        Visual Studio在我们输入partial并按空格的时候,智能感知窗口会列出当前类型定义的,还没有实现的所有分部方法声明,这样可以死方便我们选择,而且还有自动生成方法原型。可见,MiscroSoft在提高程序员编程效率方面下了很大的功夫,至少比起他以前要厚道得多了。

  • 相关阅读:
    java并发计算的几种基本使用示例
    axios、ajax和xhr前端发送测试
    Spring注解
    Android菜鸟教程笔记
    普通二叉树操作
    MyBatis
    mysql的select语句总结与索引使用
    sys.argv的意义[转]
    硬件小白学习之路(1)稳压芯片LM431
    FPGA小白学习之路(6)串口波特率问题的处理
  • 原文地址:https://www.cnblogs.com/wenjiang/p/2960579.html
Copyright © 2020-2023  润新知