当前位置:首页 > 技能相关 > C#与C++ > 正文内容

c#高级编程 继承 (三)

admin2年前 (2023-01-29)C#与C++2340 修订时间:2023-01-30 09:27:08

面向对象的三个最重要的概念是继承、封装和多态性。本问的重点是继承和多态性。当某类型的成员声明为private时,不能从外部访问,它们封装在类型中。

1、继承的类型

首先介绍一些面向对象(Object-Oriented, OO)术语,看看C#在继承方面支持和不支持的功能。

● 单重继承: 表示一个类可以派生自一个基类。C#就采用这种继承。

● 多重继承: 多重继承允许一个类派生自多个类。C#不支持类的多重继承,但允许接口的多重继承。

● 多层继承: 多层继承允许继承有更大的层次结构。类B派生自类A,类C又派生自类B。其中,类B也称为中间基类,C#支持它,也很常用。

● 接口继承: 定义了接口的继承。这里允许多重继承。

1.1 多重继承

一些语言(如C++)支持所谓的“多重继承”,即一个类派生自多个类。对于实现继承,多重继承会给生成的代码增加复杂性,还会带来一些开销。因此,C#的设计人员决定不支持类的多重继承,因为支持多重继承会增加复杂性,还会带来一些开销。

而C#又允许类型派生自多个接口。一个类型可以实现多个接口。这说明,C#类可以派生自另一个类和任意多个接口。更准确地说,因为System.Object是一个公共的基类,所以每个C#类(除了Object类之外)都有一个基类,还可以有任意多个基接口。

1.2 结构和类

使用结构的一个限制是结构不支持继承,但每个结构都自动派生自System.ValueType。不能编码实现结构的类型层次,但结构可以实现接口。换言之,结构并不支持实现继承,但支持接口继承。定义的结构和类可以总结为:

● 结构总是派生自System.ValueType,它们还可以派生自任意多个接口。

● 类总是派生自System.Object或用户选择的另一个类,它们还可以派生自任意多个接口。

2、实现继承

如果要声明派生自另一个类的一个类,就可以使用下面的语法:

class MyDerivedClass: MyBaseClass
{
// members
}

如果类(或结构)也派生自接口,则用逗号分隔列表中的基类和接口:

public class MyDerivedClass: MyBaseClass, IInterface1, IInterface2
{
// members
}

如果类和接口都用于派生,则类总是必须放在接口的前面。

对于结构,语法如下(只能用于接口继承):

public struct MyDerivedStruct: IInterface1, IInterface2
{
// members
}

如果在类定义中没有指定基类,C#编译器就假定System.Object是基类。因此,派生自Object类(或使用object关键字),与不定义基类的效果是相同的。

下面的例子定义了基类Shape。无论是矩形还是椭圆,形状都有一些共同点:形状都有位置和大小。定义相应的类时,位置和大小应包含在Shape类中。Shape类定义了只读属性Position和Shape,它们使用自动属性初始化器来初始化:

public class Position
{
public int X { get; set; }
public int Y { get; set; }
}
public class Size
{
public int Width { get; set; }
public int Height { get; set; }
}
public class Shape
{
public Position Position { get; } = new Position();
public Size Size { get; } = new Size();
}

3、虚方法

把一个基类方法声明为virtual,就可以在任何派生类中重写该方法:

public class Shape
{
public virtual void Draw()
{
    WriteLine($"Shape with {Position} and {Size}");
}
}

如果实现代码只有一行,在C# 6中,也可以把virtual关键字和表达式体的方法(使用lambda运算符)一起使用。这个语法可以独立于修饰符,单独使用:

public class Shape
{
public virtual void Draw() => WriteLine($"Shape with {Position} and {Size}");
}

也可以把属性声明为virtual。对于虚属性或重写属性,语法与非虚属性相同,但要在定义中添加关键字virtual,其语法如下所示:

public virtual Size Size { get; set; }

当然,也可以给虚属性使用完整的属性语法:

private Size _size;
public virtual Size Size
{
get
{
    return _size;
}set
{
    size = value;
}
}

为了简单起见,下面的讨论将主要集中于方法,但其规则也适用于属性。

C#中虚函数的概念与标准OOP的概念相同:可以在派生类中重写虚函数。在调用方法时,会调用该类对象的合适方法。在C#中,函数在默认情况下不是虚拟的,但(除了构造函数以外)可以显式地声明为virtual

