Hexo Next主题快速集成快评

快评是什么

快评是由深圳市一行代码科技有限公司开发的一款社会化的评论系统,快评使用Markdown编辑器进行评论回复。目前,支持使用微信或手机号进行登录。

体验地址:https://www.youngfeng.com/2019/09/25/Hexo-Next%E4%B8%BB%E9%A2%98%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E5%BF%AB%E8%AF%84/

隐藏在Flappy Bird背后的故事

还记得Flappy Bird这款小游戏吗?它在2014年毫无征兆的全球爆红,当年累积下载量超过5000万,日广告收入超过5万美元。谁曾想到这个游戏的开发者仅仅是一名普通的越南软件工程师,他的名字叫阮哈东,据说开发这款游戏仅用了一周时间左右。

正是因为这款游戏让阮哈东家喻户晓,不少的商家看到了商机,慕名而来。而就在这款游戏爆红的几天之后,阮哈东却突然在Twitter上宣布将在22小时内下架这款游戏,这就意味着阮哈东将主动放弃每天5万美元的收入。作为互联网从业者,他心里应该非常清楚自己将要失去什么。究竟是什么原因让阮哈东决定放弃这笔巨额的广告收入呢?

年纪越大,越理解郭德纲

我最讨厌那种不明白情况,就劝你一定要大度的人,这种人你一定要离他远一点,因为雷劈他的时候会连累到你…

究竟是怎样的一种人生阅历,能够让郭先生有这样的人生感悟….

写在十分简历发布前夕

关于十分简历

十分简历是我们的第一个作品,这是一个微信小程序,用于生成漂亮的简历模板。

同时,考虑到用户的隐私问题,增加了私密简历功能。

那么,到底什么是私密简历呢?

2018最新Kotlin基础视频教程上线了

从5月份开始制作这个视频,历时两个多月,终于在网易云课堂与大家见面了…

视频特色

  • 在前期课程中,使用普通编辑器进行编码,治疗你的IDE依赖症
  • 所有课程均使用live coding的方式授课,保持与你的频率一致
  • 每节课程课后都准备了相应的习题供大家巩固练习
  • 课程覆盖面广,线下效果持续跟踪
  • 项目实战部分是一个非常有意思的小项目,使用控制台完成类QQ聊天室功能(支持文字消息、文件发送、文件下载等功能)

课程目录

  • Kotlin基础环境配置、变量与空值安全
  • 一等公民函数基础语法
  • 高阶函数、lambda表达式与匿名函数
  • 面向对象知识点(类、接口、继承、扩展等)
  • Kotlin语言中多样的class类型、对象以及枚举
  • 强大的集合与控制流处理
  • 操作符重载
  • 更安全的泛型处理
  • 代理模式及其应用
  • 协程简介
  • 析构、注解与异常处理
  • Kotlin与Java互通的那些事儿
  • 控制台版本QQ聊天室项目实战

一些感想

这是我第一次真正尝试录制视频教程,种种原因,导致整个视频的录制、剪辑加发布审核一共花掉了两个多月,将近三个月。而且还是离职在家,全职的情况下。

即便如此,事实上,这个视频依然没有达到我的要求。但我想,第一次永远是青涩的,想要一步到位永远不过是痴人说梦。我坚信下一个视频我会做的更好。因此,也希望大家对这个视频提出宝贵的意见。

视频介绍看这里

在Kotlin交流群中,不少同学希望我能够出露脸视频。为了满足大家的愿望,我在课程的开始部分录制了一段真人解说视频。如果你不想看文字介绍的话,请移步到这里:http://study.163.com/course/courseLearn.htm?courseId=1005686004&share=2&shareId=400000000535034#/learn/video?lessonId=1052950023&courseId=1005686004

如何观看这个视频教程

目前,该视频教程仅上线了网易云课堂,同学们可以移步至这里观看:http://study.163.com/course/introduction.htm?courseId=1005686004&share=2&shareId=400000000535034

课程问题解惑

如果你在使用Kotlin视频教程的时候遇到了任何问题,欢迎来我的Kotlin语言交流群里面提问。只要是关于视频课程的内容,一定有问必答。如果你没有购买这个视频教程,也可以添加这个QQ交流群,群里的小伙伴都非常热情。如果遇到了Kotlin语言问题,也可以在群里提问,同样会有问必答。

视频课程资料

课程源码:https://github.com/yuanhoujun/kotlin-video-tutorial

习题答案:关注微信公众号【欧阳锋工作室】,回复“Kotlin视频教程”索取

Kotlin语言交流群

唯一官方Kotlin语言交流群:329673958,期待您的加入。

