拜读经典——大话设计模式(一)——温习C#

前言:

早就听说了程杰老师的“大话设计模式”,今天终于快递到手,开始研读,并做好笔记。

这一文章作为开头来简单过一遍C#知识,温习一下面向对象思想

 

 

 

 

 

 


温习C#

WinForm 是 Windows Form 的简称,是基于 .NET Framework 平台的客户端(PC软件)开发技术, 一般使用 C# 编程。

C# WinForm 编程需要创建「Windows窗体应用程序」项目。

.NET 提供了大量 Windows 风格的控件和事件,我们可以直接拿来使用,上手简单,开发快速。

Windows窗体应用程序:

Windows 窗体应用程序也称为事件驱动程序,也就是通过鼠标单击界面上的控件、通过键盘输入操作控件等操作来触发控件的不同事件完成相应的操作。例如单击按钮、右击界面、向文本框中输入内容等操作。

 

我们的任务:做一个动物园软件

新建一个C#的Windows窗体应用程序

在UI界面上通过工具箱新建一个button,对应代码:

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {

        }

        private void button1_Click(object sender, EventArgs e)
        {
            MessageBox.Show("喵");
        }
    }

这个partial(部分的、热爱、不完全的)关键字第一次见,可以将类拆分,好厉害!盲猜Unity的DOTween插件就是利用这个让transform有了新方法

这样我们就可以实现一个最基本的提示框。

类就是具有相同属性和功能的对象的抽象的集合

但是,这样对代码复用并不好。我们可以将字符串单独做出来一个类

     class Cat
    {
        public string Shout() {
            return "喵";
        }
    }
        private void button1_Click(object sender, EventArgs e)
        {
            //声明对象并实例化
            Cat cat = new Cat();
            MessageBox.Show(cat.Shout());
        }

这下调用它就方便多了

 

构造方法

构造方法,即在实例化对象时调用。

    class Cat
    {
        private string name = "";
        public Cat(string name){
            this.name = name;
        }
        public string Shout() {
            return "我的名字叫"+name+"喵";
        }
    }

        private void button1_Click(object sender, EventArgs e)
        {
            //声明对象并实例化
            Cat cat = new Cat("嘟嘟");
            MessageBox.Show(cat.Shout());
        }

直接构造对象:

 

方法重载

方法重载提供了创建同名的多个方法的能力,但这些方法需要不同的参数类型。

 

字段、属性与修饰符

属性是一个方法或一对方法,但是调用它的代码来看,它是一个字段,即属性适合于以字段的方式结合方法调用的场合。

字段是存储类要满足其设计所需要的数据,字段是与类相关的变量,比如刚才的“private string name ”。的name就是一个字段,那么属性是什么样的呢?示例:

        private int _shoutNum = 3;
        public int ShoutNum {
            get {
                return shoutNum;
            }
            set {
                shoutNum = value;
            }
        }

这与C++等语言不同,shouNum是一个内部字段(private),而ShoutNum才是一个属性(public),另外说一下:字段一般加_,且首字母小写,属性则大写

 

C#修饰符

  • 修饰符public:它所修饰的类成员允许其他任何类来访问。
  • 修饰符private:它所修饰的类成员只允许同一个类中的成员访问,其他类(包括子类)无法访问。
  • 修饰符protected:表示继承时子类对基类有完全访问权。

如果一个类没有加修饰符,默认为 private,

Java与这里不同,人家java的修饰符关系是这样:

上图是Java!

上图是Java!

上图是Java!

属性有两个方法get和set(两个访问器)。get访问器返回与声明的属性相同的数据类型,表示的意思是调用时可以得到内部字段的值或引用;set访问器没有显式设置参数,但他有一个隐式参数,用关键字value表示,调用属性时可以给内部的字段或引用赋值。

基于面向对象的思想,我们要尽量对一个类加以控制,所以要严格封装好它,这也是面向对象三大特性——封装性的体现

