c#笔记-继承

继承

声明类时,可以指定一个类型作为基类。

// 定义一个类Bulbasaur,表示妙蛙种子这个宝可梦
class Bulbasaur
{
    // 定义一个属性Name,表示宝可梦的名字
    public string? Name { get; set; }

    public void VineWhip()
    {
        Console.WriteLine($"{Name}使用了藤辫!");
    }
}

// 定义一个类Ivysaur,表示妙蛙草这个宝可梦,继承自Bulbasaur类
class Ivysaur : Bulbasaur
{
    public void Photosynthesis()
    {
        Console.WriteLine($"{Name}使用了光合作用!");
    }
}

这个类型将获得基类的所有实例成员。除了构造器和终结器。
静态类由于不允许存在任何实例,所以无法参与继承和被继承。

Ivysaur ivysaur = new Ivysaur();
ivysaur.Name = "蒜头王八";//尽管Ivysaur类没有声明Name但依然可以使用。
ivysaur.VineWhip();

妙蛙草继承了妙蛙种子。此时我们称妙蛙草是从妙蛙种子派生出来的。
妙蛙种子是妙蛙草的基类,妙蛙草是妙蛙种子的派生类。
继承具有传递性。所以妙蛙花也是从妙蛙种子派生的。

访问基类成员

即便基类成员是不可访问的,在派生类中也会实际存在和储存一份。
因此,在构造派生类时,必须先完整的构造出基类的数据。

派生类默认会调用基类的无参构造器,如果不可访问或没有。
那么必须自己定义一个构造器,并指示如何调用基类构造器。

访问基类成员使用base,用法和this相似。

class Bulbasaur
{
    // 定义一个属性Name,表示宝可梦的名字
    public string? Name { get; set; }

    public Bulbasaur(string? name)
    {
        Name = name;
    }

    public void VineWhip()
    {
        Console.WriteLine($"{Name}使用了藤辫!");
    }
}

// 定义一个类Ivysaur,表示妙蛙草这个宝可梦,继承自Bulbasaur类
class Ivysaur : Bulbasaur
{
    public Ivysaur(string? name) : base(name)
    {
    }

    public void Photosynthesis()
    {
        Console.WriteLine($"{Name}使用了光合作用!");
    }
}

如果派生类使用了主构造器,那么基类声明也需要以主构造器语法声明调用。
不过调用的可以是基类的任何一个构造器。

// 定义一个类Bulbasaur,表示妙蛙种子这个宝可梦
class Bulbasaur(string name)
{
    // 定义一个属性Name,表示宝可梦的名字
    public string? Name = name;

    public Bulbasaur() : this("妙蛙种子")
    {
    }

    public void VineWhip()
    {
        Console.WriteLine($"{Name}使用了藤辫!");
    }
}

// 定义一个类Ivysaur,表示妙蛙草这个宝可梦,继承自Bulbasaur类
class Ivysaur() : Bulbasaur()//基类的无参构造器不是主构造器
{

    public void Photosynthesis()
    {
        Console.WriteLine($"{Name}使用了光合作用!");
    }
}

覆写

派生类可以拥有和基类同名的成员。这种情况下,会确实存在多个同名的成员。

// 定义一个类People,表示人类
internal class People
{
	// 定义一个属性Id,表示身份证号
	public string? Id { get; set; }
}
// 定义一个类Student,表示学生,继承自People类
internal class Student : People
{
	// 定义一个属性Id,表示学生号
	public new string? Id { get; set; }//new只是为了具有提醒效果,可以不加
}

这样,Student类就覆写了People类的Id属性。如果我们创建一个Student对象,
并用People类型的变量引用它,那么我们可以访问到两个不同的Id属性。

Student st = new Student();
Console.WriteLine(st.Id); // 学生号
People p = st;
Console.WriteLine(p.Id); // 身份证号

覆写后,也可以使用base来调用基类的原始成员。

重写

虚方法