那些年,我们看不懂的那些Kotlin标准函数

Kotlin标准库中提供了一套用于常用操作的函数。最近,在我的Kotlin交流群中有人再次问到了关于这些函数的用法。今天,让我们花一点时间,一起看一下这些函数的用法。

Ready go >>>

注:这里所说的标准函数主要来自于标准库中在文件Standard.kt中的所有函数。

run#1

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

contract部分主要用于编译器上下文推断,这里我们忽略掉这部分代码。

观察源码发现,run方法仅仅是执行传入的block表达式并返回执行结果而已(block是一个lambda表达式)。

因此,如果你仅仅需要执行一个代码块,可以使用该函数

看一个例子:

1
2
3
4
5
6
7
8
9
val x = run {
println("Hello, world")
return@run 1
}
println(x)

// 执行结果
Hello,world
1

run#2

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

这个函数跟上面的函数功能是完全一样的。不同的是,block的receiver是当前调用对象,即在block中可以使用当前对象的上下文。

因此,如果你需要在执行的lambda表达式中使用当前对象的上下文的话,可以使用该函数。除此之外,两者没有任何差别

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
fun sayHi(name: String) {
println("Hello, $name")
}
}

class B {

}

fun main(args: Array<String>) {
val a = A()
val b = a.run {
// 这里你可以使用A的上下文
a.sayHi("Scott Smith")
return@run B()
}
println(b)
}

// 执行结果
Hello,Scott Smith
b@2314

从例子中,我们可以看到,这个函数还可以用于对数据类型进行转换。

with

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}

这个函数其实和run函数也是做了一样的事情。不同的是,这里可以指定block的接收者。

因此,如果你在执行lambda表达式的时候,希望指定不同的接收者的话,可以使用该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
fun sayHi(name: String) {
println("Hello, $name")
}
}


fun main(args: Array<String>) {
val a = A()
with(a) {
// 这里的接收者是对象a,因此可以调用a实例的所有方法
sayHi("Scott Smith")
}
}

apply

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

可以看到,这个方法是针对泛型参数的扩展方法,即所有对象都将拥有该扩展方法。相对于run#2方法,apply不仅执行了block,同时还返回了receiver本身。

这在链式编程中很常用,如果你希望执行lambda表达式的同时而不破坏链式编程,可以使用该方法

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
fun sayHi(name: String) {
println("Hello, $name")
}

fun other() {
println("Other function...")
}
}


fun main(args: Array<String>) {
val a = A()
a.apply {
println("This is a block")
sayHi("Scott Smith")
}.other()
}

// 执行结果
This is a block
Hello, Scott Smith
Other function...

also

1
2
3
4
5
6
7
8
9
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}

这个函数跟with又很像,不同的是,block带有一个当前receiver类型的参数。在block中,你可以使用该参数对当前实例进行操作。

这个函数和with完全可以互相通用,with函数可以直接在当前实例上下文中对其进行操作,而also函数要通过block参数获取当前类实例。因为用法完全一致,这里就不举例了

let

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}

如果你使用过RxJava,可能会感到似曾相识,这其实就是RxJava的map函数。这个函数也是针对泛型参数的扩展函数,所有类都将拥有这个扩展函数。

如果你希望对当前数据类型进行一定的转换,可以使用该方法。该方法的block中同样可以使用当前receiver的上下文

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Triangle {}

class Rectangle {}

fun main(args: Array<String>) {
val tr = Triangle()
val rect = tr.let { it ->
println("It is $it")
return@let Rectangle()
}
println(rect)
}

// 执行结果
It is Triangle@78308db1
Rectangle@27c170f0

从例子中可以看到,我们成功地将三角形转换成了矩形,这就是let函数的作用。

takeIf

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (predicate(this)) this else null
}

这个函数也是针对泛型参数的扩展函数,所有类都将拥有这个扩展。这个函数使用了一个预言函数作为参数,主要用于判断当前对象是否符合条件。
这个条件函数由你指定。如果条件符合,将返回当前对象。否则返回空值。

因此,如果你希望筛选集合中某个数据是否符合要求,可以使用这个函数

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
fun main(args: Array<String>) {
val arr = listOf(1, 2, 3)
arr.forEach {
println("$it % 2 == 0 => ${it.takeIf { it % 2 == 0 }}")
}
}

// 执行结果
1 % 2 == 0 => null
2 % 2 == 0 => 2
3 % 2 == 0 => null

takeUnless

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (!predicate(this)) this else null
}

这个函数刚好与takeIf筛选逻辑恰好相反。即:如果符合条件返回null,不符合条件返回对象本身。

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
fun main(args: Array<String>) {
val arr = listOf(1, 2, 3)
arr.forEach {
println("$it % 2 == 0 => ${it.takeUnless { it % 2 == 0 }}")
}
}