把属性利用到上面的例子,就是:

    class Cat
    {
        private string name = "";
        private int shoutNum = 3;
        public int ShoutNum {
            get {
                return shoutNum;
            }
            set {
                if (value <= 10)
                {
                    shoutNum = value;
                }
                else
                {
                    shoutNum = 10;
                }
            }
        }
        public Cat(string name){
            this.name = name;
        }
        public string Shout() {
            string result = "";
            for (int i = 0; i < shoutNum; i++) {
                result += " 喵 ";
            }
            return "我的名字叫"+name+" "+result;
        }
    }
        private void button1_Click(object sender, EventArgs e)
        {
            //声明对象并实例化
            Cat cat = new Cat("嘟嘟");
            cat.ShoutNum = 6;
            MessageBox.Show(cat.Shout());
        }

一下就可以叫6次了!如果不赋值,只会叫三声,因为shoutNum的默认值是3;

 

封装(面向对象的第一特性)

每个对象都包含它能进行操作所需要的所有信息,这个特性称为封装,因此对象不必依赖其他对象来完成自己的操作。

封装的优点:

  1. 良好的封装能够减少耦合
  2. 类内部的实现可以自由的修改
  3. 类具有清晰的对外接口

我们在刚才的基础上实现狗叫(复制一份Cat的代码)

 

继承(面向对象的第二特性)

对象的继承代表了一种“is-a”的关系,如果两个对象A和B,可以描述为“B是A”,则表明B可以继承A。

继承者还可以理解为是对被继承者的特殊化,因为它具备被继承者的特性外,还具备自己独有的个性。

继承定义了类如何相互关联、共享特性。

子类拥有父类非private的属性和功能。

另外注意,子类从他的父类中继承的成员有方法、域、属性、事件、索引指示器,但对于构造方法,它不能被继承,只能被调用,使用base关键字即可调用父类 的构造方法。注意,C#中调用子类的构造器时会默认优先调用父类的无参构造器,假如访问不到(比如父类写了一个有参构造器覆盖了编译器自动提供的无参构造器,或者无参构造器的修饰符限定了权限),则会报错,如果想要调用父类的有参构造器,就要利用好 base关键字,java也是这样。

来我们来修改目前的代码,新建一个动物类:

   class Animal
    {
        protected string name = "";
        public Animal() {
            this.name = "无名";
        }
        public Animal(string name){
            this.name = name;
        }
        protected int shoutNum = 3;
        public int ShoutNum
        {
            get
            {
                return shoutNum;
            }
            set
            {
                if (value <= 10)
                {
                    shoutNum = value;
                }
                else
                {
                    shoutNum = 10;
                }
            }
        }
    }

以Cat来示例(Dog是类似代码)

    class Cat:Animal
    {

        public Cat() : base(){

        }
        public Cat(string name):base( name){
        }
        public string Shout() {
            string result = "";
            for (int i = 0; i < shoutNum; i++) {
                result += " 喵 ";
            }
            return "我的名字叫"+name+" "+result;
        }
    }

继承要适当的用,因为继承显示是一种类与类之间强耦合的关系,最明显的缺点:父类变,子类不得不变。

 

多态(面向对象的第三特性)

多态表示不同的对象可以执行相同的动作,但要通过他们自己的实现代码来执行。

为了使子类的实例完全接替来自父类的类成员,父类必须将该成员声明为虚拟的。这是通过在该成员的返回类型之前添加virtual关键字实现的。然后,子类可以选择使用override关键字,将父类实现替换为它自己的实现,这就是方法重写Override。

Animal类里面加一句:

        public virtual string Shout() { return ""; }

派生类中实现Shout方法时加一个override即可实现多态。