虚方法是一种可以在派生类中被重写的方法,用virtual关键字来声明。
可以给属性添加virtual,但不能给访问器单独添加virtual

// 定义一个基类Rectangle,有两个虚属性Length和Width,表示长和宽
public class Rectangle
{
	// 定义一个虚属性Length
	public virtual double Length { get; set; }

	// 定义一个虚属性Width
	public virtual double Width { get; set; }

	// 定义一个只读属性Area,返回矩形的面积
	public double Area => Length * Width;
}

重写虚方法

重写后,即便用基类型的变量装载,调用的也是重写后的内容。
重写使用override修饰,重写和覆写不能同时使用,至多选择其中一种声明方式。

// 定义一个派生类Square,继承自Rectangle,有一个边长属性
public class Square : Rectangle
{
	// 定义一个边长属性
	public double Side { get; set; }

	// 重写基类的虚属性Length,返回正方形的边长
	public override double Length { get => Side; set => Side = value; }

	// 重写基类的虚属性Width,返回正方形的边长
	public override double Width { get => Side; set => Side = value; }
}

重写后的成员依然是虚成员,派生类中还能再次重写。

抽象类

抽象类是一种以被其他类继承为目的的模板类。他的构造器只能被派生类使用构造器链调用,
而不能搭配new构造自己的直接类型。抽象类使用abstract修饰。

抽象类通常用来表示一些概念或者分类,例如妙蛙种子,杰尼龟,小火龙的基类宝可梦
或者是四边形,圆形,六边形的基类形状

抽象方法

抽象方法是没有方法体的虚方法。在方法签名结束后直接使用;结束而不使用大括号。
抽象方法只能存在于抽象类中。因此如果从抽象类派生出一个非抽象类,必须重写他的所有抽象方法。

// 定义一个抽象类Pokemon,表示宝可梦的通用属性和行为
abstract class Pokemon
{
    // 定义一个抽象属性Name,表示宝可梦的名字
    public abstract string Name { get; }

    // 定义一个抽象属性Attribute,表示宝可梦的属性
    public abstract string Attribute { get; }

    // 定义一个抽象方法Attack,表示宝可梦的攻击行为
    public abstract void Attack();
}

// 定义一个从Pokemon继承的具体类Squirtle,表示杰尼龟这种宝可梦
class Squirtle : Pokemon
{
    // 重写Name属性,返回"杰尼龟"
    public override string Name => "杰尼龟";

    // 重写Attribute属性,返回"水"
    public override string Attribute => "水";

    // 重写Attack方法,输出"杰尼龟使用水枪!"
    public override void Attack()
    {
        Console.WriteLine($"{Name}使用水枪!");
    }
}

密封

关键字sealed可以用来限制继承或重写的行为。
如果一个类被sealed修饰,那么它不能作为其他类的基类。(抽象类不能密封)
如果一个重写过的虚方法被sealed修饰,那么它不能在派生类中再次被重写。

// 定义一个抽象类Food,表示食物的通用属性和行为
abstract class Food
{
	public abstract int HungerRestore { get; }

	public virtual void Eat()
	{
		Console.WriteLine($"你吃了这种食物。");
		Console.WriteLine($"你恢复了{HungerRestore}点饱食度。");
	}
}

// 定义一个从Food继承的具体类Beef,表示牛肉
sealed class Beef : Food
{
	public int Freshness { get; private set; }

	public Beef(int freshness)
	{
		Freshness = freshness;
	}

	public override sealed int HungerRestore => 10 + (int)(Freshness * 0.01);

	public override sealed void Eat()
	{
		Console.WriteLine($"你吃了一块{Freshness}%新鲜的牛肉。");
		Console.WriteLine($"你恢复了{HungerRestore}点饱食度。");
	}
}

object

c#中除了指针类型外都直接或间接地继承了object类型。
这包括值类型和静态类,即使你无法为它们显式指定基类。

ToString

这个方法返回一个表示当前对象的字符串。
这是一个虚方法,你可以在派生类中重写它,自定义字符串的格式。
他的默认实现是返回当前对象的类型的完全限定名。

