分享

Kotlin编码规范

 BingeLee 2022-07-03 发布于福建

欢迎访问我的个人网站:https://

文章目录

1 介绍

2 源文件规范

2.1 文件编码

2.2 文件命名

2.3 特殊字符

2.3.1 空格

2.3.2 特殊转义字符

2.3.3 非ASCII字符

2.3.4 文件结构

2.3.4.1 版权/许可证

2.3.4.2 文件级注解

2.3.4.3 Package声明语句

2.3.4.4 Import语句

2.3.4.5 顶级(Top-Level)定义

2.3.4.6 类成员顺序

3 格式

3.1 花括号

3.1.1 基本使用

3.1.2 非空代码块情况

3.1.2 空代码块情况

3.2 缩进

3.3 每行一句代码

3.4 代码折行

3.4.1 折行规则

3.4.2 连续缩进

3.4.3 方法折行

3.4.3.1 表达式方法

3.4.4 属性

3.5 空格

3.5.1 空白换行

3.5.2 空格

3.6 特殊结构

3.6.1 枚举类

3.6.2 注解

3.6.3 隐式返回类型及属性类型

4 命名

4.1 包名

4.2 类名

4.3 方法名

4.4 常量名

4.5 非常量名

4.5.1 备用属性(Backing properties)名

4.6 类型变量名(用于泛型)

4.7 驼峰命名方式定义

5 注释

5.1 格式

5.1.1 段落

5.1.2 注释块标签

6 编程实践

6.1 安全性

6.1.1 `try-catch`的使用

6.1.2 可空(Nullable)类型作为参数

6.1.3 类型强制转换`as`的使用

6.1.4 `!!`的使用

6.1.5 成员变量懒加载

6.2 可读性

6.2.1 模板字符串

6.2.2 闭包(Block)的使用

6.3 性能

6.3.1 纯工具静态方法的使用

6.3.2 容器的添加操作

1 介绍

本规范参考Android官方Kotlin编码规范,旨在为Kotlin开发人员提供编码风格上的指导,仅供参考,如果翻译上的失误或建议,欢迎反馈!

2 源文件规范

2.1 文件编码

所有源文件必须使用UTF-8进行编码

2.2 文件命名

所有源文件必须使用.kt作为扩展名

如一个源码文件仅包含一个顶级类,则该文件应当以这个定级类的类名作为文件名,如包含多个顶级定义,请选择一个能够描述该文件内容的名字作为文件名

2.3 特殊字符

2.3.1 空格

除了换行符之外,ASCII空格(0x20)是唯一合法的空格字符

所有在源代码中(包括字符、字符串以及注释中)出现的其他空格字符都需要转义

Tab不可以用于缩进

2.3.2 特殊转义字符

任何含有特殊意义的转义字符,如:\b、\n、\r、\t、\'、\''、\\、\$等,不可以使用Unicode进行转义

2.3.3 非ASCII字符

对于其他的非ASCII字符,建议使用其实际字符,如:∞,应尽最大可能保证代码的可读性

任何时候,包括所有代码、注释、文档中,都不推荐使用Unicode编码进行转义

示例 说明

val unitAbbrev = "μs" 最佳写法,不使用注释也可以看懂

val unitAbbrev = "\u03bcs" // μs 不推荐的写法,使用转义字符没有任何意义

val unitAbbrev = "\u03bcs" 禁止的写法,读者完全无法理解意义

return "\ufeff" + content // byte order mark 不错的写法,可打印字符使用转义字符,并添加了必要的注释

2.3.4 文件结构

一个以.kt结尾的文件必须按照以下顺序填充并组织内容:

版权(Copyright)及许可证(License)头部声明(可选)

文件级的注解

package声明语句

import语句

顶级定义

以上各项内容间,使用一个空行分隔

2.3.4.1 版权/许可证

建议使用如下格式声明版权信息,并添加作者信息:

/*

 * Copyright 2019 . All Rights Reserved.

 *

 * coderyuan created at 2019/9/20 19:08

 */

1

2

3

4

5

2.3.4.2 文件级注解

