c#面向对象编程

Author Avatar
Zhu Yuexin Dec 03, 2018

继承

public class Pet
{
    public string Name;
    public void PrintName()
    {
        Console.WriteLine("Pet's name is " + Name);
    }
}

public class Dog:Pet
{

}

子类具有父类的字段、属性和方法

Dog dog = new Dog()
dog.Name = "David";
dog.PrintName();

Object类是所有类的共同基类,它是唯一的非派生类,是继承层次结构的基础。对于其他类,父类和子类的概念都是相对的。

继承的规则:继承只有单继承,也就是只能继承一个父类。

隐藏方法

我们不能删除基类中的任何成员,但是可以用与基类成员名称相同的成员屏蔽基类成员

语法细节:

  • 屏蔽数据成员:在派生类中声明名称和类型相同的成员
  • 屏蔽函数成员:在派生类中声明新的带有相同函数签名的成员
  • 让编译器知道:在新成员前面添加new关键字,否则会有警告

虚方法和多态

基类的引用

派生类的对象包含基类部分和派生类部分,所以,我们可以通过一个基类类型的引用指向派生类。(即父类引用,指向子类)

通过指向派生类的基类引用,我们仅仅能访问派生类中的基类部分。而且派生类中即使隐藏了基类的方法,调用时依然是调用基类的方法。

//定义基类
public class Pet
{
    private string _name;
    private int _age;

    public Pet() { }

    public Pet(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public string Name
    {
        get{return _name;}

        set{_name = value;}
    }

    public int Age
    {
        get{return _age;}

        set{_age = value;}
    }

    public void PrintName()
    {
        Console.WriteLine("My name is " + Name);
    }
}
//定义派生类,继承自Pet类
public class Dog:Pet
{

    public Dog(){ }

    public Dog(string name, int age) : base(name, age)
    {
        //继承Pet的构造方法
    }

    //隐藏Pet的PrintName()方法
    new public void PrintName()
    {
        Console.WriteLine("I'm a dog, my name is " + Name);
    }

    //新定义的方法
    public void PrintType()
    {
        Console.WriteLine("Dog");
    }
}

这里定义了一个基类Pet和派生类DogDog继承了基类的构造方法,以及PrintName()方法,然后新定义了一个PrintType()方法是基类中没有的。接下来试着调用一下:

Dog dog1 = new Dog("David", 3);
Pet dog2 = new Dog("Jimmy", 2);
dog1.PrintName();
dog2.PrintName();
dog1.PrintType();
//dog2.PrintType(); //dog2无法访问派生类的PrintType()方法

打印结果(按CTRL+F5调试程序):

I'm a dog, my name is David
My name is Jimmy
Dog

可以看到派生类的引用调用的是派生类的方法,基类引用调用的是基类方法。

统一提高效率

有时,需要一个容器(比如数组)保存所有的基类,基类描述了共同的属性和行为。

这样方便管理,容易扩展,前提是基类的引用能够指向派生类。

但是基类又不能涵盖所有的情况和变化,统一的行为方法往往在基类和派生类中有所区别。

这就要用到虚方法多态

虚方法和多态

虚方法:声明为virtual的方法就是虚方法。基类的虚方法可以在派生类中使用override进行重写。

多态:通过指向派生类的基类引用,调用虚函数,会根据引用所指向派生类的实际类型,调用派生类中的同名重写函数。

示例:

修改Pet类的定义:

public class Pet
{
    ....
    //新增一个虚方法
    virtual public void Speak()
    {
        Console.WriteLine(Name + " is speaking");
    }
}

修改Dog类:

public class Dog:Pet
{
    ...
    //重写基类方法
    public override void Speak()
    {
        // base.Speak();
        Console.WriteLine("Bow~Bow");
    }
}

新增Cat类:

public class Cat:Pet
{
    ...
    //重写基类方法
    public override void Speak()
    {
        // base.Speak();
        Console.WriteLine("Meow~Meow");
    }
}

然后来调用一下:

Pet dog = new Dog("David", 3);
Pet cat = new Cat("Tom", 2);
dog.Speak();
cat.Speak();

输出结果:

Bow~Bow
Meow~Meow

定义的两个派生类,覆写了基类的虚方法Speak(),用基类类型的引用时,调用的是派生类覆写后的方法。

当使用基类引用指向派生类时,不同的派生类对象我们也可以一起放入基类类型的数组中,统一管理。

注意