GetHashCode

这个方法返回当前对象的哈希代码。
这个哈希代码通常用来作为哈希表的键值,或者用来比较两个对象是否相等。

Equals

这个方法确定指定的对象是否等于当前对象。
这是一个虚方法,你可以在派生类中重写它,自定义相等性的逻辑。
通常,如果你重写了Equals方法,你也应该重载==运算符,并重写GetHashCode方法。

MemberwiseClone

这个方法创建当前对象的浅表副本。
这是一个受保护的方法,只能在当前类或派生类中访问。他不能被重写或隐藏。
由于是在object类中定义的,所以返回类型是object。
但实际上,返回的对象与当前对象具有相同的运行时类型。因此,你可以将其强制转换为相应的类型。

多态

对象的内存分配

在使用new运算符创建一个类型的实例时,new会执行以下步骤:

  1. 检查目标类型,和他的所有基类,计算所需的内存空间
  2. 分配内存空间
  3. 从基类开始依次执行他们的字段初始分配,和执行构造器。
  4. 返回实例引用。

所需的内存空间只包括实例字段,不包括实例的方法
另外,每个引用类型的实例还会生成一个对象头,对象头里有两个指针,分别指向元数据和同步块。
元数据指针指向该类型的类型对象,其中包含该类型的方法表等信息。
同步块索引用于多线程场景中,支持数据同步和线程锁定。

方法的静态绑定

在一般情况下,要调用一个引用类型实例的方法,首先要获取该实例的对象头,
从对象头中读取元数据指针,然后根据元数据中的方法表找到对应的方法地址。然后调用该方法。

对于普通方法,编译器会直接将方法地址嵌入到代码中,省略查找对象头的步骤。
因为编译器在编译时是看着类的定义编译的,知道要调用的方法在什么位置。

但是虚方法不同。虚方法的基类实现和派生类实现都可能被调用。
而调用哪一个实现取决于实例的实际类型。

但是在密封虚方法后,它就变成了一个普通方法,编译器就可以对它进行优化了。

面向抽象

一个基类类型的变量 / 参数,可以接受派生类的对象。
在定义方法时,应该让参数使用最抽象的类型,只要能实现方法的逻辑。
你可能会想,参数越具体,能用的功能越多不是更好吗?

举个例子,一个公司想招人来搬砖,这是一项不需要特殊技能的工作。
但是他们的面试要求求职者能够制造飞机,并且熟悉天体物理学。
这种高要求淘汰了绝大部分人。最终入选的人发现,工作内容只是搬砖而已。

这不是很浪费吗?当你要求你的参数能做很多事情时,你是否意识到,
你真正需要它做的事情只是搬砖而已。它还能做什么事情,对你来说并不关心。

所以,为了提高你的方法的抽象性,增加它的可重用性,
应该在确保参数类型能实现方法逻辑的基础上,尽量降低对它的约束。

否则,你可能需要写无数的重载。

class 艾希
{
	public int hp;
	public void 万箭齐发(艾希 目标)
	{
		目标.hp -= 100;
	}
	public void 万箭齐发(盖伦 目标)
	{
		目标.hp -= 100;
	}
	public void 万箭齐发(瑞兹 目标)
	{
		目标.hp -= 100;
	}//你只想获取目标的hp,并不在乎他的技能。
}
class 盖伦
{
	public int hp;
	public void 致死打击(艾希 目标)
	{
		目标.hp -= 20;
	}
	public void 致死打击(盖伦 目标)
	{
		目标.hp -= 20;
	}
	public void 致死打击(瑞兹 目标)
	{
		目标.hp -= 20;
	}

}
class 瑞兹
{
	public int hp;
	public void 法术涌动(艾希 目标)
	{
		目标.hp -= 40;
	}
	public void 法术涌动(盖伦 目标)
	{
		目标.hp -= 40;
	}
	public void 法术涌动(瑞兹 目标)
	{
		目标.hp -= 40;
	} 
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值