C#学习笔记--面向对象三大特征

C#核心

面向对象--封装

用程序来抽象现实世界,(万物皆对象)来编程实现功能。

三大特性:封装、继承、多态。

类与对象

声明位置:namespace

样式:class 类名{}

命名:帕斯卡命名法(首字母大写)

实例化对象:根据类来新建一个对象。Person p=new Person();

成员变量

  1. 声明在类语句块中
  2. 用来描述对象的特征
  3. 可以是任意变量类型
  4. 数量不做限制
  5. 是否赋值根据需求决定
enum E_SexType
{
    Man,
    Woman
}
struct Position{}//位置结构体
class Pet{}//宠物类
//类中的成员变量
class Person
{
    public string name="TonyChang";//区别于结构体--可以默认赋初始值
    public int age=21;
    public E_SexType sex;
    public Person bestFriend;//区别于结构体---类中可以有同类的成员类型(本质是因为类属于引用类型,但不可以实例化,防止反复new,陷入死循环)
    public Position pos;
    public Pet pet;
}

成员类型的默认值:

值类型的:数字的为0,bool类型的false

引用类型:null

查看(int类型)默认值:default(int)

补充:class属于引用类型,其中的值类型也放置在中。

成员方法

  1. 声明在类语句块中
  2. 用来描述对象行为
  3. 其返回值参数不做限制
  4. 数量不做限制
  5. 帕斯卡命名法(首字母大写)

成员方法只有在实例化之后才可以使用调用。具体的一个对象的行为(方法),必须具体的对象调用。

//成员方法
class Person
{
    public string name;
    public int age;
    public void Speak()
    {
        Console.WriteLine("你好!");
    }    
}

//成员方法的使用
Person p=new Person;
p.Speak();

构造函数和析构函数

默认有一个无参构造函数,而类中可以允许自己声明无参构造函数,而结构体不行。

一旦有自定义的构造函数,默认的无参构造函数则失效!

构造函数:

  1. public修饰
  2. 无返回值,名字和类名相同
class Person
{
    public string name;
    public int age;
    //构造函数
    public Person()
    {
        name="TonyChang";
        age=21;
    }
    //此时先调用age参数的构造函数 然后再调用两个参数的构造函数
    public Person(string name,int age):this(age)
    {
        this.name=name;
        this.age=age;
    }
    public Person(string name)
    {
       this.name=name;
    }
    public Person(int age)
    {
        this.age=age;
    }
}

特殊的构造函数,在调用该函数之前先调用this的无参构造函数。

public Person(int age):this()
{
this.age=age;
}

析构函数:

由于C#中有自动的垃圾回收机制,一般不使用析构函数。

析构函数是当垃圾真正被回收时候才会调用。

~Person(){}//析构函数

成员属性:

用于保护成员变量,为成员属性的获取和赋值添加逻辑处理。

//成员属性 帕斯卡命名法
class Person
{
    private string name;
    public string Name
    {
        get{
            return name;
        }
        set{
            name=value;
        }
    }
    private int age;
    public int Age
    {
        //不可以获得年龄(或者删除set设置则可表明也无法获取age)
        private get=>age;
        //可以设置年龄
        set
        {
            age=value;
        }
    }
    //额外:
    //自动成员属性 (对于没有特殊需要的成员)
    public float Height
    {
        get;
        set;
    }
}

索引器

可以让对象像数组一样通过索引来访问其中的元素。

注意:结构体中也支持索引器。

//索引器
class Person
{
    private string name;
    private int age;
    private Person[] friends;
    private int[,] arry;
    //索引器
    public Person this[int index]
    {
        get
        {
            return friends[index];
        }
        set
        {
            //此处可以写一些控制逻辑
            friends[index]=value;
        }
    }
    //索引器的重载
    public int this[int i,int j]
    {
        get 
        {
            return array[i,j];
        }
        set
        {
            array[i,j]=value;
        }
    }
}
//使用
Person p=new Person();
p[0]=new Person();//可以像数组一样进行访问

