继承
声明类时,可以指定一个类型作为基类。
// 定义一个类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
会执行以下步骤:
- 检查目标类型,和他的所有基类,计算所需的内存空间
- 分配内存空间
- 从基类开始依次执行他们的字段初始分配,和执行构造器。
- 返回实例引用。
所需的内存空间只包括实例字段,不包括实例的方法。
另外,每个引用类型的实例还会生成一个对象头,对象头里有两个指针,分别指向元数据和同步块。
元数据指针指向该类型的类型对象,其中包含该类型的方法表等信息。
同步块索引用于多线程场景中,支持数据同步和线程锁定。
方法的静态绑定
在一般情况下,要调用一个引用类型实例的方法,首先要获取该实例的对象头,
从对象头中读取元数据指针,然后根据元数据中的方法表找到对应的方法地址。然后调用该方法。
对于普通方法,编译器会直接将方法地址嵌入到代码中,省略查找对象头的步骤。
因为编译器在编译时是看着类的定义编译的,知道要调用的方法在什么位置。
但是虚方法不同。虚方法的基类实现和派生类实现都可能被调用。
而调用哪一个实现取决于实例的实际类型。
但是在密封虚方法后,它就变成了一个普通方法,编译器就可以对它进行优化了。
面向抽象
一个基类类型的变量 / 参数,可以接受派生类的对象。
在定义方法时,应该让参数使用最抽象的类型,只要能实现方法的逻辑。
你可能会想,参数越具体,能用的功能越多不是更好吗?
举个例子,一个公司想招人来搬砖,这是一项不需要特殊技能的工作。
但是他们的面试要求求职者能够制造飞机,并且熟悉天体物理学。
这种高要求淘汰了绝大部分人。最终入选的人发现,工作内容只是搬砖而已。
这不是很浪费吗?当你要求你的参数能做很多事情时,你是否意识到,
你真正需要它做的事情只是搬砖而已。它还能做什么事情,对你来说并不关心。
所以,为了提高你的方法的抽象性,增加它的可重用性,
应该在确保参数类型能实现方法逻辑的基础上,尽量降低对它的约束。
否则,你可能需要写无数的重载。
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;
}
}