然后在客户端代码中,新建两个按钮,并编辑对应代码:

        private Animal[] arrayAnimal;
        private void button3_Click(object sender, EventArgs e)
        {
            arrayAnimal = new Animal[5];
            arrayAnimal[0] = new Cat("小花");
            arrayAnimal[1] = new Cat("阿毛");
            arrayAnimal[2] = new Cat("小黑");
            arrayAnimal[3] = new Cat("娇娇");
            arrayAnimal[4] = new Cat("咪咪");
        }

        private void button4_Click(object sender, EventArgs e)
        {
            foreach(Animal item in arrayAnimal)
            {
                //由于有了多态性,所以叫的时候,程序会自动去找item是什么对象,然后用哪个重写方法
                MessageBox.Show(item.Shout());
            }
        }

先点击按钮3(动物报名),再点击按钮4(动物叫),就可以连续弹出五个框。

这就是不同的对象可以执行相同的动作,但要通过他们自己的代码来实现

注意,对象的声明必须是父类,而不能是子类,实例化的对象是子类,这才能实现多态。多态的的原理是当方法被调用时,无论对象是否被转换为其父类,都只有位于对象继承链最末端的方法实现会被调用,也就是说,虚方法是按照其运行时类型而非编译时类型进行动态绑定调用的(向上转型)。

 

重构

重构的牛逼,你看了就明白了:

Animal:

        public string Shout() {
            string result = "";
            for (int i = 0; i < shoutNum; i++)
            {
                result += getShoutSound()+",";
            }
            return "我的名字叫" + name + " " + result;
        }
        protected virtual string getShoutSound() {
            return "";
        }

Cat:


    class Cat:Animal
    {

        public Cat() : base(){

        }
        public Cat(string name) : base(name)
        {
        }
        protected override string getShoutSound()
        {
            return "喵";
        }
    }

 

抽象类

我们发现Animal类根本不可能被实例化。

C#允许把类和方法声明为abstract,即抽象类和抽象方法。

使用abstract要注意:

  • 抽象类不能被示例化
  • 抽象方法没有方法体,括号后直接加;
  • 抽象方法必须是被子类重写的方法
  • 如果类中包含抽象方法,那么该类就必须定义为抽象类,不论是否还包含抽象方法

抽象类通常代表一个抽象概念,它提供一个继承的出发点。当设计一个新的抽象类时,一定是用来继承的,所以,在一个以继承关系形成的等级结构里面,树叶结点应当是具体类,树枝结点均应当是抽象类。

 

接口

接口就是把隐式公共方法和属性组合起来,以封装特定功能的一个集合,一旦类实现了接口,类就可以支持接口所指定的所有属性和成员。声明接口在语法上与声明类完全相同,但不允许提供接口中任何成员的执行方式。所以接口:

  1. 不能被实例化
  2. 不能有构造方法和字段
  3. 不能有修饰符
  4. 不能声明虚拟的或静态的等
  5. 实现接口的类必须要实现接口的所有方法和属性

一个类可以支持多个接口,多个类也可以支持相同接口。

接口的命名前面必须要有一个I,规范。

比如我们的动物园例子,我们要实现继承自Cat的“机器猫”

新建一个接口文件

    interface IChange
    {
        string ChangeThing(string thing);
    }

然后新建一个类——MachineCat.cs

    class MachineCat : Cat, IChange
    {
        public MachineCat() : base() {

        }
        public MachineCat(String name) : base(name) {

        }

        public string ChangeThing(string thing)
        {
            return base.Shout() + " 我有万能口袋,我可以变出" + thing;
        }
    }

然后客户端添加一个按钮,就可以实现了。

        private void button5_Click(object sender, EventArgs e)
        {
            //这里也属于向上转型
            MachineCat mcat = new MachineCat("叮当");
            IChange machineCat=mcat;

            MessageBox.Show(machineCat.ChangeThing("各式各样的东西!"));
        }

关于抽象类与接口:

  1. 类是对对象的抽象;抽象类是对类的抽象;接口是对行为的抽象
  2. 如果行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类
  3. 从设计角度讲,抽象类是从子类中发现了公共的东西,泛化出父类,然后子类继承父类,而接口是根本不知子类的存在,方法如何实现还不确定,预先定义。

实现接口和继承抽象类并不冲突,例如可以让超人类继承自人类,然后再实现飞行行为的接口。

 

