引子

相信总是有很多同学,总是在抱怨泛型无论怎么学习,都只是停留在一个简单使用的水平,所以一直为此而备受苦恼。

Kotlin 作为一门能和 Java 相互调用的语言,自然也支持泛型,不过 Kotlin 的新关键字 inout 却总能绕晕一部分人,归根结底,还是因为 Java 的泛型基本功没有足够扎实。

很多同学总是会产生这些疑问:

  • Kotlin 泛型和 Java 泛型到底有何区别?
  • Java 泛型存在的意义到底是什么?
  • Java 的类型擦除到底是指什么?
  • Java 泛型的上界、下界、通配符到底有何区别?它们可以实现多重限制么?
  • Java 的 <? extends T><? super T><?> 到底对应了什么?有哪些使用场景?
  • Kotlin 的 inout*where 到底有何魔力?
  • 泛型方法又是什么?

今天,就用一篇文章为大家解除上述疑惑。

泛型:类型安全的利刃

总所周知,Java 在 1.5 之前,是没有泛型这个概念的。那时候的 List 还只是一个可以装下一切的集合。所以我们难免会写上这样的代码:

List list = new ArrayList();
list.add(1);
list.add("nanchen2251");
String str = (String) list.get(0);

上面的代码编译并没有任何问题,但运行的时候一定会出现常见的 ClassCastException 异常:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

这个体验非常糟糕,我们真正需要的是在代码编译的时候就能发现错误,而不是让错误的代码发布到生产环境中。

而如果上述代码我们增加上泛型,就会在编译期就能看到明显的错误啦。

List<String> list = new ArrayList<>();
list.add(1);
// 报错 Required type:String but Provided:int
list.add("nanchen2251");
String str = list.get(0);

很明显,泛型的出现,让类型更加安全,使我们在使用 ListMap 等不再需要去专门编写 StringListStringMap 了,只需要在声明 List 的同时指定参数类型即可。

总的来说,泛型具备以下优势:

  • 类型检查,能在编译时就帮开发检查出错误;
  • 更加语义化,比如我们声明一个 LIst<String>,我们可以很直接知道里面存储的是 String 对象;
  • 能自动进行类型转换,获取数据的时候不需要再做强转操作;
  • 能写出更加通用化的代码。

类型擦除

可能有些同学思考过这样一个问题,既然泛型是和类型相关的,那么是不是也能使用类型的多态呢?

我们知道,一个子类型是可以赋值给父类型的,比如:

Object obj = "nanchen2251";
// 这是多态

Object 作为 String 的父类,自然可以接受 String 对象的赋值,这样的代码我们早已司空见惯,并没有什么问题。

但当我们写下这串代码:

List<String> list = new ArrayList<String>();
List<Object> objects = list;
// 多态用在这里会报错 Required type:List<Object> Provided: List<String>

上面发生了赋值错误,这是因为 Java 的泛型本身具有「不可变性 Invariance」,Java 里面认为 List<String>List<Object> 类型并不一致,也就是说,子类的泛型 List<String> 不属于泛型 List<Object> 的子类。

由于 Java 的泛型本身是一种 「伪泛型」,Java 为了兼容 1.5 以前的版本,不得以在泛型底层实现上使用 Object 引用,所以我们声明的泛型在编译时会发生「类型擦除」,泛型类型会被 Object 类型取代。比如:

class Demo<T> {
void func(T t){
// ...
}
}

会被编译成:

class Demo {
void func(Object t){
// ...
}
}

可能你会好奇,在编译时发生类型擦除后,我们的泛型都被更换成了 Object,那为什么我们在使用的时候,却不需要强转操作呢?比如:

List<String> list = new ArrayList<>();
list.add("nanchen2251");
String str = list.get(0);
// 这里并没有要求我们把 list.get(0) 强转为 String

这是因为编译器会根据我们声明的泛型类型进行提前的类型检查,然后再进行类型擦除,擦除为 Object,但在字节码中其实还存储了我们的泛型的类型信息,在使用到泛型类型的时候会把擦除后的 Object 自动做类型强转操作。所以上面的 list.get(0) 本身就是一个经过强转的 String 对象了。

