zoukankan      html  css  js  c++  java
  • 【读书笔记】.NET本质论第三章Type Basics(Part 1)

    第二章讨论的是程序集等内容。实际上作为开发者,我们更多的时候是在考虑如何组织一个类型。一个类型该放哪些东西呢?这个类型该具有哪些职责呢?所以,类型才是我们常常思考的单元。

    要唯一确定一个类型,就需要类型的完全限定名:

    命名空间.类型名,程序集名(这里分强命名还是弱命名文件名,语言文化,PublickeyToken,版本号)。这种完全限定名在.NETconfig配置文件里用的比较多:

    <add verb="*" path="get-nodes.aspx" type="WebHandler.GetNodesHandler,WebHandler"/>

    这是Web程序里,典型的用来配置HttpHandler的,用了这个完全限定名,ASP.NET就可以轻松的将请求路由到那个类型。

    对于一个类型,实际上只有三种成员:字段、方法、嵌套类型。

    那可能有人会说:事件、属性、索引呢。实际上这些都是编译器提供的语法支持,封装字段和方法的“包装成员”。

    比如属性,实际上就是一个get_xxx方法和set_xxx方法(如果该属性是可读可写的),这其中xxx就是该属性的名字。不过我们通常将属性与一个私有字段配合,不过属性本身和这个私有字段没有半点关系。也就是说属性就是两个方法加一些元数据。

    一个类包含的成员按照所有者又可分为两种:类型成员和实例成员。

    C#里,类型成员使用static来修饰(vb里用Shared,个人看来Shared更能表明类型成员真实的含义)。虽然C#里也沿用了C里的static关键字,可千万不要混淆了这两个概念。

    对于字段而言,就代表着内存如何分配。用static修饰的字段,只会分配一次内存,而默认情况下,字段是实例字段,每次实例化一个类型时都会分配内存。这样一看,好像这个static好像是共享的,所以前面说VB.NETShared关键字更能表明其含义。对于方法也是一样,static的方法属于类型的,必须通过类型名来访问(这点和Java不同)。

    NOTE常常在网络上看到有这样的提问:何时使用一个静态方法呢?对于这个也许每个人的看法都不同,可能涉及设计理念的问题。不过我个人觉得,一个静态方法,属于它的类型,所以静态方法应该是跟这个类型的领域职责联系不紧密的,属于一个中性的方法。比如有很多辅助方法、管理者类里就有很多静态方法,而这样的类一般都是一些与领域不相关的。

    对于static的字段,CLR会根据该字段的类型初始化该字段的值,如果该字段是数字类型的则会初始化为0,如果是布尔类型的则会初始化为false,而对于是引用类型的则初始化为nullCLR还会按照这个原则初始化应该分配在堆上的成员。

    值类型一定分配在栈上么?
    也许受到某类书的迫害,很多人在骨子里一直认为:引用类型分配在堆上,值类型分配在栈上。但是又想不通,如果一个类,它的实例应该分配在堆上,那它的实例字段是在栈上还是堆上呢。对于一个值类型,如果它是类型的字段成员(我不认为一个方法的参数和方法内的局部变量是它所在类型的成员)则分配在堆上。而如果该值类型是一个方法的参数或局部变量则分配在栈上。

    在我们声明一个实例字段,然后马上初始化时,类似于下面:

    private int _count = 5;

    然后编译,再用ILDasm反编译,我们会奇怪的发现,现在只有字段的声明,而字段的初始化(也就是赋初值5)的代码跑到该类的实例构造器里面了,更奇怪的是,如果你给该类提供了几个构造函数,则每个构造函数里都会拷贝一份这个初始化的代码。

    如下这样的代码:

    class Foo
    {
            
    private int _count = 5;
            
    private int _perCount = 1;
            
    private int _timeOut = 5;
            
    public Foo()
            { 
            }
            
    public Foo(int count)
            { 
            
            }
            
    public Foo(int count, int perCount)
            { 
            
            }
    }

    实际上与下面的代码是等同的:

     

    class Foo
    {
            
    private int _count;
            
    private int _perCount;
            
    private int _timeOut;
            
    public Foo()
            {
                _count 
    = 5;
                _perCount 
    = 1;
                _timeOut 
    = 5;
            }
            
    public Foo(int count)
            {
                _count 
    = 5;
                _perCount 
    = 1;
                _timeOut 
    = 5;
            }
            
    public Foo(int count, int perCount)
            {
                _count 
    = 5;
                _perCount 
    = 1;
                _timeOut 
    = 5;
            }
    }

    如此一来,代码迅速膨胀。那有什么好的办法呢?

    最佳实践
    对于这种给许多字段赋初值,而且有具有多个构造器,我们应该在一个构造器里对这些字段进行赋初值,其他构造调用该构造器。

    根据这个最佳实践,则上面的代码应该修改为:

    class Foo
    {
            
    private int _count;
            
    private int _perCount;
            
    private int _timeOut;
            
    public Foo()
            {
                _count 
    = 5;
                _perCount 
    = 1;
                _timeOut 
    = 5;
            }
            
    public Foo(int count):this()
            {
            }
            
    public Foo(int count, int perCount):this()
            {
            }
    }

    默认构造器

    当你定义一个类型时,如果没有给该类型提供任何构造器,则编译器会自动的给该类型定义一个无参的构造器。比如下面这个类型:

    class Foo
    {

    }

    虽然从表象上看,该类型没有任何成员,但实际上它具有一个编译器自动生成的构造器。不过如果你给该类型只要“手写”了一个构造器,那个默认的构造器就不会有了。

    构造器链
    有的时候我们经常需要提供下面这样一串的构造器

    public Foo()
    {
    }
    public Foo(int count)
    {
    }
    public Foo(int count, int perCount)
    {
    }
    public Foo(int count, int perCount,int timeOut)
    {
    }

    这往往是为了给类型的实例化工作带来更多的灵活性。但是这些构造器里的代码我们要怎么写呢?每个构造器里都写上初始化的代码么?答案是否定的,有个大名鼎鼎的原则DRYDon’t Repeat Yourself。说的就是不要重复,如果每个构造器里都写着初始化的代码,如果有一天初始化的方式变了(比如需要用一个小算法初始化),那所有的构造器里的代码我们都得一个个的修改,做的事情太多,就容易出错,所以我们应该这样:

    class Foo
    {
            
    private int _count;
            
    private int _perCount;
            
    private int _timeOut;
            
    public Foo():this(0,0,0)
            {

            }
            
    public Foo(int count):this(count,0,0)
            {
            }
            
    public Foo(int count, int perCount):this(count,perCount,0)
            {
            }
            
    public Foo(int count, int perCount,int timeOut)
            {
                _count 
    = count;
                _perCount 
    = perCount;
                _timeOut 
    = timeOut;
            }
    }

    在一个比较“全”的构造器里提供实现,而其他的构造器调用这个构造器,就像一个链条一样,这就是构造器链。

    默认情况下,一个实例里的字段的内存分配是不透明的,CLR会经常调整这些字段的分配顺序。这点C#就不像它的前辈C++。因为需要“数据对齐”,在C++里定义字段的顺序会带来空间或时间上的一些微妙的问题,但是在.NET里,字段定义的顺序确实无关紧要的,因为CLR会动态的调整这些字段的顺序,使得它们在时间和空间上都做的更好。(不过我们可以使用StructLayout特性显式的控制字段的偏移)。

    好了,就说这么多了。下一篇文章我会谈谈静态成员的一些事情。

  • 相关阅读:
    便利的开发文档工具doxygen
    父页面 js 取得弹出窗口所选择的值, 弹出窗口关闭后刷新父页面
    ASCII码对照表
    C#中Brush、Color、String相互转换
    C#获取标准北京时间
    2005数据库脚本在SQL2000上执行 注意事项
    给Image控件后台赋Source值
    Web服务枚举组件不可用
    泛型集合转化为DataSet
    网站常见关于"登录|注册"和"姓名|注销"用JS实现
  • 原文地址:https://www.cnblogs.com/yuyijq/p/1512867.html
Copyright © 2011-2022 走看看