قالب وردپرس درنا توس
Home / IOS Development / mikeash.com: Friday Q & A 2017-07-14: Swift.Codable

mikeash.com: Friday Q & A 2017-07-14: Swift.Codable



Friday Q & A 2017-07-14: Swift.Codable

One of the interesting additions to Swift 4 is the protocol Codable and the machine around it. This is a subject near and dear to my heart and I will discuss what it is and how it works today.

Serialization
Serializes values ​​for data that can be stored on disk or transmitted over a network is a common requirement. It is especially common in this age of always connected mobile apps.

So far, the options for serialization in Apple's ecosystem were limited:

  1. NSCoding provides intelligent serialization of complex object graphs and works with your own types but works with a poorly documented serialization format that is not suitable for platform work and requires You type code to manually code and decode types.
  2. NSPropertyListSerialization and NSJSONSerialization can convert between standard cocoa types such as NSDictionary / NSString and property listings or JSON. JSON is used specifically for server communication. Since these APIs provide low-level values, you need to type a bunch of code to extract the meaning of these values. That code is often ad hoc and handles bad data badly.
  3. NSXMLParser and NSXMLDocument are the choice of masochists or people who work poorly with systems using XML. Conversion between the basic analyzed data and more meaningful model objects is left to the programmer.
  4. Finally, there is always the opportunity to build your own from scratch. This is fun, but a lot of work, and incorrect.

These approaches tend to result in a lot of boilerplate code, declaring a property called foo of the String which is encoded by storing String stored in foo under the key "foo" and decoded by retrieving the value of the key "foo" attempts to cast it to a string save it in foo on success or cast an error in error. Then you declare a property called type of String as ….

Of course, do not like to program these repetitive tasks. Replication is what computers are for. We want to write this only:

      struct 
          
          
                  :    String 
          var    bar     String 
    } 

Make it serializable. It should be possible: all necessary information is already present.

Reflection is a common way to achieve this. Many Objective-C programmers have written code for automatically reading and writing Objective-C objects to and from JSON objects. The meal-C driving time provides all the information you need to do this automatically. For Swift, we can use the Objective-C driving time, or use Swift's Mirror, and use brittle solutions to compensate for inability to mutate properties.

Outside Apple's ecosystem, this is a common approach in many languages. This has led to various fun security issues over the years.

Reflection is not a particularly good solution to this problem. It's easy to make mistakes and create security bullets. It is less able to use static writing, so more errors occur at runtime instead of compilation time. And it tends to be quite slow, since the code must be quite general and makes many string entries with the type of metadata.

Swift has taken the approach to compilation time code generation instead of runtime reflection. This means that some of the knowledge must be built into the compiler, but the result is quick and utilizes static typing while still easy to use.

Overview
There are some basic protocols that Swift's new coding system is built up.

The protocol Encodable is used for types that can be encoded. If you adhere to this protocol and all stored features of your type are Encodable the compiler will generate an implementation for you.

The Protocol Decodable is a companion to the Encodable protocol and denotes types that can not meet the requirements, or you need special handling. can be decoded. Like Encodable the compiler will generate an implementation for you if your stored properties are all Decodable .

Because Encodable and Decodable Decodable usually goes together, there is another protocol called Codable as only the two protocols are glued together:

      typealias    Codable    =    Decodable    &    Encodable 

] These two protocols are very simple. Each one contains only one requirement:

      protocol    Encodable    {
          func    code  (  to    encoder :    Encoder  )    cast 
    } 

      protocols    decodable    {
          init  (  from    decoding :    Decoder ) [1
9659013] throws
}

Encoder and Decoder protocols specify how objects can actually code and decode themselves. You do not have to worry about these basic applications, since the standard implementation of Codable handles all the details for you, but you must use them if you type your own Codable implementation. These are complex and we will look at them later.

