Using JSON with Custom Types

Demonstrates approaches for encoding and decoding different kinds of JSON in Swift.

演示在Swift中编码和解码不同类型的JSON的方法。

Overview

JSON data you send or receive from other apps, services, and files can come in many different shapes and structures. Use the techniques described in this sample to handle the differences between external JSON data and your app’s model types.

您从其他应用程序,服务和文件发送或接收的JSON数据可以有许多不同的形状和结构。 使用此示例中描述的技术来处理外部JSON数据与应用程序模型类型之间的差异。

Art

This sample defines a simple data type, GroceryProduct, and demonstrates how to construct instances of that type from several different JSON formats.

此示例定义了一个简单的数据类型GroceryProduct,并演示了如何从几种不同的JSON格式构造该类型的实例。

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}

Read Data from Arrays

Use Swift’s expressive type system to avoid manually looping over collections of identically structured objects. This playground uses array types as values to see how to work with JSON that’s structured like this:

使用Swift的表达式系统来避免手动循环相同结构对象的集合。 这个游乐场使用数组类型作为值来查看如何使用结构如下的JSON:

[
    {
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    }
]

Change Key Names

Learn how to map data from JSON keys into properties on your custom types, regardless of their names. For example, this playground shows how to map the "product_name" key in the JSON below to the name property on GroceryProduct:

了解如何将JSON 键中的数据映射到你自定义类型的属性中,无论其名称如何。 例如,此游乐场显示如何将下面JSON中的“product_name”键映射到GroceryProduct上的name属性:

{
    "product_name": "Banana",
    "product_cost": 200,
    "description": "A banana grown in Ecuador."
}

Custom mappings let you to apply the Swift API Design Guidelines to the names of properties in your Swift model, even if the names of the JSON keys are different.
通过自定义映射,您可以将Swift API设计指南应用于Swift模型中的属性名称,即使JSON键的名称不同也是如此。

Access Nested Data

Learn how to ignore structure and data in JSON that you don’t need in your code. This playground uses an intermediate type to see how to extract grocery products from JSON that looks like this to skip over unwanted data and structure:

了解如何忽略代码中不需要的JSON结构和数据。 这个游乐场使用中间类型来查看如何从JSON中提取杂货产品,看起来像这样跳过不需要的数据和结构:

[
    {
        "name": "Home Town Market",
        "aisles": [
            {
                "name": "Produce",
                "shelves": [
                    {
                        "name": "Discount Produce",
                        "product": {
                            "name": "Banana",
                            "points": 200,
                            "description": "A banana that's perfectly ripe."
                        }
                    }
                ]
            }
        ]
    }
]

Merge Data at Different Depths

Combine or separate data from different depths of a JSON structure by writing custom implementations of protocol requirements from Encodable and Decodable. This playground shows how to construct a GroceryProduct instance from JSON that looks like this:

通过编写来自Encodable和Decodable的协议要求的自定义实现,组合或分离来自JSON结构的不同深度的数据。 这个游乐场展示了如何从JSON构建一个GroceryProduct实例,如下所示:

{
    "Banana": {
        "points": 200,
        "description": "A banana grown in Ecuador."
    }
}

示例:

When you control the structure of your data types in Swift as well as the structure of the JSON you use for encoding and decoding, use the default format generated for you by the JSONEncoder and JSONDecoder classes.

当您在Swift中控制数据类型的结构和用于编码和解码的JSON结构一样时,请使用JSONEncoder和JSONDecoder类为您生成的默认格式。

However, when the data your code models follows a specification that shouldn't be changed, you should still write your data types according to Swift best practices and conventions. This sample shows how to use a type's conformance to the Codable protocol as the translation layer between the JSON representation of data and the corresponding representation in Swift.

但是,当代码模型的数据遵循不应更改的规范时,您仍应根据Swift最佳实践和约定编写数据类型。 此示例演示如何使用类型与Codable协议的一致性作为数据的JSON表示与Swift中的相应表示之间的转换层。

1.Read Data From Arrays

