You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 18:34:54