目录
这是100部分系列的第6部分,向您介绍TypeScript(如果您以前没有使用过它);如果您使用过它,这将使您重新了解您已经知道的东西,并可能向您介绍一些您以前可能没有遇到过的新事物。在这篇文章中,我们将了解什么是继承,以及如何将它应用于类。我们还将讨论如何使类抽象并使用protected关键字,并介绍将一种类型转换为另一种类型的能力。
在第5天,我们研究了如何实现接口来说明一个类可以具有什么行为。我说,最后,我们正在进入做继承的领域。今天,我们将研究如何实现接口、从类继承等等。
什么是继承?
如果您以前从未使用过面向对象的语言,那么继承的概念起初可能看起来有点奇怪,但实际上它非常简单。为了解释继承,我将回到我以前最喜欢的吉他。我将用一个“伪类”结构来演示如何使用继承。(伪类是演示类的类,但不是用任何特定语言编写的)。首先,我将看看吉他可能具有的特性。
- 琴颈(通常是一根,但Jimmy Page等吉他手有时会使用不止一根琴颈的吉他)
- 颈部可能有品丝
- 字符串
- 拾音器(用于电吉他)
- 拾音器选择器(用于电吉他)
- 音量控制(用于电吉他)
- 音色控制(用于电吉他)
- 琴桥(琴弦进入吉他琴身的部分)
- 带式连接器
- 琴体
- 制造商
- 一个品牌
- 序列号
- 音孔(用于原声/电声吉他)
- 琴头(大多数吉他顶部的位)
- 调谐器
这不是我们可以用来制作吉他的所有东西的详尽清单,但它为我们提供了一个很好的起点。在我开始创建我的类之前,我需要决定什么是共同的,什么是专业的;换句话说,所有吉他共有的任何东西都是一个共同特征,而特定于特定类型吉他的东西是专门的。任何常见的东西都会进入我们称之为基类的东西。基类是我们将继承的东西,它具有共享的属性和行为。
很想跳进去说这里的起点是吉他,但我要退后一步。虽然我列出了吉他的属性,但重要的是要记住,吉他属于我们称为乐器的一类,并且吉他的某些特征在所有商业乐器中都很常见;如果我正在为一家音乐商店编写应用程序,我想从这些功能开始作为我吉他的基础。
乐器作为层次结构
我将从一个MusicalInstrument伪类开始,并将常见的功能放入其中。
Class MusicalInstrument
Manufacturer
Make
SerialNumber
End Class
你可能会惊讶地发现我没有说我们在那里处理的是什么类型的乐器。我没有这样做的原因是继承;我将使用继承来说明我正在处理的是什么类型的工具,所以如果我想要一个Piano类,我可能会将其视为一种Keyboard类型,即一种MusicalInstrument类型。这会给我这样的东西:
Class Keyboard InheritsFrom MusicalInstrument
... Features common to all keyboard type instruments
End Class
Class Piano InheritsFrom Keyboard
... Features that are specific to a piano
End Class
回到吉他场景,我可能想像这样创建我的层次结构:
Class Guitar InheritsFrom MusicalInstrument
Neck
Headstock
Tuners
End Class
Class AcounsticGuitar InheritsFrom Guitar
SoundHole
End Class
Class ElectricGuitar InheritsFrom Guitar
Pickups
VolumeControl
ToneControls
End Class
我不会将每个属性都添加到这些类中,但这应该让您了解继承链应该是什么样子。我们在所有这些中看到的是,继承是关于从层次结构中创建对象。
使用TypeScript进行类继承
在第5天,我演示了如何使用implements“继承”接口(从技术上讲,我们称此实现而不是继承,但人们有时将接口实现视为继承)。我现在将演示如何从类继承。为了让这件事变得有趣,我将从我在上一篇文章中实现的相同验证接口开始。
interface Validate {
IsValid(): boolean;
}
如果您还记得,上一篇文章演示了最小长度验证和最大长度验证,但是在两个类之间重复了一些代码。我想要做的是创建一个基类,它可以完成公共代码所需的一切。我将从这段代码开始。
class LengthValidationBase implements Validate {
constructor(protected text: string, protected length: number) {}
IsValid(): boolean {
}
}
此代码不会编译,因为IsValid不返回任何内容。我在这里有两个选择。我可以返回默认值,也可以使用“技巧”来避免在此处返回值。我要使用的技巧是使这个类成为一个abstract类。这意味着这个类不能自己创建;它必须继承自,并且继承的类可以被实例化。
通过创建一个abstract类,我可以创建一个abstract方法。这是一段代码,实际上是在说“我不会在这里给你一个实现,但是从我那里继承的任何东西都必须添加它自己的实现。” 不出所料,创建类或方法abstract的关键字是abstract。有了这个,我的代码现在看起来像这样:
abstract class LengthValidationBase implements Validate {
constructor(protected text: string, protected length: number) {}
IsValid(): boolean {
return this.CheckForValidity();
}
public Debug() {
console.log(`Checking whether the length check of ${this.length} for
${this.text} is valid returns ${this.CheckForValidity()}`);
}
protected abstract CheckForValidity(): boolean;
}
我想在这里谈谈我类上的一些事情。让我们从构造函数开始。在之前的一篇文章中,我谈到了如何直接在构造函数签名中声明类级变量。这是TypeScript为我们提供的那些非常棒的特性之一,在C#等语言中是看不到的。然而,我们以前从未见过的是这个有趣的protected关键字。这是什么意思?
如果我希望某些内容仅在类内可见,我将字段方法标记为private。这意味着我无法从类外看到这个变量/方法。如果我从类继承,我仍然看不到那个变量。如果您认为我需要在派生类中查看text和length中的值(当我们从类继承时,我们说我们是从该类派生的),那么您是对的。我们说一个方法或函数不能从类外部看到,但可以从派生类中看到,就是使用protected关键字。
CheckForValidity方法很有趣。这是我将从该abstract方法调用的IsValid方法,为我们提供IsValid预期的布尔返回值。你可以看到这实际上并没有做任何事情——我们可以把它看作是一个签名或合同,上面写着“当你从我那里继承时,你必须为这个方法提供一个实现”。
注意:您只能在abstract类中添加abstract方法。如果您的类未标记为abstract,则您无法在其中创建abstract方法。
这是一个快速的问题。你认为我可以创建一个private abstract方法吗?答案是否定的,如果我尝试这样做,TypeScript会告诉我我无法创建private abstract方法。我会得到的信息是“private”修饰符不能与“abstract”修饰符一起使用。当你考虑它时,这完全有道理。一个private方法在类外是看不到的,但是一个abstract方法说必须在继承的类中添加代码。
你可能想知道的事情。为什么我添加了一个Debug方法?在我们的接口中没有描述,为什么我要添加一个?我想提供写出关于类正在做什么的调试信息的能力,因为调试能力与验证接口没有任何关系。通过在控制台日志中使用反引号`而不是撇号',我可以做一些叫做string插值的事情。这是string中看起来很有趣的语法,${}中的值直接打印出来。
让我们重新审视我们的最小和最大长度验证。通过使用继承,我摆脱了这一点。
class MinimumLengthValidation implements Validate {
constructor(private text: string, private minimumLength: number) {}
IsValid(): boolean {
return this.text.length >= this.minimumLength;
}
}
class MaximumLengthValidation implements Validate {
constructor(private text: string, private maximumLength: number) {}
IsValid(): boolean {
return this.text.length <= this.maximumLength;
}
}
到:
class MaximumLengthValidation extends LengthValidationBase {
protected CheckForValidity(): boolean {
return this.text.length <= this.length;
}
}
class MinimumLengthValidation extends LengthValidationBase {
protected CheckForValidity(): boolean {
return this.text.length >= this.length;
}
}
我似乎没有删除很多代码,但这不是进行继承的主要原因。继承的主要原因是消除代码重复。在上面的代码示例中,我只需要声明一次构造函数。这是一个简单且非常微不足道的示例,但这意味着我编写的任何从我的基类继承的代码都会在构建类时产生完全相同的行为。通过为我的abstract方法提供一个实现,我已经从派生类中完全删除了IsValid代码(请注意,这里abstract已从方法名称中删除,因为我提供的是真实代码)。
测试实现
我将测试我的代码的实现。首先,我将使用与上一篇文章中相同的方法来添加验证项。
const validation: Validate[] = [];
validation.push(new MinimumLengthValidation('ABC12345', 10));
validation.push(new MinimumLengthValidation('ABC12345AB12345', 10));
validation.push(new MaximumLengthValidation('ABC12345', 10));
validation.push(new MaximumLengthValidation('ABC12345AB12345', 10));
我们需要注意的是,我已经将这个数组创建为Validate实例数组。这对我们来说将变得很重要,因为当我在这里循环验证示例时,我想调用该Debug方法,但由于我的Validate接口中不存在该方法,因此我无法直接执行此操作。
validation.forEach((val: Validate) => {
console.log(val.IsValid());
// We want to call debug but we can't here.
// val.Debug(); is not going to work
// Do something clever here.
});
你看到我说我想在这里做一些聪明的事情了吗?我在这里想做的是做一些叫做演员的事情。演员表是一种将一种类型更改为另一种类型的方法。我在这里要做的是将Validate实现转换为一个LengthValidationBase类,以便我可以调用Debug。为此,我将编写以下代码,其中我使用as关键字来说明我想将我的类转换成什么。
const lengthValidation = val as LengthValidationBase;
lengthValidation.Debug();
在以后的文章中,我将进一步处理选角问题,因为我们可以用它做各种很酷的事情——远远超出我们在这篇文章中所能涵盖的范围。目前,我将把我的代码现在的样子留给你。
validation.forEach((val: Validate) => {
console.log(val.IsValid());
// We want to call debug but we can't here.
// val.Debug(); is not going to work
const lengthValidation = val as LengthValidationBase;
lengthValidation.Debug();
});
结论
在本文中,我们研究了继承是什么,以及如何将其应用于类。我们讨论了使类abstract和使用protected关键字。我们还介绍了将一种类型转换为另一种类型的能力。与往常一样,本文的代码可在Github上找到。
https://www.codeproject.com/Articles/5317050/100-Days-of-TypeScript-Day-6