集合

数组优点:比如说数组在内存中连续存储,因此可以快速而容易地从头到尾遍历元素,可以快速修改元素等等。缺点,创建时必须要指定数组变量的大小,还有在两个元素之间添加元素也比较困难。

.NET Framework 提供了用于数据存储和检索的专用类,这些类统称为集合,这些类提供对堆栈、队列、列表、哈希表的支持。大多数集合类实现相同的接口。

我们以 ArrayList 为例。

ArrayList是命名空间 System.Collection 的一部分,它是使用大小可按需动态增加的数组实现IList接口。

ArrayList的容量是ArrayList可以保存的元素数。ArrayList的默认初始容量是0.随着元素添加到ArrayList中,容量会根据需要重新分配自动添加。使用整数索引可以访问此集合中的元素。此集合中的索引从零开始。

        IList arrayAnimal;
        private void button3_Click(object sender, EventArgs e)
        {
            //又是向上转型
            arrayAnimal = new ArrayList();
            arrayAnimal.Add(new Dog("小花"));
            arrayAnimal.Add(new Cat("阿毛"));
            arrayAnimal.Add(new Dog("小黑"));
            arrayAnimal.Add(new Cat("娇娇"));
            arrayAnimal.Add(new Cat("咪咪"));

            //有两个动物生病了
            //通过下标移除要注意,移除了一个之后,后面的会往前顶
            arrayAnimal.RemoveAt(1);
            arrayAnimal.RemoveAt(1);
        }

        private void button4_Click(object sender, EventArgs e)
        {
            foreach(Animal item in arrayAnimal)
            {
                MessageBox.Show(item.Shout());
            }
        }

另外提一下,ArrayList不管你是什么对象,他都视为object对象,即都会接受。

说明“ArrayList不是类型安全的”,假如你装了一个string类型,遇到后面的Animal遍历就会报错。

还有就是ArrayList存放值类型的数据,比如int、string型(string是一种拥有值类型特点的特殊引用类型)或是结构struct的数据,用ArrayList 就意味着需要将值类型装箱成 object对象 ,使用集合元素时,还需要执行拆箱操作,这就带来了很大的性能损耗。

什么是装箱/拆箱?

装箱就是把值类型打包到object引用类型的一个实例中。

拆箱则是指从对象中提取值类型。

相当于简单的赋值而言,装箱和拆箱过程需要进行大量的计算。对值类型进行装箱时,必须分配并构造一个全新的对象,拆箱所需的强制转换也需要进行大量的计算。

 

 

泛型

泛型是具有占位符(类型参数)的类、结构、接口和方法,这些占位符是类、结构、接口和方法所存储或使用的一个或多个类型的占位符。

泛型集合可以将类型参数用作它所存储的对象的类型的占位符;类型参数作为其字段的类型和其方法的参数类型出现。

        //声明泛型集合变量,表示此集合只能接受Animal类型,
        //其他不可以
        IList<Animal> arrayAnimal;
        private void button3_Click(object sender, EventArgs e)
        {
            //又是向上转型,实例化List对象
            arrayAnimal = new List<Animal>();
            arrayAnimal.Add(new Dog("小花"));
            arrayAnimal.Add(new Cat("阿毛"));
            arrayAnimal.Add(new Dog("小黑"));
            arrayAnimal.Add(new Cat("娇娇"));
            arrayAnimal.Add(new Cat("咪咪"));

            //有两个动物生病了
            //通过下标移除要注意,移除了一个之后,后面的会往前顶
            arrayAnimal.RemoveAt(1);
            arrayAnimal.RemoveAt(1);
        }

这里我们使用了List类来替代ArrayList类,ArrayList不支持泛型

通常情况下,都建议使用泛型集合,因为这样可以获得类型安全的直接优点而不需要从基集合类型派生并实现类型特定的成员。此外,如果集合元素为值类型,泛型集合类型的性能通常优于对应的非泛型集合类型,并优于从非泛型基集合类型派生的类型,因为使用泛型时不必对元素进行装箱。

 

 