// 执行结果
1 % 2 == 0 => 1
2 % 2 == 0 => null
3 % 2 == 0 => 3

看到了吗?这里的执行结果和takeIf恰好相反。

repeat

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
contract { callsInPlace(action) }

for (index in 0 until times) {
action(index)
}
}

这个函数意思很明显,就是将一个动作重复指定的次数。动作对应一个lambda表达式,表达式中持有一个参数表示当前正在执行的次数索引。

看一个例子:

1
2
3
4
5
6
7
8
9
fun main(args: Array<String>) {
repeat(3) {
println("Just repeat, index: $it")
}
}

Just repeat, index: 0
Just repeat, index: 1
Just repeat, index: 2

简单总结

最后,我们用一个表格简单总结一下这些函数的用法:
函数|用途|特点|形式
:—:|:—:|:—:|:—:
run#1|执行block,并返回执行结果|block中无法获取接收者上下文|全局函数
run#2|执行block,并返回执行结果|block中可以获取接收者上下文|扩展函数
with|指定接收者,通过接收者执行block|block中可以获取接收者的上下文,可以对接收者数据类型做一定转换|全局函数
apply|执行block,并返回接收者实例本身|block中可以获取接收者的上下文,可用于链式编程|扩展
also|执行block,并返回接收者实例本身|block中有一个参数代表接收者实例,可用于链式编程|扩展
let|执行block,并返回执行结果|block中有一个参数代表接收者实例,可以对接收者数据类型做一定转换|扩展
takeIf|根据条件predicate判断当前实例是否符合要求|如果符合要求,返回当前实例本身;否则返回null|扩展函数
takeUnless|根据条件predicate判断当前实例是否不符合要求|如果不符合要求,返回当前实例本身;否则返回null|扩展

搞定Receiver

理解上面这几个函数,最重要的一点是要理解Receiver。遗憾的是,Kotlin官方文档中并没有针对Receiver的详细讲解。关于这部分的讲解,请扫描下方二维码关注欧阳锋工作室,回复搞定Receiver查看文章。

欢迎加入Kotlin交流群

关于Kotlin,如果你有任何问题,欢迎加入我的Kotlin交流群: 329673958。当前群交流活跃,问题解答速度很快,期待你的加入。

测测你的Kotlin基础

文 | 欧阳锋

本次测试满分160分,测测看,你能拿几分 <<<

1)Kotlin语言有基本数据类型吗?(5分)

2)Kotlin中有哪些访问控制符,分别代表什么意思?默认访问控制符是什么?(5分)

3)Kotlin接口是否允许有方法实现?是否允许声明成员变量?(5分)

4)Sealed类有什么作用?(5分)

5)Kotlin语言中如何实现类似Java创建匿名内部类对象?(10分)

6)Kotlin的扩展相对继承有什么优势?扩展方法的执行是否也遵循多态?(10分)

7)如果一个类同时实现多个接口,接口中存在同名方法,如何解决冲突?(5分)

8)Kotlin语言中是否存在static关键字,如果没有,如何声明静态变量,并实现与Java互通(5分)

9)使用Kotlin语言是否一定不会出现空指针异常?为什么?(10分)

10)Kotlin语言中推荐使用什么方式判断两个对象是否相等?如何判断两个对象是同一个对象?(5分)

11)如果使用Foo<out T: TUpper>这种方式声明泛型,使用Foo<*>这种方式接收该对象实例,代表什么意思?如何理解Kotlin泛型,与Java有什么区别?(10分)

12)如何自定义setter/getter方法?(5分)

13)使用语句var x = null声明变量x是否合法?如果合法,x的具体类型是什么?(5分)

14)下面这段代码的输出结果是什么?(10分)

1
2
3
val list = listOf(1, 2, 3)
list.add(4)
println(list)

15)下面这段代码的执行结果是什么?(5分)

1
2
3
4
5
6
7
8
9
// Kotlin端
object A {
fun init() {
println("A init")
}
}

// Java端
A.init()

16)下面代码的执行结果是什么?(5分)

1
2
3
fun sum(a: Int, b: Int) = { a + b }

println(sum(1, 3))

17)下面代码的执行结果是什么?(5分)

1
2
println(null is Any)
println(null!! is Nothing)

18)下面代码的执行结果是什么?(10分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
init() {
f()
}

val a = "a"

fun f() {
println(a)
}
}

fun main(args: Array<String>) {
A()
}

19)下面代码的执行结果是什么?(10分)

1
2
println(127 as Int? === 127 as Int?)
println(128 as Int? === 128 as Int?)