静态成员

static修饰的成员变量/方法为静态成员。

静态成员归属为类,即不用初始化就可以使用的类的成员。

静态成员函数中不可以使用非静态成员,非静态成员函数中可以使用静态成员函数。

本质:在程序运行开始时,检查到静态成员,会再特定的区域为其开辟内存空间来存储。所以说静态成员与程序共生死。因为生命周期不同,所以静态成员函数中不可以使用非静态成员。

使用:全局性,稳定不变的常量。例如固定的数值 Π,重力加速度g等包括固定的计算方法,可以供全局成员来访问使用。但是静态过多会占用内存,引发GC。

常量与静态成员

相同点:都可以通过类名点来使用

不同点:

  1. const 必须初始化,不能修改,而static可以
  2. const只能修饰变量,而static可以修饰很多
  3. const一定是写在访问修饰符的后面,static则无此要求

静态类与静态构造函数

用 static修饰的类为静态类,往往来作为工具类。例如System中的Console类。只能包含静态成员,不能实例化。

静态构造函数 :在静态构造函数中初始化静态成员变量。

  1. 静态类和普通类中均可以有
  2. 不能使用访问修饰符
  3. 不能有参数
  4. 只会自动调用一次
//静态构造函数
static class Test
{
    public static int testInt=100;
    public static float testFloat=20.5f;
    static Test()
    {
        //静态构造函数
        Console.WriteLine("自动调用了!");
    }
}
class NormalTest
{
    public static int i=5;
    //首次使用静态成员时候 自动调用一次
    //静态成员函数
    static NormalTest()
    {
        Console.WriteLine("静态构造函数");
    }
    public NormalTest()
    {
        Console.WriteLine("非静态成员函数");
    }
}

拓展方法

拓展方法为现有的非静态变量类型添加新方法。

作用:

  1. 提升程序的拓展性
  2. 不需要再对对象中重新写方法
  3. 不需要继承来添加方法
  4. 为别人封装的类型添加额外的方法

特点:

  1. 一定写在静态类中
  2. 一定是个静态函数
  3. 第一个参数为拓展目标(为谁而拓展)
  4. 第一个参数用this修饰
//拓展方法
static class expendTool
{
    //为int添加拓展方法
    public static void SpeakValue(this int value)
    {
        Console.WriteLine("这是int的拓展方法,int的数值为{0}",value);
    }
    //为string拓展方法
    public static void SpeakStringInfo(this string str,string name,string info)
    {
         Console.WriteLine("这是string的拓展方法,string的数值为{0},该拓展方法由{1}编写,拓展内容为{2}",str,name,info);
    }
}
class Program
{
    static void Main(string[] args)
    {
        int i = 5;
        i.SpeakValue();
        string ss = "原始字符串";
        ss.SpeakStringInfo("TonyChang", "附加字符串");
    }
}

注意:如果拓展方法名称与自身现有的方法名称相同,则只会调用自身的方法,不会调用拓展的方法。

运算符重载

关键字 operator

特点:1. 一定是一个公共的静态成员方法

  1. 返回值写在operator前
  2. 逻辑处理自定义

作用:可以使自定义的数据类型来完成后相同意义的运算。

注意:

  1. 条件运算符要成对实现(有>必须有<)
  2. 一个符号可以有多个重载
  3. 不能使用ref与out
//运算符重载
class Point
{
    public int x;
    public int y;
    public Point(int x,int y)
    {
        this.x=x;
        this.y=y;
    }
    
    //重载+运算符
    //参数列表中必须要有自己的类别出现
    public static Point operator +(Point p1,Point p2)
    {
        Point sum=new Point();
        sum.x=p1.x+p2.x;
        sum.y=p1.y+p2.y;
        return sum;
    }
}
class Program
{
    static void Main(string[] args)
    {
        Point p1=new Point(1,1);
        Point p2=new Point(2,2);
        Point p3=P1+p2;
        Console.WriteLine(p3.x);
    }
}

