Bootstrap

零基础自学C#——Part3: 面向对象编程基础

我们上一部分介绍了运算符,选择语句和循环语句、数组、类型转换,接下来开始编写我们自己的函数

编程的一条基本原则是“不要重复自己(DRY)”

编程的一条基本原则是“不要重复自己(DRY)”

编程的一条基本原则是“不要重复自己(DRY)”

一、面向对象编程

在C#中,可以使用C#关键字class(大多时候)和struct(偶尔)来定义对象的类型。

··封装是与对象相关的数据和操作的组合,将数据和函数等集合在一个个的单元(类)中,它是实现面向对象程序设计的第一步,想要访问对象的数据只能通过已经定义的接口,被封装的对象我们通常称为抽象数据类型,适当的封装可以提高安全性,通常我们通过private,protected,public来实现封装

··组合是指物体由什么构成

··聚合是指什么可以与对象相结合,通过聚合两个独立的对象,可以形成新的组件

··继承就是将公用的属性或方法抽离父类的过程

1:面向对象的设计原则


单一职责原则(SRP):

  (1)、SRP(Single Responsibilities Principle)的定义:就一个类而言,应该仅有一个引起它变化的原因。简而言之,就是功能要单一。

  (2)、如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其它职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。(敏捷软件开发)

  (3)、软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。

     小结:单一职责原则(SRP)可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。责任过多,引起它变化的原因就越多,这样就会导致职责依赖,大大损伤其内聚性和耦合度。

开放关闭原则(OCP)

  (1)、OCP(Open-Close Principle)的定义:就是说软件实体(类,方法等等)应该可以扩展(扩展可以理解为增加),但是不能在原来的方法或者类上修改,也可以这样说,对增加代码开放,对修改代码关闭。

  (2)、OCP的两个特征: 对于扩展(增加)是开放的,因为它不影响原来的,这是新增加的。对于修改是封闭的,如果总是修改,逻辑会越来越复杂。

     小结:开放封闭原则(OCP)是面向对象设计的核心思想。遵循这个原则可以为我们面向对象的设计带来巨大的好处:可维护(维护成本小,做管理简单,影响最小)、可扩展(有新需求,增加就好)、可复用(不耦合,可以使用以前代码)、灵活性好(维护方便、简单)。开发人员应该仅对程序中出现频繁变化的那些部分做出抽象,但是不能过激,对应用程序中的每个部分都刻意地进行抽象同样也不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。

里氏代替原则(LSP)
 
     (1)、LSP(Liskov Substitution Principle)的定义:子类型必须能够替换掉它们的父类型。更直白的说,LSP是实现面向接口编程的基础。

      小结:任何基类可以出现的地方,子类一定可以出现,所以我们可以实现面向接口编程。 LSP是继承复用的基石,只有当子类可以替换掉基类,软件的功能不受到影响时,基类才能真正被复用,而子类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

依赖倒置原则(DIP)

     (1)、DIP(Dependence Inversion Principle)的定义:抽象不应该依赖细节,细节应该依赖于抽象。简单说就是,我们要针对接口编程,而不要针对实现编程。

     (2)、高层模块不应该依赖低层模块,两个都应该依赖抽象,因为抽象是稳定的。抽象不应该依赖具体(细节),具体(细节)应该依赖抽象。

      小结:依赖倒置原则其实可以说是面向对象设计的标志,如果在我们编码的时候考虑的是面向接口编程,而不是简单的功能实现,体现了抽象的稳定性,只有这样才符合面向对象的设计。