20)下面代码的执行结果是什么?如果运行异常,应该怎样修改才能达到预期效果?(10分)

1
2
3
4
(1..5).forEach {
if (it == 3) break
println(it)
}

21)下面代码的执行结果是什么?如果运行异常,应该怎样修改,为什么要这样修改?(10分)

1
2
3
val A.x: Int = 3

println(A().x)

22)下面这段代码的执行结果是什么?(10分)

1
2
3
4
5
6
7
8
9
10
11
12
13
fun isOdd(x: Int) = x % 2 != 0

fun length(s: String) = s.length

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}

fun main(args: Array<String>) {
val oddLength = compose(::isOdd, ::length)
val strings = listOf("a", "ab", "abc")
println(strings.filter(oddLength))
}

注:本篇例子Kotlin版本为1.2.31,更新版本可能存在部分差异

下面是你的基础等级:

得分 评价
0 ~ 80 基础较差
80 ~ 108 基础较好
108 ~ 160 基础很棒

查看答案方法

微信扫描下方二维码关注欧阳锋工作室,回复“Kotlin测试题答案”即可获取当前测试题答案

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

关于Kotlin抛弃可检测的异常处理,你怎么看?

可检测的异常英文翻译为Checked Exception,以下简称为CE。CE是一个备受争议的话题,有人主张CE是一个不可或缺的特性,也有人认为CE带来了一些问题,是一个冗余特性。这其中的支持者和反对者中都不乏软件行业的大佬。今天,我们借助这篇文章一起来讨论一下CE存在的必要性。

什么是CE

部分同学可能还不知道CE到底是什么。因此,在开始命题之前,有必要给大家解释一下CE的概念。

CE其实你每天都在用,只是你不知道它的存在而已。看一个例子你就明白了:

1
public void readString() throws IOException, FileNotFoundException {}

在这个函数声明的最后面,我们指定了函数可能抛出的异常,在使用的时候我们就可以针对具体的异常使用try {} catch() {}进行处理了。

由于我们在方法声明中指定了可能抛出的异常,因此方法具体可能抛出的异常是已知的,这就称之为可检测的异常(CE)。由于CE的声明来自编译阶段,因此IDE可能帮助你智能判断强制你针对某些异常进行处理,并给出友好提示。

这是一个很好的特性,不是吗?

想象一下,如果有一天,我们不能在函数上指定可抛出的异常了,会怎样?我们无法确定函数可能抛出的异常,并且可能会因为没有正确处理某个异常而导致程序奔溃。

可是,就是有人认为CE多此一举,并且Kotlin语言就是其中的支持者。为什么会有人坚定地认为CE多此一举呢?这是下一个我们要讨论的话题。

关于CE的争论

Java语言的CE设计借鉴了C++,而在受到Java影响的那些语言中,例如C#、Ruby等都去掉了CE的设计,这从实践的角度证明CE的存在确实意义不大。

在这个问题中,C#的主导工程师Anders Hejlsberg最有发言权。老实说,笔者并没有用过C#。可是,如果你搜索一下网络上关于C#和Java对比的文章你就会发现:C#被认为是一门比Java更优秀的编程语言,它始终在新增一些现代语言的特性,使你毫不费力地使用它。而Java作为一门古老的语言,受限于一些原始设计,在增加新特性时总是步履维艰,甚至有点不伦不类。

关于CE的设计,有人对Anders Hejlsberg进行过一次采访。采访的原文链接在这里:https://www.artima.com/intv/handcuffs.html

关于CE,Anders Hejlsberg认为它带来了两个问题版本问题扩展问题

所谓的版本问题是什么意思呢?Anders Hejlsberg举了一个例子:

假设有一个方法foo,它声明了抛出异常A、B和C,在下一个版本设计的时候,foo增加了一个新的特性,可能会抛出异常D。对于设计者来说,很明显这是一个大的改变,几乎可以确定的是,客户程序员不会去处理这个异常。为了避免出现问题,设计者不得不声明一个新的方法foo2,抛出一个新的异常。然后,客户程序员可以将针对foo的逻辑处理切换到foo2。

而所谓的扩展问题又是什么意思呢?这更好理解,以下来自Anders Hejlsberg的原话翻译并整理:

如果你在设计一个很小的系统,声明一个方法抛出一个异常,这很棒。可是,如果你尝试构建一个大的系统,其中包含了四、五个小系统的时候,问题来了。假设每个子系统可能抛出四到五个异常,而每上升一个系统,就犹如爬阶梯,异常数量会指数倍增加,最终你可能处理的异常将达到40个甚至80个。很显然,这是一个很糟糕的设计!