C#要求在派生类的函数重写另一个函数时,要使用override关键字显式声明:

public class Rectangle : Shape
{
public override void Draw() => WriteLine($"Rectangle with {Position} and {Size}");
}

注意: 重写基类的方法时,签名(所有参数类型和方法名)和返回类型必须完全匹配。否则,以后创建的新成员就不覆盖基类成员。

成员字段和静态函数都不能声明为virtual,因为这个概念只对类中的实例函数成员有意义。

4、多态性

使用多态性,可以动态地定义调用的方法,而不是在编译期间定义。编译器创建一个虚拟方法表(vtable),其中列出了可以在运行期间调用的方法,它根据运行期间的类型调用方法。

在下面的例子中,DrawShape()方法接收一个Shape参数,并调用Shape类的Draw()方法:

public static void DrawShape(Shape shape)
{
shape.Draw();
}

使用之前创建的矩形调用方法。尽管方法声明为接收一个Shape对象,但任何派生Shape的类型(包括Rectangle)都可以传递给这个方法:

var r = new Rectangle();
r.Position.X = 33;
r.Position.Y = 22;
r.Size.Width = 200;
r.Size.Height = 100;
DrawShape(r);

运行这个程序,查看Rectangle.Draw方法()而不是Shape.Draw()方法的输出。输出行从Rectangle开始。如果基类的方法不是虚拟方法或没有重写派生类的方法,就使用所声明对象(Shape)的类型的Draw()方法,因此输出从Shape开始:

Rectangle with X: 33, y: 22 and Width: 200, Height: 100

5、隐藏方法

如果签名相同的方法在基类和派生类中都进行了声明,但该方法没有分别声明为virtual和override,派生类方法就会隐藏基类方法。

在大多数情况下,是要重写方法,而不是隐藏方法,因为隐藏方法会造成对于给定类的实例调用错误方法的危险。但是,如下面的例子所示,C#语法可以确保开发人员在编译时收到这个潜在错误的警告,从而使隐藏方法(如果这确实是用户的本意)更加安全。

这也是类库开发人员得到的版本方面的好处。

假定有一个类Shape:

public class Shape
{
// various members
}

在将来的某一刻,要编写一个派生类Ellipse,用它给Shape基类添加某个功能,特别

是要添加该基类中目前没有的方法——MoveBy():

public class Ellipse: Shape
{
public void MoveBy(int x, int y)
{
Position.X += x;
Position.Y += y;
}
}

过了一段时间,基类的编写者决定扩展基类的功能。为了保持一致,他也添加了一个名为MoveBy()的方法,该方法的名称和签名与前面添加的方法相同,但并不完成相同的工作。这个新方法可能声明为virtual,也可能不是。

如果重新编译派生的类,会得到一个编译器警告,因为出现了一个潜在的方法冲突。

然而,也可能使用了新的基类,但没有编译派生类;只是替换了基类程序集。基类程序集可以安装在全局程序集缓存中(许多Framework程序集都安装在此)。

现在假设基类的MoveBy()方法声明为虚方法,基类本身调用MoveBy()方法。会调用哪个方法?基类的方法还是前面定义的派生类的MoveBy()方法?因为派生类的MoveBy()方法没有用override关键字定义(这是不可能的,因为基类MoveBy()方法以前不存在),编译器假定派生类的MoveBy()方法是一个完全不同的方法,与基类的方法没有任何关系,只是名字相同。这种方法的处理方式就好像它有另一个名称一样。

编译Ellipse类会生成一个编译警告,提醒使用new关键词隐藏方法。在实践中,不使用new关键字会得到相同的编译结果,但避免出现编译器警告:

public class Ellipse: Shape
{
new
public void Move(Position newPosition)
{
Position.X = newPosition.X;
Position.Y = newPosition.Y;
}
//. . . other members
}

不使用new关键字,也可以重命名方法,或者,如果基类的方法声明为virtual,且用作相同的目的,就重写它。然而,如果其他方法已经调用了此方法,简单的重命名会破坏其他代码。

6、调用方法的基类版本

C#有一种特殊的语法用于从派生类中调用方法的基类版本:base.<MethodName>()。例如,派生类Shape声明了Move()方法,想要在派生类Rectangle中调用它,以使用基类的实现代码。为了添加派生类中的功能,可以使用base调用它:

public class Shape
{
public virtual void Move(Position newPosition)
{
Position.X = newPosition.X;
Position.Y = newPosition.Y;
WriteLine($"moves to {Position}");
}
//. . . other members
}

Move()方法在Rectangle类中重写,把Rectangle一词添加到控制台。写出文本之后,使用base关键字调用基类的方法:

public class Rectangle: Shape
{
public override void Move(Position newPosition)
{
Write("Rectangle ");
base.Move(newPosition);
}
//. . . other members
}

现在,矩形移动到一个新位置:

r.Move(new Position { X = 120, Y = 40 });

运行应用程序,输出是Rectangle和Shape类中Move()方法的结果:

Rectangle moves to X: 120, Y: 40

注意: 使用base关键字,可以调用基类的任何方法——而不仅仅是已重写的方法。

7、抽象类和抽象方法

C#允许把类和方法声明为abstract。抽象类不能实例化,而抽象方法不能直接实现,必须在非抽象的派生类中重写。显然,抽象方法本身也是虚拟的。如果类包含抽象方法,则该类也是抽象的,也必须声明为抽象的。

下面把Shape类改为抽象类。因为其他类需要派生自这个类。新方法Resize声明为抽象,因此它不能有在Shape类中的任何实现代码:

public abstract class Shape
{
public abstract void Resize(int width, int height); // abstract method
}

从抽象基类中派生类型时,需要实现所有抽象成员。否则,编译器会报错:

public class Ellipse : Shape{
public override void Resize(int width, int height)
{
Size.Width = width;
Size.Height = height;
}
}

当然,实现代码也可以如下面的例子所示。抛出类型NotImplementationException的异常也是一种实现方式,在开发过程中,它通常只是一个临时的实现:

public override void Resize(int width, int height)
{
throw new NotImplementedException();
}

使用抽象的Shape类和派生的Ellipse类,可以声明Shape的一个变量。不能实例化它,但是可以实例化Ellipse,并将其分配给Shape变量:

Shape s1 = new Ellipse();
DrawShape(s1);

8、密封类和密封方法

如果不应创建派生自某个自定义类的类,该自定义类就应密封。给类添加sealed修饰符,就不允许创建该类的子类。密封一个方法,表示不能重写该方法。

sealedclass FinalClass
{
// etc
}
class DerivedClass: FinalClass // wrong. Cannot derive from sealed class
.
{
// etc
}

在把类或方法标记为sealed时,最可能的情形是:如果在库、类或自己编写的其他类的操作中,类或方法是内部的,则任何尝试重写它的一些功能,都可能导致代码的不稳定。

密封类有另一个原因。对于密封类,编译器知道不能派生类,因此用于虚拟方法的虚拟表可以缩短或消除,以提高性能。string类是密封的。

将一个方法声明为sealed的目的类似于一个类。方法可以是基类的重写方法,但是在接下来的例子中,编译器知道,另一个类不能扩展这个方法的虚拟表;它在这里终止继承。

class MyClass: MyBaseClass
{
public sealed override void FinalMethod()
{
// implementation}
}
class DerivedClass: MyClass
{
public override void FinalMethod() // wrong. Will give compilation error
{
}
}

要在方法或属性上使用sealed关键字,必须先从基类上把它声明为要重写的方法或属性。如果基类上不希望有重写的方法或属性,就不要把它声明为virtual。

9、派生类的构造函数

这个没搞懂,后续完善


拓展:c#修饰符














 您阅读本篇文章共花了: 

免责声明
本站内容均为博客主本人日常使用记录的存档,如侵犯你的权益请联系:lifei@zaiheze.com 546262132@qq.com 沟通删除事宜。本站仅带访问端口形式使用,已杜绝搜索引擎爬取。

扫描二维码推送至手机访问。

版权声明:本文由LIFEI - blog发布,如需转载请注明出处。

本文链接:http://www.lifeiai.com/?id=285

分享给朋友:

相关文章

C# 第一篇 踏上征程 3年前 (2022-11-14)
C# 第二篇 基础语法3年前 (2022-11-14)
C# 第三篇 流程控制3年前 (2022-11-15)
C# 第七篇 窗口与控件2年前 (2022-12-05)
C# 连接Oracle数据库方法2年前 (2022-12-06)

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。