When the JSON you use contains a homogeneous array of elements, you add a conformance to the Codable protocol on the individual element's type. To decode or encode the entire array, you use the syntax [Element].self.

当您使用的JSON包含同构数组元素时,您可以在单个元素的类型上添加Codable协议的一致性。 要解码或编码整个数组,请使用语法[Element] .self。

In the example below, the GroceryProduct structure is automatically decodable because the conformance to the Codable protocol is included in its declaration. The whole array in the example is decodable based on the syntax used in the call to the decode method.

在下面的示例中,GroceryProduct结构是可自动解码的,因为与Codable协议的一致性包含在其声明中。 示例中的整个数组可以根据对decode方法的调用中使用的语法进行解码。

If the JSON array contains even one element that isn't a GroceryProduct instance, the decoding fails. This way, data isn't silently lost as a result of typos or a misunderstanding of the guarantees made by the provider of the JSON array.

如果JSON数组甚至包含一个不是GroceryProduct实例的元素,则解码失败。 这样,由于拼写错误或对JSON数组提供程序所做的保证的误解,数据不会无声地丢失。

let json = """
[
    {
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    {
        "name": "Orange",
        "points": 100
    }
]
""".data(using: .utf8)!
struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let decoder = JSONDecoder()
        let products = try? decoder.decode([GroceryProduct].self, from: json)
        for item in products! {
            print("\(item.name) -- \(item.points)")
            if let des = item.description {
                print(des)
            }
        }
    }
}

2.Change Key Names

Names you use in your Swift code don't always match the names in JSON that refer to the same values. When working with the JSONEncoder and JSONDecoder classes in Swift, you can easily adopt conventional Swift names in your data types even when using JSON that requires the use of other names.

您在Swift代码中使用的名称并不总是与引用相同值的JSON中的名称匹配。 在Swift中使用JSONEncoder和JSONDecoder类时,即使使用需要使用其他名称的JSON,也可以轻松地在数据类型中采用传统的Swift名称。

To create a mapping between Swift names and JSON names, you use a nested enumeration named CodingKeys within the same type that adds conformance to Codable, Encodable, or Decodable.

要在Swift名称和JSON名称之间创建映射,可以使用名为CodingKeys的嵌套枚举,该枚举在同一类型中添加与Codable,Encodable或Decodable的一致性。

In the example below, see how the Swift property name points is mapped to and from the name "product_name" when the property is encoded and decoded.

在下面的示例中,查看在对属性进行编码和解码时,Swift属性名称点如何映射到名称“product_name”以及如何映射。

let json = """
[
    {
        "product_name": "Bananas",
        "product_cost": 200,
        "description": "A banana grown in Ecuador."
    },
    {
        "product_name": "Oranges",
        "product_cost": 100,
        "description": "A juicy orange."
    }
]
""".data(using: .utf8)!
struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
    
    private enum CodingKeys: String, CodingKey {
        case name = "product_name"
        case points = "product_cost"
        case description
    }
}


class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let decoder = JSONDecoder()
        let products = try? decoder.decode([GroceryProduct].self, from: json)
        
        print("The following products are available:")
        for product in products! {
            print("\t\(product.name) (\(product.points) points)")
            if let description = product.description {
                print("\t\t\(description)")
            }
        }
    }
}

Although the name description is consistent between the two representations, you still include it in the CodingKeys enumeration because it's a value required by the GroceryProduct structure. Its enumeration case doesn't need an explicit raw value because its name is the same as the corresponding property name.

虽然两个表示之间的description变量是一致的,但您仍然将它包含在CodingKeys枚举中,因为它是GroceryProduct结构所需的值。 其枚举情况不需要显式原始值,因为其名称与相应的属性名称相同。

3.Access Nested Data

You can write an app that uses JSON from an external source or an existing local format. In either instance, you might find inconsistencies between the structure of the concepts you're modeling in your app and concepts modeled by the producer of the JSON. Sometimes, a logical bundle of data for your Swift program is spread out among several nested objects or arrays in the JSON you use. Bridge the structural gap by writing a decodable type that matches the structure of the JSON you're reading in. The decodable type serves as an intermediate type that's safe to decode. It serves as the data source in an initializer for the type that you'll use in the rest of your app.

