Kotlin 1.1的新功能

译自《What's New in Kotlin 1.1》

JavaScript

从Kotlin 1.1开始,JavaScript目标不再被认为是实验性的。 支持所有语言功能,还有许多新工具可用于与前端开发环境集成。 有关更改的更详细列表,请参阅下文(below) 。

协同程序(实验性)

Kotlin 1.1中的关键新功能是协同程序(coroutines) ,支持async/awaityield和类似的编程模式。 Kotlin设计的关键特征是协同程序执行(coroutine execution)的实现是库的一部分,而不是语言,所以你不必受任何特定的编程范例或并发库的约束。

协同程序实际上是一种轻质线程(light-weight thread),可以在以后暂停和恢复(suspended and resumed later)。 通过挂起函数(suspending functions)支持协同程序:调用这样一个函数可能会暂停协同程序,而要启动一个新的协同程序,我们通常使用一个匿名挂起函数(anonymous suspending functions)(即suspending lambdas)。

我们来看看async/await ,这是在外部库kotlinx.coroutines中实现的:

// runs the code in the background thread pool
fun asyncOverlay() = async(CommonPool) {
    // start two async operations
    val original = asyncLoadImage("original")
    val overlay = asyncLoadImage("overlay")
    // and then apply overlay to both results
    applyOverlay(original.await(), overlay.await())
}

// launches new coroutine in UI context
launch(UI) {
    // wait for async overlay to complete
    val image = asyncOverlay().await()
    // and then show it in UI
    showImage(image)
}

这里,async { ... }启动协同程序,当我们使用await()时,在正被await()的操作被执行的同时,协同程序的执行被暂停,并且在正被await()的操作完成时被恢复(可能在不同的线程上)。

标准库使用协程(coroutines)来支持具有yield()yieldAll()函数的延迟生成的序列 (lazily generated sequences)。 在这样的序列中,返回序列元素的代码块在每个元素被检索之后被暂停,并且当请求下一个元素时被恢复。 以下是一个例子:

import kotlin.coroutines.experimental.*

fun main(args: Array<String>) {
    val seq = buildSequence {
        for (i in 1..5) {
            // yield a square of i
            yield(i * i)
        }
        // yield a range
        yieldAll(26..28)
    }

    // print the sequence
    println(seq.toList())
}

运行上面的代码查看结果。 随意编辑它并再次运行!

有关更多信息,请参阅协程文档(coroutine documentation)和教程(tutorial) 。

请注意,协程程序目前被认为是一个实验功能 ,这意味着Kotlin团队在最终的1.1版本之后不承诺支持此功能的向后兼容性。

其他语言功能

Type aliases

类型别名允许您为现有类型定义替代名称。 这对于通用类型(如集合)以及函数类型最为有用。 这是一个例子:

typealias OscarWinners = Map<String, String>

fun countLaLaLand(oscarWinners: OscarWinners) =
oscarWinners.count { it.value.contains("La La Land") }

// Note that the type names (initial and the type alias) are interchangeable:
fun checkLaLaLandIsTheBestMovie(oscarWinners: Map<String, String>) =
oscarWinners["Best picture"] == "La La Land"

fun oscarWinners(): OscarWinners {
    return mapOf(
            "Best song" to "City of Stars (La La Land)",
            "Best actress" to "Emma Stone (La La Land)",
            "Best picture" to "Moonlight" /* ... */)
}

fun main(args: Array<String>) {
    val oscarWinners = oscarWinners()

    val laLaLandAwards = countLaLaLand(oscarWinners)
    println("LaLaLandAwards = $laLaLandAwards (in our small example), but actually it's 6.")

    val laLaLandIsTheBestMovie = checkLaLaLandIsTheBestMovie(oscarWinners)
    println("LaLaLandIsTheBestMovie = $laLaLandIsTheBestMovie")
}

有关详细信息,请参阅文档(documentation)和KEEP

绑定的可调用引用 (Bound callable references)

现在可以使用::运算符来获取指向特定对象实例的方法或属性的成员引用 。 以前只能用lambda表示。 以下是一个例子:

val numberRegex = "\\d+".toRegex()
val numbers = listOf("abc", "123", "456").filter(numberRegex::matches)

fun main(args: Array<String>) {
    println("Result is $numbers")
}

阅读文档(documentation)和KEEP了解更多详情。

密封和数据类 (Sealed and data classes)