补充:大部运算符可以重载,逻辑运算符中只可以允许重载 逻辑非!

不能重载的运算符有:

&& || 索引符[] 强制转换符号() 特殊运算符 点. 三目运算符?:

*内部类和分部类(了解)

//内部类
class Person
{
    public class Body
    {
        class Arm
        {
            
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
       Person.Body body=new Person.Body();
    }
}
//分布类
//分布类可以分布在不同的脚本文件中
//分布类的访问修饰符要一致
partial class Student
{
    public string name;
    public bool sex;
    partial void Speak();
}
partial class Student
{
    public int age;
    public string stuId;
    partial void Speak()
    {
        Console.WriteLine("分布方法的具体实现");
    }
}

垃圾回收机制

垃圾回收,英文简称GC(Garbage Collector)

垃圾回收过程:遍历堆(Heap)上的动态分配的所有对象

通过识别它们是否被引用来确定其是否是垃圾。垃圾是指没有引用所指引的对象、变量,需要被回收释放掉占用的内存空间。

垃圾回收算法:

引用计数、标记清除、标记整理、复制集合。

注意:垃圾回收只回收heap堆上的 栈中的内存由系统管理

回收机制: 三代内存

0代内存 1代内存 2代内存

  1. 每一代内存满掉之后便会清理垃圾

  2. 高代连锁:1代清理会连带0代清理,2代清理连带0代和1代

  3. 清理完垃圾之后,非垃圾内容搬迁到下一代中(0代将非垃圾转移到1代内存,

    1代内存将非垃圾转移到2代内存)所以2代内存存储的较为老的对象实例,还包括大的对象

    一般是85kb以上的对象

  4. 0代1代的读取速度要高于1代,分配内存位置优先0代>1代>2代

手动GC:

GC.Collect();

一般在场景加载时候进行GC。


面向对象--继承

继承者(子类)继承父类(基类、超类)的特性,同时也可以有自己独特的方法性质。

只能单继承。子类只能由一个父类。

继承特性:单根性、传递性。

//继承
//老师类
class Teacher
{
    //姓名
    public string name;
    //职工号
    protected int number;
    //介绍名字
    public void SpeakName()
    {
        number = 10;
        Console.WriteLine(name);
    }
} 
//教学老师继承老师类
class TeachingTeacher : Teacher
{
    //科目
    public string subject;
    //介绍科目
    public void SpeakSubject()
    {
        number = 11;
        Console.WriteLine(subject + "老师");
    }
}
//语文老师继承教学老师类
class ChineseTeacher:TeachingTeacher
{
    public void Skill()
    {
        Console.WriteLine("一行白鹭上青天");
    }
}
 class Program
{
    static void Main(string[] args)
    {
        TeachingTeacher tt = new TeachingTeacher();
        tt.name = "汪老师";
        //tt.number = 1;
        tt.SpeakName();

        tt.subject = "Unity";
        tt.SpeakSubject();

        ChineseTeacher ct = new ChineseTeacher();
        ct.name = "张老师";
        //ct.number = 2;
        ct.subject = "语文";
        ct.SpeakName();
        ct.SpeakSubject();
        ct.Skill();
    }
}

里氏替换原则

父类容器装在子类对象。(任何父类出现的地方,子类都可以替代)

class GameObject
{

}
class Player:GameObject
{
    public void PlayerAtk()
    {
        Console.WriteLine("玩家攻击");
    }
}

class Monster:GameObject
{
    public void MonsterAtk()
    {
        Console.WriteLine("怪物攻击");
    }
}

class Boss:GameObject
{
    public void BossAtk()
    {
        Console.WriteLine("Boss攻击");
    }
}
class Program
{
    static void Main(string[] args)
    {
        //里氏替换原则
        Gameobjet player=new Player();
        Gameobjet monster=new Monster();
        Gameobjet boss=new Boss();
        //is 和 as
        if(player is Player)
        {
            (player as Player).PlayerAtk();
        }
    }
}

is和as

is是判断一个对象是否为指定类型对象,返回值为true则为真,不是则为false

as用来将一个对象转换为指定类型对象,返回值为指定类型对象,若转换失败则返回null

继承中的构造函数

子类构造函数调用之前,先执行父类的构造函数。(爷爷-->父亲-->子类)

所以要保证父类的构造函数(尤其为无参构造函数)

