zoukankan      html  css  js  c++  java
  • 《C#与.NET3.5高级程序设计(第4版)》笔记5

    第五章 定义封装的类类型

     

    本章研究c#的面向对象功能。首先介绍如何构建类类型,之后研究了封装的作用,然后讨论定义属性、字段等内容,最后研究了XML代码文档语法的作用。

    5.1类类型和及其构造函数

    类的基本概念:

    类是由数据字段(也叫成员变量)和操作这个数据的成员(构造函数、属性、方法、事件等)所构成的自定义类型。其中的字段数据用于表示类实例(也称对象)的“状态”,对象就是表示使用new关键字创建的某个类类型的实例。对象必须使用new关键字来分配到内存,如果不使用new,在使用这个对象时,产生编译器错误。例如对于下句:

    car mycar=new car();

    前半句代码只是声明了指向尚未被创建的car对象的引用(指针),但是此时指向为null,后半句当通过new把引用赋给对象之后,这个引用才会指向内存中的有效类实例。

    注意,只能定义成员和成员变量,不能对成员进行操作,比如重新赋值等,这些操作只能在成员中进行操作!

    类的构造函数:

    对于对象的状态(用成员变量表示),可以一个个去赋值,这对于一个小的类是可以的,但是若拥有很多字段的字段,每次都一个个赋值,是不可想象的。这就需要构造函数,它可以在使用对象之前先给对象的字段数据赋相关的值,也就是初始化操作。

    C#中的构造函数是一种特殊方法,在使用new关键字创建对象时被间接调用,它不会有返回值(void都不行),名字和类名一致。

    C#提供默认的构造函数,它不接收参数,其作用就是把新对象分配到内存中时,确保所有字段数据都设置为正确的默认值。当然,可以重新定义默认构造函数(不带参数),也可以定义带有参数的自定义构造函数。

    注意:上一章指出了,值类型也有默认构造函数(int,float....,结构体),这些类型系统都提供了默认构造函数(不带参数),不可以再显式重新定义无参构造函数,但是像结构体这样的值类型,可以自定义带有参数的构造函数。

    对于引用类型,一旦定义了自定义构造函数,若不显式定义默认构造函数,则系统提供的默认构造函数将被移除而失效,或者说系统就不再提供默认构造函数。因此,若希望使用默认构造函数和自定义构造函数来创建实例,则必须显式的重新定义默认构造函数。

    注意:构造函数默认是private哦,要想使用,需要加其他修饰符。

    this关键字:

    this关键字用于对当前实例(对象)的访问,也就是代表当前实例,因此,this无法用于类级别,如静态方法中,就不能使用。

    this关键字有两个用途:

    (1)this关键字解决当前传入参数的名称和类型数据字段的名称相同时产生的作用域歧义。如:

    class motorcycle 
    { public string name;//成员变量叫name 
    public void setdrviername(string name)//传入参数也叫name 
    { 
    name=name;
    //那么这句话是把哪个值赋值给哪个呢,本意应该是将参数值赋值给成员变量,//但是实际上此句执行了将参数值name赋值给参数值name了。 
    this.name=name;
    //这才是将参数值赋值给成员变量的意思 
    } 
    }

    (2)进行串联构造函数调用

    它使用一种叫做“构造函数链”的技术来设计类,当一个类中存在多个构造函数,并且构造函数中存在冗余代码(比如都要进行某种检查验证等),这可以使用。

    当然,对于常规方法,为了防止构造函数中冗余代码,可以专门设计一个方法,构造函数进行调用,但是一个更简洁的方法就是,让一个接收最多参数个数的构造函数座位“主构造函数”,实现必须的验证逻辑等。其余构造函数使用this关键字把传入的参数转发给主构造函数,并且提供所有必须的其他参数,这需要功过冒号运算符来实现,如:

    class motorcycle 
    { 
    public int driverintensity; 
    public string drivername; 
    public motorcycle() 
    {} 
    public motorcycle(int intensity):this(intensity,"") 
    {} 
    public motorcycle(string name):this(0,name) 
    {} 
    public motorcycle(int intensity,string name) 
    { //相关代码 }
    } 

    这里,this就代表被调用的构造函数,后面跟着被调用构造函数的参数。

    这样,我们就定义了一个使用许多构造函数来创建具有字段数据(成员变量)和各种成员的类了。

    注意:既然是链形式的调用,不正确的调用可能导致死循环!例如:  

    public motorcycle(int intensity):this("") 
    
    { }
    public motorcycle(string name):this(0) 
    
    { } 

    以上两个构造函数相互调用,如果实例化时调用其中任意一个,都会导致死循环构造,感觉这个问题应该在编译时就应该提示错误啊,可惜却等到了运行时才报死循环的溢出错误,实在不够智能。

    注意:this 关键字将引用类的当前实例。静态成员函数没有 this。
    this 关键字可用于从构造函数、实例方法和实例访问器中访问成员。
    在静态方法、静态属性访问器或字段声明的变量初始值设定项中引用 this 是错误的。

    public class MyClass
    {
        int a=0;
        //int b=this.a;//成员变量初始值处不能用this
    public static void RunSnippet()
    {
        //public int c=this.a;//静态方法中不能用this;
    }
    }

    5.2 static关键字

    C#类和结构都可以通过static关键字来定义许多静态成员。如果这样的话,这些成员就只能通过类级别而不是实例级别来调用。简言之,静态方法是被认为是非常普遍的项,并且不需要在调用成员时创建类型的实例,虽然任何类或结构都可以定义为静态方法,但是它们通常出现在“工具类”中。

    (1)定义静态方法(和字段)

    只需在方法定义前加static即可。注意,静态成员只能操作静态数据或调用类的静态方法,如果尝试调用非静态类数据或非静态方法,将编译错误!

    (2)定义静态数据

    要知道,如果类定义了非静态数据(实例数据),则类型的每个对象都会维护字段的独立副本,每个类实例都会分配独立的内存。

    对于静态数据,分配一次内存并且在所有对象之间共享,即使创建了新的实例,静态数据的值也不会被重新初始化,因为CLR把数据分配到内存中只会进行一次。所有对象会共享同一份数据。

    因此,静态成员只能操作静态数据,非静态方法可以使用静态数据和非静态数据,因为静态数据对类型的所有实例都是可用的。

    (3)定义静态构造函数

    静态构造函数是特殊的构造函数,非常适合于初始化在编译时未知的静态数据的值(例如需要从外部文件读取值或者生成随机数等),需要注意的是:

    • 一个类只能定义一个静态构造函数;
    • 静态构造函数不允许访问修饰符(如public static 是不对的),也不接受任何参数;
    • 无论创建多少类型的对象,静态构造函数只执行一次(也就是只能对静态成员设置一次);
    • 运行库创建类实例或者首次访问静态成员之前,运行库调用静态构造函数(也就是第一次并且只有第一次使用了这个类,执行静态构造函数);
    • 静态构造函数执行先于任何实例级别的构造函数;
    • 显然也就无法使用this和base来调用构造函数。
    • 若类存在静态成员变量,系统会自动提供一个默认静态构造函数,否则默认不提供。

    (4)定义静态类

    .net2.0之后,支持静态类的概念。它用来扩展static关键字的使用。如果一个类被定义为静态的,就不能使用new关键字来创建,只能包含用static关键字标识的成员或字段,如果我们创建一个只包含静态成员或者常量数据的类,这是个好的选择(之前为了不让类被实例化,只能将默认构造函数重定义为private,或者用abstract关键字表示为抽象类型)。有了这种类,就更加简洁、类型更加安全。

    提示:其实应用程序对象(Main入口点所在类)就可以定义为一个静态类(默认不是哦),让他只包含静态成员并且不能直接创建。

    5.3  OOP的支柱

    面向对象三个原则:继承、封装、多态。

    封装就是让用户不必了解实现细节,来保护数据完整性。

    继承是指基于已有类定义来创建新类定义的语言能力。这种is-a的关系也叫经典继承。

    在OOP中还有另一种形式的代码重用(不是继承),就是包含/重用模型(has-a关系,也叫聚合,如在一个类中包含另外一个类)。

    多态表示的是语言以同一种方式处理相关对象的能力。它允许基类为所有的派生类定义一个成员集合(多态接口),一个类类型的多态接口由任一个虚拟或抽象成员组成。

    注意:区分多态接口和接口概念,多态接口的虚拟或抽象成员,能够让派生类实现类似于接口的功能。

    5.4  访问修饰符

    在封装时,必须考虑类型的哪个方面对我们应用程序的哪部分可见。准确的说,类型以及他们的成员总是使用某个关键字来定义,用于控制他们对应用程序其他部分如何可见。

    注:这里所说的对于成员的访问,如果在前面不加修饰语句的话,其一是在定义方法的类型中直接访问,其二是通过点运算符在定义此方法类型的外部进行的此类型的类级别访问(若是静态方法或静态成员变量)或者此类的对象级别访问(非静态方法或非静态成员变量),其三包括通过经典继承从而使得子类可继承成员。有了这个概念,下面的就比较容易理解了,下面的访问也应该从这三个方面来理解。

    主要有:

    • public:修饰类型(或结构)或者类型的成员,没有限制,在内部或外部程序集均可访问;

    这个最好理解,访问没有任何限制,无需多说。

    • internal修饰类型(或结构)或者类的成员,只能在程序集内部访问,其含义是“只有属于同一程序集的类可以访问此类或此成员”;

    这个其实就是比public略有限制,如果在同一个程序集中访问internal的类或者internal方法,是访问不受限制的(和public一样),但是在另一个程序集中,是无法访问到internal的类或者类的internal方法的。

    • private修饰类的成员或嵌套类型,由定义他们的类(或结构)访问;

    那么此处的访问仅仅限定在声明的类型内部来访问,只符合上述三种访问方式的第一种。也就是说,不能通过点运算符在外部来访问,更不用说是被继承的访问了。

    • protected修饰类的成员或嵌套类型,被定义它的类型或派生类(包括直接访问和通过派生类实例来访问(若是非静态的))访问;

    这是最难理解的,这里加以总结,从定义看,符合上面的其一和其三的规则,我们可以从定义成员的类型中和通过继承在子类中直接访问,对于其二的条件,那个约束“在定义此方法类型的外部”在这里就是只能是子类(或者更准确说是在此类的继承链下的子孙类),并且要通过子孙类的实例通过点运算符来访问当前类的这个protected成员。如:

    public class Class_public
       {
           public void fun_public()
           {
               fun_protected();//通过其一的方式进行访问
            }
           protected void fun_protected()
           { } 
    
       }
    
    public class Class_child_public:Class_public
        {
            public void fun_test()
            {
                Class_public PublicClass = new Class_public();
                PublicClass.fun_public();
                PublicClass.fun_internal();
                //PublicClass.fun_protected();//不能通过此方式访问哦,这是通常犯得理解错误
                  fun_protected();//通过其三的方式访问
                  Class_child_public PublicChildClass = new Class_child_public();
                PublicChildClass.fun_protected();//这是我们加以约束的其二方式进行访问                       //当然也可以是通过Class_child_public的子孙类来访问
            }
        }
    
    public class Class_other
    
        {
    
             public void fun_test()
             {
                Class_child_public PublicChildClass = new Class_child_public();
                // PublicChildClass.fun_protected();//无法访问哦,因为这不是在继承链上
    
              }
    
        }

    可以发现,对于protected成员,只能在其继承链上进行访问!一个类(假如既有基类又有派生类),则该类成员可以访问自身的protected成员或从基类继承来的protected成员,也可以访问为该类类型对象的protected成员,同时也可以访问其类型为派生类对象的protected成员。

    • protected internal(或者internal protected )修饰类的成员或嵌套类型,在定义它的程序集或类型中或者派生类中可用。其含义是“访问范围限定于此程序集那些由它所属的类派生的类型”。

    这句话比较难以理解,稍微详细解释一下:如果某类有一个成员声明为protected internal,则位于同一程序集的类可以访问此成员,但位于另一个程序集中的类不可访问它,除非这个位于另一个程序集的类是这个成员所在类的子类。 这其实是internal和protected的可访问性的并集,符合任何一个都行。

    注意:类型(例如class)只能由public或internal修饰。所有修饰符都可以修饰类型成员,后三个类型也可以修饰嵌套类型。这是之前看书时候没有强调和注意的。

    成员的可访问性如下表所示:

    属于

    默认的成员可访问性

    该成员允许的声明的可访问性

    enum

    public

    class

    private

    public

    protected

    internal

    private

    protected internal

    interface

    public

    struct

    private

    public

    internal

    private

    另外,在类或结构内部定义的类型称为嵌套类型请参考:

    http://msdn.microsoft.com/en-us/library/ms173120(zh-cn,VS.80).aspx

    疑问:从文章第155页可以看出,实际上嵌套类型也可以使用public和internal修饰符,为什么文中没有给出呢?

    默认的访问修饰符:

    • 类型成员是隐式私有的(private)
    • 类型是隐式内部的(internal
    • 嵌套类型是隐式私有的(private)

    所谓隐式,就是即使不手动加上修饰符,编译器也会默认是这个修饰符。

    问:第128页中说默认构造函数自动设置为私有的,应该不对吧?私有的还怎么使用啊?

    答:是这样的,如果类没有给出默认构造函数,则系统提供的默认构造函数确实不是私有的,如果是自己写的默认构造函数,并且不加修饰,则就是私有的,无法访问,因此必须加上public修饰符。

    5.5 属性

    封装概念的核心是,对象的内部数据不应该从公共接口直接访问。封装提供了一种保护状态数据完整性的方法。与定义公共字段相比(很容易发生数据损坏),应该更多的定义私有数据字段,这种字段可以由调用者间接地操作,操作方法有:

    (1)定义一对传统的访问方法和修改方法

    这就需要定义了一个私有字段之后,若需要外界对它进行操作,应该定义两个非私有方法,用于访问和修改这个私有字段,从而间接完成对私有字段的操作。这个技术需要两个操作单个数据点的方法。

    (2)定义一个命名的属性

    尽管第一种已经可以满足要求,但是还是提倡使用属性来强制数据保护。对于CLR来说,属性总是映射到“实际的”访问方法和修改方法。

    属性由作用域中定义的get和set两个作用域组成。之所以上面是加了引号的实际的方法,是因为在底层,c#属性也是使用方法来真正执行操作的。实际上属性被映射到由CLR内部调用的隐藏的set_xxx/get_xxx方法。因此,如果你设置了一个叫做name的属性,那么就别在类中定义set_name和get_name这样的方法(当然这个名称的数据字段也不行)了哦~~否则编译时候会提示冲突滴!

    需要强调的是,这两种方法的使用目的是一致的,属性优点就是对象的用户可以只使用一个命名项就能操作内部数据。

    定义一个属性如下:

    private string empssn; 
    public string socialnumber//类似于定义变量,没有()哦 
    { 
    get
    {return empssn;}//使用return返回属性值 
    set
    { empssn=value;}//value关键字是属性被设置的值! 
    }

    可以对整个属性的可访问性使用修饰符(不修饰则为私有的,无法访问。)

    需要的话,也可以对set和get域可访问性进行修饰,如 protected get{empssn=value;},这样,只有当前类和派生类可以调用属性的可读性。

    这里需要注意的是,仅当属性同时具有 get 访问器和 set 访问器时,才能对访问器使用可访问性修饰符哦,也就是只有set或者之后get时,不能对他们进行单独的修饰符。可以理解,因为既然只有一个,那么干脆就将这个属性设置为protected吧,单独设置一个单独的访问器,意义好像不大。

    另外,也不能对两个访问器set和get同时设置修饰符,会提示“不能为属性的两个访问器同时指定可访问性修饰符”。还需要注意的是,对于set或get域的访问修饰符,一定不能比此属性的修饰符的可访问性大哦。

    internal string socialnumber { public get{return empssn;} set{ empssn=value;} } //无法通过编译,会提示:get访问器的可访问性修饰符必须比属性或索引器具有更强的限制

    对于属性,也可以只写get或者只写set,这样,就达到了只读属性和只写属性!

    注意:访问器的可访问性修饰符必须比属性或索引器具有更强的限制。

    静态属性:

    属性也可以是静态的哦,当然,静态属性和静态方法的操作是一致的,静态属性只能操作静态数据。

    5.6常量数据和只读字段

    常量数据:

    C#提供const关键字来定义常量,定义赋初值后就永远不能改变。注意,应该使用类(或结构).常量名来访问常量字段,而不是对象.常量名,因为常量字段是隐式静态(隐式的具有static性质),因此应该在类级别进行访问,并且不能再加上static修饰符。

    注意:常量的定义必须用基元类型,字段的定义可以是任何类型。

    定义const常量数据时,必须为常量指定初始值,因为在编译时必须知道常量的值,因此在构造函数中或其他地方赋值是不对的,因为这些(包括构造函数)都是在运行时才调用。

    只读字段:

    只读字段用readonly修饰,和常量类似,只读数据字段不能在赋初值后改变。和常量不同的是,赋给只读字段的值可以在运行时决定,因此,构造函数作用域中进行赋值是合法的,但是其他地方不行(运行时,非构造函数中不能赋值给只读字段)。另外,若不给他赋值,系统会给出一个默认值的!

    因此,如果直到运行时才知道字段值(如从外部文件获取值),并希望之后不会被改变(否则用静态构造函数),这种只读字段是很好的选择。

    和常量数据不同的是,只读字段不是隐式静态的,此时需要实例化后,用对象.只读字段来访问。因此,如想从类级别进行访问也就是静态只读字段,应该使用static来修饰这个只读字段,如static readonly。当然,若这个静态只读字段也是只能在运行时才能赋值,则这个值应该在静态的构造函数中进行赋值。

    有点儿混乱,那么既然都是常量数据,不如也让只读字段为隐式静态的就ok啦,为什么还提供对象级别的访问呢。主要是因为只读字段可以在构造函数进行赋值,因此,可能对不同的对象,需要不同的常量值,这时就要在构造函数时设置不同的常量值啦。

    这里有一篇对于两者的介绍比较容易理解了:http://blog.csdn.net/oec2003/archive/2009/06/22/4287927.aspx

    5.7C#的分部类型

    c#中,类和结构可以用名为partial的类型修饰符定义,它允许跨多个*.cs文件定义c#类型。以前版本要求一个类型的全部代码定义在一个*.cs中,现在有了分部类型,就不需要那么长的文件了,如在两个地方都定义partial class emp,则编译器编译后,最终结果仍然是一个统一的类型,这partial纯粹是设计时结构,有两点需要保证:

    • 类型名(上面的emp)必须一致;
    • 定义在相同的命名空间中。

    5.8通过XML生成C#源代码的文档

    这个就不想细说了,c#提供特有的一种注释标记,用来产生基于XML的代码文档,这个符号就是三个正斜杠(///),既可以在代码中直接注释,也可以在VS.NET类细节窗口中,输入这些XML代码注释,其好处就是智能感知时能查看这些信息。

    某些情况下,使用XML注释为代码进行文档化后,需要根据xml生成对应xml文件,这就需要使用csc编译器的/doc参数进行生成,当然,也可以通过VS.NET平台中,项目属性中,选择生成-输出的XML文档文件来指定生成名称和位置。

    接下类,可以将生成的xml文件转换为用户更友好的帮助格式,VS.NET没有提供这个功能,但是可以用第三方工具(NDOC或者sandcastle)来生成。这样就能看到诸如MSDN这样的帮助系统啦。

     

     

  • 相关阅读:
    PPTP服务器的端口
    Linux ln命令
    Git 学习笔记回到过去版本
    Iptables中SNAT和MASQUERADE的区别
    转移虚拟机后ubuntu network available SIOCSIFADDR: No such device
    用iptables做IP的静态映射
    软件项目管理
    需求工程
    软件工程——理论、方法与实践 之 软件实现
    软件工程——理论、方法与实践 之 软件工程中的形式化方法
  • 原文地址:https://www.cnblogs.com/lerit/p/1670978.html
Copyright © 2011-2022 走看看