文件级注释请参考Kotlin官方文档(https:///docs/reference/annotations.html#annotation-use-site-targets),根据相应的类型来合理使用注解,但必须在版权/许可证之后,package声明语句之前

@file:JvmName("Foo")

package com.coderyuan.demo

1

2

3

2.3.4.3 Package声明语句

package(包)声明语句不受列数限制,并且永远不会换行

2.3.4.4 Import语句

import语句可以用于导入类、方法、属性,它们集合在一个列表之内,应按包名的ASCII顺序排序

import语句和package语句一样,不受列数限制,不进行折行

禁止使用通配符

未使用的import语句,应及时删除,但在使用Kotlin Android Extensions时,请注意不要删除IDE误报的import语句

2.3.4.5 顶级(Top-Level)定义

一个.kt文件中可以定义一个或多个顶级类型、方法、属性、类别名

一个源文件中的内容必须针对同一个主题,不相干的声明必须拆分到其各自所属的文件中去,使一个文件尽量变小

对文件内容的数量和顺序没有严格的规定,但应最大程度保证文件的可阅读性

每个类都应该以业务逻辑顺序排列,而不应仅仅按时间顺序添加在最末尾

2.3.4.6 类成员顺序

成员顺序要求,与顶级定义中的顺序要求保持一致

3 格式

3.1 花括号

3.1.1 基本使用

单行的when语句分支、单行的if语句(用于代替三目运算符)不强制要求添加花括号

if (string.isEmpty()) return

val value = if (a == b) 0 else 1

when (value) {

    0 -> return

    // …

}

1

2

3

4

5

6

7

8

除以上情况外,任何使用if、when、for、do、while语句时,对于其有效作用域,都必须使用花括号,即使其中只有一行代码

// Good

val myValue = if (condition) {

    0

} else {

    1

}

// Bad

val myValue = if (condition) 

    0

else 

    1

1

2

3

4

5

6

7

8

9

10

11

12

3.1.2 非空代码块情况

在非空代码块中使用花括号时要遵循K&R风格(Kernighan and Ritchie Style):

左花括号({)前不能换行,在其后换行

在右花括号(})前要有换行

如果右花括号是一句语句,或者一个方法、构造函数、非匿名类的结尾,其后需要换行,其他情况不进行换行,如右花括号后是else或者逗号的情况

return Runnable {

    while (condition()) {

        foo()

    }

}

return object : MyClass() {

    override fun foo() {

        if (condition()) {

            try {

                something()

            } catch (e: ProblemException) {

                recover()

            }

        } else if (otherCondition()) {

            somethingElse()

        } else {

            lastThing()

        }

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

3.1.2 空代码块情况

空的代码块、构造方法也必须遵循K&R风格

try {

    doSomething()

} catch (e: Exception) {} // WRONG!

1

2

3

try {

    doSomething()

} catch (e: Exception) {

} // Okay

1

2

3

4

3.2 缩进

每次开始书写一个新的代码块时,使用4个空格进行缩进,在代码块结束时,恢复之前的缩进级别,该级别适用于整个块中的代码和注释

3.3 每行一句代码

每句语句后都必须使用一个换行,不使用分号

3.4 代码折行

建议代码每行不超过120字符,如过长须折行书写或进行缩减

例外情况:

120字符无法满足的情况,如:注释中一个很长的URL

package及import语句

注释中需要复制粘贴的shell脚本

3.4.1 折行规则

折行规则的主旨:在更高阶的语法处进行折行,最终目的是为了保证代码可读性:

非赋值运算符,在运算符前进行折行,同样适用于.和::两种符号

赋值运算符,在运算符后进行折行

方法或构造函数后的左括号,应保持在同一行

,(逗号)与其之前的内容保持在一行

Lambda箭头运算符->与其之前的参数列表保持在一行

3.4.2 连续缩进

换行时,第一行(每个连续行)之后的每一行至少缩进8个空格

如有多个连续行时,缩进可以超过8个,当且仅当它们以语法并行元素开头时,才使用同样的缩进级别

3.4.3 方法折行

当一个方法的签名无法写在一行时,请按参数定义进行折行:

每行定义一个参数,同时使用比方法多4个字符的空格进行缩进

右括号和返回类型的定义单独放在一行内,这一行不需要缩进

fun <T> Iterable<T>.joinToString(

    separator: CharSequence = ", ",

    prefix: CharSequence = "",

    postfix: CharSequence = ""

): String {

    // …

}

1

2

3

4

5

6

7

3.4.3.1 表达式方法

当一个方法仅包含一行代码时,可以将其写为表达式方法,参考:expression function

表达式方法不应折行,如果需要折行,请改为普通的方法定义方式,使用return进行返回

override fun toString(): String {

    return "Hey"

}

1

2

3

override fun toString(): String = "Hey"

1

3.4.4 属性

属性定义时如超长,请在=后进行换行,并使用连续缩进的方式进行缩进

private val defaultCharset: Charset? =

        EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)

1

2

重写get/set方法时,应当将其放在单独一行,并附加4个字符的缩进,按照普通方法的格式要求编码

var directory: File? = null

    set(value) {

        // …

    }

1

2

3

4

只读属性,可以使用减短的语法重写get方法,并保持在一行

val defaultExtension: String get() = "kt"

1

3.5 空格

3.5.1 空白换行

在一个类中的属性、构造函数、方法、内部类等成员之间,应添加一个空白换行。例外情况:

两个连续的属性之间(没有其他代码)不强制要求添加空行,但建议根据业务逻辑,使用空行进行分组隔离

private var groupAVar1 = 0

private var groupAVar2 = 0

private var groupAVar3 = 0

private var groupBVar1 = 1

private var groupBVar2 = 1

private var groupBVar3 = 1

1

2

3

4

5

6

7

枚举常量之间

各语句之间,根据业务逻辑建议添加空白换行进行分隔

第一个类成员前建议添加一个空白换行

允许连续的空行,但不建议这么做

3.5.2 空格

if、for、catch等关键字与其后的(()之间应当添加一个空格

// WRONG!

for(i in 0..1) {

}

1

2

3

// Okay

for (i in 0..1) {

}

1

2

3

else、catch等关键字与其后的(})之间应当添加一个空格

// WRONG!

}else {

}

1

2

3

// Okay

} else {

}