Anders Hejlsberg的话有理有据。可是,中国的 王垠 并不同意这个观点。关于Kotlin的CE设计,他写了一篇文章专门讲了这个问题,文章的原稿在这里:Kotlin 和 Checked Exception

看完王垠的文章,你会发现,他并不赞同Anders Hejlsberg的话。他认为,所谓的版本问题和扩展问题,其实都来自于程序员的滥用。只要处理得当,CE带来的好处是无法取代的。

其实,关于CE的争论还远不止Anders Hejlsberg和王垠两人。互联网上有很多关于这个问题的讨论。如果你感兴趣,可以Google了解一下。

你怎么看?

很显然,Kotlin语言受到了C#设计的影响,手起刀落,去掉了CE的设计。对于客户程序员来说,显而易见的一个改变是,你再也不能在Kotlin的方法声明中指定可能抛出的异常了。对于Kotlin语言的这种设计,你是赞成还是反对呢?

欢迎参与投票讨论

扫描下方二维码关注欧阳锋工作室,回复“CE”参与投票,或在所有文章中选择同名文章进行投票。

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

最完整的Markdown基础教程

提到Markdown,很多人首先想到的是简单、Easy等等。的确,Markdown为简单而生。Markdown是由一个叫 JOHN GRUBER 的哥们发明的,它的语法用一篇文章就可以说完。不相信吗?让我们来试试看…

基础语法

标题

Markdown支持6种级别的标题,对应html标签 h1 ~ h6

1
2
3
4
5
6
# h1
## h2
### h3
#### h4
##### h5
###### h6

以上标记效果如下:

h1

h2

h3

h4

h5
h6

除此之外,Markdown还支持另外一种形式的标题展示形式,其类似于 Setext 标记语言的表现形式,使用下划线进行文本大小的控制

1
2
3
4
这是一级标题
===
这是二级标题
---

使用这种方式处理标题仅有两种表现形式,即一级标题和二级标题。遗憾的是,简书并不支持这种语法。因此,我们用一张图来展示效果:

这种处理方式在Github的一些开源工程上面比较常见,显而易见的缺点是:文字大小控制级别有限。

段落及区块引用

需要记住的是,Markdown其实就是一种易于编写的普通文本,只不过加入了部分渲染文本的标签而已。其最终依然会转换为html标签,因此使用Markdown分段非常简单,前后至少保留一个空行即可。

而另外一个比较常见的需求就是,我们可能希望对某段文字进行强调处理。Markdown提供了一个特殊符号>用于段首进行强调,被强调的文字部分将会高亮显示

1
> 这段文字将被高亮显示...

以上标记显示效果如下:

这段文字将被高亮显示…

插入链接或图片

Markdown针对链接和图片的处理也比较简单,可以使用下面的语法进行标记