  • 重写虚方法必须具有相同的可访问性,且基类方法不能是private
  • 不能重写static方法或者非虚方法
  • 方法,属性,索引器,事件,都可以声明为virtualoverride

派生类及构造函数

构造函数

派生类对象中,有一部分是基类部分,在执行派生类的构造函数体之前,将会隐式显式调用基类构造函数

在构造函数重载时,还可以用this关键字,调用本类中的其他构造函数。

比如Pet类中,我们先定义了一个初始化name的构造函数public Pet(string name),然后我们又扩展了一个同时初始化nameage的构造函数,这时我们已经有一个初始化name的函数了,我们就可以用this关键字引入它,然后只写初始化age的代码public Pet(string name, int age):this(name)

public class Pet
{
    public Pet(string name)
    {
        Name = name;
    }

    public Pet(string name, int age):this(name)
    {
        //前面已经实现了name的初始化,这里只写age的代码即可
        Age = age;
    }
}

抽象类和抽象方法

抽象方法:

abstract public void Func();

抽象方法不能有函数体,必须在派生类中用override重写。

抽象成员

  • 必须是方法、属性、事件、索引
  • 必须用abstract修饰符标记
  • 不能有实现代码块

抽象类

  • 抽象类的存在只有一个目的,就是被继承
  • 抽象类不能实例化,用abstract修饰
  • 抽象类可以包含抽象成员和普通成员,以及他们的任意组合
  • 抽象类的抽象成员在派生类中需要用override关键字实现

类中只要有抽象成员,这就是一个抽象类,就需要用abstract修饰

密闭类和密闭方法

  • 密闭类:声明为sealed的类
  • 密闭方法:声明为sealed的方法

为什么需要密闭:

  • 密闭类:有些类不希望其他人通过继承来修改
  • 密闭方法:不希望其他人重写该方法

接口

接口就是指定一组函数成员,而不实现他们的引用类型

可以实现多个接口,接口的实现要写在基类后面:

结构和类

不同点:

  • 结构是值类型(在栈中),类是引用类型(在堆中)
  • 结构不支持继承,类支持继承
  • 结构不能定义默认构造函数,编译器会定义

适用场合:

结构:由于内存分配块,作用域结束即被删除,不需要垃圾回收,用于小型数据结构。但传递过程中会复制,应该使用ref提高效率。

类:用于其他的需要继承体系的场合。

静态类和静态成员

静态成员

标识为static的字段、方法、属性、构造函数、事件,就是静态成员。

静态成员将被类的所有实例共享,所有实例都访问同一内存位置。

生命周期:独立于任何实例,没有实例也可以访问。其初始化语句在任何静态成员使用之前调用。

静态函数:

静态函数也独立于任何实例,没有实例也可以调用。

静态函数不能访问实例成员,仅能访问其他静态成员。(反之可以)

静态构造函数:

  • 静态构造函数用于初始化静态字段
  • 在引用任何静态成员之前,和创建任何实例之前调用
  • 与类同名,使用static修饰,无参数,无访问修饰符

静态类

  • 如果类只包含了静态的方法和属性,并且标识为static
  • 静态类不能创建实例,不能被继承
  • 可以为静态类定义一个静态构造函数

作用:主要用于基础类库(如数学库)和扩展方法

扩展方法

要求:

  • 扩展方法所属的类,必须是static类
  • 扩展方法本身必须是static方法
  • 扩展方法的第一个参数类型,必须是this + 类名

假如我们有一个Dog类,这个类不能被继承,也不能被修改,那么我们想要扩展这个类,就只能使用静态类来进行扩展。方法如下:

static class PetGuide
{
    public static double HowToFeed(this Dog dog)  //注意此处的this
    {
        ...
    }
}

我们新建了一个类,添加了一个HowToFeed()方法,注意方法形参中的this关键字,这样定义后,Dog类就可以直接使用此方法,就像是自己的方法一样:

Dog dog = new Dog();
dog.HowToFeed(); //可以直接调用

重载操作符

装箱和拆箱

装箱

装箱:根据值类型的值,在堆上创建一个完整的引用类型对象,并返回对象的引用,是一种隐式转换

为什么需要装箱:有时候需要将值类型转化为引用类型(比如object)来进行统一的操作和统一的存储。

示例:

int i = 3;
object oi = null;
oi = i; //装箱操作

装箱过程就是新创建了一个对象oi,它具有值3

装箱的本质就是在堆上创建了引用类型的副本,新创建的引用类型和原来的值类型相互独立

拆箱

拆箱:将装箱后的对象转换回值类型的过程,是一种显示转换。

示例:

int i = 3;
object oi = i;
int j = (int)oi; //拆箱

自定义转换

为自己的结构或者类定义显示和隐式转换。

让我们自己的结构或者类可以变成一个预期相关的类型,并且使这种转换更加简单。

隐式转换语法:

public static implicit operator Dog(Cat cat)
{
    ...
}

显示转换语法:

public static explicit operator Dog(Cat cat)
{
    ...
}

调用方式:

Cat cat = new Cat("Tom");
Dog dog = cat;  //隐式转换
Dog dog = (Dog)cat; //显式转换

重载运算符

定义:利用现有的某种运算符,针对自定义类或者结构,定义某种运算操作。

语法:

public static Dog operator +(Dog male, Dog female)
{
    //重载加法运算
    ...
    return new Dog();
}

哪些运算符可以重载:

泛型

泛型类

泛型类就是一个模型类,可以传入不同类型的对象或者值。

泛型类优势:

  • 代码量小
  • 只有需要的类型才会被实例化
  • 易于维护,修改模板,所有的实例都将改变

泛型方法

泛型方法就是方法的模型,给定具体的类型,就可以实例化出一个操作该类型的具体方法。(泛型类中一般有泛型方法,普通类中也可以有泛型方法)

约束

约束就是控制泛型,缩小泛型参数的范围。

只有添加了约束,才能调用泛型参数中(比如T)的方法,否则只能调用object的方法。

约束的类型:类名,class,struct,接口名,new()

泛型接口

泛型接口允许我们将接口成员的参数和返回类型设置为泛型参数的接口。

集合

集合就是一种存放多个数据的容器类型,比如数组Array(完整的类型是System.Array

预定义的常用集合:

  • 动态数组:ArrayList
  • 列表:List
  • 字典:Dictionary
  • 队列:Queue
  • 栈:Stack

动态数组ArrayList

  • 初始化,可以不指定大小
  • 获取长度,使用Count属性
  • 添加Add
  • 删除RemoveRemoveAt
  • 访问[index]

ArrayList可以存放不同类型的变量(装箱操作),但是使用变量时需要拆箱。使用时要引入命名空间using System.Collections

ArrayList dogs = new ArrayList();
dogs.Add(new Dog("David"));
((Dog)dogs[0]).PrintName();

List<T>

由于ArrayList是类型不安全的,而且有装箱拆箱的性能问题,于是出现了List<T>

List dogs = new List();
dogs.Add(new Dog("David"));
dogs[0].PrintName();

字典Dictionary<Tkey, Tvalue>

字典容器存储的是一系列的键值对,每个值对应一个唯一的键。键的意义在于,我们可以通过键相对高效的访问到值。

字典操作:

  • 数量:Count
  • 添加:Add(key, value)
  • 删除:Remove
  • 访问:[key]
Dictionary dic = new Dictionary();
dic.Add("A", new Dog("A"));
dic["A"].PrintName();

栈是先进先出,后进后出的一种容器。

操作:

  • 出栈:Pop
  • 入栈:Push
  • 获取栈顶元素:Peek
Stack dogs = new Stack();
dogs.Push(new Dog("David"));
dogs.Peek().PrintName();
dog = dogs.Pop();

队列

队列是先进先出的容器。

操作:

  • 出队:Dequeue
  • 入队:Enqueue
Queue queue = new Queue();
queue.Enqueue(new Dog("David"));
queue.Enqueue(new Cat("Tom"));
queue.Dequeue().PrintName();

委托和事件

委托

委托就是持有一个或者多个方法的对象。并且该对象可以执行,可以传递。

声明委托类型:

delegate void ActCute();

定义委托类型的对象:

ActCute actCute;

给委托对象赋值:

class Dog
{
    public void WagTail(){...}
}

class Cat
{
    public void InnocentLook(){...}
}

actCute = dog.WagTail;
actCute += cat.InnocentLook; //可以持有多个方法

使用委托类型

像调用函数一样使用委托类型:

actCute();

示例:

delegate void ActCute();

static void Main(string[] args)
{
    Dog dog = new Dog("David");
    Cat cat = new CS_Tests.Cat("Tom");
    ActCute act = null;
    act = dog.WagTail;
    act += cat.InnocentLook;
    act(); //连续执行两个方法
}

输出结果:

Wag tail
Innocent look

在调用委托时,会连续执行它所持有的所有方法。

Lambda表达式

匿名方法

del = delegate()
{
    ...
};

Lambda表达式

del = delegate(){...};
del = ()=>{...}; //定义一个匿名方法

示例:

delegate void ActCute();

static void Main(string[] args)
{
    Dog dog = new Dog("David");
    Cat cat = new CS_Tests.Cat("Tom");
    ActCute act = null;
    act = dog.WagTail;
    act += cat.InnocentLook;
    act += ()=>
    {
        Console.WriteLine("Finish");
    };  //Lambda表达式定义的匿名方法
    act();
}

输出结果:

Wag tail
Innocent look
Finish

事件

发布者和订阅者

发布者:通知某件事情发生的,就是发布者

订阅者:对某件事情关注的,就是订阅者

事件触发和注册

事件发生时,会通知所有关注该事件的订阅者

想在事件发生时被通知,必须注册以表示关注

事件发生时,通知订阅者,就是调用订阅者的注册函数。注册,就是告诉发布者调用哪一个注册函数

事件声明

delegate void Handler();

public event Handler NewDog; //Handler是委托类型,NewDog是事件名

NewDog是一个成员,并且会被隐式自动初始化为null

事件订阅

NewDog += 方法;
NewDog -= 方法;

方法可以是实例方法,静态方法,匿名方法,Lambda表达式

事件触发

if(NewDog != null)
{
    NewDog();
}

事件可以理解成是一种封装的受限制的委托

示例:

Dog类:

public class Dog:Pet
{
    int num;

    public delegate void Handler();
    public static event Handler NewDog;

    public Dog(){ }

    public Dog(string name) : base(name)
    {
        ++num;
        if(NewDog != null)
        {
            NewDog();
        }
    }
}

Client类:

class Client
{
    //当有新狗狗时,希望收到通知
    public void DogWant()
    {
        Console.WriteLine("Great! I want to see the new Dog!");
    }
}

事件订阅:

Client c1 = new Client();
Client c2 = new Client();
Dog.NewDog += c1.DogWant; //订阅事件
Dog.NewDog += c2.DogWant; //订阅事件
Dog newDog = new Dog("Jimmy");