【问题标题】:How to "inject" traits to base type classes to use them in generic type methods with traits如何将特征“注入”到基类型类以在具有特征的泛型类型方法中使用它们
【发布时间】:2016-02-01 12:13:32
【问题描述】:

好的,也许我以错误的方式解决了整个问题,但我需要帮助,我找不到解决方案。 我正在为我自己在 Scala 中使用的 Redis 客户端实现打包。 所以我想要一个名为RedisListClient[T] 的类,它将在内部处理所有事情,而我只有一个List[T] 可以使用。

现在我有一个工厂通过以下方式创建这样的客户端:

def createRedisListClient[T <: ByteStringFormattable](name: String): RedisListClient[T] = {
  new RedisListClient[T](name)
}

泛型类型必须包含特征ByteStringFormattable。这是必需的,因为 redis 客户端必须在内部将对象序列化为 ByteString,并且必须能够向后执行。

所需的特质很简单

trait ByteStringFormattable {
  type T
  val formatter: ByteStringFormatter[T]
}

所以我把这一切都用于自定义类。假设有一个 ScoreModel,可以在其中保存分数。

class ScoreModel(userId: String, score: Long, scoreText: Option[String] = None) extends ByteStringFormattable {
  override type T = ScoreModel

  override val formatter: ByteStringFormatter[T] = new ByteStringFormatter[T] {
    override def serialize(data: T): ByteString = ByteString(s"$userId#$score#$scoreText")

    override def deserialize(bs: ByteString): T = {
      val split = bs.utf8String.split("#", 2)
      new ScoreModel(split(0), split(1).toLong, split.lift(2).map(s => Some(s)).getOrElse(None))
    }
  }
}

现在对我来说真正重要的问题是,我将如何使用createRedisListClient[String]("myName")? 我的意思是String 没有实现特征ByteStringFormattable

我尝试使用这样的隐式转换:

implicit def fromString(s: String): SerializableString = new SerializableString(s)
implicit def toString(sws: SerializableString): String = sws.get

implicit class SerializableString(string: String) extends ByteStringFormattable {
  override type T = String

  val get = string

  override val formatter: ByteStringFormatter[T] = new ByteStringFormatter[String] {
    override def serialize(data: String): ByteString = ByteString(data.getBytes("UTF8"))

    override def deserialize(bs: ByteString): String = bs.utf8String
  }
}

但这无济于事。 Scala 编译器看到该函数调用的 String 类型,检查它是否使用 trait,然后失败。

那么,有没有可能我可以将函数调用与StringInt 和 Co 等基本类型一起使用一些隐含的东西,或者我真的必须构建像 SerializableString 这样的 Wrapper 类上面的例子?

