Cris 的 Scala 笔记整理(九):面向对象高级
9. 面向对象高级
9.1 静态属性和静态方法
① 回顾 Java 的静态概念
public static 返回值类型 方法名(参数列表) {方法体}
Java
中静态方法并不是通过对象调用的,而是通过类对象调用的,所以静态操作并不是面向对象的
② Scala 中静态的概念-伴生对象
Scala 语言是完全面向对象(万物皆对象)的语言,所以并没有静态的操作(即在 Scala
中没有静态的概念)。但是为了能够和 Java
语言交互(因为 Java
中有静态概念),就产生了一种特殊的对象来模拟类对象,我们称之为类的伴生对象。这个类的所有静态内容都可以放置在它的伴生对象中声明和调用
③ Scala 伴生类和伴生对象示例代码
object Demo4 {def main(args: Array[String]): Unit = {println(Woman.sex) // 底层调用的是 Woman$.Module$.sex()Woman.shopping() // 底层调用的是 Woman$.Module$.shopping()}
}/*** 当我们吧同名的 class 和 object 放在同一个 .scala 文件中,class xxx 就是伴生类,object xxx 就是伴生对象* 通常开发中会将成员属性/方法写在伴生类中,将静态属性/方法写在伴生对象中* class xxx 编译后的文件 xxx.class 会有对 object xxx 的属性或方法操作* 而 object xxx 编译后会生成 xxx$.class ;也就是说 class xxx 编译后的文件会组合 object xxx 编译后的 xxx.class*/
class Woman {var name = "woman"
}object Woman {val sex = 'F'def shopping(): Unit = {println("女人天生爱购物")}
}
复制代码
输出
我们将字节码文件反编译后再看看原理
说明
Woman$
字节码文件先生成Woman$
类型的静态对象Woman
类中会自动生成object Woman
中定义的静态方法和静态属性对应的getter/setter
方法,这些方法的实质都是通过对Woman$
静态对象的相应方法调用
图示
小结
Scala
中伴生对象采用object
关键字修饰,伴生对象中声明的都是静态内容,可以通过伴生对象名称直接调用伴生对象对应的伴生类,他们的名字必须一致
从语法的角度,伴生对象其实就是类的静态方法和静态属性的集合
从编译的角度,伴生对象会生成一个静态的实例,由伴生类中的静态方法去调用这个静态实例的方法
伴生对象和伴生类的声明需要放在同一个
Scala
源文件中如果
class xxx
独立存在一个Scala
源文件中,那么xxx
就只是一个没有静态内容的类;如果object xxx
独立存在一个Scala
源文件中,那么xxx
会编译生成一个xxx.class
和xxx$.class
,object xxx
中声明的方法和属性可以直接通过xxx.属性
和xxx.方法
直接调用示例代码
object Demo4 {def main(args: Array[String]): Unit = {add()}def add(): Unit = {println("abc")}
}
复制代码
- 如果
Scala
源文件有一个伴生类及其对应的伴生对象,IDEA
中会如下显示
object Girl {}
class Girl{}
复制代码
④ 练习:小孩子玩游戏
object Demo5 {def main(args: Array[String]): Unit = {val c1 = new Child("cris")val c2 = new Child("james")val c3 = new Child("申惠善")Child.add(c1)Child.add(c2)Child.add(c3)Child.showNum()}
}class Child(var name: String) {}object Child {var num = 0def add(c: Child): Unit = {println(s"${c.name} 加入了游戏")num += 1}def showNum(): Unit = {println(s"当前共有 $num 个小孩子再玩游戏")}
}
复制代码
输出
总结:可以像类一样使用 object
,只需要记住 object
中定义的内容都是静态类型的即可;object
还可以当做工具类使用,只需要定义工具方法即可
⑤ apply 方法
通过 object
的 apply
方法可以直接使用 类名(实参)
的方式来生成新的对象
object Practice {def main(args: Array[String]): Unit = {// val bag1 = new Bag("LV")// apply 方法被调用...// Bag 的主构造被调用val bag = Bag("Channel")}
}class Bag(var name: String) {println("Bag 的主构造被调用")
}object Bag {def apply(name: String): Bag = {println("apply 方法被调用...")new Bag(name)}
}
复制代码
⑥ 练习
- 将程序的运行参数进行倒叙打印,并且参数之间使用
-
隔开
println(args.toBuffer)private val str: String = args.reverse.mkString("-")println(str)
}
复制代码
执行如下
编写一个扑克牌 4 种花色的枚举,让其
toString
方法分别返回♣
,♦
,♥
,♠
,并实现一个函数,检查某张牌的花色是否为红色先试试
type
关键字
object Exer extends App {// type 相当于是给数据类型起别名type MyString = Stringval name: MyString = "cris"println(name) // crisprintln(name.getClass.getName) // java.lang.String
}
复制代码
然后再来完成练习
object Exer extends App {println(Suits) // ♠,♣,♥,♦println(Suits.isDiamond(Suits.Diamond)) // trueprintln(Suits.isDiamond(Suits.Heart)) //false
}object Suits extends Enumeration {type Suits = Valueval Spade = Value("♠")val Club = Value("♣")val Heart = Value("♥")val Diamond = Value("♦")override def toString(): String = Suits.values.mkString(",")def isDiamond(s: Suits): Boolean = s == Diamond
}
复制代码
9.2 单例
单例对象是指:使用单例设计模式保证在整个的软件系统中,某个类只能存在一个对象实例
① 回顾 Java 单例对象
在 Java
中,创建单例对象分为饿汉式(加载类信息就创建单例对象,缺点是可能造成资源浪费,优点是线程安全)和懒汉式(使用时再创建单例对象)
通过静态内部类实现懒汉式单例:
构造器私有化
类的内部创建对象
向外暴露一个静态的公共方法
代码实现
public class Main {public static void main(String[] args) {Single instance = Single.getInstance();Single instance1 = Single.getInstance();
// trueSystem.out.println("(instance1==instance) = " + (instance1 == instance));}
}class Single {private Single() {}/*** 静态内部类:1. 使用时才加载;2. 加载时,不会中断(保证线程安全)*/private static class SingleInstance {private static final Single INSTANCE = new Single();}public static Single getInstance() {return SingleInstance.INSTANCE;}
}
复制代码
② Scala 中的单例模式
Scala
中实现单例异常简单~
只需要写一个 object
,就相当于创建了一个对应的单例对象
object SingletonDemo {def main(args: Array[String]): Unit = {}
}object Singleton {val name = "singleton"def init(): Unit = {println("init...")}
}
复制代码
看看反编译后的代码
图示:
9.3 特质(重点)
① 回顾 Java 的接口
在Java中, 一个类可以实现多个接口。
在Java中,接口之间支持多继承
接口中属性都是常量
接口中的方法都是抽象的(Java 1.8 之前)
② Scala 的特质(trait)
简介
从面向对象来看,接口并不属于面向对象的范畴,Scala
是纯面向对象的语言,在 Scala
中,没有接口
Scala
语言中,采用特质 trait
(特征)来代替接口的概念,也就是说,多个类具有相同的特征(特征)时,就可以将这个特质(特征)独立出来,采用关键字 trait
声明
特质的声明
trait 特质名{trait 体
}
复制代码
特质的基础语法
说明
类和特质关系,使用继承的关系;因为
Scala
的特质,有传统interface
特点,同时又有抽象类特点当一个类去继承特质时,第一个连接词是
extends
,后面是with
如果一个类在继承特质和父类时,应当把父类写在
extends
后
③ trait 传统使用案例
可以将 trait
当做传统的接口使用
请根据以下图示,使用 trait
完成需求
代码如下:
object Demo {def main(args: Array[String]): Unit = {val c = new Cval f = new Fc.getConnection()f.getConnection()}
}trait Connection {def getConnection()
}class A {}class B extends A {}class C extends A with Connection {override def getConnection(): Unit = println("连接 MySQL 数据库")
}class D {}class E extends D {}class F extends D with Connection {override def getConnection(): Unit = println("连接 HBase 数据库")
}
复制代码
我们看看反编译后的代码
代码说明
如果我们创建了一个
trait
, 该trait
只有抽象的方法,那么在底层就只会生成一个interface
继承了
trait
的类,必须实现trait
的抽象方法(这点和Java
一样)
特质的进一步说明
Scala
提供了特质(trait
),特质可以同时拥有抽象方法和具体方法,一个类可以实现/继承多个特质代码演示
object Demo2 {def main(args: Array[String]): Unit = {val account: Account = new BankAccountaccount.checkaccount.info}}trait Account {def checkdef info: Unit = {println("account info")}
}class BankAccount extends Account {override def check: Unit = {println("需要提供银行账号和密码进行验证")}
}
复制代码
看看编译后的代码,Scala
是如何实现的?
再看看 trait
的编译图示
- 特质中没有实现的方法就是抽象方法;类通过
extends
继承特质,通过with
可以继承多个特质;也可以针对特质生成匿名类
val account2 = new Account {override def check(): Unit = {println("需要提供指纹进行验证")}
}
// 需要提供指纹进行验证
account2.check()
复制代码
- 所有的
Java
接口都可以当做Scala
特质使用
class MyClass extends Serializable{}
复制代码
实质上这个 Serializable
特质继承了 Java
的Serializable
④ 特质的动态混入(MixIn)机制
首先,Scala
允许匿名子类动态的增加方法(Java
同样也支持),示例代码如下
object MixInDemo {def main(args: Array[String]): Unit = {val car = new Car {def run(): Unit = {println("什么路都能开")}}// 什么路都能开car.run()}}class Car {}
复制代码
然后看看 Scala
的特质混入机制如何实现的
object MixInDemo {def main(args: Array[String]): Unit = {val car = new Car with Transform {override def speed(): Unit = println("加速300km/h")}car.speed()}
}trait Transform {def speed()
}class Car {}
复制代码
实质还是通过匿名子类的方式来实现的,看看编译后的字节码
总结
- 除了可以在类声明时继承特质以外,还可以在构建对象时混入特质,扩展目标类的功能
- 此种方式也可以应用于对抽象类功能进行扩展、
object MixInDemo {def main(args: Array[String]): Unit = {val p = new Plain with Transformp.speed()}
}
abstract class Plaintrait Transform {def speed(): Unit = println("加速")
}
复制代码
动态混入是
Scala
特有的方式(Java
没有动态混入),可在不修改类声明/定义的情况下,扩展类的功能,非常的灵活,耦合性低动态混入可以在不影响原有的继承关系的基础上,给指定的类扩展功能
思考:如果抽象类中有没有实现的方法,如何动态混入特质?
动态混入特质的同时实现抽象方法即可
问题:Scala 中创建对象一共 有几种方式?
- new
- apply
- 动态混入
- 匿名子类
⑤ 叠加特质
构建对象的同时如果混入多个特质,称之为叠加特质
特质声明顺序从左到右,方法执行顺序从右到左
示例如下
请根据以下图示写出一个关于叠加特质的案例
object MixInDemo {def main(args: Array[String]): Unit = {// 混入对象的构建顺序和特质声明顺序一致val e = new EE with CC with DD}
}trait AA {println("AAAA")def func()
}trait BB extends AA {println("BBB")override def func(): Unit = println("BBB's func")}trait CC extends BB {println("CCC")override def func(): Unit = {println("CC's func")super.func()}
}trait DD extends BB {println("DD")override def func(): Unit = {println("CC's func")super.func()}
}class EE
复制代码
输出
如果我们调用 e
的 func
方法
输出
总结
当构建一个混入对象时,
构建顺序和 声明的顺序一致(从左到右)
,机制和类的继承一致执行方法时,
是从右到左执行(按特质)
Scala
中特质的方法中如果调用super
,并不是表示调用父特质的方法,而是向前面(左边)继续查找特质,如果找不到,才会去父特质查找
叠加特质细节
特质声明顺序从左到右。
Scala
在执行叠加对象的方法时,会首先从后面的特质(从右向左)开始执行Scala
中特质中如果调用super
,并不是表示调用父特质的方法,而是向前面(左边)继续查找特质,如果找不到,才会去父特质查找如果想要调用具体特质的方法,可以指定:
super[特质].xxx(…)
;其中的泛型必须是该特质的直接超类类型
示例代码
trait DD extends BB {println("DD")override def func(): Unit = {println("DD's func")super[BB].func()}
}def main(args: Array[String]): Unit = {val e = new EE with CC with DDe.func()}
复制代码
此时输出如下
⑥ 特质中重写抽象方法
如果执行以下代码
就会报错
修改方式如下:
- 去掉
super.xxx
- 因为调用父特质的抽象方法,实际使用时,却没有具体的实现,就无法执行成功,为了避免这种情况的发生,可以抽象重写方法,这样在使用时,可能其他特质实现了这个抽象方法
trait A2 {def func()
}trait B2 extends A2 {abstract override def func(): Unit = {println("B2")super.func()}
}
复制代码
如此重写就不会再报错了(相当诡异的语法~)
改造之前的代码
object Main3 {def main(args: Array[String]): Unit = {val e = new E2 with C2 with B2e.func()}
}trait A2 {println("A2")def func()
}trait B2 extends A2 {println("B2")abstract override def func(): Unit = {println("B2's func")super.func()}
}trait C2 extends A2 {println("C2")override def func(): Unit = {println("C2's func")}
}class E2
复制代码
解释:
对象 e
执行 func()
将会从 B2
开始执行对应的 func()
,B2
的 func()
将会调用 super.func()
,指向的就是 C2
的 func()
;而 C2
的 func()
是对父特质 A2
的抽象方法 func()
的完整实现
示意图
⑦ 富接口
既有抽象方法,又有非抽象方法的特质
trait A2 {def func()def func2(): Unit = println("A2")
}
复制代码
⑧ 特质的字段
解释:特质中可以定义字段,如果初始化该字段就成为具体字段;如果不初始化就是抽象字段。混入该特质的类具有该字段,字段不是继承,而是直接加入类,成为自己的字段
object Main3 {def main(args: Array[String]): Unit = {val e2 = new E2 with D2println(e2.name) // cris}
}trait C2 {var name: String
}trait D2 extends C2 {var name = "cris"
}class E2
复制代码
反编译后的代码
⑨ 多个特质的初始化顺序
我们除了在创建对象的时候使用 with
来继承特质,还可以在声明类的时候使用 with
来继承特质
一个类又继承超类又继承多个特质的时候,请问初始化该类的顺序?
object Main3 {def main(args: Array[String]): Unit = {val e2 = new E2}
}
trait A2{println("A2")
}
trait B2 extends A2{println("B2")
}
trait C2 extends A2{println("C2")
}
class D2{println("D2")
}
class E2 extends D2 with C2 with B2{println("E2")
}
复制代码
输出
总结
先初始化超类
超类初始化完毕后按照顺序初始化特质
初始化特质前先初始化该特质的父特质
多个特质具有相同的父特质只初始化一次
最后执行子类的初始化代码
如果是动态混入,那么类的初始化顺序又是怎么样的?
class F extends D2 {println("F")
}def main(args: Array[String]): Unit = {var f = new F with C2 with B2}
复制代码
输出
总结
- 先初始化超类
- 然后初始化子类
- 最后初始化特质,初始化顺序和上面一致
两种初始化流程的理解
- 声明类并继承特质的方式可以理解为在初始化对象之前需要初始化必须的所有超类和特质
- 创建类再继承特质的方式可以理解为混入特质之前就已经创建好了匿名类
⑩ 扩展类的特质
- 特质可以继承类,以用来拓展该类的一些功能
object Main4 {def main(args: Array[String]): Unit = {val e = new MyException {}e.func()}
}trait MyException extends Exception {def func(): Unit = {println(getMessage) // getMessage 方法来自 Exception 类(java.lang.Exception)}
}
复制代码
- 所有混入扩展特质的类,会自动成为那个特质所继承的超类的子类
object Main4 {def main(args: Array[String]): Unit = {val e = new MyException2println(e.getMessage) // this is my exception!}
}trait MyException extends Exception {def func(): Unit = {println(getMessage) // getMessage 方法来自 Exception 类(java.lang.Exception)}
}// MyException2 就是 Exception 的子类,所以可以重写 Exception 的 getMessage 方法
class MyException2 extends MyException {override def getMessage: String = "this is my exception!"
}
复制代码
如果混入该特质的类,已经继承了另一个类(
A
类),则要求A
类是特质超类的子类,否则就会出现了多继承现象
,发生错误为了便于理解,修改上面的代码
class A {}class MyException2 extends A with MyException {override def getMessage: String = "this is my exception!"
}
复制代码
执行出错
如果将 A
继承 Exception
class A extends Exception{}
复制代码
那么执行成功~
⑩① 访问特质自身类型
主要是为了解决特质的循环依赖问题,同时可以确保特质在不扩展某个类的情况下,依然可以做到限制混入该特质的类的类型
示例代码
执行代码如下
def main(args: Array[String]): Unit = {val e = new MyException2println(e.getMessage) // exception
}
复制代码
9.4 嵌套类
在 Scala
中,你几乎可以在任何语法结构中内嵌任何语法结构。如在类中可以再定义一个类,这样的类是嵌套类,其他语法结构也是一样
嵌套类类似于 Java
中的内部类
① 回顾 Java 的内部类
Java 中,一个类的内部又完整的嵌套了另一个类,这样的结构称为嵌套类;其中被嵌套的类称为内部类,嵌套的类称为外部类。
内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系
Java 内部类的分类
从定义在外部类的成员位置上区分:
- 成员内部类(无 static 修饰)
- 静态内部类(有 static 修饰)
从定义在外部类的局部位置上区分:
- 局部内部类(有类名)
- 匿名内部类(无类名)
② Scala 的内部类
示例代码
object InnerClassDemo {def main(args: Array[String]): Unit = {// 创建成员内部类实例 val outer1 = new Outerval inner1 = new outer1.Inner// 创建静态内部类实例val staticInner = new Outer.StaticInnerClass}}class Outer {
// 成员内部类class Inner {}}object Outer {
// 静态内部类class StaticInnerClass {}}
复制代码
内部类访问外部类的属性
- 内部类如果想要访问外部类的属性,可以通过外部类对象访问 即:
外部类名.this.属性名
示例代码
def main(args: Array[String]): Unit = {val outer1 = new Outerval inner1 = new outer1.Innerinner1.func() // name is cris, age is 0}
}class Outer {private var name = "cris"val age = 0class Inner {def func(): Unit = {println(s"name is ${Outer.this.name}, age is ${Outer.this.age}")}}
}
复制代码
- 内部类如果想要访问外部类的属性,也可以通过外部类别名访问 即:
外部类名别名.属性名
;关键是外部类属性必须放在别名之后再定义
示例代码
object InnerClassDemo {def main(args: Array[String]): Unit = {val outer1 = new Outerval inner1 = new outer1.Innerinner1.func() // name is 大帅, age is 12}
}class Outer {MyOuter =>class Inner {def func(): Unit = {println(s"name is ${MyOuter.name}, age is ${MyOuter.age}")}}private var name = "大帅"val age = 12
}
复制代码
③ 类型投影
修改上面的代码如下:
object InnerClassDemo {def main(args: Array[String]): Unit = {val outer1 = new Outerval inner1 = new outer1.Innerval outer2 = new Outerval inner2 = new outer2.Innerinner1.func(inner1) // name is 大帅, age is 12inner1.func(inner2) // 报错!原因就是因为 Scala 中成员内部类的类型默认是和外部类对象关联}
}class Outer {MyOuter =>class Inner {// 这里参数类型虽然是 Inner,实质上是 Outer.Inner,其中 Outer 指定生成当前内部类的外部类对象def func(i: Inner): Unit = {println(s"name is ${MyOuter.name}, age is ${MyOuter.age}")}}private var name = "大帅"val age = 12
}
复制代码
解决方式需要使用 Scala
的类型投影
类型投影是指:在方法声明上,如果使用 外部类#内部类 的方式,表示忽略内部类的对象关系,等同于 Java
中内部类的语法操作,我们将这种方式称之为类型投影(即:忽略对象的创建方式,只考虑类型)
④ 练习
java.awt.Rectangle类有两个很有用的方法translate和grow,但可惜的是像java.awt.geom.Ellipse2D这样的类没有。在Scala中,你可以解决掉这个问题。定义一个RenctangleLike特质,加入具体的translate和grow方法。提供任何你需要用来实现的抽象方法,以便你可以像如下代码这样混入该特质:
val egg = new java.awt.geom.Ellipse2D.Double(5,10,20,30) with RectangleLike
egg.translate(10,-10)
egg.grow(10,20)
复制代码
实现代码如下:
object Practice2 {def main(args: Array[String]): Unit = {val egg = new Ellipse2D.Double(5, 10, 20, 30) with RectangleLikeegg.translate(1, 2) // 3.0egg.grow(2, 4) // (2.0,4.0)}}trait RectangleLike {this: java.awt.geom.Ellipse2D.Double =>def translate(a: Double, b: Double): Unit = {println(a + b)}def grow(a: Double, b: Double): Unit = {println(a, b)}
}
复制代码
Cris 的 Scala 笔记整理(九):面向对象高级相关推荐
- Cris 的 Scala 笔记整理(七):面向对象
7. 面向对象(重点) 7.1 Scala 面向对象基础 类 [修饰符] class 类名 { 类体 } scala语法中,类并不声明为public,所有这些类都具有公有可见性(即默认就是public ...
- Cris 的 Scala 笔记整理(八):面向对象中级-封装
封装 从数据的角度:封装 (encapsulation) 就是把抽象出的数据和对数据的操作封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作(成员方法),才能对数据进行操作 从模 ...
- 笔记整理3----Java语言高级(三)11 综合练习+12 面向对象-static变量 与 代码块+13 面向对象-继承与抽象类+14 面向对象-接口与多态+15 面向对象-包修饰符
11 综合练习+12 面向对象-static变量 与 代码块+13 面向对象-继承与抽象类+14 面向对象-接口与多态+15 面向对象-包&修饰符 第11天 综合练习 今日内容介绍 综合练习 ...
- Cris 的 Scala 笔记(三):变量
文章目录 3.变量 3.1 基本概念 3.2 数据类型 3.3 数据类型体系图 3.4 整数类型 3.5 浮点数据类型 3.6 字符类型(Char) 3.7 布尔类型 3.8 Unit类型.Null类 ...
- 笔记整理4----Java语言高级(四)16 JAVA常用API-高级+17 泛型与常见数据结构+18 Map与Set集合+19 异常处理+20 IO流-高级
16 JAVA常用API-高级+17 泛型与常见数据结构+18 Map与Set集合+19 异常处理+20 IO流-高级 第05天 API 今日内容介绍 Object类 & System类 ...
- Cris 的 Scala 笔记(五):流程控制
文章目录 5. 流程控制 5.1 分支控制 单分支 双分支 多分支 分支控制if-else 注意事项 5.2 for循环控制 范围数据循环方式1 范围数据循环方式2 循环守卫 引入变量 嵌套循环 循环 ...
- 笔记整理5----Java语言高级(五--完结)21 字符流与字节流+22 多线程+23 网络编程
21 字符流与字节流+22 多线程+23 网络编程 第10天 IO流 今日内容介绍 标准输入流 & 转换流 & 打印流 对象操作流 Properties集合 第1章标准输入 ...
- 侯捷 C++面向对象高级开发(下)笔记整理
C++面向对象高级开发(下) 一.导读 (1)泛型编程和面向对象编程分属不同的思维, (2)由继承关系所形成的对象模型,包含this指针,vptr指针,vtbl虚表,虚机制,以及虚函数造成的多态. 二 ...
- 侯捷C++课程笔记01: 面向对象高级编程(上)
本笔记根据侯捷老师的课程整理而来:C++面向对象高级编程(上) pdf版本笔记的下载地址: 笔记01_面向对象高级编程(上),排版更美观一点(访问密码:3834) 侯捷C++课程笔记01: 面向对象高 ...
最新文章
- pandas使用pd.MultiIndex.from_product函数和pd.MultiIndex.from_tuples函数创建复合索引dataframe数据实战
- [Vue CLI 3] 插件开发之 registerCommand 到底做了什么
- 如何格式化电脑_U盘提示格式化后如何恢复数据
- 5G+SD-WAN实现更多应用的可能-vecloud微云
- python操作xlsx文档
- 华为NIP网络***检测系统
- c语言文件的读写通讯录,学C三个月了,学了文件,用C语言写了个通讯录程序
- SQLServer链接服务器至Oracle
- 求你了,别再随便打日志了,教你动态修改日志级别!
- ModelMap和ModelAndView的作用
- PostgreSQL学习总结(1)—— PostgreSQL 入门简介与安装
- java生成Excel文件,下载
- asp.net组件检查网站探针
- window下PC版 charles小程序抓包
- 用python算股票β系数_请教达人:Stata中计算多只股票月度beta系数的do文件怎么编写?...
- 关于腾讯应用宝上架的应用版本回退的问题
- table 表格如何设置单元格固定长度
- Ubuntu在线安装NFS服务
- 直播 | 骞云科技DevOps实践
- ros串口/摄像头 别名及查看绑定