精华内容
下载资源
问答
  • Java基础知识面试题(2020最新版)

    万次阅读 多人点赞 2020-02-19 12:11:27
    面向对象五大基本原则是什么(可选) 类与接口 抽象类和接口的对比 普通类和抽象类有哪些区别? 抽象类能使用 final 修饰吗? 创建一个对象用什么关键字?对象实例与对象引用有何不同? 变量与方法 成员变量与局部...

    Java面试总结(2021优化版)已发布在个人微信公众号【技术人成长之路】,优化版首先修正了读者反馈的部分答案存在的错误,同时根据最新面试总结,删除了低频问题,添加了一些常见面试题,对文章进行了精简优化,欢迎大家关注!😊😊

    【技术人成长之路】,助力技术人成长!更多精彩文章第一时间在公众号发布哦!

    文章目录

    Java面试总结汇总,整理了包括Java基础知识,集合容器,并发编程,JVM,常用开源框架Spring,MyBatis,数据库,中间件等,包含了作为一个Java工程师在面试中需要用到或者可能用到的绝大部分知识。欢迎大家阅读,本人见识有限,写的博客难免有错误或者疏忽的地方,还望各位大佬指点,在此表示感激不尽。文章持续更新中…

    序号内容链接地址
    1Java基础知识面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390612
    2Java集合容器面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104588551
    3Java异常面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390689
    4并发编程面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104863992
    5JVM面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390752
    6Spring面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397516
    7Spring MVC面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397427
    8Spring Boot面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397299
    9Spring Cloud面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397367
    10MyBatis面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/101292950
    11Redis面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/103522351
    12MySQL数据库面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104778621
    13消息中间件MQ与RabbitMQ面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104588612
    14Dubbo面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390006
    15Linux面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104588679
    16Tomcat面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397665
    17ZooKeeper面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397719
    18Netty面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104391081
    19架构设计&分布式&数据结构与算法面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/105870730

    Java概述

    何为编程

    编程就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到结果的过程。

    为了使计算机能够理解人的意图,人类就必须要将需解决的问题的思路、方法、和手段通过计算机能够理解的形式告诉计算机,使得计算机能够根据人的指令一步一步去工作,完成某种特定的任务。这种人和计算机之间交流的过程就是编程。

    什么是Java

    Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。

    jdk1.5之后的三大版本

    • Java SE(J2SE,Java 2 Platform Standard Edition,标准版)
      Java SE 以前称为 J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java 应用程序。Java SE 包含了支持 Java Web 服务开发的类,并为Java EE和Java ME提供基础。
    • Java EE(J2EE,Java 2 Platform Enterprise Edition,企业版)
      Java EE 以前称为 J2EE。企业版本帮助开发和部署可移植、健壮、可伸缩且安全的服务器端Java 应用程序。Java EE 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web2.0应用程序。2018年2月,Eclipse 宣布正式将 JavaEE 更名为 JakartaEE
    • Java ME(J2ME,Java 2 Platform Micro Edition,微型版)
      Java ME 以前称为 J2ME。Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。Java ME 包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。基于 Java ME 规范的应用程序只需编写一次,就可以用于许多设备,而且可以利用每个设备的本机功能。

    JVM、JRE和JDK的关系

    JVM
    Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。

    JRE
    Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包

    如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。

    JDK
    Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等

    JVM&JRE&JDK关系图

    什么是跨平台性?原理是什么

    所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。

    实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。

    Java语言有哪些特点

    简单易学(Java语言的语法与C语言和C++语言很接近)

    面向对象(封装,继承,多态)

    平台无关性(Java虚拟机实现平台无关性)

    支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的)

    支持多线程(多线程机制使应用程序在同一时间并行执行多项任)

    健壮性(Java语言的强类型机制、异常处理、垃圾的自动收集等)

    安全性

    什么是字节码?采用字节码的最大好处是什么

    字节码:Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。

    采用字节码的好处

    Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。

    先看下java中的编译器和解释器

    Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。

    Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。
    

    什么是Java程序的主类?应用程序和小程序的主类有何不同?

    一个程序中可以有多个类,但只能有一个类是主类。在Java应用程序中,这个主类是指包含main()方法的类。而在Java小程序中,这个主类是一个继承自系统类JApplet或Applet的子类。应用程序的主类不一定要求是public类,但小程序的主类要求必须是public类。主类是Java程序执行的入口点。

    Java应用程序与小程序之间有那些差别?

    简单说应用程序是从主线程启动(也就是main()方法)。applet小程序没有main方法,主要是嵌在浏览器页面上运行(调用init()线程或者run()来启动),嵌入浏览器这点跟flash的小游戏类似。

    Java和C++的区别

    我知道很多人没学过C++,但是面试官就是没事喜欢拿咱们Java和C++比呀!没办法!!!就算没学过C++,也要记下来!

    • 都是面向对象的语言,都支持封装、继承和多态
    • Java不提供指针来直接访问内存,程序内存更加安全
    • Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。
    • Java有自动内存管理机制,不需要程序员手动释放无用内存

    Oracle JDK 和 OpenJDK 的对比

    1. Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一次;

    2. OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的;

    3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题;

    4. 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能;

    5. Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本;

    6. Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。

    基础语法

    数据类型

    Java有哪些数据类型

    定义:Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。

    分类

    • 基本数据类型
      • 数值型
        • 整数类型(byte,short,int,long)
        • 浮点类型(float,double)
      • 字符型(char)
      • 布尔型(boolean)
    • 引用数据类型
      • 类(class)
      • 接口(interface)
      • 数组([])

    Java基本数据类型图

    switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上

    在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。

    用最有效率的方法计算 2 乘以 8

    2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)。

    Math.round(11.5) 等于多少?Math.round(-11.5)等于多少

    Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。

    float f=3.4;是否正确

    不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成 float f =3.4F;。

    short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗

    对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。

    而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换。

    编码

    Java语言采用何种编码方案?有何特点?

    Java语言采用Unicode编码标准,Unicode(标准码),它为每个字符制订了一个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。

    注释

    什么Java注释

    定义:用于解释说明程序的文字

    分类

    • 单行注释
      格式: // 注释文字
    • 多行注释
      格式: /* 注释文字 */
    • 文档注释
      格式:/** 注释文字 */

    作用

    在程序中,尤其是复杂的程序中,适当地加入注释可以增加程序的可读性,有利于程序的修改、调试和交流。注释的内容在程序编译的时候会被忽视,不会产生目标代码,注释的部分不会对程序的执行结果产生任何影响。

    注意事项:多行和文档注释都不能嵌套使用。

    访问修饰符

    访问修饰符 public,private,protected,以及不写(默认)时的区别

    定义:Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。

    分类

    private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
    default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
    protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
    public : 对所有类可见。使用对象:类、接口、变量、方法

    访问修饰符图

    运算符

    &和&&的区别

    &运算符有两种用法:(1)按位与;(2)逻辑与。

    &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。

    注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。

    关键字

    Java 有没有 goto

    goto 是 Java 中的保留字,在目前版本的 Java 中没有使用。

    final 有什么用?

    用于修饰类、属性和方法;

    • 被final修饰的类不可以被继承
    • 被final修饰的方法不可以被重写
    • 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的

    final finally finalize区别

    • final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表
      示该变量是一个常量不能被重新赋值。
    • finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块
      中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
    • finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调
      用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的
      最后判断。

    this关键字的用法

    this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。

    this的用法在java中大体可以分为3种:

    1.普通的直接引用,this相当于是指向当前对象本身。

    2.形参与成员名字重名,用this来区分:

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    

    3.引用本类的构造函数

    class Person{
        private String name;
        private int age;
        
        public Person() {
        }
     
        public Person(String name) {
            this.name = name;
        }
        public Person(String name, int age) {
            this(name);
            this.age = age;
        }
    }
    

    super关键字的用法

    super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。

    super也有三种用法:

    1.普通的直接引用

    与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。

    2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分

    class Person{
        protected String name;
     
        public Person(String name) {
            this.name = name;
        }
     
    }
     
    class Student extends Person{
        private String name;
     
        public Student(String name, String name1) {
            super(name);
            this.name = name1;
        }
     
        public void getInfo(){
            System.out.println(this.name);      //Child
            System.out.println(super.name);     //Father
        }
     
    }
    
    public class Test {
        public static void main(String[] args) {
           Student s1 = new Student("Father","Child");
           s1.getInfo();
     
        }
    }
    

    3.引用父类构造函数

    3、引用父类构造函数

    • super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
    • this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。

    this与super的区别

    • super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)
    • this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)
    • super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。
    • super()和this()均需放在构造方法内第一行。
    • 尽管可以用this调用一个构造器,但却不能调用两个。
    • this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
    • this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
    • 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。

    static存在的主要意义

    static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法

    static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。

    为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。

    static的独特之处

    1、被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享

    怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩?

    2、在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。

    3、static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的!

    4、被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。

    static应用场景

    因为static是被类的实例对象所共享,因此如果某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量

    因此比较常见的static应用场景有:

    1、修饰成员变量 2、修饰成员方法 3、静态代码块 4、修饰类【只能修饰内部类也就是静态内部类】 5、静态导包

    static注意事项

    1、静态只能访问静态。 2、非静态既可以访问非静态的,也可以访问静态的。

    流程控制语句

    break ,continue ,return 的区别及作用

    break 跳出总上一层循环,不再执行循环(结束当前的循环体)

    continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)

    return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)

    在 Java 中,如何跳出当前的多重嵌套循环

    在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如:

    public static void main(String[] args) {
        ok:
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; j++) {
                System.out.println("i=" + i + ",j=" + j);
                if (j == 5) {
                    break ok;
                }
    
            }
        }
    }
    

    面向对象

    面向对象概述

    面向对象和面向过程的区别

    面向过程

    优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。

    缺点:没有面向对象易维护、易复用、易扩展

    面向对象

    优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护

    缺点:性能比面向过程低

    面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。

    面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。

    面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。

    面向对象三大特性

    面向对象的特征有哪些方面

    面向对象的特征主要有以下几个方面

    抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

    封装

    封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。

    继承

    继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。

    关于继承如下 3 点请记住:

    1. 子类拥有父类非 private 的属性和方法。

    2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。

    3. 子类可以用自己的方式实现父类的方法。(以后介绍)。

    多态

    所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

    在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。

    其中Java 面向对象编程三大特性:封装 继承 多态

    封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。

    继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承可以提高代码复用性。继承是多态的前提。

    关于继承如下 3 点请记住

    1. 子类拥有父类非 private 的属性和方法。

    2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。

    3. 子类可以用自己的方式实现父类的方法。

    多态性:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。

    在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。

    方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。

    一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:

    • 方法重写(子类继承父类并重写父类中已有的或抽象的方法);
    • 对象造型(用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。

    什么是多态机制?Java语言是如何实现多态的?

    所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

    多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。

    多态的实现

    Java实现多态有三个必要条件:继承、重写、向上转型。

    继承:在多态中必须存在有继承关系的子类和父类。

    重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

    向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

    只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。

    对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。

    面向对象五大基本原则是什么(可选)

    • 单一职责原则SRP(Single Responsibility Principle)
      类的功能要单一,不能包罗万象,跟杂货铺似的。
    • 开放封闭原则OCP(Open-Close Principle)
      一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。
    • 里式替换原则LSP(the Liskov Substitution Principle LSP)
      子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~
    • 依赖倒置原则DIP(the Dependency Inversion Principle DIP)
      高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。
    • 接口分离原则ISP(the Interface Segregation Principle ISP)
      设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。

    类与接口

    抽象类和接口的对比

    抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。

    从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

    相同点

    • 接口和抽象类都不能实例化
    • 都位于继承的顶端,用于被其他实现或继承
    • 都包含抽象方法,其子类都必须覆写这些抽象方法

    不同点

    参数抽象类接口
    声明抽象类使用abstract关键字声明接口使用interface关键字声明
    实现子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现
    构造器抽象类可以有构造器接口不能有构造器
    访问修饰符抽象类中的方法可以是任意访问修饰符接口方法默认修饰符是public。并且不允许定义为 private 或者 protected
    多继承一个类最多只能继承一个抽象类一个类可以实现多个接口
    字段声明抽象类的字段声明可以是任意的接口的字段默认都是 static 和 final 的

    备注:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。

    现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。

    接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:

    • 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。
    • 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。

    普通类和抽象类有哪些区别?

    • 普通类不能包含抽象方法,抽象类可以包含抽象方法。
    • 抽象类不能直接实例化,普通类可以直接实例化。

    抽象类能使用 final 修饰吗?

    不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类

    创建一个对象用什么关键字?对象实例与对象引用有何不同?

    new关键字,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)

    变量与方法

    成员变量与局部变量的区别有哪些

    变量:在程序执行的过程中,在某个范围内其值可以发生改变的量。从本质上讲,变量其实是内存中的一小块区域

    成员变量:方法外部,类内部定义的变量

    局部变量:类的方法中的变量。

    成员变量和局部变量的区别

    作用域

    成员变量:针对整个类有效。
    局部变量:只在某个范围内有效。(一般指的就是方法,语句体内)

    存储位置

    成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。
    局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。

    生命周期

    成员变量:随着对象的创建而存在,随着对象的消失而消失
    局部变量:当方法调用完,或者语句结束后,就自动释放。

    初始值

    成员变量:有默认初始值。

    局部变量:没有默认初始值,使用前必须赋值。

    使用原则

    在使用变量时需要遵循的原则为:就近原则
    首先在局部范围找,有就使用;接着在成员位置找。

    在Java中定义一个不做事且没有参数的构造方法的作用

    Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。

    在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?

    帮助子类做初始化工作。

    一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么?

    主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。

    构造方法有哪些特性?

    名字与类名相同;

    没有返回值,但不能用void声明构造函数;

    生成类的对象时自动执行,无需调用。

    静态变量和实例变量区别

    静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。

    实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。

    静态变量与普通变量区别

    static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

    还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。

    静态方法和实例方法有何不同?

    静态方法和实例方法的区别主要体现在两个方面:

    1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
    2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制

    在一个静态方法内调用一个非静态成员为什么是非法的?

    由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。

    什么是方法的返回值?返回值的作用是什么?

    方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作!

    内部类

    什么是内部类?

    在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。

    内部类的分类有哪些

    内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类

    静态内部类

    定义在类内部的静态类,就是静态内部类。

    public class Outer {
    
        private static int radius = 1;
    
        static class StaticInner {
            public void visit() {
                System.out.println("visit outer static  variable:" + radius);
            }
        }
    }
    

    静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式,new 外部类.静态内部类(),如下:

    Outer.StaticInner inner = new Outer.StaticInner();
    inner.visit();
    
    成员内部类

    定义在类内部,成员位置上的非静态类,就是成员内部类。

    public class Outer {
    
        private static  int radius = 1;
        private int count =2;
        
         class Inner {
            public void visit() {
                System.out.println("visit outer static  variable:" + radius);
                System.out.println("visit outer   variable:" + count);
            }
        }
    }
    

    成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式外部类实例.new 内部类(),如下:

    Outer outer = new Outer();
    Outer.Inner inner = outer.new Inner();
    inner.visit();
    
    局部内部类

    定义在方法中的内部类,就是局部内部类。

    public class Outer {
    
        private  int out_a = 1;
        private static int STATIC_b = 2;
    
        public void testFunctionClass(){
            int inner_c =3;
            class Inner {
                private void fun(){
                    System.out.println(out_a);
                    System.out.println(STATIC_b);
                    System.out.println(inner_c);
                }
            }
            Inner  inner = new Inner();
            inner.fun();
        }
        public static void testStaticFunctionClass(){
            int d =3;
            class Inner {
                private void fun(){
                    // System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量
                    System.out.println(STATIC_b);
                    System.out.println(d);
                }
            }
            Inner  inner = new Inner();
            inner.fun();
        }
    }
    

    定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,new 内部类(),如下:

     public static void testStaticFunctionClass(){
        class Inner {
        }
        Inner  inner = new Inner();
     }
    
    匿名内部类

    匿名内部类就是没有名字的内部类,日常开发中使用的比较多。

    public class Outer {
    
        private void test(final int i) {
            new Service() {
                public void method() {
                    for (int j = 0; j < i; j++) {
                        System.out.println("匿名内部类" );
                    }
                }
            }.method();
        }
     }
     //匿名内部类必须继承或实现一个已有的接口 
     interface Service{
        void method();
    }
    

    除了没有名字,匿名内部类还有以下特点:

    • 匿名内部类必须继承一个抽象类或者实现一个接口。
    • 匿名内部类不能定义任何静态成员和静态方法。
    • 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。
    • 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。

    匿名内部类创建方式:

    new/接口{ 
      //匿名内部类实现部分
    }
    

    内部类的优点

    我们为什么要使用内部类呢?因为它有以下优点:

    • 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据!
    • 内部类不为同一包的其他类所见,具有很好的封装性;
    • 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。
    • 匿名内部类可以很方便的定义回调。

    内部类有哪些应用场景

    1. 一些多算法场合
    2. 解决一些非面向对象的语句块。
    3. 适当使用内部类,使得代码更加灵活和富有扩展性。
    4. 当某个类除了它的外部类,不再被其他的类使用时。

    局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?

    局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?

    先看这段代码:

    public class Outer {
    
        void outMethod(){
            final int a =10;
            class Inner {
                void innerMethod(){
                    System.out.println(a);
                }
    
            }
        }
    }
    

    以上例子,为什么要加final呢?是因为生命周期不一致, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。

    内部类相关,看程序说出运行结果

    public class Outer {
        private int age = 12;
    
        class Inner {
            private int age = 13;
            public void print() {
                int age = 14;
                System.out.println("局部变量:" + age);
                System.out.println("内部类变量:" + this.age);
                System.out.println("外部类变量:" + Outer.this.age);
            }
        }
    
        public static void main(String[] args) {
            Outer.Inner in = new Outer().new Inner();
            in.print();
        }
    
    }
    

    运行结果:

    局部变量:14
    内部类变量:13
    外部类变量:12
    

    重写与重载

    构造器(constructor)是否可被重写(override)

    构造器不能被继承,因此不能被重写,但可以被重载。

    重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?

    方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

    重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分

    重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。

    对象相等判断

    == 和 equals 的区别是什么

    == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)

    equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

    情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。

    情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

    举个例子:

    public class test1 {
        public static void main(String[] args) {
            String a = new String("ab"); // a 为一个引用
            String b = new String("ab"); // b为另一个引用,对象的内容一样
            String aa = "ab"; // 放在常量池中
            String bb = "ab"; // 从常量池中查找
            if (aa == bb) // true
                System.out.println("aa==bb");
            if (a == b) // false,非同一对象
                System.out.println("a==b");
            if (a.equals(b)) // true
                System.out.println("aEQb");
            if (42 == 42.0) { // true
                System.out.println("true");
            }
        }
    }
    

    说明:

    • String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
    • 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。

    hashCode 与 equals (重要)

    HashSet如何检查重复

    两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?

    hashCode和equals方法的关系

    面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”

    hashCode()介绍

    hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。

    散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

    为什么要有 hashCode

    我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode

    当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

    hashCode()与equals()的相关规定

    如果两个对象相等,则hashcode一定也是相同的

    两个对象相等,对两个对象分别调用equals方法都返回true

    两个对象有相同的hashcode值,它们也不一定是相等的

    因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖

    hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

    对象的相等与指向他们的引用相等,两者有什么不同?

    对象的相等 比的是内存中存放的内容是否相等而 引用相等 比较的是他们指向的内存地址是否相等。

    值传递

    当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递

    是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的

    为什么 Java 中只有值传递

    首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。 它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。

    Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。

    下面通过 3 个例子来给大家说明

    example 1

    public static void main(String[] args) {
        int num1 = 10;
        int num2 = 20;
    
        swap(num1, num2);
    
        System.out.println("num1 = " + num1);
        System.out.println("num2 = " + num2);
    }
    
    public static void swap(int a, int b) {
        int temp = a;
        a = b;
        b = temp;
    
        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }
    

    结果

    a = 20
    b = 10
    num1 = 10
    num2 = 20
    

    解析

    img

    在swap方法中,a、b的值进行交换,并不会影响到 num1、num2。因为,a、b中的值,只是从 num1、num2 的复制过来的。也就是说,a、b相当于num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。

    通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example2.

    example 2

        public static void main(String[] args) {
            int[] arr = { 1, 2, 3, 4, 5 };
            System.out.println(arr[0]);
            change(arr);
            System.out.println(arr[0]);
        }
    
        public static void change(int[] array) {
            // 将数组的第一个元素变为0
            array[0] = 0;
        }
    

    结果

    1
    0
    

    解析

    img

    array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的时同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。

    通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。

    很多程序设计语言(特别是,C++和Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为Java程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。

    example 3

    public class Test {
    
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            Student s1 = new Student("小张");
            Student s2 = new Student("小李");
            Test.swap(s1, s2);
            System.out.println("s1:" + s1.getName());
            System.out.println("s2:" + s2.getName());
        }
    
        public static void swap(Student x, Student y) {
            Student temp = x;
            x = y;
            y = temp;
            System.out.println("x:" + x.getName());
            System.out.println("y:" + y.getName());
        }
    }
    

    结果

    x:小李
    y:小张
    s1:小张
    s2:小李
    

    解析

    交换之前:

    img

    交换之后:

    img

    通过上面两张图可以很清晰的看出: 方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝

    总结

    Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。

    下面再总结一下Java中方法参数的使用情况:

    • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型》
    • 一个方法可以改变一个对象参数的状态。
    • 一个方法不能让对象参数引用一个新的对象。

    值传递和引用传递有什么区别

    值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。

    引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。

    Java包

    JDK 中常用的包有哪些

    • java.lang:这个是系统的基础类;
    • java.io:这里面是所有输入输出有关的类,比如文件操作等;
    • java.nio:为了完善 io 包中的功能,提高 io 包中性能而写的一个新包;
    • java.net:这里面是与网络有关的类;
    • java.util:这个是系统辅助类,特别是集合类;
    • java.sql:这个是数据库操作的类。

    import java和javax有什么区别

    刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来说使用。然而随着时间的推移,javax 逐渐的扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包将是太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。

    所以,实际上java和javax没有区别。这都是一个名字。

    IO流

    java 中 IO 流分为几种?

    • 按照流的流向分,可以分为输入流和输出流;
    • 按照操作单元划分,可以划分为字节流和字符流;
    • 按照流的角色划分为节点流和处理流。

    Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。

    • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
    • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

    按操作方式分类结构图:

    IO-操作方式分类

    按操作对象分类结构图:

    IO-操作对象分类

    BIO,NIO,AIO 有什么区别?

    简答

    • BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
    • NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
    • AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

    详细回答

    • BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
    • NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
    • AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

    Files的常用方法都有哪些?

    • Files. exists():检测文件路径是否存在。
    • Files. createFile():创建文件。
    • Files. createDirectory():创建文件夹。
    • Files. delete():删除一个文件或目录。
    • Files. copy():复制文件。
    • Files. move():移动文件。
    • Files. size():查看文件个数。
    • Files. read():读取文件。
    • Files. write():写入文件。

    反射

    什么是反射机制?

    JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

    静态编译和动态编译

    • **静态编译:**在编译时确定类型,绑定对象
    • **动态编译:**运行时确定类型,绑定对象

    反射机制优缺点

    • 优点: 运行期类型的判断,动态加载类,提高代码灵活度。
    • 缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。

    反射机制的应用场景有哪些?

    反射是框架设计的灵魂。

    在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。

    举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性

    Java获取反射的三种方法

    1.通过new对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制

    public class Student {
        private int id;
        String name;
        protected boolean sex;
        public float score;
    }
    
    public class Get {
        //获取反射机制三种方式
        public static void main(String[] args) throws ClassNotFoundException {
            //方式一(通过建立对象)
            Student stu = new Student();
            Class classobj1 = stu.getClass();
            System.out.println(classobj1.getName());
            //方式二(所在通过路径-相对路径)
            Class classobj2 = Class.forName("fanshe.Student");
            System.out.println(classobj2.getName());
            //方式三(通过类名)
            Class classobj3 = Student.class;
            System.out.println(classobj3.getName());
        }
    }
    

    网络编程

    网络编程的面试题可以查看我的这篇文章重学TCP/IP协议和三次握手四次挥手,内容不仅包括TCP/IP协议和三次握手四次挥手的知识,还包括计算机网络体系结构,HTTP协议,get请求和post请求区别,session和cookie的区别等,欢迎大家阅读。

    常用API

    String相关

    字符型常量和字符串常量的区别

    1. 形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符
    2. 含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)
    3. 占内存大小 字符常量只占两个字节 字符串常量占若干个字节(至少一个字符结束标志)

    什么是字符串常量池?

    字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。

    String 是最基本的数据类型吗

    不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(referencetype),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。

    这是很基础的东西,但是很多初学者却容易忽视,Java 的 8 种基本数据类型中不包括 String,基本数据类型中用来描述文本数据的是 char,但是它只能表示单个字符,比如 ‘a’,‘好’ 之类的,如果要描述一段文本,就需要用多个 char 类型的变量,也就是一个 char 类型数组,比如“你好” 就是长度为2的数组 char[] chars = {‘你’,‘好’};

    但是使用数组过于麻烦,所以就有了 String,String 底层就是一个 char 类型的数组,只是使用的时候开发者不需要直接操作底层数组,用更加简便的方式即可完成对字符串的使用。

    String有哪些特性

    • 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。

    • 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。

    • final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。

    String为什么是不可变的吗?

    简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下图所以:

    /** The value is used for character storage. */
    private final char value[];
    

    String真的是不可变的吗?

    我觉得如果别人问这个问题的话,回答不可变就可以了。 下面只是给大家看两个有代表性的例子:

    1) String不可变但不代表引用不可以变

    String str = "Hello";
    str = str + " World";
    System.out.println("str=" + str);
    

    结果:

    str=Hello World
    

    解析:

    实际上,原来String的内容是不变的,只是str由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。

    2) 通过反射是可以修改所谓的“不可变”对象

    // 创建字符串"Hello World", 并赋给引用s
    String s = "Hello World";
    
    System.out.println("s = " + s); // Hello World
    
    // 获取String类中的value字段
    Field valueFieldOfString = String.class.getDeclaredField("value");
    
    // 改变value属性的访问权限
    valueFieldOfString.setAccessible(true);
    
    // 获取s对象上的value属性的值
    char[] value = (char[]) valueFieldOfString.get(s);
    
    // 改变value所引用的数组中的第5个字符
    value[5] = '_';
    
    System.out.println("s = " + s); // Hello_World
    

    结果:

    s = Hello World
    s = Hello_World
    

    解析:

    用反射可以访问私有成员, 然后反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。

    是否可以继承 String 类

    String 类是 final 类,不可以被继承。

    String str="i"与 String str=new String(“i”)一样吗?

    不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。

    String s = new String(“xyz”);创建了几个字符串对象

    两个对象,一个是静态区的"xyz",一个是用new创建在堆上的对象。

    String str1 = "hello"; //str1指向静态区
    String str2 = new String("hello");  //str2指向堆上的对象
    String str3 = "hello";
    String str4 = new String("hello");
    System.out.println(str1.equals(str2)); //true
    System.out.println(str2.equals(str4)); //true
    System.out.println(str1 == str3); //true
    System.out.println(str1 == str2); //false
    System.out.println(str2 == str4); //false
    System.out.println(str2 == "hello"); //false
    str2 = str1;
    System.out.println(str2 == "hello"); //true
    

    如何将字符串反转?

    使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。

    示例代码:

    // StringBuffer reverse
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer. append("abcdefg");
    System. out. println(stringBuffer. reverse()); // gfedcba
    // StringBuilder reverse
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder. append("abcdefg");
    System. out. println(stringBuilder. reverse()); // gfedcba
    

    数组有没有 length()方法?String 有没有 length()方法

    数组没有 length()方法 ,有 length 的属性。String 有 length()方法。JavaScript中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。

    String 类的常用方法都有那些?

    • indexOf():返回指定字符的索引。
    • charAt():返回指定索引处的字符。
    • replace():字符串替换。
    • trim():去除字符串两端空白。
    • split():分割字符串,返回一个分割后的字符串数组。
    • getBytes():返回字符串的 byte 类型数组。
    • length():返回字符串长度。
    • toLowerCase():将字符串转成小写字母。
    • toUpperCase():将字符串转成大写字符。
    • substring():截取字符串。
    • equals():字符串比较。

    在使用 HashMap 的时候,用 String 做 key 有什么好处?

    HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。

    String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的

    可变性

    String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。

    线程安全性

    String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。

    性能

    每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

    对于三者使用的总结

    如果要操作少量的数据用 = String

    单线程操作字符串缓冲区 下操作大量数据 = StringBuilder

    多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

    Date相关

    包装类相关

    自动装箱与拆箱

    装箱:将基本类型用它们对应的引用类型包装起来;

    拆箱:将包装类型转换为基本数据类型;

    int 和 Integer 有什么区别

    Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。

    Java 为每个原始类型提供了包装类型:

    原始类型: boolean,char,byte,short,int,long,float,double

    包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double

    Integer a= 127 与 Integer b = 127相等吗

    对于对象引用类型:==比较的是对象的内存地址。
    对于基本数据类型:==比较的是值。

    如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false

    public static void main(String[] args) {
        Integer a = new Integer(3);
        Integer b = 3;  // 将3自动装箱成Integer类型
        int c = 3;
        System.out.println(a == b); // false 两个引用没有引用同一对象
        System.out.println(a == c); // true a自动拆箱成int类型再和c比较
        System.out.println(b == c); // true
    
        Integer a1 = 128;
        Integer b1 = 128;
        System.out.println(a1 == b1); // false
    
        Integer a2 = 127;
        Integer b2 = 127;
        System.out.println(a2 == b2); // true
    }
    

    常用工具类库

    单元测试

    日志

    展开全文
  • 基于最新Spring 5.x,详细介绍ConfigurationClassPostProcessor配置类后处理器的工作流程源码,以及对于重要注解的解析原理。

      基于最新Spring 5.x,详细介绍ConfigurationClassPostProcessor配置类后处理器的工作流程源码,以及对于重要注解的解析原理。

      此前我们讲解IoC容器初始化原理的时候,见识到了很多注解比如@Component(@Repository、@Service、@Controller )、@Autowired、@Resource、@Value、@Lazy、@PreDestroy、@PostConstruct……等等,由于这些注解在IoC容器初始化的过程中比较重要,因此我们就顺带都讲解了。
      实际上,还有一批很重要的注解我们并没有提及,比如常见的@Configuration、@ComponentScan、@ComponentScans、@PropertySource、@PropertySources、@Import、@Bean……,当时没有解析这些注解的原因,是因为在IoC容器初始化的时候,这些注解都不是必须的。但是,对于那些特别希望彻底摆脱XML配置文件的人们来说,这些注解就非常的重要了,并且,这些注解都和一个后处理器相关,那就是ConfigurationClassPostProcessor,该后处理器的内部就对这些注解进行了解析,所以说这个后处理器也是一个非常重要的后处理器,现在就我们来看一下ConfigurationClassPostProcessor的特性,同时学习对于常见其他重要注解的解析原理!
      本文涉及到一些IoC容器初始化的整体流程,如果你没掌握,那么直接阅读本文将会给你带来很大的困扰,如果你想要详细了解IoC容器初始化流程及其相关知识点,那么可以看我的专栏文章,这将会耗费你一定的时间Spring 5.x 源码

    Spring IoC容器初始化源码 系列文章

    Spring IoC容器初始化源码(1)—setConfigLocations设置容器配置信息

    Spring IoC容器初始化源码(2)—prepareRefresh准备刷新、obtainFreshBeanFactory加载XML资源、解析<beans/>标签

    Spring IoC容器初始化源码(3)—parseDefaultElement、parseCustomElement解析默认、扩展标签,registerBeanDefinition注册Bean定义

    Spring IoC容器初始化源码(4)—<context:component-scan/>标签解析、spring.components扩展点、自定义Spring命名空间扩展点

    Spring IoC容器初始化源码(5)—prepareBeanFactory、invokeBeanFactoryPostProcessors、registerBeanPostProcessors方法

    Spring IoC容器初始化源码(6)—finishBeanFactoryInitialization实例化Bean的整体流程以及某些扩展点

    Spring IoC容器初始化源码(7)—createBean实例化Bean的整体流程以及构造器自动注入

    Spring IoC容器初始化源码(8)—populateBean、initializeBean实例化Bean以及其他依赖注入

    < context:property-placeholder/>标签以及PropertySourcesPlaceholderConfigurer占位符解析器源码深度解析

    三万字的ConfigurationClassPostProcessor配置类后处理器源码深度解析

    基于JavaConfig的AnnotationConfigApplicationContext IoC容器初始化源码分析

    文章目录

    1 ConfigurationClassPostProcessor概述

      和前面讲的PropertySourcesPlaceholderConfigurer一样,容器中的ConfigurationClassPostProcessor对象不是凭空产生的。根据我们学习的IoC容器初始化源码的知识,我们知道,当XML配置文件中配置了< context:annotation-config/>或者< context:component-scan/>标签时,在解析这两个标签的过程中会注册一个ConfigurationClassPostProcessor。或者,我们当然也可以手动注册一个ConfigurationClassPostProcessor的bean定义,Spring Boot则自动帮我们注册了一个ConfigurationClassPostProcessor。
      在AnnotationConfigUtils的registerAnnotationConfigProcessors方法中(该方法在解析< context:annotation-config/>或者< context:component-scan/>标签时被调用),可以看到,如果此前没有手动注册名为CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME=“org.springframework.context.annotation.internalConfigurationAnnotationProcessor”的bean定义,那么以该字符串作为beanName注册一个ConfigurationClassPostProcessor类型的bean定义。我们在IoC容器初始化(3)的文章中有详细介绍:
    在这里插入图片描述
      我们首先看看ConfigurationClassPostProcessor的uml类图:
    在这里插入图片描述
      可以看到,ConfigurationClassPostProcessor实现了BeanClassLoaderAware、ResourceLoaderAware、EnvironmentAware的这几个Aware感知接口,那么在创建该类实例的时候,将会回调这些接口的setter方法,将对应的参数设置进去。
      ConfigurationClassPostProcessor还是一个特殊的BeanDefinitionRegistryPostProcessor类型的后处理器,因此将会在容器初始化过程中的refresh方法中的invokeBeanFactoryPostProcessors方法中被实例化并且按顺序回调它的postProcessBeanDefinitionRegistry和postProcessBeanFactory方法,实际上这两个方法就是ConfigurationClassPostProcessor的工作入口,它们被Spring的回调机制自动调用。
      下面我们从ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry和postProcessBeanFactory方法入手来分析它的源码。

    2 postProcessBeanDefinitionRegistry

      该方法在IoC容器初始化的invokeBeanFactoryPostProcessors步骤中被自动回调,在经过此前的obtainFreshBeanFactory方法之后,所有基于XML的bean定义(包括< context:component-scan/>等各种扩展标签引入的bean定义)已被加载,但是并没有加载完全,比如@Bean注解方法、非静态内部类的bean定义、@ComponentScan表示包、@Import引入bean等等……,这一步就是主要干这个事儿,从registry注册表中的已注册的配置类的bean定义中的内部类和其他的配置注解比如@Bean、@Conditional、@Import、@ComponentScan、@PropertySource一并解析,进一步提取 bean 定义并注册到注册表中。
      核心逻辑还是在内部的processConfigBeanDefinitions方法中完成的!

    //---------ConfigurationClassPostProcessor的相关属性
    
    /**
     * 被postProcessBeanDefinitionRegistry方法处理的registry的identityHashCode的缓存,用于防止重复处理
     */
    private final Set<Integer> registriesPostProcessed = new HashSet<>();
    
    /**
     * 被postProcessBeanFactory方法处理的registry的identityHashCode的缓存,用于防止重复处理
     */
    private final Set<Integer> factoriesPostProcessed = new HashSet<>();
    
    /**
     1. ConfigurationClassPostProcessor的方法
     2. <p>
     3. 主要功能就是从registry注册表中的已注册的配置类中进一步提取 bean 定义
     4.  5. @param registry bean定义注册表,实际类型就是当前上下文中的DefaultListableBeanFactory对象
     */
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        /*
         * 校验重复处理
         */
        //获取registry注册表的identityHashCode值作为registryId,实际上就是返回默认hashCode方法的返回值,无论有没有重写hashCode方法
        //一个registry对象有唯一的registryId值
        int registryId = System.identityHashCode(registry);
        //如果registriesPostProcessed缓存包含了该registryId值,那么说明此前该registry已被该ConfigurationClassPostProcessor处理过了
        //Spring不允许同一个ConfigurationClassPostProcessor重复处理同一个registry,直接抛出异常
        if (this.registriesPostProcessed.contains(registryId)) {
            throw new IllegalStateException(
                    "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
        }
        //如果factoriesPostProcessed缓存包含了该registryId值,那么说明此前该registry已被该ConfigurationClassPostProcessor处理过了
        //Spring不允许同一个ConfigurationClassPostProcessor重复处理同一个registry,直接抛出异常
        if (this.factoriesPostProcessed.contains(registryId)) {
            throw new IllegalStateException(
                    "postProcessBeanFactory already called on this post-processor against " + registry);
        }
        //当前registry的registryId加入registriesPostProcessed缓存
        this.registriesPostProcessed.add(registryId);
        /*
         * 继续调用processConfigBeanDefinitions方法,这是核心方法
         */
        processConfigBeanDefinitions(registry);
    }
    

    2.1 processConfigBeanDefinitions处理配置类bean定义

      该方法是处理配置类bean定义的核心方法,定义了大概骨架步骤:

    1. 获取注册表中所有beanName数组,通过checkConfigurationClassCandidate方法判断依次判断是否是配置类并且设置属性,并封装为一个BeanDefinitionHolder加入configCandidates集合。
    2. 如果configCandidates集合没有收集到任何配置类定义,那么直接返回,不需要进入下一步。
    3. 对配置类根据@Order注解进行排序,按照排序顺序先后解析每一个的配置类定义,主要涉及到parse和loadBeanDefinitions这两个核心方法,也就是对配置类的各种注解的解析和bean定义的注册,逻辑很复杂,后面有源码详解。
    4. 解析完毕之后,如果发现由于解析配置类而新增了bean定义,那么对这些新增的bean定义同样看作配置类,并且继续第三步循环处理,直到最后不再有新增的bean定义。
    /**
     * ConfigurationClassUtils的属性
     * org.springframework.context.annotation.ConfigurationClassPostProcessor.configurationClass属性名
     * 用于标记该配置类是否已被处理过了
     */
    public static final String CONFIGURATION_CLASS_ATTRIBUTE =
            Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "configurationClass");
    
    //-----------ConfigurationClassPostProcessor的相关属性-----------
    
    /**
     * 是否有本地beanName生成器,默认false
     */
    private boolean localBeanNameGeneratorSet = false;
    
    /**
     * AnnotationBeanNameGenerator类型的beanName生成器
     * 默认作为@ComponentScan组件扫描注解的解析器componentScanParser 的beanName生成器
     */
    private BeanNameGenerator componentScanBeanNameGenerator = AnnotationBeanNameGenerator.INSTANCE;
    
    /**
     * 用在注册配置类本身作为bean定义的时候,即被引入的bean的beanName生成器。
     * registerBeanDefinitionForImportedConfigurationClass方法中使用
     */
    private BeanNameGenerator importBeanNameGenerator = IMPORT_BEAN_NAME_GENERATOR;
    
    /**
     * FullyQualifiedAnnotationBeanNameGenerator类型的beanName生成器
     * 默认情况下,使用完全限定的类名作为默认的 bean 名称。
     */
    public static final AnnotationBeanNameGenerator IMPORT_BEAN_NAME_GENERATOR =
            new FullyQualifiedAnnotationBeanNameGenerator();
    
    /**
     * 环境变量,在创建该类实例时通过EnvironmentAware自动设置
     */
    @Nullable
    private Environment environment;
    
    /**
     * 问题报告器,用于抛出异常
     */
    private ProblemReporter problemReporter = new FailFastProblemReporter();
    
    /**
     * 资源加载器,用于加载外部文件资源
     */
    private ResourceLoader resourceLoader = new DefaultResourceLoader();
    
    /**
     * 元数据读取器工厂
     */
    private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory();
    
    /**
     * bean定义加载器
     */
    @Nullable
    private ConfigurationClassBeanDefinitionReader reader;
    /**
     * 手动注册的一个beanName
     */
    private static final String IMPORT_REGISTRY_BEAN_NAME =
            ConfigurationClassPostProcessor.class.getName() + ".importRegistry";
    
    
    /**
     * ConfigurationClassPostProcessor的方法
     * <p>
     * 主要功能就是从registry注册表中的已注册的配置类中进一步提取 bean 定义
     *
     * @param registry bean定义注册表,实际类型就是当前上下文中的DefaultListableBeanFactory对象
     */
    public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
        //临时集合,用于存储配置类的bean定义
        List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
        //返回此注册表中已注册的所有 bean 定义的名称数组
        String[] candidateNames = registry.getBeanDefinitionNames();
        /*
         * 1 遍历beanName数组,根据bean定义判断配置类,设置属性,并封装为一个BeanDefinitionHolder加入configCandidates集合
         */
        for (String beanName : candidateNames) {
            //根据beanName获取对应的bean定义
            BeanDefinition beanDef = registry.getBeanDefinition(beanName);
            /*
             * 获取bean定义的"org.springframework.context.annotation.ConfigurationClassPostProcessor.configurationClass"属性
             * bean定义的父类BeanMetadataAttributeAccessor的方法,如果不为null,说明当前Bean 定义已作为配置类处理过了,这里不再处理
             */
            if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
                }
            }
            /*
             * 否则,调用checkConfigurationClassCandidate校验当前bean定义是否对应着一个配置类,并为其设置相应的属性为lite或者full
             * 如果具有@Configuration注解以及派生注解,那么算作配置类,设置为full
             * 如果具有@Bean,@Component,@ComponentScan,@Import,@ImportResource注解及其派生注解之一,那么算作配置类,那么设置为lite
             * 后面会用到这个属性
             */
            else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
                //如果是配置类,则封装成为一个BeanDefinitionHolder加入到configCandidates集合中
                configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
            }
        }
    
        /*
         * 2 如果configCandidates集合没有收集到任何配置类定义,那么直接返回
         */
        if (configCandidates.isEmpty()) {
            return;
        }
        //到这一步,表示存在配置类
    
        /*
         * 3 将configCandidates集合中的配置类按照此前checkConfigurationClassCandidate解析的@Order注解的值排序,值越小越靠前
         */
        configCandidates.sort((bd1, bd2) -> {
            //获取order值进行比较
            //如果此前没有解析到@Order注解,那么将返回Ordered.LOWEST_PRECEDENCE,即Integer.MAX_VALUE,表示优先级最低,排在末尾
            int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
            int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
            return Integer.compare(i1, i2);
        });
        /*
         * 确定beanName生成器
         */
        SingletonBeanRegistry sbr = null;
        //DefaultListableBeanFactory属于SingletonBeanRegistry类型
        if (registry instanceof SingletonBeanRegistry) {
            sbr = (SingletonBeanRegistry) registry;
            //如果没有自定义的beanName生成器,可通过ConfigurationClassPostProcessor的setBeanNameGenerator方法设置,默认false
            if (!this.localBeanNameGeneratorSet) {
                //获取设置的beanName生成器,一般只有AnnotationConfigApplicationContext上下文的setBeanNameGenerator方法会设置
                //如果是其他上下文容器比如ClassPathXmlApplicationContext就是返回null
                BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
                        AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
                //如果不为null
                if (generator != null) {
                    //那么设置generator为将要使用的beanName生成器
                    this.componentScanBeanNameGenerator = generator;
                    this.importBeanNameGenerator = generator;
                }
            }
        }
        //环境变量,在创建该类实例时通过EnvironmentAware自动设置
        if (this.environment == null) {
            this.environment = new StandardEnvironment();
        }
        /*
         * 4 解析每一个配置类定义
         */
    
        /*
         * 创建配置类解析器,传递:metadataReaderFactory - 元数据解析工厂、problemReporter -问题记录器
         * environment - 环境变量、resourceLoader - 资源加载器、registry - 注册表,就是当前DefaultListableBeanFactory对象
         * componentScanBeanNameGenerator - beanName生成器,默认就是AnnotationBeanNameGenerator
         */
        ConfigurationClassParser parser = new ConfigurationClassParser(
                this.metadataReaderFactory, this.problemReporter, this.environment,
                this.resourceLoader, this.componentScanBeanNameGenerator, registry);
        //保存需要解析的配置类,默认保存configCandidates的中的全部配置类定义
        Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
        //保存已被解析的配置类
        Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
        do {
            /*
             * 通过parser解析器解析每一个配置类
             */
            parser.parse(candidates);
            /*
             * 通过parser解析器验证每一个配置类
             */
            parser.validate();
    
            //获取解析后的ConfigurationClass配置类集合
            Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
            //移除全部的alreadyParsed集合中的数据
            configClasses.removeAll(alreadyParsed);
    
            // Read the model and create bean definitions based on its content
            if (this.reader == null) {
                //创建配置类bean定义阅读器,用于创建BeanDefinition
                this.reader = new ConfigurationClassBeanDefinitionReader(
                        registry, this.sourceExtractor, this.resourceLoader, this.environment,
                        this.importBeanNameGenerator, parser.getImportRegistry());
            }
            /*
             * 通过reader加载、注册此configClasses中的bean定义,其来源包括
             * 包括配置类本身、@Bean方法、@ImportedResource注解、@Import注解
             */
            this.reader.loadBeanDefinitions(configClasses);
            //将configClasses存入已解析的集合中
            alreadyParsed.addAll(configClasses);
            //清空candidates集合
            candidates.clear();
            /*
             * 处理新增了bean定义的情况,将会循环解析新增的bean定义
             */
            //如果解析之后容器中已注册的bean定义数量大于在解析之前的bean定义数量,说明当前方法向注册表中注册了新的bean定义
            if (registry.getBeanDefinitionCount() > candidateNames.length) {
                //获取最新的bean定义名集合
                String[] newCandidateNames = registry.getBeanDefinitionNames();
                //oldCandidateNames保存一份candidateNames的副本
                Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
                //已解析的bean定义的className集合
                Set<String> alreadyParsedClasses = new HashSet<>();
                //将alreadyParsed中的全部configClass的className加入alreadyParsedClasses集合
                for (ConfigurationClass configurationClass : alreadyParsed) {
                    alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
                }
                //遍历最新bean定义名集合
                for (String candidateName : newCandidateNames) {
                    //如果当前遍历的候选beanName没在旧集合中,说明是新添加的
                    if (!oldCandidateNames.contains(candidateName)) {
                        //获取当前candidateName的bean定义
                        BeanDefinition bd = registry.getBeanDefinition(candidateName);
                        //如果当前bean定义是配置类,并且没有被解析过
                        if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
                                !alreadyParsedClasses.contains(bd.getBeanClassName())) {
                            //那么存入candidates集合中,此时candidates集合由有了数据,那么将回进行下一次循环,解析新添加的bean定义
                            candidates.add(new BeanDefinitionHolder(bd, candidateName));
                        }
                    }
                }
                //candidateNames设置最新的newCandidateNames集合
                candidateNames = newCandidateNames;
            }
        }
        //如果candidates集合不为空,那么说明在本次解析过程中新添加了bean定义,继续循环解析这些新的bean定义
        while (!candidates.isEmpty());
    
    
        // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
        //如果注册表中不包含名为"org.springframework.context.annotation.ConfigurationClassPostProcessor.importRegistry"的单例bean实例
        if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
            //那么手动注册一个单例bean实例,名为"org.springframework.context.annotation.ConfigurationClassPostProcessor.importRegistry"
            //实际上就是importStack集合对象
            sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
        }
        //默认就是CachingMetadataReaderFactory
        if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
            //清除外部提供的元数据阅读器工厂MetadataReaderFactory中的缓存
            ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
        }
    }
    

    2.1.1 getAttribute获取属性

      这里的“属性”并不是存储在BeanDefinition中的,而是存放在BeanDefinition实际类型的父类AttributeAccessorSupport的attributes缓存中的,该缓存属于对象级别,每一个bean定义都有自己的attributes缓存,因此不会共享。
      attributes默认是空集合,当调用下面的checkConfigurationClassCandidate方法时才会设置值,因此可用于判断当前bean定义是否已被解析过。

    /**
     * BeanMetadataAttributeAccessor的方法
     * 获取属性,GenericBeanDefinition、ScannedGenericBeanDefinition等都属于BeanMetadataAttributeAccessor类型
     *
     * @param name 属性名
     * @return 属性值
     */
    @Override
    @Nullable
    public Object getAttribute(String name) {
        //继续调用父类AttributeAccessorSupport的方法
        BeanMetadataAttribute attribute = (BeanMetadataAttribute) super.getAttribute(name);
        //如果没有就返回null,否则返回value值
        return (attribute != null ? attribute.getValue() : null);
    }
    
    
    /**
     * AttributeAccessorSupport的属性
     * <p>
     * 使用字符串键和对象值映射,这就是就是所谓的“属性”的缓存位置
     * 该缓存是存放在bean定义的父类AttributeAccessorSupport中的,默认是空集合
     */
    private final Map<String, Object> attributes = new LinkedHashMap<>();
    
    /**
     1. AttributeAccessorSupport的方法
     2. 获取属性
     3.  4. @param name 属性名
     5. @return 属性值
     */
    @Override
    @Nullable
    public Object getAttribute(String name) {
        Assert.notNull(name, "Name must not be null");
        return this.attributes.get(name);
    }
    

    2.1.2 checkConfigurationClassCandidate判断配置类并设置属性

      如果获取到对应属性的值,那么说明当前bean定义没有被解析过,那么调用checkConfigurationClassCandidate解析判断当前bean定义表示的类是否是配置类。该方法位于ConfigurationClassUtils类中,这个类就是专门用于判断给定的bean定义是否是配置类的一个工具类。
      解析之后如果属于配置类那么会设置对应属性的值,后面会使用到,后续再进来的时候也不会再次解析。

    1. 如果具有@Configuration注解及其派生注解标注并且proxyBeanMethods属性的值为true(默认就是true),那么算作配置类,设置属性 CONFIGURATION_CLASS_ATTRIBUTE = full,CONFIGURATION_CLASS_ATTRIBUTE就是“org.springframework.context.annotation.ConfigurationClassPostProcessor.configurationClass”;
      1. proxyBeanMethods属性就表示是否需要代理@Bean方法,即对@Bean方法的调用实施bean生命周期行为,这将通过CGLIB子类拦截方法来完成。
    2. 如果具有@Bean,@Component,@ComponentScan,@Import,@ImportResource注解及其派生注解之一,那么算作配置类,那么设置属性 CONFIGURATION_CLASS_ATTRIBUTE = lite
    3. 如果当前bean定义的类是一个配置类,并且具有@Order注解,那么会设置属性 ORDER_ATTRIBUTE = order值,ORDER_ATTRIBUTE就是“org.springframework.context.annotation.ConfigurationClassPostProcessor.order”;
    4. 这里的属性是设置给当前bean定义实际类型(GenericBeanDefinition、ScannedGenericBeanDefinition等)的父类AttributeAccessorSupport的attributes缓存中的,key为name常量,value为根据name和value封装的一个BeanMetadataAttribute对象。
    //-----------ConfigurationClassUtils的相关属性-------------
    
    /**
     * 属性key
     * org.springframework.context.annotation.ConfigurationClassPostProcessor.configurationClass
     */
    public static final String CONFIGURATION_CLASS_ATTRIBUTE =
            Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "configurationClass");
    
    /**
     * 属性key
     * org.springframework.context.annotation.ConfigurationClassPostProcessor.order
     * 如果当前bean定义的类是一个配置类,并且具有@Order注解,那么会设置属性 ORDER_ATTRIBUTE = order值
     */
    private static final String ORDER_ATTRIBUTE =
            Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "order");
    
    /**
     * 如果当前bean定义的类上具有@Configuration注解,或者以@Configuration注解为元注解的注解(派生注解)
     * 那么表示一个配置类,并且设置属性 CONFIGURATION_CLASS_ATTRIBUTE = full
     */
    public static final String CONFIGURATION_CLASS_FULL = "full";
    /**
     * 如果当前bean定义的类上具有@Bean,@Component,@ComponentScan,@Import,@ImportResource注解之一,或者以这些注解为元注解的注解(派生注解)
     * 那么表示一个配置类,并且设置属性 CONFIGURATION_CLASS_ATTRIBUTE = lite
     */
    public static final String CONFIGURATION_CLASS_LITE = "lite";
    
    /**
     * ConfigurationClassUtils的方法
     * <p>
     * 如果当前检查的类上没有@
     * 检查给定的 bean 定义是否是配置类并相应地标记它
     *
     * @param beanDef               要检查的bean定义
     * @param metadataReaderFactory 调用方正在使用的当前beanFactory工厂
     * @return 是否是配置类,true 是 false 否
     */
    public static boolean checkConfigurationClassCandidate(
            BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
        //获取类型名,如果为null或者工厂方法名不为null,说明不是普通bean定义或者是工厂方法bean,返回false
        String className = beanDef.getBeanClassName();
        if (className == null || beanDef.getFactoryMethodName() != null) {
            return false;
        }
        AnnotationMetadata metadata;
        //如果是支持注解类型的bean定义,如果是采用组件注解添加的bean定义那么支持,并且元数据来自同一个类
        if (beanDef instanceof AnnotatedBeanDefinition &&
                className.equals(((AnnotatedBeanDefinition) beanDef).getMetadata().getClassName())) {
            //获取类元数据
            metadata = ((AnnotatedBeanDefinition) beanDef).getMetadata();
        }
        /*
         * 如果是普通类型的bean定义,也就是通过XML配置添加的bean定义
         */
        else if (beanDef instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) beanDef).hasBeanClass()) {
            // Check already loaded Class if present...
            // since we possibly can't even load the class file for this Class.
            //获取所属类型
            Class<?> beanClass = ((AbstractBeanDefinition) beanDef).getBeanClass();
            //如果类型属于BeanFactoryPostProcessor,或者属于BeanPostProcessor,或者属于AopInfrastructureBean,或者属于EventListenerFactory
            //那么返回false,表示不是配置类
            if (BeanFactoryPostProcessor.class.isAssignableFrom(beanClass) ||
                    BeanPostProcessor.class.isAssignableFrom(beanClass) ||
                    AopInfrastructureBean.class.isAssignableFrom(beanClass) ||
                    EventListenerFactory.class.isAssignableFrom(beanClass)) {
                return false;
            }
            //获取类元数据
            metadata = AnnotationMetadata.introspect(beanClass);
        }
        /*
         * 否则
         */
        else {
            try {
                //直接获取元数据
                MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className);
                metadata = metadataReader.getAnnotationMetadata();
            } catch (IOException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not find class file for introspecting configuration annotations: " +
                            className, ex);
                }
                return false;
            }
        }
        /*
         * 解析类元数据,判断是否是配置类
         */
    
        //获取该类上的@Configuration注解的属性映射map,包括以@Configuration注解为元注解的注解
        Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
        /*
         * 如果config不为null,表示存在@Configuration注解获取以@Configuration注解为元注解的注解
         * 并且proxyBeanMethods属性的值为true,默认就是true
         */
        if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
            //那么设置当前bean定义的属性,bean定义的父类BeanMetadataAttributeAccessor的方法
            //org.springframework.context.annotation.ConfigurationClassPostProcessor.configurationClass = full
            beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
        }
        /*
         * 否则,如果是其他配置类
         * */
        else if (config != null || isConfigurationCandidate(metadata)) {
            //那么设置当前bean定义的属性,bean定义的父类BeanMetadataAttributeAccessor的方法
            //org.springframework.context.annotation.ConfigurationClassPostProcessor.configurationClass = lite
            beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
        }
        /*
         * 否则,表示不是配置类,返回false
         */
        else {
            return false;
        }
        //到这里,表示属于配置类
        //确定给定配置类元数据的顺序
        //获取当前bean定义的@Order注解的值
        Integer order = getOrder(metadata);
        //如果设置了order值
        if (order != null) {
            //那么设置当前bean定义的属性,bean定义的父类BeanMetadataAttributeAccessor的方法
            //org.springframework.context.annotation.ConfigurationClassPostProcessor.order = order值
            beanDef.setAttribute(ORDER_ATTRIBUTE, order);
        }
        //返回true
        return true;
    }
    

    2.1.2.1 isConfigurationCandidate是否是配置类

      如果当前bean定义所属的类没有@Configuration注解以及派生注解,那么继续调用isConfigurationCandidate方法检查:如果类上存在@Component、@ComponentScan、@Import、@ImportResource注解及其派生注解,或者至少有一个方法上具有@Bean注解,那么同样算作配置类。

    /**
     * 配置类的候选者,ConfigurationClassUtils加载的时候就填充了数据
     * 如果某个类具有内部的任意一个注解或者派生注解,那么就算作配置类
     */
    private static final Set<String> candidateIndicators = new HashSet<>(8);
    
    /*
     * 静态块
     */
    static {
        //加入@Component、@ComponentScan、@Import、@ImportResource注解类名
        candidateIndicators.add(Component.class.getName());
        candidateIndicators.add(ComponentScan.class.getName());
        candidateIndicators.add(Import.class.getName());
        candidateIndicators.add(ImportResource.class.getName());
    }
    
    /**
     * 确定是否是配置类
     *
     * @param metadata 类的元数据
     * @return true 是 false 否
     */
    public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
        // 如果是接口,那么不考虑
        if (metadata.isInterface()) {
            return false;
        }
    
        /*
         * 遍历candidateIndicators集合,如果当前类具有@Component、@ComponentScan、@Import、@ImportResource注解及其派生注解的任何一个
         * 那么算作配置类
         */
        for (String indicator : candidateIndicators) {
            if (metadata.isAnnotated(indicator)) {
                return true;
            }
        }
        //上面的循环没确定结果,那么继续查找@Bean注解标注的方法
        try {
            //如果该类的存在至少一个具有@Bean注解及其派生注解标注的方法,那么算作配置类
            return metadata.hasAnnotatedMethods(Bean.class.getName());
        } catch (Throwable ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Failed to introspect @Bean methods on class [" + metadata.getClassName() + "]: " + ex);
            }
            return false;
        }
    }
    

    2.1.2.2 getOrder获取@Order注解值

      getOrder用于获取当前bean定义所属的类上的@Order注解的值,如果存在该注解,那么后续还会添加一个ORDER_ATTRIBUTE属性到当前bean定义中。

    /**
     * 获取@Order注解的值,用于确定给定配置类元数据的顺序
     *
     * @param metadata 类元数据
     * @return 配置类的@Order注解的值,如果没有@Order注解,那么返回null
     */
    @Nullable
    public static Integer getOrder(AnnotationMetadata metadata) {
        Map<String, Object> orderAttributes = metadata.getAnnotationAttributes(Order.class.getName());
        return (orderAttributes != null ? ((Integer) orderAttributes.get(AnnotationUtils.VALUE)) : null);
    }
    

    2.1.2.3 setAttribute设置属性

      如果当前bean定义所属的类是配置类,那么设置key为CONFIGURATION_CLASS_ATTRIBUTE的属性,也就是“org.springframework.context.annotation.ConfigurationClassPostProcessor.configurationClass”,value就是“full”或者“lite”值。
      如果当前bean定义所属的类是配置类并且具有@Order注解,那么设置key为ORDER_ATTRIBUTE的属性,也就是“org.springframework.context.annotation.ConfigurationClassPostProcessor.order”,value就是order值。
      实际存入缓存的值是key和value封装的一个BeanMetadataAttribute对象。

    /**
     * BeanMetadataAttributeAccessor的方法
     * 设置属性,GenericBeanDefinition、ScannedGenericBeanDefinition等都属于BeanMetadataAttributeAccessor类型
     *
     * @param name  属性名
     * @param value 属性值
     */
    @Override
    public void setAttribute(String name, @Nullable Object value) {
        //调用父类BeanMetadataAttributeAccessor的方法
        //key为name,value为根据name和value封装的一个BeanMetadataAttribute对象
        super.setAttribute(name, new BeanMetadataAttribute(name, value));
    }
    
    
    /**
     * AttributeAccessorSupport的属性
     * <p>
     * 使用字符串键和对象值映射,这就是就是属性的缓存位置
     */
    private final Map<String, Object> attributes = new LinkedHashMap<>();
    
    /**
     * AttributeAccessorSupport的方法
     * 设置属性到缓存中
     *
     * @param name  属性名
     * @param value 属性值
     */
    @Override
    public void setAttribute(String name, @Nullable Object value) {
        Assert.notNull(name, "Name must not be null");
        if (value != null) {
            this.attributes.put(name, value);
        } else {
            removeAttribute(name);
        }
    }
    

    2.1.3 getOrder获取顺序值

      获取配置类的order顺序值,也就是配置类上的@Order注解的值,如果未声明则返回Ordered.LOWEST_PRECEDENCE,即Integer.MAX_VALUE,将会排序在集合尾部。

    /**
     * ConfigurationClassUtils的属性
     * 表示具有@Order注解的属性的key
     */
    private static final String ORDER_ATTRIBUTE =
            Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "order");
    
    
    /**
     * ConfigurationClassUtils的方法
     * 获取配置类的order顺序值,也就是
     * 配置类上的@Order注解的值,如果未声明则返回Ordered.LOWEST_PRECEDENCE,即Integer.MAX_VALUE
     *
     * @param beanDef 要检查的bean定义
     * @return 配置类的order顺序值
     */
    public static int getOrder(BeanDefinition beanDef) {
        //获取ORDER_ATTRIBUTE属性值order,这个属性我们在checkConfigurationClassCandidate方法中已尝试解析并设置到属性中了
        //这里直接取出来使用,如果没有@Order注解会返回null
        Integer order = (Integer) beanDef.getAttribute(ORDER_ATTRIBUTE);
        //如果order不为null,那么直接返回,否则返回Ordered.LOWEST_PRECEDENCE,即Integer.MAX_VALUE
        return (order != null ? order : Ordered.LOWEST_PRECEDENCE);
    }
    

    2.1.4 new ConfigurationClassParser创建解析器

      创建一个ConfigurationClassParser解析器对象,专门用于解析找到的配置类定义。初始化一些属性。

    //----------ConfigurationClassParser相关属性-------------
    
    private final MetadataReaderFactory metadataReaderFactory;
    
    private final ProblemReporter problemReporter;
    
    private final Environment environment;
    
    private final ResourceLoader resourceLoader;
    
    private final BeanDefinitionRegistry registry;
    
    private final ComponentScanAnnotationParser componentScanParser;
    
    private final ConditionEvaluator conditionEvaluator;
    
    /**
     * 创建一个ConfigurationClassParser实例,用于解析配置类集合。
     *
     * @param metadataReaderFactory          元数据解析工厂
     * @param problemReporter                问题报告器
     * @param environment                    环境变量
     * @param resourceLoader                 资源加载器
     * @param componentScanBeanNameGenerator beanName生成器,默认就是AnnotationBeanNameGenerator
     * @param registry                       注册表,就是当前DefaultListableBeanFactory对象
     */
    public ConfigurationClassParser(MetadataReaderFactory metadataReaderFactory,
                                    ProblemReporter problemReporter, Environment environment, ResourceLoader resourceLoader,
                                    BeanNameGenerator componentScanBeanNameGenerator, BeanDefinitionRegistry registry) {
        //为一些属性赋值
        this.metadataReaderFactory = metadataReaderFactory;
        this.problemReporter = problemReporter;
        this.environment = environment;
        this.resourceLoader = resourceLoader;
        this.registry = registry;
        //@ComponentScan组件扫描注解的解析器
        this.componentScanParser = new ComponentScanAnnotationParser(
                environment, resourceLoader, componentScanBeanNameGenerator, registry);
        //@Conditional条件注解评估器
        this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
    }
    

    2.1.5 parser.parse解析配置类

      通过ConfigurationClassParser解析器去解析所有的配置类定义。其内部是循环调用processConfigurationClass方法按照此前排序的先后顺序处理每一个配置类,传递的参数是一个ConfigurationClass对象和一个排除类型过滤器。ConfigurationClass表示一个配置类,保存了配置类的相关信息。

    /**
     * ConfigurationClassParser的方法
     * <p>
     * 解析配置类的bean定义集合,实际上也就是AnnotationConfigApplicationContext容器的构造器参数个数
     *
     * @param configCandidates 配置类的bean定义集合
     */
    public void parse(Set<BeanDefinitionHolder> configCandidates) {
        //遍历集合
        for (BeanDefinitionHolder holder : configCandidates) {
            //获取bean定义
            BeanDefinition bd = holder.getBeanDefinition();
            //根据类型调用不同的parse方法,其内部都是调用的processConfigurationClass方法
            try {
                //如果属于AnnotatedBeanDefinition,比如AnnotatedGenericBeanDefinition、ScannedGenericBeanDefinition、ConfigurationClassBeanDefinition
                if (bd instanceof AnnotatedBeanDefinition) {
                    //调用另一个parse方法解析,内部调用processConfigurationClass方法
                    parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
                }
                //否则,如果是AbstractBeanDefinition类型,比如GenericBeanDefinition,并且已经解析了class
                else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
                    //调用另一个parse方法解析,内部调用processConfigurationClass方法
                    parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
                }
                //否则
                else {
                    //调用另一个parse方法解析,内部调用processConfigurationClass方法
                    parse(bd.getBeanClassName(), holder.getBeanName());
                }
            } catch (BeanDefinitionStoreException ex) {
                throw ex;
            } catch (Throwable ex) {
                throw new BeanDefinitionStoreException(
                        "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
            }
        }
        //最后处理在processImports方法解析@Import注解时遇到的DeferredImportSelector,内部同样是调用processImports方法继续递归处理
        this.deferredImportSelectorHandler.process();
    }
    /*
     * 三个parse方法内部都是调用的processConfigurationClass方法,传递的参数是一个ConfigurationClass对象
     * 和一个排除类型过滤器,ConfigurationClass表示一个配置类,保存了配置类的相关信息
     */
    
    /**
     * ConfigurationClassParser的属性
     * <p>
     * 类型排除过滤器,这是一个i嗯lambda对象
     * 如果类名以"java.lang.annotation."或者"org.springframework.stereotype."开头,就返回true,或者返回false
     */
    private static final Predicate<String> DEFAULT_EXCLUSION_FILTER = className ->
            (className.startsWith("java.lang.annotation.") || className.startsWith("org.springframework.stereotype."));
    
    
    protected final void parse(@Nullable String className, String beanName) throws IOException {
        Assert.notNull(className, "No bean class name for configuration class bean definition");
        MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
        processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER);
    }
    
    protected final void parse(Class<?> clazz, String beanName) throws IOException {
        processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER);
    }
    
    protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
        processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
    }
    

    2.1.5.1 processConfigurationClass处理一个配置类

    1. 首先通过shouldSkip判断是否有必要继续解析这个配置类,参数设置的配置生效的阶段为ConfigurationPhase.PARSE_CONFIGURATION,即类的阶段。这是对于@Conditional注解的支持。这个方法我们在此前的IoC容器初始化的doScan扫描包阶段的isCandidateComponent方法中就见过了。
    2. 随后处理被Import引入的配置类的情况;
    3. 最后调用doProcessConfigurationClass核心方法,真正的去解析该配置类及其父类。
    //------------ConfigurationClassParser的相关属性-----------
    
    /**
     * 解析@Conditional条件注解的评估器
     * Spring 4.0 新增的@Conditional条件注解,可以标注在类或者方法上,在容器启动时用于控制一批或者一个bean实例是否被注入
     * 通过判断该注解中指定的条件是否满足,如果不满足则不会将对应的bean注入到容器中,如果满足则会将对应的bean进行注入
     */
    private final ConditionEvaluator conditionEvaluator;
    
    /**
     * 以解析的配置类缓存map
     */
    private final Map<ConfigurationClass, ConfigurationClass> configurationClasses = new LinkedHashMap<>();
    
    /**
     * 已知的超类配置类缓存map
     */
    private final Map<String, ConfigurationClass> knownSuperclasses = new HashMap<>();
    
    /**
     * ConfigurationClassParser的方法
     * <p>
     * 处理每一个配置类
     *
     * @param configClass 表示配置类的对象,包含配置类的一些信息
     * @param filter      类型过滤器
     */
    protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
        /*
         * 1 通过判断类上的@Conditional条件注解是否满足Condition条件来控制是否跳过该配置类的解析
         * 参数设置的配置生效的阶段为ConfigurationPhase.PARSE_CONFIGURATION,即类的阶段
         */
        if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
            return;
        }
        /*
         * 2 处理被@Import引入的bean定义的情况
         */
    
        //将当前configClass作为key,尝试获取configClass集合中的configClass的缓存
        //ConfigurationClass的hashCode和equals方法都被重写了,比较的是内部的className字符串的hashCode
        ConfigurationClass existingClass = this.configurationClasses.get(configClass);
        //如果existingClass不为null,说明此前该类已被@Import引入
        if (existingClass != null) {
            //如果当前configClass的importedBy属性集合不为空,说明当前bean定义是被@Import引入的
            if (configClass.isImported()) {
                //如果此前的existingClass的importedBy属性集合也不为空,说明此前同类型的bean定义也是被@Import引入的
                if (existingClass.isImported()) {
                    //对引入该配置类的引入类进行合并,也就是将当前@Import所在的引入类添加到内部的importedBy属性集合中
                    existingClass.mergeImportedBy(configClass);
                }
                //随后直接返回,即对于同一个被@Import引入的类在configurationClasses集合中只存一个
                // Otherwise ignore new imported config class; existing non-imported class overrides it.
                return;
            }
            //否则,表示当前的bean定义不是被@Import引入的,那么表示属于显示注入的,比如通过组件注解
            //因此需要覆盖此前保存的通过@Import引入的bean定义
            else {
                // Explicit bean definition found, probably replacing an import.
                // Let's remove the old one and go with the new one.
                //移除configurationClasses中之前的configClass
                this.configurationClasses.remove(configClass);
                //移除configurationClasses中之前的configClass
                this.knownSuperclasses.values().removeIf(configClass::equals);
            }
        }
    
        /*
         * 3 递归处理配置类及其超类
         */
    
        //获取当前配置类的SourceClass,这是一个简单的包装器,允许以统一的方式处理带注释的源类,而不管它们如何加载。
        SourceClass sourceClass = asSourceClass(configClass, filter);
        do {
            /*
             * 核心方法,处理每一个配置类及其超类
             */
            sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
        }
        while (sourceClass != null);
        //当前的configClass存入configurationClasses集合,key和value都是同一个对象
        this.configurationClasses.put(configClass, configClass);
    }
    
    2.1.5.1.1 shouldSkip解析@Conditional判断是否跳过

      通过判断class上的@Conditional条件注解是否满足Condition条件来控制是否跳过该配置类的解析。在这里,设置的配置生效的阶段参数为ConfigurationPhase.PARSE_CONFIGURATION,即解析配置类的阶段,如果此时某个Condition是REGISTER_BEAN阶段,那么该Condition无论有没有满足条件都不生效(都算作满足)。
      这里是对@Conditional条件注解的支持,Spring 4.0 新增的@Conditional条件注解,可以标注在类或者方法上,在容器启动时用于控制一批或者一个bean实例是否被注入,通过判断该注解中指定的条件是否满足,如果不满足则不会将对应的bean注入到容器中,如果满足则会将对应的bean进行注入或者进一步处理。
      这个方法我们在此前讲解的IoC容器初始化(3)的文章的doScan扫描包阶段的isCandidateComponent方法内见过了,第二个参数传递的就是null。

    /**
     1. ConditionEvaluator的方法
     2. <p>
     3. 确定是否应基于@Conditional注解跳过此项(类或者方法的解析)
     4. <p>
     5. Spring 4.0 新增的@Conditional条件注解,可以标注在类或者方法上,在容器启动时用于控制一批或者一个bean实例是否被注入
     6. 通过判断该注解中指定的条件是否满足,如果不满足则不会将对应的bean注入到容器中,如果满足则会将对应的bean进行注入
     7.  8. @param metadata 类或者方法元数据
     9. @param phase    配置生效的阶段
     10.                 ConfigurationPhase.PARSE_CONFIGURATION   解析配置类的阶段
     11.                 ConfigurationPhase.REGISTER_BEAN   解析配置类的方法阶段
     12. @return 如果此项应跳过,则返回true,否则返回false
     */
    public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
        //如果元数据所属的类或者方法(根据元数据的类型)上没有@Conditional注解,那么直接返回false
        if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
            return false;
        }
        /*
         * 如果配置生效的阶段为null,那么根据元数据类型自动选择一个阶段重新调用shouldSkip方法
         * 在前面讲的IoC容器初始化的扫描包阶段的isCandidateComponent方法中就调用了这个方法,并且phase参数传递的null
         */
        if (phase == null) {
            //如果是AnnotationMetadata类型,表示当前元数据是类元数据,比如SimpleAnnotationMetadata,那么phase设置为PARSE_CONFIGURATION
            if (metadata instanceof AnnotationMetadata &&
                    ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
                return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
            }
            //否则,表示当前元数据是方法元数据,比如MethodMetadata,那么phase设置为REGISTER_BEAN
            return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
        }
        /*
         * 提取@Conditional注解中配置的全部Condition条件
         */
        List<Condition> conditions = new ArrayList<>();
        for (String[] conditionClasses : getConditionClasses(metadata)) {
            for (String conditionClass : conditionClasses) {
                Condition condition = getCondition(conditionClass, this.context.getClassLoader());
                conditions.add(condition);
            }
        }
        /*
         * 对全部Condition条件进行排序,可以看到采用的是AnnotationAwareOrderComparator比较器
         * 该比较器支持Ordered、PriorityOrdered接口,以及@Order、@Priority注解的排序
         * 排序规则是order值越小排序越靠前,优先级越高
         */
        AnnotationAwareOrderComparator.sort(conditions);
    
        for (Condition condition : conditions) {
            //必须的配置生效的阶段,只有ConfigurationCondition类型的条件才具备该属性
            ConfigurationPhase requiredPhase = null;
            if (condition instanceof ConfigurationCondition) {
                //获取配置生效的阶段
                requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
            }
            //如果配置生效的阶段为null或者与参数的阶段相同,并且当前Condition的条件不满足,那么返回true,表示可以跳过此项目
            if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
                return true;
            }
        }
        //所有Condition都满足,那么返回false,表示不跳过
        return false;
    }
    
    2.1.5.1.1.1 @Conditional注解介绍

      Spring 4.0 新增的@Conditional条件注解,可以标注在类或者方法上,在容器启动时用于控制一批或者一个bean实例是否被注入,通过判断该注解中指定的条件是否满足,如果不满足则不会将对应的bean注入到容器中,如果满足则会将对应的bean进行注入或者进一步处理。

    1. 标注在类上时,如果它内部的Condition条件不满足,那么该类的bean定义以及该类下面的bean定义,比如通过@Bean注解指定的方法表示的bean定义都不会被解析注册。
    2. 标注在方法上时,如果它内部的Condition条件不满足,那么该方法表示的bean定义(通过@Bean注解)不会被解析注册。

      @Conditional的参数是一个类型的数组,类型必须是属于Condition接口体系,这个Condition接口有一个matches方法,该方法返回一个boolean值,返回true表示该匹配条件,返回false则表示不匹配。只有所有的Conditon条件都匹配才算做最终满足条件。该方法的ConditionContext条件变量可以获取各种容器参数,比如Registry、BeanFactory、Environment、ResourceLoader、ClassLoader。

    @FunctionalInterface
    public interface Condition {
        /**
         * 确定条件是否匹配
         *
         * @param context  条件上下文,可以从李米娜获取这种各样的内部参数
         * @param metadata 被检查的类或者方法的元数据
         * @return 条件匹配返回true,条件不匹配返回false
         */
        boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
    }
    

      Condition还有一个特性化子接口ConfigurationCondition。这个接口多了一个getConfigurationPhase方法,用于指定该Conditon的生效阶段,可选类型保存在这个接口内部的ConfigurationPhase枚举中。该接口用的比较少。
      如果某个条件是ConfigurationCondition类型,那么先判断阶段是否匹配,如果匹配,再判断条件是否匹配,如果阶段不匹配,那么该条件不生效,即默认条件匹配。这一点看上面的源码就能看出来。

    public interface ConfigurationCondition extends Condition {
    
        /**
         * 返回该条件的配置生效阶段
         */
        ConfigurationPhase getConfigurationPhase();
    
        /**
         * 配置的阶段枚举
         */
        enum ConfigurationPhase {
    
            /**
             * 解析配置类的阶段
             */
            PARSE_CONFIGURATION,
    
            /**
             * 注册Bean的阶段
             */
            REGISTER_BEAN
        }
    }
    
    2.1.5.1.1.2 @Conditional注解使用

      我们自定义两个Condition:

    /**
     * @author lx
     * @date 2020/11/5 16:16
     */
    public class MyConditional {
    
        /**
         * 用于排序的@Priority注解
         */
        @Priority(2)
        public static class MyConditional1 implements Condition {
    
            @Override
            public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
                Environment environment = context.getEnvironment();
                //如果设置了key为"p"的环境变量,那么该条件满足,否则不满足
                return environment.getProperty("p") != null;
            }
    
        }
    
        @Priority(1)
        public static class MyConditional2 implements Condition {
    
            @Override
            public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    
                return true;
            }
        }
    }
    

      定义一个测试类com.spring.source.conditional.ConditionTest,加上@Conditional注解,包含两个条件:

    /**
     * @author lx
     * @date 2020/11/5 16:16
     */
    @Component
    @Conditional({MyConditional.MyConditional1.class, MyConditional.MyConditional2.class})
    public class ConditionTest { }
    

      配置文件spring-config-conditional.xml:

    <context:component-scan base-package="com.spring.source.conditional"/>
    

      测试:

    @Test
    public void conditional() {
        //设置环境变量
        System.setProperty("p", "x");
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config-conditional.xml");
        System.out.println(ac.getBean("conditionTest"));
    }
    

      由于设置了p环境变量,因此满足条件,所以该bean顶以被注册,能够获取到对应的实例,结果如下:

    com.spring.source.conditional.ConditionTest@1198b989
    

      如果我们将设置环境变量的代码注释掉,继续测试,将会抛出异常:

    No bean named 'conditionTest' available
    

      即,由于条件不满足,造成该bean定义被没有注册和初始化,因此获取不到对应的实例。

    2.1.5.1.2 doProcessConfigurationClass处理一个配置类

      这是对一个配置类的真正的处理方法。将会对配置类的内部类、超类、以及相关注解进行处理,大概步骤为:

    1. 处理内部类:
      1. 如果当前配置类上存在@Component以及派生注解,包括@Repository、@Service、@Controller、@Configuration等注解,那么调用processMemberClasses方法首先处理内部类配置类。
      2. 如果内部类也是一个配置类,那么将会递归调用processConfigurationClass方法先解析内部配置类。非静态内部类的bean定义的注册就是从该方法开始的(因为在doScan扫描包的方法中不会解析、注册非静态内部类的bean定义)。
    2. 处理@PropertySources、@PropertySource注解:
      1. 如果当前配置类上存在@PropertySources或者@PropertySource以及它们的派生注解,那么调用processPropertySource解析这些注解。@PropertySources、@PropertySource用于引入本地本地properties属性源文件,可用来替代XML的配置,比如< context:property-placeholder/>标签。@PropertySources、@PropertySource注解可重复出现。
      2. 这里的properties属性源文件路径支出占位符,将会environment环境变量中查找,而解析出来的本地属性源同样会存入environment环境变量中。
    3. 处理@ComponentScans、@ComponentScan注解:
      1. @ComponentScans、@ComponentScan注解用于配置组件扫描,可用来替代XML的配置,比如< context:component-scan/>标签。
      2. 如果shouldSkip方法返回false,即不应该跳过,这里的phase生效的阶段参数为REGISTER_BEAN,那么调用componentScanParser.parse方法解析配置类上的@ComponentScan注解、扫描bean定义并进行注册,否则跳过解析。
      3. 该注解配置的包路径的最终同样是依靠ClassPathBeanDefinitionScanner的doScan方法进行扫描的,因此和此前< context:component-scan/>的注解解析可以说是殊途同归。
    4. 处理@Import注解:
      1. @Import注解可用于引入一批指定类型的bean定义,这是另一种注册bean定义的方式;通过processImports方法处理配置类上的@Import注解。
      2. 而@Import注解也有三种添加bean定义的方法:普通类型、ImportSelector类型、ImportBeanDefinitionRegistrar类型。注意通过@Import引入的bean定义在这里并不会被注册
    5. 处理@ImportResource注解:
      1. @ImportResource注解可用于引入一批Spring的XML配置文件,如果即基于注解开发又写了XML配置文件,那么可以使用该注解引入Spring的XML配置文件。
      2. 这里仅仅对于@ImportResource注解引入的配置文件路径进行占位符的替换(从environment环境变量中查找),并方法替换后的路径和reader读取器通过addImportedResource方法存入configClass的importedResources缓存中,后续再继续处理。
    6. 处理@Bean注解:
      1. 处理配置类内部的@Bean注解标注的方法,@Bean注解用于通过方法引入bean定义,类似于工厂方法。
      2. 这里同样是仅仅将解析后的方法元数据通过addBeanMethod方法存入configClass的beanMethods缓存中,后续再继续处理。
    7. 处理接口上的默认方法:
      1. 默认方法是Java8的新特性,主要就是对接口上标注了@Bean注解的默认方法进行类似于上一步的处理,存入configClass的beanMethods缓存中,后续再处理。
    8. 处理父类:
      1. 置类的父类将会被同样解析,配置类的父类将会被同样解析,而对于以"java."类路径开头的父类,比如Object,也就是rt.jar包中的Java核心类作为父类时不会被解析。
      2. 如果存在非核心父类,那么返回父类的sourceClass,将会在外层的processConfigurationClass中进行下一次循环解析父类。
    9. 如果没有符合规则的父类或者都解析完毕,那么返回null,将会在外层的processConfigurationClass中跳出循环,进行后续步骤。

      该方法对于通过@ComponentScans、@ComponentScan注解引入的bean定义会添加到了容器中,但是对于@Import、@Bean、@ImportResource注解引入的bean定义并没有注册到容器中,而是在后面的this.reader.loadBeanDefinitions()方法中才会真正的解析和注册。

    //--------------ConfigurationClassParser的相关属性
    /**
     * 解析@Conditional条件注解的评估器
     * Spring 4.0 新增的@Conditional条件注解,可以标注在类或者方法上,在容器启动时用于控制一批或者一个bean实例是否被注入
     * 通过判断该注解中指定的条件是否满足,如果不满足则不会将对应的bean注入到容器中,如果满足则会将对应的bean进行注入
     */
    private final ConditionEvaluator conditionEvaluator;
    
    /**
     * 解析@ComponentScan注解的解析器,在ConfigurationClassParser对象的构造器中被初始化
     */
    private final ComponentScanAnnotationParser componentScanParser;
    
    /**
     * ConfigurationClassParser的方法
     * <p>
     * 通过从sourceClass读取注解、成员和方法,应用处理并构建完整的ConfigurationClass。当发现多个相关sourceClass时,可以多次调用此方法。
     *
     * @param configClass 表示当前配置类的对象
     * @param sourceClass 源类
     * @param filter      类型过滤器
     * @return 超类的sourceClass,如果未找到或以前处理过就返回null
     */
    @Nullable
    protected final SourceClass doProcessConfigurationClass(
            ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
            throws IOException {
        /*
         * 1 处理内部类
         * 如果元数据所属的类上存在@Component以及派生注解,那么调用processMemberClasses方法首先处理内部类配置类
         * 常见包括@Component、@Repository、@Service、@Controller、@Configuration等注解
         */
        if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
            /*
             * 递归处理任何成员(嵌套)类,也就是内部类。内部配置类最终还是会调用processConfigurationClass方法
             */
            processMemberClasses(configClass, sourceClass, filter);
        }
    
        /*
         * 2 处理@PropertySources、@PropertySource注解
         * 如果元数据所属的类上存在@PropertySources或者@PropertySource以及它们的派生注解,那么调用processPropertySource解析这些注解
         * @PropertySources、@PropertySource用于引入本地properties属性源文件,可用来替代XML的配置,比如<context:property-placeholder/>标签
         * @PropertySources、@PropertySource注解可重复出现
         */
    
        /*
         * 调用attributesForRepeatable方法获取元数据所属的类上的全部@PropertySources、@PropertySource注解的属性集合AnnotationAttributes的集合
         * 遍历获取到的全部AnnotationAttributes属性集合
         */
        for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
                sourceClass.getMetadata(), PropertySources.class,
                org.springframework.context.annotation.PropertySource.class)) {
            if (this.environment instanceof ConfigurableEnvironment) {
                /*
                 * 调用processPropertySource处理通过@PropertySource注解引入的属性源,会将引入的属性源加入到environment环境变量中
                 */
                processPropertySource(propertySource);
            } else {
                logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
                        "]. Reason: Environment must implement ConfigurableEnvironment");
            }
        }
    
        /*
         * 3 处理@ComponentScans、@ComponentScan注解
         * @ComponentScans、@ComponentScan注解用于组件扫描,可用来替代XML的配置,比如<context:component-scan/>标签
         */
    
        /*
         * 调用attributesForRepeatable方法获取元数据所属的类上的全部@ComponentScans、@ComponentScan注解的属性集合AnnotationAttributes的集合
         */
        Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
                sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
        //如果存在@ComponentScan注解,并且shouldSkip方法返回false,即不应该跳过,这里的phase生效的阶段参数为REGISTER_BEAN,即注册bean的阶段
        //这两个条件都满足,那么可以解析继续@ComponentScan注解,进而继续扫描包,注册bean定义
        if (!componentScans.isEmpty() &&
                !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
            //遍历componentScans集合
            for (AnnotationAttributes componentScan : componentScans) {
                // The config class is annotated with @ComponentScan -> perform the scan immediately
                /*
                 * 通过解析器解析@ComponentScan注解,获取解析到的bean定义集合
                 */
                Set<BeanDefinitionHolder> scannedBeanDefinitions =
                        this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
                // 检查扫描到的bean定义集合,以查找任何的配置类,并根据需要递归的解析
                for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                    BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
                    if (bdCand == null) {
                        bdCand = holder.getBeanDefinition();
                    }
                    //调用checkConfigurationClassCandidate判断是否是配置类并设置属性
                    if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
                        /*
                         * 如果扫描到的bean定义是配置类,那么调用parse方法解析配置类,内部还是递归调用的processConfigurationClass方法
                         */
                        parse(bdCand.getBeanClassName(), holder.getBeanName());
                    }
                }
            }
        }
        /*
         * 4 处理@Import注解
         * @Import注解可用于引入一批指定类型的bean定义
         */
        // Process any @Import annotations
        processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
    
        /*
         * 5 处理@ImportResource注解
         * @ImportResource注解可用于引入一批Spring的XML配置文件,如果即基于注解开发又写了XML配置文件,那么可以使用该注解引入Spring的XML配置文件
         */
    
        /*
         * 调用attributesFor方法获取元数据所属的类上的@ImportResource注解的属性集合AnnotationAttributes
         */
        AnnotationAttributes importResource =
                AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
        //如果不为null,说明存在@ImportResource注解
        if (importResource != null) {
            //获取locations属性数组,就是XML配置文件的路径字符串
            String[] resources = importResource.getStringArray("locations");
            //获取reader属性,用来读取配置文件中的bean定义
            Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
            //遍历配置文件路径
            for (String resource : resources) {
                //使用environment环境变量解析路径字符串的占位符
                String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
                /*
                 * 仅仅是存入configClass的importedResources缓存中,后续再处理
                 */
                configClass.addImportedResource(resolvedResource, readerClass);
            }
        }
        /*
         * 6 处理配置类内部的@Bean注解标注的方法
         * @Bean注解用于通过方法引入bean定义,类似于工厂方法
         */
    
        // Process individual @Bean methods
        //获取所有被@Bean注解标注的方法的元数据集合
        Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
        for (MethodMetadata methodMetadata : beanMethods) {
            /*
             * 仅仅是存入configClass的beanMethods缓存中,后续再处理
             */
            configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
        }
    
        /*
         * 7 处理接口上的默认方法,Java8的新特性
         * 主要就是对接口上标注了@bean注解的默认方法进行类似于上一步的处理,存入configClass的beanMethods缓存中,后续再处理
         */
        // Process default methods on interfaces
        processInterfaces(configClass, sourceClass);
    
        /*
         * 8 处理父类
         * 配置类的父类将会被同样解析,而对于以"java."类路径开头的父类,比如Object,也就是rt.jar包中的Java核心类作为父类时不会被解析
         */
        // Process superclass, if any
        if (sourceClass.getMetadata().hasSuperClass()) {
            String superclass = sourceClass.getMetadata().getSuperClassName();
            if (superclass != null && !superclass.startsWith("java") &&
                    !this.knownSuperclasses.containsKey(superclass)) {
                this.knownSuperclasses.put(superclass, configClass);
                // Superclass found, return its annotation metadata and recurse
                //返回父类的sourceClass,将会在外层的processConfigurationClass中进行下一次循环解析父类。
                return sourceClass.getSuperClass();
            }
        }
    
        // No superclass -> processing is complete
        /*
         * 10 如果没有符合规则的父类或者都解析完毕,那么返回null,将会在外层的processConfigurationClass中跳出循环,进行后续步骤。
         */
        return null;
    }
    
    //----------ConfigurationClass的相关属性-------------
    
    /**
     * 通过@ImportResource注解引入的配置文件路径字符串到reader解析器的映射
     */
    private final Map<String, Class<? extends BeanDefinitionReader>> importedResources =
            new LinkedHashMap<>();
    /**
     * 所有被@Bean注解标注的方法的元数据集合
     */
    private final Set<BeanMethod> beanMethods = new LinkedHashSet<>();
    
    /**
     * 通过@ImportResource注解解析出来的配置文件路径存入configClass的importedResources缓存中
     */
    public void addImportedResource(String importedResource, Class<? extends BeanDefinitionReader> readerClass) {
        this.importedResources.put(importedResource, readerClass);
    }
    
    /**
     * 被@Bean注解标注的方法元数据存入beanMethods集合
     */
    public void addBeanMethod(BeanMethod method) {
        this.beanMethods.add(method);
    }
    
    2.1.5.1.2.1 processMemberClasses处理内部类配置类

      doProcessConfigurationClass方法的第一步就是尝试调用processMemberClasses来处理当前配置类的内部类,如果内部类是一个配置类,那么首先解析内部配置类,最终内部类也会调用processConfigurationClass方法递归解析,并且外部配置类会被设置到内部配置类的importedBy集合中,表示内部类算作被外部类import引入进来的。
      还记得我们之前在IoC容器初始化的时候,讲的扩展标签解析的doScan方法内的第二个isCandidateComponent方法吗,该方法将非静态内部类都排除了,因此,非静态内部类的bean定义还没有注册到容器中,但是我们却能取到对应的bean实例,那么它是在哪里解析的呢?就是在这里,processMemberClasses方法就是真正的解析、注册非静态内部类的bean定义的关键地方。

    /**
     1. ConfigurationClassParser的方法
     2. <p>
     3. 解析、注册 配置类内部的成员(嵌套)类,也就是内部类。最终还是会调用外部的doProcessConfigurationClass方法
     */
    private void processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass,
                                      Predicate<String> filter) throws IOException {
        /*
         * 获取全部内部类的SourceClass集合memberClasses
         *
         * 还记得我们之前在IoC容器初始化的时候,讲的扩展标签解析的doScan方法内的第二个isCandidateComponent方法吗
         * 该方法将非静态内部类都排除了,因此,非静态内部类的bean定义还没有注册到容器中。
         *
         * 而这里的getMemberClasses将获取所有的内部类,包括静态的和非静态的,其中,非静态内部类的bean定义就是在这里注册的
         */
        Collection<SourceClass> memberClasses = sourceClass.getMemberClasses();
        //如果memberClasses不为空
        if (!memberClasses.isEmpty()) {
            //需要处理的配置类集合
            List<SourceClass> candidates = new ArrayList<>(memberClasses.size());
            for (SourceClass memberClass : memberClasses) {
                //如果内部类也是配置类
                if (ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata()) &&
                        !memberClass.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) {
                    //加入到candidates
                    candidates.add(memberClass);
                }
            }
            //同样需要排序,这里的OrderComparator就不支持注解了
            OrderComparator.sort(candidates);
            /*
             * 遍历candidates,按照排序顺序依次处理
             */
            for (SourceClass candidate : candidates) {
                //如果importStack包含此外部配置类,那么说明import循环依赖,直接抛出异常: "A circular @Import has been detected……"
                //importStack属性用于判断import重复依赖
                if (this.importStack.contains(configClass)) {
                    this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
                } else {
                    //当前外部配置类configClass入栈
                    this.importStack.push(configClass);
                    try {
                        /*
                         * 调用processConfigurationClass方法,处理当前内部配置类
                         * 这里的asConfigClass方法将当前外部配置类的设置到内部配置类的importedBy集合中,表示算作被外部类import引入进来的
                         */
                        processConfigurationClass(candidate.asConfigClass(configClass), filter);
                    } finally {
                        //当前外部配置类configClass出栈
                        this.importStack.pop();
                    }
                }
            }
        }
    }
    
    2.1.5.1.2.2 processPropertySource处理@PropertySource属性源注解

      该方法用于解析配置类上的@PropertySources以及@PropertySource注解(@PropertySources相当于一个@PropertySource注解的集合)。
      通过这两个注解的方式,引入本地属性源,用来替代XML的资源引入,比如< context:property-placeholder/>标签,这个注解可重复出现。属性源将会被添加到到environment环境变量中。
      @PropertySource的相关属性如下:

    1. value:一个字符串数组,存放本地配置文件的路径字符串,一个@PropertySource注解可以引入多个属性配置文件;该属性只有有一个值。同样,这里的路径字符串支持${…:…}占位符解析,但是只会使用environment环境变量中的属性源。一般我们只需要设置value属性即可!
    2. name:属性源的名称,如果没有指定,那么将会自动生成;
    3. encoding:文件加载解析的字符集;
    4. factory:用于创建属性源的工厂的类型,默认为PropertySourceFactory.class;
    /**
     * PropertySourceFactory的属性
     * 默认属性源工厂
     * 如果没有指定PropertySource的factory属性,或者指定的属性值为PropertySourceFactory.class
     * 那么以该DefaultPropertySourceFactory作为属性源工厂
     */
    private static final PropertySourceFactory DEFAULT_PROPERTY_SOURCE_FACTORY = new DefaultPropertySourceFactory();
    
    
    /**
     * ConfigurationClassParser的方法
     * <p>
     * 处理给定的@PropertySource注解元数据
     *
     * @param propertySource 找到的@PropertySource注解的元数据,实际上就是一个map,包含了当前注解的属性和对应的值
     */
    private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
        //获取name属性,表示属性源的名称
        String name = propertySource.getString("name");
        if (!StringUtils.hasLength(name)) {
            name = null;
        }
        //获取encoding属性,表示编码字符集
        String encoding = propertySource.getString("encoding");
        if (!StringUtils.hasLength(encoding)) {
            encoding = null;
        }
        //获取本地配置文件的路径字符串数组,一个@PropertySource注解可以引入多个属性配置文件
        String[] locations = propertySource.getStringArray("value");
        //断言只有有一个文件路径
        Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
        //获取ignoreResourceNotFound属性的值,表示是否允许配置文件找不到
        boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
        //获取factory属性的值,也就是PropertySourceFactory的class,用来创建属性源,默认值就是PropertySourceFactory.class
        Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
        //获取PropertySourceFactory,用于创建属性源工厂
        PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
                DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
        /*
         * 遍历本地配置文件的路径字符串数组,依次加载配置文件,添加属性源
         */
        for (String location : locations) {
            try {
                /*
                 * 通过环境变量,解析路径字符串中的占位符,使用严格模式,遇到没有默认值的无法解析的占位符将抛出IllegalArgumentException异常
                 * 这说明我们指定的@PropertySource注解中的location支持${.. : ..}占位符,但是只会从environment环境变量中查找属性,这一点要注意
                 */
                String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
                //将配置文件加载成为一个Resource资源
                Resource resource = this.resourceLoader.getResource(resolvedLocation);
                /*
                 * 根据当前name、resource、encoding创建一个属性源,如果没有name那么将会生成默认的name,随后添加属性源
                 */
                addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
            } catch (IllegalArgumentException | FileNotFoundException | UnknownHostException ex) {
                // Placeholders not resolvable or resource not found when trying to open it
                if (ignoreResourceNotFound) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
                    }
                } else {
                    throw ex;
                }
            }
        }
    }
    
    2.1.5.1.2.2.1 addPropertySource添加属性源到environment

      该方法的源码就印证了我们在之前关于PropertySourcesPlaceholderConfigurer的文章的结尾的话语,即@PropertySource注解解析后的属性源将会被添加到environment环境变量中的属性源集合propertySources中。
      这个propertySources集合我们在IoC容器初始化的第一篇文章的setLocations方法中就见过了,该集合在setLocations方法中就初始化了著名的systemProperties — JVM系统属性属性源以及systemEnvironment - 系统环境属性源。在根据environment替换占位符的时候,就是从这个属性源集合中依次遍历、查找的,因此systemProperties — JVM系统属性属性源的优先级最高。
      通过@PropertySource注解添加的属性源,越后添加的属性源查找的优先级越高,但是仍低于systemProperties和systemEnvironment这两个系统级别的属性源。

    /**
     * ConfigurationClassParser的属性
     * 属性源的名称集合
     */
    private final List<String> propertySourceNames = new ArrayList<>();
    
    /**
     * 环境变量,在创建解析器的时候就初始化了
     */
    private final Environment environment;
    
    /**
     1. ConfigurationClassParser的方法
     2. <p>
     3. 添加属性源到environment环境变量中
     4.  5. @param propertySource 当前@PropertySource指定的配置文件对应的属性源
     */
    private void addPropertySource(PropertySource<?> propertySource) {
        //获取属性源的name
        String name = propertySource.getName();
        /*
         * 获取environment环境变量中的getPropertySources属性源集合,这个属性源集合,我们在IoC容器的最最最开始的setLocations方法中就见过了
         * 该集合在setLocations方法中就初始化了著名的systemProperties — JVM系统属性属性源以及systemEnvironment - 系统环境属性源
         * 在根据environment替换占位符的时候,就是从这个属性源集合中依次遍历、查找的,因此systemProperties — JVM系统属性属性源的优先级最高
         */
        MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();
        /*
         * 如果属性源名称集合中已包含该名称name,那么就扩展这个名字的属性源
         * 因为一个@PropertySource可能指定多个配置文件
         */
        if (this.propertySourceNames.contains(name)) {
            // We've already added a version, we need to extend it
            //获取该名字的属性源
            PropertySource<?> existing = propertySources.get(name);
            //如果不为null
            if (existing != null) {
                PropertySource<?> newSource = (propertySource instanceof ResourcePropertySource ?
                        ((ResourcePropertySource) propertySource).withResourceName() : propertySource);
                if (existing instanceof CompositePropertySource) {
                    //当前属性源加入到existing属性源集合的开头
                    ((CompositePropertySource) existing).addFirstPropertySource(newSource);
                } else {
                    if (existing instanceof ResourcePropertySource) {
                        existing = ((ResourcePropertySource) existing).withResourceName();
                    }
                    //当前属性源加入到属性源集合的开头
                    CompositePropertySource composite = new CompositePropertySource(name);
                    composite.addPropertySource(newSource);
                    composite.addPropertySource(existing);
                    propertySources.replace(name, composite);
                }
                return;
            }
        }
    
        /*
         * 到这里,表示属性源名称集合中不包含该名称name
         * 那么添加一个新name的属性源到环境变量的propertySources属性源集合中,同时将name加入到propertySourceNames集合中
         * 越后添加的属性源查找的优先级越高,但是低于systemProperties和systemEnvironment这两个系统级别的属性源
         */
    
        //如果是第一个加载的@PropertySource注解属性源
        if (this.propertySourceNames.isEmpty()) {
            //那么加入到propertySources属性源集合的尾部,查找的优先级最低
            propertySources.addLast(propertySource);
        }
        //否则
        else {
            //获取最后一个添加的属性源名称
            String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
            //在propertySources中的名为firstProcessed的属性源的索引处添加该属性源,原索引以及之后的属性源向后移动一位
            //即越后添加的属性源优先级越高
            propertySources.addBefore(firstProcessed, propertySource);
        }
        this.propertySourceNames.add(name);
    }
    
    2.1.5.1.2.3. ComponentScanAnnotationParser.parse处理@ComponentScan组件扫描注解

      ComponentScanAnnotationParser的parse方法用于处理@ComponentScan组件扫描注解,根据指定的包路径扫描出全部复合条件的bean定义。
      该方法主要是进行@ComponentScan注解的属性解析,真正的扫描包还是通过ClassPathBeanDefinitionScanner的doScan方法进行扫描的,无论是注解配置还是XML的配置,扫描包都是调用的这个方法,我们在此前< context:component-scan/>的注解解析文章中已经讲过了,该方法将会扫描并且注册这些指定包路径下的bean定义。
      @ComponentScan的相关属性如下,实际上和< context:component-scan/>的相关属性都差不多,我们在讲这个标签的时候就详细讲解过了:

    1. useDefaultFilters:是否注册默认类型过滤器,默认值true。
      1. 注册默认过滤器就是尝试添加@Component、@ManagedBean、@Named这三个注解类型过滤器到includeFilters缓存集合中!
      2. 这表示将会注册所有具有@Component注解及其派生注解的注解标志的类,比如@Component、@Repository、@Service、@Controller、@Configuration,还支持扫描注册 Java EE 6 的注解,比如@ManagedBean,以及JSR-330的注解,比如@Named。
    2. nameGenerator :指定的beanName生成器的class,默认值是BeanNameGenerator.class,将会使用AnnotationBeanNameGenerator生成beanName。
    3. scopedProxy、scopeResolver:使用代理模式,通常在web应用中使用。
    4. resourcePattern:控制符合组件检测条件的类文件,Spring推荐使用includeFilters和excludeFilters。
    5. includeFilters:包含的类型过滤器,默认空集合。如果扫描到的组件满足该集合中任意的类型过滤器,那么算作有资格作为候选组件。后遍历includeFilters。
    6. excludeFilters:排除的类型过滤器,默认空集合。如果扫描到的组件满足该集合中任意的类型过滤器,那么算作没有资格作为候选组件。先遍历excludeFilters。
      1. 默认添加一个AbstractTypeHierarchyTraversingFilter匿名对象,将会排除当前的配置类的扫描。
      2. lazyInit:扫描到的组件是否都延迟初始化,默认false。
    7. basePackages:要扫描的包路径字符串数组。传递的字符串支持${…:…}占位符,将会使用environment环境变量解析,还支持以","、";"、" “、”\t"、"\n"中的任意字符作为分隔符来表示传递了多个包路径。默认是个空数组。
    8. basePackageClasses:可以指定了一批类的class,Spring将解析class所在的包路经作为扫描包路径,功能和basePackages一致。默认是个空数组。
      1. 如果basePackages和basePackageClasses都没有指定值,那么默认扫描当前@ComponentScan注解所在的类所在的包下面的所有bean定义。
    //--------ComponentScanAnnotationParser的相关属性,在构造器中初始化-----------
    
    private final Environment environment;
    
    private final ResourceLoader resourceLoader;
    
    private final BeanNameGenerator beanNameGenerator;
    
    private final BeanDefinitionRegistry registry;
    
    /**
     * ConfigurableApplicationContext的属性
     * <p>
     * 分隔符常量,支持","、";"、" "、"\t"、"\n"中的任意分隔符
     */
    String CONFIG_LOCATION_DELIMITERS = ",; \t\n";
    
    
    /**
     * ComponentScanAnnotationParser的方法
     * <p>
     * 解析@ComponentScan注解,获取解析到的bean定义集合
     *
     * @param componentScan  一个@ComponentScan注解的属性集合
     * @param declaringClass 当前配置类的className
     * @return 解析到的bean定义集合
     */
    public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
        /*
         * 创建一个类路径Bean定义扫描器
         * 通过useDefaultFilters属性的值判断是否注册默认的类型过滤器,默认值true,即注册默认类型过滤器
         *
         * 这个默认类型过滤器我们在此前IoC容器初始化源码的<context:component-scan/>扩展标签解析的时候就讲了:
         * 注册默认过滤器就是尝试添加@Component、@ManagedBean、@Named这三个注解类型过滤器到includeFilters缓存集合中!
         *
         * 这表示将会注册所有具有@Component注解及其派生注解的注解标志的类,比如@Component、@Repository、@Service、@Controller、@Configuration,
         * 还支持扫描注册 Java EE 6 的注解,比如@ManagedBean,以及JSR-330的注解,比如@Named
         */
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
                componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
    
        /*
         * 获取beanName生成器,默认是AnnotationBeanNameGenerator
         */
    
        //获取nameGenerator属性值,即beanName生成器的class,默认值为BeanNameGenerator.class
        Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
        //判断是否是默认值,如果是默认值,那么使用当前创建ComponentScanAnnotationParser对象时指定的beanName生成器,
        //也就是ConfigurationClassPostProcessor类中的componentScanBeanNameGenerator,即AnnotationBeanNameGenerator
        //否则反射调用无参构造器,初始化指定类型的beanName生成器实例
        boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
        scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
                BeanUtils.instantiateClass(generatorClass));
    
        //生成代理对象的模式,通常在web应用中使用
        ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
        if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
            //设置指定的模式
            scanner.setScopedProxyMode(scopedProxyMode);
        } else {
            //自动选择JDK代理或者CGLib代理
            Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
            scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
        }
        //控制符合组件检测条件的类文件,Spring推荐使用includeFilters和excludeFilters
        scanner.setResourcePattern(componentScan.getString("resourcePattern"));
        //遍历每一个Filter
        for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
            //解析每一个Filter成为TypeFilter类型过滤器集合,继续遍历该集合
            for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                //设置到includeFilters属性中
                scanner.addIncludeFilter(typeFilter);
            }
        }
        //遍历每一个Filter
        for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
            //解析每一个Filter成为TypeFilter类型过滤器集合,继续遍历该集合
            for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                //设置到excludeFilters属性中
                scanner.addExcludeFilter(typeFilter);
            }
        }
        //获取lazyInit加载值,表示是否延迟初始化,默认false
        boolean lazyInit = componentScan.getBoolean("lazyInit");
        if (lazyInit) {
            //设置给beanDefinitionDefaults属性,这是设置一个默认值属性
            scanner.getBeanDefinitionDefaults().setLazyInit(true);
        }
        /*
         * 扫描包
         */
    
        /*
         * 解析后的包路径字符串
         * Spring将会对其解析、扫描其中的bean定义
         */
        Set<String> basePackages = new LinkedHashSet<>();
        /*
         * 获取basePackages属性数组,也就是传递的包路径字符串,默认是个空数组
         */
        String[] basePackagesArray = componentScan.getStringArray("basePackages");
        for (String pkg : basePackagesArray) {
            /*
             * 通过环境变量解析传递的路径字符串中的占位符,随后根据分隔符分割为一个路径字符串数组
             * 支持以","、";"、" "、"\t"、"\n"中的任意字符作为分隔符来表示传递了多个包路径
             */
            String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
                    ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            Collections.addAll(basePackages, tokenized);
        }
        /*
         * 获取basePackageClasses属性数组,默认是个空数组
         * basePackageClasses属性可以指定了一批类的class,Spring将解析class所在的包路径作为扫描路径
         */
        for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
            //解析class的packageName,存入basePackages包路径字符串中
            basePackages.add(ClassUtils.getPackageName(clazz));
        }
        /*
         * 如果解析后的basePackages包数组为空,即没有手动传递包路经
         */
        if (basePackages.isEmpty()) {
            //那么将当前@ComponentScan注解所在的类所属的包路径作为扫描的包路径
            //也就是,默认扫描当前@ComponentScan注解所在的包下面的所有bean定义
            basePackages.add(ClassUtils.getPackageName(declaringClass));
        }
        /*
         * 最后添加一个ExcludeFilter到excludeFilters属性中,表示排除当前的配置类的扫描
         */
        scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
            @Override
            protected boolean matchClassName(String className) {
                return declaringClass.equals(className);
            }
        });
        /*
         * 上面都是一些准备的逻辑,doScan方法才是真正执行扫描的逻辑,通过ClassPathBeanDefinitionScanner执行
         * 无论是注解配置还是XML的配置,扫描包都是调用的这个方法,我们在此前<context:component-scan/>的注解解析文章中已经讲过了
         * 该方法将会扫描并且注册这些指定包路径下的bean定义
         */
        return scanner.doScan(StringUtils.toStringArray(basePackages));
    }
    
    2.1.5.1.2.3.1 typeFiltersFor解析类型过滤器

      ComponentScanAnnotationParser的typeFiltersFor方法用于解析includeFilters或者excludeFilters属性对象,获取解析出来的类型过滤器集合。
      @ComponentScan注解的includeFilters或者excludeFilters属性写法如下:
    在这里插入图片描述

    /**
     1. ComponentScanAnnotationParser的方法
     2. <p>
     3. 解析includeFilters或者excludeFilters属性对象,获取解析出来的类型过滤器集合
     4.  5. @param filterAttributes includeFilters或者excludeFilters属性对象
     6. @return 解析出来的类型过滤器集合
     */
    private List<TypeFilter> typeFiltersFor(AnnotationAttributes filterAttributes) {
        //用于保存解析出来的类型过滤器
        List<TypeFilter> typeFilters = new ArrayList<>();
        //获取type属性值
        FilterType filterType = filterAttributes.getEnum("type");
        /*
         * 获取classes属性值,遍历
         */
        for (Class<?> filterClass : filterAttributes.getClassArray("classes")) {
            switch (filterType) {
                //添加注解类型过滤器
                case ANNOTATION:
                    Assert.isAssignable(Annotation.class, filterClass,
                            "@ComponentScan ANNOTATION type filter requires an annotation type");
                    @SuppressWarnings("unchecked")
                    Class<Annotation> annotationType = (Class<Annotation>) filterClass;
                    typeFilters.add(new AnnotationTypeFilter(annotationType));
                    break;
                //添加类或者接口的类型过滤器
                case ASSIGNABLE_TYPE:
                    typeFilters.add(new AssignableTypeFilter(filterClass));
                    break;
                //添加自定义类型过滤器
                case CUSTOM:
                    Assert.isAssignable(TypeFilter.class, filterClass,
                            "@ComponentScan CUSTOM type filter requires a TypeFilter implementation");
    
                    TypeFilter filter = ParserStrategyUtils.instantiateClass(filterClass, TypeFilter.class,
                            this.environment, this.resourceLoader, this.registry);
                    typeFilters.add(filter);
                    break;
                //其他类型的过滤器将抛出异常
                default:
                    throw new IllegalArgumentException("Filter type not supported with Class value: " + filterType);
            }
        }
        /*
         * 获取pattern属性值,遍历
         */
        for (String expression : filterAttributes.getStringArray("pattern")) {
            switch (filterType) {
                //创建一个AspectJTypeFilter,expression应该是一个AspectJ表达式,通过该表达式匹配类或者接口(及其子类、子接口)
                case ASPECTJ:
                    typeFilters.add(new AspectJTypeFilter(expression, this.resourceLoader.getClassLoader()));
                    break;
                //创建一个RegexPatternTypeFilter,expression应该是一个正则表达式,通过该表达式匹配类或者接口(及其子类、子接口)
                case REGEX:
                    typeFilters.add(new RegexPatternTypeFilter(Pattern.compile(expression)));
                    break;
                //其他类型的过滤器将抛出异常
                default:
                    throw new IllegalArgumentException("Filter type not supported with String pattern: " + filterType);
            }
        }
        //返回解析出来的类型过滤器集合
        return typeFilters;
    }
    
    2.1.5.1.2.4 processImports处理@Import注解

      processImports方法用于处理配置类上的@Import注解。@Import注解可以传递一个class的数组,用于引入bean定义。在处理上,针对传递的class分为三种类型分类处理:

    1. 实现了ImportSelector接口的class,该class对应的类本身不会被注册为bean定义,但是它的selectImports方法返回的类路径数组中的类将可能会被注册为bean定义。但是在该方法中仅仅是递归调用processImports方法,对于返回的类路径数组中的符合条件的类继续解析,直到它是一个普通类或者ImportBeanDefinitionRegistrar类型。
    2. 实现了ImportBeanDefinitionRegistrar接口的class,该class对应的类本身不会被注册为bean定义,但是它的registerBeanDefinitions方法可用于自定义注册bean定义。但是在该方法中仅仅是加入importBeanDefinitionRegistrars缓存中,后续通过reader.loadBeanDefinitions 方法统一处理。
    3. 普通类型的class,该class对应的类将尝试被注册为bean定义。但是在该方法中仅仅是将其当作配置类递归调用processConfigurationClass方法处理,并且会注册到configurationClasses缓存中,后续通过reader.loadBeanDefinitions 方法统一处理。
      1. 还会将被引入普通class的的全路径名字符串和引入该bean类的AnnotationMetadata元数据通过registerImport方法存入importStack栈的imports缓存中,这有这里会调用该方法,后面的ImportAwareBeanPostProcessor.postProcessBeforeInitialization方法就可能用到这里的存入的数据。
    4. 因此通过@Import注解引入的bean定义不会在该方法中注册,而是在后续通过reader.loadBeanDefinitions 方法统一注册。
    /**
     * ConfigurationClassParser的方法
     * <p>
     * 处理@Import注解
     *
     * @param configClass             表示当前配置类的对象
     * @param currentSourceClass      源类
     * @param importCandidates        @Import注解引入的类的SourceClass集合
     * @param exclusionFilter         类型过滤器
     * @param checkForCircularImports 是否校验循环import依赖
     */
    private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
                                Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
                                boolean checkForCircularImports) {
        //如果没有引入任何类,那么直接返回
        if (importCandidates.isEmpty()) {
            return;
        }
        //检测是否有循环import的情况
        if (checkForCircularImports && isChainedImportOnStack(configClass)) {
            this.problemReporter.error(new ConfigurationClassParser.CircularImportProblem(configClass, this.importStack));
        } else {
            //当前配置类configClass入栈
            this.importStack.push(configClass);
            try {
                //遍历sourceClass集合
                for (SourceClass candidate : importCandidates) {
                    //如果Import的类实现了ImportSelector接口
                    if (candidate.isAssignable(ImportSelector.class)) {
                        // Candidate class is an ImportSelector -> delegate to it to determine imports
                        Class<?> candidateClass = candidate.loadClass();
                        //反射创建一个ImportSelector对象
                        ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
                                this.environment, this.resourceLoader, this.registry);
                        //获取从导入候选项中排除类的谓词
                        Predicate<String> selectorFilter = selector.getExclusionFilter();
                        //连接这两个谓词
                        if (selectorFilter != null) {
                            exclusionFilter = exclusionFilter.or(selectorFilter);
                        }
                        //如果selector还属于DeferredImportSelector接口,那么将在最后执行
                        if (selector instanceof DeferredImportSelector) {
                            //存入ImportSelector的延迟处理器中,后面会统一在外部parse方法末尾通过this.deferredImportSelectorHandler.process()延迟处理
                            this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
                        } else {
                            //获取当前ImportSelector的selectImports方法返回值,也就是要导入到容器中的组件全类名数组,可以指定多个
                            String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                            //遍历要导入到容器中的组件全类名数组,排除符合exclusionFilter条件的组件类名,对于剩下的组件全类名进行加载成为SourceClass集合
                            Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
                            /*
                             * 递归调用processImports方法,对于从当前import类中解析出来的importSourceClasses继续解析
                             */
                            processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
                        }
                    }
                    //如果Import的类实现了ImportBeanDefinitionRegistrar接口
                    else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                        // Candidate class is an ImportBeanDefinitionRegistrar ->
                        // delegate to it to register additional bean definitions
                        Class<?> candidateClass = candidate.loadClass();
                        //反射创建一个ImportBeanDefinitionRegistrar对象registrar
                        ImportBeanDefinitionRegistrar registrar =
                                ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
                                        this.environment, this.resourceLoader, this.registry);
                        //当前registrar和importingClassMetadata存入configClass的importBeanDefinitionRegistrars缓存中,后续才会处理
                        configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
                    }
                    //如果Import的类是普通类型
                    else {
                        // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
                        // process it as an @Configuration class
                        //存入importStack栈的imports缓存中
                        this.importStack.registerImport(
                                currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
                        //将其当作配置类递归调用processConfigurationClass方法处理,并且会注册到configurationClasses缓存中
                        //后续通过reader.loadBeanDefinitions 方法统一处理
                        //这里的asConfigClass方法将当前外部配置类的设置到Import类的importedBy集合中,表示算作被外部类Import引入进来的
                        processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
                    }
                }
            } catch (BeanDefinitionStoreException ex) {
                throw ex;
            } catch (Throwable ex) {
                throw new BeanDefinitionStoreException(
                        "Failed to process import candidates for configuration class [" +
                                configClass.getMetadata().getClassName() + "]", ex);
            } finally {
                //当前配置类configClass出栈
                this.importStack.pop();
            }
        }
    }
    
    2.1.5.1.2.4.1 asSourceClasses获取符合条件的SourceClass集合

      该方法将从selectImports的返回值数组中继续筛选获取不匹配exclusionFilter排除过滤器的className创建SourceClass,并返回一个SourceClass集合。
      默认的exclusionFilter就DEFAULT_EXCLUSION_FILTER,该filter的条件是如果类名以"java.lang.annotation."或者"org.springframework.stereotype."开头,就返回true,或者返回false。实际含义就是如果是这些注解类型,那就丢弃。

    /**
     * ConfigurationClassParser的方法
     * <p>
     * 获取符合filter条件的类名的SourceClass集合
     */
    private Collection<SourceClass> asSourceClasses(String[] classNames, Predicate<String> filter) throws IOException {
        List<SourceClass> annotatedClasses = new ArrayList<>(classNames.length);
        for (String className : classNames) {
            annotatedClasses.add(asSourceClass(className, filter));
        }
        //返回SourceClass集合
        return annotatedClasses;
    }
    
    /**
     * ConfigurationClassParser的属性
     * 替代被排除的className的SourceClass对象
     */
    private final SourceClass objectSourceClass = new SourceClass(Object.class);
    
    
    /**
     * ConfigurationClassParser的方法
     * <p>
     * 获取符合filter条件的类名的SourceClass
     */
    SourceClass asSourceClass(@Nullable String className, Predicate<String> filter) throws IOException {
        //如果符合过滤条件,那么返回objectSourceClass常量,后续将不会注入该类型的bean实例
        //注意这里还是返回了一个objectSourceClass,而不是null,后面会处理
        if (className == null || filter.test(className)) {
            return this.objectSourceClass;
        }
        //如果class名以java开头,这表示位于rt.jar核心包中的核心类
        if (className.startsWith("java")) {
            // Never use ASM for core java types
            try {
                //直接传递生成的class对象
                return new SourceClass(ClassUtils.forName(className, this.resourceLoader.getClassLoader()));
            } catch (ClassNotFoundException ex) {
                throw new NestedIOException("Failed to load class [" + className + "]", ex);
            }
        }
        //根据className,返回SourceClass
        return new SourceClass(this.metadataReaderFactory.getMetadataReader(className));
    }
    
    2.1.5.1.2.4.2 @Import注解使用

      我们测试三种class类型,位于com.spring.source.imports包下:

    /**
     * @author lx
     */
    public class BeanTest {
    
        /**
         * 普通class
         */
        public static class NormalBean {}
    
        /**
         * 实现了ImportSelector的class
         */
        public static class ImportSelectorBean implements ImportSelector {
    
            /**
             * 返回的类路径字符串数组,将会注册这些类型的bean定义
             */
            @Override
            public String[] selectImports(AnnotationMetadata importingClassMetadata) {
                String[] strings = new String[2];
                strings[0] = "com.spring.source.imports.BeanTest.ImportSelectorBean1";
                strings[1] = "com.spring.source.imports.BeanTest.ImportSelectorBean2";
                return strings;
            }
    
    
        }
    
        /**
         * 实现了ImportBeanDefinitionRegistrar的class
         */
        public static class ImportBeanDefinitionRegistrarBean implements ImportBeanDefinitionRegistrar {
            /**
             * 可以自定义注册bean定义的逻辑
             */
            @Override
            public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
                //创建bean定义
                RootBeanDefinition beanDefinition1 = new RootBeanDefinition();
                beanDefinition1.setBeanClass(Object.class);
                RootBeanDefinition beanDefinition2 = new RootBeanDefinition();
                beanDefinition2.setBeanClass(Object.class);
                //注册bean定义
                registry.registerBeanDefinition("o1", beanDefinition1);
                registry.registerBeanDefinition("o2", beanDefinition2);
            }
        }
    
    
        public static class ImportSelectorBean1 {}
    
        public static class ImportSelectorBean2 {}
    }
    

      我们的配置类com.spring.source.imports.ImportBean:

    @Configuration
    @Import({BeanTest.NormalBean.class, BeanTest.ImportSelectorBean.class,
            BeanTest.ImportBeanDefinitionRegistrarBean.class})
    public class ImportBean { }
    

      配置文件spring-config-import.xml:

    <context:component-scan base-package="com.spring.source.imports"/>
    

      测试:

    @Test
    public void importTest() {
        //设置环境变量
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config-import.xml");
        //获取普通bean实例
        System.out.println(ac.getBean(BeanTest.NormalBean.class));
        //获取通过ImportSelector引入的bean实例
        System.out.println(ac.getBean(BeanTest.ImportSelectorBean1.class));
        System.out.println(ac.getBean(BeanTest.ImportSelectorBean2.class));
        //获取通过ImportBeanDefinitionRegistrarBean手动注册的bean实例
        System.out.println(ac.getBean("o1"));
        System.out.println(ac.getBean("o2"));
    
        //获取ImportSelector实例本身,获取不到,将会报错
        System.out.println(ac.getBean(BeanTest.ImportSelectorBean.class));
        //获取ImportBeanDefinitionRegistrarBean实例本身,获取不到,将会报错
        System.out.println(ac.getBean(BeanTest.ImportBeanDefinitionRegistrarBean.class));
    }
    

      一次测试的结果如下:

    com.spring.source.imports.BeanTest$NormalBean@5119fb47
    com.spring.source.imports.BeanTest$ImportSelectorBean1@7193666c
    com.spring.source.imports.BeanTest$ImportSelectorBean2@20deea7f
    java.lang.Object@3835c46
    java.lang.Object@1dde4cb2
    
    org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.spring.source.imports.BeanTest$ImportSelectorBean' available
    

      可以看到:

    1. 对于普通class,将会把该class本身注入到容器中;
    2. 对于ImportSelectorBean的class,将会把selectImports方法返回的一批class注入到容器中。该class本身不会注入到容器中。
      1. 源码中还要求排除匹配exclusionFilter过滤器的类名。默认的filter就DEFAULT_EXCLUSION_FILTER,该filter的条件是如果类名以"java.lang.annotation."或者"org.springframework.stereotype."开头,就返回true,或者返回false。
    3. 对于ImportBeanDefinitionRegistrarBean的class,可以通过registerBeanDefinitions方法自定义的注册一批bean定义,后面将会将实例化。该class本身不会注入到容器中。

      我们在ImportSelectorBean中重写getExclusionFilter方法,加入一个自定义的Filter:

    /**
     * 普通class
     */
    public static class NormalBean {
    }
    
    /**
     * 实现了ImportSelector的class
     */
    public static class ImportSelectorBean implements ImportSelector {
    
        /**
         * 返回的类路径字符串数组,将会注册这些类型的bean定义
         */
        @Override
        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            String[] strings = new String[2];
            strings[0] = "com.spring.source.imports.BeanTest.ImportSelectorBean1";
            strings[1] = "com.spring.source.imports.BeanTest.ImportSelectorBean2";
            return strings;
        }
    
        @Override
        public Predicate<String> getExclusionFilter() {
            return "com.spring.source.imports.BeanTest.ImportSelectorBean1"::equals;
        }
    }
    

      这个过滤器的意思就是不注册ImportSelectorBean1的bean,我们再次测试,发现更早的抛出异常,因为容器中没有注册ImportSelectorBean1类型的bean实例:

    com.spring.source.imports.BeanTest$NormalBean@7193666c
    
    org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.spring.source.imports.BeanTest$ImportSelectorBean1' available
    

    2.1.6 parser.validate校验配置类

      在通过parser.parse方法解析每一个配置类之后,随即调用parser.validate()方法校验所有解析之后的配置类,也就是ConfigurationClass。

    1. 如果当前配置类具有@Configuration注解,并且proxyBeanMethods属性为true,默认就是true:
      1. 如果当前配置类是被final修饰的最终类,那么抛出异常:"@Configuration class ‘%s’ may not be final. Remove the final modifier to continue.",因为它不能被CGLIB代理。
      2. 如果@Bean注解标注的方法是final或者private且非static修饰的方法,那么抛出异常:"@Bean method ‘%s’ must not be private or final; change the method’s modifiers to continue",因为它不能被CGLiB代理。注意@Bean方法可以被static修饰。
    /**
     * ConfigurationClassParser的方法
     * <p>
     * 校验全部ConfigurationClass
     */
    public void validate() {
        for (ConfigurationClass configClass : this.configurationClasses.keySet()) {
            //校验每一个configClass
            configClass.validate(this.problemReporter);
        }
    }
    
    /**
     * ConfigurationClass的方法
     *
     * @param problemReporter 问题报告器
     */
    public void validate(ProblemReporter problemReporter) {
        // A configuration class may not be final (CGLIB limitation) unless it declares proxyBeanMethods=false
        Map<String, Object> attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName());
        //如果当前配置类具有@Configuration注解,并且proxyBeanMethods属性为true(默认就是true)
        if (attributes != null && (Boolean) attributes.get("proxyBeanMethods")) {
            //如果当前配置类是被final修饰的最终类,那么抛出异常:"@Configuration class '%s' may not be final. Remove the final modifier to continue."
            //因为它不能被CGLIB代理
            if (this.metadata.isFinal()) {
                problemReporter.error(new FinalConfigurationProblem());
            }
            //校验全部@Bean注解方法
            for (BeanMethod beanMethod : this.beanMethods) {
                beanMethod.validate(problemReporter);
            }
        }
    }
    
    /**
     * BeanMethod的方法
     * 校验@Bean注解标注的方法
     */
    @Override
    public void validate(ProblemReporter problemReporter) {
        //如果是静态方法,不需要校验,直接返回
        if (getMetadata().isStatic()) {
            // static @Bean methods have no constraints to validate -> return immediately
            return;
        }
        //如果当前方法所属配置类具有@Configuration注解
        if (this.configurationClass.getMetadata().isAnnotated(Configuration.class.getName())) {
            //如果当前方法不能被重写,那么抛出异常:"@Bean method '%s' must not be private or final; change the method's modifiers to continue"
            //因为只有可重写的方法才能被CGLiB代理
            //如果是final、static、private修饰的方法,那么isOverridable方法就返回true
            if (!getMetadata().isOverridable()) {
                // instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIB
                problemReporter.error(new NonOverridableMethodError());
            }
        }
    }
    

    2.1.7 new ConfigurationClassBeanDefinitionReader 创建bean定义加载器

      ConfigurationClassPostProcessor将使用ConfigurationClassBeanDefinitionReader来加载、注册此前通过parse方法找到的可能存在的bean定义。

    //----------ConfigurationClassBeanDefinitionReader的相关属性
    
    private final BeanDefinitionRegistry registry;
    
    private final SourceExtractor sourceExtractor;
    
    private final ResourceLoader resourceLoader;
    
    private final Environment environment;
    
    private final BeanNameGenerator importBeanNameGenerator;
    
    private final ImportRegistry importRegistry;
    
    private final ConditionEvaluator conditionEvaluator;
    
    /**
     * ConfigurationClassBeanDefinitionReader构造器
     *
     * @param registry                bean定义要存入的注册表
     * @param sourceExtractor         简单的策略,允许工具控制源元数据如何附加到 bean 定义元数据。
     * @param resourceLoader          资源加载器
     * @param environment             环境变量
     * @param importBeanNameGenerator import方式引入的bean的beanName生成器
     * @param importRegistry          import方式引入的bean注册表
     */
    ConfigurationClassBeanDefinitionReader(BeanDefinitionRegistry registry, SourceExtractor sourceExtractor,
                                           ResourceLoader resourceLoader, Environment environment, BeanNameGenerator importBeanNameGenerator,
                                           ImportRegistry importRegistry) {
        //初始化一批属性
        this.registry = registry;
        this.sourceExtractor = sourceExtractor;
        this.resourceLoader = resourceLoader;
        this.environment = environment;
        this.importBeanNameGenerator = importBeanNameGenerator;
        this.importRegistry = importRegistry;
        this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
    }
    

    2.1.8 reader.loadBeanDefinitions 加载bean定义

      通过reader加载、注册所有configClasses中的bean定义,也就是对configClasses中的@Import、@Bean、@ImportResource等注解中可能的bean定义进行解析和注册。
      该方法结束,那么processConfigBeanDefinitions方法的主要逻辑就结束了。

    /**
     * ConfigurationClassBeanDefinitionReader的方法
     * <p>
     * 向注册表注册 bean 定义
     *
     * @param configurationModel 解析后的ConfigurationClass配置类集合
     */
    public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
        //评估@Conditional注释,判断当前配置类的解析否需要跳过,将会跟踪结果并考虑到"importBy"。
        TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
        //遍历解析后的ConfigurationClass配置类集合,加载bean定义
        for (ConfigurationClass configClass : configurationModel) {
            //从ConfigurationClass加载bean定义
            loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
        }
    }
    

    2.1.8.1 new TrackedConditionEvaluator 跟踪条件评估器

      ConfigurationClassBeanDefinitionReader的内部类,同样是用于判断当前配置类的解析否需要跳过,和之前的Evaluator区别是除了评估@Conditional注解之外,还会跟踪结果并考虑到"importedBy"。
      它的shouldSkip方法很简单,如果当前configClass是被引入的,并且引入类都是被跳过的,那么当前configClass也应该被跳过,否则通过原始的conditionEvaluator.shouldSkip方法判断是否需要跳过,这里的phase生效的阶段参数为REGISTER_BEAN,即注册bean的阶段。
      解析后的结果将会存入skipped缓存,后续再来查询时将不会再次解析。

    /**
     1. ConfigurationClassBeanDefinitionReader的内部类
     2. 评估@Conditional注解,跟踪结果并考虑到"importedBy"
     */
    private class TrackedConditionEvaluator {
    
        /**
         * ConfigurationClass的缓存,避免后续重复校验应该跳过解析
         */
        private final Map<ConfigurationClass, Boolean> skipped = new HashMap<>();
    
        /**
         * 是否应该跳过解析
         *
         * @param configClass
         * @return
         */
        public boolean shouldSkip(ConfigurationClass configClass) {
            //从缓存获取当前configClass的是否应该跳过的结果
            Boolean skip = this.skipped.get(configClass);
            //如果缓存为null,那么解析
            if (skip == null) {
                //如果是被其他类引入的
                if (configClass.isImported()) {
                    //所有的引入类被跳过的标记,默认true
                    boolean allSkipped = true;
                    //获取引入类
                    for (ConfigurationClass importedBy : configClass.getImportedBy()) {
                        //如果引入类不应该被跳过
                        if (!shouldSkip(importedBy)) {
                            //那么allSkipped为false,结束循环
                            allSkipped = false;
                            break;
                        }
                    }
                    //如果所有的引入类被跳过,那么skip设置为true,表示当前被引入的类应该被跳过
                    if (allSkipped) {
                        // The config classes that imported this one were all skipped, therefore we are skipped...
                        skip = true;
                    }
                }
                //如果skip还是为null
                if (skip == null) {
                    //那么调用conditionEvaluator的shouldSkip方法继续判断,这里的phase生效的阶段参数为REGISTER_BEAN,即注册bean的阶段
                    //这个方法我们此前就讲过了
                    skip = conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN);
                }
                //存入缓存
                this.skipped.put(configClass, skip);
            }
            //返回skip
            return skip;
        }
    }
    

    2.1.8.2 loadBeanDefinitionsForConfigurationClass加载注册beean定义

      通过reader加载、注册此configClasses中的bean定义,主要有一下4个加载的逻辑:

    1. 如果当前配置类是被引入的,通过@Import注解、处理内部类等方式找到的configClass,都算作被引入的。那么解析注册配置类本身成为AnnotatedGenericBeanDefinition类型的bean定义,并且注册到注册表中。在这里,就会对非静态内部配置类进行注册。
    2. 解析@Bean方法成为ConfigurationClassBeanDefinition类型的bean定义,并且注册到注册表中。
    3. 加载、解析@ImportedResource注解引入的XML配置文件中的bean定义到注册表中。核心还是BeanDefinitionReader.loadBeanDefinitions方法加载resource资源,解析、注册bean定义,这个方法我们在"IoC容器初始化(2)"的文章中就详细讲过了。
    4. 对于@Import注解引入的ImportBeanDefinitionRegistrar类型的对象的registerBeanDefinitions方法进行统一回调。该方法可用于自定义的注册、修改bean定义,因为它将注册表作为参数。
    /**
     * ConfigurationClassBeanDefinitionReader的方法
     * <p>
     * 读取特定的ConfigurationClass配置类,注册该类本身及其所有@Bean方法的 bean 定义
     */
    private void loadBeanDefinitionsForConfigurationClass(
            ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
        //如果trackedConditionEvaluator的shouldSkip方法返回true,即应该跳过
        if (trackedConditionEvaluator.shouldSkip(configClass)) {
            //获取beanName
            String beanName = configClass.getBeanName();
            //如果存在beanName,并且注册表中已经包含了该beanName的bean定义
            if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
                /*
                 * 从注册表中移除该beanName的bean定义
                 * 因此,此前加入进来的配置类的bean定义将可能被移除
                 */
                this.registry.removeBeanDefinition(beanName);
            }
            //移除importRegistry即importStack中的缓存
            this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
            return;
        }
        //如果当前配置类是被引入的,通过@Import注解、处理内部类等方式找到的configClass,都算作被引入的
        if (configClass.isImported()) {
            /*
             * 1 解析注册配置类本身成为AnnotatedGenericBeanDefinition类型的bean定义,并且注册到注册表中。
             * 在这里,就会对非静态内部配置类进行注册。
             */
            registerBeanDefinitionForImportedConfigurationClass(configClass);
        }
        //获取全部的@Bean方法,遍历
        for (BeanMethod beanMethod : configClass.getBeanMethods()) {
            /*
             * 2 解析@Bean方法成为ConfigurationClassBeanDefinition类型的bean定义,并且注册到注册表中。
             */
            loadBeanDefinitionsForBeanMethod(beanMethod);
        }
        /*
         * 3 加载、解析@ImportedResource注解引入的XML配置文件中的bean定义到注册表中。
         * 核心还是BeanDefinitionReader的loadBeanDefinitions方法加载resource资源,解析、注册bean定义
         * 这个方法我们在"IoC容器初始化(2)"的文章中就详细讲过了
         */
        loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
        /*
         * 4 @Import注解引入的ImportBeanDefinitionRegistrar类型的对象的registerBeanDefinitions方法的回调。
         * 该方法可用于自定义的注册、修改bean定义,因为它将注册表作为参数
         */
        loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
    }
    
    2.1.8.2.1 registerBeanDefinitionForImportedConfigurationClass注册配置类本身

      如果当前配置类是被引入的,此前讲过,通过@Import注解、处理内部类等方式找到的configClass,都算作被引入的,那么该方法将配置类本身注册为 bean 定义,这里就是对@Import普通类和非静态内部类的beean定义进行注册的地方。
      将被解析为AnnotatedGenericBeanDefinition类型的bean定义,采用的beanName生成器是FullyQualifiedAnnotationBeanNameGenerator,在没找到beanName而主动生成的时候,生成的逻辑仅仅是将了类的全路径名作为beanName,相比于父类AnnotationBeanNameGenerator更加简单。
      注册的bean定义是和XML标签的解析一样是采用registerBeanDefinition方法,这个方法我们在“IoC容器初始化(3)“的文章中就详细讲过了,就是向注册表中添加的三个缓存beanDefinitionMap、beanDefinitionNames、aliasMap,这属于容器初始化的核心操作。

    /**
     * ConfigurationClassBeanDefinitionReader的方法
     * <p>
     * 将配置类本身注册为 bean 定义,这里就是对非静态内部配置类进行注册的地方
     */
    private void registerBeanDefinitionForImportedConfigurationClass(ConfigurationClass configClass) {
        AnnotationMetadata metadata = configClass.getMetadata();
        //创建AnnotatedGenericBeanDefinition类型的bean定义
        AnnotatedGenericBeanDefinition configBeanDef = new AnnotatedGenericBeanDefinition(metadata);
    
        ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(configBeanDef);
        configBeanDef.setScope(scopeMetadata.getScopeName());
        //查找或者生成beanName,采用的生成器是FullyQualifiedAnnotationBeanNameGenerator
        //它继承了AnnotationBeanNameGenerator,区别就在于如果没指定beanName那么自己的beanName生成规则是直接以全路径类名作为beanName
        String configBeanName = this.importBeanNameGenerator.generateBeanName(configBeanDef, this.registry);
        //处理类上的其他通用注解:@Lazy, @Primary, @DependsOn, @Role, @Description
        AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata);
        //封装成为BeanDefinitionHolder对象
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(configBeanDef, configBeanName);
        //根据proxyMode属性的值,判断是否需要创建scope代理,一般都是不需要的
        definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
        //调用registerBeanDefinition方法注册BeanDefinition到注册表的缓存中,该方法此前已经讲过了
        this.registry.registerBeanDefinition(definitionHolder.getBeanName(), definitionHolder.getBeanDefinition());
        //设置beanName
        configClass.setBeanName(configBeanName);
    
        if (logger.isTraceEnabled()) {
            logger.trace("Registered bean definition for imported class '" + configBeanName + "'");
        }
    }
    
    /**
     * FullyQualifiedAnnotationBeanNameGenerator类,用于查找和生成beanName
     * 重写了Spring生成beanName0的逻辑
     */
    public class FullyQualifiedAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator {
    
        /**
         * 重写了Spring生成beanName0的逻辑
         * 相比于父类AnnotationBeanNameGenerator更加简单,直接是将全路径类名注解作为beanName
         */
        @Override
        protected String buildDefaultBeanName(BeanDefinition definition) {
            String beanClassName = definition.getBeanClassName();
            Assert.state(beanClassName != null, "No bean class name set");
            return beanClassName;
        }
    
    }
    
    2.1.8.2.2 loadBeanDefinitionsForBeanMethod从@Bean方法加载bean定义

      @Bean方法对应的bean定义将被封装成为ConfigurationClassBeanDefinition类型,如果设置了name属性,那么将第一个值作为beanName,其他的值作为别名,否则直接将方法名作为beanName。
      如果在注册当前@Bean方法的bean定义时发现注册表中存在同名的的bean定义,那么可能会覆盖此前的bean定义或者保留此前的bean定义而不注册当前bean定义(具体在isOverriddenByExistingDefinition方法中)。
      @Bean方法的bean定义的注册和XML标签的解析一样是采用registerBeanDefinition方法,这个方法我们在“IoC容器初始化(3)“的文章中就详细讲过了,就是向注册表中添加的三个缓存beanDefinitionMap、beanDefinitionNames、aliasMap,这属于容器初始化的核心操作。
      可以看到,@Bean方法对应的ConfigurationClassBeanDefinition,设置了FactoryMethodName属性,也就是说,这里的@Bean方法均被解析为工厂方法,那么在IoC容器初始化的createBeanInstance创建bean实例阶段,将会通过工厂方法创建bean实例,即调用instantiateUsingFactoryMethod方法获取实例。

    /**
     * ConfigurationClassBeanDefinitionReader的属性
     * 条件评估器,处理类或者方法上的@Conditional注解
     */
    private final ConditionEvaluator conditionEvaluator;
    /**
     * ConfigurationClass的属性
     * 当前配置类跳过的方法
     */
    final Set<String> skippedBeanMethods = new HashSet<>();
    
    /**
     1. ConfigurationClassBeanDefinitionReader的方法
     2. <p>
     3. 根据给定的BeanMethod解析为bean定义像注册表中注册
     4.  5. @param beanMethod 表示一个@Bean方法
     */
    @SuppressWarnings("deprecation")  // for RequiredAnnotationBeanPostProcessor.SKIP_REQUIRED_CHECK_ATTRIBUTE
    private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
        //获取方法所属的类
        ConfigurationClass configClass = beanMethod.getConfigurationClass();
        MethodMetadata metadata = beanMethod.getMetadata();
        //获取方法名
        String methodName = metadata.getMethodName();
        /*
         * 处理方法上的@Conditional注解,判断是否应该跳过此方法的处理
         * 这里的metadata就是方法元数据,MethodMetadata
         */
        //如果shouldSkip返回true,即当前@Bean的方法应该跳过解析,这里的phase生效的阶段参数为REGISTER_BEAN
        if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
            //那么加入到当前配置类的skippedBeanMethods缓存中
            configClass.skippedBeanMethods.add(methodName);
            return;
        }
        //如果此前就解析了该方法,并且应该跳过,那么直接返回
        if (configClass.skippedBeanMethods.contains(methodName)) {
            return;
        }
    
        //获取@Bean注解的属性集合
        AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
        Assert.state(bean != null, "No @Bean annotation attributes");
        /*
         * 考虑名字和别名
         */
        //获取name属性集合
        List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));
        /*
         * 获取beanName。如果设置了name属性,那么将第一个值作为beanName,其他的值作为别名,否则直接将方法名作为beanName
         */
        String beanName = (!names.isEmpty() ? names.remove(0) : methodName);
        for (String alias : names) {
            /*
             * 注册别名映射,registerAlias方法我们在此前"IoC容器初始化(3)"的文章中已经讲过了
             * 将是将别名alias和名字beanName的映射注册到SimpleAliasRegistry注册表的aliasMap缓存汇总
             */
            this.registry.registerAlias(beanName, alias);
        }
    
        /*
         * 校验是否存在同名的bean定义,以及是否允许同名的bean定义覆盖
         */
        if (isOverriddenByExistingDefinition(beanMethod, beanName)) {
            //如果返回true,并且如果beanName就等于当前bean方法所属的类的beanName,那么抛出异常
            if (beanName.equals(beanMethod.getConfigurationClass().getBeanName())) {
                throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
                        beanName, "Bean name derived from @Bean method '" + beanMethod.getMetadata().getMethodName() +
                        "' clashes with bean name for containing configuration class; please make those names unique!");
            }
            //如果返回true,直接返回,当前bean方法不再解析
            return;
        }
        //新建一个ConfigurationClassBeanDefinition类型的bean定义,从这里可知@Bean方法的bean定义的类型
        ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata);
        beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));
        //如果当前bean方法是静态的
        if (metadata.isStatic()) {
            //看作静态工厂方法
            // static @Bean method
            if (configClass.getMetadata() instanceof StandardAnnotationMetadata) {
                beanDef.setBeanClass(((StandardAnnotationMetadata) configClass.getMetadata()).getIntrospectedClass());
            } else {
                beanDef.setBeanClassName(configClass.getMetadata().getClassName());
            }
            //设置工厂方法名
            beanDef.setUniqueFactoryMethodName(methodName);
        } else {
            //看作实例工厂方法
            // instance @Bean method
            beanDef.setFactoryBeanName(configClass.getBeanName());
            beanDef.setUniqueFactoryMethodName(methodName);
        }
        //设置解析的工厂方法
        if (metadata instanceof StandardMethodMetadata) {
            beanDef.setResolvedFactoryMethod(((StandardMethodMetadata) metadata).getIntrospectedMethod());
        }
        //设置自动装配模式为构造器自动注入
        beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
        //设置属性
        beanDef.setAttribute(org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor.
                SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE);
        //处理方法上的其他通用注解:@Lazy, @Primary, @DependsOn, @Role, @Description
        AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata);
        //获取autowire属性,默认Autowire.NO,即不自动注入
        Autowire autowire = bean.getEnum("autowire");
        //设置自动注入模式
        if (autowire.isAutowire()) {
            beanDef.setAutowireMode(autowire.value());
        }
        //设置autowireCandidate属性,默认tue
        boolean autowireCandidate = bean.getBoolean("autowireCandidate");
        if (!autowireCandidate) {
            beanDef.setAutowireCandidate(false);
        }
        //设置initMethodName属性
        String initMethodName = bean.getString("initMethod");
        if (StringUtils.hasText(initMethodName)) {
            beanDef.setInitMethodName(initMethodName);
        }
        //设置destroyMethod属性
        String destroyMethodName = bean.getString("destroyMethod");
        beanDef.setDestroyMethodName(destroyMethodName);
        //考虑作用域和代理
        // Consider scoping
        ScopedProxyMode proxyMode = ScopedProxyMode.NO;
        AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class);
        if (attributes != null) {
            //设置作用域
            beanDef.setScope(attributes.getString("value"));
            //获取作用域代理属性,默认不使用代理
            proxyMode = attributes.getEnum("proxyMode");
            if (proxyMode == ScopedProxyMode.DEFAULT) {
                proxyMode = ScopedProxyMode.NO;
            }
        }
    
        //如有必要,将原始 bean 定义替换为代理目标定义
        BeanDefinition beanDefToRegister = beanDef;
        if (proxyMode != ScopedProxyMode.NO) {
            BeanDefinitionHolder proxyDef = ScopedProxyCreator.createScopedProxy(
                    new BeanDefinitionHolder(beanDef, beanName), this.registry,
                    proxyMode == ScopedProxyMode.TARGET_CLASS);
            beanDefToRegister = new ConfigurationClassBeanDefinition(
                    (RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata);
        }
    
        if (logger.isTraceEnabled()) {
            logger.trace(String.format("Registering bean definition for @Bean method %s.%s()",
                    configClass.getMetadata().getClassName(), beanName));
        }
        //调用registerBeanDefinition方法注册BeanDefinition到注册表的缓存中,该方法此前已经讲过了
        this.registry.registerBeanDefinition(beanName, beanDefToRegister);
    }
    
    2.1.8.2.2.1 isOverriddenByExistingDefinition是否支持现有bean定义的覆盖

      使用获取或者生成的beanName,在注册表中查找同名的bean定义并校验,对于@Bean的bean定义来说可能会覆盖其他同名bean定义或者被其他同名bean定义:

    1. 如果注册表中不包含该beanName的bean定义,那么返回false,可以继续向后解析该bean方法。后面的步骤都表示注册表中包含该beanName的bean定义;
    2. 如果现有的 bean 定义也是从通过@Bean方法创建的,那么允许bean 方法重写
      1. 如果这两个@Bean方法不属于同一个类。那么返回false,当前@Bean方法将覆盖这个同名bean定义。
      2. 如果这两个@Bean方法属于同一个类,并且这两个@Bean方法的方法名一致,即存在重载方法情况,那么返回true,将保留现有的bean定义,不再继续向后解析该@Bean方法。
    3. 从Spring 4.2开始,由组件扫描(基于组件注解)产生的bean定义可以被@Bean方法静默地覆盖
      1. 如果该同名bean定义是通过组件扫描注解产生的(通过组件扫描注解产生的bean定义类型就是ScannedGenericBeanDefinition)。那么返回false,当前bean方法将覆盖这个同名bean定义。
    4. 现有 bean 定义的role已标记为框架生成的 bean 吗?如果是,这允许当前 bean 方法重写它,因为它是应用程序级,返回false,当前bean方法将覆盖这个同名bean定义。
    5. 如果现有 bean 定义是通过XML产生的,并且不允许同名的BeanDefinition 覆盖(默认允许)那么因为存在同名的bean定义而抛出异常"@Bean definition illegally overridden by existing bean definition: "。
    6. 最后的返回true,表示不存在同名的bean定义或者允许同名bean定义覆盖。
    /**
     * ConfigurationClassBeanDefinitionReader的方法
     * 
     * 是否已存在同名bean定义或者允许现有的bean定义被覆盖
     */
    protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String beanName) {
        //如果注册表中不包含该beanName的bean定义
        if (!this.registry.containsBeanDefinition(beanName)) {
            //直接返回false
            return false;
        }
        //到这里,表示注册表中包含该beanName的bean定义
        BeanDefinition existingBeanDef = this.registry.getBeanDefinition(beanName);
    
    
        /*
         * 如果现有的 bean 定义也是从通过bean方法创建的,那么允许bean 方法重写
         */
        if (existingBeanDef instanceof ConfigurationClassBeanDefinition) {
            ConfigurationClassBeanDefinition ccbd = (ConfigurationClassBeanDefinition) existingBeanDef;
            //如果这两个bean方法属于同一个类
            if (ccbd.getMetadata().getClassName().equals(
                    beanMethod.getConfigurationClass().getMetadata().getClassName())) {
                //如果这两个bean方法的方法名一致,即重载方法情况,那么返回true,保留现有的 bean 定义
                if (ccbd.getFactoryMethodMetadata().getMethodName().equals(ccbd.getFactoryMethodName())) {
                    ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName());
                }
                return true;
            }
            //否则,返回false,因为这两个bean方法不属于同一个类,当前bean方法将覆盖这个同名bean定义
            else {
    
                return false;
            }
        }
        /*
         * 从Spring 4.2开始,由组件扫描产生的bean定义可以被@Bean方法静默地覆盖
         * 如果该同名bean定义是通过组件扫描注解产生的(通过组件扫描主角儿产生的bean定义类型就是ScannedGenericBeanDefinition)
         * 那么返回false,当前bean方法将覆盖这个同名bean定义
         */
        if (existingBeanDef instanceof ScannedGenericBeanDefinition) {
            //返回false,当前bean方法将覆盖这个同名bean定义
            return false;
        }
        /*
         * 现有 bean 定义的role已标记为框架生成的 bean 吗?如果是,这允许当前 bean 方法重写它,因为它是应用程序级
         * 那么返回false,当前bean方法将覆盖这个同名bean定义
         */
        if (existingBeanDef.getRole() > BeanDefinition.ROLE_APPLICATION) {
            return false;
        }
        /*
         * 如果现有 bean 定义是通过XML产生的,并且不允许同名的BeanDefinition 覆盖(默认允许)
         * 那么因为存在同名的bean定义而抛出异常"@Bean definition illegally overridden by existing bean definition: "
         */
        if (this.registry instanceof DefaultListableBeanFactory &&
                !((DefaultListableBeanFactory) this.registry).isAllowBeanDefinitionOverriding()) {
            throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
                    beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(String.format("Skipping bean definition for %s: a definition for bean '%s' " +
                            "already exists. This top-level bean definition is considered as an override.",
                    beanMethod, beanName));
        }
        //返回true
        return true;
    }
    
    2.1.8.2.3 loadBeanDefinitionsFromImportedResources从@ImportedResource加载bean定义

      加载、解析@ImportedResource注解引入的XML配置文件中的bean定义到注册表中。其内部最终还是调用BeanDefinitionReader的loadBeanDefinitions方法,加载resource资源,解析、注册bean定义。这个方法我们在"IoC容器初始化(2)"的文章中就详细讲过了。

    /**
     * ConfigurationClassBeanDefinitionReader的方法
     * <p>
     * 加载、解析@ImportedResource注解引入的XML配置文件中的bean定义到注册表中。
     *
     * @param importedResources 解析后的资源路径字符串
     */
    private void loadBeanDefinitionsFromImportedResources(
            Map<String, Class<? extends BeanDefinitionReader>> importedResources) {
        //bean定义读取器的缓存
        Map<Class<?>, BeanDefinitionReader> readerInstanceCache = new HashMap<>();
        /*循环加载*/
        importedResources.forEach((resource, readerClass) -> {
            // Default reader selection necessary?
            if (BeanDefinitionReader.class == readerClass) {
                //支持 Groovy 语言
                if (StringUtils.endsWithIgnoreCase(resource, ".groovy")) {
                    // When clearly asking for Groovy, that's what they'll get...
                    readerClass = GroovyBeanDefinitionReader.class;
                } else {
                    // Primarily ".xml" files but for any other extension as well
                    readerClass = XmlBeanDefinitionReader.class;
                }
            }
    
            BeanDefinitionReader reader = readerInstanceCache.get(readerClass);
            //如果reader为null,那么新建reader
            if (reader == null) {
                try {
                    // Instantiate the specified BeanDefinitionReader
                    reader = readerClass.getConstructor(BeanDefinitionRegistry.class).newInstance(this.registry);
                    // Delegate the current ResourceLoader to it if possible
                    if (reader instanceof AbstractBeanDefinitionReader) {
                        AbstractBeanDefinitionReader abdr = ((AbstractBeanDefinitionReader) reader);
                        abdr.setResourceLoader(this.resourceLoader);
                        abdr.setEnvironment(this.environment);
                    }
                    readerInstanceCache.put(readerClass, reader);
                } catch (Throwable ex) {
                    throw new IllegalStateException(
                            "Could not instantiate BeanDefinitionReader class [" + readerClass.getName() + "]");
                }
            }
    
            //最终调用reader的loadBeanDefinitions方法加载resource资源,解析、注册bean定义
            //这个方法我们在"IoC容器初始化(2)"的文章中就详细讲过了
            reader.loadBeanDefinitions(resource);
        });
    }
    
    2.1.8.2.4 loadBeanDefinitionsFromRegistrars从@Import加载bean定义

      该方法主要就是对于此前在processImports方法中的ImportBeanDefinitionRegistrar类型的引入类对象的registerBeanDefinitions方法的统一回调。

    /**
     * ConfigurationClassBeanDefinitionReader的方法
     * <p>
     * 注册@Import注解引入的bean定义到注册表中
     *
     * @param registrars importBeanDefinitionRegistrars缓存
     */
    private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
        //循环importBeanDefinitionRegistrars缓存map,回调每一个ImportBeanDefinitionRegistrar对象的三个参数的registerBeanDefinitions方法
        registrars.forEach((registrar, metadata) ->
                registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator));
    }
    

    3 postProcessBeanFactory

      postProcessBeanFactory方法同样在IoC容器初始化的invokeBeanFactoryPostProcessors步骤中被自动回调,当然调用时机晚于postProcessBeanDefinitionRegistry,并且逻辑也更加简单。
      在此前我们已经解析了全部的配置类,该方法就是对于被@Configuration及其派生注解标注的配置类生成CGLIB代理对象,主要目的是对@Bean方法进行代理增强,最后还会添加一个ImportAwareBeanPostProcessor类型的后处理器。

    /**
     * ConfigurationClassPostProcessor的方法
     * <p>
     * 对于被@Configuration注解标注的配置类生成CGLIB代理对象,主要目的是在对bean方法的调用进行代理增强
     * 最后还会添加一个ImportAwareBeanPostProcessor类型的后处理器
     */
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        //一致性哈希值的校验,类似于postProcessBeanDefinitionRegistry方法
        int factoryId = System.identityHashCode(beanFactory);
        //如果factoriesPostProcessed缓存包含了该factoryId值,那么说明此前该beanFactory已被该ConfigurationClassPostProcessor处理过了
        //Spring不允许同一个ConfigurationClassPostProcessor重复处理同一个beanFactory,直接抛出异常
        if (this.factoriesPostProcessed.contains(factoryId)) {
            throw new IllegalStateException(
                    "postProcessBeanFactory already called on this post-processor against " + beanFactory);
        }
        //当前beanFactory的factoryId加入factoriesPostProcessed缓存
        this.factoriesPostProcessed.add(factoryId);
        //如果registriesPostProcessed缓存不包含该factoryId,那么先调用processConfigBeanDefinitions处理所有的配置类
        if (!this.registriesPostProcessed.contains(factoryId)) {
            // BeanDefinitionRegistryPostProcessor hook apparently not supported...
            // Simply call processConfigurationClasses lazily at this point then.
            processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
        }
        /*
         * 1 增强配置类
         */
        enhanceConfigurationClasses(beanFactory);
        /*
         * 2 添加ImportAwareBeanPostProcessor类型的BeanPostProcessor后处理器
         */
        beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
    }
    

    3.1 enhanceConfigurationClasses增强配置类

      查找全部被@Configuration注解及其派生注解标注的proxyBeanMethods属性的值为true(默认就是true)的配置类,也就是具有"full"属性的配置类,并通过ConfigurationClassEnhancer进行代理增强。
      postProcessBeanDefinitionRegistry方法的checkConfigurationClassCandidate方法用于检测配置类并设置相应的属性,被@Configuration注解以及派生注解标注的配置类,就会被标记为"full",其他配置类则标记为“lite”。在该方法中终于用到了这个属性。
      对于full模式的Configuration,将会通过ConfigurationClassEnhancer配置类增强器生成CGLIB子类,并设置给当前bean定义的beanClass属性,那么后面在创建该配置类的实例时就会创建代理类的实例!
      创建代理子类的核心方法是enhancer.enhance方法!

    public static final String CONFIGURATION_CLASS_ATTRIBUTE = Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "configurationClass");
    
    /**
     * ConfigurationClassPostProcessor的方法
     * <p>
     * 查找全部被@Configuration注解标注的配置类,也就是具有"full"属性的配置类,并通过ConfigurationClassEnhancer进行代理增强
     */
    public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
        //需要进行代理增强的配置类集合
        Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>();
        //遍历全部的BeanDefinition
        for (String beanName : beanFactory.getBeanDefinitionNames()) {
            BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
            //获取CONFIGURATION_CLASS_ATTRIBUTE属性的值
            Object configClassAttr = beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE);
            MethodMetadata methodMetadata = null;
            if (beanDef instanceof AnnotatedBeanDefinition) {
                methodMetadata = ((AnnotatedBeanDefinition) beanDef).getFactoryMethodMetadata();
            }
            if ((configClassAttr != null || methodMetadata != null) && beanDef instanceof AbstractBeanDefinition) {
                // Configuration class (full or lite) or a configuration-derived @Bean method
                // -> resolve bean class at this point...
                AbstractBeanDefinition abd = (AbstractBeanDefinition) beanDef;
                if (!abd.hasBeanClass()) {
                    try {
                        abd.resolveBeanClass(this.beanClassLoader);
                    } catch (Throwable ex) {
                        throw new IllegalStateException(
                                "Cannot load configuration class: " + beanDef.getBeanClassName(), ex);
                    }
                }
            }
            //校验是否是full模式的Configuration
            if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) {
                if (!(beanDef instanceof AbstractBeanDefinition)) {
                    throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" +
                            beanName + "' since it is not stored in an AbstractBeanDefinition subclass");
                } else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) {
                    logger.info("Cannot enhance @Configuration bean definition '" + beanName +
                            "' since its singleton instance has been created too early. The typical cause " +
                            "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " +
                            "return type: Consider declaring such methods as 'static'.");
                }
                //当前full模式的Configuration 的beanName和bean定义都存入configBeanDefs集合
                configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);
            }
        }
        //如果configBeanDefs是空集,那么直接返回,不需要增强
        if (configBeanDefs.isEmpty()) {
            // nothing to enhance -> return immediately
            return;
        }
        //新建一个配置类增强器,通过生成CGLIB子类来增强配置类
        ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
        for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {
            AbstractBeanDefinition beanDef = entry.getValue();
            // If a @Configuration class gets proxied, always proxy the target class
            //新设置一个属性,如果@Configuration已代理,那么始终代理目标类
            beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
            // Set enhanced subclass of the user-specified bean class
            Class<?> configClass = beanDef.getBeanClass();
            //设置用户指定的 bean 类的CGLIB 增强子类
            Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);
            if (configClass != enhancedClass) {
                if (logger.isTraceEnabled()) {
                    logger.trace(String.format("Replacing bean definition '%s' existing class '%s' with " +
                            "enhanced class '%s'", entry.getKey(), configClass.getName(), enhancedClass.getName()));
                }
                //设置class为增强类的class,后续创建该配置类的实例时就会创建代理类的实例
                beanDef.setBeanClass(enhancedClass);
            }
        }
    }
    

    3.1.1 enhance增强

      被创建的CGLIB子类将继承当前目标配置类,并且实现EnhancedConfiguration接口(EnhancedConfiguration也实现了BeanFactoryAware接口)。
      被创建的CGLIB子类包括两个回调过滤器,一个是BeanMethodInterceptor拦截器,将会拦截配置类中的@Bean方法,另一个是BeanFactoryAwareMethodInterceptor拦截器,将会拦截setBeanFactory方法。所谓的“增强”,实际上就是对这两种方法的增强,而增强的具体实现就是通过这两个拦截器实现的!

    /**
     * ConfigurationClassEnhancer的方法
     * <p>
     * 加载指定的类并生成其配备容器感知回调的 CGLIB 子类
     *
     * @return 被增强的子类
     */
    public Class<?> enhance(Class<?> configClass, @Nullable ClassLoader classLoader) {
        //如果目标类型已经是EnhancedConfiguration的类型,那么直接返回
        if (EnhancedConfiguration.class.isAssignableFrom(configClass)) {
            if (logger.isDebugEnabled()) {
                logger.debug(String.format("Ignoring request to enhance %s as it has " +
                                "already been enhanced. This usually indicates that more than one " +
                                "ConfigurationClassPostProcessor has been registered (e.g. via " +
                                "<context:annotation-config>). This is harmless, but you may " +
                                "want check your configuration and remove one CCPP if possible",
                        configClass.getName()));
            }
            return configClass;
        }
        //通过一个Enhancer创建子类class
        Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
        if (logger.isTraceEnabled()) {
            logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
                    configClass.getName(), enhancedClass.getName()));
        }
        return enhancedClass;
    }
    
    
    //---------ConfigurationClassEnhancer的相关属性-------------
    
    /**
     * 要使用的回调拦截器
     */
    private static final Callback[] CALLBACKS = new Callback[]{
            //支持@Bean方法的回调拦截
            new BeanMethodInterceptor(),
            //支持BeanFactoryAware的setBeanFactory方法的回调拦截
            new BeanFactoryAwareMethodInterceptor(),
            NoOp.INSTANCE
    };
    /**
     * 条件回调筛选器,默认支持@Bean方法的回调拦截和BeanFactoryAware的setBeanFactory方法的回调拦截
     */
    private static final ConditionalCallbackFilter CALLBACK_FILTER = new ConditionalCallbackFilter(CALLBACKS);
    
    /**
     * ConfigurationClassEnhancer的方法
     * <p>
     * 创建CGLIB增强器实例
     */
    private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
        Enhancer enhancer = new Enhancer();
        //设置继承的父类为目标类型
        enhancer.setSuperclass(configSuperClass);
        //设置实现的接口为EnhancedConfiguration,并且EnhancedConfiguration也实现了BeanFactoryAware接口
        enhancer.setInterfaces(new Class<?>[]{EnhancedConfiguration.class});
        enhancer.setUseFactory(false);
        //设置类名命名策略
        enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
        //设置设置用于从此生成器创建字节码的策略
        enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
        //回调过滤器
        enhancer.setCallbackFilter(CALLBACK_FILTER);
        //设置回调类型,默认BeanMethodInterceptor和BeanFactoryAwareMethodInterceptor
        enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
        return enhancer;
    }
    

    3.1.2 BeanMethodInterceptor拦截器

      BeanMethodInterceptor该拦截器实现了cglib的MethodInterceptor(区别于Spring的MethodInterceptor),用于拦截当前配置类内部的任何@Bean方法的调用以及@Bean方法之间的调用,以确保正确处理 bean 语义。
       简单的说,对于被@Configuration注解及其派生注解标注的proxyBeanMethods属性的值为true(默认就是true)的配置类,内部的单例@Bean方法仅仅会被调用一次,用于创建对象,后续如果存在任何@Bean方法之间的调用,或者外部对象对@Bean方法的调用(这要求该对象交给Spring容器管理),都会是从容器中直接查找已创建的对象返回,不会再调用@Bean方法,能够保证是同一个实例,即都指向IoC内的单例。

      当@Bean方法被调用的时候,就会走intercept方法:

    1. 调用getBeanFactory方法获取beanFactory,实际上代理类生成了一个"$$beanFactory"属性,用于存放beanFactory。
    2. 该方法会默认将方法名作为beanName,,如果指定了@Bean注解的name属性,那么将会取第一个值作为beanName。
    3. 检查当前beanName对应的bean实例或者定义存在,并且是FactoryBean类型,第一次调用的时候应该是不存在的。如果存在就返回一个FactoryBean代理对象。
    4. isCurrentlyInvokedFactoryMethod方法用于检查给定方法是否对应于容器当前调用的方法,仅比较方法名称和参数类型。也就是说,如果@Bean方法是Spring自动调用的,比如用于创建对象,那么返回true,如果在其他@Bean方法中被调用,那么返回false。
      1. 如果是Spring自动调用的该@Bean方法,那么调用invokeSuper方法,实际上就是调用当前@Bean方法本身,用于创建bean实例,没有任何增强,返回对应的结果。
      2. 如果当前@Bean方法是在其他@Bean方法中被调用的。那么调用resolveBeanReference尝试直接从容器中获取给定BeanName的对象,如果容器中有,就直接返回,该@Bean方法后续不再被调用;如果没有,那么创建,该@Bean方法被调用一次,后续不再被调用。其核心就是beanFactory.getBean方法。
    /**
     * BeanMethodInterceptor的方法
     * <p>
     * 通过检查提供的 BeanFactory 是否已存在此 Bean 对象,来增强此@Bean方法
     *
     * @param enhancedConfigInstance 当前代理子类对象
     * @param beanMethod             调用方法
     * @param beanMethodArgs         方法参数
     * @param cglibMethodProxy       方法代理对象
     */
    @Override
    @Nullable
    public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs,
                            MethodProxy cglibMethodProxy) throws Throwable {
        //调用getBeanFactory方法获取beanFactory,实际上代理类生成了一个"$$beanFactory"属性,用于存放beanFactory
        ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
        //获取当前@Bean方法的beanName,默认就是方法名,如果指定了@Bean注解的name属性,那么将会取第一个值作为beanName
        String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);
    
        //是否是作用域代理,一般都不是
        if (BeanAnnotationHelper.isScopedProxy(beanMethod)) {
            String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName);
            if (beanFactory.isCurrentlyInCreation(scopedBeanName)) {
                beanName = scopedBeanName;
            }
        }
    
    
        //如果要处理 bean 方法间引用的情况,我们必须显式检查容器中已有缓存的实例。
    
        //检查当前beanName对应的bean定义存在并且是FactoryBean类型,第一次调用的时候应该是不存在的
        if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) &&
                factoryContainsBean(beanFactory, beanName)) {
            //获取factoryBean实例本身
            Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName);
            //创建一个FactoryBean的增强类来拦截getObject方法
            if (factoryBean instanceof ScopedProxyFactoryBean) {
                // Scoped proxy factory beans are a special case and should not be further proxied
            } else {
                // 这里将会选择合适的代理方式,JDK的代理或者CGLIB的代理
                return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName);
            }
        }
        /*
         * 检查给定方法是否对应于容器当前调用的方法,仅比较方法名称和参数类型
         * 也就是说,如果@Bean方法是Spring自动调用的,比如用于创建对象,那么返回true
         * 如果在其他@Bean方法中被调用,那么返回false
         */
        if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
            // The factory is calling the bean method in order to instantiate and register the bean
            // (i.e. via a getBean() call) -> invoke the super implementation of the method to actually
            // create the bean instance.
            if (logger.isInfoEnabled() &&
                    BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) {
                logger.info(String.format("@Bean method %s.%s is non-static and returns an object " +
                                "assignable to Spring's BeanFactoryPostProcessor interface. This will " +
                                "result in a failure to process annotations such as @Autowired, " +
                                "@Resource and @PostConstruct within the method's declaring " +
                                "@Configuration class. Add the 'static' modifier to this method to avoid " +
                                "these container lifecycle issues; see @Bean javadoc for complete details.",
                        beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName()));
            }
            //调用invokeSuper方法,实际上就是调用当前@Bean方法本身,没有任何增强,返回对应的结果
            return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
        }
        /*
         * 到这里,表示当前@Bean方法是在其他@Bean方法中被调用的
         * 那么直接从容器中获取给定BeanName的对象,如果容器中有,就直接返回,该@Bean方法后续不再被调用
         * 如果没有,那么创建,该@Bean方法被调用一次,后续不再被调用
         */
        return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
    }
    
    /**
     * 检查beanFactory中是否包含指定beanName的bean实例或者bean定义
     */
    private boolean factoryContainsBean(ConfigurableBeanFactory beanFactory, String beanName) {
        return (beanFactory.containsBean(beanName) && !beanFactory.isCurrentlyInCreation(beanName));
    }
    
    /**
     * 检查给定方法是否对应于容器当前调用的方法,仅比较方法名称和参数类型
     * 也就是说,如果@Bean方法是Spring自动调用的,比如用于创建对象,那么返回true
     * 如果在其他@Bean方法中被调用,那么返回false
     */
    private boolean isCurrentlyInvokedFactoryMethod(Method method) {
        Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod();
        return (currentlyInvoked != null && method.getName().equals(currentlyInvoked.getName()) &&
                Arrays.equals(method.getParameterTypes(), currentlyInvoked.getParameterTypes()));
    }
    

    3.1.2.1 resolveBeanReference解析bean引用

      如果当前@Bean方法是在其他@Bean方法中被调用,或者是外部对象对@Bean方法的调用(这要求该对象交给Spring容器管理),那么直接从容器中获取给定BeanName的对象,如果容器中有,就直接返回,该@Bean方法后续不再被调用。如果没有,那么创建,该@Bean方法被调用一次,后续不再被调用。
      核心方法就是IoC容器初始化的finishBeanFactoryInitialization阶段beanFactory.getBean方法(该方法我们在),如果有缓存,就从缓存取,没有就创建。创建的方法,在getBean方法内部的createBeanInstance方法中,将会通过工厂方法创建bean实例,即调用instantiateUsingFactoryMethod方法获取实例。IoC容器初始化我们在前面的文章就讲过了。
      如果@Bean方法之间存在互相调用,可以是间接的通过其他外部对象(这要求该对象交给Spring容器管理)调用,那么表示它们存在依赖关系,将会通过registerDependentBean方法注册到容器中,如果仅仅是外部对象(这要求该对象交给Spring容器管理)调用@Bean方法,那么不会注册依赖关系。

    /**
     * BeanMethodInterceptor的方法
     * <p>
     * 如果当前@Bean方法是在其他@Bean方法中被调用,或者是外部对象对@Bean方法的调用(这要求该对象交给Spring容器管理)
     * 那么直接从容器中获取给定BeanName的对象,如果容器中有,就直接返回,该@Bean方法后续不再被调用
     * 如果没有,那么创建,该@Bean方法被调用一次,后续不再被调用
     * 核心就是beanFactory.getBean方法
     */
    private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs,
                                        ConfigurableBeanFactory beanFactory, String beanName) {
    
        //当前bean是否在创建中
        boolean alreadyInCreation = beanFactory.isCurrentlyInCreation(beanName);
        try {
            if (alreadyInCreation) {
                //设置为非创建状态
                beanFactory.setCurrentlyInCreation(beanName, false);
            }
            //判断是否需要使用参数
            boolean useArgs = !ObjectUtils.isEmpty(beanMethodArgs);
            //如果需要使用参数并且当前beanName对应的bean是单例的
            if (useArgs && beanFactory.isSingleton(beanName)) {
                //如果有一个参数为null,那么useArgs设置为false,@Bean单例对象的参数不是可选的
                for (Object arg : beanMethodArgs) {
                    if (arg == null) {
                        useArgs = false;
                        break;
                    }
                }
            }
            //调用beanFactory.getBean方法获取bean实例,这一步就是从缓存中获取,如果没有,那么就创建,创建的时候就会调用那个该@Bean方法
            //如果有就直接返回,不再调用该@Bean方法
            Object beanInstance = (useArgs ? beanFactory.getBean(beanName, beanMethodArgs) :
                    beanFactory.getBean(beanName));
            //是否等于给定类型,一般都不相等,除了字符串类型
            if (!ClassUtils.isAssignableValue(beanMethod.getReturnType(), beanInstance)) {
                //这里的equals是为了检测NullBean实例
                if (beanInstance.equals(null)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug(String.format("@Bean method %s.%s called as bean reference " +
                                        "for type [%s] returned null bean; resolving to null value.",
                                beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName(),
                                beanMethod.getReturnType().getName()));
                    }
                    //如果是NullBean,那么设置为null
                    beanInstance = null;
                } else {
                    String msg = String.format("@Bean method %s.%s called as bean reference " +
                                    "for type [%s] but overridden by non-compatible bean instance of type [%s].",
                            beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName(),
                            beanMethod.getReturnType().getName(), beanInstance.getClass().getName());
                    try {
                        BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName);
                        msg += " Overriding bean of same name declared in: " + beanDefinition.getResourceDescription();
                    } catch (NoSuchBeanDefinitionException ex) {
                        // Ignore - simply no detailed message then.
                    }
                    throw new IllegalStateException(msg);
                }
            }
            //获取当前最外层正在被调用的@Bean方法,也就是直接或者见解调用该@Bean方法的@Bean方法,因此可能为null
            Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod();
            if (currentlyInvoked != null) {
                //获取外部@Bean方法的beanName
                String outerBeanName = BeanAnnotationHelper.determineBeanNameFor(currentlyInvoked);
                //注册外部@Bean方法和内部@Bean方法的依赖关系
                beanFactory.registerDependentBean(beanName, outerBeanName);
            }
            return beanInstance;
        } finally {
            //重新设置为正在创建中
            if (alreadyInCreation) {
                beanFactory.setCurrentlyInCreation(beanName, true);
            }
        }
    }
    

    3.1.3 BeanFactoryAwareMethodInterceptor拦截器

      BeanFactoryAwareMethodInterceptor拦截器用于拦截当前被@Configuration注解及其派生注解标注的配置类内部的setBeanFactory方法的调用在。
      它的intercept方法就很简单了,就是为CGLIB子类对象的"$$beanFactory"属性赋值为当前的beanFactory实例,在后面拦截@Bean方法的时候会用到(前面见识过了)。

    /**
     * BeanFactoryAwareMethodInterceptor的方法
     * <p>
     * 拦截BeanFactoryAware的setBeanFactory方法回调,为"$$beanFactory"属性赋值
     * 这里的拦截早于@Bean方法的拦截,在创建对象之后就马上调用了该方法
     */
    @Override
    @Nullable
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        //获取代理对象的"$$beanFactory"字段
        Field field = ReflectionUtils.findField(obj.getClass(), BEAN_FACTORY_FIELD);
        Assert.state(field != null, "Unable to find generated BeanFactory field");
        //设置置为第一个参数,也就是注入的beanFactory实例,在后面拦截@Bean方法的时候会用到
        field.set(obj, args[0]);
    
        //如果实际代理的目标类型还实现了BeanFactoryAware接口,那么还是调用其对应的setBeanFactory方法,否则直接退出
        if (BeanFactoryAware.class.isAssignableFrom(ClassUtils.getUserClass(obj.getClass().getSuperclass()))) {
            return proxy.invokeSuper(obj, args);
        }
        return null;
    }
    

    3.2 ImportAwareBeanPostProcessor后处理器

      在postProcessBeanFactory方法的最后,注册了一个ImportAwareBeanPostProcessor类型的后处理器,这个后处理器就是为了支持配置类代理类对象的创建!
      postProcessProperties方法在createBeanInstance方法创建bean实例之后, initializeBean初始化bean之前的populateBean装配bean的方法中被调用,就是用于对于EnhanceConfiguration类型的代理对象调用setBeanFactory方法设置beanFactory的属性值的。
      postProcessBeforeInitialization方法则是在initializeBean初始化bean的时候被调用,用于ImportAware类型的bean实例的setImportMetadata方法回调,获取引入当前类的类元数据的最后一个(当前类就是被@Import注解引入的普通类,引入类就是添加@Import注解的类),设置到setImportMetadata方法参数中。

    /**
     * ConfigurationClassPostProcessor的属性
     */
    private static final String IMPORT_REGISTRY_BEAN_NAME =
            ConfigurationClassPostProcessor.class.getName() + ".importRegistry";
    
    
    /**
     1. ImportAwareBeanPostProcessor类,位于ConfigurationClassPostProcessor内部
     */
    private static class ImportAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter {
    
        private final BeanFactory beanFactory;
    
        public ImportAwareBeanPostProcessor(BeanFactory beanFactory) {
            this.beanFactory = beanFactory;
        }
    
        /**
         * 为EnhancedConfiguration代理对象调用setBeanFactory方法设置beanFactory的属性值
         */
        @Override
        public PropertyValues postProcessProperties(@Nullable PropertyValues pvs, Object bean, String beanName) {
            //对于EnhancedConfiguration类型的代理对象注入beanFactory实例
            if (bean instanceof EnhancedConfiguration) {
                ((EnhancedConfiguration) bean).setBeanFactory(this.beanFactory);
            }
            return pvs;
        }
    
        /**
         * 用于ImportAware的setImportMetadata方法回调,设置importMetadata元数据
         * 主要是支持@Import注解引入的类
         */
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) {
            //如果是ImportAware类型
            if (bean instanceof ImportAware) {
                //获取IMPORT_REGISTRY_BEAN_NAME的bean实例,在前面的processConfigBeanDefinitions方法最后就注册了该名字的bean实例
                ImportRegistry ir = this.beanFactory.getBean(IMPORT_REGISTRY_BEAN_NAME, ImportRegistry.class);
                //获取引入当前类的类元数据的最后一个,当前类就是被@Import注解引入的普通类,引入类就是添加@Import注解的类
                AnnotationMetadata importingClass = ir.getImportingClassFor(ClassUtils.getUserClass(bean).getName());
                if (importingClass != null) {
                    //设置到setImportMetadata方法参数中
                    ((ImportAware) bean).setImportMetadata(importingClass);
                }
            }
            return bean;
        }
    }
    

    4 ConfigurationClassPostProcessor总结

      我们前面学习了ConfigurationClassPostProcessor的两大方法postProcessBeanDefinitionRegistry和postProcessBeanFactory,现在我们终于知道这个后处理器的重要性了吧?现在我们来简单总结一下:

    1. postProcessBeanDefinitionRegistry方法:
      1. 先被回调,用于整体解析配置类,注册bean定义。
      2. 如果具有@Configuration注解及其派生注解标注并且proxyBeanMethods属性的值为true(默认就是true),那么算作配置类,设置为full模式,如果具有@Bean,@Component,@ComponentScan,@Import,@ImportResource注解及其派生注解之一,那么同样算作配置类,设置为lite模式;这个模式将会在postProcessBeanFactory方法中用到。关于full和lite模式官方也有解释
      3. 解析配置类的内部类以及各种注解:@Order 排序注解、@Conditional条件注解、@PropertySources以及@PropertySource属性源注解、@ComponentScans以及@ComponentScan组件扫描注解、@Import引入bean注解、@ImportResource配置文件注解、@Bean注解。主要目的是解析出其中的bean定义并且注册到容器中,核心方法就是paese方法和loadBeanDefinitions方法。
      4. 这里也是对于非静态内部类的bean定义进行注册的地方,具体方法就是registerBeanDefinitionForImportedConfigurationClass,默认beanName就是类的全路径名。此前容器初始化过程中所讲的doScan方法中并没有解析注册非静态内部类的bean定义。
    2. postProcessBeanFactory方法:
      1. 后被回调,进一步处理配置类,对full模式配置类进行代理增强配置。
      2. 对于具有@Configuration注解及其派生注解标注的proxyBeanMethods属性的值为true(默认就是true)的配置类,将会设置CGLIB代理子类替换原来的beanClass,后面再创建bean实例的时候,实际上就会创建一个代理子类对象。
      3. 这里代理子类对象,继承了目标代理类,因此这要求配置类对象不能是final修饰的,并且还实现了EnhancedConfiguration接口,而EnhancedConfiguration也实现了BeanFactoryAware接口,将会自动回调setBeanFactory方法。
      4. 被创建的CGLIB子类包括两个回调过滤器,一个是BeanMethodInterceptor拦截器,将会拦截配置类中的@Bean方法,另一个是BeanFactoryAwareMethodInterceptor拦截器,将会拦截setBeanFactory方法。所谓的“增强”,实际上就是对这两种方法的增强,而增强的具体实现就是通过这两个拦截器实现的!
      5. 对于被@Configuration注解及其派生注解标注的proxyBeanMethods属性的值为true(默认就是true)的配置类,内部的单例@Bean方法仅仅会被调用一次,用于创建对象,后续如果存在任何@Bean方法之间的调用,或者外部对象对@Bean方法的调用(这要求该对象交给Spring容器管理),都会是从容器中直接查找已创建的对象返回,不会再调用@Bean方法,能够保证是同一个实例,即都指向IoC容器内的单例,这就是full模式配置类的好处之一,但缺点就是由于生成了代理对象,造成性能有所缺失。
      6. 对于被@Configuration注解及其派生注解标注的proxyBeanMethods属性的值为true(默认就是true)的配置类,它内部的@Bean方法,如果是static方法,则对于访问修饰符不作任何限制,因为不需要进行代理拦截,因此也没有上面的好处;如果不是static方法,就要求@Bean方法不能被private或者final修饰,因为这需要进行代理拦截。
      7. 最后会添加一个ImportAwareBeanPostProcessor后处理器,用于调用setBeanFactory和setImportMetadata方法设置相关属性。
    3. 这两个方法还有很多细节没总结出来,具体内容应该仔细看前面的源码。另外,里面的很多重要方法的源码也没讲解,比如environment.resolveRequiredPlaceholders(location)是怎么解析占位符的、loadBeanDefinitions(resource)是怎么加载资源路径字符串的、registry.registerBeanDefinition具体是怎么注册bean定义的……等等这些方法由于都是IoC容器初始化的核心方法,因此在此前IoC容器初始化的整体流程中就重点讲过了,要想掌握这些知识点,那还需要花费大量时间,大家可以看我此前的文章,总计约20万字的IoC初始化整体流程:Spring 5.x 源码
    4. 实际上,前面的文章加上该文章内容都学习完毕,那么Spring IoC的重要源码才算基本学习完毕,后面我们将介绍其他部分的核心源码,比如Spring AOP、事务机制等。

    相关文章:
      https://spring.io/
      Spring 5.x 学习
      Spring 5.x 源码

    如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

    展开全文
  • C#基础教程-c#实例教程,适合初学者

    万次阅读 多人点赞 2016-08-22 11:13:24
    C#基础教程-c#实例教程,适合初学者。 第一章 C#语言基础 本章介绍C#语言的基础知识,希望具有C语言的读者能够基本掌握C#语言,并以此为基础,能够进一步学习用C#语言编写window应用程序和Web应用程序。当然仅靠一...
    C#基础教程-c#实例教程,适合初学者。
    
    第一章 C#语言基础
    本章介绍C#语言的基础知识,希望具有C语言的读者能够基本掌握C#语言,并以此为基础,能够进一步学习用C#语言编写window应用程序和Web应用程序。当然仅靠一章的内容就完全掌握C#语言是不可能的,如需进一步学习C#语言,还需要认真阅读有关C#语言的专著。
    1.1 C#语言特点
    Microsoft.NET(以下简称.NET)框架是微软提出的新一代Web软件开发模型,C#语言是.NET框架中新一代的开发工具。C#语言是一种现代、面向对象的语言,它简化了C++语言在类、命名空间、方法重载和异常处理等方面的操作,它摒弃了C++的复杂性,更易使用,更少出错。它使用组件编程,和VB一样容易使用。C#语法和C++和JAVA语法非常相似,如果读者用过C++和JAVA,学习C#语言应是比较轻松的。
    用C#语言编写的源程序,必须用C#语言编译器将C#源程序编译为中间语言(MicroSoft Intermediate Language,MSIL)代码,形成扩展名为exe或dll文件。中间语言代码不是CPU可执行的机器码,在程序运行时,必须由通用语言运行环境(Common Language Runtime,CLR)中的既时编译器(JUST IN Time,JIT)将中间语言代码翻译为CPU可执行的机器码,由CPU执行。CLR为C#语言中间语言代码运行提供了一种运行时环境,C#语言的CLR和JAVA语言的虚拟机类似。这种执行方法使运行速度变慢,但带来其它一些好处,主要有:
     通用语言规范(Common Language Specification,CLS):.NET系统包括如下语言:C#、C++、VB、J#,他们都遵守通用语言规范。任何遵守通用语言规范的语言源程序,都可编译为相同的中间语言代码,由CLR负责执行。只要为其它操作系统编制相应的CLR,中间语言代码也可在其它系统中运行。
     自动内存管理:CLR内建垃圾收集器,当变量实例的生命周期结束时,垃圾收集器负责收回不被使用的实例占用的内存空间。不必象C和C++语言,用语句在堆中建立的实例,必须用语句释放实例占用的内存空间。也就是说,CLR具有自动内存管理功能。
     交叉语言处理:由于任何遵守通用语言规范的语言源程序,都可编译为相同的中间语言代码,不同语言设计的组件,可以互相通用,可以从其它语言定义的类派生出本语言的新类。由于中间语言代码由CLR负责执行,因此异常处理方法是一致的,这在调试一种语言调用另一种语言的子程序时,显得特别方便。
     增加安全:C#语言不支持指针,一切对内存的访问都必须通过对象的引用变量来实现,只允许访问内存中允许访问的部分,这就防止病毒程序使用非法指针访问私有成员。也避免指针的误操作产生的错误。CLR执行中间语言代码前,要对中间语言代码的安全性,完整性进行验证,防止病毒对中间语言代码的修改。
     版本支持:系统中的组件或动态联接库可能要升级,由于这些组件或动态联接库都要在注册表中注册,由此可能带来一系列问题,例如,安装新程序时自动安装新组件替换旧组件,有可能使某些必须使用旧组件才可以运行的程序,使用新组件运行不了。在.NET中这些组件或动态联接库不必在注册表中注册,每个程序都可以使用自带的组件或动态联接库,只要把这些组件或动态联接库放到运行程序所在文件夹的子文件夹bin中,运行程序就自动使用在bin文件夹中的组件或动态联接库。由于不需要在注册表中注册,软件的安装也变得容易了,一般将运行程序及库文件拷贝到指定文件夹中就可以了。
     完全面向对象:不象C++语言,即支持面向过程程序设计,又支持面向对象程序设计,C#语言是完全面向对象的,在C#中不再存在全局函数、全局变量,所有的函数、变量和常量都必须定义在类中,避免了命名冲突。C#语言不支持多重继承。
    1.2 编写控制台应用程序
    使用SDK命令行工具编写控制台程序
    第一个程序总是非常简单的,程序首先让用户通过键盘输入自己的名字,然后程序在屏幕上打印一条欢迎信息。程序的代码是这样的:
    using System;//导入命名空间。//为C#语言新增解释方法,解释到本行结束
    class Welcome//类定义,类的概念见下一节
    { /*解释开始,和C语言解释用法相同
    解释结束*/
    static void Main()//主程序,程序入口函数,必须在一个类中定义
    { Console.WriteLine("请键入你的姓名:");//控制台输出字符串
    Console.ReadLine();//从键盘读入数据,输入回车结束
    Console.WriteLine("欢迎!");
    }
    }
    可以用任意一种文本编辑软件完成上述代码的编写,然后把文件存盘,假设文件名叫做welcome.cs,C#源文件是以cs作为文件的扩展名。和C语言相同,C#语言是区分大小写的。高级语言总是依赖于许多在程序外部预定义的变量和函数。在C或C++中这些定义一般放到头文件中,用#include语句来导入这个头文件。而在C#语言中使用using语句导入名字空间,using System语句意义是导入System名字空间,C#中的using语句的用途与C++中#include语句的用途基本类似,用于导入预定义的变量和函数,这样在自己的程序中就可以自由地使用这些变量和函数。如果没有导入名字空间的话我们该怎么办呢?程序还能保持正确吗?答案是肯定的,那样的话我们就必须把代码改写成下面的样子:
    class Welcome
    { static void Main()
    { System.Console.WriteLine("请键入你的姓名:");
    System.Console.ReadLine();
    System.Console.WriteLine("欢迎!");
    }
    }
    也就是在每个Console前加上一个前缀System.,这个小原点表示Console是作为System的成员而存在的。C#中抛弃了C和C++中繁杂且极易出错的操作符象::和->等,C#中的复合名字一律通过.来连接。System是.Net平台框架提供的最基本的名字空间之一,有关名字空间的详细使用方法将在以后详细介绍,这里只要学会怎样导入名字空间就足够了。
    程序的第二行class Welcome声明了一个类,类的名字叫做Welcome。C#程序中每个变量或函数都必须属于一个类,包括主函数Main(),不能象C或C++那样建立全局变量。C#语言程序总是从Main()方法开始执行,一个程序中不允许出现两个或两个以上的Main()方法。请牢记C#中Main()方法必须被包含在一个类中,Main第一个字母必须大写,必须是一个静态方法,也就是Main()方法必须使用static修饰。static void Main()是类Welcome中定义的主函数。静态方法意义见以后章节。
    程序所完成的输入输出功能是通过Console类来完成的,Console是在名字空间System中已经定义好的一个类。Console类有两个最基本的方法WriteLine和ReadLine。ReadLine表示从输入设备输入数据,WriteLine则用于在输出设备上输出数据。
    如果在电脑上安装了Visual Studio.Net,则可以在集成开发环境中直接选择快捷键或菜单命令编译并执行源文件。如果您不具备这个条件,那么至少需要安装Microsoft.Net Framework SDK,这样才能够运行C#语言程序。Microsoft.Net Framework SDK中内置了C#的编译器csc.exe,下面让我们使用这个微软提供的命令行编译器对程序welcome.cs进行编译。假设已经将welcome.cs文件保存在d:\Charp目录下,启动命令行提示符,在屏幕上输入一行命令:d:回车,cd Charp回车,键入命令:
    C:\WINNT\Microsoft.NET\Framework\v1.0.3705\csc welcome.cs
    如果一切正常welcome.cs文件将被编译,编译后生成可执行文件Welcome.exe。可以在命令提示符窗口运行可执行文件Welcome.exe,屏幕上出现一行字符提示您输入姓名:请键入你的姓名:输入任意字符并按下回车键,屏幕将打印出欢迎信息:欢迎!
    注意,和我们使用过的绝大多数编译器不同,在C#中编译器只执行编译这个过程,而在C和C++中要经过编译和链接两个阶段。换而言之C#源文件并不被编译为目标文件.obj,而是直接生成可执行文件.exe或动态链接库.dll,C#编译器中不需要包含链接器。
    使用Visual Studio.Net建立控制台程序
    (1) 运行Visual Studio.Net程序,出现如图1.2.2A界面。
    (2) 单击新建项目按钮,出现如图1.2.2B对话框。在项目类型(P)编辑框中选择Visual C#项目,在模板(T)编辑框中选择控制台应用程序,在名称(N)编辑框中键入e1,在位置(L)编辑框中键入D:\csarp,必须预先创建文件夹D:\csarp。也可以单击浏览按钮,在打开文件对话框中选择文件夹。单击确定按钮,创建项目。出现如图1.2.2C界面。编写一个应用程序,可能包含多个文件,才能生成可执行文件,所有这些文件的集合叫做一个项目。
    (3) 修改class1.cs文件如下,有阴影部分是新增加的语句,其余是集成环境自动生成的。
    using System;
    namespace e1
    {
    /// <summary>
    /// Class1 的摘要说明。
    /// </summary>
    class Class1
    {
    /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main(string[] args)
    {
    //
    // TODO: 在此处添加代码以启动应用程序
    //
    Console.WriteLine("请键入你的姓名:");
    Console.ReadLine();
    Console.WriteLine("欢迎!");
    }
    }
    }
    (4) 按CTRL+F5键,运行程序,如右图,和1.2.1节运行效果相同。屏幕上出现一行字符,提示您输入姓名:请键入你的姓名:输入任意字符并按下回车键,屏幕将打印出欢迎信息:欢迎!输入回车退出程序。



    图1.2.2A

    图1.2.2B


    1.3 类的基本概念
    C#语言是一种现代、面向对象的语言。面向对象程序设计方法提出了一个全新的概念:类,它的主要思想是将数据(数据成员)及处理这些数据的相应方法(函数成员)封装到类中,类的实例则称为对象。这就是我们常说的封装性。
    类的基本概念
    类可以认为是对结构的扩充,它和C中的结构最大的不同是:类中不但可以包括数据,还包括处理这些数据的函数。类是对数据和处理数据的方法(函数)的封装。类是对某一类具有相同特性和行为的事物的描述。例如,定义一个描述个人情况的类Person如下:
    using System;
    class Person//类的定义,class是保留字,表示定义一个类,Person是类名
    { private string name="张三";//类的数据成员声明
    private int age=12;//private表示私有数据成员
    public void Display()//类的方法(函数)声明,显示姓名和年龄
    { Console.WriteLine("姓名:{0},年龄:{1}",name,age);
    }
    public void SetName(string PersonName)//修改姓名的方法(函数)
    { name=PersonName;
    }
    public void SetAge(int PersonAge)
    { age=PersonAge;
    }
    }
    Console.WriteLine("姓名:{0},年龄:{1}",name,age)的意义是将第二个参数变量name变为字符串填到{0}位置,将第三个参数变量age变为字符串填到{1}位置,将第一个参数表示的字符串在显示器上输出。
    大家注意,这里我们实际定义了一个新的数据类型,为用户自己定义的数据类型,是对个人的特性和行为的描述,他的类型名为Person,和int,char等一样为一种数据类型。用定义新数据类型Person类的方法把数据和处理数据的函数封装起来。类的声明格式如下:
    属性 类修饰符 class 类名{类体}
    其中,关键字class、类名和类体是必须的,其它项是可选项。类修饰符包括new、public、protected、internal、private、abstract和sealed,这些类修饰符以后介绍。类体用于定义类的成员。
    1.3.2 类成员的存取控制
    一般希望类中一些数据不被随意修改,只能按指定方法修改,既隐蔽一些数据。同样一些函数也不希望被其它类程序调用,只能在类内部使用。如何解决这个问题呢?可用访问权限控制字,常用的访问权限控制字如下:private(私有),public(公有)。在数据成员或函数成员前增加访问权限控制字,可以指定该数据成员或函数成员的访问权限。
    私有数据成员只能被类内部的函数使用和修改,私有函数成员只能被类内部的其它函数调用。类的公有函数成员可以被类的外部程序调用,类的公有数据成员可以被类的外部程序直接使用修改。公有函数实际是一个类和外部通讯的接口,外部函数通过调用公有函数,按照预先设定好的方法修改类的私有成员。对于上述例子,name和age是私有数据成员,只能通过公有函数SetName()和SetAge()修改,既它们只能按指定方法修改。
    这里再一次解释一下封装,它有两个意义,第一是把数据和处理数据的方法同时定义在类中。第二是用访问权限控制字使数据隐蔽。
    1.3.3 类的对象
    Person类仅是一个用户新定义的数据类型,由它可以生成Person类的实例,C#语言叫对象。用如下方法声明类的对象:Person OnePerson=new Person();此语句的意义是建立Person类对象,返回对象地址赋值给Person类变量OnePerson。也可以分两步创建Person类的对象:Person OnePerson;OnePerson=new Person();OnePerson虽然存储的是Person类对象地址,但不是C中的指针,不能象指针那样可以进行加减运算,也不能转换为其它类型地址,它是引用型变量,只能引用(代表)Person对象,具体意义参见以后章节。和C、C++不同,C#只能用此种方法生成类对象。
    在程序中,可以用OnePerson.方法名或OnePerson.数据成员名访问对象的成员。例如:OnePerson.Display(),公用数据成员也可以这样访问。注意,C#语言中不包括C++语言中的->符号。
    1.3.4 类的构造函数和析构函数
    在建立类的对象时,需做一些初始化工作,例如对数据成员初始化。这些可以用构造函数来完成。每当用new生成类的对象时,自动调用类的构造函数。 因此,可以把初始化的工作放到构造函数中完成。构造函数和类名相同,没有返回值。例如可以定义Person类的构造函数如下:
    public Person(string Name,int Age)//类的构造函数,函数名和类同名,无返回值。
    { name=Name;
    age=Age;
    }
    当用Person OnePerson=new Person(“张五”,20)语句生成Person类对象时,将自动调用以上构造函数。请注意如何把参数传递给构造函数。
    变量和类的对象都有生命周期,生命周期结束,这些变量和对象就要被撤销。类的对象被撤销时,将自动调用析构函数。一些善后工作可放在析构函数中完成。析构函数的名字为~类名,无返回类型,也无参数。Person类的析构函数为~ Person()。C#中类析构函数不能显示地被调用,它是被垃圾收集器撤销不被使用的对象时自动调用的。
    1.3.5 类的构造函数的重载
    在C#语言中,同一个类中的函数,如果函数名相同,而参数类型或个数不同,认为是不同的函数,这叫函数重载。仅返回值不同,不能看作不同的函数。这样,可以在类定义中,定义多个构造函数,名字相同,参数类型或个数不同。根据生成类的对象方法不同,调用不同的构造函数。例如可以定义Person类没有参数的构造函数如下:
    public Person()//类的构造函数,函数名和类同名,无返回值。
    { name="张三";
    age=12;
    }
    用语句Person OnePerson=new Person("李四",30)生成对象时,将调用有参数的构造函数,而用语句Person OnePerson=new Person()生成对象时,调用无参数的构造函数。由于析构函数无参数,因此,析构函数不能重载。
    1.3.6 使用Person类的完整的例子
    下边用一个完整的例子说明Person类的使用:(VisualStudio.Net编译通过)
    using System;
    namespace e1//定义以下代码所属命名空间,意义见以后章节
    { class Person
    { private String name="张三";//类的数据成员声明
    private int age=12;
    public void Display()//类的方法(函数)声明,显示姓名和年龄
    { Console.WriteLine("姓名:{0},年龄:{1}",name,age);
    }
    public void SetName(string PersonName)//指定修改姓名的方法(函数)
    { name=PersonName;
    }
    public void SetAge(int PersonAge)//指定修改年龄的方法(函数)
    { age=PersonAge;
    }
    public Person(string Name,int Age)//构造函数,函数名和类同名,无返回值
    { name=Name;
    age=Age;
    }
    public Person()//类的构造函数重载
    { name="田七";
    age=12;
    }
    }
    class Class1
    { static void Main(string[] args)
    { Person OnePerson=new Person("李四",30);//生成类的对象
    OnePerson.Display();
    //下句错误,在其它类(Class1类)中,不能直接修改Person类中的私有成员。
    //OnePerson.name="王五";
    //只能通过Person类中公有方法SetName修改Person类中的私有成员name。
    OnePerson.SetName("王五");
    OnePerson.SetAge(40);
    OnePerson.Display();
    OnePerson=new Person();
    OnePerson.Display();
    }
    }
    }
    键入CTRL+F5运行后,显示的效果是:
    姓名: 李四,年龄:30
    姓名: 王五,年龄:40
    姓名: 田七,年龄:12
    1.4 C#的数据类型
    从大的方面来分,C#语言的数据类型可以分为三种:值类型,引用类型,指针类型,指针类型仅用于非安全代码中。本节重点讨论值类型和引用类型。
    1.4.1 值类型和引用类型区别
    在C#语言中,值类型变量存储的是数据类型所代表的实际数据,值类型变量的值(或实例)存储在栈(Stack)中,赋值语句是传递变量的值。引用类型(例如类就是引用类型)的实例,也叫对象,不存在栈中,而存储在可管理堆(Managed Heap)中,堆实际上是计算机系统中的空闲内存。引用类型变量的值存储在栈(Stack)中,但存储的不是引用类型对象,而是存储引用类型对象的引用,即地址,和指针所代表的地址不同,引用所代表的地址不能被修改,也不能转换为其它类型地址,它是引用型变量,只能引用指定类对象,引用类型变量赋值语句是传递对象的地址。见下例:
    using System;
    class MyClass//类为引用类型
    { public int a=0;
    }
    class Test
    { static void Main()
    { f1();
    }
    static public void f1()
    { int v1=1;//值类型变量v1,其值1存储在栈(Stack)中
    int v2=v1;//将v1的值(为1)传递给v2,v2=1,v1值不变。
    v2=2;//v2=2,v1值不变。
    MyClass r1=new MyClass();//引用变量r1存储MyClass类对象的地址
    MyClass r2=r1;//r1和r2都代表是同一个MyClass类对象
    r2.a=2;//和语句r1.a=2等价
    }
    }
    存储在栈中的变量,当其生命周期结束,自动被撤销,例如,v1存储在栈中,v1和函数f1同生命周期,退出函数f1,v1不存在了。但在堆中的对象不能自动被撤销。因此C和C++语言,在堆中建立的对象,不使用时必须用语句释放对象占用的存储空间。.NET系统CLR内建垃圾收集器,当对象的引用变量被撤销,表示对象的生命周期结束,垃圾收集器负责收回不被使用的对象占用的存储空间。例如,上例中引用变量r1及r2是MyClass类对象的引用,存储在栈中,退出函数f1,r1和r2都不存在了,在堆中的MyClass类对象也就被垃圾收集器撤销。也就是说,CLR具有自动内存管理功能。
    1.4.2 值类型变量分类
    C#语言值类型可以分为以下几种:
     简单类型(Simple types)
    简单类型中包括:数值类型和布尔类型(bool)。数值类型又细分为:整数类型、字符类型(char)、浮点数类型和十进制类型(decimal)。
     结构类型(Struct types)
     枚举类型(Enumeration types)
    C#语言值类型变量无论如何定义,总是值类型变量,不会变为引用类型变量。
    1.4.3 结构类型
    结构类型和类一样,可以声明构造函数、数据成员、方法、属性等。结构和类的最根本的区别是结构是值类型,类是引用类型。和类不同,结构不能从另外一个结构或者类派生,本身也不能被继承,因此不能定义抽象结构,结构成员也不能被访问权限控制字protected修饰,也不能用virtual和abstract修饰结构方法。在结构中不能定义析构函数。虽然结构不能从类和结构派生,可是结构能够继承接口,结构继承接口的方法和类继承接口的方法基本一致。下面例子定义一个点结构point:
    using System;
    struct point//结构定义
    { public int x,y;//结构中也可以声明构造函数和方法,变量不能赋初值
    }
    class Test
    { static void Main()
    { point P1;
    P1.x=166;
    P1.y=111;
    point P2;
    P2=P1;//值传递,使P2.x=166,P2.y=111
    point P3=new point();//用new生成结构变量P3,P3仍为值类型变量
    }//用new生成结构变量P3仅表示调用默认构造函数,使x=y==0。
    }
    1.4.4 简单类型
    简单类型也是结构类型,因此有构造函数、数据成员、方法、属性等,因此下列语句int i=int.MaxValue;string s=i.ToString()是正确的。即使一个常量,C#也会生成结构类型的实例,因此也可以使用结构类型的方法,例如:string s=13.ToString()是正确的。简单类型包括:整数类型、字符类型、布尔类型、浮点数类型、十进制类型。见下表:
    保留字 System命名空间中的名字 字节数 取值范围
    sbyte System.Sbyte 1 -128~127
    byte System.Byte 1 0~255
    short System.Int16 2 -32768~32767
    ushort System.UInt16 2 0~65535
    int System.Int32 4 -2147483648~2147483647
    uint System.UInt32 4 0~4292967295
    long System.Int64 8 -9223372036854775808~9223372036854775808
    ulong System.UInt64 8 0~18446744073709551615
    char System.Char 2 0~65535
    float System.Single 4 3.4E-38~3.4E+38
    double System.Double 8 1.7E-308~1.7E+308
    bool System.Boolean (true,false)
    decimal System.Decimal 16 正负1.0到7.9之间
    C#简单类型使用方法和C、C++中相应的数据类型基本一致。需要注意的是:
     和C语言不同,无论在何种系统中,C#每种数据类型所占字节数是一定的。
     字符类型采用Unicode字符集,一个Unicode标准字符长度为16位。
     整数类型不能隐式被转换为字符类型(char),例如char c1=10是错误的,必须写成:char c1=(char)10,char c='A',char c='\x0032';char c='\u0032'。
     布尔类型有两个值:false,true。不能认为整数0是false,其它值是true。bool x=1是错误的,不存在这种写法,只能写成x=true 或x=false。
     十进制类型(decimal)也是浮点数类型,只是精度比较高,一般用于财政金融计算。
    1.4.5 枚举类型
    C#枚举类型使用方法和C、C++中的枚举类型基本一致。见下例:
    using System;
    class Class1
    { enum Days {Sat=1, Sun, Mon, Tue, Wed, Thu, Fri};
    //使用Visual Studio.Net,enum语句添加在[STAThread]前边
    static void Main(string[] args)
    { Days day=Days.Tue;
    int x=(int)Days.Tue;//x=2
    Console.WriteLine("day={0},x={1}",day,x);//显示结果为:day=Tue,x=4
    }
    }
    在此枚举类型Days中,每个元素的默认类型为int,其中Sun=0,Mon=1,Tue=2,依此类推。也可以直接给枚举元素赋值。例如:
    enum Days{Sat=1,Sun,Mon,Tue,Wed,Thu,Fri,Sat};
    在此枚举中,Sun=1,Mon=2,Tue=3,Wed=4,等等。和C、C++中不同,C#枚举元素类型可以是byte、sbyte、short、ushort、int、uint、long和ulong类型,但不能是char类型。见下例:
    enum Days:byte{Sun,Mon,Tue,Wed,Thu,Fri,Sat};//元素为字节类型
    1.4.6 值类型的初值和默认构造函数
    所有变量都要求必须有初值,如没有赋值,采用默认值。对于简单类型,sbyte、byte、short、ushort、int、uint、long和ulong默认值为0,char类型默认值是(char)0,float为0.0f,double为0.0d,decimal为0.0m,bool为false,枚举类型为0,在结构类型和类中,数据成员的数值类型变量设置为默认值,引用类型变量设置为null。
    可以显示的赋值,例如int i=0。而对于复杂结构类型,其中的每个数据成员都按此种方法赋值,显得过于麻烦。由于数值类型都是结构类型,可用new语句调用其构造函数初始化数值类型变量,例如:int j=new int()。请注意,用new语句并不是把int变量变为引用变量,j仍是值类型变量,这里new仅仅是调用其构造函数。所有的数值类型都有默认的无参数的构造函数,其功能就是为该数值类型赋初值为默认值。对于自定义结构类型,由于已有默认的无参数的构造函数,不能再定义无参数的构造函数,但可以定义有参数的构造函数。
    1.4.7 引用类型分类
    C#语言中引用类型可以分为以下几种:
     类:C#语言中预定义了一些类:对象类(object类)、数组类、字符串类等。当然,程序员可以定义其它类。
     接口。
     代表。
    C#语言引用类型变量无论如何定义,总是引用类型变量,不会变为值类型变量。C#语言引用类型对象一般用运算符new建立,用引用类型变量引用该对象。本节仅介绍对象类型(object类型)、字符串类型、数组。其它类型在其它节中介绍。
    1.4.8 对象类(object类)
    C#中的所有类型(包括数值类型)都直接或间接地以object类为基类。对象类(object类)是所有其它类的基类。任何一个类定义,如果不指定基类,默认object为基类。继承和基类的概念见以后章节。C#语言规定,基类的引用变量可以引用派生类的对象(注意,派生类的引用变量不可以引用基类的对象),因此,对一个object的变量可以赋予任何类型的值:
    int x =25;
    object obj1;
    obj1=x;
    object obj2= 'A';
    object关键字是在命名空间System中定义的,是类System.Object的别名。
    1.4.9 数组类
    在进行批量处理数据的时候,要用到数组。数组是一组类型相同的有序数据。数组按照数组名、数据元素的类型和维数来进行描述。C#语言中数组是类System.Array类对象,比如声明一个整型数数组:int[] arr=new int[5];实际上生成了一个数组类对象,arr是这个对象的引用(地址)。
    在C#中数组可以是一维的也可以是多维的,同样也支持数组的数组,即数组的元素还是数组。一维数组最为普遍,用的也最多。我们先看一个一维数组的例子:
    using System;
    class Test
    { static void Main()
    { int[] arr=new int[3];//用new运算符建立一个3个元素的一维数组
    for(int i=0;i<arr.Length;i++)//arr.Length是数组类变量,表示数组元素个数
    arr[i]=i*i;//数组元素赋初值,arr[i]表示第i个元素的值
    for (int i=0;i<arr.Length;i++)//数组第一个元素的下标为0
    Console.WriteLine("arr[{0}]={1}",i,arr[i]);
    }
    }
    这个程序创建了一个int类型3个元素的一维数组,初始化后逐项输出。其中arr.Length表示数组元素的个数。注意数组定义不能写为C语言格式:int arr[]。程序的输出为:
    arr[0] = 0
    arr[1] = 1
    arr[2] = 4
    上面的例子中使用的是一维数组,下面介绍多维数组:
    string[] a1;//一维string数组类引用变量a1
    string[,] a2;//二维string数组类引用变量a2
    a2=new string[2,3];
    a2[1,2]="abc";
    string[,,] a3;//三维string数组类引用变量a3
    string[][] j2;//数组的数组,即数组的元素还是数组
    string[][][][] j3;
    在数组声明的时候,可以对数组元素进行赋值。看下面的例子:
    int[] a1=new int[]{1,2,3};//一维数组,有3个元素。
    int[] a2=new int[3]{1,2,3};//此格式也正确
    int[] a3={1,2,3};//相当于int[] a3=new int[]{1,2,3};
    int[,] a4=new int[,]{{1,2,3},{4,5,6}};//二维数组,a4[1,1]=5
    int[][] j2=new int[3][];//定义数组j2,有三个元素,每个元素都是一个数组
    j2[0]=new int[]{1,2,3};//定义第一个元素,是一个数组
    j2[1]=new int[]{1, 2, 3, 4, 5, 6};//每个元素的数组可以不等长
    j2[2]=new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9};
    1.4.10 字符串类(string类)
    C#还定义了一个基本的类string,专门用于对字符串的操作。这个类也是在名字空间System中定义的,是类System.String的别名。字符串应用非常广泛,在string类的定义中封装了许多方法,下面的一些语句展示了string类的一些典型用法:
     字符串定义
    string s;//定义一个字符串引用类型变量s
    s="Zhang";//字符串引用类型变量s指向字符串"Zhang"
    string FirstName="Ming";
    string LastName="Zhang";
    string Name=FirstName+" "+LastName;//运算符+已被重载
    string SameName=Name;
    char[] s2={'计','算','机','科','学'};
    string s3=new String(s2);
     字符串搜索
    string s="ABC科学";
    int i=s.IndexOf("科");
    搜索"科"在字符串中的位置,因第一个字符索引为0,所以"A"索引为0,"科"索引为3,因此这里i=3,如没有此字符串i=-1。注意C#中,ASCII和汉字都用2字节表示。
     字符串比较函数
    string s1="abc";
    string s2="abc";
    int n=string.Compare(s1,s2);//n=0
    n=0表示两个字符串相同,n小于零,s1<s2,n大于零,s1>s2。此方法区分大小写。也可用如下办法比较字符串:
    string s1="abc";
    string s="abc";
    string s2="不相同";
    if(s==s1)//还可用!=。虽然String是引用类型,但这里比较两个字符串的值
    s2="相同";
     判断是否为空字符串
    string s="";
    string s1="不空";
    if(s.Length==0)
    s1="空";
     得到子字符串或字符
    string s="取子字符串";
    string sb=s.Substring(2,2);//从索引为2开始取2个字符,Sb="字符",s内容不变
    char sb1=s[0];//sb1='取'
    Console.WriteLine(sb1);//显示:取
     字符串删除函数
    string s="取子字符串";
    string sb=s.Remove(0,2);//从索引为0开始删除2个字符,Sb="字符串",s内容不变
     插入字符串
    string s="计算机科学";
    string s1=s.Insert(3,"软件");//s1="计算机软件科学",s内容不变
     字符串替换函数
    string s="计算机科学";
    string s1=s.Replace("计算机","软件");//s1="软件科学",s内容不变
     把String转换为字符数组
    string S="计算机科学";
    char[] s2=S.ToCharArray(0,S.Length);//属性Length为字符类对象的长度
     其它数据类型转换为字符串
    int i=9;
    string s8=i.ToString();//s8="9"
    float n=1.9f;
    string s9=n.ToString();//s8="1.9"
    其它数据类型都可用此方法转换为字符类对象
     大小写转换
    string s="AaBbCc";
    string s1=s.ToLower();//把字符转换为小写,s内容不变
    string s2=s.ToUpper();//把字符转换为大写,s内容不变
     删除所有的空格
    string s="A bc ";
    s.Trim();//删除所有的空格
    string类其它方法的使用请用帮助系统查看,方法是打开Visual Studio.Net的代码编辑器,键入string,将光标移到键入的字符串string上,然后按F1键。
    1.4.11 类型转换
    在编写C#语言程序中,经常会碰到类型转换问题。例如整型数和浮点数相加,C#会进行隐式转换。详细记住那些类型数据可以转换为其它类型数据,是不可能的,也是不必要的。程序员应记住类型转换的一些基本原则,编译器在转换发生问题时,会给出提示。C#语言中类型转换分为:隐式转换、显示转换、加框(boxing)和消框(unboxing)等三种。
    一. 隐式转换
    隐式转换就是系统默认的、不需要加以声明就可以进行的转换。例如从int类型转换到long类型就是一种隐式转换。在隐式转换过程中,转换一般不会失败,转换过程中也不会导致信息丢失。例如:
    int i=10;
    long l=i;
    二. 显示转换
    显式类型转换,又叫强制类型转换。与隐式转换正好相反,显式转换需要明确地指定转换类型,显示转换可能导致信息丢失。下面的例子把长整形变量显式转换为整型:
    long l=5000;
    int i=(int)l;//如果超过int取值范围,将产生异常
    三. 加框(boxing)和消框(unboxing)
    加框(boxing)和消框(unboxing)是C#语言类型系统提出的核心概念,加框是值类型转换为object(对象)类型,消框是object(对象)类型转换为值类型。有了加框和消框的概念,对任何类型的变量来说最终我们都可以看作是object类型。
    1 加框操作
    把一个值类型变量加框也就是创建一个object对象,并将这个值类型变量的值复制给这个object对象。例如:
    int i=10;
    object obj=i;//隐式加框操作,obj为创建的object对象的引用。
    我们也可以用显式的方法来进行加框操作,例如:
    int i =10;
    object obj=object(i);//显式加框操作
    值类型的值加框后,值类型变量的值不变,仅将这个值类型变量的值复制给这个object对象。我们看一下下面的程序:
    using System
    class Test
    { public static void Main()
    { int n=200;
    object o=n;
    o=201;//不能改变n
    Console.WriteLine("{0},{1}",n,o);
    }
    }
    输出结果为:200,201。这就证明了值类型变量n和object类对象o都独立存在着。
    2. 消框操作
    和加框操作正好相反,消框操作是指将一个对象类型显式地转换成一个值类型。消框的过程分为两步:首先检查这个object对象,看它是否为给定的值类型的加框值,如是,把这个对象的值拷贝给值类型的变量。我们举个例子来看看一个对象消框的过程:
    int i=10;
    object obj=i;
    int j=(int)obj;//消框操作
    可以看出消框过程正好是加框过程的逆过程,必须注意加框操作和消框操作必须遵循类型兼容的原则。
    3. 加框和消框的使用
    定义如下函数:
    void Display(Object o)//注意,o为Object类型
    { int x=(int)o;//消框
    System.Console.WriteLine("{0},{1}",x,o);
    }
    调用此函数:int y=20;Display(y);在此利用了加框概念,虚参被实参替换:Object o=y,也就是说,函数的参数是Object类型,可以将任意类型实参传递给函数。
    1.5 运算符
    C#语言和C语言的运算符用法基本一致。以下重点讲解二者之间不一致部分。
    1.5.1 运算符分类
    与C语言一样,如果按照运算符所作用的操作数个数来分,C#语言的运算符可以分为以下几种类型:
     一元运算符:一元运算符作用于一个操作数,例如:-X、++X、X--等。
     二元运算符:二元运算符对两个操作数进行运算,例如:x+y。
     三元运算符:三元运算符只有一个:x? y:z。
    C#语言运算符的详细分类及操作符从高到低的优先级顺序见下表。
    类别 操作符
    初级操作符 (x) x.y f(x) a[x] x++ x-- new type of sizeof checked unchecked
    一元操作符 + - ! ~ ++x –x (T)x
    乘除操作符 * / %
    加减操作符 + -
    移位操作符 << >>
    关系操作符 < > <= >= is as
    等式操作符 == !=
    逻辑与操作符 &
    逻辑异或操作符 ^
    逻辑或操作符 |
    条件与操作符 &&
    条件或操作符 ||
    条件操作符 ?:
    赋值操作符 = *= /= %= += -= <<= >>= &= ^= |=
    1.5.2 测试运算符is
    is操作符用于动态地检查表达式是否为指定类型。使用格式为:e is T,其中e是一个表达式,T是一个类型,该式判断e是否为T类型,返回值是一个布尔值。例子:
    using System;
    class Test
    { public static void Main()
    { Console.WriteLine(1 is int);
    Console.WriteLine(1 is float);
    Console.WriteLine(1.0f is float);
    Console.WriteLine(1.0d is double);
    }
    }
    输出为:
    True
    False
    True
    True
    1.5.3 typeof运算符
    typeof操作符用于获得指定类型在system名字空间中定义的类型名字,例如:
    using System;
    class Test
    { static void Main()
    { Console.WriteLine(typeof(int));
    Console.WriteLine(typeof(System.Int32));
    Console.WriteLine(typeof(string));
    Console.WriteLine(typeof(double[]));
    }
    }
    产生如下输出,由输出可知int和System.int32是同一类型。
    System.Int32
    System.Int32
    System.String
    System.Double[]
    1.5.4 溢出检查操作符checked和unchecked
    在进行整型算术运算(如+、-、*、/等)或从一种整型显式转换到另一种整型时,有可能出现运算结果超出这个结果所属类型值域的情况,这种情况称之为溢出。整型算术运算表达式可以用checked或unchecked溢出检查操作符,决定在编译和运行时是否对表达式溢出进行检查。如果表达式不使用溢出检查操作符或使用了checked操作符,常量表达式溢出,在编译时将产生错误,表达式中包含变量,程序运行时执行该表达式产生溢出,将产生异常提示信息。而使用了unchecked操作符的表达式语句,即使表达式产生溢出,编译和运行时都不会产生错误提示。但这往往会出现一些不可预期的结果,所以使用unchecked操作符要小心。下面的例子说明了checked和unchecked操作符的用法:
    using System;
    class Class1
    { static void Main(string[] args)
    { const int x=int.MaxValue;
    unchecked//不检查溢出
    { int z=x*2;//编译时不产生编译错误,z=-2
    Console.WriteLine("z={0}",z);//显示-2
    }
    checked//检查溢出
    { int z1=(x*2);//编译时会产生编译错误
    Console.WriteLine("z={0}",z1);
    }
    }
    }
    1.5.5 new运算符
    new操作符可以创建值类型变量、引用类型对象,同时自动调用构造函数。例如:
    int x=new int();//用new创建整型变量x,调用默认构造函数
    Person C1=new Person ();//用new建立的Person类对象。Person 变量C1对象的引用
    int[] arr=new int[2];//数组也是类,创建数组类对象,arr是数组对象的引用
    需注意的是,int x=new int()语句将自动调用int结构不带参数的构造函数,给x赋初值0,x仍是值类型变量,不会变为引用类型变量。
    1.5.6 运算符的优先级
    当一个表达式包含多种操作符时,操作符的优先级控制着操作符求值的顺序。例如,表达式x+y*z按照x+(y*z)顺序求值,因为*操作符比+操作符有更高的优先级。这和数学运算中的先乘除后加减是一致的。1.5.1节中的表总结了所有操作符从高到低的优先级顺序。
    当两个有相同优先级的操作符对操作数进行运算时,例如x+y-z,操作符按照出现的顺序由左至右执行,x+y-z按(x+y)-z进行求值。赋值操作符按照右接合的原则,即操作按照从右向左的顺序执行。如x=y=z按照x=(y=z)进行求值。建议在写表达式的时候,如果无法确定操作符的实际顺序,则尽量采用括号来保证运算的顺序,这样也使得程序一目了然,而且自己在编程时能够思路清晰。
    1.6 程序控制语句
    C#语言控制语句和C基本相同,使用方法基本一致。C#语言控制语句包括:if语句、swith语句、while语句、do…while语句、for语句、foreach语句、break语句、continue语句、goto语句、return语句、异常处理语句等,其中foreach语句和异常语句是C#语言新增加控制语句。本节首先介绍一下这些语句和C语言的不同点,然后介绍C#语言新增的控制语句。
    1.6.1 和C语言的不同点
     与C不同,if语句、while语句、do…while语句、for语句中的判断语句,一定要用布尔表达式,不能认为0为false,其它数为true。
     switch语句不再支持遍历,C和C++语言允许switch语句中case标签后不出现break语句,但C#不允许这样,它要求每个case标签项后使用break语句或goto跳转语句,即不允许从一个case自动遍历到其它case,否则编译时将报错。switch语句的控制类型,即其中控制表达式的数据类型可以是sbyte、byte、short、ushort、uint、long、ulong、char、string或枚举类型。每个case标签中的常量表达式必须属于或能隐式转换成控制类型。如果有两个或两个以上case标签中的常量表达式值相同,编译时将会报错。执行switch语句,首先计算switch表达式,然后与case后的常量表达式的值进行比较,执行第一个与之匹配的case分支下的语句。如果没有case常量表达式的值与之匹配,则执行dafault分支下的语句,如果没有dafault语句,则退出switch语句。switch语句中可以没有dafault语句,但最多只能有一个dafault语句。见下例:
    using System;
    class class1
    { static void Main()
    { System.Console.WriteLine("请输入要计算天数的月份");
    string s=System.Console.ReadLine();
    string s1="";
    switch(s)
    { case "1": case "3": case "5":
    case "7": case "8": case "10":
    case "12"://共用一条语句
    s1="31";break;
    case "2":
    s1="28";break;
    case "4": case "6": case "9":
    goto case "11";//goto语句仅为说明问题,无此必要
    case "11":
    s1="30";break;
    default:
    s1="输入错误";break;
    }
    System.Console.WriteLine(s1);
    }
    }
    1.6.2 foreach语句
    foreach语句是C#语言新引入的语句,C和C++中没有这个语句,它借用Visual Basic中的foreach语句。语句的格式为:
    foreach(类型 变量名 in 表达式) 循环语句
    其中表达式必须是一个数组或其它集合类型,每一次循环从数组或其它集合中逐一取出数据,赋值给指定类型的变量,该变量可以在循环语句中使用、处理,但不允许修改变量,该变量的指定类型必须和表达式所代表的数组或其它集合中的数据类型一致。例子:
    using System;
    class Test()
    { public static void Main()
    { int[] list={10,20,30,40};//数组
    foreach(int m in list)
    Console.WriteLine("{0}",m);
    }
    }
    对于一维数组,foreach语句循环顺序是从下标为0的元素开始一直到数组的最后一个元素。对于多维数组,元素下标的递增是从最右边那一维开始的。同样break和continue可以出现在foreach语句中,功能不变。
    1.6.3 异常语句
    在编写程序时,不仅要关心程序的正常操作,还应该考虑到程序运行时可能发生的各类不可预期的事件,比如用户输入错误、内存不够、磁盘出错、网络资源不可用、数据库无法使用等,所有这些错误被称作异常,不能因为这些异常使程序运行产生问题。各种程序设计语言经常采用异常处理语句来解决这类异常问题。
    C#提供了一种处理系统级错误和应用程序级错误的结构化的、统一的、类型安全的方法。C#异常语句包含try子句、catch子句和finally子句。try子句中包含可能产生异常的语句,该子句自动捕捉执行这些语句过程中发生的异常。catch子句中包含了对不同异常的处理代码,可以包含多个catch子句,每个catch子句中包含了一个异常类型,这个异常类型必须是System.Exception类或它的派生类引用变量,该语句只扑捉该类型的异常。可以有一个通用异常类型的catch子句,该catch子句一般在事先不能确定会发生什么样的异常的情况下使用,也就是可以扑捉任意类型的异常。一个异常语句中只能有一个通用异常类型的catch子句,而且如果有的话,该catch子句必须排在其它catch子句的后面。无论是否产生异常,子句finally一定被执行,在finally子句中可以增加一些必须执行的语句。
    异常语句捕捉和处理异常的机理是:当try子句中的代码产生异常时,按照catch子句的顺序查找异常类型。如果找到,执行该catch子句中的异常处理语句。如果没有找到,执行通用异常类型的catch子句中的异常处理语句。由于异常的处理是按照catch子句出现的顺序逐一检查catch子句,因此catch子句出现的顺序是很重要的。无论是否产生异常,一定执行finally子句中的语句。异常语句中不必一定包含所有三个子句,因此异常语句可以有以下三种可能的形式:
     try –catch语句,可以有多个catch语句
     try -finally语句
     try -catch-finally语句,可以有多个catch语句
    请看下边的例子:
    1. try–catch-finally语句
    using System
    using System.IO//使用文件必须引用的名字空间
    public class Example
    { public static void Main()
    { StreamReader sr=null;//必须赋初值null,否则编译不能通过
    try
    { sr=File.OpenText("d:\\csarp\\test.txt");//可能产生异常
    string s;
    while(sr.Peek()!=-1)
    { s=sr.ReadLine();//可能产生异常
    Console.WriteLine(s);
    }
    }
    catch(DirectoryNotFoundException e)//无指定目录异常
    { Console.WriteLine(e.Message);
    }
    catch(FileNotFoundException e)//无指定文件异常
    { Console.WriteLine("文件"+e.FileName+"未被发现");
    }
    catch(Exception e)//其它所有异常
    { Console.WriteLine("处理失败:{0}",e.Message);
    }
    finally
    { if(sr!=null)
    sr.Close();
    }
    }
    }
    2. try -finally语句
    上例中,其实可以不用catch语句,在finally子句中把文件关闭,提示用户是否正确打开了文件,请读者自己完成。
    3. try -catch语句
    请读者把上例修改为使用try-catch结构,注意在每个catch语句中都要关闭文件。
    1.7 类的继承
    在1.3节,定义了一个描述个人情况的类Person,如果我们需要定义一个雇员类,当然可以从头开始定义雇员类Employee。但这样不能利用Person类中已定义的函数和数据。比较好的方法是,以Person类为基类,派生出一个雇员类Employee,雇员类Employee继承了Person类的数据成员和函数成员,既Person类的数据成员和函数成员成为Employee类的成员。这个Employee类叫以Person类为基类的派生类,这是C#给我们提出的方法。C#用继承的方法,实现代码的重用。
    1.7.1 派生类的声明格式
    派生类的声明格式如下:
    属性 类修饰符 class 派生类名:基类名 {类体}
    雇员类Employee定义如下:
    class Employee:Person//Person类是基类
    { private string department;//部门,新增数据成员
    private decimal salary;//薪金,新增数据成员
    public Employee(string Name,int Age,string D,decimal S):base(Name,Age)
    {//注意base的第一种用法,根据参数调用指定基类构造函数,注意参数的传递
    department=D;
    salary=S;
    }
    public new void Display()//覆盖基类Display()方法,注意new,不可用override
    { base.Display();//访问基类被覆盖的方法,base的第二种用法
    Console.WriteLine("部门:{0} 薪金:{1}",department,salary);
    }
    }
    修改主函数如下:
    class Class1
    { static void Main(string[] args)
    { Employee OneEmployee=new Employee("李四",30,"计算机系",2000);
    OneEmployee.Display();
    }
    }
    Employee类继承了基类Person的方法SetName()、SetAge(),数据成员name和age,即认为基类Person的这些成员也是Employee类的成员,但不能继承构造函数和析构函数。添加了新的数据成员department和salary。覆盖了方法Display()。请注意,虽然Employee类继承了基类Person的name和age,但由于它们是基类的私有成员,Employee类中新增或覆盖的方法不能直接修改name和age,只能通过基类原有的公有方法SetName()和SetAge()修改。如果希望在Employee类中能直接修改name和age,必须在基类中修改它们的属性为protected。
    1.7.2 base 关键字
    base关键字用于从派生类中访问基类成员,它有两种基本用法:
     在定义派生类的构造函数中,指明要调用的基类构造函数,由于基类可能有多个构造函数,根据base后的参数类型和个数,指明要调用哪一个基类构造函数。参见上节雇员类Employee构造函数定义中的base的第一种用法。
     在派生类的方法中调用基类中被派生类覆盖的方法。参见上节雇员类Employee的Display()方法定义中的base的第二种用法。
    1.7.3 覆盖基类成员
    在派生类中,通过声明与基类完全相同新成员,可以覆盖基类的同名成员,完全相同是指函数类型、函数名、参数类型和个数都相同。如上例中的方法Display()。派生类覆盖基类成员不算错误,但会导致编译器发出警告。如果增加new修饰符,表示认可覆盖,编译器不再发出警告。请注意,覆盖基类的同名成员,并不是移走基类成员,只是必须用如下格式访问基类中被派生类覆盖的方法:base.Display()。
    1.7.4 C#语言类继承特点
    C#语言类继承有如下特点:
     C#语言只允许单继承,即派生类只能有一个基类。
     C#语言继承是可以传递的,如果C从B派生,B从A派生,那么C不但继承B的成员,还要继承A中的成员。
     派生类可以添加新成员,但不能删除基类中的成员。
     派生类不能继承基类的构造函数、析构函数和事件。但能继承基类的属性。
     派生类可以覆盖基类的同名成员,如果在派生类中覆盖了基类同名成员,基类该成员在派生类中就不能被直接访问,只能通过base.基类方法名访问。
     派生类对象也是其基类的对象,但基类对象却不是其派生类的对象。例如,前边定义的雇员类Employee是Person类的派生类,所有雇员都是人类,但很多人并不是雇员,可能是学生,自由职业者,儿童等。因此C#语言规定,基类的引用变量可以引用其派生类对象,但派生类的引用变量不可以引用其基类对象。
    1.8 类的成员
    由于C#程序中每个变量或函数都必须属于一个类或结构,不能象C或C++那样建立全局变量,因此所有的变量或函数都是类或结构的成员。类的成员可以分为两大类:类本身所声明的以及从基类中继承来的。
    1.8.1 类的成员类型
    类的成员包括以下类型:
     局部变量:在for、switch等语句中和类方法中定义的变量,只在指定范围内有效。
     字段:即类中的变量或常量,包括静态字段、实例字段、常量和只读字段。
     方法成员:包括静态方法和实例方法。
     属性:按属性指定的get方法和Set方法对字段进行读写。属性本质上是方法。
     事件:代表事件本身,同时联系事件和事件处理函数。
     索引指示器:允许象使用数组那样访问类中的数据成员。
     操作符重载:采用重载操作符的方法定义类中特有的操作。
     构造函数和析构函数。
    包含有可执行代码的成员被认为是类中的函数成员,这些函数成员有方法、属性、索引指示器、操作符重载、构造函数和析构函数。
    1.8.2 类成员访问修饰符
    访问修饰符用于指定类成员的可访问性,C#访问修饰符有private、protected、public和internal4种。Private声明私有成员,私有数据成员只能被类内部的函数使用和修改,私有函数成员只能被类内部的函数调用。派生类虽然继承了基类私有成员,但不能直接访问它们,只能通过基类的公有成员访问。protected声明保护成员,保护数据成员只能被类内部和派生类的函数使用和修改,保护函数成员只能被类内部和派生类的函数调用。public声明公有成员,类的公用函数成员可以被类的外部程序所调用,类的公用数据成员可以被类的外部程序直接使用。公有函数实际是一个类和外部通讯的接口,外部函数通过调用公有函数,按照预先设定好的方法修改类的私有成员和保护成员。internal声明内部成员,内部成员只能在同一程序集中的文件中才是可以访问的,一般是同一个应用(Application)或库(Library)。
    1.9 类的字段和属性
    一般把类或结构中定义的变量和常量叫字段。属性不是字段,本质上是定义修改字段的方法,由于属性和字段的紧密关系,把它们放到一起叙述。
    1.9.1 静态字段、实例字段、常量和只读字段
    用修饰符static声明的字段为静态字段。不管包含该静态字段的类生成多少个对象或根本无对象,该字段都只有一个实例,静态字段不能被撤销。必须采用如下方法引用静态字段:类名.静态字段名。如果类中定义的字段不使用修饰符static,该字段为实例字段,每创建该类的一个对象,在对象内创建一个该字段实例,创建它的对象被撤销,该字段对象也被撤销,实例字段采用如下方法引用:实例名.实例字段名。用const修饰符声明的字段为常量,常量只能在声明中初始化,以后不能再修改。用readonly修饰符声明的字段为只读字段,只读字段是特殊的实例字段,它只能在字段声明中或构造函数中重新赋值,在其它任何地方都不能改变只读字段的值。例子:
    public class Test
    { public const int intMax=int.MaxValue;//常量,必须赋初值
    public int x=0;//实例字段
    public readonly int y=0;//只读字段
    public static int cnt=0;//静态字段
    public Test(int x1,int y1)//构造函数
    { //intMax=0;//错误,不能修改常量
    x=x1;//在构造函数允许修改实例字段
    y=y1;//在构造函数允许修改只读字段
    cnt++;//每创建一个对象都调用构造函数,用此语句可以记录对象的个数
    }
    public void Modify(int x1,int y1)
    { //intMax=0;//错误,不能修改常量
    x=x1;
    cnt=y1;
    //y=10;//不允许修改只读字段
    }
    }
    class Class1
    { static void Main(string[] args)
    { Test T1=new Test(100,200);
    T1.x=40;//引用实例字段采用方法:实例名.实例字段名
    Test.cnt=0;//引用静态字段采用方法:类名.静态字段名
    int z=T1.y;//引用只读字段
    z=Test.intMax;//引用常量
    }
    }
    1.9.2 属性
    C#语言支持组件编程,组件也是类,组件用属性、方法、事件描述。属性不是字段,但必然和类中的某个或某些字段相联系,属性定义了得到和修改相联系的字段的方法。C#中的属性更充分地体现了对象的封装性:不直接操作类的数据内容,而是通过访问器进行访问,借助于get和set方法对属性的值进行读写。访问属性值的语法形式和访问一个变量基本一样,使访问属性就象访问变量一样方便,符合习惯。
    在类的基本概念一节中,定义一个描述个人情况的类Person,其中字段name和age是私有字段,记录姓名和年龄,外部通过公有方法SetName和SetAge修改这两个私有字段。现在用属性来描述姓名和年龄。例子如下:
    using System;
    public class Person
    { private string P_name="张三";//P_name是私有字段
    private int P_age=12;//P_age是私有字段
    public void Display()//类的方法声明,显示姓名和年龄
    { Console.WriteLine("姓名:{0},年龄:{1}",P_name,P_age);
    }
    public string Name//定义属性Name
    { get
    { return P_name;}
    set
    { P_name=value;}
    }
    public int Age//定义属性Age
    { get
    { return P_age;}
    set
    { P_age=value;}
    }
    }
    public class Test
    { public static void Main()
    { Person OnePerson= new Person();
    OnePerson.Name="田七";//value="田七",通过set方法修改变量P_Name
    string s=OnePerson.Name;//通过get方法得到变量P_Name值
    OnePerson.Age=20;//通过定义属性,既保证了姓名和年龄按指定方法修改
    int x=OnePerson.Age;//语法形式和修改、得到一个变量基本一致,符合习惯
    OnePerson.Display();
    }
    }
    在属性的访问声明中,只有set访问器表明属性的值只能进行设置而不能读出,只有get访问器表明属性的值是只读的不能改写,同时具有set访问器和get访问器表明属性的值的读写都是允许的。
    虽然属性和字段的语法比较类似,但由于属性本质上是方法,因此不能把属性当做变量那样使用,也不能把属性作为引用型参数或输出参数来进行传递。
    1.10 类的方法
    方法是类中用于执行计算或其它行为的成员。所有方法都必须定义在类或结构中。
    1.10.1 方法的声明
    方法的声明格式如下:
    属性 方法修饰符 返回类型 方法名(形参列表){方法体}
    方法修饰符包括new、public、protected、internal、private、static、virtual、sealed、override、abstract和extern。这些修饰符有些已经介绍过,其它修饰符将逐一介绍。返回类型可以是任何合法的C#数据类型,也可以是void,即无返回值。形参列表的格式为:(形参类型 形参1,形参类型 形参2,...),可以有多个形参。不能使用C语言的形参格式。
    1.10.2 方法参数的种类
    C#语言的方法可以使用如下四种参数(请注意和参数类型的区别):
     值参数,不含任何修饰符。
     引用参数,以ref修饰符声明。
     输出参数,以out修饰符声明。
     数组参数,以params修饰符声明。
    1. 值参数
    当用值参数向方法传递参数时,程序给实参的值做一份拷贝,并且将此拷贝传递给该方法,被调用的方法不会修改实参的值,所以使用值参数时,可以保证实参的值是安全的。如果参数类型是引用类型,例如是类的引用变量,则拷贝中存储的也是对象的引用,所以拷贝和实参引用同一个对象,通过这个拷贝,可以修改实参所引用的对象中的数据成员。
    2. 引用参数
    有时在方法中,需要修改或得到方法外部的变量值,C语言用向方法传递实参指针来达到目的,C#语言用引用参数。当用引用参数向方法传递实参时,程序将把实参的引用,即实参在内存中的地址传递给方法,方法通过实参的引用,修改或得到方法外部的变量值。引用参数以ref修饰符声明。注意在使用前,实参变量要求必须被设置初始值。
    3. 输出参数
    为了把方法的运算结果保存到外部变量,因此需要知道外部变量的引用(地址)。输出参数用于向方法传递外部变量引用(地址),所以输出参数也是引用参数,与引用参数的差别在于调用方法前无需对变量进行初始化。在方法返回后,传递的变量被认为经过了初始化。值参数、引用参数和输出参数的使用见下例:
    using System;
    class g{public int a=0;}//类定义
    class Class1
    { public static void F1(ref char i)//引用参数
    { i='b';}
    public static void F2(char i)//值参数,参数类型为值类型
    { i='d';}
    public static void F3(out char i)//输出参数
    { i='e';}
    public static void F4(string s)//值参数,参数类型为字符串
    { s="xyz";}
    public static void F5(g gg)//值参数,参数类型为引用类型
    { gg.a=20;}
    public static void F6(ref string s)//引用参数,参数类型为字符串
    { s="xyz";}
    static void Main(string[] args)
    { char a='c';
    string s1="abc";
    F2(a);//值参数,不能修改外部的a
    Console.WriteLine(a);//因a未被修改,显示c
    F1(ref a);//引用参数,函数修改外部的a的值
    Console.WriteLine(a);//a被修改为b,显示b
    Char j;
    F3(out j);//输出参数,结果输出到外部变量j
    Console.WriteLine(j);//显示e
    F4(s1);//值参数,参数类型是字符串,s1为字符串引用变量
    Console.WriteLine(s1);//显示:abc,字符串s1不被修改
    g g1=new g();
    F5(g1);//值参数,但实参是一个类引用类型变量
    Console.WriteLine(g1.a.ToString());//显示:20,修改对象数据
    F6(ref s1);//引用参数,参数类型是字符串,s1为字符串引用变量
    Console.WriteLine(s1);//显示:xyz,字符串s1被修改
    }
    }
    4. 数组参数
    数组参数使用params说明,如果形参表中包含了数组参数,那么它必须是参数表中最后一个参数,数组参数只允许是一维数组。比如string[]和string[][]类型都可以作为数组型参数。最后,数组型参数不能再有ref和out修饰符。见下例:
    using System;
    class Class1
    { static void F(params int[] args)//数组参数,有params说明
    { Console.Write("Array contains {0} elements:",args.Length);
    foreach (int i in args)
    Console.Write(" {0}",i);
    Console.WriteLine();
    }
    static void Main(string[] args)
    { int[] a = {1,2,3};
    F(a);//实参为数组类引用变量a
    F(10, 20, 30, 40);//等价于F(new int[] {60,70,80,90});
    F(new int[] {60,70,80,90});//实参为数组类引用
    F();//等价于F(new int[] {});
    F(new int[] {});//实参为数组类引用,数组无元素
    }
    }
    程序输出
    Array contains 3 elements: 1 2 3
    Array contains 4 elements: 10 20 30 40
    Array contains 4 elements: 60,70,80,90
    Array contains 0 elements:
    Array contains 0 elements:
    方法的参数为数组时也可以不使用params,此种方法可以使用一维或多维数组,见下例:
    using System;
    class Class1
    { static void F(int[,] args)//值参数,参数类型为数组类引用变量,无params说明
    { Console.Write("Array contains {0} elements:",args.Length);
    foreach (int i in args)
    Console.Write(" {0}",i);
    Console.WriteLine();
    }
    static void Main(string[] args)
    { int[,] a = {{1,2,3},{4,5,6}};
    F(a);//实参为数组类引用变量a
    //F(10, 20, 30, 40);//此格式不能使用
    F(new int[,] {{60,70},{80,90}});//实参为数组类引用
    //F();//此格式不能使用
    //F(new int[,] {});//此格式不能使用
    }
    }
    程序输出
    Array contains 3 elements: 1 2 3 4 5 6
    Array contains 4 elements: 60,70,80,90
    1.10.3 静态方法和实例方法
    用修饰符static声明的方法为静态方法,不用修饰符static声明的方法为实例方法。不管类生成或未生成对象,类的静态方法都可以被使用,使用格式为:类名.静态方法名。静态方法只能使用该静态方法所在类的静态数据成员和静态方法。这是因为使用静态方法时,该静态方法所在类可能还没有对象,即使有对象,由于用类名.静态方法名方式调用静态方法,静态方法没有this指针来存放对象的地址,无法判定应访问哪个对象的数据成员。在类创建对象后,实例方法才能被使用,使用格式为:对象名.实例方法名。实例方法可以使用该方法所在类的所有静态成员和实例成员。例子如下:
    using System;
    public class UseMethod
    { private static int x=0;//静态字段
    private int y=1;//实例字段
    public static void StaticMethod()//静态方法
    { x=10;//正确,静态方法访问静态数据成员
    //y=20;//错误,静态方法不能访问实例数据成员
    }
    public void NoStaticMethod()//实例方法
    { x=10;//正确,实例方法访问静态数据成员
    y=20;//正确,实例方法访问实例数据成员
    }
    }
    public class Class1
    { public static void Main()
    { UseMethod m=new UseMethod();
    UseMethod.StaticMethod();//使用静态方法格式为:类名.静态方法名
    m.NoStaticMethod();//使用实例方法格式为:对象名.实例方法名
    }
    }
    1.10.4 方法的重载
    在C#语言中,如果在同一个类中定义的函数名相同,而参数类型或参数个数不同,认为是不相同的函数,仅返回值不同,不能看作不同函数,这叫做函数的重载。前边Person类中定义了多个构造函数就是重载的例子。在C语言中,若计算一个数据的绝对值,则需要对不同数据类型求绝对值方法使用不同的方法名,如用abc()求整型数绝对值,labs()求长整型数绝对值,fabs()求浮点数绝对值。而在C#语言中,可以使用函数重载特性,对这三个函数定义同样的函数名,但使用不同的参数类型。下面是实现方法:
    using System;
    public class UseAbs
    { public int abs(int x)//整型数求绝对值
    { return(x<0 ? -x:x);}
    public long abs(long x)//长整型数求绝对值
    {return(x<0 ? -x:x);}
    public double abs(double x)//浮点数求绝对值
    {return(x<0 ? -x:x);}
    }
    class Class1
    { static void Main(string[] args)
    { UseAbs m=new UseAbs();
    int x=-10;
    long y=-123;
    double z=-23.98d;
    x=m.abs(x);
    y=m.abs(y);
    z=m.abs(z);
    Console.WriteLine("x={0},y={1},z={2}",x,y,z);
    }
    }
    类的对象调用这些同名方法,在编译时,根据调用方法的实参类型决定调用那个同名方法,计算不同类型数据的绝对值。这给编程提供了极大方便。
    1.10.5 操作符重载
    操作符重载是将C#语言中的已有操作符赋予新的功能,但与该操作符的本来含义不冲突,使用时只需根据操作符出现的位置来判别其具体执行哪一种运算。操作符重载,实际是定义了一个操作符函数,操作符函数声明的格式如下:
    static public 函数返回类型 operator 重新定义的操作符(形参表)
    C#语言中有一些操作符是可以重载的,例如:+ - ! ~ ++ -- true false * / % & | ^ << >> == != > < >= <=等等。但也有一些操作符是不允许进行重载的,例如:=, &&, ||, ?:, new, typeof, sizeof, is等。
    下边的例子,定义一个复数类,并且希望复数的加减乘除用符号+,-.*,/来表示。
    using System;
    class Complex//复数类定义
    { private double Real;//复数实部
    private double Imag;//复数虚部
    public Complex(double x,double y)//构造函数
    { Real=x;
    Imag=y;
    }
    static public Complex operator - (Complex a)//重载一元操作符负号,注意1个参数
    { return (new Complex(-a.Real,-a.Imag));}
    static public Complex operator +(Complex a,Complex b)//重载二元操作符加号
    { return (new Complex(a.Real+b.Real,a.Imag+b.Imag));}
    public void Display()
    { Console.WriteLine("{0}+({1})j",Real,Imag);}
    }
    class Class1
    { static void Main(string[] args)
    { Complex x=new Complex(1.0,2.0);
    Complex y=new Complex(3.0,4.0);
    Complex z=new Complex(5.0,7.0);
    x.Display();//显示:1+(2)j
    y.Display();//显示:3+(4)j
    z.Display();//显示:5+(7)j
    z=-x;//等价于z=opeator-(x)
    z.Display();//显示:-1+(-2)j
    z=x+y;//即z=opeator+(x,y)
    z.Display();//显示:4+(6)j
    }
    }
    1.10.6 this关键字
    每个类都可以有多个对象,例如定义Person类的两个对象:
    Person P1=new Person("李四",30);
    Person P2=new Person("张三",40);
    因此P1.Display()应显示李四信息,P2.Display()应显示张三信息,但无论创建多少个对象,只有一个方法Display(),该方法是如何知道显示那个对象的信息的呢?C#语言用引用变量this记录调用方法Display()的对象,当某个对象调用方法Display()时,this便引用该对象(记录该对象的地址)。因此,不同的对象调用同一方法时,方法便根据this所引用的不同对象来确定应该引用哪一个对象的数据成员。this是类中隐含的引用变量,它是被自动被赋值的,可以使用但不能被修改。例如:P1.Display(),this引用对象P1,显示李四信息。P2.Display(),this引用对象P2,显示张三信息。
    1.11 类的多态性
    在面向对象的系统中,多态性是一个非常重要的概念。C#支持两种类型的多态性,第一种是编译时的多态性,一个类的对象调用若干同名方法,系统在编译时,根据调用方法的实参类型及实参的个数决定调用那个同名方法,实现何种操作。编译时的多态性是通过方法重载来实现的。C#语言的方法重载以及操作符重载和C++语言的基本一致。
    第二种是运行时的多态性,是在系统运行时,不同对象调用一个名字相同,参数的类型及个数完全一样的方法,会完成不同的操作。C#运行时的多态性通过虚方法实现。在类的方法声明前加上了virtual修饰符,被称之为虚方法,反之为非虚方法。C#语言的虚方法和C++语言的基本一致。下面的例子说明了虚方法与非虚方法的区别:
    using System;
    class A
    { public void F()//非虚方法
    { Console.Write(" A.F");}
    public virtual void G()//虚方法
    { Console.Write(" A.G");}
    }
    class B:A//A类为B类的基类
    { new public void F()//覆盖基类的同名非虚方法F(),注意使用new
    { Console.Write(" B.F");}
    public override void G()//覆盖基类的同名虚方法G(),注意使用override
    { Console.Write(" B.G");}
    }
    class Test
    { static void F2(A aA)//注意,参数为A类引用变量
    { aA.G();}
    static void Main()
    { B b=new B();
    A a1=new A();
    A a2=b;//允许基类引用变量引用派生类对象,a2引用派生类B对象b
    a1.F();//调用基类A的非虚方法F(),显示A.F
    a2.F();//F()为非虚方法,调用基类A的F(),显示A.F
    b.F();//F()为非虚方法,调用派生类B的F(),显示B.F
    a1.G();//G()为虚方法,因a1引用基类A对象,调用基类A的G(),显示A.G
    a2.G();//G()为虚方法,因a2引用派生类B对象,调用派生类B的G(),显示B.G
    F2(b);//实参为派生类B对象,由于A aA=b,调用派生类B的函数G(),显示B.G
    F2(a1);//实参为基类A对象,调用A类的函数G(),显示A.G
    }
    }
    那么输出应该是:
    A.F A.F B.F A.G B.G B.G A.G
    注意例子中,不同对象调用同名非虚方法F()和同名虚方法G()的区别。a2虽然是基类引用变量,但它引用派生类对象b。由于G()是虚方法,因此a2.G()调用派生类B的G(),显示G.F。但由于F()是非虚方法,a2.F()仍然调用基类A的F(),显示A.F。或者说,如果将基类引用变量引用不同对象,或者是基类对象,或者是派生类对象,用这个基类引用变量分别调用同名虚方法,根据对象不同,会完成不同的操作。而非虚方法则不具备此功能。
    方法F2(A aA)中,参数是A类类型,F2(b)中形参和实参的关系是:A aA=b,即基类引用变量aA引用派生类对象b,aA.G()调用派生类B的函数G(),显示B.G。同理,F2(a1)实参为基类A对象,调用A类的函数G(),显示A.G。
    在类的基本概念一节中,定义一个描述个人情况的类Person,其中公有方法Display()用来显示个人信息。在派生雇员类Employee中,覆盖了基类的公有方法Display(),以显示雇员新增加的信息。我们希望隐藏这些细节,希望无论基类还是派生类,都调用同一个显示方法,根据对象不同,自动显示不同的信息。可以用虚方法来实现,这是一个典型的多态性例子。例子
    using System;
    public class Person
    { private String name="张三";//类的数据成员声明
    private int age=12;
    protected virtual void Display()//类的虚方法
    { Console.WriteLine("姓名:{0},年龄:{1}",name,age);
    }
    public Person(string Name,int Age)//构造函数,函数名和类同名,无返回值
    { name=Name;
    age=Age;
    }
    static public void DisplayData(Person aPerson)//静态方法
    { aPerson.Display();//不是静态方法调用实例方法,如写为Display()错误
    }
    }
    public class Employee:Person//Person类是基类
    { private string department;
    private decimal salary;
    public Employee(string Name,int Age,string D,decimal S):base(Name,Age)
    { department=D;
    salary=S;
    }
    protected override void Display()//重载虚方法,注意用override
    { base.Display();//访问基类同名方法
    Console.WriteLine("部门:{0} 薪金:{1} ", department,salary);
    }
    }
    class Class1
    { static void Main(string[] args)
    { Person OnePerson=new Person("李四",30);
    Person.DisplayData(OnePerson);//显示基类数据
    Employee OneEmployee=new Employee("王五",40,"财务部",2000);
    Person.DisplayData(OneEmployee); //显示派生类数据
    }
    }
    运行后,显示的效果是:
    姓名: 李四,年龄:30
    姓名: 王五,年龄:40
    部门:财务部 薪金:2000
    1.12 抽象类和抽象方法
    抽象类表示一种抽象的概念,只是希望以它为基类的派生类有共同的函数成员和数据成员。抽象类使用abstract修饰符,对抽象类的使用有以下几点规定:
     抽象类只能作为其它类的基类,它不能直接被实例化。
     抽象类允许包含抽象成员,虽然这不是必须的。抽象成员用abstract修饰符修饰。
     抽象类不能同时又是密封的。
     抽象类的基类也可以是抽象类。如果一个非抽象类的基类是抽象类,则该类必须通过覆盖来实现所有继承而来的抽象方法,包括其抽象基类中的抽象方法,如果该抽象基类从其它抽象类派生,还应包括其它抽象类中的所有抽象方法。
    请看下面的示例:
    abstract class Figure//抽象类定义
    { protected double x=0,y=0;
    public Figure(double a,double b)
    { x=a;
    y=b;
    }
    public abstract void Area();//抽象方法,无实现代码
    }
    class Square:Figure///类Square定义
    { public Square(double a,double b):base(a,b)
    {}
    public override void Area()//不能使用new,必须用override
    { Console.WriteLine("矩形面积是:{0}",x*y);}
    }
    class Circle:Figure///类Square定义
    { public Circle(double a):base(a,a)
    {}
    public override void Area()
    { Console.WriteLine("园面积是:{0}",3.14*x*y);}
    }
    class Class1
    { static void Main(string[] args)
    { Square s=new Square(20,30);
    Circle c=new Circle(10);
    s.Area();
    c.Area();
    }
    }
    程序输出结果为:
    矩形面积是:600
    园面积是:314
    抽象类Figure提供了一个抽象方法Area(),并没有实现它,类Square和Circle从抽象类Figure中继承方法Area(),分别具体实现计算矩形和园的面积。
    在类的基本概念一节中,定义一个描述个人情况的类Person,它只是描述了一个人最一般的属性和行为,因此不希望生成它的对象,可以定义它为抽象类。
    注意:C++程序员在这里最容易犯错误。C++中没有对抽象类进行直接声明的方法,而认为只要在类中定义了纯虚函数,这个类就是一个抽象类。纯虚函数的概念比较晦涩,直观上不容易为人们接受和掌握,因此C#抛弃了这一概念。
    1.13 密封类和密封方法
    有时候,我们并不希望自己编写的类被继承。或者有的类已经没有再被继承的必要。C#提出了一个密封类(sealed class)的概念,帮助开发人员来解决这一问题。
    密封类在声明中使用sealed修饰符,这样就可以防止该类被其它类继承。如果试图将一个密封类作为其它类的基类,C#编译器将提示出错。理所当然,密封类不能同时又是抽象类,因为抽象总是希望被继承的。
    C#还提出了密封方法(sealed method)的概念。方法使用sealed修饰符,称该方法是一个密封方法。在派生类中,不能覆盖基类中的密封方法。
    1.14 接口
    与类一样,在接口中可以定义一个和多个方法、属性、索引指示器和事件。但与类不同的是,接口中仅仅是它们的声明,并不提供实现。因此接口是函数成员声明的集合。如果类或结构从一个接口派生,则这个类或结构负责实现该接口中所声明的所有成员。一个接口可以从多个接口继承,而一个类或结构可以实现多个接口。由于C#语言不支持多继承,因此,如果某个类需要继承多个类的行为时,只能使用多个接口加以说明。
    1.14.1 接口声明
    接口声明是一种类型声明,它定义了一种新的接口类型。接口声明格式如下:
    属性 接口修饰符 interface 接口名:基接口{接口体}
    其中,关键字interface、接口名和接口体时必须的,其它项是可选的。接口修饰符可以是new、public、protected、internal和private。例子:
    public interface IExample
    {//所有接口成员都不能包括实现
    string this[int index] {get;set;}//索引指示器声明
    event EventHandler E;//事件声明
    void F(int value);//方法声明
    string P { get; set;}//属性声明
    }
    声明接口时,需注意以下内容:
     接口成员只能是方法、属性、索引指示器和事件,不能是常量、域、操作符、构造函数或析构函数,不能包含任何静态成员。
     接口成员声明不能包含任何修饰符,接口成员默认访问方式是public。
    1.14.2 接口的继承
    类似于类的继承性,接口也有继承性。派生接口继承了基接口中的函数成员说明。接口允许多继承,一个派生接口可以没有基接口,也可以有多个基接口。在接口声明的冒号后列出被继承的接口名字,多个接口名之间用分号分割。例子如下:
    using System;
    interface IControl
    { void Paint();
    }
    interface ITextBox:IControl//继承了接口Icontrol的方法Paint()
    { void SetText(string text);
    }
    interface IListBox:IControl//继承了接口Icontrol的方法Paint()
    { void SetItems(string[] items);
    }
    interface IComboBox:ITextBox,IListBox
    {//可以声明新方法
    }
    上面的例子中,接口ITextBox和IListBox都从接口IControl中继承,也就继承了接口IControl的Paint方法。接口IComboBox从接口ITextBox和IListBox中继承,因此它应该继承了接口ITextBox的SetText方法和IListBox的SetItems方法,还有IControl的Paint方法。
    1.14.3 类对接口的实现
    前面已经说过,接口定义不包括函数成员的实现部分。继承该接口的类或结构应实现这些函数成员。这里主要讲述通过类来实现接口。类实现接口的本质是,用接口规定类应实现那些函数成员。用类来实现接口时,接口的名称必须包含在类声明中的基类列表中。
    在类的基本概念一节中,定义一个描述个人情况的类Person,从类Person可以派生出其它类,例如:工人类、公务员类、医生类等。这些类有一些共有的方法和属性,例如工资属性。一般希望所有派生类访问工资属性时用同样变量名。该属性定义在类Person中不合适,因为有些人无工资,如小孩。如定义一个类作为基类,包含工资属性,但C#不支持多继承。可行的办法是使用接口,在接口中声明工资属性。工人类、公务员类、医生类等都必须实现该接口,也就保证了它们访问工资属性时用同样变量名。例子如下:
    using System;
    public interface I_Salary//接口
    { decimal Salary//属性声明
    { get;
    set;
    }
    }
    public class Person
    {…//见1.9.2属性节Person类定义,这里不重复了。
    }
    public class Employee:Person,I_Salary//Person类是基类,I_Salary是接口
    {//不同程序员完成工人类、医生类等,定义工资变量名称可能不同
    private decimal salary;
    public new void Display()
    { base.Display();
    Console.WriteLine("薪金:{0} ",salary);
    }
    //工人类、医生类等都要实现属性Salary,保证使用的工资属性同名
    public decimal Salary
    { get
    { return salary;}
    set
    { salary=value;}
    }
    }
    public class Test
    { public static void Main()
    { Employee S=new Employee();
    S.Name="田七";//修改属性Name
    S.Age=20;//修改属性Age
    S.Salary=2000;//修改属性Salary
    S.Display();
    }
    }
    如果类实现了某个接口,类也隐式地继承了该接口的所有基接口,不管这些基接口有没有在类声明的基类表中列出。因此,如果类从一个接口派生,则这个类负责实现该接口及该接口的所有基接口中所声明的所有成员。
    1.15 代表
    在这里要介绍的是C#的一个引用类型----代表(delegate),也翻译为委托。它实际上相当于C语言的函数指针。与指针不同的是C#中的代表是类型安全的。代表类声明格式如下:
    属性集 修饰符 delegate 函数返回类型 定义的代表标识符(函数形参列表);
    修饰符包括new、public、protected、internal和private。例如我们可以声明一个返回类型为int,无参数的函数的代表MyDelegate:
    public delegate int MyDelegate();//只能代表返回类型为int,无参数的函数
    声明了代表类MyDelegate,可以创建代表类MyDelegate的对象,用这个对象去代表一个静态方法或非静态的方法,所代表的方法必须为int类型,无参数。看下面的例子:
    using System;
    delegate int MyDelegate();//声明一个代表,注意声明的位置
    public class MyClass
    { public int InstanceMethod()//非静态的方法,注意方法为int类型,无参数
    { Console.WriteLine("调用了非静态的方法。");
    return 0;
    }
    static public int StaticMethod()//静态方法,注意方法为int类型,无参数
    { Console.WriteLine("调用了静态的方法。");
    return 0;
    }
    }
    public class Test
    { static public void Main ()
    { MyClass p = new MyClass();
    //用new建立代表类MyDelegate对象,d中存储非静态的方法InstanceMethod的地址
    MyDelegate d=new MyDelegate(p.InstanceMethod);//参数是被代表的方法
    d();//调用非静态方法
    //用new建立代表类MyDelegate对象,d中存储静态的方法StaticMethod的地址
    d=new MyDelegate(MyClass.StaticMethod);//参数是被代表的方法
    d();//调用静态方法
    }
    }
    程序的输出结果是:
    调用了非静态的方法。
    调用了静态的方法。
    1.16 事件
    事件是C#语言内置的语法,可以定义和处理事件,为使用组件编程提供了良好的基础。
    1.16.1 事件驱动
    Windows操作系统把用户的动作都看作消息,C#中称作事件,例如用鼠标左键单击按钮,发出鼠标单击按钮事件。Windows操作系统负责统一管理所有的事件,把事件发送到各个运行程序。各个程序用事件函数响应事件,这种方法也叫事件驱动。
    C#语言使用组件编制Windows应用程序。组件本质上是类。在组件类中,预先定义了该组件能够响应的事件,以及对应的事件函数,该事件发生,将自动调用自己的事件函数。例如,按钮类中定义了单击事件Click和单击事件函数。一个组件中定义了多个事件,应用程序中不必也没必要响应所有的事件,而只需响应其中很少事件,程序员编制相应的事件处理函数,用来完成需要响应的事件所应完成的功能。现在的问题是,第一,如何把程序员编制的事件处理函数和组件类中预先定义的事件函数联系起来。第二,如何使不需响应的事件无动作。这是本节要节的解决问题。
    1.16.2 事件的声明
    在C#中,事件首先代表事件本身,例如按钮类的单击事件,同时,事件还是代表类引用变量,可以代表程序员编制的事件处理函数,把事件和事件处理函数联系在一起。下面的例子定义了一个Button组件,这个例子不完整,只是说明问题。实际在C#语言类库中已预定义了Button组件,这里的代码只是想说明Button组件中是如何定义事件的。例子如下:
    public delegate void EventHandler(object sender,EventArgs e);//代表声明
    //EventHandler可以代表没有返回值,参数为(object sender,EventArgs e)的函数
    public class Button:Control//定义一个按钮类Button组件
    {…//按钮类Button其它成员定义
    public event EventHandler Click;//声明一个事件Click,是代表类引用变量
    protected void OnClick(EventArgs e)//Click事件发生,自动触发OnClick方法
    { if(Click!=null)//如果Click已代表了事件处理函数,执行这个函数
    Click(this,e);
    }
    public void Reset()
    { Click=null;}
    }
    在这个例子中,Click事件发生,应有代码保证(未列出)自动触发OnClick方法。Click是类Button的一个事件,同时也是代表EventHandler类的引用变量,如令Click代表事件处理函数,该函数完成Click事件应完成的功能,Click事件发生时,执行事件处理函数。
    1.16.3 事件的预订和撤消
    在随后的例子中,我们声明了一个使用Button类的登录对话框类,对话框类含有两个按钮:OK和Cancel按钮。
    public class LoginDialog: Form//登录对话框类声明
    { Button OkButton;
    Button CancelButton;
    public LoginDialog()//构造函数
    { OkButton=new Button();//建立按钮对象OkButton
    //Click代表OkButtonClick方法,注意+=的使用
    OkButton.Click+=new EventHandler(OkButtonClick);
    CancelButton=new Button();//建立按钮对象OkButton
    CancelButton.Click += new EventHandler(CancelButtonClick);
    }
    void OkButtonClick(object sender, EventArgs e)
    {…//处理OkButton.Click事件的方法
    }
    void CancelButtonClick(object sender, EventArgs e)
    {…//处理CancelButton.Click事件的方法
    }
    }
    在例子中建立了Button类的两个实例,单击按钮事件Click通过如下语句和事件处理方法联系在一起:OkButton.Click+=new EventHandler(OkButtonClick),该语句的意义是使OkButton.Click代表事件处理方法OkButtonClick,这样只要Click事件被触发,事件处理方法OkButtonClick就会被自动调用。撤消事件和事件处理方法OkButtonClick的联系采用如下语句实现:OkButton.Click-=new EventHandler(OkButtonClick),这时,OkButton.Click就不再代表事件处理方法,Click事件被触发,方法OkButtonClick就不会被调用了。务必理解这两条语句的用法。使用Visual Studio.Net集成环境可以自动建立这种联系,在自动生成的代码中包括这两条语句。
    1.17 索引指示器
    在C#语言中,数组也是类,比如我们声明一个整型数数组:int[] arr=new int[5],实际上生成了一个数组类对象,arr是这个对象的引用(地址),访问这个数组元素的方法是:arr[下标],在数组类中,使用索引访问元素是如何实现的呢?是否可以定义自己的类,用索引访问类中的数据成员?索引指示器(indexer)为我们提供了通过索引方式方便地访问类的数据成员的方法。
    首先看下面的例子,用于打印出小组人员的名单:
    using System
    class Team
    { string[] s_name = new string[2];//定义字符串数组,记录小组人员姓名
    public string this[int nIndex]//索引指示器声明,this为类Team类的对象
    { get//用对象名[索引]得到记录小组人员姓名时,调用get函数
    { return s_name[nIndex];
    }
    set//用对象名[索引]修改记录小组人员姓名时,调用set函数
    { s_name[nIndex] =value;//value为被修改值
    }
    }
    }
    class Test
    { public static void Main()
    { Team t1 = new Team();
    t1[0]="张三";
    t1[1]="李斯";
    Console.WriteLine("{0},{1}",t1[0], t1[1]);
    }
    }
    显示结果如下:张三,李斯
    1.18 名字空间
    一个应用程序可能包含许多不同的部分,除了自己编制的程序之外,还要使用操作系统或开发环境提供的函数库、类库或组件库,软件开发商处购买的函数库、类库或组件库,开发团队中其它人编制的程序,等等。为了组织这些程序代码,使应用程序可以方便地使用这些程序代码,C#语言提出了名字空间的概念。名字空间是函数、类或组件的容器,把它们按类别放入不同的名字空间中,名字空间提供了一个逻辑上的层次结构体系,使应用程序能方便的找到所需代码。这和C语言中的include语句的功能有些相似,但实现方法完全不同。
    1.18.1 名字空间的声明
    用关键字namespace声明一个名字空间,名字空间的声明要么是源文件using语句后的第一条语句,要么作为成员出现在其它名字空间的声明之中,也就是说,在一个名字空间内部还可以定义名字空间成员。全局名字空间应是源文件using语句后的第一条语句。在同一名字空间中,不允许出现同名名字空间成员或同名的类。在声明时不允许使用任何访问修饰符,名字空间隐式地使用public修饰符。例子如下:
    using System;
    namespace N1//N1为全局名字空间的名称,应是using语句后的第一条语句
    { namespace N2//名字空间N1的成员N2
    { class A//在N2名字空间定义的类不应重名
    { void f1(){};}
    class B
    { void f2(){};}
    }
    }
    也可以采用非嵌套的语法来实现以上名字空间:
    namespace N1.N2//类A、B在名字空间N1.N2中
    { class A
    { void f1(){};}
    class B
    { void f2(){};}
    }
    也可以采用如下格式:
    namespace N1.N2//类A在名字空间N1.N2中
    { class A
    { void f1(){};}
    }
    namespace N1.N2//类B在名字空间N1.N2中
    { class B
    { void f2(){};}
    }
    1.18.2 名字空间使用
    如在程序中,需引用其它名字空间的类或函数等,可以使用语句using,例如需使用上节定义的方法f1()和f2(),可以采用如下代码:
    using N1.N2;
    class WelcomeApp
    { A a=new A();
    a.f1();
    }
    using N1.N2实际上是告诉应用程序到哪里可以找到类A。请读者重新看一下1.2.1节中的例子。
    1.19 非安全代码
    在C和C++的程序员看来,指针是最强有力的工具之一,同时又带来许多问题。因为指针指向的数据类型可能并不相同,比如你可以把int类型的指针指向一个float类型的变量,而这时程序并不会出错。如果你删除了一个不应该被删除的指针,比如Windows中指向主程序的指针,程序就有可能崩溃。因此滥用指针给程序带来不安全因素。正因为如此,在C#语言中取消了指针这个概念。虽然不使用指针可以完成绝大部分任务,但有时在程序中还不可避免的使用指针,例如调用Windows操作系统的API函数,其参数可能是指针,所以在C#中还允许使用指针,但必须声明这段程序是非安全(unsafe)的。可以指定一个方法是非安全的,例如:unsafe void F1(int * p){…}。可以指定一条语句是非安全的,例如:unsafe int* p2=p1;还可以指定一段代码是非安全的,例如:unsafe{ int* p2=p1;int* p3=p4;}。在编译时要采用如下格式:csc 要编译的C#源程序 /unsafe。
    习题
    1. 从键盘输入姓名,在显示器中显示对输入姓名的问候。(提示:string为字符串类型,用语句string s=Console.ReadLine()输入姓名)
    2. 构造函数和析购函数的主要作用是什么?它们各有什么特性?
    3. 定义点类,数据成员为私有成员,增加有参数和无参数构造函数,在主函数中生成点类对象,并用字符显示点类对象的坐标。
    4. 定义矩形类,数据成员为私有成员,增加有参数和无参数构造函数,在主函数中生成矩形类对象,并用字符显示矩形类对象的长、宽和矩形左上角的坐标。
    5. 设计一个计数器类,统计键入回车的次数,数据成员为私有成员,在主程序中使用此类统计键入回车的次数。
    6. 说明值类型和引用类型的区别,并和C语言相应类型比较。
    7. 定义点结构,在主函数中生成点结构变量,从键盘输入点的位置,并重新显示坐标。
    8. 定义整型一维数组,从键盘输入数组元素数值后,用循环语句显示所有元素的值。
    9. 输入字符串,将字符串第一个字母和每个空格后的字母变为大写,其余字母为小写后输出。
    10. 输入5个数,在每两个数之间增加3个空格后输出。
    11. 编一个猜数程序,程序设定一个1位十进制数,允许用户猜3次,错了告诉比设定数大还是小,用switch语句实现。
    12. C#语言for语句可以这样使用:for(int i;i<10;i++),请问,i的有效使用范围。
    13. 用字符*在CRT上显示一个矩形。
    14. 输入一个字符串,用foreach语句计算输入的字符串长度,并显示长度。
    15. 输入两个数相加,并显示和。用异常语句处理输入错误。
    16. 将1.6.3节中try–catch-finally语句例子改为try-finally和try–catch语句。
    17. 定义点类,从点类派生矩形类,数据成员为私有成员,增加有参数和无参数构造函数,在主函数中生成矩形类对象,并用字符显示矩形类对象的长、宽和矩形左上角的坐标。
    18. 重做12题,将数据成员用属性表示。
    19. 定义一个类,将类外部的char数组元素都变为大写。主程序输入一个字符串,将其变为char数组,变为大写后输出每一个char数组元素。分别用类对象和静态函数实现。
    20. 定义分数类,实现用符号+,-,*,/完成分数的加减乘除。在主函数中输入两个数,完成运算后输出运算结果。
    21. 建立一个sroot()函数,返回其参数的二次根。重载它,让它能够分别返回整数、长整数和双精度参数的二次根。
    22. 重新设计complex类,完成复数的+、-、*、/四则运算。
    23. 定义点类,从点类派生矩形类和园类,主程序实现用同一个方法显示矩形和园的面积。
    24. 重做19题,将点类定义为抽象类。
    25. 重做19题,改为接口实现,即将点类改为接口。


    第二章 Windows编程的基础知识
    2.1 窗口
    Windows应用程序一般都有一个窗口,窗口是运行程序与外界交换信息的界面。一个典型的窗口包括标题栏,最小化按钮,最大/还原按钮,关闭按钮,系统菜单图标,菜单,工具条,状态栏,滚动条,客户区等。程序员的工作之一是设计符合自己要求的窗口,C#用控件的方法设计界面。编程另一个工作是在用户区显示数据和图形。
    2.2 Windows的消息系统
    2.2.1 消息驱动(事件驱动)
    Windows应用程序和dos程序(控制台程序)的最大区别是事件驱动,也叫消息驱动。dos程序运行时如要读键盘,则要独占键盘等待用户输入,如用户不输入,则CPU一直执行键盘输入程序,等待用户输入,即dos程序独占外设和CPU。
    Windows操作系统是一个多任务的操作系统,允许同时运行多个程序,它不允许任何一个程序独占外设,如键盘,鼠标等,所有运行程序共享外设和CPU,各个运行程序都要随时从外设接受命令,执行命令。
    因此必须由Windows操作系统统一管理各种外设。Windows把用户对外设的动作都看作事件(消息),如单击鼠标左键,发送单击鼠标左键事件,用户按下键盘,发送键盘被按下的事件等。Windows操作系统统一负责管理所有的事件,把事件发送到各个运行程序,而各个运行程序用一个函数响应事件,这个函数叫事件响应函数。这种方法叫事件驱动。每个事件都有它自己的事件响应函数,当接到Windows事件后,自动执行此事件的事件响应函数。程序员编程的主要工作就是编制这些事件的处理函数,完成相应的工作。
    2.2.2 事件队列
    Windows把用户的动作都看作事件,Windows操作系统负责管理所有的事件,事件发生后,这些事件被放到系统事件队列中,Windows操作系统从系统事件队列中逐一取出事件,分析各个事件,分送事件到相应运行程序的事件队列中。而每个运行程序,则利用消息循环方法(既循环取得自己事件队列中的事件)得到事件,并把他们送到当前活动窗口,由窗口中的事件函数响应各个事件(消息)。因此,每个运行程序都有自己的事件队列。
    2.2.3 注视窗口
    Windows操作系统允许多个程序同时运行,每个程序可能拥有多个窗口,但其中只有一个窗口是活动的,我们能从窗口的标题栏的颜色来识别一个活动窗口,这个窗口接收Windows系统发来的大部分的事件。这个应用程序的窗口被称为注视(活动)窗口。
    2.3 Windows编程接口和类库
    操作系统为了方便应用程序设计,一般都要提供一个程序库,一些设计应用程序的共用代码都包含在这个程序库中。程序员可以调用这些代码,以简化编程。这节介绍一些常用程序库。
    2.3.1 Windows编程接口(API)
    API(Application Programming Interface)是Windows98、2000和XP操作系统中提供的一组函数,这些函数采用C语言调用格式,是为程序员编制Windows应用程序提供的编程接口。程序员用C语言直接调用API也可以编制Windows应用程序,但大量的程序代码必须由程序员自己编写,而API函数非常庞大,给编程者带来很大的困难。
    2.3.2 MFC类库
    由于API函数十分庞大复杂,看不到函数之间的关系,使程序员不易使用。用C语言使用API函数编写Windows应用程序是十分困难的。微软的VC++6.0用类对API函数进行了封装,为编程提供了MFC类库。使用MFC类库简化了Windows应用程序的编制。但是,MFC类库的使用还是比较复杂的,因此,VC++一直是一些专业人员的编程工具。
    2.3.3 组件库
    为了简化Windows应用程序的设计,提出了组件(控件)的概念,组件也是类,按钮、菜单、工具条等都可以封装为组件,组件采用属性、事件、方法来描述,其中属性描述组件的特性,如按钮的标题,标签字体的颜色和大小。方法是组件类提供的函数,通过调用这些方法,可以控制组件的行为。组件通过事件和外界联系,一个组件可以响应若干个事件,可以为事件增加事件处理函数,以后每当发生该事件,将自动调用该事件处理函数处理此事件。很多组件在设计阶段是可见的,支持可视化编程,这些组件又被叫做控件。用控件编制Windows应用程序很象搭积木,将控件放到窗体中,设置好属性,漂亮的界面就设计好了。组件编程的工具有很多,例如:VB6.0、VB.Net、C#、C++Builder、Java、Delphi等快速开发工具(RAD)。这些工具都有自己的组件库。
    2.3.4 .NET框架类库
    .NET系统为编制Windows应用程序、Web应用程序、Web服务,在.Net框架(.Net FrameWork)中提供了基础类库(Base Class Library)。它是一个统一的、面向对象的、层次化的、可扩展的类库,统一了微软当前各种不同的框架和开发模式,无论开发Windows应用程序,还是开发Web应用程序,采用相同的组件名称,组件具有相同的属性、方法和事件,开发模式也类似,方便程序员学习。.Net框架类库支持控件可视化编程,.Net中的VC++.Net、VB.Net、C#语言都使用这个类库,消除了各种语言开发模式的差别。该类库包括以下功能:基础类库(基本功能,象字符串、数组等)、网络、安全、远程化、诊断和调试、I/O、数据库、XML、Web服务、Web编程、Windows编程接口等等。
    Windows98、2000和XP操作系统并不包含.NET框架类库,为了运行C#程序,必须安装.Net FrameWork。
    2.4 Windows应用程序的基本结构
    Windows应用程序和控制台应用程序的基本结构基本一样,程序的执行总是从Main()方法开始,主函数Main()必须在一个类中。但Windows应用程序使用图形界面,一般有一个窗口(Form),采用事件驱动方式工作。本节介绍Windows应用程序的基本结构。
    2.4.1 最简单的Windows应用程序
    最简单的Windows应用程序如下:
    using System;//引入名字空间
    using System.Windows.Forms;
    public class Form1:Form//类定义
    { static void Main()//主函数
    { Application.Run(new Form1());
    }
    }
    自定义类Form1以Form类为基类。Form类是.Net系统中定义的窗体类,Form类对象具有Windows应用程序窗口的最基本功能,有标题栏、系统菜单、最大化按钮、最小化按钮和关闭按钮、用户区。Form类对象还是一个容器,在Form窗体中可以放置其它控件,例如菜单控件,工具条控件等等。System.Application类中的静态方法Run负责完成一个应用程序的初始化,运行,终止等功能,其参数是本程序使用的窗体Form1类对象,Run方法还负责从操作系统接受事件,并把事件送到窗体中响应。窗体关闭,方法Run退出,Windows应用程序结束。假设已经将文件保存在d:\Charp目录下,文件名为:e1.cs。启动命令行提示符,在屏幕上输入一行命令:d:回车,cd Charp回车,键入命令:
    C:\WINNT\Microsoft.NET\Framework\v1.0.3705\csc /t:winexe /r:system.dll,System.Windows.Forms.dll e1.cs
    命令中的/t:winexe表示要建立一个Windows应用程序,/r表示要引入的命名空间。也可以用记事本建立一个批处理文件g.bat,将以上命令内容拷贝到文件中,运行g.bat,和在命令行提示符键入命令效果相同。以上方法在FrameWork SDK 2000中实现。如果一切正常e1.cs文件将被编译,编译后生成可执行文件e1.exe。运行可执行文件e1.exe,CRT上出现一个窗口如右图。
    可以在Form1类中定义新的变量,由于主窗体关闭,程序也就结束了,因此定义在主窗体Form1中的变量的生命周期和程序的生命周期是相同的,从这个意义上说,这些变量是全局变量。可以为Form1类定义构造函数,在构造函数中做一些初始化的工作,例如修改Form1标题栏中的标题。还可以在Form1中定义控件类的对象,这些控件将在Form1的用户区显示出来,换句话讲,在Form1中生成控件对象,也就是把控件放到窗体中。如在窗体中增加了一个按钮(Button)控件,单击按钮,将产生单击按钮事件,完成一定功能,下例说明了如何在窗体中增加控件,如何修改控件属性,如何增加控键的事件处理函数。
    using System;
    using System.Windows.Forms;
    public class Form1:Form
    { Button button1;//生成Button类引用变量,和应用程序有相同生命周期
    public Form1()//构造函数
    {//下句修改主窗体标题,不指明属性(方法)所属对象,默认为Form1类的属性(方法)
    Text="我的第一个程序";//也可写为:this.Text="我的第一个程序";
    button1=new Button();//生成Button类对象
    button1.Location=new Point(25,25);//修改button1属性location即按钮位置
    button1.Text="确定";//修改button1属性Text,即按钮的标题
    //下句指定button1_Click函数是按钮单击事件的单击事件处理函数
    button1.Click+=new System.EventHandler(button1_Click);
    this.Controls.Add(button1);//按钮增加到窗体中,将在主窗体用户区显示出来
    }
    static void Main()
    { Application.Run(new Form1());
    }
    private void button1_Click(object sender, System.EventArgs e)
    {//事件处理函数
    this.button1.Text="单击了我";//单击按钮事件执行的语句
    }
    }
    请注意在窗体中增加控件类的对象的步骤,首先生成一个引用变量button1,和主窗体Form1有相同的生命周期,第二步在构造函数中用new生成Button类对象,第三步在构造函数中修改button1的属性,增加button1的事件函数。这些步骤对于定义任何一个控件都是相同的。编译运行结果如右图:

    2.4.2 用Visual Studio.Net建立Windows应用程序框架
    以上所做的工作,都是一些固定的工作,可以使用Visual Studio.Net自动建立,下面介绍使用Visual Studio.Net创建Windows应用程序的具体步骤。
    (1) 运行Visual Studio.Net程序,出现如图1.2.2A界面。
    (2) 单击新建项目按钮,出现如图1.2.2B对话框。在项目类型(P)编辑框中选择Visual C#项目,在模板(T)编辑框中选Windows应用程序,在名称(N)编辑框中键入e2,在位置(L)编辑框中键入D:\csarp。也可以单击浏览按钮,在打开文件对话框中选择文件夹。单击确定按钮,创建项目。出现如图2.4.2A界面。生成一个空白窗体(Form1)。

    图2.4.2A
    (3) 在e2文件夹中下有两个文件夹和8个文件,一般只修改Form1.cs文件。右击Form1窗体,在快捷菜单中选择菜单项查看代码(C),可打开Form1.cs文件。Visual Studio.Net生成的Foem1.cs文件如下,这是使用Visual Studio.Net创建Windows应用程序的最基本的形式。底色为黑色的字是作者增加的注解。
    using System;//引入名字空间
    using System.Drawing;
    using System.Collections;
    using System.ComponentModel;
    using System.Windows.Forms;
    using System.Data;
    namespace e2//定义名字空间,///为解释
    { //此处可定义其它类
    /// <summary>
    /// Form1 的摘要说明。
    /// </summary>
    public class Form1 : System.Windows.Forms.Form//Forme1类定义
    { //此处可定义自己的变量,这些变量和运行程序同生命周期
    /// <summary>
    /// 必需的设计器变量。
    /// </summary>
    private System.ComponentModel.Container components = null;
    public Form1()//构造函数
    {
    //
    // Windows 窗体设计器支持所必需的
    //
    InitializeComponent();//此函数系统自动生成,不要修改,该函数做一些初始化工作
    //
    // TODO: 在 InitializeComponent 调用后添加任何构造函数代码
    //在构造函数增加自己的初始化代码,必须放在InitializeComponent()之后
    }
    /// <summary>
    /// 清理所有正在使用的资源。
    /// </summary>
    protected override void Dispose( bool disposing )
    {
    if( disposing )
    {
    if (components != null)
    {
    components.Dispose();
    }
    }
    base.Dispose( disposing );
    }
    #region Windows Form Designer generated code
    /// <summary>
    /// 设计器支持所需的方法 - 不要使用代码编辑器修改
    /// 此方法的内容。
    /// </summary>
    private void InitializeComponent()
    { //此函数系统自动生成,不要修改函数内容,函数做一些初始化工作
    //
    // Form1
    //
    this.AutoScaleBaseSize = new System.Drawing.Size(6, 14);
    this.ClientSize = new System.Drawing.Size(292, 273);
    this.Name = "Form1";//this 是Form1窗体对象
    this.Text = "Form1";
    }
    #endregion
    /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main()//程序入口函数 ,一般不修改
    {
    Application.Run(new Form1());//主程序建立窗体运行
    }//程序入口函数之后可定义自己的方法、属性等
    }
    }
    (4) 下边在窗体中增加一个按钮,并为按钮增加单击事件函数。单击图2.4.2A中标题为Forms.cs[设计]的窗口标签,返回标题为Forms.cs[设计]的窗口。向项目中添加控件需要使用工具箱窗口,若看不到,可以用菜单命令视图/工具箱打开这个窗口(见图2.4.2B左图)。选中工具箱窗口中Windows窗体类型下的Button条目,然后在标题为Forms.cs[设计]的窗口的Form1窗体中按下鼠标左键,拖动鼠标画出放置Button控件的位置,抬起鼠标左键,就将Button控件放到Form1窗体中。选中按钮控件,属性窗口(见图2.4.2B中图)显示按钮属性,其中左侧为属性名称,右侧为属性值,用属性窗口修改Button的Text属性值为:确定。单击属性窗体上的第4个图标,打开事件窗口(见图2.4.2B右图),显示Button控件所能响应的所有事件,其中左侧为事件名称,右侧为事件处理函数名称,如果为空白,表示还没有事件处理函数,选中Click事件,双击右侧空白处,增加单击事件处理函数。
    图2.4.2B
    完成以上设计后,集成环境生成的Foem1.cs文件如下,底色为黑色的代码是新增代码。
    using System;
    using System.Drawing;
    using System.Collections;
    using System.ComponentModel;
    using System.Windows.Forms;
    using System.Data;
    namespace e2
    {
    /// <summary>
    /// Form1 的摘要说明。
    /// </summary>
    public class Form1 : System.Windows.Forms.Form
    {
    private System.Windows.Forms.Button button1;//定义Button类引用变量
    /// <summary>
    /// 必需的设计器变量。
    /// </summary>
    private System.ComponentModel.Container components = null;
    public Form1()
    {
    //
    // Windows 窗体设计器支持所必需的
    //
    InitializeComponent();
    //
    // TODO: 在 InitializeComponent 调用后添加任何构造函数代码
    //
    }
    /// <summary>
    /// 清理所有正在使用的资源。
    /// </summary>
    protected override void Dispose( bool disposing )
    {
    if( disposing )
    {
    if (components != null)
    {
    components.Dispose();
    }
    }
    base.Dispose( disposing );
    }
    #region Windows Form Designer generated code
    /// <summary>
    /// 设计器支持所需的方法 - 不要使用代码编辑器修改
    /// 此方法的内容。
    /// </summary>
    private void InitializeComponent()
    {
    this.button1 = new System.Windows.Forms.Button();//生成对象
    this.SuspendLayout();
    //
    // button1
    //
    this.button1.Location = new System.Drawing.Point(96, 56);//修改属性
    this.button1.Name = "button1";
    this.button1.Size = new System.Drawing.Size(72, 32);
    this.button1.TabIndex = 0;
    this.button1.Text = "确定";
    this.button1.Click += new System.EventHandler(this.button1_Click);//增加事件
    //
    // Form1
    //
    this.AutoScaleBaseSize = new System.Drawing.Size(6, 14);
    this.ClientSize = new System.Drawing.Size(292, 273);
    this.Controls.AddRange(new System.Windows.Forms.Control[] {this.button1});
    this.Name = "Form1";
    this.Text = "Form1";
    }
    #endregion
    /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main()
    {
    Application.Run(new Form1());
    }
    private void button1_Click(object sender, System.EventArgs e)
    {//事件处理函数
    }
    }
    }
    请注意按钮放到窗体后,集成环境自动增加的语句。分析这些增加的语句,可知在窗体中增加Button类对象的步骤:首先定义Button类变量button1,这是Form1类的一个字段,由于主窗体关闭,程序也就结束了,因此定义在主窗体Form1中的变量的生命周期和程序的生命周期是相同的,从这个意义上说,这样的变量是全局变量。因此变量button1和主窗体Form1有相同的生命周期。第二步在构造函数中用new生成Button类对象,第三步在构造函数中修改button1的属性,第四步增加button1的事件函数,函数button1_Click()是事件处理函数,语句this.button1.Click += new System.EventHandler(this.button1_Click)把按钮Button1的事件Click和事件处理函数button1_Click()联系到一起。程序员应在事件处理函数button1_Click()中增加具体的事件处理语句。这些步骤对于增加任何控件都是相同的。可以比较一下2.4.1节中的步骤,它们基本是相同的。应熟悉以上操作步骤,学会在窗体中增加控件,修改控件属性,增加事件函数。
    2.4.3 方案(Solution)和项目(Project)
    一个应用(Application)可能包括一个或多个可执行程序,例如,学生信息管理系统,可能包括客户端程序和服务器端程序,所有这些可执行程序的集合叫做一个应用解决方案。为了生成一个可执行程序,可能需要有一个或多个文件,例如,一般需要一个窗体文件,有时还需要一个资源文件,若干图形或图像文件。所有这些文件的集合叫一个项目,因此项目是为了创建一个可执行程序所必需的所有的文件的集合。而一个方案中可能包括多个项目。为了方便管理项目和项目所在的方案,Visual Studio.Net为开发人员提供了解决方案资源管理器窗口(图2.4.3)。它可以为我们显示一个方案的树形结构,以及它所包含的项目及项目中的文件。
    一个项目一般要放在一个文件夹中,例如上边的例子,项目e2的所有文件都在文件夹e2中,共有两个文件夹和8个文件,它们的用途如下:
     bin文件夹:包含debug子文件夹,存储生成带调试信息的可执行C#程序。
     obj文件夹:包含编译过程中生成的中间代码。
     AssemblyInfo.cs:创建项目自动添加。包含各种属性设置,例如,项目最终创建的可执行文件或DLL文件中的信息,如标题、描述、公司名等。一般用工具修改该程序,不要直接修改。
     Form1.cs:窗体文件,程序员一般只修改该文件。
     Form1.resx:资源文件。程序员用集成环境提供的工具修改,不要直接修改。
     e2.suo:解决方案用户选项文件,记录用户关于解决方案的选项。
     e2.csproj:项目文件,记录用户关于项目的选项。
     e2.sln:解决方案文件。
    为了以后重新用Visual Studio.Net打开该解决方案,必须保存除了两个文件夹以外的所有文件,实际上,由于文件夹e2不太大,可以保存整个e2文件夹。如果重新开始一个解决方案,首先用菜单项文件/关闭解决方案,关闭当前项目,再新建一个项目。为了用Visual Studio.Net修改以前的程序,必须打开保存的项目文件(扩展名为csproj),或者使用菜单项文件/打开项目,打开保存的项目,同时打开项目所在的解决方案。


    图2.4.3
    习题
    (1) Windows应用程序和dos程序有那些不同。
    (2) 以键盘操作为例说明什么是事件驱动。
    (3) 那些Windows操作系统提供了.NET框架类库,那些提供了API。
    (4) 运行C#程序,应首先安装那些软件。
    (5) 定义一个和应用程序同生命周期的变量,该变量应定义在何处,说明该变量的使用范围。
    (6) 在窗体中增加一个控件,应如何操作,集成环境增加了那些代码。
    (7) 为控件增加事件函数,应如何操作,集成环境增加了那些代码。
    (8) 如何为窗体文件增加一个方法,说明该方法的使用范围。


    第三章 常用控件和类的使用
    Visual Studio.Net(简称VS.NET)使用控件(组件)设计Windows应用程序。将VS.NET工具箱窗口中的控件放到窗体中,使用属性窗口改变控件的属性,或在程序中用语句修改属性,为控件增加事件函数,完成指定的功能。
    3.1 控件通用属性
    大部分控件,例如Label、Button、TextBox等,都是Control类的派生类。Control类中定义了这些派生类控件通用的一组属性和方法,这些属性是:
     Name:控件的名称,区别控件类不同对象的唯一标志,例如建立一个Button控件类对象,可用如下语句,Button button1=new Button(),那么Name属性的值为button1。
     Location:表示控件对象在窗体中的位置。本属性是一个结构,结构中有两个变量,x和y,分别代表控件对象左上角顶点的x和y坐标,该坐标系以窗体左上角为原点,x轴向左为正方向,y轴向下为正方向,以像素为单位。修改Location,可以移动控件的位置,例如:button1.Location=new Point(100,200)语句移动按钮button1到新位置。
     Left和Top:属性值等效于控件的 Location 属性的 X 和Y。修改Left和Top,可以移动控件的位置,例如:button1.Left=100语句水平移动按钮button1。
     Size:本属性是一个结构,结构中有两个变量,Width和Height分别代表控件对象的宽和高,例如可用语句button1.Size.Width=100修改Button控件对象button1的宽。
     BackColor:控件背景颜色。
     Enabled:布尔变量,为true表示控件可以使用,为false表示不可用,控件变为灰色。
     Visible:布尔变量,为true控件正常显示,为false控件不可见。
     Modifier:定义控件的访问权限,可以是private、public、protected等。默认值为private。
     Cursor:鼠标移到控件上方时,鼠标显示的形状。默认值为Default,表示使用默认鼠标形状,即为箭头形状。
    3.2 Form类
    Form类是.Net系统中定义的窗体类(WinForm),它属于System.Windows.Forms名字空间。Form类对象具有Windows应用程序窗口的最基本功能。它可以是对话框、单文档或多文档应用程序窗口的基类。Form类对象还是一个容器,在Form窗体中可以放置其它控件,例如菜单控件,工具条控件等等,还可以放置子窗体。
    1. Form类常用属性
     AutoScroll:布尔变量,表示窗口是否在需要时自动添加滚动条。
     FormBorderStyle:窗体边界的风格,如有无边界、单线、3D、是否可调整等。
     Text:字符串类对象,窗体标题栏中显示的标题。
     AcceptButton:记录用户键入回车时,相当于单击窗体中的那个按钮对象。
     CanceButton:记录用户键入ESC键时,相当于单击窗体中的那个按钮对象。以上两个属性多用于对话框,例如打开文件对话框,用户键入回车,相当于单击确定按钮。
     MaxiMizeBox:窗体标题栏右侧最大化按钮是否可用,设置为false,按钮不可用。
     MiniMizeBox:窗体标题栏右侧最小化按钮是否可用,设置为false,按钮不可用。如果属性MaxiMizeBox和MiniMizeBox都设置为false,将只有关闭按钮。在不希望用户改变窗体大小时,例如对话框,将两者都设置为false。
    2. Form类常用方法
     Close():窗体关闭,释放所有资源。如窗体为主窗体,执行此方法,程序结束。
     Hide():隐藏窗体,但不破坏窗体,也不释放资源,可用方法Show()重新打开。
     Show():显示窗体。
    3. Form类常用事件
     Load:在窗体显示之前发生,可以在其事件处理函数中做一些初始化的工作。
    3.3 标签(Label)控件
    标签控件用来显示一行文本信息,但文本信息不能编辑,常用来输出标题、显示处理结果和标记窗体上的对象。标签一般不用于触发事件。
    1. Label控件常用属性
     Text:显示的字符串
     AutoSize:控件大小是否随字符串大小自动调整,默认值为false,不调整。
     ForeColor:Label显示的字符串颜色。
     Font:字符串所使用的字体,包括所使用的字体名,字体的大小,字体的风格等等,具体修改方法见下边的例子。
    2. 例子e3_3:我的第一个程序
    下面的例子在窗口中显示一行文本,该例虽然简单,但包括了用Visual Studio.Net建立C# Windows应用程序的基本步骤。具体实现步骤如下:
    (1) 建立一个新项目,生成一个空白窗体(Form1),见图2.4.2A。可以用属性窗口(图2.4.2B中图)修改窗体的属性,例如修改Form1的属性Text,可以修改窗体的标题。用鼠标拖动窗体的边界小正方形,可以修改窗体打开时的初始大小。
    (2) 双击工具箱窗口(图2.4.2B左图)中Windows窗体类型下的Label条目,在窗体Form1放置一个Label控件。该控件用来显示一行文本。可以用鼠标拖放Label到窗体的任意位置,并可拖动Label边界改变控件的大小。
    (3) 选中Label控件,在属性窗口中找到属性text,把它的值由“Label1”修改为“我的第一个程序”。接着在属性窗口中选中Font属性,单击Font属性右侧的标题为…的按钮,打开对话框,在对话框中可以修改Label控件显示字符串的字体名称和字号等,也可以单击Font属性左边的+号,在出现的子属性中编辑。编辑完成后,单击Font属性左边的-号,隐藏Font的子属性。修改ForeColor属性可以修改Label控件显示字符串的颜色。这是在设计阶段修改属性。
    (4) 编译,运行,可以看到窗口中按指定字体大小和颜色显示:我的第一个程序。运行效果如右图。
    (5) 保存项目。生成一个可执行程序需要多个文件,这些文件组成一个项目。一般把一个项目存到一个子目录中。单击文件/存所有文件菜单项,保存所有文件。
    (6) 关掉VS.NET,再启动。用文件/打开项目菜单项打开刚才关闭的项目文件(扩展名为sln)。应能看到刚才关闭的设计界面。必须打开项目,才能完成编译工作。
    3.4 按钮(Button)控件
    用户单击按钮,触发单击事件,在单击事件处理函数中完成相应的工作。
    1. Button 控件的常用属性和事件
     属性Text:按钮表面的标题
     事件Click:用户单击触发的事件,一般称作单击事件。
    2. 例子e3_4
    本例说明如何用程序修改属性,如何使用方法,增加事件函数。该例在窗口中显示一行文字,增加2个按纽,单击标题为红色的按纽把显示的文本颜色改为红色,单击标题为黑色的按纽把显示的文本颜色改为黑色。实现步骤如下:
    (1) 继续上例,放三个Button控件到窗体,修改属性Text,使标题分别为红色,黑色,退出。设计好的界面如右图。
    (2) 选中标题为红色的按纽,打开事件窗口(见图2.4.2B右图),显示该控件所能响应的所有事件,其中左侧为事件名称,右侧为事件处理函数名称,如果为空白,表示还没有事件处理函数,选中Click事件,双击右侧空白处,增加单击(Click)标题为红色的按钮的事件处理函数如下:
    private void button1_Click(object sender, System.EventArgs e)
    { label1.ForeColor=Color.Red;//运行阶段修改属性
    }//注意label1是控件的名字(label的Name属性),用它来区分不同的控件。
    (3) 单击(Click)标题为黑色的按纽的事件处理函数如下:
    private void button2_Click(object sender, System.EventArgs e)
    { label1.ForeColor=Color.Black;}
    (4) 单击(Click)标题为退出的按纽的事件处理函数如下:
    private void button3_Click(object sender, System.EventArgs e)
    { Close();}
    Close()为窗体(Form)的方法,作用是关闭注窗体。由于关闭了主窗体,程序也就结束了。注意,引用窗体的方法和属性时可不用指定对象名,换句话讲,如不指定属性或方法的对象名,默认为窗体的属性或方法。而使用其它组件的属性及方法要指明所属组件对象,例如label1.ForeColor=Color.Red;
    (5) 编译,运行,单击标题为红色的按纽,窗体显示字符串颜色变为红色,单击标题为黑色的按纽,窗体显示字符串颜色变为黑色,单击标题为退出的按纽,结束程序。
    3.5 事件处理函数的参数
    事件处理函数一般有两个参数,第一个参数(object sender)为产生该事件的对象的属性Name的值,例如上例单击标题为红色的按钮,第一个参数sender的值为button1。如上例标题为红色的按钮和标题为黑色的按钮使用同一个单击事件处理函数,其事件处理如下:
    private void button1_Click(object sender,System.EventArgs e)
    { if(sender==button1)
    label1.ForeColor=Color.Red;
    else
    label1.ForeColor=Color.Black;
    }
    事件处理函数第二个参数(System.EventArgs e)代表事件的一些附加信息,事件不同,所代表的信息也不相同,例如在后边的例子中可以看到,按下鼠标的事件处理函数中,e.X和e.Y分别为发生事件时鼠标位置的x坐标和y坐标,e.Button表示用户单击了鼠标那个键,如为MouseButtons.Left,表示单击了鼠标左键。
    为了使这两个按钮使用相同的单击事件处理函数,首先为标题为红色的按钮增加单击事件处理函数,即是上边的代码,事件函数名称为:button1_Click。选中标题为黑色的按钮,打开事件窗体(见图2.4.2B右图),选中Click事件,从其右侧下拉列表中选择事件处理函数为button1_Click,这样两个按钮就使用相同的单击事件处理函数了。
    3.6 文本框(TextBox)控件
    TextBox控件是用户输入文本的区域,也叫文本框。
    1. TextBox控件属性和事件
     属性Text:用户在文本框中键入的字符串
     属性MaxLength:单行文本框最大输入字符数。
     属性ReadOnly:布尔变量,为true,文本框不能编辑。
     属性PasswordChar:字符串类型,允许输入一个字符,如输入一个字符,用户在文本框中输入的所有字符都显示这个字符。一般用来输入密码。
     属性MultiLine:布尔变量,为true,多行文本框,为false,单行文本框。
     属性ScrollBars:MultiLine=true时有效,有4种选择:=0,无滚动条,=1,有水平滚动条,=2,有垂直滚动条,=3,有水平和垂直滚动条。
     属性SelLength:可选中文本框中的部分或全部字符,本属性为所选择的文本的字符数。
     属性SelStart:所选中文本的开始位置。
     属性SelText:所选中的文本
     属性AcceptsReturn:MultiLine=true时有效,布尔变量,为true,键入回车,换行,为false,键入回车键,相当于单击窗体中的默认按钮。
     事件TextChanged:文本框中的字符发生变化时,发出的事件。
    2. 例子e3_6
    本例要求用户在编辑框中输入两个乘数,单击按钮把相乘的结果在编辑框中显示出来。
    (1) 建立一个新的项目。放四个Label控件到窗体,Text属性分别为:被乘数,乘数,积,*,=。
    (2) 放三个textBox控件到窗体,属性Name从左到右分别为:textBox1、textBox2、textBox3,属性Text都为空。
    (3) 放三个Button控件到窗体,Text属性分别修改为求积,清空,退出。设计的界面如上图。
    (4) 标题为求积的按钮的单击事件处理函数如下:
    private void button1_Click(object sender, System.EventArgs e)
    { float ss,ee;
    ss=Convert.ToSingle(textBox1.Text);
    ee=Convert.ToSingle(textBox2.Text);
    textBox3.Text=Convert.ToString(ss*ee);
    }
    (5) 标题为清空的按钮的单击事件处理函数如下:
    private void button2_Click(object sender,System.EventArgs e)
    { textBox1.Text="";
    textBox2.Text="";
    textBox3.Text="";
    }
    (6) 标题为退出的按钮的单击事件处理函数如下:
    private void button3_Click(object sender, System.EventArgs e)
    { Close();}
    (7) 编译,运行,在文本框textBox1,textBox2分别输入2和3,单击标题为求积的按纽,textBox3中显示6,单击标题为清空的按钮,三个文本框被清空,单击标题为退出的按纽,结束程序。
    3.7 Convert类
    Convert类中提供了一些静态方法,用来把一种类型数据转换为另一种类型数据。例如,Convert.ToSingle(textBox1.Text)把字符串textBox1.Text转换为单浮点数。Convert.ToString(3.14)把单浮点数3.14转换为字符串。其它转换函数还有:ToInt、ToInt16等等。
    3.8 单选按钮(RadioButton)和GroupBox控件
    RadioButton是单选按钮控件,多个RadioButton控件可以为一组,这一组内的RadioButton控件只能有一个被选中。GroupBox控件是一个容器类控件,在其内部可放其它控件,表示其内部的所有控件为一组,其属性Text可用来表示此组控件的标题。例如把RadioButton控件放到GroupBox控件中,表示这些RadioButton控件是一组。有一些特性是互斥的,例如性别,选择这类特性可用RadioButton和GroupBox控件。
    1. GroupBox控件常用属性
    GroupBox控件常用属性只有一个,属性Text,指定GroupBox控件顶部的标题。
    2. RadioButton控件属性和事件
     属性Text:单选按钮控件旁边的标题。
     属性Checked:布尔变量,为true表示按钮被选中,为false表示不被选中。
     事件CheckedChanged:单选按钮选中或不被选中状态改变时产生的事件。
     事件Click:单击单选按钮控件时产生的事件。
    3. 例子e3_8
    该例用RadioButton控件修改Label控件字符串的字体为:宋体、黑体、楷体。具体实现步骤如下:
    (1) 建立一个新的项目。
    (2) 放Label控件到窗体,属性Text=“不同的字体”。字体为宋体。
    (3) 放GroupBox控件到窗体,其属性Text=“选择字体”。
    (4) 放三个RadioButton控件到GroupBox中,其属性Text分别为:宋体、黑体、楷体。宋体RadioButton控件的属性Checked=true。设计好的界面如右图。
    (5) 为三个RadioButton控件的CheckedChanged事件增加事件处理函数如下:
    private void radioButton1_CheckedChanged(object sender, System.EventArgs e)
    { if(radioButton1.Checked)
    label1.Font=new Font("宋体",label1.Font.Size);
    }//label1显示的字体变为宋体,字体大小不变
    private void radioButton2_CheckedChanged(object sender, System.EventArgs e)
    { if(radioButton2.Checked)
    label1.Font=new Font("黑体",label1.Font.Size);
    }
    private void radioButton3_CheckedChanged(object sender, System.EventArgs e)
    { if(radioButton3.Checked)
    label1.Font=new Font("楷体_GB2312",label1.Font.Size);
    }
    (6) 编译,运行,单击RadioGroup1中的三个RadioButton按钮,可以改变字体。注意三个按钮只能选一个,既只能选一种字体。考虑一下,是否可用Click事件。
    3.9 Font类
    Font类有两个构造函数:第一个是new Font(字体名称,字号),例如,label1.Font=new Font("黑体",9),用法还可参考例e3_8。第二个是new Font(字体名称,字号,字体风格),其中第三个参数是枚举类型,具体定义如下:
    enum FontStyle{
    Regular =0,//正常字体
    Bold =1,//黑体
    Italic =2,//斜体
    BoldItalic =3,//黑斜体
    Underline =4,//下划线,5=黑体下划线,6=斜体下划线,7=黑斜体下划线
    Strikeout =8}//删除线,9=黑体删除线,10=斜体删除线,依此类推。
    例如修改标签控件字体为斜体:
    label1.Font=new Font("黑体",9,label1.Font.Style|FontStyle.Italic);
    或者:label1.Font=new Font("黑体",9,label1.Font.Style|(FontStyle)2);
    修改标签控件字体不为斜体:
    label1.Font=new Font("黑体",9,label1.Font.Style&~FontStyle.Italic);
    或者:label1.Font=new Font("黑体",9,label1.Font.Style&(FontStyle)(~2));
    用法还可参考例e3_11。
    3.10 多选框(CheckBox)控件
    CheckBox是多选框控件,可将多个CheckBox控件放到GroupBox控件内形成一组,这一组内的CheckBox控件可以多选,不选或都选。可用来选择一些可共存的特性,例如一个人的爱好。
    1. CheckBox控件属性和事件
     属性Text:多选框控件旁边的标题。
     属性Checked:布尔变量,为true表示多选框被选中,为false不被选中。
     事件Click:单击多选框控件时产生的事件。
     事件CheckedChanged:多选框选中或不被选中状态改变时产生的事件。
    2. 例子e3_10A
    在窗口中增加2个CheckBox控件,分别用来选择是否爱好音乐和是否爱好文学,用鼠标单击CheckBox控件,改变爱好选择,用Label控件显示所选择的爱好。实现步骤如下:
    (1) 建立新项目。放Label控件到窗体,属性Text=“你的爱好是:”。
    (2) 放GroupBox控件到窗体,属性Text=“爱好”。放两个CheckBox控件到GroupBox中,属性Text分别为:音乐、文学。设计界面如下图。
    (3) 标题为音乐的多选框控件的CheckedChanged事件处理函数如下:
    private void checkBox1_CheckedChanged(object sender, System.EventArgs e)
    { String text1="你的爱好是:";
    if(checkBox1.Checked)
    text1=text1+checkBox1.Text;
    if(checkBox2.Checked)
    text1+=checkBox2.Text;
    label1.Text=text1;
    }
    (4) 将标题为文学的多选框控件的CheckedChanged事件处理函数,设置为标题为音乐的多选框控件的CheckedChanged事件处理函数,具体步骤见3.5节。
    (5) 编译,运行。选中音乐将在标签控件中显示:你的爱好是:音乐,再选中文学显示:你的爱好是:音乐文学,…。
    3. 例子e3_10B
    该例同上例,但按选中音乐和文学的顺序在标签中显示爱好,实现步骤如下:
    (1) 建立一个新项目。为Form1类增加私有变量String s="你的爱好是:"。
    (2) 放Label控件、GroupBox控件、两个CheckBox到窗体,属性设置同上例。
    (4)标题为音乐的多选框控件CheckBox1的CheckedChanged事件处理函数如下:
    private void checkBox1_CheckedChanged(object sender,System.EventArgs e)
    { int n=s.IndexOf("音乐");//s中有字符串"音乐"吗?n=-1表示没有
    if(n==-1)//n=-1,表示上次没选此项,此次选中,应增加"音乐"
    s+="音乐";
    else//否则,表示上次已选此项,此次不选中,应删除"音乐"
    s=s.Remove(n,2);
    label1.Text=s;
    }
    (5)标题为文学的多选框控件CheckBox2的CheckedChanged事件处理函数如下:
    private void checkBox2_CheckedChanged(object sender,System.EventArgs e)
    { int n=s.IndexOf("文学");//s中有字符串"文学"吗?=-1表示没有
    if(n==-1)//=-1,表示上次没选此项,此次选中,应增加"文学"
    s+="文学";
    else//否则,表示上次已选此项,此次不选中,应删除"文学"
    s=s.Remove(n,2);
    label1.Text=s;
    }
    (6)编译,运行。选中音乐在标签中显示:你的爱好是:音乐,再选中文学显示:你的爱好是:音乐文学,不选音乐显示:你的爱好是:文学,再选音乐显示:你的爱好是:文学音乐。
    3.11 列表选择控件(ListBox)
    列表选择控件列出所有供用户选择的选项,用户可从选项中选择一个或多个选项。
    1. 列表选择控件的常用属性、事件和方法
     属性Items:存储ListBox中的列表内容,是ArrayList类对象,元素是字符串。
     属性SelectedIndex:所选择的条目的索引号,第一个条目索引号为0。如允许多选,该属性返回任意一个选择的条目的索引号。如一个也没选,该值为-1。
     属性SelectedIndices:返回所有被选条目的索引号集合,是一个数组类对象。
     属性SelectedItem:返回所选择的条目的内容,即列表中选中的字符串。如允许多选,该属性返回选择的索引号最小的条目。如一个也没选,该值为空。
     属性SelectedItems:返回所有被选条目的内容,是一个字符串数组。
     属性SelectionMode:确定可选的条目数,以及选择多个条目的方法。属性值可以使:none(可以不选或选一个)、one(必须而且必选一个)、MultiSimple(多选)或MultiExtended(用组合键多选)。
     属性Sorted:表示条目是否以字母顺序排序,默认值为false,不允许。
     方法GetSelected():参数是索引号,如该索引号被选中,返回值为true。
     事件SelectedIndexChanged:当索引号(即选项)被改变时发生的事件。
    2. 例子e3_11
    根据列表框的选择,为字符串加下划线、删除线、变斜体、变粗体。具体步骤如下:
    (1) 建立一个新项目。放Label控件到窗体,其属性Text=“字体风格”。
    (2) 放置ListBox控件到窗体中,属性Name=listBox1。选中ListBox控件,在属性窗口中,单击Items属性右侧的三个小点,打开字符串集合编辑器对话框,在其中输入四项:粗体、斜体、下划线、删除线,注意每一项要换行。如上图。
    (3) 设置列表选择控件ListBox1属性SelectionMode为MultiExtended,允许多选。
    (4) 为列表选择控件的事件SelectedIndexChenged增加事件处理函数如下:
    private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e)
    { int Style=0,k=1;//Style=0正常字体,1=黑体,2=斜体,3=黑斜体等,参见3.9节
    for(int i=0;i<listBox1.Items.Count;i++)//此例Count=4,为什么?
    {if(listBox1.GetSelected(i))//例如此例GetSelected(0)=true表示粗体被选中
    Style=Style|k;//增加指定风格
    else
    Style=Style&(~k);//取消指定风格
    k=k*2;
    }
    FontStyle m=new FontStyle();
    m=(FontStyle)Style;
    label1.Font=new Font(label1.Font.Name,9,m);}
    (5) 编译,运行,单选或用Ctrl键多选,看一下效果。运行效果如上图。
    3.12 下拉列表组合框(ComboBox)控件
    控件ComboBox中有一个文本框,可以在文本框输入字符,其右侧有一个向下的箭头,单击此箭头可以打开一个列表框,可以从列表框选择希望输入的内容。现介绍该控件用法。
    1. ComboBox控件的常用属性、事件和方法
     属性DropDownStyle:确定下拉列表组合框类型。为Simple表示文本框可编辑,列表部分永远可见。为DropDown是默认值,表示文本框可编辑,必须单击箭头才能看到列表部分。为DropDownList表示文本框不可编辑,必须单击箭头才能看到列表部分。
     属性Items:存储ComboBox中的列表内容,是ArrayList类对象,元素是字符串。
     属性MaxDropDownItems:下拉列表能显示的最大条目数(1—100),如果实际条目数大于此数,将出现滚动条。
     属性Sorted:表示下拉列表框中条目是否以字母顺序排序,默认值为false,不允许。
     属性SelectedItem:所选择条目的内容,即下拉列表中选中的字符串。如一个也没选,该值为空。其实,属性Text也是所选择的条目的内容。
     属性SelectedIndex:编辑框所选列表条目的索引号,列表条目索引号从0开始。如果编辑框未从列表中选择条目,该值为-1。
     事件SelectedIndexChanged:被选索引号改变时发生的事件。
    2. 例子e3_12 选择Windows操作系统提供的所有字体
    增加一个ComboBox控件,用来选择字符串使用的字体名。本例提供方法使控件ComboBox的下拉列表中显示Windows操作系统中使用的所有字体名。运行效果如右图。实现步骤如下:
    (1) 建立新项目。放Label控件到窗体,其属性Text=“选择不同字体”。
    (2) 放ComboBox控件到窗体中,属性Name=comboBox1,属性DropDownStyle=DropDownList,不能在编辑框中输入字体名,只能从下拉列表中选取。
    (3) 为窗体Form1的事件Load增加事件处理函数如下:
    private void Form1_Load(object sender, System.EventArgs e)
    {//Families是类FontFamily的一个静态属性,得到操作系统中所使用的所有字体名
    FontFamily[] families=FontFamily.Families;//静态属性没有类的对象也可使用
    foreach (FontFamily family in families)
    comboBox1.Items.Add(family.Name);//注意Add方法的使用
    }
    (4) 为comboBox1的事件SelectedIndexChenged增加事件处理函数如下:
    private void comboBox1_SelectedIndexChanged(object sender, System.EventArgs e)
    {label1.Font=new Font(comboBox1.Text,9);}
    (5) 编译,运行,在下拉列表中选择不同字体名,标签的字体变为选择的字体。从下拉列表中可以看到操作系统中的所有字体名称已经在列表中。
    3.13 ToolTip控件
    在一些Windows应用程序中,例如Word程序,当鼠标在工具条的按钮上停留一段时间后,会在旁边出现提示,ToolTip控件就是为实现此功能的。可以用ToolTip控件为任何控件增加提示,本节介绍该控件的使用方法。
    例子e3_13 为Button控件增加提示
    (1) 建立一个新项目。放Button控件到窗体,Name属性为Button1。
    (2) 把toolTip控件放到窗体中,属性Name=ToolTip1。
    (3) 在Form1的构造函数中,增加语句如下:
    toolTip1.SetToolTip(button1,"这是一个按钮");
    (4) 编译,运行,当鼠标在Button上停留一段时间后,会在旁边出现提示:这是一个按钮。
    3.14 超级链接(LinkLable)控件
    控件LinkLable是控件Label的派生类,和控件Label不同的是显示的字符有下划线,可以为LinkLable控件的LinkClicked事件增加事件处理函数,当鼠标指向LinkLable控件,鼠标形状变为手形,单击该控件,调用这个事件处理函数,可以打开文件或网页。
    1. 超级链接控件的属性、方法和事件
     属性LinkColor:用户未访问过的链接的字符颜色,默认为蓝色。
     属性VisitedLinkColor:用户访问链接后的字符颜色。
     属性LinkVisited:如果已经访问过该链接,则为true;否则为false。
     属性LinkArea: 是一个结构,变量LinkArea.Start表示字符串中开始加下划线的字符位置,LinkArea.Length表示字符串中加下划线字符的个数。
     事件LinkClicked:单击控件LinkLable事件。
    2. 例子e3_14:用LinkLabel控件超级链接到微软网站。
    (1) 建立一个新工程。放LinkLabel控件到窗体,属性Text=“介绍微软的操作系统”。
    (2) 修改LinkLabel控件属性LinkArea.Length=2,LinkArea.Start=2。也可在构造函数用语句修改:linkLabel1.LinkArea=new LinkArea(2,2);
    (3) 为LinkLabel控件的事件LinkClicked增加事件处理函数:
    private void linkLabel1_LinkClicked(object sender, System.Windows.Forms.LinkLabelLinkClickedEventArgs e)
    { linkLabel1.LinkVisited=true;
    System.Diagnostics.Process.Start("http://www.micosoft.com.cn");
    }
    (4) 运行,效果如右图,注意只有字符微软带下划线。单击微软,打开浏览器访问微软主页。
    (5) 如果要打开一个窗口,列出C盘根目录下的文件及文件夹,LinkLabel控件事件LinkClicked事件处理函数修改如下:
    linkLabel1.LinkVisited=true;
    System.Diagnostics.Process.Start("C:/");
    (6) 如果要打开指定程序,例如打开记事本程序,修改LinkClicked事件处理函数如下:
    linkLabel1.LinkVisited=true;
    System.Diagnostics.Process.Start("notepad");
    3.15 定时(Timer)控件
    定时控件(Timer)也叫定时器或计时器控件,是按一定时间间隔周期性地自动触发事件的控件。在程序运行时,定时控件是不可见的。
    3. 定时控件的属性、方法和事件
     属性Interval:周期性地自动触发事件的时间间隔,单位为毫秒。
     属性Enabled:为true,启动定时器。调用方法Start()也可启动定时器。
     方法Start()和Stop():启动和停止定时器。设置属性Enabled=false也可停止定时器。
     事件Tick:每间隔属性Interval指定的时间,产生事件Tick。
    4. 例子e3_15用标签控件显示当前日期和时间
    (1) 建立一个新项目。放Timer组件到窗体,Name属性为timer1。
    (2) 放Label控件到窗体,Name属性为label1。
    (3) 为窗体Form1的事件Load增加事件处理函数如下:
    private void Form1_Load(object sender, System.EventArgs e)
    { this.timer1.Interval=100;
    this.timer1.Enabled=true;
    label1.Text=DateTime.Now.ToString();
    }
    (4) 为Timer1的Tick事件增加事件处理函数如下:
    private void timer1_Tick(object sender, System.EventArgs e)
    { label1.Text=DateTime.Now.ToString();
    }
    (5) 编译,运行,标签控件位置显示日期和时间。运行效果如上图。
    3.16 DateTime类
    DateTime类中提供了一些静态方法,可以用来得到日期、星期和时间,下面是一些常用的方法。
     得到日期和时间,并转换为字符串。
    String s=DateTime.Now.ToString();//或DateTime.Today.ToString()
     得到年、月和日期
    int y=DateTime.Now.Year;//得到年
    int m=DateTime.Now.Month;//得到月
    int d=DateTime.Now.Day;//得到日期
    String s=DateTime.Now.DayOfWeek.ToString();//英文表示的星期
     得到小时、分和秒
    int h=DateTime.Now.Hour;//得到小时
    int m=DateTime.Now.Minute;//得到分
    int s=DateTime.Now.Second;//得到秒
     定义一个DateTime类对象,表示1999年1月13日3时57分32.11秒
    System.DateTime moment=new System.DateTime(1999,1,13,3,57,32,11);
     加法和减法(减法请读者自己完成)
    System.DateTime dTime=new System.DateTime(1980,8,5);//1980年8月5日
    //时间间隔,17天4小时2分1秒
    System.TimeSpan tSpan=new System.TimeSpan(17,4,2,1);
    System.DateTime result=dTime+tSpan;//结果是:1980年8月22日4:2:1 AM.
    3.17 菜单
    Windows应用程序一般都有一个菜单,通过选择菜单中的不同菜单项,完成指定的功能。使用主菜单控件MainMenu可以很容易建立windows应用程序的主菜单。
    1. 菜单的组成及功能
    放主菜单控件MainMenu到窗体中,可以为窗体增加一个主菜单。主菜单一般包括若干顶级菜单项,例如,文件、编辑、帮助等。单击顶级菜单项,可以出现弹出菜单,弹出菜单中包含若干菜单项,例如单击文件顶级菜单项,其弹出菜单一般包括打开文件、存文件、另存为等菜单项,用鼠标单击菜单项,可以执行菜单项命令。有的菜单项还包括子菜单。
    所有菜单项都可以有快捷键,即菜单项中带有下划线的英文字符,当按住ALT键后,再按顶级菜单项的快捷键字符,可以打开该顶级菜单项的弹出菜单。弹出菜单出现后,按菜单项的快捷键字符,可以执行菜单项命令。增加快捷键的方法是在菜单项的标题中,在要设定快捷键英文字符的前边增加一个字符&,例如,菜单项的标题为:打开文件(&0),菜单项的显示效果为:打开文件(0)。菜单项可以有加速键,一般在菜单项标题的后面显示,例如,菜单项打开文件的加速键一般是Ctrl+O,不打开菜单,按住Ctrl键后,再按O键,也可以执行打开文件命令。设定加速键的方法是修改菜单项的ShortCut属性。
    2. 用程序生成菜单
    放主菜单控件MainMenu到窗体中,可以为该窗体增加一个主菜单,Visual Studio.Net自动添加如下语句:
    MainMenu mainMenu1=new MainMenu();
    This.Menu=mainMenu1;//指定主窗口的主菜单是mainMenu1。
    可以建立多个MainMenu类对象,用第二条语句修改使主窗口使用不同的主菜单。有了主菜单对象,用如下语句为主菜单增加顶级菜单项:
    MenuItem myFile=mainMenu1.MenuItem.Add(“文件(&F)”);//顶级菜单项:文件
    有了顶级菜单项对象,用如下语句为顶级菜单项的弹出菜单增加菜单项:
    myFile.MenuItem.Add(“打开(&O)”);//文件顶级菜单项的弹出菜单的菜单项:打开
    实际上,这些都可以用Visual Studio.Net自动生成。
    3. 菜单项的属性和事件
     属性Checked:布尔变量,=true,表示菜单项被选中,其后有标记:∨。
     属性ShortCut:指定的加速键,可以从下拉列表中选择。
     属性ShowShortCut:布尔变量,true(默认值),表示显示加速键,false,不显示。
     属性Text:菜单项标题。如为字符-,为分隔线。如指定字符前加&,例如:颜色(&c),增加快捷键,即用Alt+c访问颜色菜单。
     常用事件Click:单击菜单项事件。
    4. 例子e3_17 增加菜单
    本例在窗体中建立主菜单,主菜单包括一个顶级菜单项:颜色,其弹出菜单包括两个菜单项:红色、黑色,单击标题为红色的菜单项,把窗体中显示的字符串变为红色,单击标题为黑色的菜单项,把窗体中显示的字符串变为黑色。实现步骤如下:
    (1) 建立一个新项目。放Label控件到窗体。
    (2) 双击工具箱中Mainmenu控件,在窗体中增加主菜单。右下角有一主菜单图标,在左上角有一方框,其中有文字:请在此处输入,在此方框中输入菜单标题。
    (3) 在方框内输入字符”颜色”,在其下部方框内输入字符”红色”为一菜单项,在”红色”下输入字符”黑色”为另一菜单项,再输入”退出”菜单项。如希望在选中某一菜单项后出现下一级子菜单,可在菜单项右侧方框中输入子菜单项名。如果菜单项属性Text的值为-,则菜单项为分隔符。可以用鼠标拖动菜单项移动菜单项的位置。集成环境设计界面如下图。

    (4) 标题为红色的菜单项的单击(Click)事件处理函数如下:
    private void menuItem2_Click(object sender,System.EventArgs e)
    {label1.ForeColor=Color.Red;}//改变字体颜色为红色
    (5) 标题为黑色的菜单项的单击(Click)事件处理函数如下:
    private void menuItem3_Click(object sender, System.EventArgs e)
    {label1.ForeColor=Color.Black;}//改变字体颜色为黑色
    (6) 标题为退出的菜单项的单击(Click)事件处理函数如下:
    private void menuItem4_Click(object sender, System.EventArgs e)
    { Close();}//退出程序
    (7) 编译,运行,单击红色和黑色菜单项,能改变字符串的颜色。效果如上图。
    3.18 工具条
    一般Windows应用程序都有一个工具条,可以认为工具条上的按钮为菜单的某一菜单项的快捷按钮,单击工具条按钮相当于单击相应菜单项,完成同样的功能。
    1. 工具条的组成及功能
    放工具条控件ToolBar到窗体中,可以为该窗体增加一个工具条。在工具条中可以增加Button按钮和其它控件,例如象Word程序的工具条中用下拉列表控件(ComboBox)选择字号、字体等。一般工具条按钮上都有一个图标,提示用户该按钮的使用功能。按钮的所有图标存放到ImageList类对象中。单击任何一个按钮,都产生工具条控件的ButtonClick事件,在这个事件处理事件函数中,要用语句区分用户单击了那一个按钮,以完成相应的功能。
    2. 控件ToolBar的属性、事件和方法
     属性BorderStyle:边界风格,=None(默认值),无边界;=FixedSingle,单线边界;=Fixed3D,立体风格边界。
     属性Button:集合属性,存储ToolBar的按钮对象。单击其后的按钮,可以打开ToolBarButton集合编辑器对话框(见下图),增加或删除按钮,修改按钮属性。

     属性ImageList:指定一个ImageList类对象,该对象中可以存储若干图标,这些图标作为ToolBar控件按钮的图标。
     属性Wrappable:布尔变量,=true(默认值),当窗体Form水平尺寸小于工具条的水平尺寸时,一行不能显示所有按钮,允许下一行显示;=false,不允许。
     事件ButtonClick:ToolBar控件的单击事件。在ButtonClick事件处理事件函数中,要用语句区分用户单击了那一个按钮,以完成相应的功能。
     属性ShowToolTips:布尔变量,=true,允许显示提示信息。
     方法IndexOF():参数为ToolBar控件中按钮的属性Name,返回其索引值。
    3. ToolBar控件中ToolBarButton按钮的属性
    ToolBar控件中ToolBarButton按钮可以看作独立的控件,它有自己独立的属性。下面介绍ToolBar控件中ToolBarButton按钮的属性。
     属性ImageIndex:ToolBar控件属性ImageList指定一个ImageList类对象,该对象中的图标作为ToolBar控件按钮的图标。这个属性指定本按钮使用ImageList类对象中存储的第几个图标。
     属性Style:有4个值,=PushButton,为普通按钮;=Separator,为一分割符,再左边和右边的两个按钮中间增加一个间隙;=ToggleButton,开关按钮,单击该按钮,按钮被按下,不抬起,再单击,抬起。=DropDownButton,下拉按钮,按钮右侧有一个下拉箭头,单击下拉箭头,可以弹出下拉列表。
     属性Text:ToolBar控件中按钮除了有图标外,还可以有属性Text指定的文字。
     属性ToolTipText:当鼠标在工具条按钮上停留一段时间后,将在工具条按钮旁边出现此属性指定的提示。
    4. 例子e3_18
    现为上例的菜单增加工具条,有两个按钮,单击按钮分别使字体变红、变黑。步骤如下:
    (1) 继续菜单的例子,放ImageList控件到窗体。
    (2) 放ToolBar控件到窗体。修改属性ImageList=ImageList1。
    (3) 单击ImageList属性Images后按钮,打开Image集合编辑器,单击添加按钮,打开选择文件对话框。按指定路径选择图标的文件后,单击确定按钮,增加图标到ImageList对象中。在C:\Program Files\Microsoft Office\Office\forms\2052文件夹和C:\program files\Microsoft Visual Studio.Net\Common7\Graphics\Icon\Misc文件夹中有若干图标。也可用画笔程序自己设计图标,图标的宽和高应比工具条按钮的宽和高略小,存为.ico文件。也可以用抓图软件抓其它程序的图标。任选以上方法,为ImageList对象增加两个图标。
    (4) 单击ToolBar控件属性Buttons后按钮,打开ToolBarButton集合编辑器(见上图),单击添加按钮,增加一个按钮,从其属性ImageIndex后的下拉列表中选择按钮使用的图标,设置按钮的ToolTipText属性为:改变字体为红色,为工具按钮增加提示。同样方法增加第二个按钮,按钮的ToolTipText属性为:改变字体为黑色。
    (5) 设定ToolBar控件属性ShowToolTips为true。
    (6) 为ToolBar控件的ButtonClick事件增加事件函数如下:
    private void toolBar1_ButtonClick(object sender,
    System.Windows.Forms.ToolBarButtonClickEventArgs e)
    { int n=toolBar1.Buttons.IndexOf(e.Button);//n为工具条中被单击按钮的序号
    switch(n)
    { case 0://第一个按钮,调用相应的菜单项的事件处理函数。
    this.menuItem3_Click(sender,e);
    break;
    case 1://第二个按钮
    this.menuItem2_Click(sender,e);
    break;
    }
    }
    (7) 编译,运行,单击两个工具条按钮,可以分别使字体变为红色或黑色。见上图。
    3.19 状态栏(StatusBar)控件
    Windows应用程序的状态栏一般用来显示一些信息,如时间,鼠标位置等。
    1. 状态栏控件的属性
     属性Panels:集合属性,存储状态栏中的各个分栏对象。单击其后标题为…的按钮,可以打开StatusBarPanels集合编辑器对话框,增加或删除分栏,修改分栏属性。
     属性ShowPanel:布尔变量,=true,允许显示多栏;=false,不允许。
    2. 状态栏(StatusBar)控件分栏的属性
    状态条可以为单栏,也可以为多栏。属性Text,表示在状态栏中显示的内容。如为单栏,在单栏中显示字符串的语句是:statusBar1.Text=”在单栏中显示的文本”,如为多栏,在第2栏中显示字符串的语句是:statusBar1.Panels[1].Text=”在第2栏中显示的文本”。
     属性Alignment:对齐方式,可以为左对齐、右对齐和中间对齐。
     属性Text:表示在状态栏中显示的内容。
     属性Width:栏的宽度。
     属性BorderStyle:指定状态栏控件上 每个分栏的边框外观。边界风格,=None(默认值),不显示边框;=Raised,三维凸起边框;=Sunken,三维凹陷边框显示。
    3. 例子e3_19 为窗体增加状态条,在状态条内显示时间和鼠标位置。
    (1) 建立新项目。放StatusBar控件到窗体。单击StatusBar控件属性Panels后按钮,打开StatusBarPanels集合编辑器(如下图),单击添加按钮,增加若2栏。其序号为0、1。

    (2) 修改StatusBar控件属性ShowPanel=true。
    (3) 放Timer组件到窗体,Name=Timer1,属性Interval=1000,Enabled=true。
    (4) 为Timer1的Tick事件增加事件处理函数如下:
    private void timer1_Tick(object sender, System.EventArgs e)
    { statusBar1.Panels[0].Text=DateTime.Now.ToString();
    }
    (5) 为Form1的MouseMove事件增加事件处理函数如下:
    private void Form1_MouseMove(object sender,System.Windows.Forms.MouseEventArgs e)
    {statusBar1.Panels[1].Text="X:"+e.X.ToString()+",Y:"+e.Y.ToString();
    }
    (6) 编译,运行,如右图,在第1栏中可以看到当前时间,在窗口中移动鼠标,在第2栏中可以看到鼠标的位置不断变化。
    3.20 鼠标事件
    从类System.Windows.Forms.Control派生的控件都有鼠标事件,控件的Click事件本质上也是鼠标事件。一些控件还有单独的鼠标事件,例如Form。鼠标事件有:
     MouseDown:如果鼠标位于控件区域,按下鼠标按键时产生该事件。
     MouseUp:如果鼠标位于控件区域,抬起鼠标按键时产生该事件。
     MouseMove:如果鼠标在控件区域移动,产生该事件。
     MouseEnter:鼠标进入控件区域,产生该事件。
     MouseLeave:鼠标离开控件区域,产生该事件。
    鼠标事件处理函数一般有两个参数,第一个参数(object sender)是产生该事件的对象的属性Name的值,例如,为Form1的MouseDown事件增加事件函数,单击Form1,第一个参数sender代表Form1对象。(System.Windows.Forms.MouseEventArgs e)是事件处理函数第二个参数,代表事件的一些信息,事件不同,所代表的信息也不相同,鼠标按下事件处理函数中,e.X为发生事件时鼠标位置的x坐标,e.Y为发生事件时鼠标位置的y坐标,e.Button为MouseButtons.Left,表示单击了鼠标左键等等,Right和Middle则分别代表右键和中间键。e.Clicks为鼠标单击的次数,如果大于2次,则为双击。
    例子e3_20:在窗体中的指定区域,双击鼠标左键,用Label控件显示双击鼠标的位置。指定区域的左上角坐标为(20,20),宽为200,高为200。
    (1) 建立一个新项目。放Label控件到窗体。属性Name=label1。
    (2) Panel控件可以将窗体分为多个区域。放Panel控件到窗体,属性Location.X=20,Location.Y=20,属性Width=200,Height=200,属性Name=p1。
    (3) 为Panel的MouseDown事件增加事件函数如下:
    private void p1_MouseDown(object sender,System.Windows.Forms.MouseEventArgs e)
    { if(e.Button==MouseButtons.Left&&e.Clicks>1)//如果是双击左键
    label1.Text="X:"+e.X.ToString()+",Y:"+e.Y.ToString();
    }
    (4) 编译,运行,分别在指定区域和区域外双击鼠标左键,看一下效果。分别在指定区域和区域外双击鼠标右键,看一下效果。
    3.21 快捷菜单(ContextMenu)
    使用过Word程序的人都知道,在其程序窗口的不同位置单击右键,会出现不同弹出菜单,这个弹出菜单叫快捷菜单,这节介绍如何在应用程序中增加快捷菜单。快捷菜单和主菜单的属性、事件和方法基本一致,只是快捷菜单没有顶级菜单项,因此这里就不多介绍了。
    例子e3.21
    例子在窗口中显示一行字符串,加入两个按纽,单击按纽button1把字符串变为红色,单击按纽button2把字符串变为黑色。为两个按钮建立快捷菜单,快捷菜单中有2个菜单项,单击菜单项把字符串变为红色或黑色。为窗体建立快捷菜单,菜单中仅有1个退出菜单项,单击退出菜单项,退出程序。具体实现步骤如下:
    (1) 建立一个新项目。放Label控件到窗体。
    (2) 放2个Button控件到窗体,标题(属性Text)分别为红色,黑色。
    (3) 标题为红色的按钮的单击事件处理函数如下:
    private void button1_Click(object sender, System.EventArgs e)
    { label1.ForeColor=Color.Red;}
    (4) 标题为黑色的按钮的单击事件处理函数如下:
    private void button2_Click(object sender, System.EventArgs e)
    { label1.ForeColor=Color.Black;}
    (5) 放2个ContextMenu控件到窗体,属性Name分别为contextMenu1,contextMenu2。
    (6) 选中contextMenu1控件,在菜单编辑器中增加两个标题分别为红色和黑色的菜单项,它们的单击事件处理函数分别是单击红色按钮和单击黑色按钮的事件处理函数。
    (7) 选中contextMenu2控件,在菜单编辑器中增加标题为退出的菜单项,并为其增加单击事件处理函数,为事件处理函数增加语句:Close();
    (8) 将红色按钮和黑色按钮的属性ContextMenu指定为contextMenu1。Form的属性ContextMenu指定为contextMenu2。
    (9) 编译,运行,右击标题为红色的按钮,快捷菜单contextMenu1打开,单击快捷菜单中标题为红色的菜单项,将使窗体显示的字符串颜色变为红色,右击标题为黑色的按钮,快捷菜单contextMenu1打开,单击快捷菜单中标题为黑色的菜单项,将使窗体显示的字符串颜色变为黑色,右击窗体,快捷菜单contextMenu2打开,单击快捷菜单中标题为退出的菜单项,将退出应用程序。运行效果如上图。
    3.22 综合例子:计算器
    具体步骤如下:
    (1) 建立一个新项目。Form属性MaxiMizeBox=false,属性MiniMizeBox=false。属性FormBorderStyle=FixedDialog,窗口不能修改大小。
    (2) 放textBox控件到窗体,属性Name=textBox1,属性Text="0",属性ReadOnly=true。
    (3) 增加10个Button控件,前9个按钮属性Name分别为:Button1-Button9,最后一个为Button0,属性Text分别为:1、2、3、4、5、6、7、8、9、0。
    (4) 增加7个Button控件,属性Name分别为:btn_dot、btn_equ、btn_add、btn_sub、btn_mul、btn_div、btn_C,属性Text分别为:.、=、+、-、*、/、C。设计界面如下图。
    (5) 控件Button0单击事件处理函数如下:
    private void button0_Click(object sender, System.EventArgs e)
    { if(sender==button0) append_num(0);
    if(sender==button1) append_num(1);
    if(sender==button2) append_num(2);
    if(sender==button3) append_num(3);
    if(sender==button4) append_num(4);
    if(sender==button5) append_num(5);
    if(sender==button6) append_num(6);
    if(sender==button7) append_num(7);
    if(sender==button8) append_num(8);
    if(sender==button9) append_num(9);
    }
    (6) 为Form1类增加方法如下:
    public void append_num(int i)
    { if(textBox1.Text!="0")
    textBox1.Text+=Convert.ToString(i);
    else
    textBox1.Text=Convert.ToString(i);
    }
    (7) 将Button1-Button9的单击事件处理函数设定为Button0单击事件处理函数。
    (8) 为标题为.按钮增加事件处理函数如下:
    private void btn_dot_Click(object sender, System.EventArgs e)
    { int n=textBox1.Text.IndexOf(".");
    if(n==-1)//如果没有小数点,增加小数点,否则不增加
    textBox1.Text=textBox1.Text+".";
    }
    (9) 编译,单击数字按钮,在textBox1可以看到输入的数字,也可以输入小数。
    (10) 先实现加法,必须定义一个浮点类型变量sum,初始值为0,记录部分和。
    (11) 输入了第一个加数,然后输入任一运算符(+、-、*、\或=),应首先清除编辑框中显示的第一个加数,才能输入第二个加数。为实现此功能,必须定义一个布尔变量blnClear,初始值为false,表示输入数字或小数点前不清除编辑框中显示,输入运算符(+、-、*、\或=)后,blnClear=true,表示再输入数字或小数点先清除编辑框中显示。修改前边程序,输入数字或小数点前,要判断变量blnClear,如为true,清除编辑框中显示的内容后,再显示新输入的数字或小数点,同时修改blnClear=false。为此修改append_num方法如下:
    public void append_num(int i)
    { if(blnClear)//如果准备输入下一个加数,应先清除textBox1显示内容
    { textBox1.Text="0";//阴影部分为新增语句
    blnClear=false;
    }
    if(textBox1.Text!="0")
    textBox1.Text+=Convert.ToString(i);
    else
    textBox1.Text=Convert.ToString(i);
    }
    (12) 修改btn_dot_Click方法如下:
    private void btn_dot_Click(object sender, System.EventArgs e)
    { if(blnClear) //如果准备输入下一个数,应先清除textBox1显示内容
    { textBox1.Text="0";//阴影部分为新增语句
    blnClear=false;
    }
    int n=textBox1.Text.IndexOf(".");
    if(n==-1)//如果没有小数点,增加小数点,防止多次输入小数点
    textBox1.Text=textBox1.Text+".";
    }
    (13) 如果计算1+2-3的运算结果,先单击按钮1,编辑框中显示1,再单击按钮+,执行运算sum=sum+1(注意此时sum=0),显示sum到编辑框中(实际显示不变),记住此次输入的运算符,这里为+号。单击按钮2,编辑框中显示2,再单击按钮-,按记录的运算符(这里是+)计算sum=sum+2,显示sum到编辑框中,记住此次输入的运算符,这里为-号,依此类推。为实现此功能,必须定义一个字符串变量strOper,记录输入的运算符,初始值为"+",保证输入第一个运算符后,执行运算sum=sum+第一个加数,由于初始sum=0,也就是sum=第一个加数。标题为+的按钮的单击事件处理函数如下:
    private void btn_add_Click(object sender, System.EventArgs e)
    { double dbSecond=Convert.ToDouble(textBox1.Text);
    if(!blnClear)//如果未输入第二个操作数,不运算
    switch(strOper)//按记录的运算符号运算
    { case "+":
    sum+=dbSecond;
    break;
    //在此增加其它运算符-、*、\代码
    }
    if(sender==btn_add)
    strOper="+";
    //在此增加运算符-、*、\、=代码
    textBox1.Text=Convert.ToString(sum);
    blnClear=true;
    }
    (14) =号处理语句和+号处理基本一致,修改标题为+按钮的事件函数如下:
    private void btn_add_Click(object sender, System.EventArgs e)
    { double dbSecond=Convert.ToDouble(textBox1.Text);
    if(!blnClear)//如果未输入第二个操作数,不运算
    switch(strOper)//按记录的运算符号运算
    { case "+":
    sum+=dbSecond;
    break;
    //在此增加运算符-、*、\代码
    }
    if(sender==btn_add)
    strOper="+";
    if(sender==btn_equ)//为=号处理增加的语句
    strOper="=";
    textBox1.Text=Convert.ToString(sum);
    blnClear=true;
    }
    将btn_equ按钮的单击事件函数设定为+按钮的单击事件函数。
    (15) 为标题为C按钮增加事件函数如下:
    private void btn_C_Click(object sender, System.EventArgs e)
    { textBox1.Text="0";
    sum=0;
    blnClear=false;
    strOper="+";
    }
    (16) 请读者自己补上减法,乘法,除法运算的语句。
    习题:
    (1) 在窗口中显示一行字符串,加入两个按纽,单击按纽1把字符串改为红色,单击按纽2把字符串改为黑色。使字符串为红色时红色按纽不能使用,字符串为黑色时黑色按纽不能使用。(提示:可以修改按钮的属性Enabled为false使其不能使用。)
    (2) 将上题改为用按扭修改字体的大小,分别为大字体和小字体。(参见3.9节)
    (3) 加一文本框控件和一按纽,单击按纽将文本框控件输入内容显示标签控件上。(提示:单击按钮事件处理函数中加语句label1.Text=textBox1.Text)。
    (4) 修改上题,使文本框控件和标签控件文本同步显示(提示:文本框控件的TextChanged事件处理函数中加语句label1.Text=textBox1.Text)。
    (5) 加一文本框控件和一按纽,单击按纽将文本框控件输入的文本中选中的内容显示在标签控件上(提示:单击按钮事件处理函数中加语句label1.Text=textBox1.SelText。)
    (6) 加一文本框控件和一按纽,单击按纽将文本框控件输入的文本的字符、选中的内容的字符数和选中的内容的开始位置显示在标签控件上。
    (7) 用控件RadioButton选择性别,把选择的结果用Label控件显示出来。
    (8) 例子e3_8中如改为响应单击事件Click,可能出现什么问题?
    (9) 用控件ComboBox修改标签控件字体的大小。(用属性Item在下拉列表中输入大小)。
    (10) 放ListBox控件到窗体中,属性Name=listBox1。列表框有三项分别为:苹果,梨子,香蕉。允许多选。标签控件同步显示ListBox控件所做的选择。提示:为ListBox控件的SelectedIndexChenged事件增加事件函数,
    label1.Text="所选择的是:";
    for(int i=0;i<listBox1.SelectedIndices.Count;i++)
    label1.Text+=listBox1.SelectedItems[i].ToString()+",";
    (11) 放ListBox、TextBox和3个Button控件到窗体中,属性Name分别为listBox1、textBox1、Button1、Button2、Button3。Button控件属性Text分别为:增加、删除、清空。单击增加按钮,把textBox中输入的内容作为一个条目增加到listBox1中,单击删除按钮,删除listBox1中所选择的条目,单击清空按钮,清除listBox1所有条目。提示:增加用语句:listBox1.Items.Add(textBooooox1.Text)。删除所选择的条目用语句:listBox1.Items.RemoveAt(listBox1.SelectedIndex)。清除listBox1所有条目用语句:listBox1.Items.Clear()。
    (12) 在窗体中显示字符,每隔1秒字体变大些,变到一定尺寸后,每隔1秒字体变小些,如此循环。增加一个按钮,可以启动和停止字符串字体大小变化,按钮标题给出正确提示。
    (13) 在窗体中显示字符,每隔1秒字符移动一定距离,先右移,移到右边界,再左移,移到左边界,又一次右移,如此循环。(提示:修改Label的Left属性值。)
    (14) 修改例子e3_17,使显示字符串为红色时,标题为红色的菜单项无效;使显示字符串黑色时,标题为黑色的菜单项无效。
    (15) 修改例子e3_17,使显示字符串为红色时,标题为红色的菜单项前增加选中标志;使显示字符串黑色时,标题为黑色的菜单项前增加选中标志。
    (16) 为例e3_17的菜单项增加加速键,键入Alt+c打开顶级菜单项颜色的弹出菜单,弹出菜单打开后,键入B执行标题为黑色的菜单项命令,键入R执行标题为红色的菜单项命令。
    (17) 为例子e3_17的菜单项定义加速键(属性Shortcut),键入ctrl+r使显示字符串为红色,键入ctrl+b使显示字符串黑色。
    (18) 为例子e3_17顶级菜单项颜色增加单击事件处理函数,在事件处理函数中判断显示的字符串的颜色,决定是否为相应的菜单项增加选中标志。
    (19) 拖动鼠标左键时,在状态栏中显示鼠标的位置。
    (20) 模拟画笔程序,在左侧增加工具按钮,在下部增加颜色按钮。
    (21) 在工具栏中加三个按钮,单击按钮时,按钮保持按下状态,再单击按钮,按钮抬起。在按下状态,使标签控件中字符串加下画线、斜体或加粗,抬起则取消。
    (21) 工具栏中按钮的属性Style设置为ToolBarButtonStyle.DropDownButton,按钮可有一个下拉菜单。首先创建一个ContextMenu菜单,指定工具栏中按钮的属性DropDownMenu的值为创建的ContextMenu菜单对象,将在按下按钮时显示这个菜单。请用工具栏中按钮的下拉菜单实现使标签控件字符的颜色变为红色、黑色。(提示:工具栏中按钮的属性Style设置为ToggleButton。属性Pushed是一个布尔变量,表示工具栏按钮当前是否处于按下状态)
    (22) 用工具栏中按钮的下拉菜单实现使标签控件字符的颜色变为红色、黑色。(提示:如工具栏中按钮的属性Style设置为DropDownButton,按钮可有一个下拉菜单。首先创建一个ContextMenu菜单,指定工具栏中按钮的属性DropDownMenu的值为创建的ContextMenu菜单对象,将在按下按钮时显示这个菜单。)
    (23) 完成计算器的减法和乘除程序。增加求平方,对数等功能。(例如Math.Sqrt())


    第四章 文本编辑器的实现
    本章的目的是建立一个文本编辑器,同时继续介绍控件的用法。有两类文本编辑器:单文档文本编辑器和多文档文本编辑器,单文档文本编辑器一次只允许打开一个文件,如果要打开另一个文件,必须关闭当前打开的文件,微软的写字板程序就是一个典型的单文档字处理程序。多文档文本编辑器同时允许打开多个文件,每个文件占用一个子窗口,微软的Word程序就是一个典型的多文档字处理程序。本章首先介绍建立一个单文档文本编辑器的方法,然后介绍建立多文档文本编辑器的方法。
    4.1 用RichTextBox控件实现文本编辑器
    RichTextBox控件可以用来输入和编辑文本,该控件和TextBox控件有许多相同的属性、事件和方法,但比TextBox控件的功能多,除了TextBox控件的功能外,还可以设定文字的颜色、字体和段落格式,支持字符串查找功能,支持rtf格式等。这里只介绍在TextBox控件中没有介绍的属性、事件和方法,相同部分就不介绍了,可参见TextBox控件。RichTextBox控件的属性、事件和方法如下:
     属性Dock:很多控件都有此属性,它设定控件在窗体中的位置,可以是枚举类型DockStyle的成员None、Left、Right、Top、Bottom或Fill,分别表示在窗体的任意位置、左侧、右侧、顶部、底部或充满客户区。在属性窗口中,属性DOCK的值用周边5个矩形,中间一个矩形的图形来表示。
     属性SelectedText:获取或设置RichTextBox控件内的选定文本。
     属性SelectionLength:获取或设置RichTextBox控件中选定文本的字符数。
     属性SelectionStart:获取或设置RichTextBox控件中选定的文本起始点。
     属性SelectionFont:如果已选定文本,获取或设置选定文本字体,如果未选定文本,获取当前输入字符采用字体或设置以后输入字符采用字体。
     属性SelectionColor:如果已选定文本,获取或设置选定文本的颜色,如果未选定文本,获取当前输入字符采用的颜色或设置以后输入字符采用的颜色。
     属性Lines:记录RichTextBox控件中所有文本的字符串数组,每两个回车之间字符串是数组的一个元素。
     属性Modified:指示用户是否已修改控件的内容。为true,表示已修改。
     事件SelectionChange:RichTextBox控件内的选定文本更改时发生的事件。
     事件TextChanged:RichTextBox控件内的文本内容改变时发生的事件。
     方法Clear():清除RichTextBox控件中用户输入的所有内容,即清空属性Lines。
     方法Copy()、Cut()、Paste():实现RichTextBox控件的拷贝、剪贴、粘贴功能。
     方法SelectAll():选择RichTextBox控件内的所有文本。
     方法Find():实现查找功能。从第二个参数指定的位置,查找第一个参数指定的字符串,并返回找到的第一个匹配字符串的位置。返回负值,表示未找到匹配字符串。第三个参数指定查找的一些附加条件,可以是枚举类型RichTextBoxFinds的成员:MatchCase(区分大小写)、Reverse(反向查找)等。允许有1个、2个或3个参数。
     方法SaveFile():存文件,它有2个参数,第一个参数为要存文件的全路径和文件名,第二个参数是文件类型,可以是:纯文本,RichTextBoxStreamType.PlainText;Rtf格式流,RichTextBoxStreamType.RichText;采用Unicode编码的文本流,RichTextBoxStreamType.UnicodePlainText。
     方法LoadFile():读文件,参数同方法SaveFile(),注意存取文件的类型必须一致。
     方法Undo():撤消RichTextBox控件中的上一个编辑操作。
     方法Redo():重新应用RichTextBox控件中上次撤消的操作。
    4.2 实现文本编辑器的剪贴板功能
    许多程序都支持剪贴板功能。通过剪贴板可以完成数据的剪贴(Cut),复制(Copy),粘贴(Paste)等功能。剪贴板可以理解为一块存储数据的公共区域,用户可以把数据复制或剪贴到剪贴板中,本任务或其它任务要用剪贴板中的数据时,可以用粘贴功能从剪贴板中把数据取出。存入剪贴板中的数据,可以是字符,位图,或者其它格式数据。实现文本编辑器的编辑和剪贴板功能的具体步骤如下:
    (1) 新建项目。放RichTextBox控件到窗体。属性Name=richTextBox1,Dock=Fill,Text=””。
    (2) 放Mainmenu控件到窗体中。增加顶级菜单项:编辑,为其弹出菜单增加菜单项:剪切、复制、粘贴、撤销和恢复,属性Name分别为:mainMenuEdit、menuItemEditCut、menuItemEditCopy、menuItemEditPaste、menuItemEditUndo、menuItemEditRedo。为各个菜单项增加事件处理函数如下:
    private void menuItemEditCut_Click(object sender, System.EventArgs e)
    { richTextBox1.Cut();} //剪切
    private void menuItemEditCopy_Click(object sender, System.EventArgs e)
    { richTextBox1.Copy();} //拷贝
    private void menuItemEditPaste_Click(object sender, System.EventArgs e)
    { richTextBox1.Paste();} //粘贴
    private void menuItemEditUndo_Click(object sender, System.EventArgs e)
    { richTextBox1.Undo();} //撤销
    private void menuItemEditRedo_Click(object sender, System.EventArgs e)
    { richTextBox1.Redo();} //恢复
    (3) 编译,运行,输入一些字符后,选中一些字符,试验一下剪切、复制、粘贴等功能,并查看一下在剪贴板中字符是否能粘贴到其它字处理软件中,例如写字板。查看一下撤销和恢复功能是否可用。
    4.3 实现文本编辑器的存取文件功能
    文本编辑器都具有文件存取功能,顶级菜单项文件的弹出菜单中一般包括如下菜单项:新建、打开、关闭、保存和另存为等。本节实现以上菜单项。
    4.3.1 OpenFileDialog和SaveFileDialog控件
    OpenFileDialog对话框用来选择要打开的文件路径及文件名,SaveFileDialog对话框用来选择要存储文件的路径及文件名。两个对话框的外观如下图,它们的属性和方法基本相同,这里在一起介绍。

    图4.3.1A 打开文件对话框

    图4.3.1B 文件另存为对话框
     属性Filter:字符串类型,选择在对话框中显示的文件类型。属性Filter有多项,中间用|分开,每两项是一组,每组的第一项将出现在对话框保存类型(T)下拉列表编辑框的下拉列表中(见图4.3.1A),供用户选择,第二项表示如第一项被选中,对话框实际列出的文件。例如Filter="纯文本文件(*.txt)|*.txt|所有文件(*.*)|*.*",表示打开对话框,对话框的文件类型(T)下拉列表编辑框的下拉列表有两项:纯文本文件(*.txt)和所有文件(*.*),供用户选择。如果从文件类型下拉列表编辑框的下拉列表中选中"纯文本文件(*.txt)",表示打开对话框,只列出所有扩展名为.txt的文件,如果选中"所有文件(*.*)",表示打开对话框,将列出所有文件。
     属性FilterIndex:表示打开对话框后,对话框的文件类型(T)下拉列表编辑框的下拉列表中首先被选中的项的索引号。可以在设计阶段在属性窗口修改属性FilterIndex和Filter,也可在程序中用下列语句修改:openFileDialog1.Filter="纯文本文件(*.txt)|*.txt|所有文件(*.*)|*.*",openFileDialog1.FilterIndex=1。
     属性FileName:用户选取的文件的路径和文件名。
     属性InitialDirectory:打开对话框首先显示该属性指定的文件夹中的文件。
     属性DefaultExt:如果用户未指定扩展名,自动增加属性指定的文件扩展名。
     方法ShowDialog():打开对话框,根据方法的返回值确定用户单击了那个按钮,如返回DialogResult.Cancle,用户单击了忽略按钮,如返回DialogResult.OK,用户单击了打开或保存按钮。
    4.3.2 存取文件功能的实现
    (4) 把OpenFileDialog和SaveFileDialog控件放到窗体中。属性Name分别是openFileDialog1和saveFileDialog1。
    (5) 增加顶级菜单项:文件,为其弹出菜单增加菜单项:新建、打开...、保存...、另存为...、退出。修改Name属性分别为:mainMenuFile、menuItemFileNew、menuItemFileOpen、menuItemFileSave、menuItemFileSaveAs、menuItemFileExit。
    (6) 为Form1类增加string类型变量记录当前编辑的文件名:string s_FileName="",如果为空,表示还未记录文件名,即编辑的文件还没有名字。当单击菜单项保存,保存文件时,必须请用户输入文件名。
    (7) 为新建菜单项增加事件处理函数如下:
    private void menuItemFileNew_Click(object sender, System.EventArgs e)
    { richTextBox1.Text="";//或richTextBox1.Clear();
    s_FileName="";//新建文件没有文件名。
    }
    (8) 为打开文件菜单项增加事件处理函数如下:
    private void menuItemFileOpen_Click(object sender, System.EventArgs e)
    { if(openFileDialog1.ShowDialog()==DialogResult.OK)
    { s_FileName=openFileDialog1.FileName;
    richTextBox1.LoadFile(openFileDialog1.FileName,
    RichTextBoxStreamType.PlainText);
    }
    }
    (9) 为另存为菜单项增加事件处理函数如下:
    private void menuItemFileSaveAs_Click(object sender, System.EventArgs e)
    { if(saveFileDialog1.ShowDialog()==DialogResult.OK)
    { s_FileName=saveFileDialog1.FileName;
    richTextBox1.SaveFile(saveFileDialog1.FileName,
    RichTextBoxStreamType.PlainText);
    }//注意存取文件类型应一致。
    }
    (10) 为保存文件菜单项增加事件处理处理函数如下:
    private void menuItemSaveFile_Click(object sender, System.EventArgs e)
    { if(s_FileName.Length!=0)
    richTextBox1.SaveFile(s_FileName,RichTextBoxStreamType.PlainText);
    else
    menuItemFileSaveAs_Click(sender,e);//调用另存为菜单项事件处理函数
    }
    (11) 把SaveFileDialog控件放到窗体中,将自动创建控件对象,其生命周期等于窗体生命周期,将长期占用存储空间。实际上SaveFileDialog控件对象只在存文件菜单项事件处理函数中有用,其它时间无用。为了节约存储空间,可以在存文件菜单项事件处理函数中建立SaveFileDialog控件对象,退出该事件处理函数时,自动释放该对象。修改另存为菜单项事件处理函数如下(首先删除增加的控件SaveFileDialog):
    private void menuItemFileSaveAs_Click(object sender, System.EventArgs e)
    { SaveFileDialog saveFileDialog1=new SaveFileDialog();
    saveFileDialog1.Filter="纯文本文件(*.txt)|*.txt|所有文件(*.*)|*.*";
    saveFileDialog1.FilterIndex=1;
    if(saveFileDialog1.ShowDialog()==DialogResult.OK)
    { s_FileName=saveFileDialog1.FileName;
    richTextBox1.SaveFile(saveFileDialog1.FileName,
    RichTextBoxStreamType.PlainText);
    }//也可以用此方法修改打开文件菜单项事件处理函数。
    }
    (12) 为退出菜单项增加事件处理函数如下:
    private void menuItemExit_Click(object sender, System.EventArgs e)
    { Close();}
    (13) 编译,运行,可以存取文件。
    4.4 修改字体属性
    为修改字体属性,首先打开字体对话框FontDialog,选择指定字体。可以按两种方式修改字体,如果未选中字符,表示以后键入的字符将按选定字体输入。如果选中字符,则仅修改选定字符的字体。修改字符颜色也根据同样原则。
    4.4.1 FontDialog控件属性和方法
    用户可以用FontDialog对话框选定指定字体,FontDialog控件和OpenDialog控件的属性和方法基本相同,这里只介绍不同部分。属性Font:用户用FontDialog对话框选定的字体。FontDialog对话框显示效果如图4.3.1。
    4.4.2 修改字体属性的实现方法
    (14) 放FontDialog控件到窗体,属性Name=fontDialog1。增加顶级菜单项:格式,为格式顶级菜单项的弹出菜单增加菜单项:字体,属性Name分别为mainMenuModel和menuItemModelFont,为字体菜单项增加事件处理函数如下:
    private void menuItemModelFont_Click(object sender, System.EventArgs e)
    { if(fontDialog1.ShowDialog()==DialogResult.OK)
    richTextBox1.SelectionFont=fontDialog1.Font;
    }
    (15) 编译,运行,在选中字符和不选中字符两种情况下,用字体菜单项修改字体,看是否能实现写字板中同样的功能。

    图4.3.1字体对话框
    4.5 实现About对话框
    前边介绍的SaveDialog、OpenDialog和FontDialog都是类库中预先定义的对话框,本节介绍如何创建满足一定要求的自制对话框。对话框其实就是窗体,其基类和主窗体一样,是System.Windows.Forms.Form。只是一般对话框只有关闭按钮,没有最大化和最小化按钮,对话框的边界是固定的,不能改变。设计自己的对话框是经常遇到的工作。
    (16) 选择菜单项项目/添加Windows窗体,弹出对话框(见图4.5),在模板(T)编辑框中选择Windows窗体,在名称栏(N)编辑框中输入窗体文件名称:formAbout.cs,单击打开按钮,可以见到一个新窗体。从文件formAbout.cs可以看到新建窗体类名也为formAbout。
    (17) 修改formAbout属性StartPosition=CenterParent,表示打开对话框时,对话框在父窗口的中间。修改属性MaximizeBox=False,MinimizeBox=False,表示没有最大化和最小化按钮,既不能最大化和最小化。属性FormBorderStyle=FixedDialog,窗口不能修改大小。属性Text="关于记事本"。可以在窗体中增加各种控件,例如,小图标,Label控件等。本例仅增加Label控件表示版权信息,其属性Text="版权所有"。一个按钮,属性Text="确定",按钮单击事件处理函数如下:
    private void button1_Click(object sender,System.EventArgs e)
    {Close();}
    (18) 为Form1窗体增加顶级菜单项:帮助,为帮助顶级菜单项弹出菜单增加菜单项:关于…,属性Name为menuItemAbout。关于…菜单项单击事件处理函数如下:
    private void menuItemAbout_Click(object sender, System.EventArgs e)
    { formAbout AboutDialog=new formAbout();
    AboutDialog.ShowDialog(this);
    }//注意不能使用Show()函数
    (19) 编译,运行,单击关于…菜单项,将出现一个formAbout对话框(如右图),并且不关闭此对话框,不能回到主窗口,一般把这样的对话框叫做模式对话框。


    图4.5
    4.6 实现文本编辑器查找替换功能
    本节首先介绍模式对话框和非模式对话框的概念。并用非模式对话框实现文本编辑器程序的查找和替换功能。
    4.6.1 模式对话框和非模式对话框
    模式对话框和非模式对话框的区别是:打开模式对话框后,只有关闭该模式对话框,才能转到其他窗口,例如前边讲到的SaveDialog和OpenDialog都是典型的模式对话框。而打开非模式对话框后,不必退出该模式对话框,就可以转到其他窗口,例如查找和替换对话框都是典型的非模式对话框。两类对话框本质上都是窗体,是System.Windows.Forms.Form类的派生类,只是打开时使用的方法不一样,打开模式对话框,使用方法ShowDialog(),而打开非模式对话框,使用方法Show()。文本编辑器程序中,查找和替换对话框一般是非模式对话框。
    4.6.2 写字板查找替换功能的实现
    (20) 建立查找替换对话框。对话框其实就是窗体,其基类是System.Windows.Forms.Form。选择菜单项项目/添加Windows窗体,弹出对话框(如图4.5),选择Windows窗体,在名称栏输入窗体文件名称:formFindReplace.cs,单击打开按钮,可以见到一个新窗体。其属性Name=formFindReplace。
    (21) 修改formFindReplace窗体属性StartPosition=CenterParent,表示打开对话框时,对话框在父窗口的中间。修改属性MaximizeBox=False,MinimizeBox=False,表示没有最大化和最小化按钮,既不能最大化和最小化。FormBorderStyle=FixedDialog,窗口不能修改大小。属性Text="查找和替换"。在窗体中增加两个Label控件,属性Text分别为"查找字符串"和"替换字符串"。两个TextBox控件,属性Text=""。两个按钮,属性Text分别为"查找下一个"和"替换查到字符"。修改属性TopMost=true,使该窗口打开时总在其它窗体的前边。对话框界面如右图。
    (22) 为formFindReplace窗体增加变量:Form1 MainForm1;
    (23) 修改formFindReplace类构造函数如下(阴影部分是所做的修改):
    public formAbout(Form1 form1)//增加参数
    {
    //Windows窗体设计器支持所必需的
    InitializeComponent();
    //TODO:在InitializeComponent调用后添加任何构造函数代码
    MainForm1=form1;//新增语句,这里Form1是主窗体的属性Name的值
    }//有了Form1,可以在formFindReplace窗体中调用主窗体的公有方法
    (24) 为主窗体Form1增加方法如下,该方法将被formFindReplace窗体类调用。
    public void FindRichTextBoxString(string FindString)
    {} //以后步骤将在此方法中增加查找语句
    (25) formFindReplace窗体中查找下一个按钮单击事件处理函数如下:
    private void buttonFind_Click(object sender, System.EventArgs e)
    { if(textBox1.Text.Length!=0)//如果查找字符串不为空,调用主窗体查找方法
    MainForm1.FindRichTextBoxString(textBox1.Text);//上步增加的方法
    else
    MessageBox.Show("查找字符串不能为空","提示",MessageBoxButtons.OK);
    }//MessageBox时对话框,使用方法见4.7.1节
    (26) 为主窗体Form1增加方法如下,该方法将被formFindReplace窗体类调用。
    public void ReplaceRichTextBoxString(string ReplaceString)
    {} //以后步骤将在此方法中增加替换语句
    (27) 为替换查到字符按钮单击事件增加事件处理函数如下:
    private void buttonReplace_Click(object sender, System.EventArgs e)
    { if(textBox2.Text.Length!=0)//如果查找字符串不为空,调用主窗体替换方法
    MainForm1.ReplaceRichTextBoxString(textBox1.Text,textBox2.Text);
    else//方法MainForm1.ReplaceRichTextBoxString见(26)中定义
    MessageBox.Show("替换字符串不能为空","提示", MessageBoxButtons.OK);
    }
    (28) 为Form1窗体增加变量:int FindPostion=0,记录查找位置。
    (29) 为Form1窗体顶级菜单项编辑的弹出菜单增加菜单项:查找和替换。为查找和替换菜单项单击事件增加事件处理函数如下:
    private void menuItemFindReplace_Click(object sender, System.EventArgs e)
    { FindPostion=0;
    formAbout FindReplaceDialog=new formAbout(this);//注意this
    FindReplaceDialog.Show();//打开非模式对话框使用Show()方法
    }
    (30) 为在前边定义的Form1主窗体的FindRichTextBoxString方法增加语句如下:
    public void FindRichTextBoxString(string FindString)
    { if(FindPostion>=richTextBox1.Text.Length)//已查到文本底部
    { MessageBox.Show("已到文本底部,再次查找将从文本开始处查找",
    "提示",MessageBoxButtons.OK);
    FindPostion=0;
    return;
    }//下边语句进行查找,返回找到的位置,返回-1,表示未找到,参数1是要找的字符串
    //参数2是查找的开始位置,参数3是查找的一些选项,如大小写是否匹配,查找方向等
    FindPostion=richTextBox1.Find(FindString,
    FindPostion,RichTextBoxFinds.MatchCase);
    if(FindPostion==-1)//如果未找到
    { MessageBox.Show("已到文本底部,再次查找将从文本开始处查找",
    "提示", MessageBoxButtons.OK);
    FindPostion=0;//下次查找的开始位置
    }
    else//已找到
    { richTextBox1.Focus();//主窗体成为注视窗口
    FindPostion+=FindString.Length;
    }//下次查找的开始位置在此次找到字符串之后
    }
    (31) 为在前边定义的Form1主窗体的ReplaceRichTextBoxString方法增加语句如下:
    public void ReplaceRichTextBoxString(string ReplaceString)
    { if(richTextBox1.SelectedText.Length!=0)//如果选取了字符串
    richTextBox1.SelectedText=ReplaceString;//替换被选的字符串
    }
    (32) 编译,运行,输入若干字符,选中菜单项:编辑/查找和替换,打开对话框,注意该对话框可以在不关闭的情况下,转到主窗体,并且总是在其它窗体的前边,因此它是一个典型的非模式对话框。在对话框中输入查找和替换的字符,单击标题为查找下一个的按钮,可以找到所选字符,并被选中,单击标题为替换所选字符按钮,可以看到查找到的字符被替换。运行效果如右图:
    4.7 提示用户保存修改的文件
    用户在新建文本,打开其他文本或者退出文本编辑器时,如果编辑内容发生了改变,应提示用户是否保存已修改的文本内容。因此就需要在用户关闭当前文件前,弹出提示对话框,提醒用户是否保存当前文件。本节实现此功能。
    4.7.1 对话框MessageBox
    使用MessageBox可以打开一个对话框,用法如下:
    MessageBox.Show(this,"要保存当前更改吗?","保存更改吗?",
    MessageBoxButtons.YesNoCancel,MessageBoxIcon.Question);
    第一个参数是父窗口,第二个参数是提示信息,第三个参数是标题栏的内容,第四个参数是有那些按钮,此例有YES,NO,CANCEL按钮,还可以使用AbortRetryIgnore(中止、重试和忽略按钮)、OK(确定按钮)、OKCancel(确定和取消按钮)、RetryCance(重试和忽略按钮)、YesNo(是和否按钮)等选项。第五个参数是使用那一个图标,此例是一个问号图标,还可以是Asterisk、Error、Exclamation、Hand、Stop、Warning等图标,如为None则无图标。返回值是System.Windows.Forms.DialogResult变量,代表用户按了那一个按钮。如果返回值是System.Windows.Forms.DialogResult.Yes,则表示按了YES键,表示要存修改的文件。如果返回值是System.Windows.Forms.DialogResult.Cancel,按Cancel键,表示忽略此次操作。如果返回值是System.Windows.Forms.DialogResult.No,则表示按了No键,表示不存修改的文件。以上设计的对话框MessageBox如下图:

    4.7.2 提示用户保存修改的文件的实现
    (33) 为Form1类增加一个bool变量bSave=false作为标记,用来跟踪RichTextBox中文本内容改变的情况。在程序开始运行、建立和打开一个新文件时,bSave=false,表示不必保存当前文本。RichTextBox控件有一个TextChanged事件,当文本发生改变的时候,这个事件就会被激活,在该事件处理函数中,使bSave=true。
    (34) 首先增加一个函数,其功能是判断是否需要将已修改的文件存盘,之所以要增加这个函数是因为有三处要用到此函数。该函数返回true,表示继续操作,该函数返回false,表示忽略此次操作,该函数定义如下:
    public bool IfSaveOldFile()
    { bool ReturnValue=true;
    if(bSave)
    { System.Windows.Forms.DialogResult dr;
    dr=MessageBox.Show(this,"要保存当前更改吗?","保存更改吗?",
    MessageBoxButtons.YesNoCancel,MessageBoxIcon.Question);
    switch(dr)
    { case System.Windows.Forms.DialogResult.Yes://单击了yes按钮,保存修改
    bSave=false;
    if(s_FileName.Length!=0)
    richTextBox1.SaveFile(s_FileName,RichTextBoxStreamType.PlainText);
    else
    { SaveFileDialog saveFileDialog1=new SaveFileDialog();
    saveFileDialog1.Filter="纯文本文件(*.txt)|*.txt|所有文件(*.*)|*.*";
    saveFileDialog1.FilterIndex=1;
    if(saveFileDialog1.ShowDialog()==DialogResult.OK)
    { s_FileName=saveFileDialog1.FileName;
    richTextBox1.SaveFile(saveFileDialog1.FileName,
    RichTextBoxStreamType.PlainText);
    }
    }
    ReturnValue=true;
    break;
    case System.Windows.Forms.DialogResult.No://单击了no按钮,不保存
    bSave=false;
    ReturnValue=true;
    break;
    case System.Windows.Forms.DialogResult.Cancel://单击了Cancel按钮
    ReturnValue=false;
    break;
    }
    }
    return ReturnValue;
    }
    (35) 在新建和打开菜单项的事件处理函数的头部增加如下语句:
    if(!IfSaveOldFile())//如果忽略,退出。
    return;
    (36) 修改存文件菜单项单击事件处理函数如下:
    private void menuItemSaveFile_Click(object sender, System.EventArgs e)
    { if(s_FileName.Length!=0)
    { bSave=false;//阴影为增加的语句
    richTextBox1.SaveFile(s_FileName,RichTextBoxStreamType.PlainText);
    }
    else
    menuItemSaveAs_Click(sender,e);
    }
    (37) 修改另存为菜单项单击事件处理函数如下:
    private void menuItemSaveAs_Click(object sender, System.EventArgs e)
    { SaveFileDialog saveFileDialog1=new SaveFileDialog();
    saveFileDialog1.Filter="纯文本文件(*.txt)|*.txt|所有文件(*.*)|*.*";
    saveFileDialog1.FilterIndex=1;
    if(saveFileDialog1.ShowDialog()==DialogResult.OK)
    { s_FileName=saveFileDialog1.FileName;
    richTextBox1.SaveFile(saveFileDialog1.FileName,
    RichTextBoxStreamType.PlainText);
    bSave=false;//阴影为增加的语句
    }
    }
    (38) 为RichTextBox控件TextChanged事件增加事件处理函数如下:
    private void richTextBox1_TextChanged(object sender, System.EventArgs e)
    { bSave=true;}
    (39) 为Form1窗体Closing事件是在关闭窗口之前发送的事件,此时,窗体中的控件还存在,还可以保存修改的内容,也可以不退出。增加它的事件处理函数如下:
    private void Form1_Closing(object sender,System.ComponentModel.CancelEventArgs e)
    { if(!IfSaveOldFile())
    e.Cancel=true;//不退出
    }
    (40) 编译,运行,键入若干字符,选中菜单项新建或打开,或退出,将看到提示信息,问是否保存修改的文件。有三种选择:存文件,不存文件,忽略此次操作,试验单击不同按钮的效果。
    4.8 打印和打印预览
    打印和打印预览是一个编辑器必须具有的功能,本节介绍实现打印和打印预览的方法。一般要实现如下菜单项:打印、打印预览、页面设置。
    4.8.1 PrintDocument类
    PrintDocument组件是用于完成打印的类,其常用属性、方法和事件如下:
     属性DocumentName:字符串类型,记录打印文档时显示的文档名(例如,在打印状态对话框或打印机队列中显示)。
     方法Print:开始文档的打印。
     事件BeginPrint:在调用Print方法后,在打印文档的第一页之前发生。
     事件PrintPage:需要打印新的一页时发生。
     事件EndPrint:在文档的最后一页打印后发生。
    若要打印,首先创建PrintDocument组件的对象。然后使用页面设置对话框PageSetupDialog设置页面打印方式,这些设置作为要打印的所有页的默认设置。使用打印对话框PrintDialog设置对文档进行打印的打印机的参数。在打开两个对话框前,首先设置对话框的属性Document为指定的PrintDocument类对象,修改的设置将保存到PrintDocument组件对象中。第三步是调用PrintDocument.Print方法来实际打印文档。当调用该方法后,引发下列事件:BeginPrint、PrintPage、EndPrint。其中每打印一页都引发PrintPage事件,打印多页,要多次引发PrintPage事件。完成一次打印,可以引发一个或多个PrintPage事件。
    程序员应为这3个事件编写事件处理函数。BeginPrint事件处理函数进行打印初始化,一般设置在打印时所有页的相同属性或共用的资源,例如所有页共同使用的字体、建立要打印的文件流等。PrintPage事件处理函数负责打印一页数据。EndPrint事件处理函数进行打印善后工作。这些处理函数的第2个参数System.Drawing.Printing.PrintEventArgs e提供了一些附加信息,主要有:
     e.Cancel:布尔变量,设置为true,将取消这次打印作业。
     e.Graphics:所使用的打印机的设备环境,参见第五章。
     e.HasMorePages:布尔变量。PrintPage事件处理函数打印一页后,仍有数据未打印,退出事件处理函数前设置HasMorePages=true,退出PrintPage事件处理函数后,将再次引发PrintPage事件,打印下一页。
     e.MarginBounds:打印区域的大小,是Rectangle结构,元素包括左上角坐标:Left和Top,宽和高:Width和Height。单位为1/100英寸。
     e.MarginBounds:打印纸的大小,是Rectangle结构。单位为1/100英寸。
     e.PageSettings:PageSettings类对象,包含用对话框PageSetupDialog设置的页面打印方式的全部信息。可用帮助查看PageSettings类的属性。
    下边为这3个事件编写事件处理函数,具体步骤如下:
    (41) 在最后一个using语句之后增加语句:
    using System.IO;
    using System.Drawing.Printing;
    (42) 本例打印或预览RichTextBox中的内容,增加变量:StringReader streamToPrint=null。如果打印或预览文件,改为:StreamReader streamToPrint,流的概念参见第六章。增加打印使用的字体的变量:Font printFont。
    (43) 放PrintDocument控件到窗体,属性name为printDocument1。
    (44) 为printDocument1增加BeginPrint事件处理函数如下:
    private void printDocument1_BeginPrint(object sender,
    System.Drawing.Printing.PrintEventArgs e)
    { printFont=richTextBox1.Font;//打印使用的字体
    streamToPrint=new StringReader(richTextBox1.Text);//打印richTextBox1.Text
    }//如预览文件改为:streamToPrint=new StreamReader("文件的路径及文件名");
    (45) printDocument1的PrintPage事件处理函数如下。streamToPrint.ReadLine()读入一段数据,可能打印多行。本事件处理函数将此段数据打印在一行上,因此方法必须改进。
    private void printDocument1_PrintPage(object sender,
    System.Drawing.Printing.PrintPageEventArgs e)
    { float linesPerPage=0;//记录每页最大行数
    float yPos=0;//记录将要打印的一行数据在垂直方向的位置
    int count=0;//记录每页已打印行数
    float leftMargin=e.MarginBounds.Left;//左边距
    float topMargin=e.MarginBounds.Top;//顶边距
    string line=null;//从RichTextBox中读取一段字符将存到line中
    //每页最大行数=一页纸打印区域的高度/一行字符的高度
    linesPerPage=e.MarginBounds.Height/printFont.GetHeight(e.Graphics);
    //如果当前页已打印行数小于每页最大行数而且读出数据不为null,继续打印
    while(count<linesPerPage&&((line=streamToPrint.ReadLine())!=null))
    { //yPos为要打印的当前行在垂直方向上的位置
    yPos=topMargin+(count*printFont.GetHeight(e.Graphics));
    e.Graphics.DrawString(line,printFont,Brushes.Black,
    leftMargin,yPos,new StringFormat());//打印,参见第五章
    count++;//已打印行数加1
    }
    if(line!=null)//是否需要打印下一页
    e.HasMorePages=true;//需要打印下一页
    else
    e.HasMorePages=false;//不需要打印下一页
    }
    (46) 为printDocument1增加EndPrint事件处理函数如下:
    private void printDocument1_EndPrint (object sender,
    System.Drawing.Printing.PrintEventArgs e)
    { if(streamToPrint!=null)
    streamToPrint.Close();//释放不用的资源
    }
    4.8.2 打印设置对话框控件PageSetupDialog
    Windows窗体的PageSetupDialog控件是一个页面设置对话框,用于在Windows应用程序中设置打印页面的详细信息,对话框的外观如图4.8.2。

    图4.8.2
    用户使用此对话框能够设置纸张大小(类型)、纸张来源、纵向与横向打印、上下左右的页边距等。在打开对话框前,首先设置其属性Document为指定的PrintDocument类对象,用来把页面设置保存到PrintDocument类对象中。为文本编辑器增加页面设置功能的具体步骤如下:
    (47) 为文件顶级菜单项的弹出菜单增加菜单项:页面设置。
    (48) 放PageSetupDialog控件到窗体,属性name为pageSetupDialog1。
    (49) 为页面设置菜单项增加单击事件处理函数如下:
    private void menuItem5_Click(object sender,System.EventArgs e)
    { pageSetupDialog1.Document=printDocument1;
    pageSetupDialog1.ShowDialog();
    }
    (50) 打开对话框pageSetupDialog1后,如果单击了确定按钮,PageSetupDialog对话框中所做的的页面设置被保存到PrintDocument类对象printDocument1中,如果单击了取消按钮,不保存这些修改,维持原来的值。当调用PrintDocument.Print方法来实际打印文档时,引发PrintPage事件,该事件处理函数的第二个参数e提供了这些设置信息。
    4.8.3 打印预览
    用PrintPreviewDialog类可以在屏幕上显示PrintDocument的打印效果,既打印预览。实现打印预览的具体步骤如下:
    (51) 为文件顶级菜单项的弹出菜单增加菜单项:打印预览。
    (52) 放PrintPreviewDialog控件到窗体,属性name为printPreviewDialog1。
    (53) 为打印预览菜单项增加单击事件处理函数如下:
    private void menuItemPrintView_Click(object sender,System.EventArgs e)
    { printPreviewDialog1.Document=printDocument1;
    printPreviewDialog1.ShowDialog();
    }
    (54) 编译,运行,输入若干字符,试验一下预览的效果,预览的效果如图4.8.3。

    图4.8.3
    4.8.4 用打印对话框PrintDialog实现打印
    PrintDialog组件是类库中预先定义的对话框,用来设置对文档进行打印的打印机的参数,包括打印机名称、要打印的页(全部打印或指定页的范围)、打印的份数以及是否打印到文件等。在打开对话框前,首先设置其属性Document为指定的PrintDocument类对象,打开PrintDialog对话框后,修改的设置将保存到PrintDocument类的对象中。PrintDialog对话框的外观如图4.8.4。

    图4.8.4
    增加打印功能的具体步骤如下:
    (55) 放PrintDialog控件到窗体属性Name=printDialog1。
    (56) 为文件顶级菜单项的弹出菜单增加菜单项:打印。
    (57) 为打印菜单项增加单击事件处理函数如下:(不能打印?)
    private void menuItemPrint_Click(object sender, System.EventArgs e)
    { printDialog1.Document=printDocument1;
    if(printDialog1.ShowDialog(this)==DialogResult.OK)
    printDocument1.Print();
    }
    (58) 编译,运行,输入若干字符,试验一下打印效果。
    4.9 编写多文档界面应用程序
    本节首先介绍如何建立类似Microsoft Word的文本编辑器,然后介绍如何建立类似Visualstudio.Net的编辑器那样的文本编辑器,有多个选项卡页。
    4.9.1 建立类似Microsoft Word的编辑器
    建立一个类似Microsoft Word的编辑器,可以有多页,每页处理一个文档。多文档界面(MDI)应用程序具有一个主窗体(父窗体),主窗体在其工作区内包含一组窗体(子窗体)。每个子窗体都是一个限制为只能在该父窗体内出现的窗体。这些子窗体通常共享父窗体界面的菜单栏、工具栏以及其他部分。创建多文当编辑器的具体步骤如下:
    (1) 新建项目。修改主窗体属性IsMdiContainer=true,表示主窗体是一个子窗体容器。
    (2) 放主菜单控件Mainmenu到主窗体。增加顶级菜单项:文件,属性Name=menuItemFile。为文件菜单增加菜单项:新建、打开、另存为、关闭当前窗口、退出,属性Name分别为menuItemNew、menuItemOpen、menuItemSaveAs、menuItemCloseChild、menuItemExit。增加顶级菜单项:窗口,属性Name=menuItemWindow,属性MdiList=true,该属性将在窗口菜单下增加子窗口列表。为窗口菜单增加菜单项:水平平铺、层叠、垂直平铺,属性Name分别为menuItemTileH、menuItemCascade、menuItemTileV。
    (3) 创建子窗体,选择菜单项:项目/添加Windows窗体,弹出对话框(见图4.5),选择Windows窗体,在名称栏输入窗体文件名称:FormChild.cs,单击打开按钮,可以见到一个新窗体。定义新窗体的类名也为FormChild。此窗体作为主窗体的子窗体。
    (4) 放RichTextBox1控件到子窗体。修改属性Dock=Fill,Text="",Modifiers=public,使RichTextBox1为公有成员,在主窗体可以访问RichTextBox1。
    (5) 为主窗体菜单项新文件增加单击事件处理函数如下:
    private void menuItemNew_Click(object sender,System.EventArgs e)
    { FormChild formChild=new FormChild();
    formChild.MdiParent=this;
    formChild.Show();
    }
    (6) 把OpenFileDialog控件放到窗体中。单击打开文件菜单项事件处理函数如下:
    private void menuItemOpen_Click(object sender, System.EventArgs e)
    { if(openFileDialog1.ShowDialog(this)==DialogResult.OK)
    { FormChild ChildForm=new FormChild();
    ChildForm.MdiParent=this;
    ChildForm.richTextBox1.LoadFile(openFileDialog1.FileName,
    RichTextBoxStreamType.PlainText);
    ChildForm.Show();
    }
    }
    (7) 把SaveFileDialog控件放到子窗体中。另存为菜单项事件处理函数如下:
    private void menuItemChildSaveAs_Click(object sender, System.EventArgs e)
    { if(saveFileDialog1.ShowDialog(this)==DialogResult.OK)
    { FormChild ChildForm=(FormChild)this.ActiveMdiChild;
    ChildForm.richTextBox1.SaveFile(saveFileDialog1.FileName,
    RichTextBoxStreamType.PlainText);
    }
    }
    (8) 为主窗体菜单项关闭当前窗口增加单击事件函数如下:
    private void menuItemCloseChild_Click(object sender, System.EventArgs e)
    { this.ActiveMdiChild.Close();}
    (9) 为主窗体菜单项退出增加单击事件函数如下:
    private void menuItemExit_Click(object sender, System.EventArgs e)
    { Close();}
    (10) 为主窗体菜单项水平平铺增加单击事件函数如下:
    private void menuItemTileH_Click(object sender, System.EventArgs e)
    { this.LayoutMdi(MdiLayout.TileHorizontal);}
    (11) 为主窗体菜单项层叠增加单击事件函数如下:
    private void menuItemCascade_Click_1(object sender, System.EventArgs e)
    { this.LayoutMdi(MdiLayout.Cascade);}
    (12) 为主窗体菜单项垂直平铺增加单击事件函数如下:
    private void menuItemTileV_Click(object sender, System.EventArgs e)
    { this.LayoutMdi(MdiLayout. TileVertical);}
    (13) 运行,运行效果如下,子窗体为层叠排列。

    4.9.2 主窗口和子窗口的菜单的融合
    在许多多文档编辑器应用程序中,在没有子窗体打开时,菜单比较简单,而有子窗体打开后,菜单增多。实现这种功能一般是在主窗体中创建一个简单菜单,子窗体没打开时,只显示这个简单菜单。在子窗体中也创建一个菜单,包含主窗体菜单中没有的菜单项。打开子窗体后,子窗体的菜单和主窗体菜单合并成为一个菜单,这个功能叫做主窗口和子窗口的菜单的融合。创建具有这种功能的多文档编辑器应用程序可以按下列步骤:
    (1) 新建项目。修改主窗口属性IsMdiContainer为true。
    (2) 把Mainmenu控件放到主窗体中。增加顶级菜单项:文件。其属性MergeType=MergeItems,表示打开子窗体后,主窗体和子窗体中属性MergeOrder相同的顶级菜单项的弹出菜单中的菜单项合并为一个弹出菜单。属性MergeOrder=0。子窗体的顶级菜单项文件的属性MergeType也应为MergeItems,MergeOrder属性也应为0,这样打开子窗口后,才能合并为一个弹出菜单。属性Name=menuItemFile。为文件菜单增加菜单项:新建、打开、退出,属性Name分别为menuItemNew、menuItemOpen、menuItemExit,属性MergeType都为Add,属性MergeOrder依次为1、2、6,目的是打开子窗口后,在新建和打开菜单项后加入子窗口菜单栏中的文件菜单的另存为菜单项。增加菜单:帮助,其属性MergeType=Add,属性MergeOrder=7,属性Name=menuItemHelp。为帮助菜单增加菜单项:关于…,属性Name=menuItemAbout。其余菜单在子窗口中实现。注意属性MergeOrder分别为0、7,打开子窗口后,子窗口中的菜单将按顺序插入到主窗口的菜单中,例如,子窗口有菜单:编辑,其属性MergeOrder=3,合并后,菜单排列顺序为:文件、编辑、帮助。
    (3) 创建子窗体,选择菜单项:项目/添加Windows窗体,弹出对话框,选择Windows窗体,在名称栏输入窗体文件名称:formChild.cs,单击打开按钮,可以见到一个新窗体。定义新窗体的类名也为formChild。
    (4) 为formChild窗体增加变量:Form1 MainForm1;
    (5) 修改formChild类构造函数如下(阴影部分是所做的修改):
    public formChild(Form1 form1)//增加参数
    {
    //Windows窗体设计器支持所必需的
    InitializeComponent();
    //TODO:在InitializeComponent调用后添加任何构造函数代码
    MainForm1=form1;//新增语句,这里Form1是主窗体的属性Name的值
    }//有了Form1,可以在formChild窗体中调用主窗体的公有方法
    (6) 把Mainmenu控件放到子窗体中。增加顶级菜单项:文件,其属性MergeType=MergeItems,属性MergeOrder=0。为文件顶级菜单项弹出菜单增加菜单项:另存为…,属性MergeType=Add,属性MergeOrder=3,菜单合并后,另存为…菜单项将出现在主窗口文件菜单的新建和打开菜单项之后。增加菜单项:关闭当前文件,属性MergeType=Add,属性MergeOrder=4。
    (7) 增加顶级菜单项:编辑,其属性MergeType=Add,属性MergeOrder=3。注意属性MergeOrder=3,菜单合并后,编辑菜单将出现出现菜单文件之后。为编辑顶级菜单项弹出菜单增加菜单项:拷贝、剪贴、粘贴。
    (8) 增加顶级菜单项:窗口,其属性MergeType=Add,属性MergeOrder=6,属性Name=menuItemWindow,属性MdiList=true,该属性将在窗口菜单下增加子窗口列表。为窗口菜单增加菜单项:层叠,属性Name为menuItemCascade。
    (9) 放RichTextBox1控件到子窗体。修改属性Dock=Fill,Text=””,属性 Modifiers=public,使RichTextBox1为公有成员,在主窗体可以访问RichTextBox1。
    (10) 为主窗体菜单项新文件增加单击事件函数如下:
    private void menuItemNew_Click(object sender, System.EventArgs e)
    { formChild ChildForm=new formChild(this);
    ChildForm.MdiParent=this;
    ChildForm.Show();
    }
    (11) 把OpenFileDialog控件放到主窗体中。单击打开文件菜单项事件处理函数如下:
    private void menuItemOpen_Click(object sender, System.EventArgs e)
    { if(openFileDialog1.ShowDialog(this)==DialogResult.OK)
    { formChild ChildForm=new formChild(this);
    ChildForm.MdiParent=this;
    ChildForm.richTextBox1.LoadFile(openFileDialog1.FileName);
    ChildForm.Show();
    }
    }
    (12) 为主窗体菜单项退出增加单击事件处理函数如下:
    private void menuItemExit_Click(object sender, System.EventArgs e)
    { Close();}
    (13) 为子窗体菜单项层叠增加单击事件处理函数如下:
    private void menuItemCascade_Click_1(object sender, System.EventArgs e)
    { MainForm1.LayoutMdi(MdiLayout.Cascade);}
    (14) 把SaveFileDialog控件放到子窗体中。为子窗体菜单项另存为增加单击事件函数如下:
    private void menuItemChildSaveAs_Click(object sender, System.EventArgs e)
    { if(saveFileDialog1.ShowDialog(this)==DialogResult.OK)
    { richTextBox1.SaveFile(saveFileDialog1.FileName,
    RichTextBoxStreamType.PlainText);
    }
    }
    (15) 为子窗体菜单项关闭当前窗口增加单击事件函数如下:
    private void menuItemCloseChild_Click(object sender, System.EventArgs e)
    { Close();}
    (16) 为子窗体菜单项拷贝、剪贴和粘贴增加单击事件函数如下:
    语句分别为:richTextBox1.Cut();richTextBox1.Copy();richTextBox1.Paste();
    (17) 运行效果如下图。


    4.9.3 建立类似Visualstudio.Net的编辑器
    Visualstudio.Net的编辑器有多个选项卡页,可以编辑多个文件。建立选项卡页数固定,每选项卡页显示一行文本,类似Visualstudio.Net的编辑器的文本编辑器的具体实现步骤如下:
    (1) 新建项目。放TabControl控件到子窗体。修改属性Dock=Fill。
    (2) 单击TabControl属性TabPages后按钮,打开TabPage集合编辑器,单击添加按钮,增加1个选项卡页。修改属性Text分别为:第一页,第二页。如图4.9.3。
    (3) 选中第一页,可以在页中放置控件,例如放置Label控件,属性Text=”这是第一个选项卡页”。同样在第二页中也放置Label控件,属性Text=”这是第二个选项卡页”。如果放置RichTextBox控件,可以做成多文档编辑器。
    (4) 运行,可以看到多页,单击每页的标题,可以转换选项卡页。运行效果如右图:

    图4.9.3
    如可以有多个选项卡页,每选项卡页处理一个文档,并能动态增加新选项卡页,关闭当前选项卡页。实现步骤如下:
    (1) 新建项目。放TabControl控件到子窗体。修改属性Dock=Fill。
    (2) 把Mainmenu控件放到主窗体中。增加顶级菜单项:文件,为其弹出菜单增加4个菜单项:新页、关闭当前页、打开、另存为。属性Name分别为:menuItemFile、menuItemFileNew、menuItemFileClose、menuItemFileOpen、menuItemFileSaveAs。
    (3) 增加一个新方法MakeNewTbpage()如下:
    private object MakeNewTbpage()
    { //增加选项卡页TabPage
    TabPage tabPage1=new TabPage();
    tabControl1.Controls.Add(tabPage1);//将tabPage1放到tabControl1中
    tabPage1.Location=new Point(4, 21);
    tabPage1.Size=new Size(284, 248);
    tabPage1.Text="第"+tabPage1.TabIndex.ToString()+"页";
    //增加RichTextBox
    RichTextBox richTextBox1=new RichTextBox();
    richTextBox1.Dock=DockStyle.Fill;
    richTextBox1.Size=new Size(284, 248);
    richTextBox1.Text="";
    tabPage1.Controls.Add(richTextBox1);//将richTextBox1放到tabPage1中
    return (object)richTextBox1;
    }
    (4) 为菜单项新页增加事件处理函数如下:
    private void menuItemFileNew_Click(object sender, System.EventArgs e)
    { MakeNewTbpage();}
    (5) 为菜单项关闭当前页增加事件处理函数如下:
    private void menuItemFileClose_Click(object sender, System.EventArgs e)
    { TabPage tabPage1=tabControl1.SelectedTab;//得到当前选定的选项卡页
    tabControl1.Controls.Remove(tabPage1);//从tabControl1中移走该页
    //得到当前选定的选项卡页中第0个控件,即RichTextBox控件
    RichTextBox richTextBox1=(RichTextBox)tabPage1.Controls[0];
    if(richTextBox1!=null)
    richTextBox1.Dispose();//删除当前选定选项卡页中RichTextBox控件对象
    if(tabPage1!=null)
    tabPage1.Dispose();//删除当前选定的选项卡页
    }
    (6) 把OpenFileDialog控件放到子窗体中。为菜单项打开增加事件处理函数如下:
    private void menuItemFileOpen_Click(object sender, System.EventArgs e)
    { if(openFileDialog1.ShowDialog()==DialogResult.OK)
    { RichTextBox richTextBox1=(RichTextBox)MakeNewTbpage();
    richTextBox1.LoadFile(openFileDialog1.FileName,
    RichTextBoxStreamType.PlainText);
    }
    }
    (7) 把SaveFileDialog控件放到子窗体中。为菜单项另存为增加事件处理函数如下:
    private void menuItemFileSaveAs_Click(object sender, System.EventArgs e)
    { if(saveFileDialog1.ShowDialog()==DialogResult.OK)
    { TabPage tabPage1=tabControl1.SelectedTab;
    RichTextBox richTextBox1=(RichTextBox)tabPage1.Controls[0];
    richTextBox1.SaveFile(saveFileDialog1.FileName,
    RichTextBoxStreamType.PlainText);
    }
    }
    (8) 编译,运行,建立新文件,关闭当前选项卡页,打开新文件,存文件,开是否正常。
    习题
    (1) RichTextBox控件Lines属性记录控件中所有文本的字符串数组,每两个回车之间字符串是数组的一个元素。定义一个数组,将属性Lines 中的内容存到这个数组中。(提示:string[] s=new string [richTextBox1.Lines.Length];s= richTextBox1.Lines)
    (2) 为设计的单文档写字板增加工具栏,实现建新文件,打开文件,存文件等功能。
    (3) 在工具栏中,增加2个下拉列表文本框,一个选择使用的字体,一个选择字体的字号。
    (4) 在工具栏中,增加3个按钮,分别设定字符为黑体,斜体,增加下划线。
    (5) 在工具栏中,增加1个按钮,用ColorDialog对话框选择字体的颜色。
    (6) 如何实现全选菜单项。
    (7) RichTextBox控件的属性Modified可以指示用户是否修改文本框控件的内容。请修改4.7节程序,使用属性Modified判断用户是否修改了RichTextBox控件中文本内容。
    (8) 在查找对话框中,增加两个多选框,选择是否允许反向查和区分大小写,并实现反向查找和不区分大小写查找。
    (9) 在实现打开文件和另存为功能时,使用属性InitialDirectory和属性DefaultExt。
    (10) RichTextBox控件的属性SelectionAlignment表示段落的对齐方式,在工具栏中增加三个按钮,分别实现段落的左对齐(HorizontalAlignment.Left)、右对齐(HorizontalAlignment.Right)、中间对齐(HorizontalAlignment.Center)。(提示:3个按钮应是互斥的,用分隔符表示3个按钮是一组。工具条按钮属性Style设置为ToolBarButtonStyle.Separator,则按钮将显示为一个按钮分隔符,而不是按钮。)
    (11) RichTextBox控件的属性SelectionCharOffset可以设定选中的字符为上下标,可以为一个整数,为0表示正常字符,负数表示下标,正数表示上标。请在工具条中增加三个按钮,分别实现上下标功能。
    (12) 请实现完整的单文档编辑器,具有前边介绍的单文档编辑器的功能。
    (13) 请实现完整的多文档编辑器,具有前边介绍的单文档编辑器的功能。
    (14) 请实现完整的选项卡式多文档文本编辑器,具有前边介绍的单文档编辑器的功能。


    第五章 图形图像编程
    本章的目的是介绍图形图像编程的方法,希望在学了本章以后,能编制象Windows画图那样的程序。本章的重点是学习Graphics类中对象(象笔、刷子等)及各种方法的使用,以及Bitmap类的应用。
    5.1 图形设备环境接口(GDI)
    为了在Windows窗口输出数据(字符或图形),Windows操作系统提供了一些工具和函数,例如提供笔用来定义图形外轮廓线的颜色及粗细,提供刷子定义添充封闭图形内部的颜色和格式,提供不同输出字体,提供函数用来输出字符或绘制图形等等。所有这些工具和函数被放在图形设备接口函数库中(GDI32.DLL),它负责CRT显示及打印。根据设备不同,可以构造不同的设备环境(GDI),使输出图形或字符与设备无关,既无论是在CRT显示还是在打印机上打印同一个图形或字符,都用相同的函数。GDI所扮演的角色如下图所示:

    用户应用程序根据是在CRT显示还是在打印机打印,首先选择CRT显示设备环境或打印设备环境,然后调用GDI中的同名函数实现在CRT显示或在打印机上打印。而GDI设备环境根据选择的不同设备,调用不同的设备驱动程序,在CRT上显示或在打印机上打印。而这些驱动程序都是各个设备制造厂商提供的。这样做的最大好处是应用程序和设备无关,应用程序不必为不同的设备编制不同的程序。为使用不同的设备,无论是不同的显卡,还是不同的打印机,只要安装该设备的驱动程序,应用程序就可以使用该设备,微软的Word程序可以使用不同的打印机就是使用了这个原理。
    .NET系统的基础类库(.Net FrameWork)对Windows操作系统的图形设备接口函数库(GDI32.DLL)进行了扩充,并用类进行了封装,一般叫做GDI+。使用GDI+绘图更加方便快捷。为了使用GDI+图形功能,必须引入以下命名空间:System.Drawing,System.Drawing.Priniting,System.Drawing.Imaging,System.Drawing.Drawing2D,System.Drawing.Design,System.Drawing.Text。
    5.2 Graphics类
    System.Drawing.Graphics类对GDI+进行了封装,Graphics类提供一些方法完成各种图形的绘制。Graphics类对象与特定的设备关联,为了在不同的设备上用完全相同的代码完成同样的图形,应根据不同的设备建立不同的Graphics类对象。Graphics类是密封类,不能有派生类。
    5.2.1 使用Graphics类绘图的基本步骤
    GDI+大部分功能被封装在Graphics类中,Graphics类提供了一些工具和函数,例如提供笔用来定义图形外轮廓线的颜色及粗细,提供刷子定义添充封闭图形内部的颜色和格式,提供不同输出字体,提供函数用来输出字符或绘制图形等等。为了在窗体中或其它控件中使用这些工具和函数绘图,必须首先得到这些窗体或控件的使用的Graphics类对象。下面的例子,在窗体中增加了一个按钮,单击按钮将在窗体中画一个边界为红色,内部填充蓝色的圆。该程序段说明了使用Graphics类绘图的基本步骤。按钮的单击事件处理函数如下:
    private void button1_Click(object sender,System.EventArgs e)
    { Graphics g=this.CreateGraphics();//得到窗体使用的Graphics类对象
    Pen pen1=new Pen(Color.Red);//创建红色笔对象
    SolidBrush brush1=new SolidBrush(Color.Blue);//创建蓝色刷子对象
    g.DrawEllipse(pen1,10,10,100,100);//用红色笔画圆的边界
    g.FillEllipse(brush1,10,10,100,100);//用蓝色刷子填充圆的内部
    }
    运行后,单击按钮,出现边界为红色,内部填充为蓝色的圆。
    5.2.2 窗体的Paint事件
    运行上例,单击按钮,出现边界为红色,内部填充蓝色的圆。最小化后,再最大化,图形不见了。这是因为用户Form窗体用户区内容可能被破坏,例如窗体最小化后,再最大化,菜单被打开再关闭,打开对话框再关闭等,用户区内容被覆盖。Windows并不保存被破坏的用户区内容,而是由应用程序自己恢复被破坏的用户区的内容。当应用程序窗口用户区内容被破坏后需恢复时,Windows操作系统向应用程序发送Paint事件,应用程序应把在窗口用户区输出数据的语句放在Paint事件处理函数中,Windows发Paint事件时,能调用这些在窗口用户区输出数据的语句恢复被破坏的内容。Form窗体不能自动响应Paint事件,程序员必须生成Paint事件处理函数。修改上例,增加Form窗体的Paint事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Graphics g=e.Graphics;//得到窗体的使用的Graphics类对象
    Pen pen1=new Pen(Color.Red);
    SolidBrush brush1=new SolidBrush(Color.Blue);
    g.DrawEllipse(pen1,10,10,100,100);
    g.FillEllipse(brush1,10,10,100,100);
    }
    运行后,出现边界为红色,内部填充蓝色的圆。最小化后,再最大化,图形不消失。
    5.3 GDI+中三种坐标系统:
    GDI+定义了三种坐标系统,并提供了三种坐标转换的方法Graphics.TransformPoints()。
     全局坐标系统。
     页面(Page)坐标系统:左上角为原点,横向x轴向右为正方向,纵向y轴向下为正方向。单位为像素。这是默认的坐标系统。
     设备坐标系统:可以指定特定测量单位的页面(Page)坐标系统。如果单位为像素,和页面(Page)坐标系统相同。
    5.4 GDI+中常用的结构
    本节介绍GDI+中常用的结构,包括:Point、PointF、Size、SizeF、Rectangle、RectangleF、Color等。它们都在名字空间System.Drawing中定义的。
    5.4.1 结构Point和PointF
    点结构有两个成员:X,Y,表示点的x轴和y轴的坐标。其常用构造函数如下:
    Point p1=new Point(int X,int Y);//X,Y为整数
    PointF p2=new PointF(float X,floa Y);//X,Y为浮点数
    5.4.2 结构Size和SizeF
    Size和SizeF用来表示尺寸大小,有两个成员:Width和Height。常用构造函数如下:
    public Size(int width,int height);
    public SizeF(float width,float height);
    5.4.3 结构Rectangle和RectangleF
    结构Rectangle和RectangleF用来表示一个矩形,常用属性如下:
     Top:Rectangle结构左上角的y坐标。
     Left:Rectangle结构左上角的x坐标。
     Bottom:Rectangle结构右下角的y坐标。
     Right:Rectangle结构右下角的x坐标。
     Width:获取或设置此Rectangle结构的宽度。
     Height:获取或设置此Rectangle结构的高度。
     Size:获取或设置此Rectangle的大小。
     X:获取或设置此Rectangle结构左上角的x坐标。
     Y:获取或设置此Rectangle结构左上角的y坐标。
    其常用构造函数为:
    //参数为矩形左上角坐标的点结构location和代表矩形宽和高的Size结构size
    Rectangle(Point location,Size size);//参数也可为PointF和SizeF
    //参数为矩形左上角x和y坐标,宽,高
    Rectangle(int X,int Y,int width,int height);//X和Y也可为float
    5.4.4 结构Color
    Color结构表示颜色,结构中包含一个无符号32位数代表颜色。任何一种颜色可以用透明度(al),蓝色(bb),绿色(gg),红色(rr)合成,格式为0xalrrbbgg,其中al,bb,gg,rr为0到255间的二进制数。常用方法如下:
     public static Color FromArgb(int alpha,int rr,int gg,int bb);
    从四个分量(透明度、红色、绿色和蓝色)值创建Color结构。每个分量的值仅限于8位(小于256)。alpha值表示透明度,=0为完全透明,=255为完全不透明
     public static Color FromArgb(int rr,int gg,int bb);
    从指定的8位颜色值(红色、绿色和蓝色)创建Color结构。透明度值默认为255(完全不透明)。每个分量的值仅限于8位(小于256)。红色为(255,0,0),绿色为(0,255,0),蓝色为(0,0,255)。
     public static Color FromArgb(int alpha,Color color);
    从指定的Color结构创建新Color结构,使用新指定的透明度值alpha。alpha值仅限于8位。透明度及颜色的使用方法的例子如下:
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=e.Graphics;
    SolidBrush RedBrush=new SolidBrush(Color.FromArgb(128,255,0,0));//半透明
    SolidBrush GreenBrush=new SolidBrush(Color.FromArgb(128,0,255,0));
    SolidBrush BlueBrush=new SolidBrush(Color.FromArgb(128,0,0,255));
    g.FillRectangle(RedBrush,0,0,80,80);
    g.FillRectangle(GreenBrush,40,0,80,80);
    g.FillRectangle(BlueBrush,20,20,80,80);
    }
    效果如右图,可以将透明度alpha值设为255,再运行一次,看看有何不同。C#中还预定义了一些颜色常数,例如黑色为Color.Black,红色为Color.Red等等,可用帮助察看。
    5.5 画笔
    Pen类对象指定绘制的图形外轮廓线宽度和颜色。Pen类有4个构造函数,分别是:
     public Pen(Color color);//建立颜色为color的笔,宽度默认为1
     public Pen(Color color,float width);//建立颜色为color的笔,宽度为width
     public Pen(Brush brush);//使用刷子为笔
     public Pen(Brush,float width);//使用刷子为笔,宽度为width
    Pen类常用的属性:Color为笔的颜色,Width为笔的宽度,DashStyle为笔的样式,EndCap和StartCap为线段终点和起点的外观。下例显示各种笔的DashStyle、EndCap和StartCap不同选项的样式(见下图)。主窗体Paint事件处理函数如下:
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=e.Graphics;
    Pen pen1=new Pen(Color.Red,6);//默认为实线笔
    g.DrawLine(pen1,10,10,100,10);//画实线,图中左边第1条线
    pen1.DashStyle=System.Drawing.Drawing2D.DashStyle.Dash;//虚线笔
    g.DrawLine(pen1,10,20,100,20);//画虚线,图中左边第2条线
    pen1.DashStyle=System.Drawing.Drawing2D.DashStyle.DashDot;//点,短线风格的线
    g.DrawLine(pen1,10,30,100,30);//图中左边第3条线
    //双点,短线风格的线
    pen1.DashStyle=System.Drawing.Drawing2D.DashStyle.DashDotDot;
    g.DrawLine(pen1,10,40,100,40);//图中左边第4条线
    pen1.DashStyle=System.Drawing.Drawing2D.DashStyle.Dot;//由点组成的线
    g.DrawLine(pen1,10,50,100,50);//图中左边第5条线
    pen1.DashStyle=System.Drawing.Drawing2D.DashStyle.Solid;//实线笔
    pen1.EndCap=System.Drawing.Drawing2D.LineCap.ArrowAnchor;//后箭头
    g.DrawLine(pen1,150,10,250,10);//图中右边第1条线
    pen1.StartCap=System.Drawing.Drawing2D.LineCap.ArrowAnchor;//前箭头
    g.DrawLine(pen1,150,22,250,22);//图中右边第2条线
    pen1.EndCap=System.Drawing.Drawing2D.LineCap.RoundAnchor;
    g.DrawLine(pen1,150,34,250,34);//图中右边第3条线
    pen1.EndCap=System.Drawing.Drawing2D.LineCap.SquareAnchor;
    g.DrawLine(pen1,150,46,250,46);//图中右边第4条线
    pen1.EndCap=System.Drawing.Drawing2D.LineCap.Triangle;
    g.DrawLine(pen1,150,58,250,58);//图中右边第5条线
    pen1.EndCap=System.Drawing.Drawing2D.LineCap.DiamondAnchor;
    //图中右边第6条线
    g.DrawLine(pen1,150,70,250,70);
    }
    运行效果如右图:


    5.6 创建画刷
    画刷类对象指定填充封闭图形内部的颜色和样式,封闭图形包括矩形、椭圆、扇形、多边形和任意封闭图形。GDI+系统提供了了几个预定义画刷类,包括:
     SolidBrush:单色画刷。在名字空间System.Drawing中定义。
     HatchBrush:阴影画刷。以下画刷在名字空间System.Drawing.Drawing2D中定义。
     TextureBrush:纹理(图像)画刷。
     LinearGradientBrush:颜色渐变画刷。
     PathGradientBrush:使用路径及复杂的混合渐变画刷。
    5.6.1 单色画刷SolidBrush
    前边已使用过单色画刷。其构造函数只有1个,定义如下:
    SolidBrush brush1=new SolidBrush(Color color);//建立指定颜色的画刷
    在使用中可以修改其属性Color来修改其颜色,例如:brush1.Color=Color.Green;
    5.6.2 阴影画刷HatchBrush
    用指定样式(例如,多条横线、多条竖线、多条斜线等)、指定线条的颜色和指定背景颜色定义的画刷,阴影画刷有两个构造函数:
    //指定样式和线条的颜色的构造函数,背景色被初始化为黑色。
    HatchBrush brush1=new HatchBrush(HatchStyle h,Color c);
    //指定样式、线条的颜色和背景颜色的构造函数。
    HatchBrush brush1=new HatchBrush(HatchStyle h,Color c1,Color c2);
    有3个属性如下:
     属性backgroundColor:画刷背景颜色。
     属性foreColor:画刷线条的颜色。
     属性HatchStyle:该属性是只读的,不能修改,表示画刷的不同样式。
    例子5.6.2:显示了阴影画刷属性HatchStyle为不同值时画刷的不同样式。在Form1.cs文件头部增加语句:using System.Drawing.Drawing2D,主窗体Paint事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Graphics g=e.Graphics;//得到窗体的使用的Graphics类对象
    HatchBrush b1=new
    HatchBrush(HatchStyle.BackwardDiagonal,Color.Blue,Color.LightGray);
    g.FillRectangle(b1,10,10,50,50);//矩形被填充左斜线,第1图
    HatchBrush b2=new HatchBrush(HatchStyle.Cross,Color.Blue,Color.LightGray);
    g.FillRectangle(b2,70,10,50,50);//矩形被填充方格,第2图
    HatchBrush b3=new
    HatchBrush(HatchStyle.ForwardDiagonal,Color.Blue,Color.LightGray);
    g.FillRectangle(b3,130,10,50,50);//矩形被填充右斜线,第3图
    HatchBrush b4=new
    HatchBrush(HatchStyle.DiagonalCross,Color.Blue,Color.LightGray);
    g.FillRectangle(b4,190,10,50,50);//矩形被填充菱形,第4图
    HatchBrush b5=new
    HatchBrush(HatchStyle.Vertical,Color.Blue,Color.LightGray);
    g.FillRectangle(b5,250,10,50,50);//矩形被填充竖线,第5图
    HatchBrush b6=new
    HatchBrush(HatchStyle.Horizontal,Color.Blue,Color.LightGray);
    g.FillRectangle(b6,310,10,50,50);//矩形被填充横线,第6图
    }
    运行效果如右图:

    5.6.3 纹理(图像)画刷TextureBrush
    纹理(图像)画刷使用图像来填充封闭曲线的内部,有8个构造函数,最简单的构造函数如下,其余请用帮助查看。
    TextureBrush(Image bitmap);//使用位图类对象作为画刷构造函数的参数
    下边的例子使用文件n2k.bmp建立位图类对象作为画刷的图案,在Form1文件的头部增加语句:using System.Drawing.Drawing2D,主窗体Paint事件处理函数如下:
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=e.Graphics;//得到窗体的使用的Graphics类对象
    Pen pen1=new Pen(Color.Red);
    //位图类对象作为画刷图案,使用文件n2k.bmp建立位图类对象见5.10节
    TextureBrush b1=
    new TextureBrush(new Bitmap("C:\\WINNT\\system32\\n2k.bmp"));
    g.FillRectangle(b1,10,10,200,100);
    g.DrawRectangle(pen1,10,10,200,100);
    }
    文件C:\WINNT\system32\n2k.bmp定义的图形的显示效果如下:

    运行效果如右图。
    5.6.4 颜色渐变画刷LinearGradientBrush
    该类封装双色渐变和自定义多色渐变画刷。所有颜色渐变都是沿由矩形的宽度或两个点指定的直线定义的。默认情况下,双色渐变是沿指定直线从起始色到结束色的均匀水平线性变化。有8个构造函数,最简单的构造函数如下,其余请用帮助查看。
    public LinearGradientBrush(
    Point point1,//point1作为线性渐变直线开始点,也可以为PointF
    Point point2,//point2作为线性渐变直线结束点,也可以为PointF
    Color color1,Color color2);//线性渐变开始颜色和结束颜色
    下边的例子显示了不同线性渐变直线开始点和结束点,使用颜色渐变画刷从黄渐变到蓝的效果。在Form1文件的头部增加语句:using System.Drawing.Drawing2D,主窗体Paint事件处理函数如下:
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=e.Graphics;//得到窗体的使用的Graphics类对象
    Pen pen1=new Pen(Color.Red);
    Point p1=new Point(10,10);//p1作为渐变直线开始点,也可以为PointF
    Point p2=new Point(50,10);//p2作为渐变直线结束点,也可以为PointF
    LinearGradientBrush brush1=//从黄渐变到蓝,见下图左图
    new LinearGradientBrush(p1,p2,Color.Yellow,Color.Blue);
    g.FillRectangle(brush1,10,10,200,100);
    g.DrawRectangle(pen1,10,10,200,100);
    p1=new Point(220,10);
    p2=new Point(270,50);
    LinearGradientBrush brush2=//从黄渐变到蓝,见下图右图
    new LinearGradientBrush(p1,p2,Color.Yellow,Color.Blue);
    g.FillRectangle(brush2,230,10,200,100);
    g.DrawRectangle(pen1,230,10,200,100);
    }
    运行效果如下图:

    5.6.5 画刷PathGradientBrush
    画刷PathGradientBrush可以实现复杂的渐变颜色。有5个构造函数,这里只介绍其中的一个,其参数GraphicsPath类使用见例子及5.7.11节。其余构造函数请用帮助查看。
    public PathGradientBrush(GraphicsPath path);//画刷PathGradientBrush构造函数
    画刷属性SurroundColors:一个Color结构的数组,它表示与PathGradientBrush对象填充的路径中的各点相关联的颜色的数组。SurroundColors数组中的每一Color结构都对应于路径中的点。
    下边的例子介绍画刷PathGradientBrush的使用方法,在Form1.cs文件的头部增加语句:using System.Drawing.Drawing2D,主窗体Paint事件处理函数如下,运行效果如下图。
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Graphics g=e.Graphics;//得到窗体使用的Graphics类对象
    Point[] p1=new Point[6];//点结构数组,有6个元素
    p1[0]=new Point(30,0);
    p1[1]=new Point(160,30);
    p1[2]=new Point(90,60);
    p1[3]=new Point(100,90);
    p1[4]=new Point(30,60);
    p1[5]=new Point(0,30);
    GraphicsPath path=new GraphicsPath(//建立GraphicsPath类对象
    p1,//由点数组p1定义绘制刷子的外轮廓线的路径
    new Byte[]{//定义数组p1每个点元素的关联类型,第1点是开始点
    //贝塞尔曲线必须由4点组成,因此第1、2、3、4点组成一条贝塞尔曲线,
    (byte)PathPointType.Start,(byte)PathPointType.Bezier,
    (byte)PathPointType.Bezier,(byte)PathPointType.Bezier,
    //第5、6点是直线,从第4点到第5点和从第5点到第6点画直线
    (byte)PathPointType.Line,(byte)PathPointType.Line,});
    //为了形成闭合曲线,增加最后一条直线
    PathGradientBrush brush1=new PathGradientBrush(path);//生成画刷
    brush1.SurroundColors=new Color[]{Color.Green,Color.Yellow,Color.Red,
    Color.Blue,Color.Orange,Color.OliveDrab,};//设置属性SurroundColors的值
    g.FillPath(brush1,path);
    }
    5.7 基本图形的绘制和填充
    Graphics类提供了一些绘图方法,用来绘制或填充各种图形。本节介绍这些方法。
    5.7.1 绘制线段
    两个绘制线段的函数和一个绘制多条线段的函数定义如下:
     void DrawLine(Pen pen,int x1,int y1,int x2,int y2);
    其中pen为画笔,(x1,y1)为画线起点坐标,(x2,y2)为画线终点坐标。
     DrawLine(Pen pen,Point p1,Point p2);
    其中pen为画笔,点p1为画线起点坐标,点p2为画线终点坐标。
     public void DrawLines(Pen pen,Point[] points);
    此方法绘制多条线段。从points[0]到points[1]画第1条线,从points[1]到points[2]画第2条线,依此类推。
    例子e5_7_1A:使用DrawLine()的例子,为主窗体Paint事件增加事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Graphics g=e.Graphics;
    Pen pen1=new Pen(Color.Red);
    g.DrawLine(pen1,30,30,100,100);//用笔pen1从点(30,30)到(100,100)画直线
    Point p1=new Point(30,40);
    Point p2=new Point(100,110);
    g.DrawLine(pen1,p1,p2);//用笔pen1从点(30,40)到(100,110)画直线
    }
    例子e5_7_1B:使用绘制线段函数画任意曲线(画正弦曲线,注意如何使用数学函数)。
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();//得到窗体的使用的Graphics类对象
    Pen pen1=new Pen(Color.Red);
    float y=50,y1,x1,x2;
    for(int x=0;x<720;x++)//画正弦曲线
    { x1=(float)x;
    x2=(float)(x+1);
    y1=(float)(50+50*Math.Sin((3.14159/180.0)*(x+1)));
    g.DrawLine(pen1,x1,y,x2,y1);
    y=y1;
    }
    }
    运行,在窗体中可以看到一条红色正弦曲线如下图。

    例子e5_7_1C:在画图程序中,可以用鼠标画任意曲线,现实现用拖动鼠标左键在主窗体中画曲线。每条曲线都是由若干很短的线段组成。鼠标左键按下状态下,移动鼠标,每移动很短距离,画出这段线段,所有这些线段组合起来,形成一条曲线。
    (1) 新建项目。增加两个私有变量:
    private bool mark=false;//表示鼠标左键是否按下,如按下鼠标再移动将画曲线
    private Point point;//记录画下一很短线段的起始点。
    (2) 为Form窗体的事件OnMouseDown,OnMouseUp,OnMouseMove增加事件函数如下:
    private void Form11_MouseDown(object sender,//鼠标按下事件处理函数
    System.Windows.Forms.MouseEventArgs e)
    { if(e.Button==MouseButtons.Left)//如果鼠标左键按下
    { point.X=e.X;//记录曲线的第一个点的坐标
    point.Y=e.Y;
    mark=true;//表示鼠标左键已按下,鼠标如果再移动,将画曲线
    }
    }
    private void Form1_MouseMove(object sender,//鼠标移动事件处理函数
    System.Windows.Forms.MouseEventArgs e)
    { if(mark)//如果鼠标左键按下
    { Graphics g=this.CreateGraphics();//得到窗体的使用的Graphics类对象
    Pen pen1=new Pen(Color.Black);//黑笔
    g.DrawLine(pen1,point.X,point.Y,e.X,e.Y);//画线
    point.X=e.X;//记录画下一线段的起始点的坐标
    point.Y=e.Y;
    }
    }
    private void Form1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
    { mark=false;}//停止画线
    (3) 运行,在Form窗体拖动鼠标左键可以画线。但最小化后再最大化后,图形消失。修改上例,使其能克服这个缺点。实现的思路是记录每一条曲线的每一条很短线段的坐标。使用ArrayList类对象记录曲线以及曲线中的点,请注意ArrayList类使用方法。
    (4) 为定义主窗体的Form1类中增加私有变量:
    private ArrayList Point_List;//用来记录1条曲线的所有点。
    private ArrayList Line_List;//用来记录每条曲线,既Point_List对象。
    在Form1类构造函数中增加语句:Line_List=new ArrayList();
    (5) 修改主窗体事件OnMouseDown,OnMouseUp,OnMouseMove事件处理函数如下:
    private void Form1_MouseDown(object sender,//阴影部分为修改的内容
    System.Windows.Forms.MouseEventArgs e)
    { if(e.Button==MouseButtons.Left)
    { Point_List=new ArrayList();//建立数组,记录1条曲线的所有点
    point.X=e.X;
    point.Y=e.Y;
    mark=true;
    Point_List.Add(point);//曲线起点的坐标
    }
    }
    private void Form1_MouseMove(object sender,
    System.Windows.Forms.MouseEventArgs e)
    { if(mark)
    { Graphics g=this.CreateGraphics();
    Pen pen1=new Pen(Color.Black);
    g.DrawLine(pen1,point.X,point.Y,e.X,e.Y);
    point.X=e.X;
    point.Y=e.Y;
    Point_List.Add(point);//记录曲线中其它点的坐标
    }
    }
    private void Form1_MouseUp(object sender,System.Windows.Forms.MouseEventArgs e)
    { mark=false;
    Line_List.Add(Point_List);//记录此条线,注意参数是Point_List
    }
    (6) 增加Form窗体的Paint事件函数如下:
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=e.Graphics;
    Pen pen1=new Pen(Color.Black);
    Point p1,p2;
    foreach(ArrayList l in Line_List)//取出每条线
    { for(int k=0;k<(l.Count-1);k++)//重画每条线的点
    { p1=(Point)l[k];
    p2=(Point)l[k+1];
    g.DrawLine(pen1,p1,p2);
    }
    }
    }
    (7) 运行,在Form窗体拖动鼠标可以画线。最小化后再最大化后,图形不消失。
    5.7.2 ArrayList类
    ArrayList类是容量可以动态增加的数组,其元素类型可以是任意类型。和其它数组一样,ArrayList类可以使用对象名[索引号]引用其元素,索引号也从零开始。前边已多次使用此类,例如:控件ListBox和ComboBox的属性Items,以及5.7.1节中的例子。其常用的属性及方法如下:
     属性Count:ArrayList中实际包含的元素数。
     方法Add:将参数指定的对象添加到ArrayList对象的结尾处。
     方法Clear:从ArrayList中移除所有元素。
     方法Contains:bool类型,确定参数指定的元素是否在ArrayList中。
     方法IndexOf:int类型,顺序查找和参数指定对象相同的第一个元素的索引。
     方法Insert:插入数据,第1个参数为插入的位置(索引号),第2个参数为插入的对象。
     方法LastIndexOf:顺序查找和参数指定对象相同的最后一个元素的索引。
     方法RemoveAt:移除指定索引处的元素。
     方法Sort:对整个ArrayList中的元素进行排序。
    5.7.3 画椭圆(圆)及键盘消息的使用
    两个画椭圆的函数的功能是画指定矩形的内切椭圆,如为正方形则画圆,两个函数如下:
     void DrawEllipse(Pen pen,int x,int y,int width,int height);
    其中pen为画笔,画外轮廓线,(x1,y1)为指定矩形的左上角坐标,width为指定矩形的宽,height为指定矩形的高。
     void DrawEllipse(Pen pen,Rectangle rect);
    其中pen为画笔,画外轮廓线,rect为指定矩形结构对象。
    例子5_7_3A:画椭圆。为主窗体Paint事件增加事件处理函数如下:
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();
    Pen pen1=new Pen(Color.Red);
    g.DrawEllipse(pen1,10,10,200,100);
    Rectangle rect=new Rectangle(20,20,100,100);
    g.DrawEllipse(pen1,rect);
    }
    例子5_7_3B:用四个箭头键移动窗体中的圆球。移动圆球,实际是先把前边画的圆擦掉,在新的位置重新画圆。如要擦掉圆,可以用窗体背景色作为笔和刷子的颜色,在圆的原先位置重画和填充圆。注意键盘事件处理函数的使用。具体实现步骤如下:
    (1) 新建项目。在Form1类中增加变量:int x,y,记录定义圆位置的矩形左上角的坐标。
    (2) 在Form1类中增加一个方法,该方法按照参数指定颜色画圆,方法定义如下:
    void DrawCir(Color color)//参数是画圆的笔和刷子的颜色
    { Graphics g=this.CreateGraphics();
    Pen pen1=new Pen(color);
    SolidBrush brush1=new SolidBrush(color);
    g.DrawEllipse(pen1,x,y,100,100);
    g.FillEllipse(brush1,x,y,100,100);
    }
    (3) 为主窗体Paint事件增加事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { DrawCir(Color.Red);}
    (4) 为主窗体KeyDown事件增加事件函数如下:(注意不要使用KeyPress事件,其事件处理函数的第2个参数e的e.KeyChar是按下键的ASCII值,但很多键无ASCII值。)
    private void Form1_KeyDown(object sender,System.Windows.Forms.KeyEventArgs e)
    { switch (e.KeyCode)//e.KeyCode是键盘每个键的编号
    { case Keys.Left://左箭头键编号
    DrawCir(this.BackColor);//用Form窗体的背静色画圆,即擦除圆
    x=x-10;//圆左移
    DrawCir(Color.Red);//在新的位置用红色画圆,效果是圆左移
    break;
    case Keys.Right://圆右移
    DrawCir(this.BackColor);
    x+=10;
    DrawCir(Color.Red);
    break;
    case Keys.Down://圆下移
    DrawCir(this.BackColor);
    y+=10;
    DrawCir(Color.Red);
    break;
    case Keys.Up://圆上移
    DrawCir(this.BackColor);
    y=y-10;
    DrawCir(Color.Red);
    break;
    }
    }
    (5) 运行,可以用4个箭头键移动红色圆。
    使用KeyDown事件,事件处理函数的第2个参数e的e.KeyCode是键盘每个键的编号,其它常用键的编号如下:数字键0-9编号为Keys.D0-Keys.D9;字母键A-Z为Keys.A-Keys.Z;F0-F12键表示为Keys.F0-Keys.F12等。
    如使用KeyPress事件,事件处理函数的第2个参数e的e.KeyChar表示按键的ACSII值,例如可用如下语句if(e.KeyChar==(char)13)判断是否按了回车键。
    5.7.4 画矩形
    两个绘制1个矩形(正方形)的函数和一个绘制多个矩形(正方形)的函数定义如下:
     void DrawRectangle(Pen pen,int x,int y,int width,int height);
    其中pen为画笔,画外轮廓线,(x1,y1)为矩形的左上角坐标,width为指定矩形的宽,height为指定矩形的高。
     void DrawRectangle(Pen pen,Rectangle rect);
    其中pen为画笔,画外轮廓线,rect为矩形结构对象。
     public void DrawRectangles(Pen pen,Rectangle[] rects);
    绘制一系列由Rectangle结构指定的矩形。
    例子5_7_3:画矩形。为主窗体Paint事件增加事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();
    Pen pen1=new Pen(Color.Red);
    g.DrawRectangle(pen1,10,10,200,100);
    Rectangle rect=new Rectangle(20,20,100,100);
    g.DrawRectangle(pen1,rect);
    }
    5.7.5 绘制圆弧
    DrawArc方法绘制指定矩形的内切椭圆(圆)中的一段圆弧,方法定义如下:
    void DrawArc(Pen pen,int x,int y,int width,int height,int StartAngle,int EndAngle);
    其中pen为画笔,画外轮廓线,(x1,y1)为矩形的左上角坐标,width为指定矩形的宽,height为指定矩形的高。StartAngle为圆弧的起始角度,EndAngle为圆弧的结束角度,单位为度。指定矩形的中心点做矩形宽和高的的垂线作为x,y轴,中心点为圆点。圆点右侧x轴为0度,顺时针旋转为正角度,逆时针旋转为负角度。
    例子5_7_4:画圆弧。为主窗体Paint事件增加事件处理函数如下:
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();
    Pen pen1=new Pen(Color.Red);
    g.DrawArc(pen1,10,10,200,100,0,30);
    }
    5.7.6 DrawPie方法
    DrawPie方法方法绘制指定矩形的内切椭圆(圆)中的一段圆弧,并且用指定矩形的中心点连接开始点和结束点,这个图形叫做饼图,方法定义如下:
    void DrawPie(Pen pen,int x,int y,int width,int height,int StartAngle,int EndAngle);
    方法参数和DrawArc方法参数相同。
    例子5_7_5:画饼图。为主窗体Paint事件增加事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();
    Pen pen1=new Pen(Color.Red);
    g.DrawPie(pen1,10,10,200,100,0,30);
    }
    5.7.7 Bezier曲线
    可以使用DrawBezier方法画一条Bezier曲线。它的两个画线函数定义如下:
     Void DrawBezier(Pen pen,float x1,float y1,
    float x2,float y2,float x3,float y3,float x4,float y4);
    其中pen是画笔对象,画轮廓线,(x1,y1)是起始点,(x2,y2)是第一控制点,(x3,y3)是第二控制点,(x4,y4)是结束点。
     Void DrawBezier(Pen pen,Point p1,Point p2,Point p3,Point P4);
    其中pen是画笔对象,画轮廓线,p1是起始点,p2是第一控制点,p3是第二控制点,p4是结束点。
    例子5_7_6:画Bezier曲线。为主窗体Paint事件增加事件处理函数如下:
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();
    Pen pen1=new Pen(Color.Red);
    g.DrawBezier(pen1,10,10,200,100,50,60,100,200);
    }
    5.7.8 DrawPolygon方法
    该方法画一个多边形,使用点结构数组定义多边形的顶点。两个画线函数定义如下:
     void DrawPolygon(Pen pen,Point[] points);
     void DrawPolygon(Pen pen,PointF[] points);//点坐标可以是小数
    例子5_7_7:画一个多边形如下图,为主窗体Paint事件增加事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();
    Pen pen1=new Pen(Color.Red);
    Point[] p1=new Point[]
    { new Point(10,10),
    new Point(60,40),
    new Point(100,80),
    new Point(60,100)
    };
    g.DrawPolygon(pen1,p1);
    }
    5.7.9 DrawClosedCurve方法
    DrawClosedCurve方法用来绘制经过Point结构数组中每个点的闭合基数样条。基数样条是一连串单独的曲线,这些曲线连接起来形成一条较大的曲线。样条由点的数组指定,并通过该数组中的每一个点。基数样条平滑地通过数组中的每一个点,请比较一下本节的图形和上节图形的区别。如果最后一个点不匹配第一个点,则在最后一个点和第一个点之间添加一条附加曲线段以使该图闭合,点Point结构数组必须至少包含四个元素,此方法使用默认张力0.5。有4个画线函数,常用的2个画线函数定义如下:
     void DrawClosedCurve(Pen pen,Point[] points);
     void DrawClosedCurve(Pen pen,PointF[] points);//点坐标可以是小数
    例子e5_7_9:使用DrawClosedCurve方法,绘制有4个元素的Point结构数组定义的闭合基数样条闭合曲线如下图,为主窗体Paint事件增加事件处理函数如下:
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();
    Pen pen1=new Pen(Color.Red);
    Point[] p1=new Point[]
    { new Point(10,10),
    new Point(60,40),
    new Point(100,80),
    new Point(60,100)
    };
    g.DrawClosedCurve(pen1,p1);
    }
    5.7.10 DrawCurve方法
    用DrawCurve方法和DrawClosedCurve方法一样,用来绘制经过Point结构数组中每个点的闭合基数样条,但最后两个点之间不连线。常用的两个画线函数定义如下:
     void DrawPolygon(Pen pen,Point[] points);
     void DrawPolygon(Pen pen,PointF[] points);
    例子5_7_9:使用DrawCurve方法,绘制有4个元素的Point结构数组定义的闭合基数样条闭合曲线如下图,为主窗体Paint事件增加事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();
    Pen pen1=new Pen(Color.Red,3);
    Point[] p1=new Point[]
    { new Point(10,10),
    new Point(60,40),
    new Point(100,80),
    new Point(60,100)
    };
    g.DrawCurve(pen1,p1);
    }
    5.7.11 DrawPath方法和GraphicsPath类
    用DrawPath方法可以绘制多个曲线,方法参数GraphicsPath类对象path定义每个曲线类型。DrawPath方法定义如下:
    void DrawPath(Pen pen,GraphicsPath path);
    例子5_7_10A:用DrawPath方法画一个矩形和其内切椭圆如下图,在Form1.cs文件的头部增加语句:using System.Drawing.Drawing2D,主窗体Paint事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Rectangle myEllipse=new Rectangle(20,20,100,50);
    GraphicsPath myPath=new GraphicsPath();//建立GraphicsPath()类对象
    myPath.AddEllipse(myEllipse);//追加一椭圆
    myPath.Add Rectangle(myEllipse);//追加一矩形
    Pen myPen=new Pen(Color.Black,2);
    //画这条曲线,即椭圆和矩形。
    e.Graphics.DrawPath(myPen,myPath);
    }
    GraphicsPath类是表示一系列相互连接的直线和曲线的路径,应用程序使用此路径来绘制曲线的轮廓、填充形状内部和创建剪辑区域。由属性PathPoints(点数组)定义绘制直线和曲线的路径。路径可由任意数目的图形(子路径)组成,每一图形都是由一系列相互连接的直线和曲线或几何形状基元构成的。由属性PathTypes(字节数组)定义属性PathPoints(点数组)中每个点元素的关联图形或曲线类型,图形的起始点是相互连接的一系列直线和曲线中的第一点。终结点是该序列中的最后一点。GraphicsPath在System.Drawing.Drawing2D名字空间。常用属性、事件和方法定义如下:
     构造函数GraphicsPath();//建立空对象
     构造函数public GraphicsPath(Point[] pts,byte[] types);
    参数pts为Point结构数组,数组元素表示构造路径所使用的点。参数types指定路径中相应点的关联图形或曲线类型数组。参见5.6.5节。
     属性PointCount:获取 PathPoints 或 PathTypes 数组中的元素数。
     方法IsVisible:指定点是否包含在此 GraphicsPath 对象内。
     方法:void AddArc(Rectangle rect,float startAngle,float sweepAngle);
    在代表要描绘图形的GraphicsPath类对象中追加一段椭圆弧。
     方法:void AddEllipse(Rectangle rect),追加一椭圆(圆)
     方法:void AddLine(Point pt1,Point pt2),追加一线段
     方法:void AddRectangle(Rectangle rect),追加一矩形
    还有其它增加曲线的方法,例如:AddBezier 方法、AddBeziers 方法、AddClosedCurve 方法、AddCurve 方法、AddLines 方法、AddPath 方法、AddPie 方法、AddPolygon 方法、AddRectangles 方法、AddString 方法等,请用帮助查看。
    5.7.12 DrawString方法
    DrawString方法在指定位置并且用指定的Brush和Font对象绘制指定的文本字符串。有6个重载方法,常用的一个是:
    public void DrawString(string s,//s是为要显示的字符串
    Font font,//显示的字符串使用的字体
    Brush brush,//用刷子写字符串
    PointF point);//显示的字符串左上角的坐标
    最后一个参数也可以是RectangleF对象,仍表示显示的字符串位置。还可以再增加一个参数,即第5个参数,StringFormat对象,它指定应用于所绘制文本的格式化属性(如行距和对齐方式)。在打印和打印预览一节已使用了这个方法。
    例子5_7_12:用DrawString方法显示字符串,主窗体Paint事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { String drawString="Sample Text";//要显示的字符串
    Font drawFont=new Font("Arial",16);//显示的字符串使用的字体
    SolidBrush drawBrush=new SolidBrush(Color.Black);//写字符串用的刷子
    PointF drawPoint=new PointF(20.0F,20.0F);//显示的字符串左上角的坐标
    e.Graphics.DrawString(drawString,drawFont,drawBrush,drawPoint);
    }
    5.7.13 DrawImage和DrawIcon方法
    用来在指定的位置绘制指定的Image对象和图标。Graphics类中有多个DrawImage重载方法,最简单的是以下方法:
     public void DrawImage(Image image,Point point);
    在指定的位置使用原始物理大小绘制指定的Image对象。参数1为要绘制的Image对象,参数2表示所绘制图像的左上角在窗体中的位置。
     public void DrawImage(Image image,Point[] destPoints);
    在指定位置并且按指定形状和大小绘制指定的Image对象。参数1为要绘制的Image对象,参数2表示有3个元素的Point结构数组,三个点定义一个平行四边形。缩放和剪切image参数表示的图像,以在此平行四边形内显示。参数2也可以是一个矩形结构。
     public void DrawImage(Image image,//要绘制的Image对象
    Rectangle destRect,//指定所绘制图像的位置和大小,图像进行缩放以适合该矩形
    Rectangle srcRect,//指定image对象中要绘制的部分
    GraphicsUnit srcUnit);//枚举的成员,指定srcRect参数所用的度量单位
    例子5_7_1A:在指定位置绘制Image对象指定部分。
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Image newImage=Image.FromFile("d:\\CSARP\\1.jpg");//建立要绘制的Image图像
    Rectangle destRect=new Rectangle(10,10,150,150);//显示图像的位置
    Rectangle srcRect=new Rectangle(50,50,150,150);//显示图像那一部分
    GraphicsUnit units=GraphicsUnit.Pixel;//源矩形的度量单位设置为像素
    e.Graphics.DrawImage(newImage,destRect,srcRect,units);//显示
    }//如果把显示图像的位置变宽,看一下效果,为什么?其它重载方法可用帮助查看。
     public void DrawIcon(Icon icon,Rectangle targetRect);
    在Rectangle结构指定的区域内绘制指定的Icon对象表示的图标。
    例子5_7_1B:在指定位置画图标。
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Icon newIcon=new Icon("d:\\CSARP\\TASKS.ICO");
    Rectangle rect=new Rectangle(100,100,200,200);
    e.Graphics.DrawIcon(newIcon,rect);
    }
    5.7.14 FillEllipse方法
    该方法用指定画刷来填充指定矩形的内切椭圆(圆)。两个填充函数的定义如下:
     void FillEllipse(Brush brush,int x,int y,int width,int height);
    其中brush为指定画刷,(x1,y1)为指定矩形的左上角坐标,width为指定矩形的宽,height为指定矩形的高。
     void DrawEllipse(Pen pen,Rectangle rect);
    其中brush为指定画刷,rect为指定矩形结构对象。
    例子5_7_13:用指定画刷来填充指定矩形的内切椭圆。
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();
    SolidBrush brush=new SolidBrush(Color.Blue);
    g.FillEllipse(brush,10,10,200,100);
    Rectangle rect=new Rectangle(120,120,100,100);
    g.FillEllipse(brush,rect);
    }
    5.7.15 FillRectangle方法
    FillRectangle方法用指定画刷来填充指定矩形。两个填充函数定义如下:
     void FillRectangle(Brush brush,int x,int y,int width,int height);
    其中brush为指定画刷,(x1,y1)为矩形的左上角坐标,width为指定矩形的宽,height为指定矩形的高。
     void FillRectangle(Brush brush,Rectangle rect);
    其中brush为指定画刷,rect为矩形结构对象。
    例子5_7_14:用指定画刷来填充指定矩形。
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();
    SolidBrush brush=new SolidBrush(Color.Blue);
    g.FillRectangle(brush,10,10,200,100);
    Rectangle rect=new Rectangle(120,120,100,100);
    g.FillRectangle(brush,rect);
    }
    5.7.16 FillPie方法
    FillPie方法用指定画刷来填充指定饼图。函数定义如下:
    void FillPie(Brush brush,int x,int y,int width,
    int height,int StartAngle,int EndAngle);
    其中brush为指定画刷,方法其它参数和DrawArc方法参数相同。
    例子5_7_15:用指定画刷来填充指定饼图。
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { Graphics g=this.CreateGraphics();
    SolidBrush brush=new SolidBrush(Color.Blue);
    g.FillPie(brush,10,10,200,100,0,30);
    }
    5.7.17 FillRegion方法和Region类
    FillRegion方法用刷子填充区域Region类对象内部。Region类对象由矩形和路径构成。如果区域不闭合,则在最后一个点和第一个点之间添加一条额外的线段来将其闭合。方法定义如下:
    public void FillRegion(Brush brush,Region region);
    第1个参数是填充使用的刷子,第2个参数是指定的区域。
    例子5.7.16A:用纯蓝色刷子,使用FillRegion方法填充一个矩形区域。
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    { SolidBrush blueBrush=new SolidBrush(Color.Blue);
    Rectangle fillRect=new Rectangle(10,10,100,100);
    Region fillRegion=new Region(fillRect);
    e.Graphics.FillRegion(blueBrush,fillRegion);
    }
    区域是输出设备显示区域的一部分。区域可以是简单的(单个矩形)或复杂的(多边形和闭合曲线的组合)。下图中的左数第1图显示了两个区域:一个利用矩形构造,另一个利用路径构造。可以通过合并现有的区域来创建复杂区域。Region类提供了以下合并区域的方法:Intersect、Union、Xor、Exclude和Complement。两个区域的交集是同时属于两个区域的所有点的集合,方法Intersect可以得到两个Region类对象的交集。并集是多个区域的所有点的集合,方法Union可以得到两个Region类对象的并集。方法Xor可以得到两个Region类对象的并集减去这两者的交集,即下图中的左数第4图显示的蓝色区域。方法Exclude和Complement可以得到1个Region类对象和参数指定的Region类对象的不相交的部分,即下图中的左数第5图显示区域。

    Region类常用的方法如下:
     构造函数Region:可以没有参数,即创建一个空区域。也可以有一个参数,可以是GraphicsPath、Rectangle、RectangleF和RegionData类型,由此生成一个区域。
     方法Exclude和Complement:得到1个Region类对象和参数指定的Region类对象的不相交的部分。参数可以是GraphicsPath、Rectangle、RectangleF和Region类型。
     方法Equals:比较2个区域是否相等。参数1是要比较的区域,参数2是要绘制表面的Graphics对象。
     方法Intersect:可以得到两个Region类对象的交集。参数可以是GraphicsPath、Rectangle、RectangleF和Region类型。
     方法IsEmpty:测试是否为空区域。
     方法IsVisible:测试参数指定的点或矩形是否在区域中。
     方法Union:可以得到两个Region类对象的并集。
     方法Xor:可以得到两个Region类对象的并集减去这两者的交集。
    例子5_7_16B:建立2个矩形,有部分相交,将相交部分填充为蓝色。在Form1.cs文件的头部增加语句:using System.Drawing.Drawing2D,主窗体Paint事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { Graphics g=e.Graphics;
    Rectangle regionRect=new Rectangle(10,10,50,50);
    Pen pen1=new Pen(Color.Black);
    g.DrawRectangle(pen1,regionRect);//绘制第1个矩形
    RectangleF unionRect=new RectangleF(25,25,50,50);//第2个矩形
    pen1.Color=Color.Red;
    g.DrawEllipse(pen1,unionRect);//画椭圆
    GraphicsPath myPath=new GraphicsPath();//建立GraphicsPath()类对象
    myPath.AddEllipse(unionRect);//追加一椭圆
    Region myRegion=new Region(regionRect);//建立表示第1个矩形的区域
    myRegion.Intersect(myPath);//得到两个区域的交集
    SolidBrush myBrush=new SolidBrush(Color.Blue);
    e.Graphics.FillRegion(myBrush,myRegion);//填充区域
    }
    运行效果如右图。除了以上介绍的填充方法,还有如下方法:FillClosedCurve方法、FillPath方法、FillPolygon方法等,请用帮助查看。
    5.8 Matrix类和图形的平移、变形、旋转
    本节介绍使用Matrix类实现图形的平移、变形、旋转。
    5.8.1 Matrix类
    Matrix类封装了表示几何变形的3行3列仿射矩阵,可以记录图形的平移、变形、旋转等操作。主要包括如下方法:
     构造函数Matrix():创建一个空Matrix类对象。
     方法Rotate:在Matrix类对象中增加相对于原点顺时针旋转指定角度的操作。参数指定旋转角度。
     方法RotateAt:在Matrix类对象中增加相对于指定点顺时针旋转指定角度的操作。参数1指定旋转角度。参数2指定相应的点。
     方法Scale:在X轴或Y轴方向对图形放大或缩小。参数1指定在X轴方向缩放的值,参数2指定在Y轴方向缩放的值。
     方法Translate:使图形在X轴或Y轴方向移动。参数1指定在X轴方向移动的值,参数2指定在Y轴方向移动的值。
    例子5_8_1:下面的示例创建了复合变形(先旋转30度,再在y方向上缩放2倍,然后在x方向平移5个单位)的Matrix类对象。注意变形得顺序非常重要。一般说来,先旋转、再缩放、然后平移,与先缩放、再旋转、然后平移是不同的。
    Matrix myMatrix=new Matrix();
    myMatrix.Rotate(30);
    myMatrix.Scale(1,2,MatrixOrder.Append);
    myMatrix.Translate(5,0,MatrixOrder.Append);
    5.8.2 图形的平移、变形、旋转
    GraphicsPath类的Transform方法可以缩放、转换、旋转或扭曲GraphicsPath对象,参数Matrix对象表示需要的变形。
    例子5_8_2A:下面的示例代码执行下列操作:创建一个路径并向该路径添加一个椭圆。将路径绘制到主窗体上。创建一个Matrix类对象,在对象中增加在X轴方向上将路径移动100个单位操作。将该已变形的路径绘制到屏幕。观察一下变换前和变换后的不同,注意,初始椭圆是以黑色绘制的,而变形后的椭圆是以红色绘制的。在Form1.cs文件的头部增加语句:using System.Drawing.Drawing2D,主窗体Paint事件处理函数如下:
    private void Form1_Paint(object sender,System.Windows.Forms.PaintEventArgs e)
    { GraphicsPath myPath=new GraphicsPath();//创建一个路径
    myPath.AddEllipse(0,0,50,70);//向路径添加一个椭圆
    e.Graphics.DrawPath(Pens.Black,myPath);//用黑笔画出这个椭圆
    Matrix translateMatrix=new Matrix();//创建一个Matrix类对象
    translateMatrix.Translate(25,0);//在X轴方向上移动25个单位
    //根据Matrix类对象修改路径myPath
    myPath.Transform(translateMatrix);
    //用红笔按新路径画这个椭圆
    e.Graphics.DrawPath(new Pen(Color.Red,2),myPath);
    }
    运行效果如右图。请读者实现变形、旋转。
    5.8.3 仿射矩阵
    m×n矩阵是以m行和n列排列的一组数字,例如一个3×3矩阵记为如下图形式,也可简记为:[a33]。

    两个行、列分别相同的矩阵可以相加,例如:[a33]+[b33]=[c33],矩阵相加运算的规则是:ci j=ai j+bi j,i和j为常量,即相对应位置的项相加。如果有矩阵[am n]和[bn k],[am n]矩阵的列数等于[bn k]矩阵的行数,两个矩阵可以相乘,记为:[am n]*[bn k]=[cm k],矩阵相乘的运算的规则是:ci j=∑(ai t+bt j),其中,i和j为常量,t为变量,初始值为1,最大值为n。
    如果将平面中的点视为1×2矩阵,则可通过将该点乘以2×2变换矩阵来变形该点。下图是点(2,1)在X轴按比例3放大,Y轴不变。

    下图表示点(2,1)旋转了90度。

    下图表示点(2,1)以x轴为对称轴的新点。

    假定要从点(2,1)开始,将其旋转90度,在x方向将其平移3个单位,在y方向将其平移4个单位。可通过先使用矩阵乘法再使用矩阵加法来完成此操作。

    如果用矩阵[2 1 1]代表点(2,1),使用一个3×3变换矩阵,可以用一个矩阵乘法代替以上的两个矩阵运算,见下图:

    注意运运结果的矩阵[2 6 1]代表点(2,6),即点(2,1)映射到了点(2,6)。这个3×3矩阵叫作仿射矩阵,Matrix类中用这个仿射矩阵记录增加的各种变换操作。它和前边的两个2×2矩阵的关系如下图,其中第三列固定为0、0、1。

    Matrix类增加了一些方法处理这个仿射矩阵,主要包括:逆转方法Invert、相乘方法Multiply、重置为单位矩阵方法Reset等。
    5.9 图形文件格式
    在磁盘中存储图形和图像的文件格式有多种。GDI+支持以下图形文件格式。
     位图文件(.bmp):
    位图文件是Windows使用的一种标准格式,用于存储设备无关和应用程序无关的图像。BMP文件通常不压缩,因此不太适合Internet传输。
     可交换图像文件格式(.gif):
    GIF是一种用于在Web页中显示图像的通用格式。GIF文件是压缩的,但是在压缩过程中没有信息丢失,解压缩的图像与原始图像完全一样。GIF文件中的一种颜色可以被指定为透明,这样,图像将具有显示它的任何Web页的背景色。在单个文件中存储一系列GIF图像可以形成一个动画GIF。GIF文件每个像素颜色最多用8位表示,所以它们只限于使用256种颜色。
     JPG文件(.jpg):
    JPEG是联合摄影专家组提出的一种适应于自然景观(如扫描的照片)的压缩方案。一些信息会在压缩过程中丢失,但是这些丢失人眼是察觉不到的。JPEG文件每像素颜色用24位表示,因此能够显示超过16,000,000种颜色。JPEG文件不支持透明或动画。JPEG图像中的压缩级别是可以控制的,但是较高的压缩级别(较小的文件)会导致丢失更多的信息。对于一幅以20:1压缩比生成的图像,人眼难以把它和原始图像区别开来。JPEG是一种压缩方案,不是一种文件格式。“JPEG文件交换格式(JFIF)”是一种文件格式,常用于存储和传输根据JPEG方案压缩的图像。Web浏览器显示的JFIF文件使用.jpg扩展名。
     可移植网络图形(.PNG)
    PNG格式不但保留了许多GIF格式的优点,还提供了超出GIF的功能。像GIF文件一样,PNG文件在压缩时也不损失信息。PNG文件能以每像素8、24或48位来存储颜色,并以每像素1、2、4、8或16位来存储灰度。相比之下,GIF文件只能使用每像素1、2、4或8位。PNG文件还可为每个像素存储一个透明度alpha值,该值指定了该像素颜色与背景颜色混合的程度。PNG优于GIF之处在于它能够逐渐显示一幅图像,也就是说,当图像通过网络连接到达时显示将越来越近似。PNG文件可包含伽玛校正和颜色校正信息,以便图像可在各种各样的显示设备上精确地呈现。
     图元文件(.emf):
    GDI+提供Metafile类,以便能够记录和显示图元文件。图元文件,也称为矢量图像,是一种存储为一系列绘图命令和设置的图像。Metafile对象记录的命令和设置可以存储在内存中或保存到文件或流。下面示例在主窗体显示了一个图元文件的图形。
    Graphics g=this.CreateGraphics();
    Metafile myMetafile=new Metafile("SampleMetafile.emf");
    myGraphics.DrawImage(myMetafile,10,10);//图形左上角的位置是(10,10)
     支持的文件格式还有:图标文件(.ico)、.EXIF、.TIFF、.ICON、.WMF等
    5.10 图形框PictureBox控件
    PictureBox控件常用于图形设计和图像处理程序,又称为图形框,该控件可显示和处理的图像文件格式有:位图文件(.bmp)、图标文件(.ico)、GIF文件(.gif)和JPG文件(.jpg)。其常用的属性、事件和方法如下:
     属性Image:指定要显示的图像,一般为Bitmap类对象。
     属性SizeMode:指定如何显示图像,枚举类型,默认为Normal,图形框和要显示的图像左上角重合,只显示图形框相同大小部分,其余不显示;为CentreImage,将图像放在图形框中间,四周多余部分不显示;为StretchImage,调整图像大小使之适合图片框。
     方法CreateGraphics():建立Graphics对象。
     方法Invalidate():要求控件对参数指定区域重画,如无参数,为整个区域。
     方法Update():方法Invalidate()并不能使控件立即重画指定区域,只有使用Update()方法才能立即重画指定区域。使用见5.10.4节中的鼠标移动事件处理函数。
    例子e5_10:使用PictureBox控件显示图像
    (1) 新建项目。放PictureBox控件到窗体。属性Name=pictureBox1。
    (2) 放Button控件到窗体。属性Name=button1。
    (3) 放OpenFileDialog控件到窗体。属性Name=openFileDialog1。
    (4) 可以在设计阶段修改属性Image为指定图形文件,设定初始显示的图像。
    (5) button1控件事件处理函数如下:
    private void button1_Click(object sender, System.EventArgs e)
    { if(openFileDialog1.ShowDialog()==DialogResult.OK)
    { Bitmap p1=new Bitmap(openFileDialog1.FileName);//Bitmap类见下节
    pictureBox1.Image=p1;
    }
    }
    5.11 Bitmap类
    System.Drawing命名空间有一个类Image,用来处理图像。Image类的派生类Bitmap类封装了GDI+中的位图,可以处理由像素数据定义的图像。Image类的派生类metafile处理元文件,此类文件用记录绘图命令的方法存储图像。
    5.11.1 Bitmap类支持的图像类型
    使用Bitmap类可以显示和处理多种图像文件,可处理的文件类型及文件扩展名如下:扩展名为.bmp的位图文件、扩展名为.ico的图标文件、扩展名为.gif的GIF文件、扩展名为.jpg的JPG文件。当使用构造函数Bitmap(string FileName)建立Bitmap类对象时,如果文件是以上类型,将自动转换为位图格式存到Bitmap类对象中。可使用Bitmap类方法Save(string FileName,ImageFormat imageFormat)把Bitmap类对象中的位图存到文件中,其中第1个参数是选定的文件名,第2个参数是指定文件存为那种类型,可以是如下类型:System.Drawing.Imaging.ImageFormat.bmp(或.ico、.gif、.jpg)。
    5.11.2 Bitmap类的方法
     方法SetPixel():画点方法,前2个参数是指定点的位置,第3个参数是颜色值。
     方法GetPixle():得到指定点的颜色,2个参数是指定点的位置,返回颜色值。
     有多个构造函数例如:new Bitmap(”图像文件名”),new Bitmap(宽,高)等。
     方法Save():第1个参数是文件名,第2个参数是指定文件存为那种类型,可以是如下类型:System.Drawing.Imaging.ImageFormat.bmp(或.ico、.gif、.jpg)。
     方法Dispose():释放位图对象
    5.11.3 画点
    例子e5_11_3:用SetPixel画点,GetPixle得到指定点的颜色。放Button和PictureBox控件到主窗体。增加Button控件单击事件函数如下:
    private void button1_Click(object sender,System.EventArgs e)
    { pictureBox1.Width=720;//设定pictureBox1的宽和高
    pictureBox1.Height=110;
    Bitmap bits=new Bitmap(720,110);//建立位图对象,宽=720,高=110
    int x,y;
    for(x=0;x<720;x++)//画正弦曲线
    { y=(int)(50+50*Math.Sin((3.14159/180.0)*x));
    bits.SetPixel(x,y,Color.Red);
    }
    pictureBox1.Image=bits;//位图对象在pictureBox1中显示
    Color c1=bits.GetPixel(20,20);
    string s="R="+c1.R+",G="+c1.B+",G+"+c1.G;
    MessageBox.Show(s);
    }
    5.11.4 在PictureBox中画任意曲线
    例子e5_11_4:例子5_7_1C实现了画任意曲线程序,用ArrayList类对象记录绘制的曲线。在该程序中增加橡皮功能、图像的拷贝、图像的剪贴、图像的粘贴比较困难,也不能和画图程序交换文件。为了实现这些功能,用图形框(PictureBox控件)显示绘制图形。绘制图形必须存在图形框属性Image引用的位图对象中,图形框显示的图像被破坏,图形框响应Paint事件,将用其属性Image引用的位图对象恢复所绘制的图形。仅将图形绘制在图形框表面,图形框响应Paint事件,绘制的图形将不能被恢复,也就是说,绘制在图形框表面的图形丢失了。具体实现步骤如下:
    (1) 新建项目。增加4个私有变量:private bool mark=false; private Point point;
    private Bitmap bits; private Graphics bitG;
    (2) 放PictureBox控件到窗体,修改属性Dock=Fill。
    (3) 在构造函数中增加语句:
    //建立位图对象,宽和高为指定值
    bits=new Bitmap(pictureBox1.Width,pictureBox1.Height);
    bitG=Graphics.FromImage(bits);//得到位图对象的Graphics类的对象
    bitG.Clear(Color.White);//用白色清除位图对象中的图像
    pictureBox1.Image=bits;//位图对象在pictureBox1中显示
    (4) 为控件PictureBox事件OnMouseDown,OnMouseUp,OnMouseMove增加事件处理函数:
    private void pictureBox1_MouseDown(object sender,//鼠标按下事件处理函数
    System.Windows.Forms.MouseEventArgs e)
    { if(e.Button==MouseButtons.Left)//是否是鼠标左键按下
    { point.X=e.X;
    point.Y=e.Y;//画线段开始点
    mark=true;//鼠标左键按下标识
    }
    }
    private void pictureBox1_MouseMove(object sender,//鼠标移动事件处理函数
    System.Windows.Forms.MouseEventArgs e)
    { if(mark)//如果鼠标左键按下
    { Graphics g=pictureBox1.CreateGraphics();
    Pen pen1=new Pen(Color.Black);
    g.DrawLine(pen1,point.X,point.Y,e.X,e.Y);//图形画在PictureBox表面
    bitG.DrawLine(pen1,point.X,point.Y,e.X,e.Y);//图形画在位图对象bits中
    EndPoint.X=e.X;
    EndPoint.Y=e.Y;//下次绘制画线段开始点
    }
    }
    private void pictureBox1_MouseUp(object sender,
    System.Windows.Forms.MouseEventArgs e)
    { mark=false;
    pictureBox1.Image=bits;//保存了所画的图形
    }
    (5) 运行,在PictureBox控件拖动鼠标可以画线。最小化后再最大化后,图形不消失。
    5.11.5 存取位图文件
    例子e5_11_5:为上例增加存取位图文件功能。
    (6) 把Mainmenu控件放到主窗体中。增加菜单:文件,属性Name=menuItemFile。为文件菜单增加菜单项:新建、打开、另存为、退出,属性Name分别为menuItemNew、menuItemOpen、menuItemSaveAs、menuItemExit。
    (7) 为主窗体菜单项新文件增加单击事件函数如下:
    private void menuItemNew_Click(object sender, System.EventArgs e)
    { bitG.Clear(Color.White);//用白色清空位图对象bitG
    pictureBox1.Image=bits;//pictureBox1显示用白色清空位图对象bitG
    }
    (8) 把OpenFileDialog控件放到窗体中。为主窗体菜单项打开文件增加单击事件函数如下:
    private void menuItemOpen_Click(object sender,System.EventArgs e)
    { if(openFileDialog1.ShowDialog(this)==DialogResult.OK)
    { bits.Dispose();//撤销bitG所引用的对象
    bits=new Bitmap(openFileDialog1.FileName);//建立指定文件的新位图对象
    bitG=Graphics.FromImage(bits);//得到位图对象使用的Graphics类对象
    pictureBox1.Image=bits;
    }
    }
    (9) 把SaveFileDialog控件放到子窗体中。为主窗体菜单项另存为增加单击事件函数如下:
    private void menuItemSaveAs_Click(object sender,System.EventArgs e)
    { if(saveFileDialog1.ShowDialog(this)==DialogResult.OK)
    { string s=saveFileDialog1.FileName+".bmp";
    bits.Save(s,System.Drawing.Imaging.ImageFormat.Bmp);
    }
    }//也可以存为其它格式,例如:Jpg,Gif等。请读者试一下。
    (10) 为主窗体菜单项退出增加单击事件函数如下:
    private void menuItemExit_Click(object sender, System.EventArgs e)
    { Close();}
    (11) 运行,在PictureBox控件拖动鼠标可以画线。存所画的图形到文件,再重新读出该文件,看是否正常运行。
    5.11.6 用拖动鼠标方法画椭圆或圆
    例5_11_6:画笔程序中,拖动鼠标方法画椭圆或圆,拖动鼠标时显示椭圆或圆的轮廓,鼠标抬起时,按指定条件画椭圆或圆。如果图形仅画在图形框(PictureBox控件)上,而不保存到其属性Image引用的位图对象中,当调用图形框的Invalidate()方法,图形框响应Paint事件,用图形框属性Image引用的位图对象恢复图像,将擦除仅画在图形框上的图形。拖动鼠标方法画椭圆或圆时,仅将椭圆或圆画在PictureBox上,在鼠标拖动到下一个位置,用图形框的Invalidate()方法将前一位置所画的图形擦除。实现步骤如下:
    (1) 新建项目。增加5个私有变量:private bool mark=false;private Point StartPoint; private Bitmap bits;private Graphics bitG;private Point EndPoint;
    (2) 放PictureBox控件到子窗体。修改属性Dock=Fill。
    (3) 在构造函数中增加语句:
    //bits用来保存pictureBox1中位图图像
    bits=new Bitmap(pictureBox1.Width,pictureBox1.Height);
    bitG=Graphics.FromImage(bits);
    bitG.Clear(Color.White);
    pictureBox1.Image=bits;
    (4) 在Form1类中增加MakeRectangle方法返回由参数指定的两个点定义的矩形。方法如下:
    private Rectangle MakeRectangle(Point p1,Point p2)
    { int top,left,bottom,right;
    top=p1.Y<=p2.Y? p1.Y:p2.Y;//计算矩形左上角点的y坐标
    left=p1.X<=p2.X? p1.X:p2.X;//计算矩形左上角点的x坐标
    bottom=p1.Y>p2.Y? p1.Y:p2.Y;//计算矩形右下角点的y坐标
    right=p1.X>p2.X? p1.X:p2.X;//计算矩形右下角点的x坐标
    return(new Rectangle(left,top,right-left,bottom-top));//返回矩形
    }
    (5) 为PictureBox事件OnMouseDown、OnMouseUp、OnMouseMove增加事件处理函数如下:
    private void pictureBox1_MouseDown(object sender,//鼠标按下事件处理函数
    System.Windows.Forms.MouseEventArgs e)
    { if(e.Button==MouseButtons.Left)
    { StartPoint.X=e.X;
    StartPoint