面向对象
C#是面向对象的语言,面向对象(Object Oriented,OO)是一种编程思想,强调系统的结构应该直接与现实世界的结构相对应,应该围绕现实世界中的对象来构造系统,而不是围绕功能来构造系统。
所有面向对象的语言都具有三个基本的特征,它们是:
- 封装:把客观事物封装成类,并将类内部实现隐藏,以保证数据的完整性。
- 继承:通过继承可以复用父类的代码。
- 多态:允许每个对象以适合自身的方式去响应共同的消息。
C#中的面向对象编程指的是运用这三个基本的特征来编写程序。
封装
封装指的是把类的内部实现隐藏起来,不让对象直接对其进行操作。C#提供了属性机制对内部的状态进行操作。在C#中,封装可以通过public,protected,private,internal来体现。为什么使用封装,因为有的时候,我们把内部的数据定义为public之后,外部对象可以进行任意的操作,很可能导致不符合逻辑规则。面向对象的封装特性,是一种保护状态数据完整性的方法,在面向对象编程中,应更多的定义私有字段,并且使用属性机制来对私有字段进行访问。
封装的好处是数据安全,提供重用性,分工合作和方便构建大型复杂的项目
继承
在C#中,一个类可以继承另外一个已有的类(密封类除外),被继承的类称为基类(或者父类),继承的类称为派生类,子类将获得基类除构造函数和析构函数以外的所有成员。此外,静态类也不能被继承。通过继承,程序可以实现对父类代码的复用。因为子类可以继承父类的所有成员,父类中定义的代码便不需要在子类中重复定义了。
使用了继承之后,当我们初始化一个子类时,除了会调用子类的构造函数之外,同时也会调用基类的构造函数,子类的初始化顺序如下:
1:初始化类的实例字段
2:调用基类的构造函数,若没有基类,调用System.Object的构造函数。
3:调用子类的构造函数。
多态
由于可以继承基类的成员,子类就有了相同的行为,但是有时子类的某些行为需要相互区别。子类需要覆盖父类中的方法来实现子类特有的行为,这就是多态。多态即相同类型的对象调用相同的方法却表现出不同行为的现象。当派生类从基类继承时,它会获得基类的所有方法、字段、属性和事件。面向对象的语言使用虚方法表达多态。若要更改基类的数据和行为,您有两种选择:可以使用新的派生成员替换基成员,或者可以重写虚拟的基成员。
通过接口和抽象类实现多态
实践中,可以通过接口和抽象类实现多态。
如下,多个不同的子类可以通过实现IExtend接口,实现对同一个方法有不同的行为。子类也可以通过override抽象父类的抽象方法从而实现不同的行为。但是可以注意到,我们在调用BasePhone的Video方法,会产生编译报错,即使我们实例化的是一个iPhone,具有Video方法,编译器还是不能读取到该信息,因此我们需要用dynamic关键字避开编译器的检查。
class Program
{
static void Main(string[] args)
{
BasePhone phone = new iPhone();
phone.System();//运行时多态
//phone.Video(); 编译报错,这是因为编译器的限制;实际在运行时是正确的
IExtend extend = new iPhone();
extend.Video();//运行时多态
//extend.Call() //
dynamic dPhone1 = phone; //通过dynamic关键字避开编译器的检查
dPhone1.Video();
dynamic dPhone2 = extend;
dPhone2.Call();
}
}
public interface IExtend
{
void Video();
}
public abstract class BasePhone
{
public void Call()
{
Console.WriteLine($"Use {this.GetType().Name} Call");
}
public abstract void System();
}
public class iPhone : BasePhone, IExtend
{
public void Video()
{
Console.WriteLine($"{this.GetType().Name} Video");
}
public override void System()
{
Console.WriteLine($"{this.GetType().Name} System is IOS");
}
}
public class Mi : BasePhone, IExtend
{
public void Video()
{
Console.WriteLine($"{this.GetType().Name} Video");
}
public override void System()
{
Console.WriteLine($"{this.GetType().Name} System is Android");
}
}
那么,在实践过程中,如何选择抽象类和接口呢?
我们需要了解抽象类和接口的特点。对于接口而言,是约束,多实现,更加灵活,语义方面有点像:can do,可以说“实现接口的子类 can do 接口约束”。抽象类 是父类+约束,可完成通用的实现,只能单继承,语义方面有点像: is a,可以说“子类is a 父类”。
因此,有的子类有,有的子类没有的情况,就要考虑用接口。
选择接口还是抽象类的原则:子类都一样的,放在父类;子类都有但是不同,放在父类抽象一下;有的子类有,有的子类没有,那就用接口。
一般情况,接口用的更多,因为接口更简单灵活 除非有些共有的需要继承。
抽象方法和虚方法的区别和选择?
最本质的区别:虚方法带有实现(方法体),可以被重写;抽象方法没有实现,必须被重写(override)。
在实践中,虚方法,抽象方法在继承中的行为也不同,如下所示。
class Program
{
static void Main(string[] args)
{
//虚方法/抽象方法,程序编译时候,左边遇到virtual/abstract时,做个标记,等程序运行的时候,判断右边是否有override,如果有,就以右边为准
ParentClass parent = new Child();
parent.CommonMethod();//普通方法,父类为准;这个是编译时确定的,特点是:效率高
parent.VirtualMethod1();//虚方法,子类没重载,左边为准
parent.VirtualMethod2();//虚方法,子类有重载,右边为准;运行时确定,特点是为了灵活
parent.AbstractMethod();//抽象方法,右边为准;运行时确定,特点是为了灵活
Console.Read();
}
}
public class Child : ParentClass
{
public override void VirtualMethod2()//子类重载了父类的 VirtualMethod2
{
Console.WriteLine("我是子类,重载虚方法2");
}
public sealed override void AbstractMethod() //sealed保证子类不能再重写该方法
{
Console.WriteLine("我是子类,实现后的抽象方法");
}
}
public abstract class ParentClass
{
/// <summary>
/// 普通父类方法
/// </summary>
public void CommonMethod()
{
Console.WriteLine("我是父类,普通方法");
}
/// <summary>
/// 父类虚方法,必须包含实现,可以被重载
/// </summary>
public virtual void VirtualMethod1()
{
Console.WriteLine("我是父类,虚方法1");
}
public virtual void VirtualMethod2()
{
Console.WriteLine("我是父类,虚方法2");
}
/// <summary>
/// 我是抽象方法
/// </summary>
public abstract void AbstractMethod();
}
参考:
面向对象详解
C#面向对象基本概念总结