Kotlin 1.1删除了Kotlin 1.0中存在的封装和数据类的一些限制。 现在,您可以在同一个文件的顶层定义顶级密封类(sealed class)的子类,而不仅仅是封装类的嵌套类(nested classes of the sealed class)。 数据类(Data classes)现在可以扩展其他类。 这可以用来很好且干净地定义表达式类的层次结构(hierarchy of expression classes):

sealed class Expr

data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

fun eval(expr: Expr): Double = when (expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
}
val e = eval(Sum(Const(1.0), Const(2.0)))

fun main(args: Array<String>) {
    println("e is $e") // 3.0
}

阅读文档(documentation)或密封类(sealed class)和数据类(data class) 的KEEP了解更多详细信息。

lambda表达式的解构(Destructuring in lambdas)

现在可以使用解构声明(destructuring declaration)语法来解包传递给lambda的参数。 以下是一个例子:

fun main(args: Array<String>) {
    val map = mapOf(1 to "one", 2 to "two")
    // before
    println(map.mapValues { entry ->
                           val (key, value) = entry
                           "$key -> $value!"
                          })
    // now
    println(map.mapValues { (key, value) -> "$key -> $value!" })    
}

阅读文档(documentation)和KEEP了解更多详情。

用于未使用参数的下划线 (Underscores for unused parameters)

对于具有多个参数的lambda,可以使用_字符替换不使用的参数的名称:

fun main(args: Array<String>) {
    val map = mapOf(1 to "one", 2 to "two")

    map.forEach { _, value -> println("$value!") }    
}

这也在解构声明中起作用:

data class Result(val value: Any, val status: String)

fun getResult() = Result(42, "ok").also { println("getResult() returns $it") }

fun main(args: Array<String>) {
    val (_, status) = getResult()
    println("status is '$status'")
}

阅读KEEP了解更多详情。

用于数字文字的下划线 (Underscores in numeric literals)

就像Java 8一样,Kotlin现在允许在数字文字中使用下划线来分隔数字组:

val oneMillion = 1_000_000
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010

fun main(args: Array<String>) {
    println(oneMillion)
    println(hexBytes.toString(16))
    println(bytes.toString(2))
}

阅读KEEP了解更多详情。

更短的属性语法 (Shorter syntax for properties)

对于将getter定义为表达式主体的属性,现在可以省略属性类型:

data class Person(val name: String, val age: Int) {
    val isAdult get() = age >= 20 // Property type inferred to be 'Boolean'
}

fun main(args: Array<String>) {
    val akari = Person("Akari", 26)
    println("$akari.isAdult = ${akari.isAdult}")
}

内联属性访问器 (Inline property accessors)

您现在可以使用inline修饰符标记属性访问器,如果属性没有后缀字段。 这些访问器的编译方式与内联函数相同。

public val <T> List<T>.lastIndex: Int
inline get() = this.size - 1

fun main(args: Array<String>) {
    val list = listOf('a', 'b')
    // the getter will be inlined
    println("Last index of $list is ${list.lastIndex}")
}

您也可以将整个属性标记为inline - 那么就会将修饰符应用于两个访问器。

阅读文档(documentation)和KEEP 了解更多详情。

本地委托属性 (Local delegated properties)

您现在可以对局部变量使用委托属性(delegated property )语法。 一个可能的用途是定义一个懒惰的局部变量:

import java.util.Random

fun needAnswer() = Random().nextBoolean()

fun main(args: Array<String>) {
    val answer by lazy {
        println("Calculating the answer...")
        42
    }
    if (needAnswer()) {                     // returns the random value
        println("The answer is $answer.")   // answer is calculated at this point
    }
    else {
        println("Sometimes no answer is the answer...")
    }
}

阅读KEEP了解更多详情。

拦截委托属性绑定 (Interception of delegated property binding)

对于委托属性(delegated properties) ,现在可以使用provideDelegate运算符拦截对属性绑定的委托(intercept delegate to property binding)。 例如,如果我们要在绑定之前检查属性名称,我们可以这样写:

class ResourceLoader<T>(id: ResourceID<T>) {
   operator fun provideDelegate(thisRef: MyUI, prop: KProperty<*>): ReadOnlyProperty<MyUI, T> {
       checkProperty(thisRef, prop.name)
       ... // property creation
   }

