写点什么

Scala 中 String 和 Int 隐式转换的问题分析

用户头像
木子李G
关注
发布于: 2020 年 12 月 11 日

引出问题



首先来看一个需求:将String类型的数字赋值给Int类型的变量。



也就是这样:



val num:Int="20"



要想实现这样的效果,小伙伴们应该都能想到使用隐式方法这个技能。许多小伙伴一鸡冻就撸出了如下的代码:



implicit def strToInt(str:String):Int= {
str.toInt
}



*友情提示:*隐式转换的代码要定义在object中哦~~~



定义了如上的隐式方法后,接下来我们来使用一下该隐式方法。接下来来一段完整代码尝尝:



object TestDemo {
def main(args: Array[String]): Unit = {
val num:Int = "20"
println(num)
}
/**
* 定义的隐式方法
* 该方法的功能是将String转成Int
* @param str 需要转换的字符串
* @return 返回 Int
*/
implicit def strToInt(str:String):Int= {
str.toInt
}
}



代码撸完,感觉还不错,接着我们运行以上代码:哐当,出错啦。。。。



控制台输出以下错误信息:



Error:(17, 5) type mismatch;
found : str.type (with underlying type String)
required: ?{def toInt: ?}
Note that implicit conversions are not applicable because they are ambiguous:
both method augmentString in object Predef of type (x: String)scala.collection.immutable.StringOps
and method strToInt in object TestDemo of type (str: String)Int
are possible conversion functions from str.type to ?{def toInt: ?}
str.toInt
Error:(17, 9) value toInt is not a member of String
str.toInt



大伙看错误信息中有个关键的单词ambiguous(模糊不清的,模棱两可的),以上错误信息的大致意思是说:



隐式转换在这里不适用了,因为隐式转换出现了模糊不清的情况,这里有两个方法

一个在object Predef中有一个augmentString方法将x:String转成scala.collection.immutable.StringOps,

另一个在object TestDemo中有strToInt方法将str: String转成了Int

以上两个方法都可以将str: String转成了Int,所以隐式转换出现了模糊不清的情况



看完后估计有的小伙伴还是一头雾水,这到底是说的啥,这到底是为什么??好,那接下来我们就看看其中的究竟。



分析原因



分析String相关的源码



首先我们来看一段代码:



def main(args: Array[String]): Unit = {
val str:String = "20"
val i = str.toInt
println(i)
}



以上代码的正确编译运行,输出结果是Int类型的20



接下来我们按住键盘上的Ctrl,鼠标点击String查看String类型,发现在object Predef中有这么一段代码:



type String = java.lang.String



通过这段代码我们知道,Scala中的String其实是使用了java中的String。好了,那我们回顾以下java中的String有toInt()方法吗,熟悉Java的肯定立马就能回答:没有。对的,就是没有!!



String中没有toInt()方法,但是这里的str却可以使用toInt(),那说明str.toInt这里发生了隐式转换。没错,这里的str由String转换成了StringOps。这么转换的呢,我们上源码:



implicit def augmentString(x: String): StringOps = new StringOps(x)



在object Perdef中有一个隐式方法augmentString将String转换成了StringOps。接着我们再来看看StringOps的源码:



final class StringOps(override val repr: String) extends AnyVal with StringLike[String] {
override protected[this] def thisCollection: WrappedString =
new WrappedString(repr)
override protected[this] def toCollection(repr: String): WrappedString =
new WrappedString(repr)
/** Creates a string builder buffer as builder for this class */
override protected[this] def newBuilder = StringBuilder.newBuilder
override def apply(index: Int): Char = repr charAt index
override def slice(from: Int, until: Int): String = {
val start = if (from < 0) 0 else from
if (until <= start || start >= repr.length)
return ""
val end = if (until > length) length else until
repr.substring(start, end)
}
override def toString = repr
override def length = repr.length
def seq = new WrappedString(repr)
}



在StringOps中没有直接发现toInt()方法,不要慌,仔细看看StringOps继承了StringLike,接着我们看看StringLike源码:



trait StringLike[+Repr] extends Any with scala.collection.IndexedSeqOptimized[Char, Repr] with Ordered[String] {
//省略代码................
def toInt: Int = java.lang.Integer.parseInt(toString)
/**
* @throws java.lang.NumberFormatException - If the string does not contain a parsable long.
*/
def toLong: Long = java.lang.Long.parseLong(toString)
}