1
2
[点击跳转至百度](http://www.baidu.com)
![图片](https://user-gold-cdn.xitu.io/2018/4/18/162d75d959444389?w=1240&h=703&f=jpeg&s=56927)

以上标记显示效果如下:

点击跳转至百度
图片

注: 引用图片和链接的唯一区别就是在最前方添加一个感叹号。

列表

Markdown支持有序列表和无序列表两种形式:

  • 无序列表使用*或+或-标识
  • 有序列表使用数字加.标识,例如:1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
* 黄瓜
* 玉米
* 茄子

+ 黄瓜
+ 玉米
+ 茄子

- 黄瓜
- 玉米
- 茄子

1. 黄瓜
2. 玉米
3. 茄子

以上标记显示效果如下:

  • 黄瓜
  • 玉米
  • 茄子
  • 黄瓜
  • 玉米
  • 茄子
  • 黄瓜
  • 玉米
  • 茄子
  1. 黄瓜
  2. 玉米
  3. 茄子

注:这里比较有趣的地方是,对于有序列表,Markdown将只关注你的第一个项目的数字编号。例如:如果第一个项目编号是3,以此类推,第二个项目应该是4,最终将显示为3、4、5。而如果你指定了第一个编号,后面的编号指定错误也没有关系,Markdown将只在乎你的第一个项目编号。

使用列表的一些注意事项

如果在单一列表项中包含了多个段落,为了保证渲染正常,*与段落首字母之间必须保留四个空格

1
2
3
4
5
6
*    段落一

小段一
* 段落二

小段二

以上标记显示效果如下:

  • 段落一

    小段一

  • 段落二

    小段二

另外,如果在列表中加入了区块引用,区域引用标记符也需要缩进4个空格

1
2
3
4
* 段落一
> 区块标记一
* 段落二
> 区块标记二
  • 段落一

    区块标记一

  • 段落二

    区块标记二

注:记住一个原则,如果在和列表配合使用的时候出现了问题,就缩进一次,四个空格或者一个制表符代表一次缩进。如果一次缩进没有解决问题,那就两次。

分隔线

有时候,为了排版漂亮,可能会加入分隔线。Markdown加入分隔线非常简单,使用下面任意一种形式都可以

1
2
***
---

*

产生分隔线的语法要求比较松,符号之间添加空格也可以。

强调

有时候,我们希望对某一部分文字进行强调,使用*或_包裹即可。使用单一符号标记的效果是斜体,使用两个符号标记的效果是加粗

1
2
3
4
5
*这里是斜体*
_这里是斜体_

**这里是加粗**
__这里是加粗__

这里是斜体
这里是斜体

这里是加粗
这里是加粗

高级用法

插入代码块

Markdown在IT圈子里面比较流行的一个重要原因是,它能够轻松漂亮地插入代码。

方法是,使用反引号`进行包裹即可。如果是行内代码引用,使用单个反引号进行包裹

这是一段var x = 3行内代码

如果插入一整段代码,需要至少使用两个以上反引号进行包裹, 看效果:

1
2
3
fun (x: Int, y: Int): Int {
return x + y
}

注:很多人不知道怎么输入反引号。在英文模式下,找到键盘最左侧esc键下面的第一个键点击即可。

插入表格

表格是Markdown语法中比较复杂的一个,其语法如下:

1
2
3
表头|条目一|条目二
:---:|:---:|:---:
项目|项目一|项目二

以上标记显示效果如下:

表头 条目一 条目二
项目 项目一 项目二

注:三个短斜杠左右的冒号用于控制对齐方式,只放置左边冒号表示文字居左,只放置右边冒号表示文字居右,如果两边都放置冒号表示文字居中。

其它

特殊符号处理

Markdown使用反斜杠\插入语法中用到的特殊符号。在Markdown中,主要有以下几种特殊符号需要处理:

1
2
3
4
5
6
7
8
9
10
11
12
\   反斜线
` 反引号
* 星号
_ 底线
{} 花括号
[] 方括号
() 括弧
# 井字号
+ 加号
- 减号
. 英文句点
! 惊叹号

例如,如果你需要插入反斜杠,就连续输入两个反斜杠即可:\\ => \ 。

注:在内容中输入以上特殊符号的时候一定要注意转义,否则将导致内容显示不全,甚至排版混乱。

如何给文字上色

使用Markdown的同学最郁闷的地方恐怕就是不能给文字添加颜色了。事实上,Markdown的最初目标就是为纯写作而生的。因此,它并没有考虑文字颜色这一点。所以,单纯使用Markdown设置文字颜色已经做不到了。但你可以这样做:

  1. 先用Markdown编辑完成
  2. 导出为html,在需要上色的部分手动添加标签<font color='#ff0000'></font>保存即可。

Markdown软件推荐

如果你是一个Mac用户,我推荐你使用开源的 MacDown
Macdown

如果你愿意花钱的话,一些收费产品其实更好。例如:Byword,Ulysses,Typora等等都很不错。

日常使用,我就用Macdown。写书,我就用Ulysses。

简单总结

Markdown是一门比html更简单的标记语言,其主要用于日常写作。最终通过相应的编辑器或者脚本转换成html用于页面渲染。如果你是一个作家,或者是一个程序员,Markdown对你来说是一门必备的技能。掌握Markdown真的很简单,如你所见,这篇文章使用了这么短的篇幅就已经将Markdown的语法全部介绍完了。

不过,由于Markdown基础语法的限制,出现了一些针对Markdown语法的加强版本。它支持的语法特性更多,但这不是我们这篇文章的讨论范围。如果你希望快速掌握Markdown,很简单,动起来吧!

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

Kotlin语言中的泛型设计哲学

文 | 欧阳锋

Kotlin语言的泛型设计很有意思,但并不容易看懂。关于这个部分的官方文档,我反复看了好几次,终于弄明白Kotlin语言泛型设计的背后哲学。这篇文章将讲述Kotlin泛型设计的整个思考过程及其背后的哲学思想,希望可以解答你心中的疑问。不过,可以预见地,即使看完,你也未必完全明白这篇文章在说什么,但至少希望你通过这篇文章可以快速掌握Kotlin泛型的用法。

Kotlin泛型的设计初衷

我们认为,Kotlin是一门比Java更优秀的JVM编程语言,Kotlin泛型设计的初衷就是为了解决Java泛型设计中一些不合理的问题。这样说可能不够直观,看下面这个例子:

1
2
3
 List<String> strs = new ArrayList<>();
// 这里将导致编译错误,Java语言不允许这样做
List<Object> objs = strs;

很明显,String和Object之间存在着安全的隐式转换关系。存放字符串的集合应该可以自由转换为对象集合。这很合理,不是吗?

如果你这样认为的话,就错了!继续往下看,我们扩展这个程序:

1
2
3
4
5
List<String> strs = new ArrayList<>();
List<Object> objs = strs;
objs.add(1);

String s = strs.get(0);

很明显,这不合理!我们在第一个位置存入了整型数值1,却在取的时候将它当成了字符串。strs本身是一个字符串集合,用字符串接收读取的数据的逻辑是合理的。却因为错误的类型转换导致了不安全写入出现了运行时类型转换问题,因此,Java语言不允许我们这样做。

大多数情况下,这种限制没有问题。可是,在某些情况下,这并不合理。看下面的例子:

1
2
3
4
5
6
7
interface List<T> {
void addAll(List<T> t);
}

public void copy(List<String> from, List<Object> to) {
to.addAll(from);
}

这是一个类型绝对安全的操作,但在Java语言中这依然是不允许的。原因是,泛型是一个编译期特性,一旦指定,运行期类型就已经固定了。换而言之,泛型操作的类型是不可变的。这就意味着,List并不是List的子类型。

为了允许正确执行上述操作,Java语言增加了神奇的通配符操作魔法。

1
2
3
interface List<T> {
void addAll(List<? extends T> t);
}

? extends T意味着集合中允许添加的类型不仅仅是T还包括T的子类,但这个集合中可以添加的类型在集合参数传入addAll时就已经确定了。因此,这并不影响参数集合中可以存放的数据类型,它带来的一个直接影响就是addAll方法参数中终于可以传入泛型参数是T或者T的子类的集合了,即上面的copy方法将不再报错。

这很有意思,在使用通配符之前我们并不能传入类型参数为子类型的集合。使用通配符之后,居然可以了!这个特性在C#被称之为协变(covariant)。

协变这个词来源于类型之间的绑定。以集合为例,假设有两个集合L1、L2分别绑定数据类型F、C,并且F、C之间存在着父子关系,即F、C之间存在着一种安全的从C->F的隐式转换关系。那么,集合L1和L2之间是否也存在着L2->L1的转换关系呢?这就牵扯到了原始类型转换到绑定类型的集合之间的转换映射关系,我们称之为“可变性”。如果原始类型转换和绑定类型之间转换的方向相同,就称之为“协变”。

用一句话总结协变:如果绑定对象和原始对象之间存在着相同方向的转换关系,即称之为协变

PS:以上关于协变的概念来自笔者的总结,更严谨的概念请参考C#官方文档

文章开头我们将不可变泛型通过通配符使其成为了可变泛型参数,现在我们知道这种行为叫做协变。很明显,协变转换中写入是不安全的。因此,协变行为仅仅用于读取。如果需要写入怎么办呢?这就牵扯到了另外一个概念逆变(contravariance)。

逆变协变恰恰相反,即如果F、C之间存在着父子转换关系,L1、L2之间存在着从L1->L2的转换关系。其绑定对象的转换关系与原始对象的转换关系恰好相反。Java语言使用关键字super(?super List)实现逆变

举个例子:假设有一个集合List<? super String>,你将可以安全地使用add(String)或set(Int,String)方法。但你不能通过get(Int)返回String对象,因为你无法确定返回的对象是否是String类型,你最终只能得到Object。

因此,我们认为,逆变可以安全地写入数据,但并不能安全地读取,即最终不能获取具体的对象数据类型。

为了简化理解,我们引入官方文档中 Joshua Bloch 说的一句话:

Joshua Bloch calls those objects you only read from Producers, and those you only write to Consumers. He recommends: “For maximum flexibility, use wildcard types on input parameters that represent producers or consumers”

Joshua Bloch是Java集合框架的创始人,他把那些只能读取的对象叫做生产者;只能写入的对象叫做消费者。为了保证最大灵活性,他推荐在那些代表了生产者和消费者的输入参数上使用通配符指定泛型。

相对于Java的通配符,Kotlin语言针对协变逆变引入两个新的关键词outin

out用于协变,是只读的,属于生产者,即用在方法的返回值位置。而in用于逆变,是只写的,属于消费者,即用在方法的参数位置。

用英文简记为:POCI = Producer Out , Consumer In。

如果一个类中只有生产者,我们就可以在类头使用out声明该类是对泛型参数T协变的:

1
2
3
interface Link<out T> {
fun node(): T
}

同样地,如果一个类中只有消费者,我们就可以在类头使用in声明该类是对泛型参数T逆变的:

1
2
3
interface Repo<in T> {
fun add(t: T)
}

out等价于Java端的? extends List通配符,而in等价于Java端的? super List通配符。因此,类似下面的转换是合理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Link<out T> {
fun node(): T
}

fun f1(linkStr: Link<String>) {
// 这是一个合理的协变转换
val linkAny: Link<Any> = linkStr
}

interface Repo<in T> {
fun add(t: T)
}

fun f2(repoAny: Repo<Any>) {
// 这是一个合理的逆变转换
val repoStr: Repo<String> = repoAny
}

小结:协变和逆变

协变逆变对于Java程序员来说是一个全新的概念,为了便于理解,我用一个表格做一个简单的总结:

- 协变 逆变
关键字 out in
读写 只读 可写
位置 返回值 参数
角色 生产者 消费者

类型投影

在上面的例子中,我们直接在类体声明了泛型参数的协变或逆变类型。在这种情况下,就严格限制了该类中只允许出现该泛型参数的消费者或者生产者。很显然,这种场景并不多见,大多数情况下,一个类中既存在着消费者又存在着生产者。为了适应这种场景,我们可以将协变或逆变声明写在方法参数中。Kotlin官方将这种方式叫做 类型投影(Type Projection)

这里我们直接使用官方文档的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Array<T>(val size: Int) {
fun get(index: Int): T { /* ... */ }
fun set(index: Int, value: T) { /* ... */ }
}

fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }

// 由于泛型参数的不变性,这里将出现问题
copy(ints, any)

很明显,我们希望from参数可以接收元素为Any或其子类的任意元素,但我们并不希望修改from,以防止出现类似文章开头的问题。因此,我们可以在from参数中添加out修饰,使其协变:

1
2
fun copy(from: Array<out Any>, to: Array<Any>) {
}

一旦添加out修饰符,你就会发现,当你尝试调用set方法的时候,编译器将会提示你在out修饰的情况下禁止调用该方法。

注:Java语言在使用”协变“的情况下,from参数依然可以调用set方法。从这里可以看出,Kotlin语言在泛型安全控制上比Java更加精细。

星号投影

除了上述明确的类型投影方式之外,还有一种非常特殊的投影方式,称之为星号投影(star projection)。

在某些情况下,我们并不知道具体的类型参数信息。为了适应这种情况,Java语言中我们会直接忽略掉类型参数:

1
2
3
4
5
6
7
8
class Box<T> {
public void unPack(T t) {
...
}
}

// 在不确定类型参数的情况下,我们会这样做
Box box = new Box();

在Kotlin语言中,我们使用星号对这种情况进行处理。因为,Kotlin针对泛型有严格的读写区分。同样地,使用*号将限制泛型接口的读写操作:

  • Foo<out T: TUpper>,这种情况下,T是协变类型参数,上边界是TUpper。Foo<*>等价于Foo,这意味着你可以安全地从Foo<*>读取TUpper类型。
  • Foo<in T>,在这种情况下,T是逆变类型参数,下边界是T。Foo<*>等价于Foo,这意味着在T未知的情况下,你将无法安全写入Foo<*>。
  • Foo<T: TUpper>,在这种情况下,T是不可变的。Foo<*>等价于你可以使用Foo安全读取值,写入等价于Foo,即无法安全写入。

泛型约束

在泛型约束的控制上,Kotlin语言相对于Java也技高一筹。在大多数情况下,泛型约束需要指定一个上边界。这同Java一样,Kotlin使用冒号代替extends:

1
fun <T: Animal> catch(t: T) {}

在使用Java的时候,经常碰到这样一个需求。我希望泛型参数可以约束必须同时实现两个接口,但遗憾的是Java语言并没有给予支持。令人惊喜的是,Kotlin语言对这种场景给出了自己的实现:

1
2
3
4
fun <T> swap(first: List<T>, second: List<T>) where T: CharSequence, 
T: Comparable<T> {

}

可以看到,Kotlin语言使用where关键字控制泛型约束存在多个上边界的情况,此处应该给Kotlin鼓掌。

总结

Kotlin语言使用协变逆变来规范可变泛型操作,out关键字用于协变,代表生产者。in关键字用于逆变,代表消费者。out和in同样可以用于方法参数的泛型声明中,这称之为类型投影。在针对泛型类型约束的处理上,Kotlin增加了多个上边界的支持。

Kotlin语言最初是希望成为一门编译速度比Scala更快的JVM编程语言!为了更好地设计泛型,我们看到它从C#中引入了协变逆变的概念。这一次,我想,它至少同时站在了Scala和C#的肩膀上。

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×