Finally, there is a CodingKey protocol used to denote keys used for encoding and decoding. This adds an additional layer of static type, which controls the process compared with the use of common strings everywhere. It provides a string and optionally an Int for location keys:

      protocol    CodingKey    {
          var    stringValue : [19659013] String    {   few  } 
          init   (  stringValue :    String )?               Int 

[ public init ( intValue : [19659013] Int ) ]

Encoders and Decoders
The Basic Concept of Encoder and Decoder Similar to NSCoder . Objects receive a tag and then call its methods to code or decode.

API for NSCoder is straight forward. NSCoder has a number of methods like encodeObject: forKey: and encodeInteger: forKey: which objects call to perform their encoding. Objects can also use unkeyed methods like encodeObject: and encodeInteger: to make things positioned instead of by key.

Swift's API is more indirect. Encoder has no own methods of encoding values. Instead, it gives containers and the containers then have methods for encoding values. There is a container for key encoding, one for encoding without encoding and one for encoding a single value.

This helps make things clearer and fits better with portable serialization formats. NSCoder only has to work with Apple's encoding format, so it just has to put the same as it came in. Encoder must work with things like JSON. If an object encode values ​​with keys, it should produce a JSON dictionary. If it uses unkeyed encoding, it should produce a JSON array. What if the object is empty and codes for no values? With the NSCoder approach, it would not indicate what would be printed. With Encoder the object will still require a key or untreated container and the encoder can figure it out.

Decoder works the same way. You do not decode values ​​directly from it, but ask for a container and decode the values ​​from the container. Encoder Decoder provides key-processed, unkeyed and single value containers.

Due to this container design, the Encoder and decoder Decoder ] protocols themselves are small. They contain some accounting information and methods for obtaining containers:

      protocol    Encoder    {
          was    codingPath :    [  CodingKey ? ]      
          public    var    userinfo :    [  CodingUserInfoKey  :    All [19659017]        } 

          FUNC    The 

[ Key > [ KeyedBy Type : Key Type ) -> KeyedEncodingContainer < Enter > where Enter . CodingKey FUNC UnkeyedContainer () -> UnkeyedEncodingContainer FUNC singleValueContainer () -> SingleValueEncodingContainer SingleValueEncodingContainer ] protocols Decoder { of codingPath : [ CodingKey ?] { ] ] var userinfo : [19659014] [ CodingUserInfoKey : Any ] { few } } FUNC The

Key > (. KeyedBy Type : Key Type Type ] throws -> KeyedDecodingContainer < Key > where Key : CodingKey FUNC unkeyedContainer () kaster -> UnkeyedDecodingContainer func singleValueContainer () kaster -> SingleValueDecodingContainer } ] The complexity is in the container types. You can get quite far by recursively reviewing the characteristics of Codable types, but at some point you may have to come down to some raw coding types that can be coded directly and decoded. For Codable these types include the different integer types, Float Double Bool and String . It provides a whole bunch of really similar code / decode methods. Unkeyed containers also support coding sequences of the raw codable types.

In addition to these basic methods, there are a number of methods that support exotic usage cases. KeyedDecodingContainer has methods called decodeIfPresent which returns an optional and return null for missing keys instead of throwing. The coding containers have methods of weak coding, which only encodes an object if something else codes for it (useful for parent references in a complex graph). There are methods for getting nested containers, which allows you to encode hierarchies. Finally, there are methods for getting a "super" encoder or decoder, which is meant to allow subclasses and superclasses to coexist peacefully when encoding and decoding. The subclass can code directly, and then ask the superclass to tag itself with a "super" code that ensures that the keys do not conflict. Implementation Implementation Codable
Implementation Implementation ] Codable is simple: declare compliance and let the compiler generate it for you.

It's useful to know exactly what it does, though. Let's take a look at what it ends up generating and how you will do it yourself. We start with an example Codable type:

      struct    Person :    Codable    {
          var    name :   ] 
          was    age :    Int 
          was    quest :    String 
    } 

The compiler generates a CodingKeys type nested inside Person . If we did it ourselves, the nested type would look like this:

      private    enum    CodingKeys :    CodingKey    {
          case    case 
          case    age 
          case    quest 
    } 

The case names correspond to the Person s property name. Compiler magic gives each CodingKeys a string value that matches its name, which means that the property names are also the keys used for encoding them.

If we need different names, we can easily accomplish this by providing our own CodingKeys with custom raw values. For example, we can write this:

      private    enum    CodingKeys :    String     CodingKey    {
          case    name    name 

] "person_name" case age case quest }