1

2

3

左花括号({)前应当添加一个空格.

// WRONG!

if (list.isEmpty()){

}

1

2

3

// Okay

if (list.isEmpty()) {

}

1

2

3

双目运算符的左右各添加一个空格.

// WRONG!

val two = 1+1

1

2

// Okay

val two = 1 + 1

1

2

对于类似的运算符同样适用:

Lambda表达式箭头运算符(->)

// WRONG!

ints.map { value->value.toString() }

1

2

// Okay

ints.map { value -> value.toString() }

1

2

例外情况(不应在其左右添加空格):

使用双冒号运算符(::)引用类成员时

// WRONG!

val toString = Any :: toString

1

2

// Okay

val toString = Any::toString

1

2

点运算符(.)

// WRONG

it . toString()

1

2

// Okay

it.toString()

1

2

范围运算符(..)

// WRONG

for (i in 1 .. 4) print(i)

1

2

// Okay

for (i in 1..4) print(i)

1

2

当且仅当在类定义(继承、实现)及泛型中使用where约束的冒号前(:)应当添加一个空格

// WRONG!

class Foo: Runnable

1

2

// Okay

class Foo : Runnable

1

2

// WRONG

fun <T: Comparable> max(a: T, b: T)

1

2

// Okay

fun <T : Comparable> max(a: T, b: T)

1

2

// WRONG

fun <T> max(a: T, b: T) where T: Comparable<T>

1

2

// Okay

fun <T> max(a: T, b: T) where T : Comparable<T>

1

2

逗号(,)及冒号(:)之后,应当添加一个空格

// WRONG!

val oneAndTwo = listOf(1,2)

1

2

// Okay

val oneAndTwo = listOf(1, 2)

1

2

// WRONG!

class Foo :Runnable

1

2

// Okay

class Foo : Runnable

1

2

行尾双斜杠(//)注释的左右各添加至少一个空格

// WRONG!

var debugging = false//disabled by default

1

2

// Okay

var debugging = false // disabled by default

1

2

3.6 特殊结构

3.6.1 枚举类

无函数及文档的常量枚举类,可以定义在一行

enum class Answer { YES, NO, MAYBE }

1

当常量定义在多行时,不强制要求添加空行,但含有body的定义除外

enum class Answer {

    YES,

    NO,

    MAYBE {

        override fun toString() = """¯\_(ツ)_/¯"""

    }

}

1

2

3

4

5

6

7

8

其他情况参照普通类的要求

3.6.2 注解

含有参数的注解,各占一行

@Retention(SOURCE)

@Target(FUNCTION, PROPERTY_SETTER, FIELD)

annotation class Global

1

2

3

无参数的注解,可以写在一行

@JvmField @Volatile

var disposable: Disposable? = null

1

2

如仅有一个无参数的注解,可以将其与其修饰内容的定义放在一行

@Volatile var disposable: Disposable? = null

@Test fun selectAll() {

    // …

}

1

2

3

4

5

3.6.3 隐式返回类型及属性类型

如果函数的返回值、属性初始化的值可以直观的判断出其类型时,类型声明可以省略,其他情况不可省略类型定义

override fun toString(): String = "Hey"

// becomes

override fun toString() = "Hey"

1

2

3

private val ICON: Icon = IconLoader.getIcon("/icons/kotlin.png")

// becomes

private val ICON = IconLoader.getIcon("/icons/kotlin.png")

1

2

3

编写类库代码时,public接口建议保留显示的类型声明,以便使用者阅读

4 命名

所有标识符的命名只可以使用ASCII字母及数字,极少数情况下会使用到下划线,符合\w+正则

除实现备用属性(Backing properties)外,不建议使用特殊前缀或后缀,如:s_name、mName

4.1 包名

包名必须使用小写字母,连续单词直接拼接即可,不允许使用下划线

// Okay

package com.example.deepspace

// WRONG!

package com.example.deepSpace

// WRONG!

package com.example.deep_space

1

2

3

4

5

6

4.2 类名

类名(包括接口名)必须使用首字母大写的驼峰式命名(PascalCase)

类名和接口名建议使用名词或名词短语(例:Character、List),但接口名有时候可为形容词或形容词短语(例:Callable)

测试类建议以Test作为后缀名

4.3 方法名

方法名必须使用首字母小写驼峰式命名(camelCase),通常为动词或动词短语(例:stop、handleMessage)

仅在测试方法用于区分逻辑时可以使用下划线,其他情况不允许使用下划线命名

@Test fun pop_emptyStack() {

    // …

}

1

2

3

4.4 常量名

常量命名全部大写,使用下划线连接各单词,通常为名词或名词短语

常量定义的标准:使用val定义,无自定义get方法,内容不可变且无其他影响的属性,包括不可变类型、不可变类型的集合,及使用const修饰的纯量和字符串

const val NUMBER = 5

val NAMES = listOf("Alice", "Bob")

val AGES = mapOf("Alice" to 35, "Bob" to 32)

val COMMA_JOINER = Joiner.on(',') // Joiner is immutable

val EMPTY_ARRAY = arrayOf<SomeMutableType>()

1

2

3

4

5

常量的定义:

建议使用top-level的形式,直接定义在.kt文件中

纯量和字符串形式的常量,必须使用const修饰

不建议在object或companion object中定义常量,避免开辟不必要的内存空间

// Good

private const val SAMPLE_CONST = 100

class TestClass {

    // Some things

}

// Not Good

class TestClass {

    companion object {

        const val SAMPLE_CONST = 100

    }

}

// Forbidden

class TestClass {

    companion object {

        val SAMPLE_CONST = 100

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

4.5 非常量名

非常量以首字母小写的驼峰式命名(camelCase),适用于实例属性、本地属性和参数名,通常为名词及名词短语

val variable = "var"

val nonConstScalar = "non-const"

val mutableCollection: MutableSet<String> = HashSet()

val mutableElements = listOf(mutableInstance)

val mutableValues = mapOf("Alice" to mutableInstance, "Bob" to mutableInstance2)

val logger = Logger.getLogger(MyClass::class.java.name)

val nonEmptyArray = arrayOf("these", "can", "change")

1

2

3

4

5

6

7

4.5.1 备用属性(Backing properties)名

备用属性命名和普通属性的命名规则保持一致,并在最前面添加一个下划线作为前缀

private var _table: Map<String, Int>? = null

val table: Map<String, Int>

    get() {

        if (_table == null) {

            _table = HashMap()

        }

        return _table ?: throw AssertionError()

    }

1

2

3

4

5

6

7

8

9

4.6 类型变量名(用于泛型)

两种风格:

单大写字母,后面可加数字,如:E、T、X、T2

一种类的命名形式,并附加一个大写字母T,例:RequestT, FooBarT

4.7 驼峰命名方式定义

通常有多种方式将短语组织成驼峰方式,像一些缩写词:IPv6、iOS等。为了统一,必须遵循以下几点规则。

将字符全部转换为ASCII字符,并且去掉’等符号。例如,Müller’s algorithm被转换为Muellers algorithm

在空格和标点符号处对上一步的结果进行切分,组成一个词组。

推荐:一些已经是驼峰命名的词语,也应该在这个时候被拆分。(例如 AdWords 被拆分为 ad words)。但是- 例如iOS之类的词语,它其实不是一个驼峰形式的词语,而是人们惯例使用的一个词语,因此不用做拆分。

经过上面两步后,先将所有的字母转换为小写,再把每个词语的第一个字母转换为大写。

最后,将所有词语连在一起,形成一个标识符。

注意:词语原来的大小写规则,应该被完全忽略。以下是一些例子:

原始短语 正确写法 非法写法

“XML Http Request” XmlHttpRequest XMLHTTPRequest

“new customer ID” newCustomerId newCustomerID

“inner stopwatch” innerStopwatch innerStopWatch

“supports IPv6 on iOS” supportsIpv6OnIos supportsIPv6OnIOS

“YouTube importer” YouTubeImporter

YoutubeImporter*

[*]号表示可以接受,但是不建议使用。

注意:有些词语在英文中,可以用[-]连接使用,也可以不使用[-]直接使用,例如:nonempty和non-empty都可以,因此方法名字为checkNonempty或者checkNonEmpty都是合法的写法

5 注释

5.1 格式

建议使用如下的基本的KDoc形式或单行注释

/**

 * A func for other use.

 * 

 * @param aParam The first parameter, Type: String?

 * @param bParam The second parameter, Type: Int

 * @return Boolean The result

 */

fun exportFunc(aParam: String?, bParam: Int) : Boolean {

    return false

}

1

2

3

4

5

6

7

8

9

10

/** An especially short bit of KDoc. */

1

未实现、未完成的方法、类,如需要提交代码,建议添加TODO

fun incompleteFunc() {

    // TODO To be finished.

}

1

2

3

public方法、变量、常量(即对外部暴露的内容)建议用doc的形式编写注释,私有内容不做强制要求,方法建议对参数、返回值进行说明,以便使用者参考,尽快理解含义

5.1.1 段落

每段注释,使用一个带有*的前缀空行分隔

5.1.2 注释块标签

可使用如下标签对相关内容进行标注:@constructor、@receiver、@param、@property、@return、@throws、@see,同时必须包含描述

当块标签无法放在一行时,其第二行从(@)位置起偏移8个空格

6 编程实践

6.1 安全性

6.1.1 try-catch的使用

对于Java中要求强制加try-catch的场景,编译器不会自动提示,需要我们自己加上,如:new JSONObject("xxx"),xx.toInt()等

大量不会出现异常的代码段,加入了try-catch保护,避免不必要的性能开销

catch住的Exception应当正确处理,如throw给上层调用,或printStackTrace,不应直接忽略

I/O操作及加锁操作或可能出现内存泄露的场景,必须使用finally语句进行处理

6.1.2 可空(Nullable)类型作为参数

方法里的参数要声明为可空类型,防止由于运行时数据为空或Kotlin与Java代码相互调用产生IllegalStateException异常导致Crash

6.1.3 类型强制转换as的使用

建议所有进行类型转换的操作,都使用as?,以防止类型不匹配导致的异常问题

class MyFragment : Fragment() {

    // Good

    (context as? Activity)?.finish()

    // Not Good

    (context as Activity).finish()

}

1

2

3

4

5

6

7

6.1.4 !!的使用

所有场景,如果不能100%确认某个值的类型及是否可能为空,禁止使用!!修饰,例外:使用TextUtils.isEmpty()校验字符串后,IDE误报

val myValue: String? = getValueFromJavaOrNativeMethod()    // myValue may be null

// Forbidden

val noNullValue: String = myValue!!

// Good

val noNullValue: String = myValue ?: ""

// Good

myValue?.let {

    noNullValue = it

}

// Good

if (!TextUtils.isEmpty(myValue)) {

    noNullValue = myValue

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

6.1.5 成员变量懒加载

尽量不要使用lateinit来定义不可空类型的变量,可能会在使用时出现null的情况

只读变量可以使用by lazy { }实现懒加载,并根据场合设置线程安全类型,UI线程建议设置为NONE,可变变量var需要重写其中的方法,不推荐

private val lazyImmutableValue: String by lazy {

    "Hello"

}

private val lazyImmutableValueNoThreadSafe: String by lazy(LazyThreadSafetyMode.NONE) {

    "Hello"

}

1

2

3

4

5

6

7

可变变量可以使用改写get方法的形式实现懒加载

private var lazyValue: Fragment? = null

    get() {

        if (field == null) {

            field = Fragment()

        }

        return field

    }

1

2

3

4

5

6

7

6.2 可读性

6.2.1 模板字符串

需要拼接变量的字符串,建议使用模板字符串,不要使用+

过长的、需要换行的、含有特殊的字符串,建议使用三个引号的模板字符串,不要使用+拼接,也可以避免使用转义符号,更容易阅读

    // Good

    var templateString = "My name is $myName"

    var jsonString = """

        {

            "status": 0,

            "data": {

                "dataList": [{

                    "id": 378,

                    "author": {

                    "id": 192,

                    "userName": "user"

                    }

                }]

            }

        }

    """

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

    // Not Good

    var plusString = "My Name is " + myName

    var jsonStr = "{\n" +

        "\t\"status\": 0,\n" +

        "\t\"data\": {\n" +

        "\t\t\"dataList\": [{\n" +

        "\t\t\t\"id\": 378,\n" +

        "\t\t\t\"author\": {\n" +

        "\t\t\t\t\"id\": 192,\n" +

        "\t\t\t\t\"userName\": \"user\"\n" +

        "\t\t\t}\n" +

        "\t\t}]\n" +

        "\t}\n" +

        "}" 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

6.2.2 闭包(Block)的使用

回调状态比较少的情况,建议使用闭包,不建议使用interface,以减少代码复杂度

闭包嵌套层级不要超过3层,如层数过多,建议将Block单独提出来使用,以增加代码可读性

// Bad

fun blockFunc() {

    a { 

        b {

            c {

                // Somethings

            }

        }

    }

}

1

2

3

4

5

6

7

8

9

10

闭包的回调参数,如未使用,建议不要使用_隐藏,防止后继使用时难以理解代码

// Good

fun blockFunc() {

    a { v1, v2 -> {

            // TODO

        } 

    }

}

// Not Good

fun blockFunc() {

    a { _, _ -> {

            // TODO

        } 

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

6.3 性能

6.3.1 纯工具静态方法的使用

直接写到.kt文件里,即Top-Level Function的形式

在类里的companion object中定义,会有额外开销,如不得不在伴生对象中实现静态方法,请添加@JvmStatic注解

禁止在object里定义,避免额外创建单例

// Good

// SimpleClass.kt

fun myUtilMethod() {

    // Some things

}

// Not Good

// SimpleClass.kt 

class SimpleClass {

    companion object {

    fun myUtilMethod() {

    }

    }

}

// Forbidden

// SimpleClass.kt 

object SimpleClass {

    fun myUtilMethod() {

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

6.3.2 容器的添加操作

Kotlin容器如list、set、map等,定义初始化元素时不要直接声明,应一个一个添加,否则会有性能损耗

// Not Good

val list = listOf("a", "b")

————————————————

版权声明:本文为CSDN博主「袁国正_yy」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/yuanguozhengjust/article/details/101296404

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多