c#笔记-继承

继承

在声明一个类时,可以指定他的基类。声明类将获得基类的所有实例成员(构造器,终结器除外)。
静态类由于不允许存在任何实例,所以无法参与继承和被继承。

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

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

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

在声明时,class B : A表示B类继承了A类的所有成员。我们称A是B的基类,B是A的派生类
如果同时我们声明了一个class C : B,那么C类也继承了B类(和A类)的所有成员。
我们称B是C的基类,C是B的派生类,C是A的间接派生类
因为A在继承链C -> B -> A中也处于C的上方。

覆写

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

// 定义一个类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.访问基类的成员,就像使用this.访问自己的成员一样。
这样可以避免与派生类的同名成员发生冲突或混淆。

特别是在覆写或重写了基类成员时,如果还想调用基类的原始实现,就必须使用base.来指定。
静态成员则不需要,直接用基类的类名就可以访问。

需要注意的是,base.只能访问直接基类的成员,如果基类也继承了其他类,
并且覆写或重写了它们的成员,那么我们无法通过base.访问到更上层的基类成员。

另外,有一种情况下,我们必须访问基类成员,那就是在派生类的构造器中。
因为构造器不会被继承,所以派生类必须自己调用基类的构造器来初始化基类部分的状态。
编译器会自动帮我们调用基类的无参构造器,
但如果基类没有无参构造器,或者无参构造器无法访问(比如是私有的),
那么派生类必须显式地声明一个构造器,并使用构造器链来调用一个基类构造器。

// 定义一个基类Person,有一个只读的名字属性和一个问候方法,构造器会初始化名字
public class Person
{
    
    
    // 定义一个只读的名字属性
    public string Name {
    
     get; }

    // 定义一个问候方法,输出名字和一句话
    public void Greet()
    {
    
    
        Console.WriteLine($"你好,我叫{
      
      Name}。");
    }

    // 定义一个构造器,接受一个名字参数并赋值给属性
    public Person(string name)
    {
    
    
        Name = name;
    }
}

// 定义一个派生类Waiter,继承自Person类,覆写问候方法,在问候方法中先输出一段话,然后调用原始实现
public class Waiter : Person
{
    
    
    // 覆写问候方法,在问候方法中先输出一段话,然后调用原始实现
    public new void Greet()
    {
    
    
        Console.WriteLine("欢迎光临。");
        base.Greet(); // 调用基类的问候方法
    }

    // 定义一个构造器,接受一个名字参数并显示调用基类的构造器
    public Waiter(string name) : base(name)
    {
    
    
        
    }
}

必须用构造器链依次执行所有基类的构造器是因为,一个派生类一定具有基类的全部数据。
即便是私有的,也只是你无法访问而已,他是实际存在的。

重写

虚方法

虚方法是一种可以在派生类中被重写的方法,用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修饰,那么它不能在派生类中再次被重写。

抽象类的必须继承和密封的理念是冲突的,所以不能同时使用。
一个刚定义的虚方法,也是必须通过继承和重写发挥意义,所以也和密封冲突。
虚方法也不能使用private修饰,尽管内部类是可以访问到也能重写他的。

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

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

// 定义一个从Food继承的具体类Beef,表示牛肉
internal 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;
	}
}
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;
	} 
}

猜你喜欢

转载自blog.csdn.net/zms9110750/article/details/130574950