接口隔离原则(ISP)

     (1)、接口隔离原则(Interface Segregation Principle, ISP)指的是使用多个专门的接口比使用单一的总接口要好。也就是说不要让一个单一的接口承担过多的职责,而应把每个职责分离到多个专门的接口中,进行接口分离。过于臃肿的接口是对接口的一种污染。

     (2)、使用多个专门的接口比使用单一的总接口要好。

     (3)、一个类对另外一个类的依赖性应当是建立在最小的接口上的。

     (4)、一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。

     (5)、“不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。”这个说得很明白了,再通俗点说,不要强迫客户使用它们不用的方法,如果强迫用户使用它们不使用的方法,那么这些客户就会面临由于这些不使用的方法的改变所带来的改变。

       小结:接口隔离原则(ISP)告诉我们,在做接口设计的时候,要尽量设计的接口功能单一,功能单一,使它变化的因素就少,这样就更稳定,其实这体现了高内聚,低耦合的原则,这样做也避免接口的污染。

   ⑥组合复用原则(CRP)

     (1)、组合复用原则(Composite Reuse Principle, CRP)就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分。新对象通过向这些对象的委派达到复用已用功能的目的。简单地说,就是要尽量使用合成/聚合,尽量不要使用继承。

     (2)、要使用好组合复用原则,首先需要区分”Has—A”和“Is—A”的关系。 “Is—A”是指一个类是另一个类的“一种”,是属于的关系,而“Has—A”则不同,它表示某一个角色具有某一项责任。导致错误的使用继承而不是聚合的常见的原因是错误地把“Has—A”当成“Is—A”.例如:鸡是动物,这就是“Is-A”的表现,某人有一个手枪,People类型里面包含一个Gun类型,这就是“Has-A”的表现。

     小结:组合/聚合复用原则可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏替换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。

迪米特法则(Law of Demeter)

      (1)、迪米特法则(Law of Demeter,LoD)又叫最少知识原则(Least Knowledge Principle,LKP),指的是一个对象应当对其他对象有尽可能少的了解。也就是说,一个模块或对象应尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立,这样当一个模块修改时,影响的模块就会越少,扩展起来更加容易。

      (2)、关于迪米特法则其他的一些表述有:只与你直接的朋友们通信;不要跟“陌生人”说话。

      (3)、外观模式(Facade Pattern)和中介者模式(Mediator Pattern)就使用了迪米特法则。

       小结:迪米特法则的初衷是降低类之间的耦合,实现类型之间的高内聚,低耦合,这样可以解耦。但是凡事都有度,过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

2:类(class)

其实关于类我们之前有简单的介绍过,类是一种抽象的概念,它其实是具有相同特性(数据元素)和行为(功能)的对象们的抽象。可以说类的实例是对象,类实际上就是一种数据类型。

①创建声明类