您可以编写一个使用外部JSON或现有本地JSON的应用程序。 在任何一种情况下,您可能会发现在应用程序中你的模型结构与JSON中的模型结构之间存在不一致的地方。 有时,Swift程序的逻辑数据包分散在您使用的JSON的几个嵌套对象中或数组中。 通过编写与您正在读取的JSON结构相匹配的可解码类型来桥接结构间隙。可解码类型用作可安全解码的中间类型。 它作为初始化程序中的数据源,用于您将在应用程序接下来使用的类型。

With intermediate types, you can use the most natural types in your own code while maintaining compatibility with a variety of shapes of external JSON.

对于中间类型,您可以在自己的代码中使用最自然的类型,同时保持与各种形状的外部JSON的兼容性。

The example below introduces a type representing a grocery store and a list of the products it sells:

下面的示例介绍了代表杂货店的类型以及它销售的产品列表:

// An API might supply information about grocery stores using JSON that's structured as follows:
// API可能会使用JSON提供有关杂货店的信息,其结构如下:

let json = """
[
    {
        "name": "Home Town Market",
        "aisles": [
            {
                "name": "Produce",
                "shelves": [
                    {
                        "name": "Discount Produce",
                        "product": {
                            "name": "Banana",
                            "points": 200,
                            "description": "A banana that's perfectly ripe."
                        }
                    }
                ]
            }
        ]
    },
    {
        "name": "Big City Market",
        "aisles": [
            {
                "name": "Sale Aisle",
                "shelves": [
                    {
                        "name": "Seasonal Sale",
                        "product": {
                            "name": "Chestnuts",
                            "points": 700,
                            "description": "Chestnuts that were roasted over an open fire."
                        }
                    },
                    {
                        "name": "Last Season's Clearance",
                        "product": {
                            "name": "Pumpkin Seeds",
                            "points": 400,
                            "description": "Seeds harvested from a pumpkin."
                        }
                    }
                ]
            }
        ]
    }
]
""".data(using: .utf8)!
struct GroceryStore {
    var name: String
    var products: [Product]
    
    struct Product: Codable {
        var name: String
        var points: Int
        var description: String?
    }
}

The JSON returned by the API contains more information than is needed to populate the corresponding Swift type. In particular, it has a structural incompatibility with the GroceryStore structure defined earlier: its products are nested inside aisles and shelves. Although the provider of the JSON likely needs the extra information, it might not be useful inside all of the apps that depend on it.

API返回的JSON包含的信息多于填充相应Swift类型所需的信息。 特别是,它与前面定义的GroceryStore结构具有结构不兼容性:它的产品嵌套在过道和货架内。 虽然JSON的提供者可能需要额外的信息,但它可能在所有依赖它的应用程序中都没有用。

To extract the data you need from the outer containers, you write a type that mirrors the shape of the source JSON and mark it as Decodable. Then, write an initializer on the type you'll use in the rest of your app that takes an instance of the type that mirrors the source JSON.

要从外部容器中提取所需的数据,可以编写一个反映源JSON形状的类型,并将其标记为Decodable。 然后,在您将在应用程序的其余部分中使用的类型上编写初始化程序,该类型采用镜像源JSON的类型的实例。

In the example below, the GroceryStoreService structure serves as an intermediary between the grocery JSON and the GroceryStore structure that is ideal for its intended use in an app:

在下面的示例中,GroceryStoreService结构充当杂货JSON和GroceryStore结构之间的中介,非常适合在应用程序中使用它:

struct GroceryStoreService: Decodable {
    let name: String
    let aisles: [Aisle]
    
    struct Aisle: Decodable {
        let name: String
        let shelves: [Shelf]
        
        struct Shelf: Decodable {
            let name: String
            let product: GroceryStore.Product
        }
    }
}

Because the GroceryStoreService structure matches the structure of the source JSON—including aisles and shelves—its conformance to the Decodable protocol is automatic when the protocol is included in the structure's list of inherited types. The GroceryStore structure's nested Product structure is reused in the Shelf structure because the data uses the same names and types.

