C#自学笔记:继承与多态

继承

实例

class Teacher
{
    public string name;
    public int number;
    public void SpeakName()
    {
        Console.WriteLine(name);
    }
}

class TeachingTeacher : Teacher
{
    public string subject;
    public void SpeakSubject()
    {
        Console.WriteLine(subject + "老师");
    }
}

//可以连续继承,有传递性,拥有所有父类的属性
class ChineseTeacher : TeachingTeacher
{
    public void Skill()
    {
        Console.WriteLine("一行白鹭上青天");
    }
}

image

子类也能继承父类的private属性,只是在子类中无法访问。

子类和父类的同名成员

概念

C#中允许子类存在和父类同名的成员

但是:极不建议使用

会冒波浪线,提醒你是否有意隐藏父类的同名成员,意思是会隐藏父类的同名成员,使用的时候是子类成员,如果是有意的话请用new,意思是故意要隐藏父类的同名成员的话最好用new,但是加不加都是默认使用子类成员。

class Teacher
{
    public string name;
}

class TeachingTeacher : Teacher
{
    public new string name;
}

里氏替换原则

里氏替换原则是面向对象七大原则中最重要的原则

概念:

  • 任何父类出现的地方,子类都可以替代

重点:

  • 语法表现——父类容器装子类对象,因为子类对象包含了父类的所有内容

作用:

  • 方便进行对象存储和管理

基本实现

GameObject player = new Player();
GameObejct monster = new Monster();
GameObject boss = new Boss();

GameObject[] objects = new GameObject[] {new Player(), new Monster(), new Boss()};

is和as

基本概念

is

  • is:判断一个对象是否是指定类对象
  • 返回值:bool,是为true,不是为false

as

  • as:将一个对象转换为指定类对象
  • 返回值:指定类型对象

成功返回指定类型对象,失败返回null

基本语法

if (player is Player) //即使是父类引用指向子类对象,可以判断指向的对象
{
    //Player p = player as Player;
    //p.PlayerAtk();

    (player as Player).PlayerAtk();
    //目的是为了调用子类的特有方法,原来是GameObject类,现在转换成了Player类
}

目的:对单个类影响不大,主要是用于对象数组。

继承中的构造函数

基本概念

特点

  • 当声明一个子类对象时,先执行父类的构造函数,再执行子类的构造函数

注意:

  1. 父类的无参构造很重要
  2. 子类可以通过base关键字代表父类来调用父类构造

继承中构造函数的执行顺序

父类的父类构造->父类构造->子类构造

如果父类的无参构造被有参构造顶掉的话,子类继承时会报错,因为子类实例化时默认自动调用的是父类的无参构造,所以父类的无参构造很重要

通过base调用指定父类构造

此时没有无参构造就不会报错了,因为指定了一个有参构造,不会默认调用无参构造
class Father
{
    public Father(int i)
    {

    }
}

class Son : Father
{
    public Son(int i) : base(i)
    {
    
    }
}

装箱拆箱

发生条件

用object存值类型(装箱)

再把object转为值类型(拆箱)

装箱

把值类型用引用类型存储

栈内存会迁移到堆内存中

拆箱

把引用类型存储的值类型取出来

堆内存会迁移到栈内存中

好处:不确定类型时可以方便参数的存储和传递

坏处:存在内存迁移,增加性能消耗

总结:

  • 万物之父:object
  • 装箱拆箱是基于里氏替换原则的,可以用object容器装载一切类型的变量
  • object是所有类型的基类
  • 装箱拆箱不是不能用,而是尽量少用,因为存在内存的迁移,增加了性能消耗

密封类

基本概念

是使用sealed密封关键字修饰的类

作用:让类无法再被继承

有人说是太监类、结扎类、绝育类

作用

  • 在面向对象程序的设计中,密封类的主要作用就是不允许最底层子类被继承
  • 可以保证程序的规范性、安全性
  • 目前对于大家来说,可能用处不大
  • 随着大家的成长,以后制作复杂系统或者程序框架时,便能慢慢体会到密封的作用

