zoukankan      html  css  js  c++  java
  • 【C#进阶系列】08 方法

    实例构造与引用类型

    之前的章节其实已经写过了引用类型的构造过程:

    首先当然是,在堆中,为引用类型的实例对象分配内存,然后初始化对象的附加字段(即类型对象指针和同步块索引)。

    这个时候为对象分配的内存都是直接被置为0的,所以如果所用到的构造器中没有对对象中的一些字段做处理,那么这些字段的初始值都应该为0或者null。

    如果一个类,没有构造函数,那么这个类构造的时候就会定义一个默认无参构造器,它里面就简单调用基类的无参构造器。

    极少数的情况下,会有不实用实例构造器就能创建类型的实例的情况,比如MemberwiseClone方法(深复制)和反序列化对象时。(反序列化会调用GetUninitializedObject或GetSafeUninitializedObject方法为对象分配内存,还是后面讲吧。)

    还记得上一章节写的,内联初始化可能会导致性能问题吧:

    因为每一次内联初始化实际上都会将这些初始化字段的操作,嵌入构造函数的代码中(注意会先进行这些操作,再进行真正的构造函数的操作)。如果只有一个构造器函数,那么不会有什么影响。然而如果有多个构造器函数,那么这几个构造器函数里都会插入这段初始化字段的代码。

    所以当存在多个构造器参数,而代码里又有一大堆内联初始化,那么实际生成的代码中就会有大量的冗余代码。可以用以下方法解决:

      public class Troy{
            //不进行内联初始化
            int _a;
            int _b;
            public Troy() {
                //将初始化过程放在某一构造参数内,一般就是无参构造函数中
                this._a = 1;
                this._b = 2;
            }
            public Troy(int i):this()  //在其它构造函数时,调用this()
            {
    
            }
            public Troy(int i,int j) : this()
            {
    
            }
        }

    实例构造与值类型

    值类型其实不需要定义构造器,C#编译器也根本就不会为值类型内联嵌入默认的无参构造器。

    只有嵌套在引用类型的值类型才会被初始化为0或null,如果是基于栈的值类型,那么在读取之前,被要求强制初始化,否则报错。

    值类型的实例构造器只有显式调用才有用,否则其字段都会被初始化为0或null。(也就是说,即时这个struct有无参构造函数,只要你没有显示调用,那么就不会自动调用无参构造函数。实际上C#编译器也不允许你在结构体里写一个无参构造函数,毕竟这个点来说,太容易误会了)。

    由于C#不允许值类型定义无参构造函数,所以值类型同样不准内联参数化。(静态字段可以内联初始化,因为是在类型对象里面,而不是实例对象)

    且值类型的任何构造函数在初始化的时候,必须对值类型的所有字段都赋值。

    对于这么麻烦的设定,当然也有解决的办法:

        public struct Troy {
            public int a;
            public int b;
            public Troy(int i) {
                this = new Troy();//先初始化所有的字段都为0或null
                //初始化自己想玩的字段
                a = i;
            }
        }

    (我不得不吐槽,我刚刚用VS自己写了个值参数初始化的小例子,然后被360给当做病毒删掉了。)

    关于类型构造器

    首先要了解到,类型构造器实际上就是构造CLR分配内存中的类型对象初始化时用的。

    类型构造器是不允许有参数的,当然也就只能定义一个类型构造函数。

    实际上类型构造器必须是私有的,甚至不允许显式写上private修饰符,这样做正是为了防止开发人员调用。它的调用总是由CLR负责的。

    简单样例:

    class Program
        {
            static void Main(string[] args)
            {
                Troy obj = new Troy();
            }
        }
        public class Troy {
            static Troy() {
                Console.WriteLine("我就问你6不6?");
            }
        }

    构造过程:

    JIT编译器编译一个方法时,会查看代码引用了哪些类型。任何一个类型定义了类型构造器,那么JIT编译器就会检查针对当前AppDomain是否执行了这个类型构造器。是就不调用,否就调用。因为CLR希望在每个AppDomain中一个类型构造器只执行一次,所以为了不使多个线程同时调用类型构造器,在第一个调用类型构造器的线程调用时,会获取一个互斥线程同步锁。这样一来,就只有一个线程可以调用了,后面的线程要用的时候发现已经调用过了,就不会再调用类型构造器了。(因为类型构造器线程安全,所以很适合在里面初始化任何单例对象。)

    虽然可以在值类型中定义类型构造函数,然而实际上因为值类型根本就不会在堆中有类型对象,所以自然里面的代码都不会被调用。

    关于操作符重载

    实际上CLR对操作符重载一无所知,因为这是编程语言的语法。

    当C#这种语言写的操作符重载语句被编译成IL代码时,其实已经变成了一个带有specialname标志的函数。

    当编译器看到有+这种操作符时,就会看几个操作数的类型中是否有定义了名为op_Addition这个函数(被编译后的真正的函数名),而且该方法参数兼容于操作数的类型。

    所以操作符重载函数中,一定要有一个参数的类型与定于这个重载方法的类型相同:

      public class Troy {
            public static int operator +(Troy a, Troy b) {
                return 10;
            }
        }

    关于转换操作符方法

      class Program
        {
            static void Main(string[] args)
            {
                Troy obj = 3;//隐式转换成功
                string a = obj;//由于是显示转换重载,所以这种写法会编译不过
                string a =(String)obj;//显示转换成功
            }
        }
        public class Troy {
            // 隐式转换操作符implicit重载
            public static implicit operator Troy(Int32 num) {
                return new Troy();
            }
            // 显式转换操作符explicit重载
            public static explicit operator String(Troy troy)
            {
                return "怎么转都是我";
            }
        }

    和一般的+-这种操作符重载一样,实际上生成的IL代码中,换了一个名字,前缀加上了op_。

    当C#编译器检测到代码中一个对象期望得到另一个类型不同的对象时,就回去找这两个类型中是否定义了隐式转换的op_Implicit方法,是就转。显示类似。

    可以参考Decimal类的定义去理解。

    关于扩展方法

    先说一下我自己的认识吧,其实我不建议使用这个东西。

    因为写得不规范的扩展方法,会增加了代码的阅读难度,增加维护成本。(我真的很确定有的人会把这个东西写得到处都是)

    这个东西之前在写《重构》的学习笔记中提到过,主要用于解决别人封装的类库,没法增加自己想要的函数。

    第19点.不完美的类库讲到的

    简单来讲,还是慎用,自己写的类就别用扩展方法。

    另外扩展方法必须是顶级静态类中定义的静态方法,如果是嵌套类中的话,编译会出错。

    实际上扩展方法在C#编译器编译过后也只是个一般的静态对象里的静态函数,只不过加了个[Extension]的特性。然而实际上这个ExtensionAttribute特性还不能在代码中用,都是C#编译器去自动生成的。

    关于分部方法

    分部方法和分部类很像,不过是方法前面加上partial修饰符。

    这样的话,如果其它分部类实现了这个方法,那么就会加上这个方法,如果没有实现,那么这条代码在编译的时候就会被忽略。

    但是分部方法只能在分部类和结构中用,且返回类型总是void,任何参数都不能用out来修饰。之所以会这样限制,是因为方法在运行时可能就并不存在,所以也就不会有返回。

    分部方法总是private的,但是C#编译器禁止将private修饰符显式写在分部方法前面。(和类型构造器在这个点上类似)

  • 相关阅读:
    IDEA激活方式(亲测有效)加汉化方式
    IDEA快捷键
    (转)RBAC权限模型——项目实战
    Nginx负载均衡策略
    nginx proxy_pass
    Nginx rewrite
    web cache server方案比较:varnish、squid、nginx
    LVS负载均衡工作模式和调度算法
    四层 七层负载均衡区别
    Nginx每天自动日志分割
  • 原文地址:https://www.cnblogs.com/vvjiang/p/5260418.html
Copyright © 2011-2022 走看看