This will cause the name property to be encoded and decoded under person_name . And this is all we need to do. The compiler generally accepts our custom CodingKeys type while still providing a standard implementation for the rest of Codable and that standard implementation uses our custom type. You can mix and match customizations with compiler-provided code.

The compiler also generates an implementation for code (for :) and init (from :) . The implementation of encode (to :) receives a key container and then encodes each property in turn:

      func    encode  (  to    encoder : [19659013] Iver )    throws    {
          where    the container    =    gives .   the container  (  keyedBy [19659017]: ..    CodingKeys    self ) 

          sample    container    code  ]   name     Forkey : ..      name ) 
          sample    container    code  ([19659046] age     Forkey [19659017]:      age ) 
          sample    container    code  (  hunted  ]  Forkey .:  .   quest ) 
    } 

The compiler generates an implementation of init (from :) that reflects this: decoder : Decoder ) cast [ = sample decoder container ( keyedB y . CodingKeys itself ). name = sample container decode ( String self Forkey .. : . name )
age = sample the container . decode Int . [19659306] self Forkey : .. age )
the hunt ] sample container decode ( String self Forkey .: . ] search )
}

The & # 39; s everything is there. In the same way as with CodingKeys you can implement your own version of one of these methods if you let the compiler generate the rest. Unfortunately, it is not possible to specify custom behavior for a single property, so you need to print the whole thing, even if you want standard behavior for the rest. This is not very terrible, though.

If you do everything by hand, the complete implementation of Codable for Person would look like this:

      extension [19659013] person    {
          private    enum    CodingKeys :    CodingKey    {
              case    name 
              case    age 
              By    search 
        } [19659013]                      to    gives :    Iver  =    gives    the container  (  keyedBy .:.    CodingKeys    Self ) 

              sample    the container [19659017]   code  (  name     Forkey .:.      name ) [19659446] sample    container .   code  [  age  [19659016] Forkey : ..      age ) 
              sample    container             Forkey :      Search ) 
        } 

           (  from             ] [19659013] castor    [[196590]]    container    =    sample    decoder    container    ]   keyedBy .    CodingKeys .   self ) 

              name    =    sample    the container .   decode    String . [19659306] self     Forkey : ..      name ) 
              age    ]         Decode  (  Int    Self     Forkey .:      ] ] 
              search    = [19659013] sample    the container    decode  (  String    self     Forward .: ..      search ] 
        } 
    ] 

Implementation Encoder and Decoder 19659004] You may never have to implement your own Encoder or Decoder . Swift offers implementations for JSON and property lists that take care of the usual uses.

You can implement your own in order to support a custom format. The size of container protocols means that this will take some effort. Fortunately, it's mostly a matter of size, not complexity.

To implement a custom Encoder you need something to implement the protocol Encoder and implementations of container protocols. Implementation of the three container protocols involves a lot of repetitive code to implement encoding or decoding methods for all the different direct coding types.

How they work is up to you. Encoder will probably need to store the data encoded and the containers will inform Encoder about the different things they code for.

Implementing a custom decoder is similar. You must implement the protocol plus the container protocols. The decoder will keep the serialized data and the containers will communicate with it to provide the requested values.

I have experimented with a custom binary code and decoder as a way to learn the protocols, and I hope to present it in a future article as an example of how to do it.

Conclusion
Swift 4s Codable The API looks great and should simplify a lot of common code. For typical JSON tasks, it is sufficient to declare compliance with Codable in model types and let the compiler do the rest. When necessary, you can implement parts of the protocol to handle things differently, and you can implement as necessary.

The composition Encoder and decoder is more complex, but rightly so. Support for a custom format by implementing your own Encoder and Decoder takes some work, but most of it is a question of filling out many similar topics.

That's it for today! Come back again for more exciting serialization-related material, and maybe even things that are not related to serialization. Until then, Friday Q & A is powered by reading ideas, so if you have a topic you want to see covered here, please submit it!

Do you like this article? I sell all the books full of them! Volumes II and III are now out! They are available as ePub, PDF, Print, and on iBooks and Kindle. Click here for more information.


Comments:


Comments RSS feed for this page

Add your thoughts, post a comment:

Spam and off topic posts are deleted without notice. Culprits can be humiliated publicly in my own discretion.

Code syntax highlighting thanks to Pygments.

Source link