委托和约束

委托是对函数的封装,可以当做给方法的特征指定一个名称。

而事件则是委托的一种特殊形式,当发生有意义的事情时,事件对象处理通知过程。

委托是一种引用方法的类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为,委托的关键字用 delegate 来声明。而事件是说在发生其他类或对象关注的事情时,类或对象可通过事件通知他们,事件对象用event关键字声明。

我们新建一个项目来做实验:

Cat.cs:

    class Cat
    {
        //声明委托(无返回值、无参数)
        public delegate void CatShoutEventHandler();
        //声明事件,事件类型是委托CatShoutEventHandler
        public event CatShoutEventHandler CatShout;

        private string name;
        public Cat(string name) {
            this.name = name;
        }

        public void Shout()
        {
            Console.WriteLine("喵,我是{0}", name);
            if (CatShout != null) {
                //有登记事件,则执行
                CatShout();
            }
        }
    }

Mouse.cs

    class Mouse
    {
        //声明委托(无返回值、无参数)
        public delegate void CatShoutEventHandler();
        //声明事件,事件类型是委托CatShoutEventHandler
        public event CatShoutEventHandler CatShout;

        private string name;
        public Mouse(string name) {
            this.name = name;
        }

        public void Run()
        {
            Console.WriteLine("猫来了,{0}快跑", name);
        }
    }

Program.cs

    class Program
    {
        static void Main(string[] args)
        {
            Cat cat = new Cat("Tom");
            Mouse mouse1 = new Mouse("Jerry");
            Mouse mouse2 = new Mouse("Jack");

            //将Mouse的Run方法通过实例化委托登记到Cat的事件当中
            cat.CatShout += new Cat.CatShoutEventHandler(mouse1.Run);
            cat.CatShout += new Cat.CatShoutEventHandler(mouse2.Run);

            cat.Shout();

            Console.Read();
        }
    }

其实给委托添加方法那里可以直接 cat.CatShout+=mouse1.Run,效果是一样的

在做C#的.NET应用程序或Web程序时,总能看到IDE生成的事件参数,比如private void button1_Click(object sender,EventArgs e),这里的sender和e有什么用呢?

我们继续往下看。

新增一个类——CatShoutEventArgs,让它继承EventArgs,EventArgs 是包含事件数据的类的基类,换句话说,这个类的作用就是用来在事件触发时传递数据用的。

    class CatShoutEventArgs:EventArgs
    {
        private string _name;
        public string Name {
            get {
                return _name;
            }
            set {
                this._name = value;
            }
        }
    }

改写Cat代码

    class Cat
    {
        //声明委托(无返回值、无参数)
        public delegate void CatShoutEventHandler(
            object sender,
            CatShoutEventArgs args);
        //声明事件,事件类型是委托CatShoutEventHandler
        public event CatShoutEventHandler CatShout;

        private string name;
        public Cat(string name) {
            this.name = name;
        }

        public void Shout()
        {
            Console.WriteLine("喵,我是{0}", name);
            if (CatShout != null) {
                //声明并实例化一个CatShoutEventArgs
                //并给Name属性赋值猫的名字
                CatShoutEventArgs e = new CatShoutEventArgs();
                e.Name = this.name;
                //事件发生时,将自己和需要的数据发送过去
                CatShout(this,e);
            }
        }
    }

object sender就是传递发送通知的对象,而EventArgs是包含事件数据的类。

Mouse类也可以改一改:

        public void Run(object sender,CatShoutEventArgs args)
        {
            Console.WriteLine("猫{0}来了,{1}快跑",
                args.Name, name);
        }

主函数不用变,这样,老鼠连猫叫什么也可以显示了

喵,我是Tom
猫Tom来了,Jerry快跑
猫Tom来了,Jack快跑

 


后话

C#还有很多知识,这里就不深入了,简单温习了一下C#,准备开始我们的“设计模式”之旅!

 

 

 

 

 

 

 

商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢

 

 

发表评论