关于 option.contains() 的 Scala 问题

shi*_*iuu 0 functional-programming scala

看来选项.contains()并不总是按预期工作。我有以下代码:

case class Person (name: String, nickName: Option[String])

val people = Seq(Person("Ned", Some("d")), Person("Alex", None))
val suspects = Map("c" -> 1, "d" -> 2)

val result1 = people.filter(_.nickName.contains(suspects.contains(_)))
val result2 = people.filter{ p =>
    p.nickName.contains{ n =>
        suspects.contains(n)
    }
}
    
println(result1)
println(result2)
Run Code Online (Sandbox Code Playgroud)

你可能期望result1result2包含一个人,但他们实际上是空的。为什么?

事实证明以下代码有效:

val result3 = people.filter{ _.nickName match {
        case Some(n) => suspects.contains(n)
        case None => false
    }
}
val result4 = people.filter( _.nickName.map(suspects.contains).getOrElse(false))
val result5 = people.filter( _.nickName.fold(false)(suspects.contains))

println(result3)
println(result4)
println(result5)
Run Code Online (Sandbox Code Playgroud)

我在 Scala 2.12 和 2.13 上都尝试过,结果是一样的。为什么nickName.contains不起作用?

Bor*_*nov 5

对您问题的简短回答:发生这种情况是因为您Option contains在尝试将函数( String => Boolean) 与选项String进行比较时使用了错误的方法。

contains但在方法上存在一些困难Option。我们看一下containsscala源码中的定义:

/**@example {{{
 *  // Returns true because Some instance contains string "something" which equals "something".
 *  Some("something") contains "something"
 *
 *  // Returns false because "something" != "anything".
 *  Some("something") contains "anything"
 *
 *  // Returns false when method called on None.
 *  None contains "anything"
 *  }}}
 *
 *  @param elem the element to test.
 *  @return `true` if the option has an element that is equal (as
 *  determined by `==`) to `elem`, `false` otherwise.
 */
final def contains[A1 >: A](elem: A1): Boolean =
  !isEmpty && this.get == elem
Run Code Online (Sandbox Code Playgroud)

我们看到,contains将包含 的值与具有较低类型界限 的Option[A]类型的某些传递元素进行比较。这意味着类型参数或抽象类型引用 type 的超类型。这意味着我们可以传递任何类型的超类型来检查例如。在您的情况下,您尝试传递给 contains函数,但请查看定义:A1A[A1 >: A]A1A1AStringcontainsAny String => BooleanFunction1

trait Function1[@specialized(scala.Int, scala.Long, scala.Float, scala.Double) -T1, @specialized(scala.Unit, scala.Boolean, scala.Int, scala.Float, scala.Long, scala.Double) +R] extends AnyRef
Run Code Online (Sandbox Code Playgroud)

它是一种延伸的特征AnyRef(而且Any也是)。因此,对于编译器来说,这是一个合法的情况,它检查什么String是子类型Any并编译代码。在运行时,它尝试比较String值和函数 String => BooleanString!= ),这就是始终返回您并过滤空列表String => Boolean的原因。containsfalse

因此,要解决您的主要问题 - 使用地图正确过滤列表,最好使用exists函数:

val people = Seq(Person("Ned", Some("d")), Person("Alex", None))
val suspects = Map("c" -> 1, "d" -> 2)

val resultExist = people.filter { p =>
  p.nickName.exists {
    suspects.contains
  }
}
val resultExistUnsugar = people.filter { p =>
  p.nickName.exists {
    n => suspects.contains(n)
  }
}
println(resultExist) // List(Person(Ned,Some(d)))
println(resultExistUnsugar) // List(Person(Ned,Some(d)))
Run Code Online (Sandbox Code Playgroud)

为了保护代码的类型安全,我建议您在使用方法时设置具体类型参数contains

// it will not compile because suspects.contains type is not String
val result1 = people.filter(_.nickName.contains[String](suspects.contains))
// it will not compile also by the same reason
val result2 = people.filter { p =>
  p.nickName.contains[String] {
    n => suspects.contains(n) 
  }
}
Run Code Online (Sandbox Code Playgroud)

但我们仍然不知道为什么contains有类型参数边界[A1 >: A]。为了澄清这一点,让我们看一下Option[+A]类定义:

sealed abstract class Option[+A] extends Product with Serializable { // ... }
Run Code Online (Sandbox Code Playgroud)

这里我们看到Option有协方差类型参数+A,这就是我们应该向方法添加参数边界的原因contains在此线程中查看更多原因。

综上所述:

  • 在所有容器中使用方法时都要小心,contains而不仅仅是在Option
  • 最好绑定一些可以类型参数化的方法,以实现类型安全和更可预测的行为
  • 有关docs.scala-lang上的类型下限和协方差的更多信息

  • 我对类型检查感到有点惊讶。 (2认同)