(@RyanCavanuagh 更新)
请在询问“任何更新”、“请立即添加”等之前查看此评论。没有有意义地添加到讨论中的评论将被删除,以保持线程长度在一定程度上合理。
(注意,这不是问题#1524 的副本。这里的提议更符合 C++ 覆盖说明符,这对打字稿更有意义)
覆盖关键字在打字稿中将非常有用。 这将是覆盖超类方法的任何方法上的可选关键字,并且类似于 C++ 中的覆盖说明符,表示“_此方法的名称+签名应该始终匹配超类方法的名称+签名_”的意图. 这在较大的代码库中捕获了一系列问题,否则这些问题很容易被忽略。
再次类似于 C++,_从被覆盖的方法中省略 override 关键字不是错误_。 在这种情况下,编译器的行为与当前完全相同,并跳过与 override 关键字相关的额外编译时间检查。 这允许更复杂的无类型 javascript 场景,其中派生类覆盖与基类的签名不完全匹配。
class Animal {
move(meters:number):void {
}
}
class Snake extends Animal {
override move(meters:number):void {
}
}
// Add an additional param to move, unaware that the intent was
// to override a specific signature in the base class
class Snake extends Animal {
override move(meters:number, height:number):void {
}
}
// COMPILE ERROR: Snake super does not define move(meters:number,height:number):void
// Rename the function in the base class, unaware that a derived class
// existed that was overriding the same method and hence it needs renaming as well
class Animal {
megamove(meters:number):void {
}
}
// COMPILE ERROR: Snake super does not define move(meters:number):void
// Require the function to now return a bool, unaware that a derived class
// existed that was still using void, and hence it needs updating
class Animal {
move(meters:number):bool {
}
}
// COMPILE ERROR: Snake super does not define move(meters:number):void
除了额外的编译时验证,override 关键字为 typescript 智能感知提供了一种机制,可以轻松显示和选择可用的超级方法,其目的是在派生类中专门覆盖其中一个。 目前这非常笨重,需要浏览超类链,找到要覆盖的方法,然后将其复制粘贴到派生类中以保证签名匹配。
在类声明中:
确实是个好提议。
但是,以下示例对我来说是有效的覆盖:
class Snake extends Animal {
override move(meters:number, height=-1):void {
}
}
class A {...}
class Animal {
setA(a: A): void {...}
getA(): A {...}
}
class B extends A {...}
class Snake extends Animal {
override setA(a: B): void {...}
override getA(): B {...}
}
此外,我会添加一个编译器标志来强制出现 override 关键字(或报告为警告)。
原因是在重命名继承类已经实现的基类中的方法时捕获(但不应该是覆盖)。
啊很好的例子。 一般来说,我希望使用 override 关键字来强制签名的_exact_匹配,因为使用它的目标是维护严格的类型化类层次结构。 因此,为了解决您的示例:
class C extends A {...}
var animal : Animal = new Snake();
animal.setA(new C());
// This will have undefined run-time behavior, as C will be interpreted as type B in Snake.setA
因此,示例 (2.) 实际上是一个很好的演示,展示了 override 关键字如何在编译时捕获微妙的边缘情况,否则会被错过! :)
而且我要再次强调,这两个示例在可能需要的特定受控/高级 javascript 场景中可能是有效的……在这种情况下,用户可以选择省略 override 关键字。
这将很有用。 我们目前通过包含对 super 方法的虚拟引用来解决此问题:
class Snake extends Animal {
move(meters:number, height?:number):void {
super.move; // override fix
}
}
但这只能防止第二种情况:超级方法被重命名。 签名的更改不会触发编译错误。 此外,这显然是一个 hack。
我也不认为派生类方法签名中的默认和可选参数应该触发编译错误。 这可能是正确的,但与 JavaScript 固有的灵活性背道而驰。
@rwyborn
看来我们并不期望同样的行为。
您将使用此 override 关键字来确保相同的签名,而我会更多地将其用作 readibiliy 选项(因此我请求添加编译器选项以强制其使用)。
事实上,我真正期望的是 TS 检测到无效的覆盖方法(即使没有使用覆盖)。
通常:
class Snake extends Animal {
move(meters:number, height:number):void {}
}
应该引发错误,因为它实际上是 Animal.move() (JS 行为)的覆盖,但不兼容(因为高度不应该是可选的,而如果从 Animal“引用”调用它将是未定义的)。
实际上,使用 override 只会(由编译器)确认此方法确实存在于基类中(因此具有兼容的签名,但由于前一点,而不是由于 override 关键字)。
@stephanedr ,作为一个单独的用户,我实际上同意你的观点,编译器应该总是确认签名,因为我个人喜欢在我的类层次结构中强制执行严格的类型(即使 javascript 没有!!)。
然而,在通过 override 关键字提出此行为是可选的时,我试图记住最终 javascript 是无类型的,因此默认强制执行严格的签名匹配将导致某些 javascript 设计模式不再在 Typescript 中表达。
@rwyborn ,我很高兴您提到了 C++ 实现,因为这正是我在到达这里之前所想象的它应该如何工作 - 可选。 虽然,强制使用 override 关键字的编译器标志在我的书中会很好地记录下来。
该关键字将允许开发人员笨拙的打字时出现编译时错误,这是我最担心他们当前形式的覆盖。
class Base {
protected commitState() : void {
}
}
class Implementation extends Base {
override protected comitState() : void { /// error - 'comitState' doesn't exist on base type
}
}
目前(从 1.4 开始)上面的Implementation
类只会声明一个新方法,开发人员在注意到他们的代码不起作用之前不会更聪明。
在建议审查中讨论。
我们绝对了解这里的用例。 问题是在语言的这个阶段添加它会增加比它消除的更多的混乱。 具有 5 个方法的类,其中 3 个被标记override
,并不意味着其他 2 个_aren't_ 覆盖。 为了证明它的存在,修改器确实需要比这更干净地划分世界。
请原谅抱怨,但老实说,虽然你的论点确实适用于public
关键字在默认情况下一切都是公开的语言,确实是世界划分,有像abstract
和可选的override
关键字只会帮助开发人员感到更安全,犯更少的错误并浪费更少的时间。
覆盖是该语言为数不多的对打字错误高度敏感的方面之一,因为输入错误的覆盖方法名称并不是一个明显的编译时问题。 override
的好处是显而易见的,因为它允许您声明您的重写意图 - 如果基本方法不存在,则它是编译时错误。 所有人都欢呼类型系统。 为什么会有人不想要这个?
我 100% 同意@hdachev , @RyanCavanaugh也提到的小不一致很容易被关键字在将编译时检查引入方法覆盖方面的好处而被权衡。 我要再次指出,C++ 成功地使用了可选的 override 关键字,其方式与为 typescript 建议的方式完全相同。
我不能足够强调覆盖检查在具有复杂 OO 树的大规模代码库中的差异有多大。
最后我要补充一点,如果可选关键字的不一致确实是一个问题,那么可以使用 C# 方法,即强制使用“new”或“override”关键字:
class Dervied extends Base {
new FuncA(newParam) {} // "new" says that I am implementing a new version of FuncA() with a different signature to the base class version
override FuncB() {} // "override" says that I am implementing exactly the same signature as the base class version
FuncC() {} // If FuncC exists in the base class then this is a compile error. I must either use the override keyword (I am matching the signature) or the new keyword (I am changing the signature)
}
这与public
不同,因为已知没有访问修饰符的属性是公共的; 没有override
的方法_不_已知是不可覆盖的。
这是一个使用装饰器(在 TS1.5 中提供)的运行时检查解决方案,它可以以很少的开销产生良好的错误消息:
/* Put this in a helper library somewhere */
function override(container, key, other1) {
var baseType = Object.getPrototypeOf(container);
if(typeof baseType[key] !== 'function') {
throw new Error('Method ' + key + ' of ' + container.constructor.name + ' does not override any base class method');
}
}
/* User code */
class Base {
public baseMethod() {
console.log('Base says hello');
}
}
class Derived extends Base {
// Works
<strong i="9">@override</strong>
public baseMethod() {
console.log('Derived says hello');
}
// Causes exception
<strong i="10">@override</strong>
public notAnOverride() {
console.log('hello world');
}
}
运行此代码会产生错误:
错误:Derived 的方法 notAnOverride 没有覆盖任何基类方法
由于此代码在类初始化时运行,因此您甚至不需要特定于所讨论方法的单元测试; 一旦您的代码加载,错误就会发生。 您还可以加入不检查生产部署的“快速”版本的override
。
@RyanCavanaugh所以我们在 Typescript 1.6 中,装饰器仍然是一个实验性功能,而不是我想在大规模生产代码库中部署的东西,作为一种黑客来获得覆盖工作。
从另一个角度来看,所有流行的类型化语言都支持“覆盖”关键字; Swift、ActionScript、C#、C++ 和 F# 等等。 所有这些语言都有你在这个线程中表达的关于覆盖的小问题,但显然有一大群人认为覆盖的好处远远超过了这些小问题。
您的反对纯粹是基于成本/收益吗? 如果我真的要在 PR 中实现这一点,它会被接受吗?
这不仅仅是成本/收益问题。 正如 Ryan 解释的那样,问题在于将一个方法标记为覆盖并不意味着另一个方法_不是_一个覆盖。 唯一有意义的方法是,如果所有覆盖都需要用override
关键字标记(如果我们强制要求,这将是一个重大更改)。
@DanielRosenwasser如上所述,在 C++ 中,override 关键字是可选的(完全按照 Typescript 的建议),但每个人都可以毫无问题地使用它,并且它在大型代码库中非常有用。 此外,在 Typescript 中,由于 javascript 函数重载,它实际上是可选的,这很有意义。
class Base {
method(param: number): void { }
}
class DerivedA extends Base {
// I want to *exactly* implement method with the same signature
override method(param: number): void {}
}
class DerivedB extends Base {
// I want to implement method, but support an extended signature
method(param: number, extraParam: any): void {}
}
至于整个“并不意味着另一种方法不是覆盖”的说法,它完全类似于“私有”。 您可以编写整个代码库而无需使用 private 关键字。 该代码库中的一些变量只会被私下访问,一切都会编译并正常工作。 然而,“private”是一些额外的语法糖,你可以用来告诉编译器“不,如果有人试图访问它,编译错误”。 同样,“重载”是额外的语法糖,告诉编译器“我希望它与基类声明完全匹配。如果它没有编译错误”。
你知道吗,我认为你们对“覆盖”的字面解释很着迷。 它真正标记的是“exactly_match_signature_of_superclass_method”,但这并不那么可读:)
class DerivedA extends Base {
exactly_match_signature_of_superclass_method method(param: number): void {}
}
我也希望使用 override 关键字,如果基类中标记为 override 的方法不存在或具有不同的签名,则让编译器生成错误。 这将有助于可读性和重构
+1,工具也会变得更好。 我的用例是使用反应。 每次使用ComponentLifecycle
方法时,我都必须检查定义:
``` C#
接口组件生命周期
{
componentWillMount?(): 无效;
componentDidMount?(): 无效;
componentWillReceiveProps?(nextProps: P, nextContext: any): void;
shouldComponentUpdate?(nextProps: P, nextState: S, nextContext: any): boolean;
componentWillUpdate?(nextProps: P, nextState: S, nextContext: any): void;
componentDidUpdate?(prevProps: P, prevState: S, prevContext: any): void;
componentWillUnmount?(): 无效;
}
With override, or other equivalent solution,you'll get a nice auto-completion.
One problem however is that I will need to override interface methods...
``` C#
export default class MyControlextends React.Component<{},[}> {
override /*I want intellisense here*/ componentWillUpdate(nextProps, nextState, nextContext): void {
}
}
@olmobrutall似乎您的用例可以通过提供重构(如“实现接口”)或提供更好的完成的语言服务来更好地解决,而不是通过向语言添加新关键字来解决。
让你不要分心 :) 语言服务功能只是在大型代码库中维护接口层次结构的一小部分。 到目前为止,最大的胜利实际上是当你的层次结构中某个地方的类不符合时,编译时错误。 这就是 C++ 添加可选覆盖关键字(非破坏性更改)的原因。 这就是 Typescript 应该这样做的原因。
Microsoft 的 C++ 覆盖文档很好地总结了一切, https://msdn.microsoft.com/en-us/library/jj678987.aspx
使用覆盖有助于防止代码中的无意继承行为。 以下示例显示了在不使用 override 的情况下,派生类的成员函数行为可能不是预期的。 编译器不会为此代码发出任何错误。
...
当您使用 override 时,编译器会生成错误,而不是默默地创建新的成员函数。
到目前为止,最大的胜利实际上是当你的层次结构中某个地方的类不符合时,编译时错误。
我不得不同意。 我们团队中出现的一个陷阱是人们认为他们已经覆盖了方法,而实际上他们只是稍微输入错误或扩展了错误的类。
在跨多个项目工作时,基础库中的接口更改比使用 TypeScript 等具有议程的语言更难。
我们有很多很棒的东西,但也有像这样的奇怪之处,而且没有类级别的 const 属性。
@RyanCavanaugh是的,但是语言服务可以在编写覆盖后触发,就像许多语言一样,没有关键字更难确定何时是正确的时刻。
关于实现接口,请注意接口中的大多数方法都是可选的,您只需要覆盖您需要的少数方法,而不是整个包。 您可以打开带有复选框的对话框,但仍然...
虽然我目前的痛点是找到方法的名称,但如果有人重命名或更改基本方法的签名,将来会很好地收到编译时错误的通知。
不写覆盖时,不能通过添加警告来解决不一致吗? 我认为 typescript 正在做正确的事情,添加小的合理的重大更改,而不是保留错误的决定。
抽象也已经存在,他们将成为一对很棒的情侣:)
我也觉得需要一个“覆盖”说明符。 在大中型项目中,此功能变得必不可少,恕我直言,我希望 Typescript 团队重新考虑拒绝此建议的决定。
对于任何感兴趣的人,我们编写了一个自定义 tslint 规则,它通过使用装饰器提供与 override 关键字相同的功能,类似于上面 Ryan 的建议,但在编译时检查。 我们很快就会开源它,我会在它可用时回帖。
我也强烈感觉到'override'关键字的必要性。
就我而言,我更改了基类中的一些方法名称,但忘记重命名覆盖方法的一些名称。 当然,这会导致一些错误。
但是,如果有这样的特性,我们可以很容易地找到这些类方法。
正如@RyanCavanaugh提到的,如果此关键字只是一个可选关键字,则此功能会造成混淆。 那么,如何在 tsc 中做一些标记来启用这个功能呢?
请重新考虑此功能....
对我来说,如果 override 关键字有用,就需要强制执行,就像在 C# 中一样。 如果您在 C# 中指定了覆盖基方法签名的方法,则_必须_将其标记为覆盖或新建。
与 C# 相比,C++ 在某些地方令人讨厌且劣势,因为它使太多的关键字可选,因此排除了_consistency_。 例如,如果你重载了一个虚方法,重载的方法可以被标记为虚拟或不被标记——无论哪种方式它都是虚拟的。 我更喜欢后者,因为它可以帮助其他开发人员阅读代码,但我无法让编译器强制将其放入,这意味着我们的代码库无疑会在它们真正应该存在的地方缺少虚拟关键字。 override 关键字同样是可选的。 在我看来,两者都是垃圾。 这里忽略的是代码可以用作文档并通过强制需要关键字而不是“接受或离开”方法来提高可维护性。 C++ 中的“throw”关键字类似。
为了在 TypeScript 中实现上述目标,编译器需要一个标志来“启用”这种严格的行为与否。
就 base 和 override 的函数签名而言,它们应该是相同的。 但是希望在返回类型的规范中允许协变以强制执行编译时检查。
我从 AS3 来到 TS,所以当然,我也会在这里为override
关键字投票。 对于不熟悉任何给定代码库的开发人员来说,看到override
是了解(子)类中可能发生的事情的巨大线索。 我认为这样的关键字极大地增加了价值。 我会选择让它成为强制性的,但我可以看到这将是一个重大变化,因此应该是可选的。 虽然我对可选的override
和默认的public
关键字之间的区别很敏感,但我真的没有看到强加的歧义性问题。
对于所有 +1 人——您能谈谈上面显示的装饰器解决方案还有哪些不足之处吗?
对我来说,它感觉像是一种人工构造,而不是语言本身的属性。 我想这是设计使然,因为它就是这样。 我猜这会向开发人员(好吧,无论如何我)发送消息,即它是暂时的,而不是最佳实践。
显然每种语言都有自己的范式,而且作为 TypeScript 的新手,我对范式转换很慢。 我不得不说,尽管出于多种原因, override
对我来说确实是一种最佳实践。 我正在切换到 TypeScript,因为我已经完全接受了强类型的 koolaid,并且相信成本(在击键和学习曲线方面)远远超过了无错误代码和代码理解的好处。 override
是这个难题中非常重要的一块,它传达了一些关于覆盖方法角色的非常重要的信息。
对我来说,IDE 的便利性不那么重要,尽管在得到适当支持的情况下无疑是很棒的,更多的是关于实现我认为是您构建该语言所依据的原则。
@RyanCavanaugh我看到的一些问题:
不过,编译器已经在检查参数列表和返回类型。 而且我认为即使override
确实作为一等关键字存在,我们也不会强制执行一些关于签名相同性的新规则
而且我认为即使 override 确实作为一等关键字存在,我们也不会强制执行一些关于签名相同性的新规则。
@RyanCavanaugh那么我认为您可能在另一个页面上关于关键字的意图。 它的全部意义在于您_想要_强制签名相同的情况。 这适用于设计模式,在这种设计模式中,您有一个位于定义接口方法契约的基类上的深层复杂层次结构,并且层次结构中的所有类必须_完全_匹配这些签名。 通过在所有派生类中的这些方法上添加 override 关键字,您会收到任何签名与基类中规定的约定不同的情况的警报。
正如我一直说的,这不是一些深奥的用例。 在处理大型代码库(例如数百或数千个类)时,这是每天都会发生的事情,即有人需要更改基类的签名(修改合同),并且您希望编译器提醒您任何整个层次结构中不再匹配的案例。
编译器已经警告非法覆盖。 与问题
当前的实现是在声明覆盖时缺乏意图
方法。
之前提到的装饰器似乎与语言背道而驰
正在努力实现。 不应该产生运行时成本
可以在编译时处理的事情,无论成本多么小。
我们希望尽可能多地发现出错的东西,而不需要
运行代码找出答案。
可以使用装饰器和自定义自定义 tslint 来实现
规则,但对工具生态系统和社区来说会更好
具有官方关键字的语言。
我认为可选是一种方法,但与某些 C++ 编译器一样
您可以设置一些标志来强制使用它(例如,建议覆盖,
不一致的缺失覆盖)。 这似乎是避免的最佳方法
重大更改,并且似乎与添加的其他新功能一致
最近,例如可空类型和装饰器。
2016 年 3 月 23 日星期三 21:31,Rowan Wyborn [email protected]写道:
而且我认为即使 override 确实作为一流的关键字存在,我们
不会强制执行一些关于签名相同性的新规则。@RyanCavanaugh https://github.com/RyanCavanaugh那么我想你可能会
在关于关键字意图的不同页面上。 整点
它适用于您_想要_强制签名相同的情况。 这
适用于您有很深的复杂层次结构的设计模式
在定义接口方法契约的基类上,以及所有
层次结构中的类必须_完全_匹配这些签名。 通过增加
所有派生类中这些方法的 override 关键字,你是
警告任何签名与合同规定不同的情况
在基类中。正如我一直说的,这不是一些深奥的用例。 工作时
大型代码库(例如数百或数千个类)这是一个 _every
day_发生,即有人需要更改base的签名
类(修改合同),并且您希望编译器提醒您任何
整个层次结构中不再匹配的案例。—
您收到此消息是因为您订阅了此线程。
直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/2000#issuecomment -200551774
@kungfusheep fwiw 编译器仅捕获某一类非法覆盖,即声明的参数之间存在直接冲突的地方。 它_not_ 捕获参数的添加或删除,也没有捕获返回类型的变化。 这些额外的检查是任何覆盖关键字都会打开的。
如果您 _add_ 将参数添加到覆盖函数,编译器会正确地警告您。
但是,删除参数_完全有效_:
class BaseEventHandler {
handleEvent(e: EventArgs, timestamp: number) { }
}
class DerivedEventHandler extends BaseEventHandler {
handleEvent(e: EventArgs) {
// I don't need timestamp, it's OK
}
}
更改返回类型也是_完全有效_:
class Base {
specialClone(): Base { ... }
}
class Derived extends Base {
specialClone(): Derived { ... }
}
@RyanCavanaugh是的,从严格的语言角度来看,它们是有效的,但这就是我开始使用上面的“合同”一词的原因。 如果一个基类列出了一个特定的签名,当我使用覆盖时,我声明我希望我的派生类严格遵循该签名。 如果基类添加了额外的参数或更改了返回类型,我想知道我的代码库中不再匹配的每个点,因为它们最初编写的合同现在已经以某种方式发生了变化。
拥有一个大类层次结构的想法,每个类层次结构都有自己的基本方法略有不同的排列(即使它们从语言的角度来看是有效的)是一场噩梦,让我回到了打字稿出现之前 javascript 的糟糕旧时代:)
如果可选性是关键字的主要问题,那么当基类方法定义为abstract
时,为什么不强制它呢? 这样,如果您想严格执行该模式,您可以简单地添加一个抽象基类。 为了避免破坏代码,编译器开关可以禁用检查。
我们现在似乎在谈论两组不同的期望。 有缺失覆盖/非法覆盖的情况,然后是显式签名的情况。 我们是否都同意前者是我们对这个关键字的期望的绝对最低限度?
我之所以这么说,是因为目前还有其他方法可以强制执行显式方法签名,例如接口,但目前还没有明确说明重写意图的方法。
好的,没有办法强制重写方法的显式签名,但鉴于编译器强制任何签名更改至少是“安全的”,那么似乎围绕该问题的解决方案有一个单独的对话。
是的,同意了。 如果我必须选择,丢失覆盖/非法覆盖情况是更重要的问题要解决。
我参加聚会有点晚了...我认为 Typescript 的全部意义在于在编译时而不是在运行时强制执行规则,否则我们都将使用纯 Javascript。 此外,使用 hacks/kludges 来做这么多语言的标准操作有点奇怪。
Typescript 中应该有override
关键字吗? 我当然相信。 它应该是强制性的吗? 出于兼容性原因,我会说它的行为可以用编译器参数指定。 它应该强制执行确切的签名吗? 我认为这应该是一个单独的讨论,但到目前为止我对当前的行为没有任何问题。
关闭它的最初原因似乎是我们不能引入所有被覆盖的方法都需要override
说明符的重大更改,这会让人感到困惑,因为未标记为override
的方法也可以在事实上被覆盖。
为什么不使用编译器选项强制执行它,和/或如果类中有_至少一个_方法标记override
,那么所有被覆盖的方法都必须标记为这样,否则会出错?
是否值得在它悬而未决的时候重新打开它?
2016 年 4 月 8 日星期五 14:38,Peter Palotas [email protected]写道:
原来关闭this的原因好像是我们不能引入
所有被覆盖的方法都需要覆盖的重大变化
说明符,这会让人感到困惑,因为方法没有用
“覆盖”实际上也可以是覆盖。为什么不使用编译器选项和/或如果有
类中标记为覆盖的至少“一个”方法,然后是所有方法
覆盖必须这样标记,否则会出错?—
你收到这个是因为你被提到了。
直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/2000#issuecomment -207434898
我说请重新打开! 我会对编译器选项感到满意。
是的 - 请重新打开
重新打开这个!
ES7 是否需要这种覆盖/重载具有相同的多个方法
姓名?
2016 年 4 月 8 日上午 10:56,“Aram Taieb” [email protected]写道:
是的,请重新打开这个!
—
您收到此消息是因为您订阅了此线程。
直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/2000#issuecomment -207466464
+1 - 对我来说,这是我在日常 TypeScript 开发中最大的类型安全差距。
每次我实现像“componentDidMount”这样的 React 生命周期方法时,我都会搜索相关的 React 文档页面并复制/粘贴方法名称,以确保我没有拼写错误。 我这样做是因为它需要 20 秒,而来自拼写错误的错误是间接且微妙的,并且可能需要更长的时间来追踪。
具有 5 个方法的类,其中 3 个被标记为覆盖,并不意味着其他 2 个不是覆盖。 为了证明它的存在,修改器确实需要比这更干净地划分世界。
如果这是主要问题,请调用关键字check_override
以明确您选择覆盖检查,并暗示其他方法是_未检查_而不是_未覆盖_。
使用implements.
怎么样? 像这样:
class MyComponent extends React.Component<MyComponentProps, void>{
implements.componentWillMount(){
//do my stuff
}
}
这种语法有一些优点:
implements.
之后,IDE 有一个很好的机会来显示一个自动完成弹出窗口。class MyComponent<MyComponentProps, MyComponentState> {
implements.state = {/*auto-completion for MyComponentState here*/};
implements.componentWillMount(){
//do my stuff
}
}
注意:或者我们可以使用base.
,它的排序更直观,但可能更令人困惑(定义或调用?)并且含义与接口实现不太兼容。 例子:
class MyComponent<MyComponentProps, MyComponentState> {
base.state = {/*auto-completion for MyComponentState here*/};
base.componentWillMount(){ //DEFINING
//do my stuff
base.componentWillMount(); //CALLING
//do other stuff
}
}
我认为implements
不足以涵盖所有用例
前面提到过,有点模棱两可。 它不再给予
向 IDE 提供override
也无法获取的信息,因此它似乎
坚持使用许多其他语言习惯的术语是有意义的
达到同样的目的。
2016 年 4 月 13 日星期三 19:06,Olmo [email protected]写道:
使用工具怎么样。? 像这样:
MyComponent 类扩展了 React.Component
{
实现.componentWillMount(){
//做我的事
}
}这种语法有一些优点:
- 它使用已经存在的关键字。
- 写完之后。 IDE 有一个很好的展示机会
一种自动完成方法。- 关键字阐明可用于强制检查摘要
基类的方法,但也实现了接口。- 语法模棱两可,也可用于字段,例如
这:类 MyComponent
{
implements.state = {/_此处为 MyComponentState 自动完成_/};implements.componentWillMount(){ //do my stuff }
}
—
你收到这个是因为你被提到了。
直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/2000#issuecomment -209571753
override 的问题是,一旦你使用相同的关键字,你就会有相同的期望:
abstract
。virtual
关键字来注释那些打算被覆盖但具有默认行为的方法。我认为 TS 团队不想给语言添加这么多 OO 包袱,我认为这是个好主意。
通过使用implements.
,您可以使用轻量级语法来获得主要好处:自动完成和编译时检查名称,无需发明新关键字或增加概念计数。
还具有为类和接口以及方法(在原型中)或直接字段工作的好处。
使override
成为强制性的方法已经在线程中讨论过,并且解决方案与编译器实现的其他功能没有什么不同。
virtual
关键字在语言的上下文中并没有真正意义,而且对于以前没有使用过 C++ 等语言的人来说,它的命名方式也不是很直观。 如果需要,提供这种类型的保护的更好解决方案可能是final
关键字。
我同意不应该匆忙添加可能造成“包袱”的语言特性,但是override
在继承系统中插入了许多其他语言认为必要的合法漏洞。 它的功能最好用一个新的关键字来实现,当然当替代方法是建议基本的语法更改时。
设置继承字段与重新定义新字段怎么样? React state
就是一个很好的例子。
或者实现可选的接口方法?
如果重新声明它们,则可能会在字段上使用覆盖
在子类体内。 可选的接口方法不是覆盖所以是
超出了我们在这里讨论的范围。
2016 年 4 月 14 日星期四 11:58,Olmo [email protected]写道:
设置继承字段与重新定义新字段怎么样? 反应状态
是一个很好的例子。或者实现可选的接口方法?
—
你收到这个是因为你被提到了。
直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/2000#issuecomment -209879217
可选的接口方法不是覆盖,因此超出了我们在这里讨论的范围。
据我所知,可选接口方法对于 TypeScript 来说是一个相当独特的概念,我认为 TypeScript 的“覆盖”实现应该适用于它们。
对于像 componentDidMount 这样的 React 生命周期方法,这些是可选的接口方法,在超类中没有实现。
对于像 componentDidMount 这样的 React 生命周期方法,这些是可选的接口方法,在超类中没有实现。
没错,所以他们不会覆盖任何东西。 我认为我们对override
关键字在此处提供的内容感到困惑,因为如果您只是在寻找一种获得更好的代码提示/智能感知的方法,则无需添加即可实现其他方法语言的新关键字。
伙计们,恕我直言,我们能否保持专注。 我认为讨论替代关键字会适得其反,特别是因为问题是从特定请求开始的。
我们是否都同意我们需要override
并很好地要求 Typescript 团队添加它或放弃请求并让问题关闭?
我认为在问题所在的背景下提出请求总是有用的,总是有不同的方法来解决问题。 override的症结在于它是否是强制性的(有些人指出它在 C++ 中不是强制性的)。 许多人提到的问题是为可覆盖函数(可能会或可能不会在超类中实现)命名正确 - React 的生命周期函数是主要问题。
如果覆盖不起作用,那么也许我们应该关闭这个问题并打开一个关于这个问题的更一般的观点。 这里的一般问题是与超类的接口契约是无类型且未经检查的,这让开发人员感到不安,如果 TS 或工具可以提供帮助,那就太好了。
@armandn我不认为我们缺乏封闭性或我们的建议的好处是导致请求被拒绝的原因,而是 C# 和 TS 语义之间的差异:
C# 基方法覆盖:
override
关键字C#接口实现:
因此,C# 中的行为完全不同,具体取决于您询问类或接口,但无论如何您都会获得主要的三个好处:
由于语义稍有不同,我们在 TS 中已经有了 1),不幸的是,缺少 2) 和 3)。 在我看来, override
被拒绝的原因是类似的语法假定了类似的行为,这是不可取的,因为:
具有 5 个方法的类,其中 3 个被标记为覆盖,并不意味着其他 2 个不是覆盖。
碰巧我们在编写_object literals_时已经有1)2)和3),但在编写扩展其他类或实现接口的类成员时却没有。
考虑到这一点,我认为我们都可以就这个语义达成一致:
check
或super.
或MyInterface.
)abstract/virtual
但检查基类/实现的接口中是否存在成员。 (好处二)此外,我认为这两个对于解决方案的完整性是必要的*:
这两点在 React 组件实现的非常真实的用例中很有用:
``` C#
类 MyComponent
implements.state = {/此处为 MyComponentState 自动完成/};
implements.componentWillMount(){
//do my stuff
}
}
```
@RyanCavanaugh的基于注释的解决方案还不够,因为:
@override
后编写自动完成列表后,无法添加 QuickFix。鉴于语义,它只是选择正确的语法。 这里有一些替代方案:
override componentWillMount()
:直观但具有误导性。check componentWillMount()
:显式但消耗关键字。super componentWillMount()
:implements componentWillMount()
:super.componentWillMount()
:implements.componentWillMount()
:this.componentWillMount()
:ReactComponent.componentWillMount()
:意见?
@olmobrutall很好的总结。 几点:
一个关键字选项(取自 C#)将是新的 - 表示一个新插槽:
class MyComp extends React.Component<IProps,IState> {
...
new componentWillMount() { ... }
componentWillMount() { ...} // would compile, maybe unless strict mode is enabled
new componentwillmount() { ... } <-- error
我的另一点是使用上述内容的强制性问题。 作为父超类和派生类之间的契约,指定上述语法有效的接口点是有意义的。 这些实际上是超类的内部扩展点,例如:
class Component<P,S> {
extendable componentWillMount() {...}
}
这同样适用于接口。
谢谢 :)
只写this
怎么样?
class MyComponent<MyComponentProps, MyComponentState> {
this.state = {/*auto-completion for MyComponentState here*/};
this.componentWillMount(){
//do my stuff
}
}
关于extendable
,TS 1.6 中已经有abstract
了,添加 virtual 可能会再次造成世界分裂问题?
是的,我想到了抽象,但超类中可能有一个实现,所以它真的没有意义。 与virtual类似,因为这意味着非虚拟成员不是虚拟的 - 这是误导性的。
this.
有效,我想你甚至可以拥有(作为一个长形式):
this.componentWillMount = () => { }
唯一的问题是它应该只限于指定的扩展点,而不是基类的所有成员。
到底是怎么回事...
TypeScript 不是,也从来不是 C# 的 javascript 版本。 所以原因不是因为建议的功能与 C# 的语义不同。 Ryan 陈述并随后由 Daniel R 澄清的关闭原因是
正如 Ryan 解释的那样,问题在于将方法标记为覆盖并不意味着另一个方法不是覆盖。 唯一有意义的方法是所有覆盖都需要用覆盖关键字标记(如果我们强制要求,这将是一个重大更改)。
然而,您仍然坚持围绕自动完成的问题。 自动完成不需要语言中的新关键字来为您提供改进的建议。
这个线程存在的原因是当一个函数被非法覆盖或者当一个方法被声明为一个覆盖但实际上并没有覆盖基类中的一个函数时,会出现编译器错误。 这是一个跨多种语言的简单且定义明确的概念,它也支持打字稿必须提供的大多数(如果不是更多)语言功能。 它不需要解决所有的世界问题,它只需要作为 override 关键字,用于覆盖。
从那以后,更多的人对最初的提案表现出兴趣,所以让我们坚持提出问题的主题,并为新想法提出新问题。
TypeScript 不是,也从来不是 C# 的 javascript 版本。
我将它与 C# 作为参考语言进行比较,因为我认为这是你们假设的行为。
自动完成不需要语言中的新关键字来为您提供改进的建议。
你建议如何触发它? 当我们只想声明一个新字段或方法时,如果在类上下文中写入随机名称时显示自动完成组合框将非常烦人。
这个线程存在的原因是当一个函数被非法覆盖或者当一个方法被声明为一个覆盖但实际上并没有覆盖基类中的一个函数时,会出现编译器错误。
我绝对在Benefit 2下包含了这个用例。
它不需要解决所有的世界问题,它只需要作为 override 关键字,用于覆盖。
所以你的建议是_一次修复一步_而不是退后一步,看看类似/相关的问题? 这对你的 Scrum Board 来说可能是个好主意,但对设计语言来说却不是。
他们对添加关键字持保守态度的原因是他们无法从语言中删除功能。
由于缺乏完成,C#中的一些设计错误:
var
仅适用于变量类型,不适用于自动泛型参数或返回类型。 Mayble auto
将成为像 C++ 中更好的关键字?只需尝试使用 React 片刻,您就会看到图片的另一面。
覆盖已经在基类中实现的方法和实现接口方法是_两件完全不同的事情_。 所以,是的,建议使用专用关键字修复其中一种情况,而不是尝试提出一些 Swiss-Army 关键字。
对于第一次阅读某些代码的开发人员来说,一个可以表示 3 种不同事物中的任何一种的关键字有什么好处? 这是模棱两可和令人困惑的,特别是如果您正在谈论使用像this
这样的关键字,它已经在语言中做了其他(完全不相关!)的事情 - 它不能更通用,几乎没用。
如果您主要关心的是自动完成,那么编辑器_现在_有足够的信息可以“在您键入时”从基类和实现的接口中建议方法。
覆盖已经在基类中实现的方法和实现接口方法是完全不同的两件事。
在一般情况下是的,但我们不是在谈论实现任何接口方法。 我们正在讨论一个可选的接口方法_其中父类实现了接口_。 在这种情况下,您可以说 1) 接口允许将方法实现为undefined
,2) 父类具有未定义的实现,以及 3) 子类使用方法实现覆盖未定义的实现。
@olmobrutall我认为您对设计语言以及它如何不是 Scrum 板的评论有点自私。 在不到一年的时间里,我已经看到了 TS 的四次更新。
如果语言设计如您所暗示的那样得到充分考虑,那么已经有一个语言规范文档告诉我们覆盖应该如何工作,我们甚至可能不会进行这个对话。
我不会对 TS 开发者/设计者做出任何贬低的评论,因为 TS 已经非常出色了,我害怕不得不使用标准的 JS。
是的,TS 不是 C#,也不是 C++。 但是许多语言选择了 override 关键字来满足这里讨论的目标,因此建议完全陌生的语法似乎适得其反。
主要问题似乎是不想引入重大变化。 简单的答案是编译器标志,故事结束。 对于像我这样的人来说,可选的 override 关键字是没有用的。 对于其他人,他们希望逐步修饰他们的代码。 编译器标志解决了这个难题。
签名差异是不同的对话。 new 关键字似乎是不必要的,因为 JS 不能支持同名的多个方法(除非 TS 创建签名派生的重整名称 a'la C++,这是极不可能的)。
在不到一年的时间里,我已经看到了 TS 的四次更新。
我并不是说你不能快速迭代。 我和任何人一样高兴 ES6 和 TS 正在快速发展。 我的意思是你必须尝试预测未来,以避免将语言置于死胡同。
我可以同意使用override
关键字。 使用适当的参数甚至可以将字段和接口保持在范围之外,但我不能同意“_让我们保持专注并像其他语言那样解决这个特定问题而无需考虑太多_”的论点。
但是许多语言选择了 override 关键字来满足这里讨论的目标,因此建议完全陌生的语法似乎适得其反。
这些语言都没有原型继承或可选方法(既不是抽象的也不是虚拟的方法,它们只是在运行时_不存在_),这是在做出承诺之前必须讨论(并且可能被丢弃)的相关问题。
换句话说:假设我们按照您的建议进行操作,并且我们在没有考虑太多的情况下实施了覆盖。 然后我,或者其他任何使用 TSX 的人,添加了一个问题,为什么override
不适用于 React 组件。 你的计划是什么?
在一般情况下是的,但我们不是在谈论实现任何接口方法。 我们正在讨论父类实现接口的可选接口方法。
接口在哪里设置无关紧要,事实是它们不是同一个东西,因此不应该共享关键字,因为程序的_intent_不清楚。
例如,您可以覆盖已在基类中实现接口合规性的方法; 如果我们将所有的鸡蛋都放在一个关键字中来处理这两种不同的事情,那么任何人怎么会知道这是该函数的初始声明还是对先前在基类中定义的函数的覆盖? 你不会,而且如果不进一步检查基类就不可能知道——它甚至可能在第 3 方.d.ts
文件中,因此它绝对是一场噩梦,具体取决于多深在继承链中,函数最初是被实现的。
换句话说:假设我们按照您的建议进行操作,并且我们在没有考虑太多的情况下实施了覆盖。 然后我,或者其他使用 TSX 的人,添加了一个问题,即为什么 override 不适用于 React 组件。 你的计划是什么?
为什么这需要修复 React? 如果 React 的问题与它试图解决的问题不同,那么我一生都无法理解为什么override
需要修复它? 您是否尝试过打开另一个问题来建议对接口实现做些什么?
我不同意没有对此进行足够的思考。 我们建议采用一种久经考验的技术,该技术在我能想到的所有其他语言中都取得了成功,并实现了它。
事实是它们不是一回事,因此不应共享关键字,因为程序的意图尚不清楚。
不是? 看看这两个BaseClass
的替代定义
class BaseClass {
abstract myMethod();
}
interface ISomeInterface {
myMethod?();
}
class BaseClass extends ISomeInterface {
}
然后在您的代码中执行以下操作:
``` C#
类具体类{
覆盖我的方法(){
// 做东西
}
}
You think it should work in just one case and not in the other? The effect is going to be 100% identical in Javascript (creating a new method in ConcreteClass prototype), from the external interface and from the tooling perspective.
Even more, maybe you want to capture `this` inside of the method, implementing it with a lambda (useful for React event handling). In this case you'll write something like this:
``` C#
class ConcreteClass {
override myMethod = () => {
// Do stuff
}
}
如果方法是抽象的或来自接口,则行为将再次相同:在类中添加一个字段,并使用 lambda 实现它。 但是override
一个字段看起来有点奇怪,因为您只是在分配一个值。
不要让我们使用super.
(我现在最喜欢的语法,但我对替代方案持开放态度)来看待它。
``` C#
类具体类{
super.myMethod() { //原型中的方法
// 做东西
}
super.myMethod = () => { //method in lambda
// Do stuff
}
}
```
现在这个概念在概念上更简单了:我的超类说有一个方法或字段,并且 ConcreteClass 可以在原型中定义它/分配它/读取它/调用它。
为什么这需要修复 React?
不只是反应,看角度:
当然,大多数接口都不是要实现的,也不是所有要覆盖的类,但有一点很清楚:在 Typescript 中,接口比类更重要。
您是否尝试过打开另一个问题来建议对接口实现做些什么?
我该怎么称呼它? override
用于接口和字段?
我们建议采用一种久经考验的技术,该技术在我能想到的所有其他语言中都取得了成功。
你心目中的语言是完全不同的。 它们具有基于静态 VTable 的继承。
在 Typescript 中,一个类只是一个接口 + 方法的自动原型继承。 方法只是内部带有函数的字段。
为了使override
功能适应 TS,必须考虑这两个基本差异。
请@kungfusheep努力考虑解决我的问题。 如果您想添加不同的关键字并在第二阶段实施它们是可以的,但请花点时间想象一下它应该是怎样的。
我想不出另一种方式来表达我已经说过的话。 她们不一样。 它们相似,但不一样。 请参阅其中一位 TS 开发人员 RE 的评论:只读关键字 - https://github.com/Microsoft/TypeScript/pull/6532#issuecomment -179563753 - 这加强了我的意思。
我同意一般的想法,但让我们在实践中看看:
class MyComponent extends React.Component<{ prop : number }, { value: string; }> {
//assign a field defined in the base class without re-defining it (you want type-checking)
assign state = { value : number};
//optional method defined in an interface implemented by the base class
implement componentDidMount(){
}
//abstract method defined in the base class
override render(){
}
}
这看起来像 VB 或 Cobol,不是吗?
至少它看起来是有道理的。
考虑这个例子,是否只有 override(或只有一个)关键字。
interface IDo {
do?() : void;
}
class Component implements IDo {
protected commitState() : void {
/// do something
}
override public do() : void {
/// base implements 'do' in this case
}
}
现在让我们使用我们刚刚编写的内容来实现我们自己的组件。
class MyComponent extends Component {
override protected commitState(){
/// do our own thing here
super.commitState();
}
override do() : void {
/// this is ambiguous. Am I implementing this from an interface or overriding a base method? I have no way of knowing.
}
}
一种了解方法是super
的类型:
override do() : void {
super.do(); // this compiles, if it was an interface then super wouldn't support `do`
}
确切地! 这意味着设计是错误的。 不应该对略读代码进行调查,当你阅读它时它应该是有意义的。
这是模棱两可的。 我是从接口实现它还是重写基本方法? 我没有办法知道。
实践上有什么不同? 该对象只有一个名称空间,您只能在do
字段中放置一个 lambda。 无论如何,没有办法显式地实现接口。 实施将是:
MyComponent.prototype.do = function (){
//your stuff
}
独立于你写的东西。
输出是什么并不重要。 您可能有意或无意地覆盖基类中的某些功能,但在关键字中没有任何意图是模棱两可的。
有两个关键字可以解决什么错误或意外行为?
现在来吧伙计。 你显然是个聪明人。 我刚刚说过“无意中覆盖了基类中的某些功能”; 我们不能从中推断出任何可能刚刚发生的意外行为吗?
需要明确的是,我不建议将这个问题变成针对两个关键字的提案。 这个问题是针对 override 关键字的——其他任何事情都需要一个新的提议和它自己关于语义的讨论。
需要明确的是,我不建议将这个问题变成针对两个关键字的提案。 这个问题是针对 override 关键字的——其他任何事情都需要一个新的提议和它自己关于语义的讨论。
它应该讨论多少个问题,或者这个想法来自谁并不重要。 您建议拆分两个非常相关的想法,甚至不考虑一致的语法。
我们是否需要 1、2 或 3 个关键字的论点属于该线程并且尚未完成(......但变得重复)。 然后也许我们可以在另一个线程中讨论语法(因为无论如何语义都是相同的:P)。
在我的例子中:
class MyComponent extends React.Component<{ prop : number }, { value: string; }> {
//assign a field defined in the base class without re-defining it (you want type-checking)
assign state = { value : number};
//optional method defined in an interface implemented by the base class
implement componentDidMount(){
}
//abstract method defined in the base class
override render(){
}
}
不要assign
, implement
和override
做完全相同的事情:检查名称是否存在(在基类中,实现的接口,由基类实现的接口类等...)。
如果基类和某些实现的接口之间存在名称冲突,无论是使用 1、2 还是根本没有关键字,都会出现编译时错误。
还要考虑对象文字:
var mc = new MyComponent();
mc.state = null;
mc.componentDidMount =null;
mc.render = null;
使用完全相同的语法,我可以独立地重新分配来自基类、直接接口实现或在基类中实现的接口的字段或方法。
不要分配,实现和覆盖做完全相同的事情:检查名称是否存在(在基类中,实现的接口,基类实现的接口等......)。
您刚刚在那里描述了 3 种不同的场景,因此显然它们并不相同。 我有一种感觉,我可以描述为什么他们整天都与你不同,而你仍然会争论他们不是,所以我现在要退出这个特定的讨论路线。 无论如何,TS家伙目前仍在考虑这一点,没什么可说的。
随着#6118 的关闭,我认为有理由讨论那里的问题和这里的问题是否可以同时解决。
我不知道https://github.com/Microsoft/TypeScript/pull/6118。 这个想法看起来像是添加override
的可能替代方案。
如果我正确理解了这个问题,那么问题是您可以拥有多个具有兼容但不相同的成员声明的基类/接口,并且在没有类型的子类中初始化它们时必须统一它们。
如果不知道后果,我会很高兴:
更重要的 IMO 是在编写类成员(Ctrl+Space)时有某种触发自动完成的方法。 当光标直接位于类内部时,您可以定义新成员或重新定义继承的成员,因此自动完成不能太激进,但手动触发行为应该没问题。
关于@RyanCavanaugh评论:
我们绝对了解这里的用例。 问题是在语言的这个阶段添加它会增加比它消除的更多的混乱。 具有 5 个方法的类,其中 3 个被标记为覆盖,并不意味着其他 2 个不是覆盖。 为了证明它的存在,修改器确实需要比这更干净地划分世界。
将变量键入为any
并不意味着其他变量也是或不是any
。 但是,有一个编译器平面--no-implicit-any
来强制我们显式声明它。 我们同样可以有一个--no-implicit-override
,如果它可用,我会打开它。
使用override
关键字可以让开发人员在阅读他们不熟悉的代码时获得大量洞察力,并且执行它的能力将提供一些额外的编译时间控制。
对于所有 +1 人——您能谈谈上面显示的装饰器解决方案还有哪些不足之处吗?
有什么方法可以让装饰器比覆盖关键字更好的解决方案? 导致它更糟的原因有很多:1)它增加了运行时开销,无论多么小; 2) 这不是编译时检查; 3)我必须将此代码包含到我的每个库中; 4) this 无法捕获缺少 override 关键字的函数。
让我们举个例子。 我有一个包含三个类的库ChildA
, ChildB
, Base
。 我在ChildA
和ChildB
中添加了一些方法doSomething()
#$ 。 经过一些重构后,我添加了一些额外的逻辑,进行了优化,并将doSomething()
移至Base
类。 同时,我有另一个依赖于我的库的项目。 我有ChildC
和doSomething()
。 当我更新我的依赖项以发现ChildC
现在隐式覆盖doSomething()
时,没有办法,但是以未优化的方式也缺少一些检查。 这就是为什么@overrides
装饰器永远不够用的原因。
我们需要的是一个override
关键字和一个--no-implicit-override
编译器标志。
override
关键字对我有很大帮助,因为我在项目中使用了一个简单的基类层次结构来创建我的所有组件。 我的问题在于这些组件可能需要声明要在其他地方使用的方法,并且该方法可能会或可能不会在父类中定义,并且可能已经或可能不会做我需要的东西。
例如,假设一个函数validate
接受一个带有getValue()
方法的类作为参数。 为了构建这个类,我可以继承另一个已经定义了这个getValue()
方法的类,但是除非我查看它的源代码,否则我无法真正知道这一点。 我本能地做的是在我自己的类中实现这个方法,只要签名是正确的,没有人会告诉我任何事情。
但也许我不应该那样做。 以下可能性都假设我做了一个隐式覆盖:
super
,但没有人建议我这样做。有一个强制覆盖关键字会告诉我“嘿,你正在覆盖,所以也许你应该在做你的事情之前检查原始方法的样子”。 这将大大改善我的继承经验。
当然,正如建议的那样,它应该放在--no-implicit-override
标志下,因为这将是一个重大变化,而且大多数人并不关心这一切。
我喜欢与any
和--no-implicit-any
进行比较的@eggers ,因为它是相同类型的注释,并且会以完全相同的方式工作。
@olmobrutall我在看你关于重写抽象方法和接口方法的一些讨论。
如果我认为override
意味着存在super.
接口中定义的抽象方法和方法都不能通过super.
调用调用,因此不应被覆盖(没有什么可以覆盖的)。 相反,如果我们为这些情况做一些更明确的事情,它应该是一个implements
关键字。 但是,这将是一个单独的功能讨论。
以下是我看到的问题,从最重要到最不重要。 我错过了什么吗?
当你没有覆盖基类方法时,很容易_认为_覆盖:
class Base {
hasFilename(f: string) { return true; }
}
class Derived extends Base {
// oops
hasFileName(f: string) { return false; }
}
这可能是最大的问题。
这是非常密切相关的,尤其是当实现的接口具有可选属性时:
interface NeatMethods {
hasFilename?(f: string): boolean;
}
class Mine implements NeatMethods {
// oops
hasFileName(f: string) { return false; }
}
这是一个不如_failure to override_ 重要的问题,但它仍然很糟糕。
可以在没有意识到的情况下覆盖基类方法
class Base {
hasFilename(f: string) { return true; }
}
class Derived extends Base {
// I didn't know there was a base method with this name, so oops?
hasFilename(f: string) { return true; }
}
这应该是比较少见的,因为可能的方法名称的空间非常大,而且你也写一个兼容的签名_并且_并不意味着一开始就这样做的可能性很低。
any
s很容易认为覆盖方法将获取其基类的参数类型:
class Base {
hasFilename(f: string) { return true; }
}
class Derived extends Base {
// oops
hasFilename(f) { return f.lentgh > 0; }
}
我们尝试自动键入这些参数,但遇到了问题。 如果hasFilename
被明确标记override
,我们也许能够更轻松地解决这些问题。
一个方法何时覆盖基类方法以及何时不是,目前尚不清楚。 这似乎是一个较小的问题,因为今天有合理的解决方法(评论、装饰器)。
在编写派生类覆盖方法时,您必须手动输入基方法名称,这很烦人。 我们可以通过始终在完成列表中提供这些名称来轻松解决这个问题(我认为我们实际上已经有一个错误),所以我们真的不需要语言功能来解决这个问题。
将这些列为属于单独问题或根本不会发生的事情:
override
混在一起,而且在很多好的场景中它确实有不受欢迎的语义implements.foo = ...
)。 不需要在这里骑自行车@RyanCavanaugh我也同意以上关于该顺序的所有内容。 就像评论一样,我认为override
关键字不应该解决“实施失败”的问题。 正如我上面所说,我认为这个问题应该通过不同的关键字(例如implements
)作为不同票证的一部分来解决。 ( override
应该暗示一个super.
方法)
@RyanCavanaugh我同意所有观点,但我没有看到任何对在父类型中声明的初始化字段也非常相关的问题的任何引用,而没有覆盖类型(然后失去类型检查)。 我认为该功能不应仅限于方法只是对象字段中的函数的语言中的方法声明。
@eggers即使最后需要两个关键字,语义也会非常相似,我认为这些功能应该一起讨论。
覆盖应该意味着一个超级。 方法
在 C# 中,覆盖也可以(并且必须)用于抽象方法(没有 .super)。 在 Java 中, @override是一个属性。 当然,它们是不同的语言,但具有相似的关键字,您期望相似的行为。
我同意@RyanCavanaugh的观点,尽管我会说“意外覆盖”问题可能比他认为的更常见(尤其是在处理扩展已经扩展了其他东西的类时,比如已经扩展的 React 组件在第一个扩展中定义一个componentWillMount
方法)。
不过,我相信,就像@eggers所说的那样,“实现失败”的问题是完全不同的,并且将override
关键字用于父类中不存在的方法会感觉很奇怪。 我知道这些是具有不同问题的不同语言,但在 C# 中override
不用于接口。 我建议,如果我们可以获得--no-implicit-override
标志,那么接口方法必须以接口名称为前缀(这看起来很像 C#,因此感觉更自然)。
就像是
interface IBase {
method1?(): void
}
class Base {
method2() { return true; }
}
class Test extends Base implements IBase {
IBase.method1() { }
override method2() { return true; }
}
我认为@RyanCavanaugh列出了基本问题。 我会用“有什么分歧”来补充这一点:
override
的接口? 例如: interface IDrawable
{
draw?( centerPoint: Point ): void;
}
class Square implements IDrawable
{
draw( centerPoint: Point ): void; // is this an override?
}
override
的接口? interface IPoint2
{
x?: number;
y?: number;
}
class Circle implements IPoint2
{
x: number; // is this an override?
y: number; // is this an override?
radius: number;
}
implements
关键字来处理上述情况,而不是override
关键字,以及它将采用的形式。@kevmeister68感谢您明确指出分歧。
一件事:您必须使接口成员可选才能真正显示问题,这样打字稿编译器会在未实现字段或方法时抱怨,解决“无法实现”。
@olmobrutall谢谢你。 我从来没有声明一个方法是可选的——可以吗(我来自 C# 背景,没有这样的概念)? 我已经更新了上面列出的示例,将成员变量更改为可选 - 更好吗?
@kevmeister68也是IDrawable
中的draw
方法 :)
@RyanCavanaugh
不够奇特的语法(例如 implements.foo = ...)。 不需要在这里骑自行车
谢谢你教我一个新的表达方式。 我在功能的语义上没有看到很多问题:
大多数问题都依赖于它们所暗示的语法和行为:
让我们比较一下树更有希望的语法:
class Person{
dateOfBirth: Date;
abstract talk();
walk(){ //...}
}
interface ICanFly{
fly?();
altitude?: number;
}
class SuperMan extends Person implements ICanFly {
dateOfBirth = new Date(); //what goes here?
override talk(){/*...*/}
walk = () => {/* force 'this' to be captured*/} //what goes here
implements fly() {/*...*/}
altitude = 1000; //what goes here?
}
优点:
extends
/ implements
比较时感觉不错。缺点:
override
或implements
感觉正确。 也许是super.
,但是我们用接口做什么呢?function()
的 lambda(用于捕获此问题)覆盖方法,则会发生相同的问题。InterfaceName.
class Person{
dateOfBirth: Date;
abstract talk();
walk(){ //...}
}
interface ICanFly{
fly?();
altitude?: number;
}
class SuperMan extends Person implements ICanFly {
dateOfBirth = new Date(); //what goes here?
override talk(){/*...*/}
walk = () => {/* force 'this' to be captured*/} //what goes here
ICanFly.fly() {/*...*/}
ICanFly.altitude = 1000; //what goes here?
}
优点:
super.
。缺点:
this.
class Person{
dateOfBirth: Date;
abstract talk();
walk(){ //...}
}
interface ICanFly{
fly?();
altitude?: number;
}
class SuperMan extends Person implements ICanFly {
this.dateOfBirth = new Date();
this.talk(){/*...*/}
this.walk = () => {/* force 'this' to be captured*/}
this.fly() {/*...*/}
this.altitude = 1000;
}
优点:
this.
触发自动完成感觉很自然。缺点:
虽然未能实现非可选接口方法将导致编译器错误,但如果接口在未来某个时间点发生更改(假设某个方法被删除),则不会对实现该方法的所有类发出警告。 这可能没有可选的问题那么大,但我认为可以公平地说这超出了它们的范围。
但是,我仍然相信override
是完全不同的东西,并且可能有两个关键字而不是一个关键字会更好地传达程序的意图。
例如,考虑一个场景,开发人员_相信_他们正在实现接口方法,而实际上他们正在覆盖已经在基类中声明的方法。 使用两个关键字,编译器可以防止这种错误,并以method already implemented in a base class
的语气抛出错误。
interface IDelegate {
execute?() : void;
}
class Base implements IDelegate {
implement public execute() : void { /// fine, this is correctly implementing execute
}
}
class Derived extends Base {
implement public execute() : void {
/// ERROR: `method "execute():void" already implemented in a base class`
}
}
我不知道这在 JS 中是否相关,但类字段在 OO 中应该是私有的,所以我认为我们不需要显式覆盖字段,因为它们是私有的这一事实已经阻止我们完全覆盖.
实例方法虽然是字段,但据我所知,很多人使用它们而不是原型方法。 关于那个,
class Person{
walk(){ //...}
}
class SuperMan extends Person {
walk = () => {/* force 'this' to be captured*/}
}
是编译器错误,因为walk
是Person
SuperMan
的实例方法。
无论如何,我不确定override
是否适合这里,因为我们不会覆盖字段。 同样,它看起来像 C#,但我宁愿在这里使用new
关键字而不是override
。 因为它与方法覆盖不同(我可以在覆盖中调用super.myMethod
,而不是在这里)。
我的首选解决方案将类似于(假设我们处于严格覆盖模式):
class Person{
dateOfBirth: Date;
talk() { }
walk = () => { }
}
interface ICanFly {
fly?();
altitude?: number;
}
class SuperMan extends Person implements ICanFly {
new dateOfBirth = new Date();
override talk() { }
new walk = () => { }
implements fly() {/*...*/}
implements altitude = 1000;
}
我主要关心的是接口。 我们不应该为非可选接口成员编写implements
,因为我们已经被编译器捕获了。 然后,如果我们这样做,就会混淆来自接口的内容和不来自接口的内容,因为并非所有接口成员都会以implements
为前缀。 而这个词是拗口的。 我还没准备好写这样的东西:
class C extends React.Component {
implements componentWillMount() { }
implements componentDidMount() { }
implements componentWillReceiveProps(props) { }
/// ... and the list goes on
}
我之前建议使用接口名称作为前缀,但@olmobrutall告诉我这是一个更糟糕的主意。
无论如何,我非常确信我们需要 3 个不同的新关键字来正确解决这个问题。
我也不认为有必要隐式键入覆盖,因为编译器已经阻止我们编写不兼容的东西,特别是因为它显然很难做到正确。
对于本质上相同的 JS 语义,$ new
、 override
和implement
关键字开销是不是太大了? 即使在 C# 中它们是不同的东西,在 JS/TS 中也没有任何区别。
它也很模棱两可:
new
implements
?override
, implements
,两者中的任何一个或同时两者?也想想这个:
class Animal {
}
class Human extends Animal {
}
class Habitat {
owner: Animal;
}
class House extends Habitat {
owner = new Human();
}
var house = new House();
house.owner = new Dog(); //Should this be allowed??
问题是:
owner = new Human();
是否使用新的(但兼容的)类型重新定义字段,或者只是分配一个值。
我认为通常您重新定义了该字段,除非您使用 _magic 关键字_。 我现在的偏好是this.
。
class House extends Habitat {
this.owner = new Human(); //just setting a value, the type is still Animal
}
@olmobrutall这可能确实有点多的关键字开销,但所有 3 确实是_不同_的东西。
override
将应用于方法(在原型上定义),这意味着它不会_不_擦除原始方法,该方法仍然可以在super
后面访问。new
将应用于字段,这意味着原始字段已被新字段删除。implements
将适用于接口中的任何内容,这意味着它不会删除或替换父类中已经存在的任何内容,因为接口“不存在”。如果您要更改使用接口实现的基类的方法,您应该使用什么? 我可以看到四种可能性: override , implements ,两者中的任何一个或同时两者?
在我看来,这将是override
似乎很合乎逻辑,因为你在这里有效地做了什么。 implements
意味着您正在从接口实现一些其他方式不存在的东西。 从某种意义上说, override
和new
将优先于implements
。
关于您的字段覆盖示例,它今天的工作方式不应改变。 我不确定这里会发生什么,但无论如何它都不应该改变(或者也许应该改变,但这不是我们在这里讨论的内容)。
我觉得override
将适用于基本方法和属性,包括抽象方法(如下所述)。
new
在概念上是一个有趣的想法,但是这个特定的关键字可能会与类实例化混淆,因此它可能给人一种错误的印象,它只适用于类,尽管属性可以有原始、接口、联合甚至函数类型。 也许像reassign
这样的不同关键字在那里可以更好地工作,但它的问题是它可能会给出一个错误的想法,如果值只是重新声明但实际上没有分配派生类中的任何东西( new
也可能有这个问题)。 我认为redefine
也很有趣,但可能导致错误地认为“重新定义”属性也可能具有与基本属性不同的类型,所以我不确定..(_edit:实际上我检查了只要新的类型是基本类型的子类型,它就可以有不同的类型,所以这可能不是那么糟糕_)。
implement
(为了与override
保持一致,我更喜欢这种特殊的动词形式)似乎适用于接口。 我相信它在技术上也适用于抽象基础方法,但使用override
会感觉更一致,尽管语义略有不同。 另一个原因是将方法从抽象更改为非抽象会有点不方便,因为需要转到所有派生类并将override
更改为implement
。
可能有更好的想法,但这就是我目前所拥有的一切......
我提出关键字new
是因为它是 C# 用于这一确切功能的关键字,但我同意这不是最好的关键字。 implement
确实是比implements
更好的选择。 我认为我们不应该将它用于抽象方法,因为它们是基类的一部分而不是接口。 override
+ new
-> 基类, implement
-> 接口。 这似乎更清楚了。
@JabX
我已经检查过,你是对的:
class Person{
walk(){ //...}
}
class SuperMan extends Person {
walk = () => {/* force 'this' to be captured*/}
}
编译器错误失败: Class 'Person' defines instance member function 'walk', but extended class 'SuperMan' defines it as instance member property.
在这种情况下,我真的看不出失败的原因,因为父类型合同已经履行,你可以这样写:
var p = new SuperMan();
p.walk = () => { };
甚至
class SuperMan extends Person {
constructor() {
super();
this.walk = () => { };
}
}
override
override 将应用于方法(在原型上定义),这意味着它不会删除原始方法,该方法仍然可以在 super 后面访问。
这实际上是一个论据。 override
和implements
...
override
和implements
都在prototype
中实现,能够使用super
是独立的,因为您调用super.walk()
而不仅仅是super()
,并且在每个方法(覆盖、实现、新闻和普通定义)中都可用。
class SuperMan extends Person implements ICanFly {
new dateOfBirth = new Date();
override talk() { } //goes to prototype
new walk = () => { }
implements fly() {/*...*/} //also goes to the prototype
implements altitude = 1000;
}
如果您分配给实例,并且能够使用super.walk()
也可以使用
class SuperMan extends Person {
constructor() {
super();
this.walk = () => { super.walk(); };
}
//or with the this. syntax
this.walk = () => { super.walk(); };
}
已经有一个明确的方法来区分prototype
字段和实例字段,并且是=
令牌。
class SuperMan extends Person implements ICanFly {
this.dateOfBirth = new Date(); //instance
this.talk() { } //prototype
this.walk = () => { } //instance
this.fly() {/*...*/} //prototype
this.altitude = 1000; //instance
}
使用this.
语法可以更优雅地解决问题:
this.
表示检查我的类型(基类和实现的接口)。=
表示分配给实例而不是原型。New
new
将应用于字段,这意味着原始字段已被新字段删除。
考虑到你不能用不同的类型改变字段或方法,因为你会破坏类型系统。 在 C# 或 Java 中,当您执行new
时,新方法将仅用于对新类型的静态分派调用。 在 Javascript 中,所有调用都是动态的,如果您更改类型,代码可能会在运行时中断(但是,出于实际目的,可以允许强制类型,但不能扩大)。
class Person {
walk() { }
run() {
this.walk();
this.walk();
this.walk();
}
}
class SuperMan extends Person {
new walk(destination: string) { } //Even if you write `new` this code will break
}
所以超过new
关键字应该是assign
,因为这是你可以做的唯一类型安全的事情:
class Person{
dateOfBirth: Date;
}
class SuperMan extends Person implements ICanFly {
assign dateOfBirth = new Date();
}
请注意, reassign
没有意义,因为 Person 仅声明了该字段,但并未对其设置任何值。
写assign
似乎是多余的,但是很明显我们正在分配,因为我们在另一边有一个=
。
this.
语法再次优雅地解决了这个问题:
class Person{
dateOfBirth: Date;
}
class SuperMan extends Person implements ICanFly {
this.dateOfBirth = new Date();
}
我不确定您如何以有意义的方式将其应用于字段。
我们正在讨论添加一个仅适用于类主体的关键字,但可以在实例生命周期内的_任意点_重新分配字段。
Typescript 已经允许您在类主体中初始化字段:
class Person {
dateOfBirth : new Date();
}
它还允许您重新初始化父类中声明的字段,但情况很糟糕。 当然,您可能会写错名称(类似于override
的问题),但类型也会被删除。 这意味着:
class Person {
fullName: { firstName: string; lastName?: string };
}
class SuperMan extends Person {
fullName = { firstName: "Clark" };
bla() {
this.fullName.lastName; //Error
}
}
此代码失败并显示:类型“{ firstName: string; 上不存在属性“lastName”;
我们正在讨论添加一个仅适用于类主体的关键字,但可以在实例生命周期的任何时候重新分配字段。
还有方法:
class Person {
talk() { }
}
class SuperMan extends Person {
talk() { }
changeMe(){
this.talk = () => { };
}
changeMyPrototype() {
SuperMan.prototype.talk = () => { };
}
}
@kungfusheep我同意,可以重新分配字段,但原型上的方法也是如此。
我们需要一个关键字来“覆盖”字段的主要原因是因为实例方法_are_字段并且它们被广泛使用而不是适当的原型方法,因为它们是 lambdas,因此会自动绑定上下文。
@olmobrutall
这是被禁止的,我相信有充分的理由(这不是同一件事,所以它不应该兼容),但你是对的,直接操作类的实例时可以这样做。 我不知道为什么它被允许,但我们不是来改变语言的工作方式,所以我相信我们不应该在这里干涉。
类接口描述类的实例端,即原型中的字段和方法。 我什至认为接口中的实例方法和原型方法之间没有区别,我相信您可以将方法描述为一种或另一种,并将其实现为一种或另一种。 所以implement
可以(并且应该)也适用于字段。
new
应该具有与override
相同的语义,因此显然您不能将类型更改为不兼容的类型。 我的意思是,我们并没有改变语言的工作方式。 我只是想要一个单独的关键字,因为它不是同一种重新定义。 但是,我同意,正如您向我展示的那样,我们已经可以通过使用=
来区分字段和方法,因此也许可以使用相同的关键字/语法而不会产生歧义。
但是,统一的字段/方法覆盖语法不应该是this.method()
或this.field
,因为它看起来很像可能是有效 Javascript 的东西,而实际上不是。 在成员之前添加关键字会更清楚地表明确实是 Typescript 的事情。
this
是个好主意,所以也许我们可以去掉点,写一些不那么模棱两可的东西,比如
class Superman {
this walk() { }
}
但是,它仍然看起来很奇怪。 并且将它与implement
混合看起来有点不合适(因为我们可以接受这个this
语法不会与接口一起使用的事实?),我喜欢implement
/ override
二人组。 字段上的override
修饰符仍然让我感到厌烦,但也许最好有第三个模棱两可的new
关键字。 由于分配( =
)使方法和字段之间的区别变得清晰,所以我现在可以接受(与几个小时前我所说的相反)。
我同意,可以重新分配字段,但原型上的方法也是如此。
是的,虽然在 TypeScript 中直接修改原型,但在很大程度上是在胡闹的领域。 它远没有简单地在某物的实例上重新分配属性那么普遍。 我们很少将字段用作函数,超过 150k+ 行代码库,所以我认为说它被广泛使用可能是主观的。
我同意你的大多数其他观点,但我不同意(从它的声音来看,你也不是......)关于在这种情况下使用new
关键字。
我们很少将字段用作函数,超过 150k+ 行代码库,所以我认为说它被广泛使用可能是主观的。
我也没有,但我必须求助于@autobind
装饰器才能在我的方法中正确绑定this
,这比简单地编写 lambda 更麻烦。 只要您不使用继承,使用字段就可以按照您的预期工作。 我的印象是,至少在 JS/React 世界中,大多数使用 ES6 类的人都使用箭头函数作为方法。
接口方法既可以用适当的方法实现,也可以用实例方法实现,这无助于澄清差异。
我相信就覆盖而言,字段与方法具有相同的问题。
class A {
firstName: string;
get name() {
return this.firstName;
}
}
class B extends A {
firstname = "Joe" // oops
}
这里可能的解决方法是在构造函数中分配字段
class B extends A {
constructor() {
this.firstName = "Joe"; // can't go wrong
}
}
但这不适用于接口。 这就是为什么我(和这里的其他人)认为我们需要在类主体中直接声明一个字段时检查它的预先存在。 而且它不是override
。 现在我在这里提出了更多的想法,我相信字段的问题与接口成员的问题完全相同。 我想说我们需要一个override
关键字用于已经在父类中实现的方法(原型方法,也许还有实例方法),另一个用于定义但没有实现的东西(非函数包括的字段)。
我的新主张是,并使用试探性的member
关键字(可能不是最佳选择):
interface IBase {
interfaceField?: string;
interfaceMethod(): void
}
abstract class Base {
baseField: number;
baseMethod() { }
baseLambda: () => { };
abstract baseAbstractMethod();
}
class Derived extends Base implements IBase {
member interfaceField = "Hello";
member interfaceMethod() { }
member baseField = 2;
override baseMethod() { }
override baseLambda = () => { };
member baseAbstractMethod() { }
}
请注意,我会为抽象方法选择member
关键字,因为我说过override
将用于具有实现的事物,表明我们正在替换现有行为。 实例方法需要使用override
,因为它是一种特殊类型的字段(编译器已经将其识别为这样,因为可以使用它实现方法签名)。
我认为 Typescript 应该是 JavaScript 的编译时检查版本,通过学习 TS,你也应该学习 JS,就像通过学习 C#,你对 CLR 有很好的直觉。
原型继承是 JS 的一个很酷的概念,也是一个相当核心的概念,TS 不应该试图用其他语言的概念来隐藏它,这些概念在这里没有太多意义。
如果您要继承方法或字段,则原型继承没有任何区别。
例如:
class Rectangle {
x: number;
y: number;
color: string;
}
Rectangle.prototype.color = "black";
在这里,我们在原型对象中设置了一个简单的字段,因此所有矩形默认都是黑色的,而不必为其设置实例字段。
class BoundingBox {
override color = "transparent"; // or should be member?
}
member
关键字也让班上的其他成员嫉妒。
我们需要的是一种语法,它允许在类声明上下文中与我们在对象字面量或成员表达式中已经拥有的相同类型的行为(编译时检查/自动完成/重命名)。
也许this.
的更好选择只是.
:
class Derived {
.interfaceField = "hello";
.interfaceMethod() {}
.baseField = 2;
.baseMethod() {}
.baseLambda = () => {};
.baseAbstractMethod(){};
someNewMethod(){}
someNewField = 3;
}
总之,我认为 Typesystem 不应该跟踪哪些值来自原型,哪些值来自实例,因为一旦您可以强制访问原型,这就是一个难题,而且不会解决任何问题,而会限制 JS 的表现力。
所以函数字段、箭头函数字段和方法之间没有区别,在类型系统中也没有区别,在生成的代码中也没有区别(如果不使用this
)。
嘿,伙计们,这是很有趣的东西,但特别是在override
方面,它与切线相去甚远。 我很乐意为这类事情提供一些新的讨论问题,除非我们可以更具体地将这些评论与原始建议联系起来。
您在这里所说的很多内容也并非特定于 TypeScript,因此启动 ESDiscuss 线程也可能是合适的。 当然,他们也同样考虑过这类事情(就原型与实例的情况而言)。
@olmobrutall
ES6 类已经隐藏了原型的东西,就像@kungfusheep所说,直接搞乱它并不是一件正常的事情。
所以函数字段、箭头函数字段和方法之间没有区别,在类型系统中也没有区别,在生成的代码中也没有区别(如果不使用的话)。
好吧,在生成的代码中,类方法放在原型中,其他所有内容(不以static
前缀)放在实例中,这确实对继承产生了影响。
无论如何,我现在同意我们不必为这两种方法使用单独的语法,但是如果我们要使用override
关键字,它应该仅限于方法,我们必须找到其他的东西。 对所有事物都有一个独特的关键字可能很好,但它的含义应该非常清楚。 override
很清楚,但仅适用于方法。 你点语法,像this.
对我来说仍然太接近现有的 JS。
@RyanCavanaugh
就
override
而言,这是非常不切实际的
我的覆盖问题是对于接口方法和字段来说不够通用。 我看到三个选项:
override
用于基类方法。override
member
、 implement
、 this.
、 .
等...)我认为这个决定应该在这里做出。
您所说的很多内容实际上并不特定于 Typescript
绝对地! 我们都对转译的当前状态感到满意,但对类型检查/工具却不满意。
无论关键字是什么,它都只会是 Typescript(就像抽象一样)
@JabX
当然,您对生成的代码是正确的。 我的观点是你可以写这样的东西:
class Person {
name: string = "John";
saySomething() {
return "Hi " + this.name;
}
}
我们声明一个类, name
将转到实例,而saySomething
将转到原型。 我仍然可以这样写:
Person.prototype.name = "Unknown";
因为Person.prototype
的类型是整个人。 为简单起见,Typescript 不会跟踪他的类型系统上的内容。
对所有事物都有一个独特的关键字可能很好,但它的含义应该非常清楚。
我认为更重要并且我们都同意的是语义:
修改后的XXX
检查该成员是否已在基类或实现的接口中声明,通常用于覆盖基类中的函数。
由于.
或this.
看起来太陌生,而member
是多余的,我认为最好的选择可能是在所有情况下滥用override
。 设置字段或实现接口方法无论如何都是一个附带功能。
有很多先例:
static class
不再是真正的_class of objects_。static
字段没有任何_static_。virtual
方法是相当具体的(VB 使用Overrideable
)。它看起来像这样:
class Person{
dateOfBirth: Date;
abstract talk();
walk(){ //...}
}
interface ICanFly{
fly?();
altitude?: number;
}
class SuperMan extends Person implements ICanFly {
override dateOfBirth = new Date();
override talk(){/*...*/}
override walk = () => {/* force 'this' to be captured*/}
override fly() {/*...*/}
override altitude = 1000;
}
我们能解决这个问题吗?
对于来自接口的任何内容,我宁愿看到一个单独的implement
关键字(如果两者都存在,则优先于override
),因为它就是这样:实现,而不是覆盖。
否则,我同意最好对父类的所有内容滥用override
。
但是implement
和override
之间没有语义区别。
两者都会有类似的自动完成/错误消息/转换为 JS ......这只是一个哲学差异。
解释两个关键字真的很值得,一个迂腐的编译器会告诉你: Error you should use 'override' instead of 'implement'
。
这种情况怎么样:
interface IComparable {
compare(): number;
}
class BaseClass implements IComparable {
implement compare();
}
class ChildClass extends BaseClass implements IComparable { //again
override compare(); // or implements...
}
问题是......谁在乎?
还有implement
/ implements
问题。
让我们滥用override
。 一个概念一个关键词,这很重要。
好吧,我已经建议override
应该优先于implement
,但我可能还不够清楚。
我仍然不相信这是同一件事。 另一个例子:对于强制接口成员,实现失败是编译器错误,而覆盖失败不是问题(除非基本成员是抽象的......)。
我只是认为override
没有在父类上声明的东西有任何意义。 但也许我是唯一一个想要区别的人。 无论如何,无论我们最终使用什么关键字,我只是希望我们能够解决一些问题并提供一个严格的模式来强制使用它。
好吧,我已经提出覆盖应该优先于实现,但我可能还不够清楚。
当然,我只是想说有四种情况。 基类,接口,基类中的接口,基类中重新实现接口。
@JabX
我只是不认为覆盖父类上未声明的东西有任何意义。 但也许我是唯一一个想要区别的人。
你不。 它们是两个根本不同的东西,在语言层面上进行分离是有好处的。
我认为,如果要在这个新功能中支持接口,那么它不能是一些被固定在override
上的半生不熟的解决方案。 出于同样的原因, extends
implements
的单独实体存在, override
需要与implement
类似的东西按顺序配对来解决这个问题。 这是_替换功能_与_定义功能_。
@olmobrutall
解释两个关键字真的很值得,一个迂腐的编译器会告诉你:错误你应该使用“覆盖”而不是“实现”。
我觉得我现在已经做出了大约 4 次这种区分,但这并不是迂腐; 这是非常重要的信息!
考虑你自己的例子
interface IComparable {
compare(): number;
}
class BaseClass implements IComparable {
implement compare();
}
class ChildClass extends BaseClass implements IComparable { //again
override compare(); // or implements...
}
在这种情况下,“或实现...”是一个完全错误的假设。 仅仅因为您已经告诉编译器您想要实现与要扩展的基类相同的接口并不会改变您已经实现compare
的事实。
如果你在 ChildClass 中写implement
而不是override
那么你应该庆幸编译器会告诉你你的错误假设,因为这是你在不知不觉中抹去的大事出一个以前实现的方法!
在我负责的代码库中,这无疑是一个大问题; 所以我欢迎任何可以防止任何此类开发人员错误的编译器功能!
@kungfusheep
如果您要在 ChildClass 中编写实现而不是覆盖,那么您应该感谢编译器会告诉您您的错误假设,因为您将在不知不觉中清除以前实现的方法是一件大事!
如果重写已经实现的方法是一个如此重要的决定,那么implement
也应该用于抽象方法。 如果您将方法从abstract
更改为virtual
或以其他方式更改,则应检查(并更改)所有已实现的版本以考虑调用super
或仅删除该方法。
实际上,这在 C# 中从来都不是问题。
我同意, override
用于已实现的方法, implements
用于未实现的方法(抽象/接口)。
它可以用于抽象,是的。
在 C# 中没有“可选”接口场景,因此可能不被认为是一个大问题。
但我的观点是,在 C# 中,我们override
实现了和未实现的方法,我从未听说有人抱怨。
我不认为这真的是一个值得让解释该功能至少加倍困难的问题。
好吧,我们需要implement
关键字的唯一原因是接口成员可以是可选的,而在 C# 中是不可能的(可能在大多数 OO 语言中也是如此)。 那里没有关键字,因为您不能_不_完全实现接口,所以没有问题。 不要将你的论点过多地基于“在 C# 中是一样的”,因为我们这里有不同的问题。
override
用于_base class_ 方法,无论它们是否是抽象的。 我想我们应该在这里相同( override
而不是implement
抽象方法),因为我认为区别应该在于方法(类或接口)的起源而不是存在的实施。 并且在将来,如果您决定为您的抽象方法提供默认实现,您将不必通过您的代码(或者更糟糕的是,使用您的类的其他人的代码)来替换关键字。
我现在的主要问题是,我们是否应该有一个严格的override
/ implement
标志(我完全想要这个),我们是否应该在强制接口成员上强制使用implement
关键字? 因为它没有多大帮助(你不能不实现这些,否则它不会编译)并且可能导致很多不必要的冗长。 但另一方面,在某些接口成员上使用implement
可能具有欺骗性,但不是在所有接口成员上都有。
@JabX如果基类添加了抽象方法的实现,我个人会想要一个警告。
但是,我并没有 100% 相信一开始就使用 implements 关键字的想法。 如果你没有实现某些东西,编译器会警告你。 唯一真正有用的地方是可选方法。
无论如何,这与我为什么在这个线程中无关。 我只是在寻找是否有办法将函数指定为父类的覆盖。
我现在的主要问题是,我们是否应该有一个严格的覆盖/实现标志(我完全想要这个),我们是否应该在强制接口成员上强制实现关键字?
我认为不写应该是一个警告。
唯一真正有用的地方是可选方法。
是的,这就是重点。
如果没有这个,您想要覆盖的拼写错误是非常常见的错误,但实际上您正在创建新方法。 引入此类关键字只是指出覆盖功能意图的唯一方式。
这可能是警告或其他什么,但目前它非常痛苦。
我正在向alm.tools中的 UML 类视图添加覆盖注释:rose:
我还为覆盖alm中的基类成员的类成员添加了一个装订线指示器。
接受 PR 的范围解决方案,我们认为以最低的复杂性实现最大的价值:
override
在类方法和属性声明(包括 get/set)上有效override
get
和set
都必须标记override
,如果其中之一是override
是错误的--noImplicitOverride
(请随意在此处删除名称)使override
强制用于覆盖的内容declare class
或 .d.ts 文件)目前超出范围:
@RyanCavanaugh
我目前正在尝试实现这一点(刚刚开始,我欢迎对此提供任何帮助,特别是对于测试),但我不确定我是否理解背后的原因
如果有的话,所有签名(包括实现)都必须有
override
既然你不能在一个类中拥有一个方法的多个签名,那么你是在谈论继承上下文吗? 你的意思是(没有强制使用override
的标志,否则显然是一个错误)
class A {
method() {}
}
class B extends A {
override method() {}
}
class C extends B {
method() {}
}
应该在 C 类中输出错误,因为我没有在method
override
#$ 吗?
如果是这样,我不确定我们为什么要强制执行。 它也应该反过来工作吗? 如果我在 C 中而不是 B 中指定了override
,则在 B 类中输出错误?
是的,我认为它适用于继承树签名。
而且我认为它不会反过来起作用,一旦在层次结构中的某个级别使用它就应该开始强制执行覆盖规则。 在某些情况下,可能会在派生自开发人员不拥有的类的类上做出应用覆盖的决定。 因此,一旦使用它,它应该只适用于所有派生类。
我还有一个问题
get
和set
都必须标记override
,如果其中一个是
如果我的父类只定义了一个getter,而我想在我的派生类中定义setter,会发生什么? 如果我遵循这条规则,这意味着我的 setter 必须定义为覆盖,但父类中没有 setter,覆盖不存在的东西应该是错误的。 好吧,在实践中(在我当前的幼稚实现中),没有矛盾,因为 getter 和 setter 用于相同的属性。
我什至可以写如下内容:
class A {
get value() { return 1; }
}
class B extends 1 {
override set value(v: number) {}
}
这对我来说显然是错误的。
因为你不能在一个类中拥有一个方法的多个签名
同一个方法可以有多个重载签名:
class Base { bar(): { } }
class Foo extends Base {
// Must write 'override' on each signature
override bar(s: string): void;
override bar(s?: number): void;
override bar(s: string|number) { }
}
我认为您对其他情况的直觉是正确的。
关于 getter 和 setter,我认为override
只适用于属性槽。 编写一个没有相应 getter 的覆盖 setter 显然是非常可疑的,但在override
存在的情况下它或多或少是可疑的。 由于我们并不总是知道基类是如何实现的,我认为规则很简单,即override
getter 或 setter 必须具有_some_ 对应的基类属性,如果你说override get {
, 任何set
声明也必须有override
(反之亦然)
哦,好吧,我不知道我们可以在这样的类中编写方法签名,这非常简洁。
我认为virtual
也会有意义。
所有关键字: virtual
, override
, final
实际上会产生很大的不同,尤其是在重构类时。
我正在使用大量继承,现在很容易“错误地”覆盖方法。
virtual
也将改进智能感知,因为您可以在 override 关键字之后仅提出抽象/虚拟方法。
请,请优先考虑这些。 谢谢!
我同意@pankleks - 我经常使用继承,并且在不直接指定任何内容的情况下覆盖函数只是感觉不对。
“虚拟”、“覆盖”和“最终”将是完美的。
这对我来说是一个重要的问题。
@RyanCavanaugh
参数或初始化程序的上下文类型(之前尝试过,但这是一场灾难)
你能详细说明一下吗? 实际上,我在试图找出为什么 TS 没有在方法覆盖上推断我的参数类型时发现了这个问题,这让我感到惊讶。 我看不到重写时不同的参数列表何时正确,或者返回类型不兼容。
@pankleks
我认为
virtual
也会有意义。
就其本质而言,JS 中的一切都是“虚拟的”。 恕我直言,支持final
就足够了:如果您有一个不能被覆盖的方法(或属性),您可以将其标记为这样,以便在违反时触发编译器错误。 那么不需要virtual
,对吧? 但这在问题 #1534 中进行了跟踪。
@avonwyss这里有两个问题。
当我们尝试在上下文中键入 _property initializers_ 时,我们遇到了https://github.com/Microsoft/TypeScript/pull/6118#issuecomment -216595207 中描述的一些问题。 我们认为我们在 #10570 有一个更成功的新方法
方法声明是一个不同的蜡球,我们还有一些其他的事情要弄清楚,比如这里应该发生什么:
declare class Base {
method(x: string): string[];
method(x: number, count: number): number[];
}
class Derived extends Base {
method(x) { // x: ???
return x;
}
}
我们可能只是决定只有单签名方法从基础中获取参数类型。 但这不会让每个人都开心,也不能真正解决“我希望我的派生类方法具有相同的_signatures_但只是提供不同的_implementation_”的常见问题。
好吧,在这种情况下, x
的类型将是string | number
,但我知道可能很难始终如一地找出。
但是您可能不希望x
string | number
- 假设Derived
具有与Base
相同的语义,它不会用(3)
或('foo', 3)
调用是不合法的
嗯,是的,我错过了。
我看到您建议将其限制为单签名方法,但我相信它可以“轻松”扩展到“匹配签名”方法,例如您的示例:
declare class Base {
method(x: string): string[];
method(x: number, count: number): number[];
}
class Derived extends Base {
method(x, count) { // x: number, count: number
return [];
}
}
因为只有基类上的签名匹配。
那会比我们拥有的(什么都没有)要好得多,而且我认为这不会很难做到吗? (仍然超出此问题的范围,可能还有更多工作)
@RyanCavanaugh感谢您的回复。 如前所述,我对override
的希望是它会对参数(和返回类型)问题进行排序。 但我不认为你的例子有问题,因为重载总是必须有一个与所有重载兼容的单一“私有”实现(这就是为什么@JabX的建议在概念上不起作用)。
所以,我们会有这样的事情:
declare class Base {
method(x: string): string[];
method(x: number, count: number): number[];
// implies: method(x: string|number, count?: number): string[]|number[]
// or fancier: method<T extends string|number>(x: T, count?: number): T[]
}
class Derived extends Base {
override method(x, count?) { // may only be called like the method on Base
return [x];
}
}
这个逻辑已经存在,并且覆盖显然只适用于“私有”实现方法,而不是不同的覆盖(就像你不能显式地实现单个重载一样)。 所以我在这里没有看到问题 - 或者我错过了什么?
设计为被覆盖的方法通常无论如何都没有重载,因此即使采用简单的方法并且如果存在重载则不推断参数和返回类型将是非常好的和有用的。 因此,即使在覆盖重载方法时行为是原样的,在--no-implicit-any
模式下运行时也会出现错误,并且必须指定兼容的类型,这似乎是一种非常好的方法。
我可能遗漏了一些东西,但是在 Javascript(以及 Typescript)中,你不能有两个同名的方法——即使它们有不同的签名?
在 C# 中,您可以
public string method(string blah) {};
public int method(int blah) {};
但是在这里你永远不能有两个同名的函数,不管你用签名做什么......所以无论如何你的例子都不可能,对吧? 除非我们在同一个类上讨论多个扩展......但这不是你的例子所显示的,所以可能不是? 这不是问题吗?
现在我正在使用继承,这非常令人沮丧......我不得不对函数进行评论以表明它们是虚拟的(即我在某处覆盖它们)或覆盖(即这个函数正在覆盖底层函数)。 超级烦人和凌乱! 而且不安全!
@sam-s4s 是的,可以为一个方法声明多个逻辑重载签名,但它们最终都在同一个实现上(然后必须通过传递的参数找出正在调用哪个“重载”)。 这被许多 JS 框架大量使用,例如 jQuery,其中$(function)
添加了一个就绪函数,但$(string)
通过选择器搜索 DOM。
有关文档,请参见此处: https ://www.typescriptlang.org/docs/handbook/functions.html#overloads
@avonwyss啊,是的,声明,但不是实现,这是有道理的:)谢谢
这已经持续太久了,我觉得很烦人,我必须阅读整个臃肿的线程才能开始弄清楚为什么 TS 仍然没有这个基本的编译时断言。
我们在哪里(a) override
关键字或(b) @override
jsdoc 注释,它通知编译器“具有相同名称和类型签名的方法必须存在于超类中。” ?
更具体地说,@RyanCavanaugh 在https://github.com/Microsoft/TypeScript/issues/2000#issuecomment -224776519 (2016 年 6 月 8 日)中的评论是否表明下一步是(社区)公关?
class Bar extends Foo {
/**
* <strong i="12">@override</strong>
*/
public toString(): string {
// ...
}
override public toString(): string {
// ...
}
}
更具体地说, @RyanCavanaugh在#2000(评论)(2016 年 6 月 8 日)中的评论是否表明下一步是(社区)公关?
@pcj正确
我已经在#13217 开始了一个 PR。 任何对此功能感兴趣的人都非常欢迎参与和/或提供建议。 谢谢!
我更喜欢与带有装饰器的 java 相同
<strong i="6">@Override</strong>
public toString(): string {
// ...
}
@Chris2011同意这看起来很熟悉,但这意味着作为装饰器@Override
将在运行时而不是编译时进行评估。 一旦#13217 override 关键字( public override toString()
)在审查中前进,我计划在单独的 PR 中使用 javadoc /** <strong i="7">@override</strong> */
。
试图跟随这个线程的对话。 这包括静态函数吗? 我看不出有什么理由不能允许在派生类中覆盖具有完全不同签名的静态函数。 这在 ES6 中得到支持。 如果有class A { } ; A.create = function (){}
然后class B extends A { } ; B.create = function (x,y) { }
,调用A.create()
不会调用B.create()
并导致问题。 出于这个原因(并创建在类型上使用与工厂函数相同的函数名称的能力),您还应该允许覆盖基本静态函数的签名。 它不会破坏任何东西,并增加了做一些简洁的东西的能力(特别是对于游戏引擎框架,如果一直使用“新”任何东西,如果没有从对象缓存中提取以减少 GC 冻结,那么它真的是一种邪恶)。 由于 ES6 不支持可调用构造函数,因此为类型上的方法创建通用命名约定是唯一的其他选择; 但是,目前这样做需要派生的静态函数显示基静态函数签名及其自己的重载,这对于仅处理该类型的类型的工厂函数没有用。 :/ 同时,我唯一的选择是强制我的框架的用户在其派生类型上复制所有基本层次结构静态函数签名(例如SomeType.create()
),依此类推,这真的很愚蠢.
这是一个关于我正在谈论的“愚蠢”的示例(它有效,但在可扩展框架中不是我引以为豪的东西):
class A {
static create(s: string) {
var inst: A;
/* new or from cache */
inst.init(s);
}
protected init(s: string) { }
}
class B extends A {
static create(s: string);
static create(n: number);
static create(n:any) {
var inst: B;
/* new or from cache */
inst.init(n);
}
protected init(s: string);
protected init(n: number);
protected init(n: any) {
super.init(n.toString());
}
}
class C extends B {
static create(s: string)
static create(n: number)
static create(b: boolean)
static create(b: any) {
var inst: C;
/* new or from cache */
inst.init(b);
}
protected init(s: string);
protected init(n: number);
protected init(b: boolean);
protected init(b: any) {
super.init(b ? 0 : 1);
}
}
(https://goo.gl/G01Aku)
这会更好:
class A {
static create(s: string) {
var inst: A;
/* new or from cache */
inst.init(s);
}
protected init(s: string) { }
}
class B extends A {
new static create(n:number) {
var inst: B;
/* new or from cache */
inst.init(n);
}
new protected init(n: number) {
super.init(n.toString());
}
}
class C extends B {
new static create(b: boolean) {
var inst: C;
/* new or from cache */
inst.init(b);
}
new protected init(b: boolean) {
super.init(b ? 0 : 1);
}
}
@rjamesnw您可能对具有工厂函数合同的静态成员的多态“this”感兴趣,这在整个评论中作为主要用例进行了讨论。
因此,再次重申一年多前@pcj表达的感觉(评论),不得不在这里和其他地方阅读如此多的 PR 和评论以确定此功能请求的位置是令人困惑的。
似乎#13217 是如此接近,然后被@DanielRosenwasser和公司再次击落,作为一个可能不适合该语言的功能,似乎在这个问题上重新进入了关于是否应该完成的循环对话。 也许这个“设计时”装饰器(#2900)会解决它,也许不是? 很高兴知道。
以下是几年前发布的运行时装饰器方法的一些缺点,我没有看到提到:
如果唯一需要注意的是检查是在类初始化时发生的,那么我可能会接受它作为临时措施,但是限制太多了。
坦率地说,自从我第一次记录这个问题以来,我不敢相信这个功能在 3 年内仍然如此有争议。 每一种“严肃”的面向对象语言都支持这个关键字,C#、F#、C++ ......
你可以整天争论关于为什么 javascript 是不同的并且需要不同的方法的假设。 但是从每天在一个非常大的 Typescript 代码库中工作的实际角度来看,我可以告诉你,覆盖将对代码的可读性和维护产生巨大的影响。 由于派生类意外地覆盖了具有兼容但略有不同签名的基类方法,它还将消除整个类的错误。
我真的很想看到虚拟/覆盖/最终的正确实现。 我会一直使用它,它会使代码更具可读性且不易出错。 我觉得这是一个重要的功能。
同意。 看到添加了相对晦涩/边缘情况的功能是多么令人沮丧,而一些……基本的东西却被拒绝了。 我们能做些什么来推动这一点吗?
来人吧! 这么多评论,三年多了,为什么还没实施呢!
来吧:joy_cat:
请具有建设性和具体性; 我们不需要几十条请做的评论
但是,伙计们,也请具体说明您的立场。
显然社区对该功能非常感兴趣,而 TS 团队并没有向我们提供有关此功能未来的任何细节。
我个人完全同意@armandn ,最近发布的 TS 带来了相对很少使用的功能,而像这样的东西正在举行。
如果您不打算这样做,请告诉我们。 否则,请让我们知道社区如何提供帮助。
这里只是一个时间线,因为评论比 GitHub 愿意在加载时显示的要多:
这并不是说我们在这里没有考虑。 这与功能接近,并且我们基于类似的理由拒绝了我们自己的功能想法- 请参阅 #24423 以获取最近的示例。
我们真的很想有意地发展语言。 这需要时间,你应该期待有耐心。 相比之下,C++ 比这个线程中的很多人都要老; TypeScript 还没有成熟到可以在没有成人监督的情况下在家。 一旦我们在语言中添加了一些东西,我们就不能再把它拿出来,所以每一个添加都必须仔细权衡它的利弊。 我从经验中说,如果我们仅仅因为实现它们而实现它们(扩展方法,我在看着你),就会破坏我们继续发展语言的能力(没有交集类型,没有联合类型,没有条件类型)。有很多 GitHub 评论。 我并不是说要添加override
是一件危险的事情,只是我们总是非常谨慎地处理这个问题。
如果我现在必须总结override
的核心问题:
override
精确对齐(增加认知负荷)strict
但实际上不能的标志,因为它太大了重大变更(降低交付价值)。 任何暗示新命令行标志的东西都是我们需要认真权衡的配置空间的另一倍,因为在做出未来决策时,您只能将某些东西翻倍,否则它会消耗您的全部心理预算override
是否可以应用于实现接口成员的内部存在相当强烈的分歧(降低了感知价值,因为对于某些人来说,期望与现实不符)这里的总好处是你可以 a) 表明你正在覆盖某些东西(你今天可以用评论来做),b) 发现自动完成应该对你有帮助的拼写错误,以及 c) 使用新标志,捕获您“忘记”放置关键字并且现在需要放置的地方(一个简单的机械任务,不太可能找到真正的错误)。 我知道 b)非常令人沮丧,但同样,我们需要在这里满足复杂性标准。
归根结底,如果您认为override
关键字会极大地帮助 JS 类,那么支持 TC39 提案将其添加到核心运行时将是一个不错的起点。
它不会让你做任何你今天不能用装饰器做的事情(所以就“你可以完成的事情”而言,它“不是新的”)
也许我误解了这一点,但是装饰器不能做关键字可以做的非常重要的事情。 我在https://github.com/Microsoft/TypeScript/issues/2000#issuecomment -389393397 中提到了一些。 如果这些事情是可能的,请绝对纠正我,因为我想使用装饰器方法,但非抽象方法只是我使用此功能的一小部分。
另一个没有提到的好处是 override 关键字将使更改基类中的某些内容更加可行。 更改名称或将某些内容分成两部分等很难知道任何派生类都可以正常编译而无需更新以匹配,但可能会在运行时失败。 此外,考虑到没有可用的最终/密封/内部关键字,几乎任何导出的类都可能派生于某个地方,这使得更改几乎任何非私有的东西比使用覆盖可用时风险更大。
我的印象是 TSLint 规则将是迄今为止最顺利的前进道路。 @kungfusheep在https://github.com/Microsoft/TypeScript/issues/2000#issuecomment -192502734 中简要提到了这个想法,但我没有看到任何后续行动。 有其他人知道此功能的 TSLint 实现吗? 如果没有,我可能会在有机会时开始破解一个。 😄
我目前的想法是它只是一个评论, // @override
,就像我见过的// @ts-ignore
和其他各种基于评论的指令一样。 我个人更喜欢回避装饰器,因为(以当前的形式)它们具有运行时语义,并且它们仍处于第 2 阶段并且默认情况下被禁用。
运气好的话,自定义 TSLint 规则将是 90% 的解决方案,只是真的缺乏美学,当然它可以向前推进,而不必承诺语言的任何细节。 使用类型感知规则、tslint-language-service 和自动修复,从开发人员的角度来看,TSLint 错误和内置 TS 错误之间并没有太大区别。 我希望一个 TSLint 插件(或多个竞争插件)能给社区带来一些经验,并有机会就最佳语义达成更多共识。 然后也许它可以作为核心 TSLint 规则添加,也许这将提供足够的清晰度和动力来证明核心语言中override
关键字的美学优势是合理的。
只是在这里跳出框框思考。 我们这里有两种不同的场景。 C#(仅作为示例)使用virtual
和override
,因为默认情况下方法不是虚拟的。 在 JavaScript 中,一个类的所有函数默认都是virtual
(本质上)。 然后反转过程并使用nooverride
类型修饰符不是更有意义吗? 当然人们仍然可以强制它,但至少它有助于推动一个约定,即不应触及基类中的某些函数。 再次,只是在这里的规范之外思考。 ;) 它也可能不是一个突破性的变化。
然后反转过程并使用
nooverride
类型修饰符不是更有意义吗?
我喜欢你的思维方式,我认为你正在寻找的是最终的。
但是readonly
呢? 我相信在 JS 中重写一个方法实际上意味着在实例化过程中(当原型链被应用时)用子级替换父级。 在这种情况下,使用readonly
方法来表示“每个人都可以看到它,但我不希望任何人更改它,无论是通过继承还是在实例化后动态更改”是很有意义的。 它已经为成员实现了,为什么不为方法也这样做呢?
是否已经有提案? 如果不是,它可能值得研究作为替代覆盖......
_edit:_ 原来你可以覆盖一个只读成员,所以整个论点都崩溃了。
也许允许private
类函数是另一种选择?
编辑:我在想“而不是将其标记为最终”,但我一定是半睡半醒(显然“最终”意味着公开但不能覆盖),哈哈; 没关系。
@rjamesnw您已经可以将类函数定义为公共、受保护、私有。
我不认为只有“最终”是一个解决方案。 问题是人们可能会意外地在一个继承自已使用该名称的基类的类中创建一个新函数,然后您会在不知道原因的情况下默默地破坏事物。 这发生在我身上几次,非常令人沮丧,因为你经常不会出错,而且奇怪的事情会发生(或不发生)......
所以我认为,实际上,我们正在查看一个新的 tsconfig.json 条目,当某些东西被覆盖而没有被标记为虚拟(并且覆盖的东西被标记为覆盖或最终)时,它将强制编译器抛出错误。
我认为,如果没有override
,TypeScript 对编译时安全和类型安全的 [perceived] 承诺会让用户失望。
随着代码的发展(正如其他人指出的那样),“使用 IDE 命令覆盖方法”参数会失效。
看起来所有主要的可比语言都以某种形式添加了override
。
为了不使其成为重大更改,它可能是一个 tslint 规则(类似于 Java 中的规则)。
顺便说一句,为什么不允许在主要版本语言之间进行重大更改,例如 2.x -> 3.x。 我们不想被卡住,是吗?
如果事实证明override
的某些细节有问题,并且需要在 3.x 中进行一些重大更改调整,那就这样吧。 我想大多数人都会理解并欣赏进化速度与兼容性之间的权衡。 真的没有override
我不能推荐 TS 作为比 Java 或 C# 更实用的东西......
如果人们想要严格,应该有某种强制性的override
方式(可能是可选的 tslint 规则)。 但是,强制override
的值要低得多,因为意外覆盖超类中的方法的可能性似乎远低于意外未覆盖。
真的,这个问题自 2015 年以来一直是开放的,对我的 TypeScript 热情和布道是一盆冷水......
这不处理来自祖*父母的覆盖方法:
/* Put this in a helper library somewhere */
function override(container, key, other1) {
var baseType = Object.getPrototypeOf(container);
if(typeof baseType[key] !== 'function') {
throw new Error('Method ' + key + ' of ' + container.constructor.name + ' does not override any base class method');
}
}
在这个问题上,GH 上的No one assigned
也是令人难过和失望的。 也许是时候使用 TypeScript fork 了? ;) 编译时间安全脚本...
整个线程看起来像是一个很大的“分析瘫痪”反模式。 为什么不只做“80% 的案例价值与 20% 的工作”,然后在实践中尝试它,并在必要时在 3.x 中对其进行调整?
简单、频繁和非常有价值的用例被一些大多数人永远不需要担心的极端案例“挟持”。
因此,这可以通过装饰器在编译时进行类型检查,尽管并非没有一点帮助:
export const override = <P extends Function>() => <K extends keyof P["prototype"]>(
target: Object,
methodName: K,
descriptor: TypedPropertyDescriptor<P["prototype"][K]>
) => {
// this is a no-op. The checking is all performed at compile-time, so runtime checks are not needed.
}
class Bar {
biz (): boolean {
return true;
}
qux (): string {
return "hi";
}
}
class Foo extends Bar {
// this is fine
@override<typeof Bar>()
biz (): boolean {
return false;
}
// error: type '() => number' is not assignable to type '() => boolean'
@override<typeof Bar>()
biz (): number {
return 5;
}
// error: argument of type '"baz"' is not assignable to parameter of type '"biz" | "qux"'
@override<typeof Bar>()
baz (): boolean {
return false;
}
}
我不确定是否可以在不显式传递的情况下获得对超类型的引用。 这会产生很小的运行时成本,但检查都是编译时间。
@alangpierce
我的印象是 TSLint 规则将是迄今为止最顺利的前进道路。
[...]
然后也许它可以作为核心 TSLint 规则添加,也许这将提供足够的清晰度和动力来证明核心语言中覆盖关键字的美学优势是合理的。
100% 同意。 让我们开始吧!
嗨 - 比计划晚一点,但这是我们使用装饰器实现的 tslint 规则。
https://github.com/bet365/override-linting-rule
几年来,我们一直在 ~1mloc TypeScript 代码库中使用它,最近更新了它以使用语言服务,使其执行速度更快,开源更可行(之前的迭代需要项目的完整通过收集遗产信息)。
对于我们的用例来说,它是无价的——尽管我们仍然认为责任最终应该由编译器承担,因此 linting 规则应该被视为权宜之计。
谢谢
我同意。 我也相信涉及 TS 编译器的解决方案会比装饰器和 lint 规则好得多。
此功能的状态如何? 它是否被重新审视为编译器功能?
因此,这可以通过装饰器在编译时进行类型检查,尽管并非没有一点帮助:
一些改进:
function override< Sup >( sup : { prototype : Sup } ) {
return <
Field extends keyof Sup ,
Proto extends { [ key in Field ] : Sup[ Field ] } ,
>(
proto : Proto ,
field : Field ,
descr : TypedPropertyDescriptor< Sup[ Field ] > ,
)=> {}
}
class Foo {
bar( a : number ) {
return a
}
bar2( a : number , b : number ) {
return a
}
}
class Foo2 {
@override( Foo )
bar( a : number ) {
return 1
}
@override( Foo )
bar2( a : number , b : number ) {
return 1
}
xxx() { return '777' }
}
class Foo3 extends Foo2 {
@override( Foo ) // OK
bar( a : number ) { return 5 }
@override( Foo ) // Error: less args than should
bar2( a : number ) { return 5 }
@override( Foo ) // Error: accidental override Foo2
xxx() { return '666' }
@override( Foo ) // Error: override of absent method
yyy() { return 0 }
}
好的,找到了防止编译错误的覆盖解决方案。
节点可读流示例:
// Interface so you will keep typings for all Readable methods/properties that are not overriden:
// Fileds that are `Omit`-ed should be overriden (with any signature you want, it do not have to be compatible with parent class)
interface ReadableObjStream<T> extends Omit<stream.Readable, 'push' | 'read'> {}
// Use extends (TYPE as any) to avoid compilation errors and override `Omit`-ted methods
class ReadableObjStream<T> extends (stream.Readable as any) {
constructor() {
super({objectMode: true}); // force object mode. You can merge it with original options
}
// Override `Omit`-ed methods with YOUR CUSTOM SIGNATURE (can be non-comatible with parent):
push(myOwnNonCompatibleSignature: T): string { /* implementation*/ };
read(options_nonCompatibleSignature: {opts: keyof T} ): string { /* implementation*/ }
}
let typedReadable = new ReadableObjMode<{myData: string}>();
typedReadable.push({something: 'else'}); // will throw compilation error as expected
typedReadable.pipe(...) // non overloaded methods typings supported as expected
此解决方案的唯一缺点是调用super.parentMethod
时缺少类型(但感谢interface
ReadableObjStream,您在使用 ReadableObjStream 实例时拥有所有类型。
覆盖说明符是 c++ 11 中的杀手级功能。
它对重构代码有很大帮助和保护。
我绝对希望在没有任何障碍的情况下在 TS 基础支持中获得此功能(投票!)
我们已经明确表示对此功能的大力支持,但似乎他们仍然不打算在短期内添加此功能。
另一个想法:
class Obj {
static override<
This extends typeof Obj,
Over extends keyof InstanceType<This> = never,
>(this: This, ...overs: Over[]) {
return this as This & (
new(...a:any[])=> InstanceType<This> & Protect< Omit<InstanceType<This> , Over > >
)
}
}
class Foo extends Obj {
bar(a: number) {
return 0
}
bar2(a: number) {
return 0
}
foo = 1
}
class Foo2 extends Foo.override('bar') {
foo = 2
bar( a : number ) {
return 1
}
// Error: Class 'Foo & Protect<Pick<Foo, "bar2" | "foo">>'
// defines instance member property 'bar2',
// but extended class 'Foo2' defines it as instance member function.
bar2( a : number ) {
return 1
}
bar3( a : number ) {
return 1
}
}
declare const Protected: unique symbol
type Protect<Obj> = {
[Field in keyof Obj]:
Object extends () => any
? Obj[Field] & { [Protected]: true }
: Obj[Field]
}
在这里加上我的两分钱。
我们有一个典型的角度组件,它处理表单并需要取消订阅 valueChanges 等。 我们不想在所有地方重复代码(有点当前状态),所以我制作了一个“TypicalFormBaseComponent”,它(除其他外)实现了角度 OnInit 和 OnDestroy 方法。 问题是,现在如果您实际使用它并添加您自己的 OnDestroy 方法(这是非常标准的做法),您会隐藏原来的方法并破坏系统。 调用 super.OnInit 修复它,但我目前没有机制强迫孩子们这样做。
如果您使用构造函数执行此操作,它会强制您调用 super()...我正在寻找类似的东西,找到了这个线程,老实说,我有点沮丧。
在 ts 中实现“新”和“覆盖”可能是一个突破性的变化,但在基本上任何代码库中修复它都会非常简单(只需在它尖叫的地方添加“覆盖”)。 这也可能只是一个警告。
无论如何,我还有其他方法可以强迫孩子们超级召唤吗? 防止隐藏的 TSLint 规则,或类似的东西?
PS:我不同意“不评论”的政策。 它可以防止人们在这里抛出示例。 也许您不会实现“覆盖”,但您会实现其他东西,以解决他们的问题……也就是说,如果您真的可以阅读它们。
@GonziHere在其他语言(例如 C# 或 Java)中,覆盖并不意味着需要调用 super 方法 - 实际上通常并非如此。 构造函数是特殊的而不是普通的“虚拟方法”。
override 关键字将用于指定该方法必须已在基类上使用兼容签名定义,并且编译器可以断言这一点。
是的,但是需要覆盖迫使您注意到该方法存在。
@GonziHere看起来您可能真正需要的是创建具有抽象函数的抽象基类。 也许你可以创建私有的_onInit
和_onDestroy
方法,你可以更加依赖,然后创建受保护的onInit
和onDestroy
抽象函数(或常规函数,如果它们是不需要)。 私有函数将调用其他函数,然后正常完成。
@GonziHere @rjamesnw安全的做法实际上是以某种方式强制onInit
和onDestroy
是_final_,并定义最终方法调用的空受保护抽象模板方法。 这保证了没有人可以意外地覆盖这些方法,并且尝试这样做(假设有一种方法可以强制执行最终方法)将立即将用户指向实现他们想要的正确方法。
@shicks , @rjamesnw ,我和@GonziHere 的情况一样。 我不希望基类具有抽象方法,因为我希望为每个生命周期挂钩执行一些功能。 问题是,当有人在子类上添加 ngOnInit 或 ngOnDestroy 而不调用super()
时,我不希望该基本功能被意外替换。 要求override
会引起开发人员的注意,这些方法存在于基类中,并且他们应该选择是否需要调用super()
;
根据我编写 Java 的经验, @Override
非常普遍,以至于它变成了噪音。 在许多情况下,被覆盖的方法是抽象的或空的(其中super()
应该_不_被调用),使得override
的存在并不是一个特别明确的信号,即需要super()
。
根据我编写 Java 的经验,
@Override
非常普遍,以至于它变成了噪音。 在许多情况下,被覆盖的方法是抽象的或空的(其中super()
应该_不_被调用),使得override
的存在并不是一个特别明确的信号,即需要super()
。
这与手头的主题无关。 你也可以说使用组合而不是继承来避免这个问题。
对我来说, override
最有用的部分是在重构时出错。
现在很容易重命名基类中的某些方法而忘记重命名覆盖它的方法。
记住调用super
并不重要。 在某些情况下不调用它甚至是正确的。
我认为为什么_override_很重要存在误解。 不是强迫某人调用 _super()_ 方法,而是让他们意识到存在一个,并让他们有意识地决定是要抑制它的行为还是扩展它。
我用来确保正确性的模式是使用Parameters
和ReturnType
同时非常明确地引用基类,如下所示:
class Base {
public methodName(arg1: string, arg2: number): boolean {
return false; // base behaviour, may be stub.
}
}
class Derived extends Base {
public methodName(...args: Parameters<Base["methodName"]>): ReturnType<Base["methodName"]> {
const [meaningful, variableNames] = args;
return true; // implemented behaviour here.
}
}
这种模式是我建议添加inherit
关键字的基础。 . 这确保了对基类的任何更新都会自动传播,如果在派生类中无效,则会给出错误,或者基类的名称更改也会给出错误,这也是inherit
的想法您不仅限于为基类提供相同的签名,而是可以直观地扩展它。
我也认为对为什么override
很重要存在误解,但我并不真正理解“是否需要super()
”的所有这些问题。
为了回应@lorenzodallavecchia和该问题的原作者所说的,将函数标记为override
是一种避免在重构超类时发生错误的机制,特别是当被覆盖的函数发生变化或函数被删除。
请记住,更改覆盖函数签名的人可能不知道存在覆盖。 如果(例如在 C++ 中)覆盖没有明确标记为覆盖,那么更改覆盖函数的名称/签名不会引入编译错误,它只会导致这些覆盖不再是覆盖。 (并且.. 可能不再有用,不再被曾经调用它们的代码调用,并引入了一堆新错误,因为应该调用覆盖的东西现在正在调用基本实现)
在 C++ 中实现的 override 关键字使更改基本实现的人免于这些问题,因为在重构之后,编译错误将立即向他们表明存在一堆覆盖(覆盖现在不存在的基本实现)和线索他们认为他们可能还需要重构覆盖。
使用override
修饰符还有次要的 IDE 可用性好处(原作者也提到过),即在输入单词override
时,IDE 可以帮助您显示一堆可能性可以被覆盖。
与override
关键字一起,最好还引入一个标志,该标志在启用时要求所有覆盖另一个方法的方法都使用override
关键字,以避免您创建一个子类中的方法和将来的基类(可能在第 3 方库中)创建一个与您在子类中创建的方法同名的方法(现在可能会导致错误,因为子类已覆盖该方法)。
在这种情况下得到一个编译错误,说你需要在这个方法中添加override
关键字(即使你可能不会真正覆盖该方法,只需更改其名称以免覆盖新创建的方法基类),会更好并避免可能的运行时错误。
根据我编写 Java 的经验, @Override非常常见,以至于它变成了噪音。
经过许多 Java 年之后,我强烈不同意上述说法。 覆盖是一种通信方式,就像检查的异常一样。 这不是关于喜欢或不喜欢,而是关于质量和期望。
因此对@lucasbasquerotto是一个很大的+1,但从另一个角度来看:如果我在子类中引入一个方法,我希望有一个明确的覆盖或不覆盖的语义。 例如,如果我想重写一个方法,那么我想有一种方法来明确地告诉它。 如果我的实现有问题,例如拼写错误或复制粘贴错误,那么我想获得有关它的反馈。 或者其他方式:如果我不希望覆盖,我也想要反馈,如果覆盖偶尔发生。
来自另一位 Java 开发人员的反馈...@override 是我在Typescript中真正缺少的唯一功能。
我有大约 15 年的 Java 经验和 1-2 年或 Typescript 的经验,所以两者都非常熟悉。
@override的禅意是你可以从接口中删除一个方法,编译就会中断。
这就像新方法的紧密绑定的反向。 它允许您删除旧的。
如果您实现一个接口并添加一个方法,编译将失败,但没有相反的情况。
如果你删除一个方法,你最终会得到死代码。
(虽然也许这可以通过 linter 修复)
需要明确的是,当我说@Override
是噪音时,我特别指的是关于它是你需要调用super
的信号的评论。 它提供的检查很有价值,但是对于任何给定的重写方法,调用super
是必需的还是毫无意义的,都是一个完全的折腾。 由于如此大的一部分被覆盖的方法_不应该_,所以它对于这个目的是无效的。
我的全部观点是,如果您想确保子类回调到您的可覆盖方法中,唯一有效的方法是使方法final
(请参阅 #33446 等)并将其调用到_不同命名_空模板方法,可以安全地重写_without_ super
调用。 根本没有其他合理的方法可以获得该不变量。 我完全赞成override
,但是作为一个被用户错误地继承我的 API 的人(而且我有责任不破坏它们),我相信值得将注意力转移到final
作为这个问题的正确解决方案,而不是像上游建议的override
。
我有点困惑为什么人们总是建议final
或override
中的一个而不是另一个。 我们当然想要两者,因为它们解决不同的问题。
覆盖
防止扩展类的人意外地用他们自己的函数覆盖一个函数,甚至在不知道的情况下破坏过程中的东西。 使用 override 关键字可以让开发人员知道他们实际上是在重写现有函数,然后他们可以选择重命名他们的函数或使用 override(然后他们可以确定是否需要调用 super)。
最终的
防止扩展类的人完全覆盖一个函数,这样类的原始创建者就可以保证完全控制。
@sam-s4s 我们也想要定义:-)
问题示例..
class Base {}
class Entity extends Base {
id() {
return 'BUG-123' // busisess entity id
}
}
class Base {
id() {
return '84256635572' // storage object id
}
}
class Entity extends Base {
id() {
return '12' // busisess entity id
}
}
我们在这里意外超载。
define
关键字的案例class Base {
define id() {
return '84256635572' // storage object id
}
}
class Entity extends Base {
define id() {
return '12' // busisess entity id
}
}
应该是错误:意外重新定义。
@nin-jin 与使用override
的define
$ 不同吗?
@sam-s4s 我们也想要定义:-)
哦,我还没有看到这里有人提到关键字define
- 但是根据您的描述,我假设您的意思就像 C# 中的virtual
一词?
(我也发现你的例子有点令人困惑,因为你的课程Base
和Entity
不相关。你的意思是Entity
扩展Base
吗?)
我想有2种情况...
a) 你编写你的基类,假设所有没有final
的东西都可以被覆盖。
b) 你写你的基类,假设没有virtual
的所有东西都不能被覆盖。
@sam-s4s 实体当然扩展了 Base。 :-) 我已经修复了我的信息。 virtual
是关于别的东西。
@lorenzodallavecchia不使用override
是define | override
用于编译器。 使用define
只是define
并且编译器可以严格检查这一点。
@nin-jin 在您的模型中,这意味着不使用override
关键字仍然是合法的,对吧? 例如:
class Base {
myMethod () { ... }
}
class Overridden extends Base {
// this would not fail because this is interpreted as define | override.
myMethod () { ... }
}
理想情况下,除非您使用override
关键字,否则上述示例会产生错误。 尽管如此,我想会有一个编译器标志来选择退出覆盖检查,其中选择退出与所有方法的override | define
相同
@bioball是的,它是与许多现有代码兼容所必需的。
@sam-s4s 实体当然扩展了 Base。 :-) 我已经修复了我的信息。
virtual
是关于别的东西。
我仍然不确定你的定义是什么意思......这是 C# 中virtual
的定义:
The virtual keyword is used to modify a method, property, indexer, or event declaration and allow for it to be overridden in a derived class.
也许你的意思是相反的,而不是需要将函数标记为virtual
来覆盖,你可以将函数标记为define
然后意味着你必须使用override
关键字来覆盖它?
(为什么是define
?我还没有看到其他语言的关键字)
我在几分钟内浏览了这个主题,但我还没有看到有人建议使用覆盖作为避免重复指定参数和返回值的方法。 例如:
class Animal {
move(meters:number):void {
}
}
class Snake extends Animal {
override move(meters) {
}
}
在上面的示例中, move
将具有相同的必需返回类型 ( void
) 并且meters
也将具有相同的类型,但没有必要指定它。 我工作的大公司正试图将我们所有的 javascript 迁移到 typescript,远离 Closure 编译器类型。 然而,在 Closure 中我们可以只使用@override
并且所有类型都将从超类中推断出来。 这对于减少不匹配的可能性和减少重复非常有用。 甚至可以想象实现这一点,即使没有为超类中指定的参数指定类型,也可以使用附加参数扩展方法。 例如:
class Animal {
move(meters:number):number {
}
}
class Snake extends Animal {
override move(meters, time:number) {
}
}
我来这里写评论的动机是,我们公司现在要求我们指定所有类型,即使是被覆盖的方法,因为 typescript 没有这个功能(而且我们正在进行长期迁移)。 它很烦人。
FWIW,虽然它更容易编写,但省略参数类型(以及跨文件类型推断的其他实例)会妨碍可读性,因为它需要不熟悉代码的人四处挖掘以找到超类的定义位置,以便知道什么类型meters
是(假设您在 IDE 之外阅读它,这对于尚未设置 IDE 的不熟悉项目很常见)。 而且代码的阅读频率比编写的频率高得多。
@shicks您可以说任何导入的变量或类。 将您可能需要的所有信息复制到单个文件中会破坏抽象和模块化的目的。 强制类型被复制违反了 DRY 原则。
最有用的评论
请原谅抱怨,但老实说,虽然你的论点确实适用于
public
关键字在默认情况下一切都是公开的语言,确实是世界划分,有像abstract
和可选的override
关键字只会帮助开发人员感到更安全,犯更少的错误并浪费更少的时间。覆盖是该语言为数不多的对打字错误高度敏感的方面之一,因为输入错误的覆盖方法名称并不是一个明显的编译时问题。
override
的好处是显而易见的,因为它允许您声明您的重写意图 - 如果基本方法不存在,则它是编译时错误。 所有人都欢呼类型系统。 为什么会有人不想要这个?