这个技术看起来还蛮好的,但却有一个弊端。就是既然擦成 Object 了,那么在运行的时候,你根本不能确定这个对象到底是什么类型,虽然你可以通过编译器帮你插入的 checkcast 来获得此对象的类型。但是你并不能把 T 真正的当作一个类型使用:比如这条语句在 Java 中是非法的。

T a = new T();
// 报错:Type parameter 'T' cannot be instantiated directly

同理,因为都被擦成了 Object,你就不能根据类型来做某种区分。

比如 instanceof

if("nanchen2251" instanceof T.class){
// 报错:Identifier expected Unexpected token
}

比如重载:

void func(T t){
// 报错:'func(T)' clashes with 'func(E)'; both methods have same erasure
}
void func(E e){
}

同样,因为基本数据类型不属于 oop,所以也不能被擦除为 Object,所以 Java 的泛型也不能用于基本类型:

List<int> list;
// 报错:Type argument cannot be of primitive type

oop:面向对象的程序设计(Object Oriented Programming)

到这里,是不是可以回答上面的第 3 个问题了:Java 的类型擦除到底是指什么?

首先你要明白一点,一个对象的类型永远不会被擦出的,比如你用一个 Object 去引用一个 Apple 对象,你还是可以获得到它的类型的。比如用 RTTI。

RTTI:运行时类型信息,运行时类型识别 (Run Time Type Identification)

Object object = new Apple();
System.out.println(object.getClass().getName());
// will print Apple

哪怕它是放到泛型里的。

class FruitShop<T>{
private T t; public void set(T t){
this.t = t;
} public void showFruitName(){
System.out.println(t.getClass().getName());
}
}
FruitShop<Apple> appleShop = new FruitShop<Apple>();
appleShop.set(new Apple());
appleShop.showFruitName();
// will print Apple too

为啥?因为引用就是一个用来访问对象的标签而已,对象一直在堆上放着呢。

所以不要断章取义认为类型擦除就是把容器内对象的类型擦掉了,所谓的类型擦除,是指容器类FruitShop<Apple>,对于 Apple 的类型声明在编译期的类型检查之后被擦掉,变为和 FruitShop<Object> 等同效果,也可以说是 FruitShop<Apple>FruitShop<Banana> 被擦为和 FruitShop<Object> 等价,而不是指里面的对象本身的类型被擦掉!

那,Kotlin 中有类型擦除么?

C# 和 Java 在一开始都是不支持泛型的。Java 在 1.5 开始才加入了泛型。为了让一个不支持泛型的语言支持泛型,只有两条路可以走:

  • 以前的非泛型容器保持不变,然后平行的增加一套泛型化的类型。
  • 直接把已有的非泛型容器扩展为泛型,不添加任何新的泛型版本。

Java 由于 1.5 之前市面上一句有大量的代码,所以不得以选择了第 2 种方式,而 C# 比较机智就选择了第一种。

而 Kotlin 本身就是基于 Java 1.6 编写的,一开始就有泛型,不存在兼容老版本代码的问题,那 Kotlin 实现的泛型还具备类型擦除么?

当然具备。上面其实已经说的很清楚了,Kotlin 本身就是基于 Java 1.6 编写的,而且 Kotlin 和 Java 有极强的互调能力,当然也存在类型擦除。

不过...

你还是会发现有意思的点:

val list = ArrayList()
// 报错:Not enough information to infer type variable E

在 Java 中,不指定泛型类型是没问题的,但 Kotlin 这样不好使了。想来也简单,毕竟在 Java 1.5 之前是肯定不存在上述类似代码的,而泛型的设计初衷就不是用来装默认的 Kotlin Any 的。

泛型的上界通配符

前面说到:因为 Java 的泛型本身具有「不可变性 Invariance」,所以即使 Fruit 类是 Apple 类的父类,但 Java 里面认为 List<Fruit>List<Apple> 类型并不一致,也就是说,子类的泛型 List<Apple> 不属于泛型 List<Fruit> 的子类。