因为GroceryStoreService结构与源JSON的结构(包括通道和架子)匹配 - 当协议包含在结构的继承类型列表中时,它与Decodable协议的一致性是自动的。 GroceryStore结构的嵌套Product结构在Shelf结构中重用,因为数据使用相同的名称和类型。

To complete the GroceryStoreService structure's role as an intermediate type, use an extension to the GroceryStore structure. The extension adds an initializer that takes a GroceryStoreService instance and removes the unnecessary nesting by looping through and discarding the aisles and shelves:

要完成GroceryStoreService结构作为中间类型的角色,请使用GroceryStore结构的扩展。 扩展添加了一个初始化程序,它接受GroceryStoreService实例并通过循环并丢弃过道和架子来删除不必要的嵌套:

extension GroceryStore {
    init(from service: GroceryStoreService) {
        name = service.name
        products = []
        
        for aisle in service.aisles {
            for shelf in aisle.shelves {
                products.append(shelf.product)
            }
        }
    }
}

Using the relationships between types in the examples above, you can safely and succinctly read in JSON, pass it through the GroceryStoreService intermediate type, and use the resulting GroceryStore instances in your app:

使用上面示例中的类型之间的关系,您可以安全,简洁地读取JSON,将其传递给GroceryStoreService中间类型,并在您的应用中使用生成的GroceryStore实例:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let decoder = JSONDecoder()
        let serviceStores = try! decoder.decode([GroceryStoreService].self, from: json)
        
        let stores = serviceStores.map { GroceryStore(from: $0) }
        
        for store in stores {
            print("\(store.name) is selling:")
            for product in store.products {
                print("\t\(product.name) (\(product.points) points)")
                if let description = product.description {
                    print("\t\t\(description)")
                }
            }
        }
    }
}

4.Merge Data from Different Depths

合并来自不同深度的数据

Sometimes the data model used by a JSON file or API doesn't match the model you're using in an app. When that happens, you may need to merge or separate objects from the JSON when encoding and decoding. As a result, the encoding or decoding of a single instance involves reaching upward or downward in the JSON object's hierarchy.

有时,JSON文件或API使用的数据模型与您在应用程序中使用的模型不匹配。 发生这种情况时,您可能需要在编码和解码时将对象与JSON合并或分离。 因此,单个实例的编码或解码涉及在JSON对象的层次结构中向外层或向内层来获取。

The example below demonstrates a common occurrence of this style of data merging. It models a grocery store that keeps track of the name, price, and other details for each of the products it sells.

下面的示例演示了此类数据合并的常见情况。 它为杂货店建模,跟踪其销售的每种产品的名称,价格和其他详细信息。

let json = """
{
    "Banana": {
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    "Orange": {
        "points": 100
    }
}
""".data(using: .utf8)!

Notice that the name of the product is also the name of the key that defines the rest of the details of the product. In this case, that the information for the "Banana" product is stored in an object nested under the product name itself. However, it's only by convention that it's clear that the name of the product comes from the object's identifying key.

请注意,产品的名称也是定义产品其余详细信息的键的名称。 在这种情况下,“Banana”产品的信息存储在嵌套在产品名称本身下的对象中。 但是,按照惯例,很明显产品的名称来自对象的识别键。

By contrast, another formulation of the JSON structure could have had a "product" key for each product and a "name" key to store each of the individual product names. That alternative formulation matches how you model the data in Swift, as seen in the example below:

相比之下,JSON结构的另一种表述可能是每个产品都有一个“产品”键,而一个“名称”键用于存储每个产品名称。 该替代公式与您在Swift中建模数据的方式相匹配,如下例所示:

struct GroceryStore {
    struct Product {
        let name: String
        let points: Int
        let description: String?
    }

    var products: [Product]

    init(products: [Product] = []) {
        self.products = products
    }
}