终于在StringLike中找到了toInt,因为StringOps继承了StringLike,所以StringOps也就有了toInt()。



再看刚才的隐式方法:



implicit def augmentString(x: String): StringOps = new StringOps(x)



隐式方法augmentString将String转换成了StringOps,所以:



def main(args: Array[String]): Unit = {
val str:String = "20"
val i = str.toInt // 底层代码实现:augmentString(str).toInt
println(i)
}



好了,通过以上代码分析,我们知道StringOps中存在toInt方法,所以通过隐式转换将str转成StringOps后就可以调用toInt了。



分析Int相关的源码



接下来,我们继续分析Int类的源码:



final abstract class Int private extends AnyVal {
//省略代码
def toInt: Int
def toLong: Long
//省略代码
}



通过查看Int的源码发现在Int中也存在一个toInt的方法,那么现在如果也存在一个将String转成Int的隐式方法,那么,String也能调用toInt了。分析到这里,我们渐渐的感觉到发现冲突产生的地方了,好的,我们马上来看看我们写的隐式方法的代码:



/**
* 定义的隐式方法
* 该方法的功能是将String转成Int
* @param str 需要转换的字符串
* @return 返回 Int
*/
implicit def strToInt(str:String):Int= {
/**
* 通过分析我们知道StringOps和Int都有toInt方法,所以
* str.toInt在这里有两个隐式函数都可以进行转换
* 1、使用scala.Predef 中的
* implicit def augmentString(x: String): StringOps = new StringOps(x)
* 所以str.toInt 就等价于 augmentString(str).toInt
*
* 2、使用自己定义这个隐式方法(递归调用)
* augmentString 就等价于 strToInt(str).toInt
*
* 好了,我们知道隐式转换只能匹配一个,不能有多个,
* 而这里str.toInt找到两个隐式转换都可以实现,所以出现了之前“模棱两可”的错误信息
*/
str.toInt
}



既然分析清楚了出现错误的原因,接下来我们就根据原因来看看如何解决吧。



解决方法的本质就是不要让toInt隐式转换的时候找到多个隐式方法出现“模棱两可”。



解决方案



方案一



使用Integer.parseInt(str)替换str.toInt。完整代码代码如下:



object TestDemo {
def main(args: Array[String]): Unit = {
val str:Int = "20"
println(str)
}
implicit def strToInt(str:String):Int= {
Integer.parseInt(str)
}
}



使用Integer.parseInt(str)替换str.toInt,没有使用str.toInt了,隐式转换自然就就没有了。



方案二



使用 new StringOps(str).toInt替换str.toInt。完整代码代码如下:



object TestDemo {
def main(args: Array[String]): Unit = {
val str:Int = "20"
println(str)
}
implicit def strToInt(str:String):Int= {
new StringOps(str).toInt
}
}



使用 new StringOps(str).toInt替换str.toInt。这里使用 new StringOps(str)显式调用了toInt,所以也没有隐式转换了。



方案三



将自己定义的隐式方法implicit def strToInt(str:String):Int的返回值省略,该方法就不能被递归调用了,那这时候str.toInt就只有implicit def augmentString(x: String): StringOps这一个隐式转换方法了,也解决了两个隐式方法的冲突问题。完整代码如下:



object TestDemo {
def main(args: Array[String]): Unit = {
val str:Int = "20" // error
println(str)
}
implicit def strToInt(str:String) = {
str.toInt
}
}



以上代码第四行val str:Int = "20"编译错误,错误信息如下:



Error:(8, 19) type mismatch;
found : String("20")
required: Int
Note: implicit method strToInt is not applicable here because it comes after the application point and it lacks an explicit result type
val str:Int = "20"



出现该错误的原因是:



隐式方法没有显式给出返回类型,必须位于应用点之前



所以正确代码如下:



object TestDemo {
implicit def strToInt(str:String) = {
str.toInt
}
def main(args: Array[String]): Unit = {
val str:Int = "20"
println(str)
}
}



好了,到这里我们的问题分析及解决方案就结束了,希望对大家有所帮助。



发布于: 2020 年 12 月 11 日阅读数: 17
用户头像

木子李G

关注

还未添加个人签名 2018.03.22 加入

在IT领域浪迹多年的码农。

评论

发布
暂无评论
Scala中String和Int隐式转换的问题分析