JS++ |访问修饰符和“超级”
访问修饰符允许我们更改类(或模块)成员的“可见性”和“访问权限”。这些最好通过示例来理解。
JS++ 具有三个访问修饰符:private、protected 和 public。
私人成员是最不宽容的。如果成员被声明为“私有”,则只能从声明它的类或模块访问它。这是一个例子:
class Animal
{
private string name;
string getName() {
return name; // OK
}
}
class Dog : Animal
{
string getName() {
return name; // Error
}
}
Animal animal = new Animal();
animal.name; // ERROR
animal.getName(); // OK
可以从任何子类或子模块访问受保护的成员。这是一个例子:
class Animal
{
protected string name;
string getName() {
return name; // OK
}
}
class Dog : Animal
{
string getName() {
return name; // OK
}
}
Animal animal = new Animal();
animal.name; // ERROR
animal.getName(); // OK
最后,还有“public”访问修饰符。 'public' 访问修饰符是最不宽松的。声明为“public”的成员对访问没有限制,甚至可以从类外部访问(前提是从类实例访问它)。这是一个例子:
class Animal
{
public string name;
string getName() {
return name; // OK
}
}
class Dog : Animal
{
string getName() {
return name; // OK
}
}
Animal animal = new Animal();
animal.name; // OK
animal.getName(); // OK
访问修饰符启用封装。封装是面向对象编程的支柱之一(正如我们在本章开头所讨论的那样),它指的是数据(字段)和操作该数据的方法(例如方法、getter/setter 等)的捆绑.简单来说:通过将字段设为私有来隐藏您的数据,并且只能通过公共/受保护的方法、getter 或 setter 来访问它们。
JS++ 默认访问规则启用封装。在 JS++ 中,字段的默认访问修饰符为“private”。所有其他类成员的默认访问修饰符为“public”。换句话说,JS++ 访问规则是“成员敏感的”,而在Java和 C# 等语言中,您通常需要手动指定访问修饰符以实现封装,这可能会导致代码冗长。
为什么我们需要封装?回想一下我们的 getter 和 setter 示例,我们必须定义 getter 和 setter 方法来读取和修改 cat 的“name”字段。假设,假设我们的需求发生了变化,我们想在所有猫的名字前面加上“Kitty”。通过封装,我们只需要更改我们的 setter 方法。相反,如果我们将字段设为“public”并且必须通过其实例直接操作名称,则我们必须手动将前缀添加到实例对“name”字段的每个直接操作中。随着项目的复杂性增加,这将是不可取的。
既然我们已经对访问修饰符和封装有了深入的了解,那么让我们回到我们的项目。我们需要我们的“Cat”类来渲染()与“Animal”基类提供的不同。第一步是编辑我们的“Animal”基类以使 $element 字段“受保护”,以便我们的派生类(如“Cat”)可以访问该字段:
external $;
module Animals
{
class Animal
{
protected var $element = $(
"""
"""
);
void render() {
$("#content").append($element);
}
}
}
接下来,让我们将 render() 方法恢复为“Cat”:
external $;
module Animals
{
class Cat : Animal
{
string _name;
Cat(string name) {
_name = name;
}
void render() {
$element.attr("title", _name);
$("#content").append($element);
}
}
}
如果你现在尝试编译,你会得到一个编译错误。错误本身应该非常具有描述性:
JSPPE0252: `void Animals.Cat.render()’ conflicts with `void Animals.Animal.render()’. Either create a method with a different name or use the ‘overwrite’ modifier
在这种情况下,我们的派生类(“Cat”)试图定义一个名为“render”的方法,但基类(“Animal”)已经有一个名为“render”的方法。因此,我们有冲突。 JS++ 还建议我们修复:A)创建一个不同名称的方法,或 B)使用“覆盖”修饰符。
从概念上讲,这两种方法都描述了一个概念:渲染到网页。因此,我们可能不希望两个不同的名称来描述同一个概念。相反,我们想通过使用 'overwrite' 修饰符告诉 JS++ 编译器这是故意的:
external $;
module Animals
{
class Cat : Animal
{
string _name;
Cat(string name) {
_name = name;
}
overwrite void render() {
$element.attr("title", _name);
$("#content").append($element);
}
}
}
在其他面向对象的语言中,这被称为“方法隐藏”或“方法隐藏”。 JS++ 这样做的原因是为了防止潜在的错误和拼写错误(尤其是对于更复杂的类)。如果我们有两个不同的概念,比如“Cat”在内存中渲染而“Animal”在网页中渲染,在这种情况下我们不应该有相同的方法名称。
现在编译你的代码。它应该成功。打开网页,您现在应该可以再次将鼠标悬停在猫上以查看它们的名字。
在这个阶段,我们仍然有代码重复。下面看一下 'Animal' 的 render() 方法:
void render() {
$("#content").append($element);
}
这是我们的“Cat”渲染()方法:
overwrite void render() {
$element.attr("title", _name);
$("#content").append($element);
}
你注意到重复了吗?如果我们想稍后渲染到 ID 为“content”的元素之外的其他 HTML 元素怎么办?我们将不得不更改所有相关类中的渲染代码!
我们的“猫”类“扩展”了“动物”的概念。同样,我们的 Cat 的 render() 方法通过添加 HTML 'title' 属性“扩展”了 Animal 的 render() 方法,因此我们可以将鼠标悬停在上面并查看名称。但是,除此之外,我们的渲染逻辑是相同的:将元素添加到 ID 为“内容”的 HTML 元素。我们可以做得更好。让我们在“Cat”类中“重用”“Animal”类的渲染代码:
external $;
module Animals
{
class Cat : Animal
{
string _name;
Cat(string name) {
_name = name;
}
overwrite void render() {
$element.attr("title", _name);
super.render();
}
}
}
编译、运行并观察结果。现在,无论我们的渲染逻辑如何变化,它都将应用于所有相关的类。关键是“超级”关键字。 'super' 关键字是指当前类的超类。在这种情况下,我们使用它来访问“Animal”类的“render”方法。如果没有“super”,我们将调用当前类的“render”方法——导致无限递归! (例如,使用 'this' 而不是 'super' 将允许您引用 'Cat' 类的 'render' 方法......但它会导致无限递归。)
到目前为止,我们已经了解了私有、受保护和公共字段和方法,但是构造函数呢?打开 main.jspp 并添加以下代码:
import Animals;
Cat cat1 = new Cat("Kitty");
cat1.render();
Cat cat2 = new Cat("Kat");
cat2.render();
Animal animal = new Animal();
animal.render();
编译并运行。
哦哦!我们在页面上渲染了三只猫。至少当您将鼠标悬停在最后一只猫上时,它不会显示名称。但是,“动物”不是“猫”(但“猫”是“动物”)。我们有三个猫图标的原因是因为我们在 Animal.jspp 中有这个:
protected var $element = $(
"""
"""
);
换句话说,当我们的 $element 字段被初始化时,它总是被初始化为一个给我们一个猫图标的值。相反,我们可能想在“Animal”上定义一个构造函数来参数化这个初始化。让我们更改 Animal.jspp 以便在构造函数中初始化该字段:
external $;
module Animals
{
class Animal
{
protected var $element;
protected Animal(string iconClassName) {
string elementHTML = makeElementHTML(iconClassName);
$element = $(elementHTML);
}
public void render() {
$("#content").append($element);
}
private string makeElementHTML(string iconClassName) {
string result = '';
result += '';
result += "";
return result;
}
}
}
我在所有类成员上添加了访问修饰符以使代码更清晰。为了清楚起见,我还将 HTML 文本的构造分离到一个单独的函数中。养成实践单一职责原则的习惯:所有类做一件事,所有函数/方法做一件事。在上面的代码中,我们的构造函数做了一件事:初始化字段;我们的 render() 方法做了一件事:渲染到网页;最后,我们的“makeElementHTML”方法做了一件事:为我们的元素生成 HTML。这导致了干净的代码,并且 JS++ 在设计时考虑到了干净的代码,因此请尝试从设计中受益。
您可能已经注意到的另一个巧妙技巧是使用'
(单引号)来包装 HTML字符串,如上面的代码所示。这是为了避免在我们的 'makeElementHTML' 方法中转义用于包围 HTML 属性的"
(双引号)。
您可能已经注意到所有新的访问修饰符都不同:受保护的构造函数、公共的 render() 和私有的 makeElementHTML。让我们将其从限制性最强的(私人)分解为限制性最小的(公共)。
'makeElementHTML' 是私有的原因是因为它是一个实现细节。 'makeElementHTML' 的唯一用途是在我们的 'Animal' 类中。 'Cat' 类无法访问该方法,并且 main.jspp 无法访问该方法(通过实例化)。 'Cat' 类永远不需要调用 'makeElementHTML' —— 相反,'Cat' 类继承自 'Animal' 类。通过继承,“Cat”类将调用“Animal”构造函数。 (由于代码目前无法编译,我们很快就会谈到这一点,但首先要理解这些概念更重要。)因此,“Cat”类将通过“Animal”类构造函数调用“makeElementHTML”,但它无法访问该方法,也无法直接调用它。这样,'makeElementHTML' 是 'Animal' 类的实现细节,不会暴露给我们代码的任何其他部分。这种隐藏与其他类和代码无关的细节在面向对象编程中被称为“抽象”。
正如我们在本章开头提到的,抽象是面向对象编程(OOP)的另一个基本支柱。例如,想象一辆汽车。当您踩下汽车的油门踏板时,您无需了解内燃机的具体工作原理。内部工作的复杂性通过一个简化的界面呈现给您:油门踏板。通过抽象,我们使复杂的系统变得简单,这是 OOP 的理想属性。
在私有“makeElementHTML”方法之后,下一个具有访问权限的代码是“受保护”构造函数。再一次,“受保护”访问修饰符的限制性低于“私有”,但不如“公共”(除了范围之外没有访问限制)那么宽松。
具体来说,使构造函数“受保护”意味着什么?回想一下,“受保护”访问修饰符允许类中的所有成员访问,但也包括所有派生类。还记得类的实例化执行构造函数中指定的代码。从逻辑上讲,我们可以得出结论,受保护的构造函数意味着不能在特定上下文之外实例化一个类。
这些具体的背景是什么?明显的情况是我们不能从 main.jspp 中实例化“Animal”。如果你现在尝试,你会得到一个编译错误。然而,由于 'protected' 只能从类本身和所有派生类中访问,因此我们代码中受保护的构造函数的目的是将类限制为仅继承。回想一下,'Cat' 类不能直接调用私有的 'makeElementHTML';此方法在继承期间通过“Animal”构造函数执行。在继承期间,构造函数就像在实例化中一样被执行。
如果您要将构造函数设为“私有”,则基本上会阻止类的实例化和继承。 (旁注:这是 JS++ 标准库“System.Math”类的实现方式。)请记住:除字段之外的所有内容的默认访问规则都是“公共”。换句话说,如果我们未指定构造函数的访问修饰符,它将默认为“public”。
我们之前用来访问超类方法的“super”关键字是指在实例化过程中创建的超类的一个实例。当我们实例化“Cat”时,我们也实例化了“Animal”。所有相关的构造函数都将从链的底部开始执行。在我们的例子中,当 'Cat' 被实例化时,我们首先执行 'Cat' 构造函数,然后我们向上移动继承链,然后执行 'Animal' 类构造函数。 (JS++ 使用“统一类型系统”,其中“System.Object”是所有内部类型的根,因此也将调用此类的构造函数——但前提是它被确定为必要且不是“死代码消除”的候选对象”——但这超出了本章的范围,将在标准库章节中讨论。)
知道在继承期间会调用构造函数,我们现在可以解决代码中的剩余问题:您会注意到代码当前无法编译。原因是当我们定义自定义构造函数时,我们已经停止使用“Animal”类的隐式默认构造函数。
我们的“Animal”构造函数采用一个参数:
protected Animal(string iconClassName) {
string elementHTML = makeElementHTML(iconClassName);
$element = $(elementHTML);
}
我们需要更改“Cat”构造函数代码,以便我们指定应该如何调用超类构造函数。我们可以通过 'super' 关键字再次执行此操作。 'Animal' 类想知道我们要渲染的动物类型的图标名称。如果您不记得图标的名称,为了方便起见,我已将其包含在“超级”调用中:
external $;
module Animals
{
class Cat : Animal
{
string _name;
Cat(string name) {
super("icofont-animal-cat");
_name = name;
}
overwrite void render() {
$element.attr("title", _name);
super.render();
}
}
}
对“super”关键字的函数调用将执行超类的相关构造函数。 “super”调用必须始终是第一个语句,因为从语义上讲,超类的构造函数将在其派生类的构造函数代码之前执行。
最后,我们需要修改 main.jspp 以删除“Animal”类的实例化。请记住,由于我们将“Animal”构造函数设置为“protected”,因此无论如何我们将无法从 main.jspp 实例化“Animal”:
import Animals;
Cat cat1 = new Cat("Kitty");
cat1.render();
Cat cat2 = new Cat("Kat");
cat2.render();
至此,可以编译了,项目应该编译成功了。再一次,我们应该有两只猫:
最后,我们可以添加更多的动物。
狗.jspp:
external $;
module Animals
{
class Dog : Animal
{
string _name;
Dog(string name) {
super("icofont-animal-dog");
_name = name;
}
overwrite void render() {
$element.attr("title", _name);
super.render();
}
}
}
Dog.jspp 与 Cat.jspp 非常相似,因为狗也是需要命名的驯养动物。
熊猫.jspp:
external $;
module Animals
{
class Panda : Animal
{
Panda() {
super("icofont-animal-panda");
}
}
}
与 Cat.jspp 和 Dog.jspp 不同,Panda.jspp 明显更简单。 'Panda' 类所做的所有事情都是从 'Animal' 继承并指定要渲染的图标。它没有名称,它的 render() 方法与 Animal 的方法完全相同,因为它不必在鼠标悬停时添加 HTML 'title' 属性来显示名称。
犀牛.jspp:
external $;
module Animals
{
class Rhino : Animal
{
Rhino() {
super("icofont-animal-rhino");
}
}
}
就像 Panda.jspp 一样,Rhino.jspp 也是一个非常简单的类。它只是继承自“Animal”,不需要设置或渲染名称。
最后,修改 main.jspp 以实例化新的动物:
import Animals;
Cat cat1 = new Cat("Kitty");
cat1.render();
Cat cat2 = new Cat("Kat");
cat2.render();
Dog dog = new Dog("Fido");
dog.render();
Panda panda = new Panda();
panda.render();
Rhino rhino = new Rhino();
rhino.render();
像这样编译整个项目:
$ js++ src/ -o build/app.jspp.js
再一次,在所有平台(Windows、Mac 和 Linux)上,我们都是从命令行操作的,因此每个人的编译说明都应该相同。此外,完全没有必要指定“编译顺序”。 'Cat' 依赖于 'Animal' 并不重要,因此,'Animal.jspp' 应该在 'Cat.jspp' 之前处理。即使是最复杂的项目(例如循环导入和复杂依赖项),JS++ 也会自动为您解析编译顺序。只需指定输入目录,让 JS++ 递归查找输入文件并找出编译顺序。
在您的网络浏览器中打开 index.html。结果应如下所示:
验证您的两只猫有名字,您的狗有名字,但是当您将鼠标悬停在上面时,熊猫和犀牛不应该有名字。
如果一切正常:恭喜!此时,您可能已经注意到我们可以将继承层次结构更改如下:
Animal
|_ DomesticatedAnimal
|_ Cat
|_ Dog
|_ WildAnimal
|_ Panda
|_ Rhino
但是,这留给读者作为练习。
我们现在已经涵盖了我们在介绍章节中讨论的 OOP 的四个基本概念中的三个:抽象、封装和继承。 OOP 的最后一个基本支柱是多态性,我们将在下一节中介绍。