【问题讨论】:

    标签: scala generics traits


    【解决方案1】:

    我没有时间为你编写整个代码,因为你有所有这些 Redis 依赖项,所以我复制起来很乏味,但让我大致解释一下。

    你的方法

    def createRedisListClient[T <: ByteStringFormattable](name: String)
    

    只能接受扩展 ByteStringFormattable 的内容。如果您希望它能够接受在范围内可以使用隐式转换 T -> ByteStringFormattable 的类型 T,则必须这样说:

    def createRedisListClient[T](name: String)(implicit conv: T => ByteStringFormattable)
    

    您也可以使用诸如view bounds and context bounds 之类的语法糖来做到这一点(视图边界已被弃用,因此我会避免使用它们)。

    一般来说,您的问题的标准解决方案是type classes。让我也指出我的blog post on this topic。我写它是因为我觉得我们需要一篇将所有这些东西粘合在一起的文章,从你遇到的问题到隐式转换、视图/上下文边界和类型类。如果您有 15-20 分钟的空闲时间,我认为这会对您有所帮助。

    【讨论】:

    • 那些文章看起来很有趣,我一定会读的。猜猜这是非常需要的。作为隐式的隐式转换看起来很有趣。 =&gt; 表示 T 可以转换为 ByteStringFormattable?这是否也意味着我可以在客户端内直接调用this.formatter?或者我将如何使用它?
    • 好吧,理想情况下,您可以将方法编写为def createRedisListClient[T](name: T)(implicit conv: T =&gt; ByteStringFormattable)。请注意,我将名称从 String 更改为 T。然后在方法主体中,您可以说 conv(name),这将返回一个 ByteStringFormattable,您可以在其上调用 formatter=&gt; 表示函数; T =&gt; ByteStringFormattable 是一个从 T 到 ByteStringFormattable 的函数。范围内的隐式方法可以解决问题(Scala 在函数和方法之间存在细微差别,但这是另一回事)。
    【解决方案2】:

    从你所拥有的代码到工作代码的最短变化是使用 view bound 而不是你现在拥有的 bound。视图绑定会断言存在从TByteStringFormattable 的某种隐式转换,这涵盖了T 是子类的情况,但也涵盖了String 的情况。

    但是,在我们这样做之前,我认为需要进行一些重新安排。具体来说,我认为您对ByteStringFormattable 的定义有问题,因为被转换的类型与ByteStringFormattable 之间没有任何联系。照原样,现在你可以写这个,它会编译,即使它是非常错误的:

    class ScoreModel(userId: String, score: Long, scoreText: Option[String] = None) extends ByteStringFormattable {
      override type T = String
    
      override val formatter: ByteStringFormatter[T] = new ByteStringFormatter[T] {
        override def serialize(data: String): ByteString = ByteString(data.getBytes("UTF8"))
    
        override def deserialize(bs: ByteString): String = bs.utf8String
      }
    }
    

    也就是说,现在您没有类型保证返回的 ByteStringFormatter 将是正确类的格式化程序。

    因此,我将ByteStringFormattable 重新定义为:

    trait ByteStringFormattable[T] {
      val formatter: ByteStringFormatter[T]
    }
    

    那么,这里是我写的一些东西,它编译了你想要的东西,并带有一个视图绑定。但是请继续阅读,因为视图边界在 Scala 2.11 中已被弃用,所以我稍后会展示更好的、非弃用的方式(隐式参数)。

    请注意,我必须调整您对 ByteString 上的方法的调用才能在此处编译它,因为我没有 redis 的 ByteString,必须使用来自 com.google.protobuf 的那个。

    class RedisListClient[T <% ByteStringFormattable[T]](val name: String) {
    
    }
    
    trait ByteStringFormatter[T] {
      def serialize(data: T): ByteString
      def deserialize(bs: ByteString): T
    }
    
    trait ByteStringFormattable[T] {
      val formatter: ByteStringFormatter[T]
    }
    
    class ScoreModel(userId: String, score: Long, scoreText: Option[String] = None) extends ByteStringFormattable[ScoreModel] {
      override val formatter: ByteStringFormatter[ScoreModel] = new ByteStringFormatter[ScoreModel] {
        override def serialize(data: ScoreModel): ByteString = ByteString.copyFromUtf8(s"$userId#$score#$scoreText")
    
        override def deserialize(bs: ByteString): ScoreModel = {
          val split = bs.toStringUtf8.split("#", 2)
          new ScoreModel(split(0), split(1).toLong, split.lift(2).map(s => Some(s)).getOrElse(None))
        }
      }
    }
    
    
    object RedisListClient {
    
      implicit class SerializableString(string: String) extends ByteStringFormattable[String] {
        val get = string
    
        override val formatter: ByteStringFormatter[String] = new ByteStringFormatter[String] {
          override def serialize(data: String): ByteString = ByteString.copyFrom(data.getBytes("UTF8"))
    
          override def deserialize(bs: ByteString): String = bs.toStringUtf8
        }
      }
    
    
      def createRedisListClient[T <% ByteStringFormattable[T]](name: String): RedisListClient[T] = {
        new RedisListClient[T](name)
      }
    
      def testIt() = {
        val x = createRedisListClient[ScoreModel]("ClientSM")
        val y = createRedisListClient[String]("Wat")
      }
    }
    

    编译,但正如我所说,它使用不推荐使用的视图边界,在现代 scala 中被隐式参数替换。这是一种使用隐式参数做你想做的事情的方法:

    class RedisListClient[T](val name: String, conv: T => ByteStringFormattable[T]) {
    
    }
    
    trait ByteStringFormatter[T] {
      def serialize(data: T): ByteString
      def deserialize(bs: ByteString): T
    }
    
    trait ByteStringFormattable[T] {
      val formatter: ByteStringFormatter[T]
    }
    
    class ScoreModel(userId: String, score: Long, scoreText: Option[String] = None) extends ByteStringFormattable[ScoreModel] {
      override val formatter: ByteStringFormatter[ScoreModel] = new ByteStringFormatter[ScoreModel] {
        override def serialize(data: ScoreModel): ByteString = ByteString.copyFromUtf8(s"$userId#$score#$scoreText")
    
        override def deserialize(bs: ByteString): ScoreModel = {
          val split = bs.toStringUtf8.split("#", 2)
          new ScoreModel(split(0), split(1).toLong, split.lift(2).map(s => Some(s)).getOrElse(None))
        }
      }
    }
    
    
    object RedisListClient {
    
      implicit class SerializableString(string: String) extends ByteStringFormattable[String] {
        val get = string
    
        override val formatter: ByteStringFormatter[String] = new ByteStringFormatter[String] {
          override def serialize(data: String): ByteString = ByteString.copyFrom(data.getBytes("UTF8"))
    
          override def deserialize(bs: ByteString): String = bs.toStringUtf8
        }
      }
    
    
      def createRedisListClient[T](name: String)(implicit mkFormatter: T => ByteStringFormattable[T]): RedisListClient[T] = {
        new RedisListClient[T](name, mkFormatter)
      }
    
      def testIt() = {
        val x = createRedisListClient[ScoreModel]("ClientSM")
        val y = createRedisListClient[String]("Wat")
      }
    }
    

    这一切都很好,但是如果您已经打算使用隐式参数,您可能想要的是隐式ByteStringFormatter 对象,而不是必须在每个元素上调用formatter。因此,让我们再重做一次,但使用隐式格式化程序对象,并且根本没有可格式化的特征:

    class RedisListClient[T](val name: String, formatter: ByteStringFormatter[T]) {
    
    }
    
    trait ByteStringFormatter[T] {
      def serialize(data: T): ByteString
      def deserialize(bs: ByteString): T
    }
    
    class ScoreModel(userId: String, score: Long, scoreText: Option[String] = None) {
      def toBs: ByteString =  ByteString.copyFromUtf8(s"$userId#$score#$scoreText")
    }
    
    
    object RedisListClientImplicits {
    
      implicit val formatterSM: ByteStringFormatter[ScoreModel] = new ByteStringFormatter[ScoreModel] {
        override def serialize(data: ScoreModel): ByteString = data.toBs
    
        override def deserialize(bs: ByteString): ScoreModel = {
          val split = bs.toStringUtf8.split("#", 2)
          new ScoreModel(split(0), split(1).toLong, split.lift(2).map(s => Some(s)).getOrElse(None))
        }
      }
    
      implicit val formatterString: ByteStringFormatter[String] = new ByteStringFormatter[String] {
        override def serialize(data: String): ByteString = ByteString.copyFrom(data.getBytes("UTF8"))
    
        override def deserialize(bs: ByteString): String = bs.toStringUtf8
      }
    }
    
    object RedisListClient {
      def createRedisListClient[T](name: String)(implicit f: ByteStringFormatter[T]): RedisListClient[T] = {
        new RedisListClient[T](name, f)
      }
    }
    
    object RedisListClientTest {
      import RedisListClient._
      import RedisListClientImplicits._
    
      def testIt() = {
        val x = createRedisListClient[ScoreModel]("ClientSM")
        val y = createRedisListClient[String]("Wat")
      }
    }
    

    【讨论】:

    • 我现在接受你的答案作为答案,即使@slouc 已经提出了类似的建议,因为你的更详细,你甚至解释了原因和方式。我非常专注于泛型和使用特征和东西,以至于我没有想到 scala 隐式可以做的惊人的东西。它甚至在我已经使用的 redis 代码中,所以我只需要将它扩展到我自己的东西。你的方式甚至摆脱了 ByteStringFormattable 特征,这并不是真正需要的。真不错。
    • 哦,因为这是公认的答案,所以我将发布我所做的一个小修改:我将 RedisListClientImplicits 类仅用于已经存在的类,主要是默认类型,如 String、Int 等。对于像 ScoreModel 这样的类,我将在伴随对象中定义隐式格式化程序,这使得格式化程序在类本身可用的地方自动可用。
    【解决方案3】:

    你真的想这样调用你的方法吗:

    createRedisListClient[String]("myName")
    

    即类型参数是 String 对您来说很重要吗?如另一个答案中所述,由于您的类型约束,这无法正常工作,您必须将其更改为上下文绑定。

    但这真的是你想要的,还是你真的不在乎类型参数是什么,只是想将 String 隐式转换为合适的东西?

    如果您这样称呼它,则可以应用您的隐式转换:

    createRedisListClient("myName")
    

    您必须删除隐式转换方法。这些方法是为隐式类生成的。 (实际上,生成这些方法是唯一声明类隐式所做的事情)。

    【讨论】:

    • 我需要类型参数字符串,以便我可以直接与客户端合作。假设客户端有一个名为“members”的方法,它将基于泛型参数 T 返回List[T]。如果我使用上面的语法,我可以调用client.members 并像处理List[String] 一样处理它。这就是我想使用类型参数的原因。你的建议可以吗?
    • 好的,如果你真的需要字符串——你的例子有很多不必要的细节让我快速看到——然后按照建议使用上下文边界。只需编写 [T: ByteStringFormatter] 来声明具有上下文绑定的类型参数,并使用隐式 [ByteStringFormatter[T]] 来访问隐式提供的实例。使用隐式转换和隐式属性来提供该实例。 (我必须习惯这里按回车发送消息......)
    • 是的,对于文字墙感到抱歉。在这里询问时,我通常会选择“提供尽可能多的信息”,而不是遗漏一些重要信息。另一张海报链接了一篇关于上下文边界的文章。我会读的。谢谢。
    猜你喜欢
    • 2023-01-12
    • 1970-01-01
    • 2021-07-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多