  1. 保证父类的无参构造函数
  2. 通过base调用指定的有参构造函数
//继承中的构造函数
class Father
{
    //父类的无参构造函数很重要!
    public Father()
    {
        
    }
    public Father(int i)
    {
        Console.WriteLine("Father的有参构造");
    }
}
class Son:Father
{
    public Son(int i):base(i)
    {
        //构造函数
    }
}

万物之父--装箱和拆箱

object 是所有类型的基类,

作用:

  1. 可以利用里氏替换原则,用父类装子类
  2. 可以用来表示不确定类型,作为函数的参数类型

装箱:

用object存值类型。本该在栈中数值转移到堆上

拆箱

将object转换为值类型,将堆上的值类型转移到栈上(配合 is和as 使用)

优点:统一对象类型(里氏替换原则),方便对不同类型对象数值的管理

缺点:消耗性能,

//装箱拆箱
int i=5;
object obj=i;//装箱
i=(int)obj;//拆箱

*密封类(了解)

使用sealed关键字修饰的类,不可以被派生。(结扎了!)

面向对象--多态

V: virtual(虚函数)

O: override(重写)

B: base(父类)

让继承同一父类的子类们在执行相同方法有不同的表现与状态。

就是说,继承是一脉相承父类的品质,而多态是由自己的个性,尽管做的和父辈的事情相同。

解决的问题:

class Father
{
    public void SpeakName()
    {
        Console.WriteLine("Father的方法");
    }
}
class Son:Father
{
    public new void SpeakName()
    {
        Console.WriteLine("Son的方法");
    }
}
class Program
{
    static void Main(string[] args)
    {
        #region 解决的问题
        Father f = new Son();
        f.SpeakName();//调用的是父亲的方法
        (f as Son).SpeakName();//调用的是儿子的方法
        #endregion
    }
}

使用多态来保证(继承类)一个类方法的独立性

class GameObject
{
    public string name;
    public GameObject(string name)
    {
        this.name = name;
    }

    //虚函数 可以被子类重写
    public virtual void Atk()
    {
        Console.WriteLine("游戏对象进行攻击");
    }
}

class Player:GameObject
{
    public Player(string name):base(name)
    {

    }

    //重写虚函数
    public override void Atk()
    {
        //base的作用
        //代表父类 可以通过base来保留父类的行为
        base.Atk();
        Console.WriteLine("玩家对象进行攻击");
    }
}

抽象类与抽象方法

抽象类不可以被实例化

abstract class Thing{

public string name;

}

抽象方法:没有方法体的纯虚方法,继承的子类必须实现纯虚方法。(子类必须重写该方法,子类的子类不必强制实行,但也可以继续重写。)

抽象方法与virtual(虚函数)方法区别:

  1. 抽象方法只能在抽象类中出现,没有方法体,子类必须重写实现
  2. 虚函数则有方法体,可在普通类中出现,由子类选择性的实现
abstract class Fruits
{
    public string name;

    //抽象方法 是一定不能有函数体的
    public abstract void Bad();

    public virtual void Test()
    {
        //可以选择是否写逻辑
    }
}
class Apple : Fruits
{
    public override void Bad()
    {

    }
    //虚方法是可以由我们子类选择性来实现的
    //抽象方法必须要实现
}

接口(重要)

概念:接口是行为的抽象规范

关键字:interface

声明规范:

  1. 不能包含成员变量
  2. 只能包含方法、属性、索引器、事件
  3. 成员不能被实现
  4. 成员可以不用写访问修饰符,默认为public,不能是private
  5. 接口不能继承类,但是可以继承另一个接口

使用规范:

  1. 类可以继承多个接口
  2. 类继承接口,必须实现接口中所有成员

特点:

  1. 和类的声明相似
  2. 接口就是用来继承的
  3. 接口不能被实例化,可以作为容器存储对象(里氏替换原则,父类装子类)

接口是抽象行为的”基类“

//接口的声明
//命名规范 I+帕斯卡命名法
interface IFly
{
    void Fly();//方法
    string Name//属性
    {
        get;
        set;
    }
    int this[int index]//索引器
    {
        get;
        set;
    }
    event Action doSomthing;//事件委托
}

接口的使用---类的继承

  1. 一个类只能继承一个基类,但是可以继承多个接口
  2. 继承接口之后,必须实现其中的内容
//接口的使用
class Animal
{
    
}
class Person:Animal,IFly
{
    //实现接口方法也可以加virtual来实现
    public virtual void Fly()
    {
        
    }
    public string Name
    {
        set;
        get;
    }
    public int this[int index]
    {
        get
        {
            return 0;
        }
        set
        {
            
        }
    }
    public event Action doSomething;
}

接口的使用---接口的继承

接口继承基类接口之后,不需要实现接口中的内容(抽象继承抽象,还是抽象)

等到最后类来具体实现

//接口继承接口
interface IWalk
{
    void Walk();
}
interface IMove:IFly,IMove
{
    
}
//必须实现所有相关的
//继承来的抽象内容(接口,接口的父接口中的成员)
class Test:IMove
{
    
       public int this[int index] { 
           get => throw new NotImplementedException(); 
           set => throw new NotImplementedException(); 
       }

        public string Name { 
            get => throw new NotImplementedException(); 
            set => throw new NotImplementedException();
        }

        public event Action doSomthing;

        public void Fly()
        {
            throw new NotImplementedException();
        }

        public void Walk()
        {
            throw new NotImplementedException();
        }
}

显示实现接口

//接口的使用--当作容器 父类装子类
 interface IAtk
{
    void Atk();
}

interface ISuperAtk
{
    void Atk();
}
//显示实现接口
class Player : IAtk, ISuperAtk
{
    //遇到相同方法名字
    //显示实现接口 就是用 接口名.行为名 去实现
    void IAtk.Atk()
    {

    }

    void ISuperAtk.Atk()
    {

    }

    public void Atk()
    {

    }
}
class Progarm
{
    static void Main(string[] args)
    {
        IFly f = new Person();
		//里氏替换原则
        IMove im = new Test();
        IFly ifly = new Test();
        IWalk iw = new Test();

        IAtk ia = new Player();
        ISuperAtk isa = new Player();
        ia.Atk();
        isa.Atk();

        Player p = new Player();
        (p as IAtk).Atk();//IAtk的
        (p as ISuperAtk).Atk();//ISuperAtk的Atk
         p.Atk();//自己的Atk
    }
}

*密封方法(了解)

sealed 修饰的重写方法,子类不会被重写方法


其它关联知识点

命名空间namespace

  1. 命名空间是个工具包,用来管理类
  2. 不同命名空间中允许由同名类
  3. 不同命名空间相互使用时,using引用命名空间 或者指明出处
  4. 命名空间可以包裹命名空间

万物之父Object中的方法

string

  1. string 本质是char[]数组 可以有

    string ss="Tony",char[0]='T'

  2. 字符串的拼接

  3. 正向查找字符的位置 IndexOf()

  4. 反向查找字符串的位置 LastIndexOf()

  5. 移除指定位置后的字符 Remove(index)//注意接受返回值

  6. 字符串的替换

  7. 大小写转换

  8. 字符串的截取