多态vob

基本概念

多态按字面意思就是“多种状态”

让继承同一父类的子类们,在执行相同方法时有不同的表现(状态)

目的

  • 同一个父类的对象执行相同行为(方法)时有不同的表现

解决的问题

  • 让同一个对象有唯一行为的特征

vob

v:virtual(虚函数)

o:override(重写)

b:base(父类)

实现

public GameObject
{
    public string name;

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

    public virtual void Atk()
    {
        Console.WriteLine("游戏对象进行攻击");
    }
}

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

    public override void Atk()
    {
        Console.WriteLine("玩家对象进行攻击");
    }
}

public Main
{
    public static void main(String[] args)
    {
        GameObject p = new Player("柠凉");
        p.Atk();//玩家对象进行攻击
    }
}

抽象类

概念

被抽象关键字abstract修饰的类

特点:

  1. 不能被实例化的类
  2. 可以包含抽象方法
  3. 继承抽象类必须重写其抽象方法

抽象函数

又叫纯虚方法

用abstract关键字修饰的方法

特点:

  1. 只能在抽象类中声明
  2. 没有方法体
  3. 不能是私有的
  4. 继承后必须实现,用override重写

如何选择普通类还是抽象类?

  • 不希望被实例化的对象,相对比较抽象的类可以使用抽象类
  • 父类中的行为不太需要被实现的,只希望子类去定义具体的规则的,可以选择抽象类然后使用其中的抽象方法来定义规则。

接口

它也是一种自定义类型

关键字:interface

接口声明的规范

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

接口的使用规范

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

特点:

  1. 它和类的声明类似
  2. 接口是用来继承的
  3. 接口不能被实例化,但是可以作为容器存储对象

接口的声明

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

接口命名规范:帕斯卡前面加个I

语法:

interface 接口名
{
}

举例:

interface IFly
{
    void Fly();

    string Name
    {
        get;
        set;
    }

    int this[int index]
    {
        get;
        set;
    }

    event Action doSomething;
}

接口的使用

  1. 类可以继承1个类,n个接口
  2. 继承了接口后,必须实现其中的内容,并且必须是public的
  3. 实现的接口函数,可以加v再在子类重写,意思是可以在实现了接口的类当中的函数方法前面加virtual,这样子类继承这个方法就可以重写
  4. 接口也遵循里氏替换原则

接口可以继承接口

接口继承接口时,不需要实现

待类继承接口后,类自己去实现所有内容

interface IWalk
{
    void Walk();
}
interface IMove : IFly, IWalk
{
}

显示实现接口

当一个类继承两个接口

但是接口中存在着同名方法时

注意:显示实现接口时,不能写访问修饰符

class IAtk
{
    void Atk();
}

class ISuperAtk
{
    void Atk();
}
//显示实现接口,就是用接口名.行为名,去实现
class Player : IAtk, ISuperAtk
{
    void IAtk.Atk()
    {
    }
    void ISuperAtk.Atk()
    {
    }
    public void Atk()
    {
    }
}

//使用
class Main
{
    static void main(String[] args)
    {
        IAtk ia = new Player();
        ISuperAtk isa = new Player();
        Player p = new Player();
        ia.Atk();//调用的是IAtk.Atk()
        isa.Atk();//调用的是ISuperAtk.Atk()
        p.Atk();//调用的是Player自己的Atk()
    }
}

总结

继承类:是对象间的继承,包括特征行为等等

继承接口:是行为间的实现,继承接口的行为规范,按照规范去实现内容

由于接口也是遵循里氏替换原则,所以可以用接口容器装对象

那么就可以实现装载各种毫无关系但是却有相同行为的对象

密封方法

基本概念

用密封关键字sealed修饰的重写函数

作用:让虚方法或者抽象方法之后不能再被重写

特点:和override一起出现

class Person : Animal
{
    public sealed override void Eat()
    {
        base.Eat();
    }
}