所以这样的代码并不被运行。

List<Apple> apples = new ArrayList<Apple>();
List<Fruit> fruits = apples;
// 多态用在这里会报错 Required type:List<Fruit> Provided: List<Apple>

那假如我们想突破这层限制,怎么办?使用上界通配符 ? extends

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
// 使用上界通配符后,编译不再报错

「上界通配符」,可以使 Java 泛型具有「协变性 Covariance」,协变就是允许上面的赋值是合法的。

在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。extends 限制了泛型类型的父类型,所以叫上界。

它有两层意思:

  • 其中 ? 是个通配符,表示这个 List 的泛型类型是一个未知类型。
  • extends 限制了这个未知类型的上界,也就是泛型类型必须满足这个 extends 的限制条件,这里和定义 classextends 关键字有点不一样:
    • 它的范围不仅是所有直接和间接子类,还包括上界定义的父类本身,也就是 Fruit
    • 它还有 implements 的意思,即这里的上界也可以是 interface

这个突破限制有意义么?

有的有的。

假如我们有一个接口 Fruit

interface Fruit {
float getWeight();
}

有两个水果类实现了 Fruit 接口:

class Banana implements Fruit {
@Override
public float getWeight() {
return 0.5f;
}
} class Apple implements Fruit {
@Override
public float getWeight() {
return 1f;
}
}

假设我们有个需求是需要给水果称重:

List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
float totalWeight = getTotalWeight(apples);
// 报错:Required type: List<Fruit> Provided: List<Apple> private float getTotalWeight(List<Fruit> fruitList) {
float totalWeight = 0;
for (Fruit fruit : fruitList) {
totalWeight += fruit.getWeight();
}
return totalWeight;
}

想来这也是一个非常正常的需求,秤可以称各种水果的重量,但也可以只称苹果。你不能因为我只买苹果就不给我称重吧。所以把上面的代码加上上界通配符就可以啦。

List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
float totalWeight = getTotalWeight(apples);
// 不再报错
// 增加了上界通配符 ? extends
private float getTotalWeight(List<? extends Fruit> fruitList) {
float totalWeight = 0;
for (Fruit fruit : fruitList) {
totalWeight += fruit.getWeight();
}
return totalWeight;
}

不过,上面使用 ? extends 上界通配符突破了一层限制,却被施加了另一层限制:只可输出不可输入

什么意思呢?

比如:

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
Fruit fruit = fruits.get(0);
fruits.add(new Apple());
// 报错:Required type: capture of ? extends Fruit Provided: Apple

声明了上界通配符泛型的集合,不再允许 add 新的对象,Apple 不行,Fruit 也不行。拓展开来说:不止是集合,自己编写一个泛型做输入也不行

interface Shop<T> {
void showFruitName(T t);
T getFruit();
} Shop<? extends Fruit> apples = new Shop<Apple>(){
@Override
public void showFruitName(Apple apple) { } @Override
public Apple getFruit() {
return null;
}
};
apples.getFruit();
apples.showFruitName(new Apple());
// 报错:Required type: capture of ? extends Fruit Provided: Apple

泛型的下界通配符

泛型有上界通配符,那有没有下界通配符呢?

有的有的。

与上界通配符 ? extends 对应的就是下界通配符 ? super

下界通配符 ? super 所有情况和 ? extends 上界通配符刚刚相反:

  • 通配符 ? 表示 List 的泛型类型是一个 未知类型
  • super 限制了这个未知类型的下界,也就是泛型类型必须满足这个 super 的限制条件
    • 它的范围不仅是所有直接和间接子父类,还包括下界定义的子类本身。
    • super 同样支持 interface

它被施加的新限制是:只可输入不可输出

Shop<? super Apple> apples = new Shop<Fruit>(){
@Override
public void showFruitName(Fruit apple) { } @Override
public Fruit getFruit() {
return null;
}
};
apples.showFruitName(new Apple());
Apple apple = apples.getFruit();
// 报错:Required type: Apple Provided: capture of ? super Apple