   private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

class MyUI {
   val image by bindResource(ResourceID.image_id)
   val text by bindResource(ResourceID.text_id)
}

在创建MyUI实例期间,将为每个属性调用provideDelegate方法,并且可以立即执行必要的验证。

阅读文档(documentation )了解更多详情。

通用枚举值访问 (Generic enum value access)

现在可以以通用的方式枚举枚举类的值。

enum class RGB { RED, GREEN, BLUE }

inline fun <reified T : Enum<T>> printAllValues() {
    print(enumValues<T>().joinToString { it.name })
}

fun main(args: Array<String>) {
    printAllValues<RGB>() // prints RED, GREEN, BLUE
}

DSL中隐式接收器的范围控制 (Scope control for implicit receivers in DSLs)

@DslMarker注释允许限制在DSL上下文中来自外部范围的接收器的使用。 考虑规范的HTML构建器示例 (HTML builder example):

table {
    tr {
        td { +"Text" }
    }
}

在Kotlin 1.0中,传递给tdlambda中的代码可以访问三个隐含的接收器:传递给table ,给tr和给td 。 这允许您调用在上下文中无意义的方法 - 例如在td调用tr,从而将<tr>标签放在<td>

在Kotlin 1.1中,您可以限制,因此只有在td的隐式接收器上定义的方法才能在传递给tdlambda内部可用。 您可以通过定义标注有@DslMarker元注释的注释并将其应用于标记类的基类来实现。

阅读文档(documentation)和KEEP了解更多详情。

rem操作符

mod运算符现在已被弃用,而rem则被替代。 看到这个(this issue)问题的动机。

标准库

字符串到数字转换 (String to number conversions)

在String类中有一堆新的扩展将其转换为一个数字,而不会在无效数字上抛出异常: String.toIntOrNull(): Int?, String.toDoubleOrNull(): Double?等等

val port = System.getenv("PORT")?.toIntOrNull() ?: 80

还有整数转换函数,如Int.toString()String.toInt()String.toIntOrNull() ,每个都使用radix参数进行重载,这允许指定转换的基础(2到36)。

onEach()

onEach()是一个小而有用的扩展函数,用于集合和序列,它允许在操作链中的集合/序列的每个元素上执行一些可能具有副作用的操作。 在iterable上,它的行为类似于forEach()但是还可以进一步返回iterable实例。 在序列(sequences)上,它返回一个包装序列(wrapping sequence),它在元素被迭代时懒惰地应用给定的动作。

inputDir.walk()
        .filter { it.isFile && it.name.endsWith(".txt") }
        .onEach { println("Moving $it to $outputDir") }
        .forEach { moveFile(it, File(outputDir, it.toRelativeString(inputDir))) }

also(), takeIf() 以及takeUnless()

这些是三种通用扩展函数,适用于任何接收器(receiver)。

also 就像 apply :它接受接收器(receiver),对它做一些动作,并返回该接收器(receiver)。 不同之处在于,在apply的块内部,接收器(receiver)是作为this可用的,而在also的块内部,接收器(receiver)作为it可用(如果需要,可以给它另一个名称)。 当您不想遮挡外部范围的this时,这很方便:

class Block {
    lateinit var content: String
}

fun Block.copy() = Block().also {
    it.content = this.content
}

// using 'apply' instead
fun Block.copy1() = Block().apply {
    this.content = this@copy1.content
}

fun main(args: Array<String>) {
    val block = Block().apply { content = "content" }
    val copy = block.copy()
    println("Testing the content was copied:")
    println(block.content == copy.content)
}

takeIf就像一个单一值的filter 。 它检查接收器(receiver)是否满足预测(predicate),且如果它满足则返回接收器(receiver),或者如果没有满足则返回null 。 结合一个elvis操作符和早期的返回,它允许编写如下结构:

val outDirFile = File(outputDir.path).takeIf { it.exists() } ?: return false
// do something with existing outDirFile
fun main(args: Array<String>) {
    val input = "Kotlin"
    val keyword = "in"

    val index = input.indexOf(keyword).takeIf { it >= 0 } ?: error("keyword not found")
    // do something with index of keyword in input string, given that it's found
    
    println("'$keyword' was found in '$input'")
    println(input)
    println(" ".repeat(index) + "^")
}

takeUnlesstakeIf相同,但它需要反向预测(predicate)。 当它不符合预测(predicate)时返回接收器(receiver),否则返回null 。 所以上面的例子之一可以使用takeUnless重写如下:

val index = input.indexOf(keyword).takeUnless { it < 0 } ?: error("keyword not found")

当您具有可调用引用(callable reference)而不是lambda时也很方便:

private fun testTakeUnless(string: String) {
    val result = string.takeUnless(String::isEmpty)

    println("string = \"$string\"; result = \"$result\"")
}

fun main(args: Array<String>) {
    testTakeUnless("")
    testTakeUnless("abc")
}

groupingBy()

此API可用于按键(key)分组,同时折叠(fold)每个组。 例如,它可以用于计算从每个字母开始的单词数:

fun main(args: Array<String>) {
    val words = "one two three four five six seven eight nine ten".split(' ')
    val frequencies = words.groupingBy { it.first() }.eachCount()
    println("Counting first letters: $frequencies.")

    // The alternative way that uses 'groupBy' and 'mapValues' creates an intermediate map, 
    // while 'groupingBy' way counts on the fly.
    val groupBy = words.groupBy { it.first() }.mapValues { (_, list) -> list.size }
    println("Comparing the result with using 'groupBy': ${groupBy == frequencies}.")
}

Map.toMap() 以及Map.toMutableMap()

这些功能可用于轻松复制映射:

class ImmutablePropertyBag(map: Map<String, Any>) {
    private val mapCopy = map.toMap()
}

Map.minus(key)

操作符plus提供了一种方法来添加键值对到只读映射生成一个新的映射,但是没有一个简单的方法来做相反的事情:从映射中删除一个键你必须诉诸不太直接的方法,例如Map.filter()Map.filterKeys()。 现在操作符minus弥补了这个差距。 有4个重载可用:用于删除单个键(a single key),一合集键(a collection of keys),一系列键(a sequence of keys)和一数组键(an array of keys)。

fun main(args: Array<String>) {
    val map = mapOf("key" to 42)
    val emptyMap = map - "key"
    
    println("map: $map")
    println("emptyMap: $emptyMap")
}

minOf() 以及 maxOf()

这些函数可用于查找两个或三个给定值的最小和最大值,其中值是原始数字或Comparable对象。 如果要比较本身不可比较的对象,那么每个函数也会占用一个附加的Comparator实例。

fun main(args: Array<String>) {
    val list1 = listOf("a", "b")
    val list2 = listOf("x", "y", "z")
    val minSize = minOf(list1.size, list2.size)
    val longestList = maxOf(list1, list2, compareBy { it.size })
    
    println("minSize = $minSize")
    println("longestList = $longestList")
}

类似Array的List实例化函数 (Array-like List instantiation functions)

类似于Array构造函数,现在有了创建ListMutableList实例的函数,并通过调用lambda初始化每个元素:

fun main(args: Array<String>) {
    val squares = List(10) { index -> index * index }
    val mutable = MutableList(10) { 0 }

    println("squares: $squares")
    println("mutable: $mutable")
}

Map.getValue()

Map上的这个扩展返回与给定键对应的现有值或引发异常,并提及未找到哪个键。 如果映射是使用withDefault生成,则此函数将返回默认值,而不是抛出异常。

fun main(args: Array<String>) {


    val map = mapOf("key" to 42)
    // returns non-nullable Int value 42
    val value: Int = map.getValue("key")

    val mapWithDefault = map.withDefault { k -> k.length }
    // returns 4
    val value2 = mapWithDefault.getValue("key2")

    // map.getValue("anotherKey") // <- this will throw NoSuchElementException
    
    println("value is $value")
    println("value2 is $value2")
}

抽象集合 (Abstract collections)

这些抽象类可以在实现Kotlin集合类时用作基类。 为了实现只读集合,有AbstractCollectionAbstractListAbstractSetAbstractMap ,对于可变集合,还有AbstractMutableCollectionAbstractMutableListAbstractMutableSetAbstractMutableMap 。 在JVM上,这些抽象可变集合从JDK的抽象集合继承其大部分功能。

数组操作功能 (Array manipulation functions)

标准库现在提供了一组用于逐个元素操作的函数:比较( contentEqualscontentDeepEquals ),哈希码计算( contentHashCodecontentDeepHashCode )以及转换为字符串( contentToStringcontentDeepToString )。 它们都支持JVM(它们作为java.util.Arrays的相应函数的别名)和JS(在Kotlin标准库中提供实现)。

fun main(args: Array<String>) {
    val array = arrayOf("a", "b", "c")
    println(array.toString())  // JVM implementation: type-and-hash gibberish
    println(array.contentToString())  // nicely formatted as list
}

JVM后端

Java 8字节码支持 (Java 8 bytecode support)

Kotlin现在可以选择生成Java 8字节码( -jvm-target 1.8命令行选项或Ant/Maven/Gradle中的相应选项)。 现在这并不会改变字节码的语义(特别是接口和lambdas中的默认方法与Kotlin 1.0完全相同),但是我们打算进一步利用这一点。

Java 8标准库支持 (Java 8 standard library support)

现在有标准库的单独版本,支持在Java 7和8中添加的新JDK API。如果需要访问新的API,请使用kotlin-stdlib-jre7kotlin-stdlib-jre8 maven 工件(artifacts),而不是标准的kotlin-stdlib 。 这些工件(artifacts)是在kotlin-stdlib之上的微型扩展,它们将它作为传递依赖关系( transitive dependency)带入您的项目。

字节码中的参数名称 (Parameter names in the bytecode)

Kotlin现在支持在字节码中存储参数名称。 这可以使用-java-parameters命令行选项启用。

常量内联 (Constant inlining)

现在编译器将const val属性的值嵌入到使用它们的位置。

可变闭包变量 (Mutable closure variables)

用于捕获不再具有volatile字段的lambdas中的可变闭包变量(mutable closure variables)的框类(box classes)。 这种改变提高了性能,但是在一些罕见的使用情况下可能会导致新的竞争条件。 如果受此影响,您需要提供自己的同步方式来访问变量。

javax.script支持 (javax.script support)

Kotlin现在与javax.script API(JSR-223)集成。 API允许在运行时评估代码段:

val engine = ScriptEngineManager().getEngineByExtension("kts")!!
engine.eval("val x = 3")
println(engine.eval("x + 2"))  // Prints out 5

请参阅这里( here)使用API​​的更大的示例项目。

kotlin.reflect.full

为了准备Java 9支持(prepare for Java 9 support) , kotlin-reflect.jar库中的扩展函数和属性已被移动到包kotlin.reflect.full。 旧包中的名称( kotlin.reflect )已被弃用,将在Kotlin 1.2中删除。 请注意,核心反射接口(如KClass )是Kotlin标准库的一部分,不是kotlin-reflect的部分 ,不受此次移动的影响。

JavaScript后端

统一标准库 (Unified standard library)

Kotlin标准库的大部分现在可以从编译为JavaScript的代码中使用。 特别地, kotlin包下定义了关键类,如集合( ArrayListHashMap等),异常( IllegalArgumentException等)和其他几个( StringBuilderComparator)。 在JVM上,这些名称是相应JDK类的类型别名(type aliases),而在JS上,这些类在Kotlin标准库中实现。

更好的代码生成 (Better code generation)

JavaScript后端现在可以生成更多的静态可检查代码,这对JS代码处理工具(比如minifieroptimizerslinters等)来说更为友善。

external修饰符 (The external modifier)

如果您需要以类型安全的方式访问Kotlin中以JavaScript实现的类,则可以使用external修饰符编写Kotlin声明。 (在Kotlin 1.0中,使用@native注释。) 与JVM目标不同,JS允许对类和属性使用external修饰符(use external modifier with classes and properties)。 例如,下面是如何声明DOM Node类:

external class Node {
    val firstChild: Node

    fun appendChild(child: Node): Node

    fun removeChild(child: Node): Node

    // etc
}

改进的import处理 (Improved import handling)

您现在可以更精确地描述应该从JavaScript模块导入的声明。 如果在外部声明中添加了@JsModule("<module-name>")注释,那么在编译期间它将被正确地导入模块系统(CommonJS或AMD)。 例如,使用CommonJS,声明将通过require(...)函数导入。 另外,如果要将声明导入为模块或全局JavaScript对象,则可以使用@JsNonModule注释。

例如,以下是将JQuery导入Kotlin模块的方法:

external interface JQuery {
    fun toggle(duration: Int = definedExternally): JQuery
    fun click(handler: (Event) -> Unit): JQuery
}

@JsModule("jquery")
@JsNonModule
@JsName("$")
external fun jquery(selector: String): JQuery

在这种情况下,JQuery将被导入为一个名为jquery的模块。 或者,它可以用作$对象,具体取决于Kotlin编译器配置使用的模块系统。

您可以在应用程序中使用这些声明,如下所示:

fun main(args: Array<String>) {
    jquery(".toggle-button").click {
        jquery(".toggle-panel").toggle(300)
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容