Sorbet接口要求实现签名类型为父类型而非子类型的原因咨询
关于Sorbet类型检查中接口方法参数类型兼容问题的解答
问题背景
在使用Sorbet进行Ruby类型检查时,定义Lifecycle::PostAuthAction接口的sync方法,其block参数要求接受T::Struct类并返回T::Struct实例;但在Admin::PostAuthAction实现该接口时,使用T::Struct的子类PostActionContext作为block的参数和返回类型,触发Sorbet错误,提示参数类型必须是父类方法对应参数类型的超类型。预期接口应允许使用T::Struct的子类,但实际出现约束问题,最终通过泛型实现了符合里氏替换原则(LSP)的效果。
错误信息
editor.rb:29: Block parameter _block of type T.proc.params(arg0: T.class_of(Admin::PostAuthAction::PostActionContext)).returns(Admin::PostAuthAction::PostActionContext) not compatible with with type of abstract method Lifecycle::PostAuthAction#sync https://srb.help/5035 29 | def sync(&_block) ^^^^^^^^^^^^^^^^^ editor.rb:11: The super method parameter _block was declared here with type T.proc.params(arg0: T.class_of(T::Struct)).returns(T::Struct) 11 | def sync(&_block); end ^^^^^^^^^^^^^^^^^ Note: A parameter's type must be a supertype of the same parameter's type on the super method.
相关代码
# typed: strict module Lifecycle module PostAuthAction extend T::Sig extend T::Helpers interface! sig do abstract.params( _block: T.proc.params(arg0: T.class_of(T::Struct)).returns(T::Struct) ).void end def sync(&_block); end end end module Admin class PostAuthAction include Lifecycle::PostAuthAction extend T::Sig class PostActionContext < T::Struct const :user, Object end PostActionContextCallback = T.type_alias do T.proc.params(arg0: T.class_of(PostActionContext)).returns(PostActionContext) end sig { override.params(_block: PostActionContextCallback).void } def sync(&_block) context = yield(PostActionContext) end end end
问题原因
- Sorbet对函数类型的参数遵循逆变(Contravariance)规则:子类方法的参数类型必须是父类方法参数类型的超类型(更宽泛的类型),而返回值类型遵循**协变(Covariance)**规则(可以是更具体的子类型)。
- 原接口中
sync方法的block参数要求接受任意T::Struct类,而实现类的block仅接受PostActionContext类(T::Struct的子类),此时T.class_of(PostActionContext)是T.class_of(T::Struct)的子类型,违反了参数逆变的要求——接口要求方法能处理更宽泛的输入,而实现类只能处理更窄的输入,不符合里氏替换原则。
泛型解决方案
# typed: strict module Lifecycle module PostAuthAction extend T::Sig extend T::Helpers extend T::Generic # 定义泛型类型成员,用于约束上下文类型 U = type_member interface! sig do abstract.params( _block: T.proc.params(arg0: T::Class[U]).returns(U) ).void end def sync(&_block); end end end module Admin class PostAuthAction include Lifecycle::PostAuthAction extend T::Generic extend T::Sig class PostActionContext < T::Struct const :user, Object end # 将泛型成员固定为具体的子类类型 U = type_member {{ fixed: PostActionContext }} sig do override.params( _block: T.proc.params(arg0: T.class_of(PostActionContext)).returns(PostActionContext) ).void end def sync(&_block) context = yield(PostActionContext) end end end
方案说明
- 通过在接口中引入泛型类型成员
U,将原本固定的T::Struct替换为可参数化的类型,让实现类可以指定具体的上下文类型。 - 实现类通过
type_member {{ fixed: PostActionContext }}将泛型成员绑定到子类,既满足了接口的约束,又允许使用更具体的类型,完全符合里氏替换原则:此时接口的sync方法针对特定的U类型,实现类的方法参数和返回值类型与U完全匹配,不存在逆变/协变冲突。
内容的提问来源于stack exchange,提问作者PaulSCoder
相关产品推荐
相关产品推荐