解释下,首先 ? 表示未知类型,编译器是不确定它的类型的。

虽然不知道它的具体类型,不过在 Java 里任何对象都是 Object 的子类,所以这里只能把apples.getFruit() 获取出来的对象赋值给 Object。由于类型未知,所以直接赋值给一个 Apple 对象肯定是不负责任的,需要我们做一层强制转换,不过强制转换本身可能发生错误。

Apple 对象一定是这个未知类型的子类型,根据多态的特性,这里通过 showFruitName 输入 Button 对象是合法的。

小结下,Java 的泛型本身是不支持协变和逆变的:

  • 可以使用泛型通配符 ? extends 来使泛型支持协变,但是「只能读取不能修改」,这里的修改仅指对泛型集合添加元素,如果是 remove(int index) 以及 clear 当然是可以的。
  • 可以使用泛型通配符 ? super 来使泛型支持逆变,但是「只能修改不能读取」,这里说的不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转当然也是可以的。

理解了 Java 的泛型之后,再理解 Kotlin 中的泛型,就比较容易了。

Kotlin 的 out 和 in

和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。

不过换了一种表现形式:

  • 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends
  • 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super
val appleShop: Shop<out Fruit>
val fruitShop: Shop<in Apple>

它们完全等价于:

Shop<? extends Fruit> appleShop;
Shop<? super Apple> fruitShop;

换了个写法,但作用是完全一样的。out 表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in 就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。

泛型的上下界约束

上面讲的都是在使用的时候再对泛型进行限制,我们称之为「上界通配符」和「下界通配符」。那我们可以在函数设计的时候,就设置这个限制么?

可以的可以的。

比如:

open class Animal
class PetShop<T : Animal?>(val t: T)

等同于 Java 的:

class PetShop<T extends Animal> {
private T t; PetShop(T t) {
this.t = t;
}
}

这样,我们在设计宠物店类 PetShop 就给支持的泛型设置了上界约束,支持的泛型类型必须是 Animal 的之类。所以我们使用的话:

class Cat : Animal()

val catShop = PetShop(Cat())
val appleShop = PetShop(Apple())
// 报错:Type mismatch. Required: Animal? Found: Apple

很明显,Apple 并不是 Animal 的子类,当然不满足 PetShop 泛型类型的上界约束。

那....可以设置多个上界约束么?

当然可以,在 Java 中,给一个泛型参数声明多个约束的方式是,使用 &

class PetShop<T extends Animal & Serializable> {
// 通过 & 实现了两个上界,必须是 Animal 和 Serializable 的子类或实现类
private T t; PetShop(T t) {
this.t = t;
}
}

而在 Kotlin 中舍弃了 & 这种方式,而是增加了 where 关键字:

open class Animal
class PetShop<T>(val t: T) where T : Animal?, T : Serializable

通过上面的方式,就实现了多个上界的约束。

Kotlin 的通配符 *

前面我们说的泛型类型都是在我们需要知道参数类型是什么类型的,那如果我们对泛型参数的类型不感兴趣,有没有一种方式处理这个情况呢?

有的有的。

在 Kotlin 中,可以用通配符 * 来替代泛型参数。比如:

val list: MutableList<*> = mutableListOf(1, "nanchen2251")
list.add("nanchen2251")
// 报错:Type mismatch. Required: Nothing Found: String

这个报错确实让人匪夷所思,上面用通配符代表了 MutableList 的泛型参数类型。初始化里面也加入了 String 类型,但在新 add 字符串的时候,却发生了编译错误。

而如果是这样的代码:

val list: MutableList<Any> = mutableListOf(1, "nanchen2251")
list.add("nanchen2251")
// 不再报错

看来,所谓的通配符作为泛型参数并不等价于 Any 作为泛型参数。MutableList<*>MutableList<Any> 并不是同一种列表,后者的类型是确定的,而前者的类型并不确定,编译器并不能知道这是一种什么类型。所以它不被允许添加元素,因为会导致类型不安全。