Person的子类将无法再重写Person的Eat()方法

命名空间

概念

命名空间是用来组织和重用代码的

作用

就像是一个工具包,类就像是一件一件的工具,都是声明在命名空间中的

基本语法

namespace 命名空间名
{
    class 类名
        ...
}
  • 不同命名空间中相互使用时,需要引用命名空间或指明出处

using 命名空间名命名空间名.方法名()

命名空间可以包裹命名空间

namespace MyGame
{
    namespace UI
    {
        class Image
        {
        }
    }

    namespace Game
    {
        class Image
        {
        }
    }
}

命名空间中的类默认为internal(访问修饰符),只能在同一个工程里面调用,不同工程不能相互调用

万物之父中的方法

object中的静态方法

  1. Equals:判断两个对象是否相等
  • 最终的判断权,交给左侧对象的Equals方法,不管值类型引用类型都会按照左侧对象Equals方法的规则来进行比较。

Object.Equals(1, 1);//返回true或false

  1. ReferenceEquals
  • 比较两个对象是否是相同的引用,主要是用来比较引用类型的对象
  • 值类型对象返回值始终是false

Object.ReferenceEquals(t1, t2);

object中的成员方法

  1. GetType
  • 该方法在反射相关知识点中是非常重要的方法
  • 该方法的主要作用就是获取对象运行时的类型Type
  • 通过Type结合反射相关知识点可以做很多关于对象的操作
Test t = new Test();
Type type = t.GetType();
  1. MemberwiseClone
  • 该方法用于获取对象的浅拷贝对象,口语化的意思就是会返回一个新的对象,但是新对象中的引用变量会和老对象中一致。(弹幕说值类型是独立的,引用类型是共用的)
  • 浅拷贝:就是简单的把地址复制了一份,堆里面如果有成员变量是引用变量不变。
  • 深拷贝:自己创建了新地址,把旧地址数据拷贝过来,之后和旧地址无关。

object中的虚方法

  1. Equals
  • 默认实现还是比较两者是否为同一个引用,即相当于ReferenceEquals
  • 但是微软在所有值类型的基类System.ValueType中重写了该方法,用来比较值相等
  • 我们也可以重写该方法,定义自己的比较相等的规则
  1. GetHashCode
  • 该方法是获取对象的哈希码
  • (一种通过算法算出的,表示对象的唯一编码,不同对象哈希码有可能一样,具体值根据哈希算法决定),我们可以通过重写该函数来自己定义对象的哈希码算法,正常情况下,我们使用的极少,基本不用。
  1. ToString
  • 该方法用于返回当前对象代表的字符串,我们可以重写它定义我们自己的对象转字符串规则
  • 该方法非常常用。当我们调用打印方法时,默认使用的就是对象的ToString方法后打印出来的内容。

String

字符串指定位置获取

//字符串本质是char数组
string str = "柠凉";
Console.WriteLine(str[0]);//本质上是个索引器
//转为char数组
char[] chars = str.ToCharArray();

字符串拼接

str = string.Format("{0}{1}", 1, 3333);
Console.WriteLine(str);//13333

//第二种方法是用+拼接

正向查找指定字符串位置

str = "我是柠凉!"
int index = str.IndexOf("柠");
Console.WriteLine(index);//2,如果没找到会返回-1

反向查找指定字符串位置

str = "我是柠凉柠凉";
int index = str.LastIndexOf("柠凉");
Console.WriteLine(index);//4
index = str.LastIndexOf("唐老师");//打印出来是-1

移除指定位置后的字符

str = "我是柠凉柠凉";
str.Remove(3);
Console.WriteLine(str);//打印出来不变,注意,这个方法不会改变原字符串,只会返回一个新字符串

str = str.Remove(3);
Console.WriteLine(str);//我是柠

//指定两个参数进行移除
//参数1:开始位置
//参数2:字符个数
str = str.Remove(1, 1);
Console.WriteLine(str);//我柠