  9. 字符串的切割 str.Split(',');按照,切割

StringBuilder

字符串频繁拼接使用StringBuilder,不会再次频繁的创建新的对象,减少垃圾产生。

容量问题:初始时候本身有一定容量,在容量允许范围内,直接存储。

超过容量之后,会以2倍大小扩容。相对于string每一次更改便会新建对象,可减少垃圾产生。

//StringBuilder
StringBuilder str=new StringBuilder("My Name is Tony");
//获取容量
str.Capacity;
//增加
str.Append("Chang");
str.AppendFormat("{0}{1}",123,456);
//插入
str.Insert(0,"Hello");
//删除
str.Remove(0,10);
//清空
str.Clear();
//查
str[1];
//替换
str.Replace("Name"."name");
//重新赋值
str.Clear();
str.Append("Hello World");
//equals
if(str.Equals("123456"))
{
    
}

String 还是StringBuilder?

String的方法种类较多,使用更加方便和灵活,但性能上不如StringBuilder,不如StringBuilder产生垃圾少

需要频繁修改的字符串选用StringBuilder更好些。

如何优化内存?

  1. 节约内存
    1. 少new对象 少产生垃圾
    2. 合理使用static
    3. 合理使用String与StringBuilder
  2. 减少GC产生

结构体与类的区别

  1. 存储位置 结构体是值类型,存储在栈中 类是引用类型,存储在堆中
  2. 结构体中的成员变量不可以赋初始值,类中可以
  3. 结构体具备封装特性但是不具备继承和多态 而类都具有
  4. 结构体不具备继承特性,所以不可以使用protected保护修饰符修饰
  5. 结构体声明有参构造之后,无参构造不会被顶掉
  6. 结构体不能声明析构函数,而类可以
  7. 结构体需要在构造函数中初始化所有成员变量,而类随意
  8. 结构体不能被static修饰,不存在静态的结构体,而类随意
  9. 结构体不能在内部声明与自己一样的结构体变量(会无限创建...栈溢出),类可以(因为是引用)
  10. 结构体可以继承接口(不可以继承类、结构体)

如何选择结构体和类:

  1. 如果想要用继承和多态时,直接淘汰结构体,如玩家、怪物
  2. 存储的对象是数据的集合时候,优先考虑结构体,如向量、坐标等
  3. 从本质上考虑,如果经常要改变赋值对象,原有的数值不想跟着改变的,选用结构体(值类型,复制拷贝,不影响本身),如坐标、向量、旋转角等

抽象类与接口的区别

相同点:

  1. 都可以被继承
  2. 都不可以直接实例化
  3. 都可以包含方法的声明
  4. 子类必须实现未实现的方法
  5. 都遵循里氏替换原则(父类装子类)

区别:

  1. 抽象类中可以有构造函数,而接口不可以
  2. 抽象类只能被单一继承,接口可以被继承多个
  3. 抽象类中可以有成员变量,接口中不能
  4. 抽象类中可以有声明成员方法,虚方法,抽象方法,静态方法而接口中只能声明没有实现的抽象方法
  5. 抽象类方法可以使用访问修饰符;接口中建议不写,默认为public

如何选择抽象类与接口

表示对象的选用抽象类,表示行为拓展的用接口。不同对象的相同行为,我们可以抽象出行为,用接口来实现。

面向对象的七大原则

总目标:高内聚、低耦合

减少类内部对其它类的调用,减少模块与模块之间的交互复杂度。

  1. 单一职责原则(一个类专注于一个功能)
  2. 里氏替换原则(父类可以装子类)
  3. 开闭原则(对拓展开放,对修改关闭,要保持开放和扩展,减少修改)
  4. 依赖倒转原则(依赖于抽象,不依赖于抽象具体)
  5. 迪米特原则(最少知识原则,不要和陌生人说话)
  6. 接口隔离原则(一个接口不应该提供过多的功能,)
  7. 合成复用原则(尽量使用组合复用实现功能,减少继承高耦合行为)
posted @ 2023-10-12 13:15  畅知  阅读(849)  评论(3编辑  收藏  举报