不过细心的同学肯定发现了,这个和前面泛型的协变非常类似。其实通配符 * 不过是一种语法糖,背后也是用协变来实现的。所以:MutableList<*> 等价于 MutableList<out Any?>,使用通配符与协变有着一样的特性。

在 Java 中,也有一样意义的通配符,不过使用的是 ? 作为通配。

List<?> list = new ArrayList<Apple>();

Java 中的通配符 ? 也等价于 ? extends Object

多个泛型参数声明

那可以声明多个泛型么?

可以的可以的。

HashMap 不就是一个典型的例子么?

class HashMap<K,V>

多个泛型,可以通过 , 进行分割,多个声明,上面是两个,实际上多个都是可以的。

class HashMap<K: Animal, V, T, M, Z : Serializable>

泛型方法

上面讲的都是都是在类上声明泛型类型,那可以声明在方法上么?

可以的可以的。

如果你是一名 Android 开发,ViewfindViewById 不就是最好的例子么?

public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}

很明显,View 是没有泛型参数类型的,但其 findViewById 就是典型的泛型方法,泛型声明就在方法上。

上述写法改写成 Kotlin 也非常简单:

fun <T : View?> findViewById(@IdRes id: Int): T? {
return if (id == View.NO_ID) {
null
} else findViewTraversal(id)
}

Kotlin 的 reified

前面有说到,由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。比如你不能检查一个对象是否为泛型类型 T 的实例:

<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // IDE 会提示错误,illegal generic type for instanceof }
}

Kotlin 里同样也不行:

fun <T> printIfTypeMatch(item: Any) {
if (item is T) { // IDE 会提示错误,Cannot check for instance of erased type: T
println(item)
}
}

这个问题,在 Java 中的解决方案通常是额外传递一个 Class<T> 类型的参数,然后通过 Class#isInstance 方法来检查:


<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) { }
}

Kotlin 中同样可以这么解决,不过还有一个更方便的做法:使用关键字 reified 配合 inline 来解决:


inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // 这里就不会在提示错误了 }
}

上面的 Gson 解析的时候用的非常广泛,比如咱们项目里就有这样的扩展方法:

inline fun <reified T> String?.toObject(type: Type? = null): T? {
return if (type != null) {
GsonFactory.GSON.fromJson(this, type)
} else {
GsonFactory.GSON.fromJson(this, T::class.java)
}
}

总结

本文花了非常大的篇幅来讲 Kotlin 的泛型和 Java 的泛型,现在再回过头去回答文首的几个问题,同学你有谱了吗?如果还是感觉一知半解,不妨多看几遍。

文章中有比较多的参考「码上开学」的文章:Kotlin 的泛型

甚至有一部分直接截取过来,主要本意是不想重复造轮子。文章中如有疏漏,欢迎在评论区进行留言。

扫盲:Kotlin 的泛型的更多相关文章

  1. Kotlin介绍

    Kotlin介绍 转 https://www.jianshu.com/p/d30406daaf25 Google在今年的IO大会上宣布,将Android开发的官方语言更换为Kotlin,作为跟着Goo ...

  2. Kotlin开发语言文档(官方文档)-- 目录

    开始阅读Kotlin官方文档.先上文档目录.有些内容还未阅读,有些目录标目翻译还需琢磨琢磨.后续再将具体内容的链接逐步加上. 文档链接:https://kotlinlang.org/docs/kotl ...

  3. 【Bugly 技术干货】Android开发必备知识:为什么说Kotlin值得一试

    1.Hello, Kotlin Bugly 技术干货系列内容主要涉及移动开发方向,是由 Bugly邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创,转载请标明出处. 1. ...

  4. Android平台的Swift—Kotlin

    WeTest 导读 Kotlin 已经出来较长一段时间了,有些同学已经对Kotlin进行了深入的学习,甚至已经运用到了自己的项目当中,但是还有较多同学可能只是听过Kotlin或简单了解过,这篇文章的目 ...

  5. Kotlin——高级篇(二):高阶函数详解与标准的高阶函数使用

    在上面一个章节中,详细的讲解了Kotlin中关于Lambda表达式的语法以及运用,如果还您对其还不甚理解,请参见Kotlin--高级篇(一):Lambda表达式详解.在这篇文章中,多次提到了Kotli ...

  6. Kotlin学习笔记

    Kotlin的注释 Kotlin 的代码注释和Java一模一样 Kotlin的运行方式也是先kotlinc生成字节码,再kotlin字节码 如果一行里面只有一条语句,那么可以不写分号.但如果打算在同一 ...

  7. Android Weekly Notes Issue #290

    Android Weekly Issue #290 December 31st, 2017 Android Weekly Issue #290 本期内容包括介绍Kotlin逆变协变的一篇(虽然没说清楚 ...

  8. Kotlin 泛型

    泛型,即 "参数化类型",将类型参数化,可以用在类,接口,方法上. 与 Java 一样,Kotlin 也提供泛型,为类型安全提供保证,消除类型强转的烦恼. 声明一个泛型类: cla ...

  9. Kotlin中的“忍者”函数 —— 理解泛型的能力(KAD 12)

    作者:Antonio Leiva 时间:Feb 8, 2017 原文链接:https://antonioleiva.com/generic-functions-kotlin/ Kotlin的一些特性组 ...

