在Scala中实现一个字符串类,该类可以进行大小写不敏感的比较。

5

我有许多带有字段的类,这些字段应该是不区分大小写的,并且我想将这些类的实例放入HashMap中,并通过不区分大小写的字符串查找它们。

与其每次想要按其字符串索引实例或按其字符串查找实例时都使用toLowerCase,我尝试将此逻辑封装在CaseInsensitiveString类中:

/** Used to enable us to easily index objects by string, case insensitive
 * 
 * Note: this class preservse the case of your string!
 */
class CaseInsensitiveString ( val _value : String ) {
  override def hashCode = _value.toLowerCase.hashCode
  override def equals(that : Any) = that match {
    case other : CaseInsensitiveString => other._value.toLowerCase ==_value.toLowerCase
    case other : String => other.toLowerCase == _value.toLowerCase
    case _ => false
  }
  override def toString = _value
}

object CaseInsensitiveString {
  implicit def CaseInsensitiveString2String(l : CaseInsensitiveString) : String = if ( l ==null ) null else l._value
  implicit def StringToCaseInsensitiveString(s : String) : CaseInsensitiveString = new CaseInsensitiveString(s)

  def apply( value : String ) = new CaseInsensitiveString(value)
  def unapply( l : CaseInsensitiveString) = Some(l._value)
}

你能提出更清晰或更好的方法吗?

我遇到的一个缺点是当使用junit的assertEquals时,像这样:

assertEquals("someString", instance.aCaseInsensitiveString)

它失败了,说它期望得到"someString",但实际上得到了CaseInsensitiveString<"someString">。

如果我反转assertEquals中变量的顺序,那么它就可以工作了,可能是因为这样它会调用CaseInsensitiveString类中的equals函数。我目前通过保持顺序相同(所以预期的实际上是预期的)但在CaseInsensitiveString上调用.toString来解决这个问题:

assertEquals("someString", instance.aCaseInsensitiveString.toString)

这也可以工作:
assertEquals(CaseInsensitiveString("someString"), instance.aCaseInsensitiveString)

我可以添加一个隐式的字符串相等方法来解决这个问题吗?


你有没有想过创建一个trait来处理从hashmap中获取字符串的逻辑,以便它可以正常工作? - James Black
我没有考虑过这个,但不幸的是我还没有完全理解你的建议。詹姆斯,你能再详细解释一下吗?我应该在实例化时将这个特性混合到HashMap中吗? - waterlooalex
5个回答

7

这里有一种更加清晰的实现方式,使用“Proxy”和“Ordered”特性:

// http://www.scala-lang.org/docu/files/api/scala/Proxy.html
// http://www.scala-lang.org/docu/files/api/scala/Ordered.html


case class CaseInsensitive(s: String) extends Proxy with Ordered[CaseInsensitive] {
  val self: String = s.toLowerCase
  def compare(other: CaseInsensitive) = self compareTo other.self
  override def toString = s
  def i = this // convenience implicit conversion
}

关于("string" == CaseInsensitive("String"))问题,暂时没有帮助。

您可以这样进行隐式转换:

  implicit def sensitize(c: CaseInsensitive) = c.s
  implicit def desensitize(s: String) = CaseInsensitive(s)

这应该能够轻松进行比较:

  assertEquals("Hello"i, "heLLo"i)

代理模式是如何实现的?它是否使用了反射?它仍然值得使用吗? - waterlooalex
没有 Proxy 类的魔法。看看源代码多简单:http://scala-tools.org/scaladocs/scala-library/2.7.1/Proxy.scala.html - Mitch Blevins
啊,好的,所以代理将hashCode、equals和toString转发到内部类。出于某种原因,我认为它会做更多的事情。 - waterlooalex
谢谢,我注意到当我的测试失败时 :) 这种实现方法也不支持对字符串进行等于比较,而另一种方法则支持。但考虑到它只能单向工作(例如,CaseInsensitiveString == String,而不是String == CaseInsensitiveString),这可能不是一个好主意。 - waterlooalex
这是一个链接,链接到一个完整的、可编译的示例,输出为"yay"。 http://gist.github.com/236656这个想法被从RichString类中偷来,使用"blah".r将其转换为Regex。我也看到了"myRegex"r的调用使用方式。 - Mitch Blevins
显示剩余12条评论

3
在Scala 2.8中,您想定义一个Ordering [String],并覆盖compare方法以进行不区分大小写的比较。然后,您可以将其传递(或定义一个implicit val)给任何需要进行比较的函数--所有标准集合都接受Ordering [T]进行比较。

谢谢Ken,听起来很有趣,不过我现在还在使用2.7.7。 - waterlooalex

1
我今天遇到了这个问题。这是我选择解决它的方法:
首先,我声明了一个 Ordering 类型的对象来进行排序:
import scala.math.Ordering.StringOrdering
object CaseInsensitiveStringOrdering extends StringOrdering {
  override def compare(x: String, y: String) = {
    String.CASE_INSENSITIVE_ORDER.compare(x,y)
  }
}

接下来,当我创建我的TreeMap时,我使用了以下对象:

val newMap = new TreeMap[String,AnyRef]()(CaseInsensitiveStringOrdering)

顺便提一下,这是使用的Scala 2.11.8版本。


0

这是使用 Ordering 的示例(自2.8以来)

val s = List( "a", "d", "F", "B", "e")

res0:List[String] = List(B, F, a, d, e)

res0:列表[字符串] = 列表[B,F,a,d,e]

object CaseInsensitiveOrdering extends scala.math.Ordering[String] {
    def compare(a:String, b:String) = a.toLowerCase compare b.toLowerCase
}

定义了一个名为CaseInsensitiveOrdering的对象

val orderField = CaseInsensitiveOrdering

orderField: CaseInsensitiveOrdering.type = CaseInsensitiveOrdering$@589643bb

排序字段:CaseInsensitiveOrdering.type = CaseInsensitiveOrdering$@589643bb

s.sorted(orderField)

res1: List[String] = List(a, B, d, e, F)

避免使用 toLowerCase,因为它无法通过“火鸡测试”(Google一下并查看字母“i”的结果)。可能会改用 String.CASE_INSENSITIVE_ORDER.compare,这也可以避免昂贵的字符串复制。 - Luke Usherwood
哎呀,String.CASE_INSENSITIVE_ORDER 也没有通过 Turkey Test :o) 那么也许可以使用 Collator.getInstance() 来在用户的语言环境中进行正确的比较。 - Luke Usherwood

0

我认为Java的String.equalsIgnoreCase是解决相等性问题所需使用的方法。由于JUnit期望一个字符串,确保你的类派生自String,这样它就能解决问题了。此外,记住相等性的对称性,如果a == b,则b == a,这对编程的影响是,如果你有两个对象obj1和obj2,则obj1.equals(obj2)== obj2.equals(obj1)

确保你的代码符合这些约束条件。


1
re.equalsIgnoreCase,是的,这就是我想要的功能类型,你建议我如何使用它?从我所了解的来看,re源自String,而String是final/sealed的,不允许你扩展它。 - waterlooalex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接