【问题标题】:How to redeclare parent method in a subclass?如何在子类中重新声明父方法?
【发布时间】:2020-12-24 04:29:05
【问题描述】:

我有一个抽象的Vehicle 类,它有抽象的Wheels。我知道Vehicle 的所有子类都会以同样的方式计算它们的_wheels,所以为了避免重复,我将实现写在基类中:

abstract class Vehicle {
  abstract _wheels: Wheel[];
  wheels() {
    return this._wheels; // assume it's more complex than this
  }
}

abstract class Wheel {}

然后我创建一个特定的子类:

class Car extends Vehicle {
  _wheels: CarWheel[];
  constructor() {
    super();
    this._wheels = [];
  }
}

class CarWheel extends Wheel {
  hubcap = new CarWheelHubcap()
}
class CarWheelHubcap {}

它没有Wheels,而是CarWheels 和CarWheelHubcap。然而,当需要枚举它们时,我遇到了一个问题:

const car = new Car();
car.wheels().forEach(wheel => wheel.hubcap);
                              ~~~~~~~~~~~~

Error: Property 'hubcap' does not exist on type 'Wheel'.

我希望这不会发生,因为我们已经将_wheels 重新声明为CarWheel[]。我的猜测是它沿着原型链向上,找到 Vehicle.prototype.wheels 并得出结论它的返回类型是 Wheel (?)

问题

是否可以在不重新实现 wheels() 类中的 wheels() 方法的情况下解决此问题?再一次,子类都将共享Vehicle.prototype.wheels,所以重写会很浪费

wheels() {
  return super.wheels() as CarWheel[];
}

在每个子类中。

跟进问题

Modified playground link

【问题讨论】:

  • 当您执行const car = new Car(); 时,_wheels 究竟应该包含什么?
  • @atiyar 我跳过了初始化以保持帖子简短。重要的一点是 _wheels 的类型是 CarWheel 而不是 Wheel

标签: typescript oop inheritance


【解决方案1】:

鉴于您的示例代码,

class CarWheelHubcap { }

class CarWheel extends Wheel {
  hubcap = new CarWheelHubcap();
}

然后继续到您遇到错误的地方,

const car = new Car();
car.wheels().forEach(wheel => wheel.hubcap);

Error: Property 'hubcap' does not exist on type 'Wheel'.

正如错误所说,Wheel 没有定义名为 hubcap 的成员。

您尝试重新声明具有更具体类型的Car 中的_wheels 属性(即影子)的方法存在许多问题。一方面,它不是类型安全的,因为数组是可变的。另一方面,实际错误源于wheels() 方法的返回类型是从Vehicle 类中属性_wheels 的类型推断出来的,这让您陷入编写额外的、无意义的代码或编写复杂的计算在这种相对简单的情况下不需要的类型注释。

相反,我们将利用Parametric Polymorphism。这个概念在 TypeScript 中作为泛型公开,而你 可以在TypeScript: Handbook - Generics 部分了解它们。

看来你想表达Car继承Vehicle.prototype,确实有轮子,但不只是Wheels,而是CarWheels。继承不足以表达这种双轴关系,我们需要使用泛型来表达和加强这种关系。

让我们先试一试:

class Vehicle<TWheel extends Wheel> {
  abstract _wheels: TWheel[];
  wheels() {
    return this._wheels; // assume it's more complex than this
  }
}

我们所做的是使用类型参数 TWheel 参数化 Vehicle,它指定它将具有哪些轮子。此外,我们规定为类型参数TWheel 提供的任何类型参数都必须扩展Wheel

现在在定义Car 时,我们必须为TWheel 传递一个类型参数

class Car extends Vehicle<CarWheel> {
  _wheels: CarWheel[] = [];
}

注意:我删除了多余的空构造函数,将其替换为字段初始值设定项。这相当于constructor() {super(); this._wheels = [];},同时更易于阅读和维护。

但是,由于 _wheels 数组的元素类型在我们扩展它时被传递给 Vehicle&lt;TWheel&gt;,我们可以通过在车辆中编写从 _wheels 中删除 abstract 修饰符来进一步改进代码,实现那里的数组并消除了在子类中定义它的任何需要。

class Vehicle<TWheel extends Wheel> {
  _wheels: TWheel[] = [];
  wheels() {
    return this._wheels; // assume it's more complex than this
  }
}

这将Car 简化为

class Car extends Vehicle&lt;CarWheel&gt;{ }

使用Car 参数化VehicleCarWheel,您的原始代码现在可以工作了。

car.wheels().forEach(wheel => wheel.hubcap);

以上方法现在有效,因为 Car 的轮子已知为 CarWheel

注意:为了保持与现有代码的兼容性,并允许没有专用车轮的车辆简单而简洁地扩展车辆,我们可以为我们的类型参数指定一个默认值。因此,如果说另一个类,比如代表装甲车,不使用专门的轮子,我们可以写class Armor extends Vehicle { }

class Vehicle<TWheel extends Wheel = Wheel> {
  _wheels: TWheel[] = [];
  wheels() {
    return this._wheels;
  }
}

最后,让我们对Vehicle 进行修改,使其更加地道。 wheels 不需要是方法,我们可以使用 get 属性访问器。

class Vehicle<TWheel extends Wheel = Wheel> {
  get wheels() { return this._wheels; }
}

这让消费者在编写car.wheels.forEach(...) 时无需考虑封装技术(“哦,这是一种包装方法”),这是他们不必担心的问题。

放在一起:

class Wheel {
  diameter = 30;
}

class Vehicle<TWheel extends Wheel = Wheel> {
  _wheels: TWheel[] = [];
  get wheels() {
    return this._wheels; // assume it's more complex than this
  }
}

class CarWheelHubcap {
  size = 15;
}

class CarWheel extends Wheel {
  hubcap = new CarWheelHubcap();
}

class Car extends Vehicle<CarWheel> {
  drivetrain: 'AWD' | 'FWD' | 'RWD' = 'AWD';
}

const car = new Car();

car.wheels
  .map(wheel => wheel.hubcap)
  .forEach(console.log);

Playground Link

请注意,我已为每个类添加了一个成员,因为空类是一种糟糕的做法,可能会导致错误并引起混乱和混乱。

此外,如果我们需要一个带有轮子数组的构造函数,参数多态性解决方案仍然可以很好地使用。我们可以简单地向Vehicle 添加一个带轮子的构造函数,所有子类都将公开一个适当类型的构造函数,需要指定TWheel 的数组。 Playground Link

【讨论】:

  • 惊人的答案,非常感谢!与原始问题无关,但是否有解决循环参数多态性的方法?例如,Wheel 现在有一个 _vehicle 父引用并使用 TVehicle 类型参数。 (修改后的游乐场链接发布在 Q 中)
  • @Alex 您可以使用any 打破循环,它可以按预期工作,但感觉不太正确,可能应该进行修改。更好的解决方案是提取某种中间条件类型或类似的东西,但我现在想不出。
猜你喜欢
  • 1970-01-01
  • 2011-07-01
  • 2017-03-11
  • 2023-03-31
  • 2020-07-27
  • 1970-01-01
  • 2020-12-10
  • 1970-01-01
  • 2016-06-11
相关资源
最近更新 更多