Kotlin进阶学习第三篇
读者学习本篇文章前请先学习之前的文章
Kotlin
系列已更新:
文章目录
扩展
拓展函数
拓展函数:对类的方法进行补充,动态的给类添加方法
笔者之前在百度贴吧做过一段时间iOS
,iOS
的开发语言OC
就是支持扩展函数的
Java
没有办法对系统类进行拓展,而Kotlin
可以对其进行拓展
比如:实现一个统计String
中字母个数的函数
若不借助拓展函数,定义StringUtil
并实现lettersCount
object StringUtil {
fun lettersCount(string: String): Int {
var count = 0;
for (char in string) {
if (char.isLetter()) count++
}
return count
}
}
调用如下:
StringUtil.lettersCount("ab2")
若借助拓展函数,可直接将lettersCount()
方法添加到String
类中,不必再创建StringUtil
创建如下:
创建String.kt,其职责就是对String
进行拓展,创建新的文件可使得拓展函数拥有全局访问域,不定义新文件也是可以的,郭霖大佬是建议定义新文件。
fun String.lettersCount(): Int {
var count = 0;
for (char in this) {
if (char.isLetter()) count++
}
return count
}
Kotlin
访问则可直接使用String
var count = "111asd".lettersCount()
Java
中调用需要如下方式:
StringKt.lettersCount("aaa");
拓展属性
Kotlin
不仅可以拓展函数,还可以拓展属性,在String.kt加入以下代码,则相当于给String
添加了一个值为10
的int
,get()
是固定语法
val String.value : Int get() = 10
Kotlin
中访问如下:
var value = "".value
Java
中访问如下:
int value = StringKt.getValue("aaa");
原理
查看String.kt
反编译的java
代码
public final class StringKt {
public static final int getValue(@NotNull String $this$value) {
//判断参数是否为空
Intrinsics.checkNotNullParameter($this$value, "$this$value");
return 10;
}
public static final int lettersCount(@NotNull String $this$lettersCount) {
//判断参数是否为空
Intrinsics.checkNotNullParameter($this$lettersCount, "$this$lettersCount");
int count = 0;
String var4 = $this$lettersCount;
int var5 = $this$lettersCount.length();
for(int var3 = 0; var3 < var5; ++var3) {
char var2 = var4.charAt(var3);
boolean var7 = false;
if (Character.isLetter(var2)) {
++count;
}
}
return count;
}
}
生成两个方法,两个方法都需参数,这也是为什么Java
调用必须传参的原因,Kotlin
不用传参是因为语法糖自动把调用者传递了
查看调用代码的反编译Java
代码
var count = "111asd".lettersCount() //反编译等价于int count = StringKt.lettersCount("111asd");
var value = "".value //反编译等价于 int value = StringKt.getValue("");
运算符重载
operator的使用
此节不太好理解,但很有趣
运算符重载是拓展运算符的功能,比如对象可使用运算符操作
val obj1 = Obj()
val obj2 = Obj()
val value = obj1 + obj2
运算符在编译时会替换成方法的调用,比如加法会替换成plus
方法,Kotlin
的每个运算符都有其对应的方法。
而有些对象运算没有实际意义,下面实现一个有意义运算的Money
类
拓展运算符需要使用operator关键字
Money
class Money(val value: Int) {
operator fun plus(money: Money): Money {
val sum = value + money.value
return Money(sum)
}
}
使用如下:
fun main() {
val money = Money(10) + Money(30)
println(money.value)
//输出 40
}
若想其可和int
直接相加,可重载plus
方法
operator fun plus(money: Int): Money {
val sum = value + money
return Money(sum)
}
调用此重载方法则可和int
相加
fun main() {
val money = Money(10) + 20
println(money.value)
//输出30
}
其上述不仅只有加法运算,Kotlin
所支持的可拓展运算符如下:
语法糖表达式 | 实际调用函数 |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.rem(b) |
a++ | a.inc() |
a– | a.dec() |
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a == b | a.equals(b) |
a > b | |
a < b | |
a >= b | a.compareTo(b) |
a <= b | |
a…b | a.rangeTo(b) |
a[b] | a.get(b) |
a[b] = c | a.set(b, c) |
a in b | b.contains(a) |
原理
Kotlin
的语法糖,在编译器可以把加号转换成相对应的方法调用
上述的代码:
fun main() {
val money = Money(10) + 20
println(money.value)
//输出30
}
反编译成java
代码为:
public static final void main() {
//调用对象的plus方法
Money money = (new Money(10)).plus(20);
int var1 = money.getValue();
System.out.println(var1);
}
高阶函数
定义与语法规则
一个函数接收另一个函数作为参数,或者返回值是另一个函数,那此函数则为高阶函数.
高级函数和Lambda紧密相连,之前都是调用Kotlin
内置的可传入Lambda
的函数,那么程序员怎么定义此种类型的函数呢。
在学习定义函数之前,首先要学会定义函数类型,语法如下:
(String, Int)-> Unit
解释上述规则,既然是函数类型,必不可少则是参数类型和返回类型,括号内的则为参数类型,多参数使用中间使用,隔开,->右边为返回值类型。
将此类型的参数加到方法上,则此方法就是高阶函数,如下:
fun test(func: (String, Int)->Unit) {
func("123",1)
}
示例
实现一个操作两个Int
的函数,可能是加法,可能是减法,等等,其没有实际意义,只是演示所用。
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int)->Unit) = operation(num1, num2)
声明加减方法:
fun plus(num1: Int, num2: Int) = num1 + num2
fun minus(num1: Int, num2: Int) = num1 - num2
调用如下:
fun main() {
println(num1AndNum2(3, 2, ::plus))
println(num1AndNum2(1, 2, ::minus))
}
//结果 5,-1
在Kotlin
中引用方法需使用::
若不定义新方法,则可使用Lambda
,借助Lambda
使用如下:
fun main() {
num1AndNum2(1, 2) { num1, num2 -> num1 - num2 }
num1AndNum2(3, 2) { num1, num2 -> num1 + num2 }
}
//结果 -1,5
模仿实现apply函数
拓展StringBuilder
类的方法,给其加上build
,使用效果与apply
相同
回想apply
,apply
内部可以对调用者本身进行操作,那么意味着在函数式中能拿到调用者的上下文,上述的定义方式显然不能拿到调用者的上下文,此时需借助其他语法,定义如下:
fun StringBuilder.bulid(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
在()前加上类名.,表示此函数类型是定义在此类中的,此时则可操作此对象,并返回
调用如下:
fun main() {
val rs = StringBuilder().bulid {
append("abc")
}
println(rs)
//结果 abc
}
原理
笔者发现反编译的java
文件有问题,此次分析直接分析下述代码的字节码 HelloWorld.kt
如下
fun main() {
var a = num1AndNum2(1, 2) { a,b -> a + b }
}
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int{
return operation(num1, num2)
}
完整的字节码如下:
// ================com/hbsd/demo/HelloWorldKt.class =================
// class version 52.0 (52)
// access flags 0x31
public final class com/hbsd/demo/HelloWorldKt {
// access flags 0x19
public final static main()V
L0
LINENUMBER 10 L0
ICONST_1
ICONST_2
GETSTATIC com/hbsd/demo/HelloWorldKt$main$a$1.INSTANCE : Lcom/hbsd/demo/HelloWorldKt$main$a$1;
CHECKCAST kotlin/jvm/functions/Function2
INVOKESTATIC com/hbsd/demo/HelloWorldKt.num1AndNum2 (IILkotlin/jvm/functions/Function2;)I
ISTORE 0
L1
LINENUMBER 13 L1
RETURN
L2
LOCALVARIABLE a I L1 L2 0
MAXSTACK = 3
MAXLOCALS = 1
// access flags 0x1009
public static synthetic main([Ljava/lang/String;)V
INVOKESTATIC com/hbsd/demo/HelloWorldKt.main ()V
RETURN
MAXSTACK = 0
MAXLOCALS = 1
// access flags 0x19
// signature (IILkotlin/jvm/functions/Function2<-Ljava/lang/Integer;-Ljava/lang/Integer;Ljava/lang/Integer;>;)I
// declaration: int num1AndNum2(int, int, kotlin.jvm.functions.Function2<? super java.lang.Integer, ? super java.lang.Integer, java.lang.Integer>)
public final static num1AndNum2(IILkotlin/jvm/functions/Function2;)I
// annotable parameter count: 3 (visible)
// annotable parameter count: 3 (invisible)
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 2
L0
ALOAD 2
LDC "operation"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 17 L1
ALOAD 2
ILOAD 0
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ILOAD 1
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
INVOKEINTERFACE kotlin/jvm/functions/Function2.invoke (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; (itf)
CHECKCAST java/lang/Number
INVOKEVIRTUAL java/lang/Number.intValue ()I
IRETURN
L2
LOCALVARIABLE num1 I L0 L2 0
LOCALVARIABLE num2 I L0 L2 1
LOCALVARIABLE operation Lkotlin/jvm/functions/Function2; L0 L2 2
MAXSTACK = 3
MAXLOCALS = 3
@Lkotlin/Metadata;(mv={1, 5, 1}, k=2, d1={"\u0000\u0016\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0008\n\u0002\u0008\u0003\n\u0002\u0018\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001\u001a0\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u00032\u0018\u0010\u0006\u001a\u0014\u0012\u0004\u0012\u00020\u0003\u0012\u0004\u0012\u00020\u0003\u0012\u0004\u0012\u00020\u00030\u0007\u00a8\u0006\u0008"}, d2={"main", "", "num1AndNum2", "", "num1", "num2", "operation", "Lkotlin/Function2;", "app_debug"})
// access flags 0x18
final static INNERCLASS com/hbsd/demo/HelloWorldKt$main$a$1 null null
// compiled from: HelloWorld.kt
}
// ================com/hbsd/demo/HelloWorldKt$main$a$1.class =================
// class version 52.0 (52)
// access flags 0x30
// signature Lkotlin/jvm/internal/Lambda;Lkotlin/jvm/functions/Function2<Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;>;
// declaration: com/hbsd/demo/HelloWorldKt$main$a$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function2<java.lang.Integer, java.lang.Integer, java.lang.Integer>
final class com/hbsd/demo/HelloWorldKt$main$a$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2 {
// access flags 0x1041
public synthetic bridge invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
ALOAD 0
ALOAD 1
CHECKCAST java/lang/Number
INVOKEVIRTUAL java/lang/Number.intValue ()I
ALOAD 2
CHECKCAST java/lang/Number
INVOKEVIRTUAL java/lang/Number.intValue ()I
INVOKEVIRTUAL com/hbsd/demo/HelloWorldKt$main$a$1.invoke (II)I
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ARETURN
MAXSTACK = 3
MAXLOCALS = 3
// access flags 0x11
public final invoke(II)I
// annotable parameter count: 2 (visible)
// annotable parameter count: 2 (invisible)
L0
LINENUMBER 11 L0
ILOAD 1
ILOAD 2
IADD
L1
IRETURN
L2
LOCALVARIABLE this Lcom/hbsd/demo/HelloWorldKt$main$a$1; L0 L2 0
LOCALVARIABLE a I L0 L2 1
LOCALVARIABLE b I L0 L2 2
MAXSTACK = 2
MAXLOCALS = 3
// access flags 0x0
<init>()V
ALOAD 0
ICONST_2
INVOKESPECIAL kotlin/jvm/internal/Lambda.<init> (I)V
RETURN
MAXSTACK = 2
MAXLOCALS = 1
// access flags 0x19
public final static Lcom/hbsd/demo/HelloWorldKt$main$a$1; INSTANCE
// access flags 0x8
static <clinit>()V
NEW com/hbsd/demo/HelloWorldKt$main$a$1
DUP
INVOKESPECIAL com/hbsd/demo/HelloWorldKt$main$a$1.<init> ()V
PUTSTATIC com/hbsd/demo/HelloWorldKt$main$a$1.INSTANCE : Lcom/hbsd/demo/HelloWorldKt$main$a$1;
RETURN
MAXSTACK = 2
MAXLOCALS = 0
@Lkotlin/Metadata;(mv={1, 5, 1}, k=3, d1={"\u0000\n\n\u0000\n\u0002\u0010\u0008\n\u0002\u0008\u0003\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u00012\u0006\u0010\u0003\u001a\u00020\u0001H\n\u00a2\u0006\u0002\u0008\u0004"}, d2={"<anonymous>", "", "a", "b", "invoke"})
OUTERCLASS com/hbsd/demo/HelloWorldKt main ()V
// access flags 0x18
final static INNERCLASS com/hbsd/demo/HelloWorldKt$main$a$1 null null
// compiled from: HelloWorld.kt
}
// ================META-INF/app_debug.kotlin_module =================
com.hbsd.demoHelloWorldKt" *
分析num1AndNum2(1 2) { a,b -> a + b }
的字节码
LINENUMBER 10 L0
ICONST_1
ICONST_2
GETSTATIC com/hbsd/demo/HelloWorldKt$main$a$1.INSTANCE : Lcom/hbsd/demo/HelloWorldKt$main$a$1;
CHECKCAST kotlin/jvm/functions/Function2
INVOKESTATIC com/hbsd/demo/HelloWorldKt.num1AndNum2 (IILkotlin/jvm/functions/Function2;)I
ISTORE 0
num1AndNum2方法需传递三个参数1, 2, HelloWorldKt$main$a$1.INSTANCE
num1AndNum2方法的声明字节码如下
// access flags 0x19
// signature (IILkotlin/jvm/functions/Function2<-Ljava/lang/Integer;-Ljava/lang/Integer;Ljava/lang/Integer;>;)I
// declaration: int num1AndNum2(int, int, kotlin.jvm.functions.Function2<? super java.lang.Integer, ? super java.lang.Integer, java.lang.Integer>)
public final static num1AndNum2(IILkotlin/jvm/functions/Function2;)I
declaration
表示此函数的参数,需要三个参数,两个int
,一个接口对象,这个接口对象是num1AndNum2(1, 2) { a,b -> a + b }
传递的HelloWorldKt$main$a$1.INSTANCE
operation(num1, num2)
此行代码的字节码如下(num1AndNum2
方法的片段):
LINENUMBER 17 L1
ALOAD 2
ILOAD 0
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ILOAD 1
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
INVOKEINTERFACE kotlin/jvm/functions/Function2.invoke (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; (itf)
CHECKCAST java/lang/Number
INVOKEVIRTUAL java/lang/Number.intValue ()I
IRETURN
调用传入参数的valueof
拿到Integer
对象,最终调用第三个参数的invoke
方法并将上述两个Integer
对象传递过去,下面分析第三个参数是谁
在整个文件的字节码中还存在一个内部类
final class com/hbsd/demo/HelloWorldKt$main$a$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2
而第三个参数的声明如下
public final static Lcom/hbsd/demo/HelloWorldKt$main$a$1; INSTANCE
则operation(num1, num2)
调用的invoke
则是HelloWorldKt$main$a$1
的invoke
在HelloWorldKt$main$a$1
存在两个invoke
方法,此时执行的为需要两个Object
的方法,此方法关键代码如下:
ALOAD 0
ALOAD 1
CHECKCAST java/lang/Number
INVOKEVIRTUAL java/lang/Number.intValue ()I
ALOAD 2
CHECKCAST java/lang/Number
INVOKEVIRTUAL java/lang/Number.intValue ()I
INVOKEVIRTUAL com/hbsd/demo/HelloWorldKt$main$a$1.invoke (II)I
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
获取两个Integer
对象的int值,执行invoke (II)
方法,最终将结果的int
包装成Integer
返回
invoke(II)
中的关键代码
LINENUMBER 11 L0
ILOAD 1
ILOAD 2
IADD
读取两个值,进行加法操作
总结:Lambda
会帮助生成一个内部类,此内部类是继承自kotlin/jvm/functions/Functionx
,x
代表Lambda
需要几个参数,且内部包含此类的静态变量INSTANCE
,类内部还会生成invoke
方法,Lambda
的逻辑会copy
到invoke
。
当我们执行高阶函数时,Lambda
参数会被编译成上述INSTANCE
对象,高阶函数内部又会去调用此对象的invoke
方法,invoke
内部又是Lambda
中的逻辑,因此Lambda
就被执行了。
内联函数
inline
上述可以看到使用Lambda
会生成内部类,肯定会造成额外内存和性能开销
Kotlin
提供了某种机制可将Lambda
表达式的弊端去除
fun main() {
var a = num1AndNum2(1, 2) { a,b -> a + b }
}
inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
return operation(num1, num2)
}
查看其字节码,其反编译的Java
代码是正确的,查看其对应的Java
代码:
public static final void main() {
int num1$iv = 1;
int num2$iv = 2;
int var10000 = num1$iv + num2$iv;
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
将num1AndNum2(1, 2) { a,b -> a + b }
直接替换成a + b
,避免了内部类的生成,无不感叹Kotlin
强大的编译器。
Java
中也存在许多内联优化,比如:
java
早期final
修饰的方法会直接被编译器编译成嵌套方法
final
变量在编译时就直接替换
若方法的实现很简单,也会进行方法内联,避免栈帧的生成
但其也存在缺点,调用者越多,复制品越多,越消耗空间。
noinline
假设一个函数有多个函数型参数,若加上inline
会使全部的参数参与内联,若某些不想参与内联呢,则需要在参数前加上noinline
关键字.
使用如下:
inline fun test(lambda1: () -> Unit, noinline lambda2: () -> Unit) {
lambda1()
lambda2()
}
此时参数二将不参与内联
内联不是好处多多吗,为何有时候又不需要内联了吗?
内联的函数被代码替换,其没有真正的参数含义,只能传递给另一个内联函数,而非内联的函数是一个真正的参数。
内联函数可使用return
,非内联只能局部return
内联时return
会跑到调用者的函数中,return
结束的是调用者的函数,非内联上述分析字节码可知结束的是内部类中的方法
crossinine
内联还存在以下问题:
翻译过来为,block
可能存在不是本地的return
内联函数的return
结束的是调用者,而这么写就会导致return
结束的是Runnable
的run
函数,从而导致冲突。
解决方式有两个:
去掉inline
,不使用内联,return@runRunnable
结束的还是内部类中的invoke
函数
加上crossinine
关键字
inline fun runRunnable(crossinline block : () -> Unit) {
Runnable() {
block()
}.run()
}
crossinine
像一个契约,保证Lambda
一定不使用return
,但是仍可使用return@runRunnable
,其除了return
的特性有些变化,保留了内联函数的其他特性。
高阶函数的最佳实践
上述中我们学习了高阶函数的使用和原理,本节对其使用进行实践:
SharedPreferences使用优化
熟悉安卓的同学都了解过SharedPreferences
,其帮助我们持久化键值对数据,使用如下:
//传入文件名和权限,拿到edit对象
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
//添加数据
editor.putString("name", "zjm")
editor.putInt("name", 21)
editor.putBoolean("isBoy", true)
//提交数据
editor.apply()
使用高级函数可对其进行优化:
fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}
使用如下:
getSharedPreferences("data", Context.MODE_PRIVATE).open {
putString("name", "zjm")
putInt("name", 21)
putBoolean("isBoy", true)
}
其实KTX
已经支持此函数,其函数名为edit()
,我们只是对其进行仿照实现
KTX
的导入如下(在创建项目时已经自动导入):
implementation 'androidx.core:core-ktx:1.3.2'
ContentValues使用优化
了解SQLiteDB
的同学肯定知道ContentValues
,不知道也没有关系,只需要知道使用它需要下面方式:
//初始化ContentValues
val values = ContentValues()
//赋值
values.put("name", "zjm")
values.put("age", 21)
values.put("isBoy", true)
//插入数据库,第一个参数为表名,第二个参数为若第三个参数没有此列的默认值,添加的数据
db.insert("person", null, values)
在入门篇中我们了解了mapOf
的用法,其传入key to value
的方式即可添加一个Map
元素,其实在底层A to B
这种语法会创建一个Pair
对象,其接收一个键值对。
依靠Pair
即可对上述代码进行优化:
声明一个顶层方法cvOf
:
fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
val cv = ContentValues()
for (pair in pairs) {
val key = pair.first
val value = pair.second
when(value) {
is Int -> cv.put(key, value)
is Long -> cv.put(key, value)
is Short -> cv.put(key, value)
is Float -> cv.put(key, value)
is Double -> cv.put(key, value)
is Boolean -> cv.put(key, value)
is String -> cv.put(key, value)
is Byte -> cv.put(key, value)
is ByteArray -> cv.put(key, value)
null -> cv.putNull(key)
}
}
return cv
}
为什么需要借助when
呢,直接cv.put(key, value)
可以吗,不可以,直接使用会越界,put
并不接收Any
参数,when
如果命中is
,会触发类型推导自动帮助类型转换
上述使用并没有借助高阶函数,下面使用apply
优化,apply
会返回调用者,在内部可直接调用调用者的函数,这样我们就能省略很多代码
fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
for (pair in pairs) {
val key = pair.first
val value = pair.second
when(value) {
is Int -> put(key, value)
is Long -> put(key, value)
is Short -> put(key, value)
is Float -> put(key, value)
is Double -> put(key, value)
is Boolean -> put(key, value)
is String -> put(key, value)
is Byte -> put(key, value)
is ByteArray -> put(key, value)
null -> putNull(key)
}
}
}
使用如下:
val values = cvOf("a" to 1, "b" to "c", "d" to true)
db.insert("person", null, values)
同样的KTX
也给我们提供了此函数,函数名为contentValueOf()
到这本篇结束
✨ 原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下
👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!} 点赞,你的认可是我创作的动力!
⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!} 收藏,你的青睐是我努力的方向!
✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!