public class ClassName(假设不指定继承,类将从System.Object继承

*在C#中,关键字pulic位于class之前,public叫做访问修饰符,public访问修饰符表示允许所有其他代码访问这个ClassName类。创建新的类意味着在当前项目中产生了一种新的数据类型。

②创建类的成员

成员可以是字段、方法或它们二者的特定版本

     字段:有常量字段、只读字段、事件

常量字段:数据永远不变,编译器会将数据复制到读取它们的任何代码中

只读字段:在类实例化后,数据不能改变

事件:数据引用一个或多个方法,方法在发生事件时执行(详情见后续)

     方法:用于执行语句,有构造函数、属性、索引器、运算符

构造函数:使用new关键字分配内存和实例化类时执行的语句。

属性:获取或设置数据时执行的语句,属性是封装字段的首选方法,除非需要公开字段的内存地址

索引器:使用数组语法[]获取或设置数据时执行的语句

运算符:对类型的操作数进行运算符执行

     第一种:无static的访问方式(变量为普通变量)

public class MyClass  //>>>>>>>>>创建了类,声明了变量和函数
{
    public int a=10;           //创建了int普通成员
    public void Func1()        //创建了func1普通成员函数
    {
        Debug.Log("Func1 Called");
    }
}

     第二种:有static的访问方式(静态变量)

*static声明的类内的变量,称为类的静态成员变量

*static声明的类内的函数,成为类的静态成员函数

public class MyClass  
{
    public static int a=10;      //静态成员变量
    public static void Func1()   //静态成员函数
    {
        Debug.Log("Func1 Called");
    }
}

③实例化类

用类的定义来创建一个实例,就叫做类的实例化。

实例化的具体方法:类  对象= new 类()

public class MyClass 
{
    public int a=10; 
    public void Func1()
    {
        Debug.Log("Func1 Called");
    }
}
public class StudyBaseClass : MonoBehaviour
{
    void Start()
    {
        MyClass myClass=new MyClass();  //使用了new进行类的实例化,
    }

④类的访问

在类的内部声明的变量或函数,若想访问,必须通过类名进行访问

无static时访问如下:可以直接访问实例化后的类名

int b=myClass.a;

有static时访问如下:必须访问实例化之前的类名

int b=MyClass.a;

3:类---字段中存储数据

①:定义字段

假设一个人的信息由姓名和出生日期组成,在Person类内部封装这两个值,他们在类外可见,可以对字段使用任何类型,包括数组和集合等。

public class Person: Object
{
  public string Name;
  public DateTime DateOfBirth;
}

②:理解访问修饰符

封装的一部分时选择成员的可见性。

访问修饰符有4个,并且有两种组合可以应用到类内成员

private成员只能在类内访问,这是默认设置                        
internal成员可在类型内部或者同一程序集的任何类型中访问
protected成员可以在类型内部或从类型继承的任意类型中访问
public成员可在任何地方进行访问
internal protected成员可在类型内部或者同一程序集、从类型继承的任何类型中访问,相当于internal_or_protected
private protected成员可在类型内部或者同一程序集、从类型继承的任何类型中访问,相当于internal_and_protected

4:与类同名的构造函数和析构函数

Using system;

nameSpace LineApplication
{
  Public class Line
    {
      Private double length; //线条的长度
      Public Line()//  构造函数写法,与类名同名;
      ~Line()//      析构函数写法,与类名同名,前加~;
    }
}

类名为Line

构造函数为Public Line():构造函数分为有参数和无参数的,没有任何返回值类型的声明。发生在new实例时候,会被自动执行。可以携带参数,当你显示写了自己的构造函数后,系统将不再为你提供默认的无参构造函数了。

析构函数为~Line():析构函数没有参数也没有任何返回类型的声明,析构函数在对象销毁时自动被调用

作用域的问题:函数内声明的变量(包括类对象),在执行完本函数时,会被自动销毁,但是,在类内声明的变量(包括类对象),只有当前类被销毁时,它管理的其他变量(包括类对象)才会被销毁。

5:继承

继承允许我们根据一个类来定义另一个类,这使得创建和维护应用程序变得容易,已有的类称之为基类(父类),新类为派生类(子类),C#不支持多重继承。

public class Polygon
{
 public int Length;
 public int Width;
 public string color;
 public string name;
}

class Rectangle:Polygon
{
 public Rectangle()
 {
 }
 
 ~Rectangle()
 {
 }
}

class Triangle:Polygon
{
 public Triangle()
 {
 }
 
 ~Triangle()
 {
 }
}

void Start()
{
 Rectangle recTangle=new Rectangle();//实例化子类
 recTangle.Length=3;
 recTangle.Width=3;
 recTangle.name="矩形";
 recTangle.color="红色";
 Debug.Log(recTangle.color+"多边形"+recTangle.name+",长"+recTangle.Length+",宽"+recTangle.Width);
 
 三角形代码同上
}

6:封装

   隐藏技术细节,通过接口的形式暴露给第三方,而不需要关心细节,有两种封装的方法,分别是

枚举enum进行封装,覆盖override和virtual。

①:枚举以及枚举封装

(1)枚举:enum类型是一种非常有效的方式,可以存储一个或多个选项,因为在内部,enum类型结合了整数值和使用字符串描述的查找表。

     每一个枚举元素都是有枚举值,默认情况下,第一个枚举值为0,后面的枚举值依次递增,可以修改值,后面的枚举数的值依次递增。(第一个写100,第二个即为101)

     枚举元素默认为Int,准许使用的枚举类型有byte,sbyte,short,ushort,int,long,ulong

enum  Name :long    //(不修改默认为int,加:可修改)
{
   Up=0,
   Down=1,
   Left=1,
   Right=1,
}

     具体的实现如下:

public class EnumTest : MonoBehaviour
{

   public enum emAction
   {
       None=0,
       Getup,//0+1=1
       Wash,//1+1=2
       Eat,//2+1=3
       Play//3+1=4
   }
    //定义成员变量
    public emAction mAction=emAction.Play;
    
    void Start()
    {
        Func1();
    }
    
    void Update()
    {
        
    }
    void Func1()
    {
        Debug.Log(mAction);

(2):枚举和int的互相转换

枚举→int

int iPlay=(int)mAction;
   Debug.Log("Enum-->int:"+iPlay);

int→枚举(两种方式)

mAction=(emAction)3;
    Debug.Log("int-->Enum:"+mAction);
mAction=(emAction)Enum.ToObject(typeof(emAction),1);
    Debug.Log("int-->Enum"+mAction);

(3)枚举和string的互相转换

枚举→string

mAction.ToString()

string→枚举

mAction=(emAction)Enum.Parse(typeof(emAction),"Wash" );
  Debug.Log( "string-->Enum"+mAction );

(4)Switch+枚举,实现特定显示

  switch (mAction)
       {
        case emAction.Getup:
            Debug.Log("1.起床");
         break;
        case emAction.Wash:
            Debug.Log("2.洗漱");
         break;  
        case emAction.Eat:
            Debug.Log("3.吃饭");
         break;
        case emAction.Play:;
            Debug.Log("玩");
         break;
        default:
        Debug.Log("不知道做什么");
        break;
       }

(5):利用进行枚举封装

public enum POLYGON
{
    Rectangle,
    Triangle,
}
public class Polygon
{
    public int Length;
    public int Width;
    public string color;
    public string name;
    public float Area;
    public POLYGON emPolygon;//成员变量,因为枚举也是数据类型,类似int a,后面是名字
    public void ShowBaseInfo()
    {
        Debug.Log(color+"多边形"+name+",长"+Length+",宽"+Width);//
    }
    public void CalcArea()
    {
        if (emPolygon==POLYGON.Rectangle)
        {
            Area=Length*Width;
        }
        else if(emPolygon==POLYGON.Triangle)
        {
            Area=Length*Width/2;
        }
    }

}

class Rectangle:Polygon
{
    public Rectangle()
    {

    }

    ~Rectangle()
    {

    }
}
class Triangle:Polygon
{
    public Triangle()
    {

    }

    ~Triangle()
    {

    }
}
public class inhe: MonoBehaviour
{
    void Start()
    {
        Rectangle recTangle=new Rectangle();
        recTangle.emPolygon=POLYGON.Rectangle;
        recTangle.Length=3;
        recTangle.Width=4;
        recTangle.name="矩形";
        recTangle.color="红色";
        recTangle.ShowBaseInfo();
        recTangle.CalcArea();
        Debug.Log("矩形的面积"+recTangle.Area);

        Triangle triangle=new Triangle();
        triangle.emPolygon=POLYGON.Triangle;
        triangle.Length=3;
        triangle.Width=4;
        triangle.name="三角形";
        triangle.color="黄色";
        triangle.ShowBaseInfo();
        triangle.CalcArea();
        Debug.Log("三角形的面积"+triangle.Area);
    }

(6)利用override以及virtual进行封装 *

virtual代表虚函数,意味着子类可以覆盖(覆写)其实现;如果子类不覆盖,将使用父类的同名函数

public enum POLYGON
{
    Rectangle,
    Triangle,
}
public class Polygon 
{
    public int Length;
    public int Width;
    public string color;
    public string name;
    public float Area;    
    public POLYGON emPolygon;
    
    public void ShowBaseInfo()
    {
        Debug.Log(color+"多边形"+name+",长"+Length+",宽"+Width);//
    }
    public virtual void CalcArea() 

    {
     if(emPolygon==POLYGON.Rectangle)
     {
        Area=Length*Width;
     }
     else if (emPolygon==POLYGON.Triangle)
     {
        Area=Length*Width/2;
     }
    }

}

class Rectangle:Polygon 
{
    public Rectangle()
    {

    }

    ~Rectangle()
    {
        
    }
    public override void CalcArea()//override代表覆盖
    {
        Area=Length*Width;
    }
}
class Triangle:Polygon  
{
    public Triangle()
    {

    }

    ~Triangle()
    {
        
    }
    public override void CalcArea()
    {
        Area=Length*Width/2;
    }
}
public class inhe: MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Rectangle recTangle=new Rectangle();
        recTangle.emPolygon=POLYGON.Rectangle;
        recTangle.Length=3;
        recTangle.Width=4;
        recTangle.name="矩形";
        recTangle.color="红色";
        recTangle.ShowBaseInfo();
        recTangle.CalcArea();
        Debug.Log("矩形的面积"+recTangle.Area);

        Triangle triangle=new Triangle();
        triangle.emPolygon=POLYGON.Triangle;
        triangle.Length=3;
        triangle.Width=4;
        triangle.name="三角形";
        triangle.color="黄色";
        triangle.ShowBaseInfo();
        triangle.CalcArea();
        Debug.Log("三角形的面积"+triangle.Area);
    }

7:多态

表示同一个接口,使用不同的实例而执行不同操作。通俗来说,就是多种形态,当不同的对象去完成时会产生出不同的状态。

 静态多态(编译时):在编译时,函数和对象的连接机制被成为早期绑定,也称静态绑定,C#提供了两种技术来实现静态多态性,分别为函数重载和运算符重载。

 动态多态(运行时):在运行前无法确认哪个方法,只有在运行时才能确定,根据实例对象,执行同一个函数的不同行为。

public enum POLYGON    //创建枚举和名称,并规定枚举成员
{
    Rectangle,
    Triangle,
}
public class Polygon   //创建多边形父类
{
    public int Length;   //声明成员变量长度
    public int Width;     //声明成员变量宽度
    public string color;   //声明成员变量颜色,为字符型
    public string name;    //声明字符型名字
    public float Area;     //声明浮点型面积
    public POLYGON emPolygon;  //声明枚举成员变量
    
    public void ShowBaseInfo()   //声明成员函数,多边形的颜色,长宽高
    {
        Debug.Log(color+"多边形"+name+",长"+Length+",宽"+Width);//
    } 
    public virtual void CalcArea()  //声明虚函数,多边形面积
    {
     
    }
    public virtual void Show()          //声明虚函数Show
    {
        Debug.Log("Poylgon Show");
    }

}

class Rectangle:Polygon           //子类继承
{
    public Rectangle()             //声明构造函数
    {

    }
 
    ~Rectangle()                       //声明析构函数
    {
        
    }
    public override void CalcArea()      //重写面积方法
    {
        Area=Length*Width;
    }
        public override void Show()       //重写show方法
    {
        Debug.Log("Rectangle Show");
    }
}
class Triangle:Polygon                    //同上
{
    public Triangle()
    {
    }

    ~Triangle()
    {
    }
    public override void CalcArea()
    {
        Area=Length*Width/2;
    }
    public override void Show()
    {
        Debug.Log("Triangle Show");
}
public class inhe: MonoBehaviour
{
    void Start()
    {
        Rectangle recTangle=new Rectangle();              //对矩形实例化
        recTangle.emPolygon=POLYGON.Rectangle;            //对矩形枚举进行赋值
        recTangle.Length=3;                                //矩形长度赋值
        recTangle.Width=4;                               //矩形宽度赋值
        recTangle.name="矩形";                              //矩形名字赋值
        recTangle.color="红色";                            //矩形颜色赋值
        recTangle.ShowBaseInfo();                          //调用SHowBaseInfo函数
        recTangle.CalcArea();                              //调用面积函数,此时调用重写后的
        Debug.Log("矩形的面积"+recTangle.Area);

        Triangle triangle=new Triangle();
        triangle.emPolygon=POLYGON.Triangle;
        triangle.Length=3;
        triangle.Width=4;
        triangle.name="三角形";
        triangle.color="黄色";
        triangle.ShowBaseInfo();
        triangle.CalcArea();
        Debug.Log("三角形的面积"+triangle.Area);

        //          父           子
        //          子           父    
            只能子给父赋值,父不能给子赋值。
        Polygon baseParent1 =recTangle;                    //创建父类对象。将矩形类赋值给它
        Polygon baseParent2 =triangle;

        baseParent1.Show();
        baseParent2.Show();                                //调用Show方法

8:覆盖和重载

覆盖(override):发生在 继承关系中,通过virtual和override;函数名和函数参数是一样的。

重载(overolad):发生在 任何关系中,只要保证函数名字一致,参数不一致带参数顺序不一致,或者是参数个数不一致也是可以的,即可实现重载。

     重载的主要好处就是不用为了对不同的参数类型或参数个数,而写多个函数。多个函数用同一个名字,但参数表,即参数的个数或(和)数据类型可以不同,调用的时候,虽然方法名字相同,但根据参数表可以自动调用对应的函数。

//定义了几种相同的函数名,而内部参数或顺序不同。 
    public void ShowFunc()
    {

    }
    public void ShowFunc(int a)
    {

    }
    public void ShowFunc2(float c,int a,string b)
    {

    }   
    public void ShowFunc2(float c,string b,int a)
    {

    }   
    public void ShowFunc3(int a,string b,float c)
    {

    }

;