替换指定字符串

str = "我是柠凉柠凉";
str = str.Replace("柠凉", "程序");
Console.WriteLine(str);//我是程序程序

大小写转换

str = "abcdefg";
str = str.ToUpper();
Console.WriteLine(str);//ABCDEFG

str = str.ToLower();
Console.WriteLine(str);//abcdefg

字符串截取

str = "柠凉柠凉";
str = str.Substring(1);
Console.WriteLine(str);//凉柠凉

//参数1:开始位置
//参数2:指定个数
//不会自动的帮你判断是否越界,需要自己去判断
str = str.Substring(1, 2);
Console.WriteLine(str);//凉柠

字符串切割(非常重要)

str = "1,2,3,4,5,6,7,8";
string[] strs = str.Split(',');
for (int i = 0; i < strs.Length; i++)
{
    Console.WriteLine(strs);
}
//1
//2
//...

StringBuilder

C#提供的一个用于处理字符串的公共类

主要解决的问题是:

修改字符串而不创建新的对象,需要频繁修改和拼接的字符串可以使用它,可以提升性能

本质上是个数组

初始化

using System.Text;

StringBuilder str = new StringBuilder("123123");

容量

StringBuider存在一个容量,每次往里面添加时,如果超出容量,则会自动扩容
Console.WriteLine(str.Capacity);//容量
Console.WriteLine(str.Length);	//现在的长度

增删改查替换

//增
str.Append("4444");
str.AppendFormat("{0}{1}", 100, 999);
//插入
str.Insert(0, "柠凉");
//删除
//第一个参数为开始位置
//第二个参数为个数
str.Remove(0, 10);
//清空
str.Clear();
//查找
Console.WriteLine(str[0]);
//修改
str[0] = 'A';
//替换
//替换指定字符
str.Replace("1", "柠");

//判断是否相等
//不能使用==
if (str.Equals("123123"))
    return true;

结构体和类的区别

最大的区别是在存储空间里,因为结构体是值,类是引用,因此它们一个存储在栈上,一个存储在堆上。
  • 结构体和类在使用上类似,结构体甚至可以使用面向对象的思想来形容一类对象。
  • 结构体具备面向对象思想中的封装特性,但是它不具备继承和多态的特性,因此大大减少了它的使用频率
  • 由于结构体不具备继承的特性,所以它不能够使用protected保护访问修饰符

细节区别

  1. 结构体是值类型,类是引用类型
  2. 结构体存在中,类存在
  3. 结构体成员不能使用protected访问修饰符,而类可以
  4. 结构体成员变量声明不能指定初始值,而类可以
  5. 结构体不能声明无参的构造函数,而类可以
  6. 结构体声明有参构造函数后,无参构造不会被顶掉,而类会被顶掉
  7. 结构体不能声明析构函数,而类可以
  8. 结构体不能被继承,而类可以
  9. 结构体需要在构造函数中初始化所有成员变量,而类都行
  10. 结构体不能被静态static修饰(不存在静态结构体),而类可以
  11. 结构体不能在自己内部声明和自己一样的结构体变量,而类可以
  • 特点:结构体可以继承接口,因为接口是行为的抽象

如何选择结构体和类

  1. 想要用继承和多态时,直接淘汰结构体,比如:玩家、怪物等等。
  2. 对象是数据集合时,优先考虑结构体,比如:位置、坐标等等。
  3. 从值类型和引用类型赋值时的区别上考虑,比如经常被赋值传递的对象,并且改变赋值对象,原对象不想跟着变化时,就用结构体。比如:坐标、向量、旋转等等

抽象类和接口的区别

相同点

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

不同点

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

如何选择抽象类和接口

  • 表示对象的用抽象类
  • 表示行为拓展的用接口

举例:动物是一类对象,我们选择抽象类。飞翔是一个行为,我们选择接口。

posted @ 2025-07-11 15:48  柠凉w  阅读(15)  评论(0)    收藏  举报