The following extension to the GroceryStore structure makes it conform to the Encodable protocol, which brings the structure halfway to eventual conformance to the Codable protocol. Notably, it uses a nested structure, ProductKey, rather than the more typical enumeration with the same kind of conformance to the CodingKey protocol. A structure is needed to account for a possibly unlimited number of coding keys that might be used as names for instances of the Product structure.

GroceryStore结构的以下扩展使其符合Encodable协议,该协议使结构中途最终符合Codable协议。 值得注意的是,它使用嵌套结构ProductKey,而不是更典型的枚举,与CodingKey协议具有相同的一致性。 需要一种结构来考虑可能无限数量的编码密钥,这些编码密钥可以用作产品结构实例的名称。

extension GroceryStore: Encodable {
    struct ProductKey: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }

        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }

        static let points = ProductKey(stringValue: "points")!
        static let description = ProductKey(stringValue: "description")!
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: ProductKey.self)
        
        for product in products {
            // Any product's `name` can be used as a key name.
// 任何产品的“名称”都可以用作键名。
            let nameKey = ProductKey(stringValue: product.name)!
            var productContainer = container.nestedContainer(keyedBy: ProductKey.self, forKey: nameKey)
            
            // The rest of the keys use static names defined in `ProductKey`.
// 其余的键使用`ProductKey`中定义的静态名称。
            try productContainer.encode(product.points, forKey: .points)
            try productContainer.encode(product.description, forKey: .description)
        }
    }
}

With the conformance to the Encodable protocol in the example above, any GroceryStore instance can be encoded into JSON using a JSONEncoder instance:

通过上面示例中的Encodable协议的一致性,可以使用JSONEncoder实例将任何GroceryStore实例编码为JSON:

var encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let store = GroceryStore(products: [
    .init(name: "Grapes", points: 230, description: "A mixture of red and green grapes."),
    .init(name: "Lemons", points: 2300, description: "An extra sour lemon.")
])

print("The result of encoding a GroceryStore:")
let encodedStore = try encoder.encode(store)
print(String(data: encodedStore, encoding: .utf8)!)
print()

The second half of implementing conformance to the Codable protocol is decoding. The following extension completes the conformance for the GroceryStore structure. As part of decoding incoming JSON objects, the initializer loops over all of the keys of the first level of nesting in the object.

实现与Codable协议一致的后半部分是解码。 以下扩展完成了GroceryStore结构的一致性。 作为解码传入JSON对象的一部分,初始化器循环遍历对象中第一级嵌套的所有键。

extension GroceryStore: Decodable {
    public init(from decoder: Decoder) throws {
        var products = [Product]()
        let container = try decoder.container(keyedBy: ProductKey.self)
        for key in container.allKeys {
            // Note how the `key` in the loop above is used immediately to access a nested container.
//注意如何立即使用上面循环中的`key`来访问嵌套容器。

            let productContainer = try container.nestedContainer(keyedBy: ProductKey.self, forKey: key)
            let points = try productContainer.decode(Int.self, forKey: .points)
            let description = try productContainer.decodeIfPresent(String.self, forKey: .description)

            // The key is used again here and completes the collapse of the nesting that existed in the JSON representation.
// 此处再次使用该键,并完成JSON表示中存在的嵌套的崩溃。
            let product = Product(name: key.stringValue, points: points, description: description)
            products.append(product)
        }

        self.init(products: products)
    }
}
let decoder = JSONDecoder()
let decodedStore = try decoder.decode(GroceryStore.self, from: json)

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

推荐阅读更多精彩内容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,266评论 0 10
  • 新媒体写作,追热点是一个不能忽略的主题,很多人认为,选题做到好,抓住热点就能获得高阅读量。 文章有新闻性,新闻本身...
    丹菡阅读 2,889评论 1 8
  • 路的尽头,不是你 张小娴说过,“忘掉一段感情最好的办法是时间和新欢,如果时间和新欢也没能让你忘掉这段感情,只能说明...
    南山没有山阅读 285评论 0 1
  • 没有目标有时真的令人焦虑。正常状况应该是有了目标,向目标前行、靠近,在这个过程中努力前进,但也有张有弛,享受过程中...
    云梦大泽阅读 488评论 0 4