随机推荐

  1. 整理一下dedecms的相关知识

    dedecms更改数据库连接 文件   data/common.inc.php ------------------------------------------------------------ ...

  2. 【2020.12.02提高组模拟】球员(player)

    题目 题目描述 老师们已经知道学生喜欢睡觉,Soaring是这项记录保持者.他只会在吃饭或玩FIFA20时才会醒来.因此,他经常做关于足球的梦,在他最近的一次梦中,他发现自己成了皇家马德里足球俱乐部的 ...

  3. 关于esp8266的SDK开发串口打印mismatch map 3,spi_size_map 6 system_partition_table_regist fail解决办法

    最近在学习esp8266 用的sdk开发,烧录碰到个问题,本人使用的esp8266模块是esp8266-12f,模块是4M的也就是32Mbit 参考了网上的很多办法,大部分写的不清楚也没解决,摸索了几 ...

  4. MySQL索引(一)索引基础

    索引是数据库系统里面最重要的概念之一.一句话简单来说,索引的出现其实是为了提高数据查询的效率,就像书的目录一样. 常见模型 索引的出现是为了提高查询效率,但是实现索引的方式却有很多种,这里就介绍三种常 ...

  5. 第8.26节 重写Python类中的__getattribute__方法实现实例属性访问捕获

    一. 引言 在<第7.23节 Python使用property函数定义属性简化属性访问的代码实现>和<第7.26节 Python中的@property装饰器定义属性访问方法gette ...

  6. PyQt(Python+Qt)学习随笔:Qt Designer组件属性编辑界面中对话窗QDialog的modal属性

    modal属性表示窗口执行show()操作时是以模态窗口还是非模态窗口形式展示,缺省为False,设置该值与QWidget.windowModality的值设置为 Qt.ApplicationModa ...

  7. 分布式计算框架-Spark(spark环境搭建、生态环境、运行架构)

    Spark涉及的几个概念:RDD:Resilient Distributed Dataset(弹性分布数据集).DAG:Direct Acyclic Graph(有向无环图).SparkContext ...

  8. web网络漏洞扫描器编写

    这两天看了很多web漏洞扫描器编写的文章,比如W12scan以及其前身W8scan,还有猪猪侠的自动化攻击背景下的过去.现在与未来,以及网上很多优秀的扫描器和博客,除了之前写了一部分的静湖ABC段扫描 ...

  9. SpringMVC拦截html页面访问

    在 web.xml 配置文件 原本的前端控制器后面加一个 servlet-mapping 即可简单解决 虽能解决 html 访问的问题,但不清楚是否对后面的操作产生何种影响 <servlet&g ...

  10. rsync 参数说明及使用参数笔记

    第1章 rsync 命令简介 rsync 是一款开源的.快速的.多功能的.可实现全量及增量的本地或远程数据镜像同步备份的优秀工具. 1.1.1 语法格式 三种模式: 1)本地模式 rsync [选项] ...