[译]Swift:利用Enum灵活映射多重类型Data model

IlaMetts 5年前
   <p><img src="https://simg.open-open.com/show/cf2415901a2110bf92e4f43436719662.png"></p>    <p>原文链接: <a href="/misc/goto?guid=4959677092887051947" rel="nofollow,noindex">Swift: Typecasing</a></p>    <h2>一个字段中返回了多种相似的类型</h2>    <p>先来看下项目中我遇到的一个情况,服务端在人物中返回了一组数据。这些人物有几个相同的属性,但是又有各自不同的角色各有的属性。json数据如下:</p>    <pre>  <code class="language-swift">"characters" : [      {          type: "hero",          name: "Jake",          power: "Shapeshift"      },      {          type: "hero",          name: "Finn",          power: "Grass sword"      },      {          type: "princess",          name: "Lumpy Space Princess",          kingdom: "Lumpy Space"      },      {          type: "civilian",          name: "BMO"      },      {          type: "princess",          name: "Princess Bubblegum",          kingdom: "Candy"      }  ]</code></pre>    <p>那么我们可以怎么解析这样的数据呢?</p>    <h2>利用类和继承</h2>    <pre>  <code class="language-swift">class Character {      type: String      name: String  }  class Hero : Character {      power: String  }  class Princess : Character {      kingdom: String  }  class Civilian : Character {   }  ...  struct Model {      characters: [Character]  }</code></pre>    <p>这其实就是项目中我原来使用的方案。但是很快就会觉得有点苦逼,因为使用的时候要不断的类型判断,然后类型转换后才能访问到某个具体类型的属性:</p>    <pre>  <code class="language-swift">// Type checking  if model.characters[indexPath.row] is Hero {      print(model.characters[indexPath.row].name)  }  // Type checking and Typecasting  if let hero = model.characters[indexPath.row] as? Hero {      print(hero.power)  }</code></pre>    <h2>利用结构体和协议</h2>    <pre>  <code class="language-swift">protocol Character {      var type: String { get set }      var name: String { get set }  }  struct Hero : Character {      power: String  }  struct Princess : Character {      kingdom: String  }  struct Civilian : Character {   }  ...  struct Model {      characters: [Character]  }</code></pre>    <p>这里我们使用了结构体,解析的性能会好一些。但是看起来和前面类的方案差不多。我们并没有利用上protocol的特点,使用的时候我们还是要进行类型判断:</p>    <pre>  <code class="language-swift">// Type checking  if model.characters[indexPath.row] is Hero {      print(model.characters[indexPath.row].name)  }  // Type checking and Typecasting  if let hero = model.characters[indexPath.row] as? Hero {      print(hero.power)  }</code></pre>    <h2>类型转换的潜在问题</h2>    <p>上面的这种类型转换可能引入潜在的问题。如果后台此时增加了一个类型对代码会产生什么样的影响呢?可能想到这种情况提前做了处理,也可能没有处理导致崩溃。</p>    <pre>  <code class="language-swift">{      type: "king"      name: "Ice King"      power: "Frost"  }</code></pre>    <p>当我们在写代码的时候,应该考虑到这样的场景,当有新类型出现时能不能友好的提示哪里需要处理呢?毕竟swift的设计目标之一就是更安全的语言。</p>    <h2>另外一种可能:Enum</h2>    <p>我们如何创建一个包含不同类型数据的数组,然后访问他们的属性的时候不用类型转换呢?</p>    <pre>  <code class="language-swift">enum Character {      case hero, princess, civilian  }</code></pre>    <p>当switch一个枚举时,每种case都需要被照顾到,所以使用enum可以很好的避免一些潜在的问题。但是如果只是这样依然不够好,我们可以更进一步:</p>    <h3>Associated values:关联值</h3>    <pre>  <code class="language-swift">enum Character {      case hero(Hero)       case princess(Princess)      case civilian(Civilian)  }  ...  switch characters[indexPath.row] {      case .hero(let hero):          print(hero.power)      case .princess(let princess):          print(princess.kingdom)      case .civilian(let civilian):          print(civilian.name)  }</code></pre>    <p>:ok_hand:!</p>    <p>现在使用的时候不再需要类型转换了。并且如果增加一种新类型,只要在enum中增加一个case,你就不会遗漏需要再修改何处的代码,消除了潜在的问题。</p>    <h3>Raw Value</h3>    <pre>  <code class="language-swift">enum Character : String { // Error: :x:      case hero(Hero)       case princess(Princess)      case civilian(Civilian)  }</code></pre>    <p>你可能会发现这个枚举没有实现 <strong>RawRepresentable</strong> 协议,这是因为关联值类型的枚举不能同时遵从 <strong>RawRepresentable</strong> 协议,他们是互斥的。</p>    <h3>如何初始化</h3>    <p>如果实现了 <strong>RawRepresentable</strong> 协议,就会自带一个利用raw value 初始化的方法。但是我们现在没有实现这个协议,所以我们需要自定义一个初始化方法。</p>    <p>先定义一个内部使用的枚举表示类型:</p>    <pre>  <code class="language-swift">enum Character {        private enum Type : String {          case hero, princess, civilian          static let key = "type"      }    }</code></pre>    <h3>Failable initializers</h3>    <p>因为传回来的json可能出现映射失败的情况,比如增加的一个新类型,所以这里的初始化方法是可失败的。</p>    <pre>  <code class="language-swift">// enum Character  init?(json: [String : AnyObject]) {      guard let           string = json[Type.key] as? String,          type = Type(rawValue: string)          else { return nil }      switch type {          case .hero:              guard let hero = Hero(json: json)               else { return nil }              self = .hero(hero)          case .princess:              guard let princess = Princess(json: json)               else { return nil }              self = .princess(princess)                case .civilian:              guard let civilian = Civilian(json: json)               else { return nil }              self = .civilian(civilian)      }  }</code></pre>    <h3>使用枚举解析json</h3>    <pre>  <code class="language-swift">// Model initialisation  if let characters = json["characters"] as? [[String : AnyObject]] {      self.characters = characters.flatMap { Character(json: $0) }  }</code></pre>    <p>注意这里使用了flatMap。当一条数据的type不在我们已经定义的范围内时,Character(json: [String : AnyObject])返回一个nil。我们当然希望过滤掉这些无法处理的数据。所以使用flatMap,flatMap过程中会抛弃为nil的值,所以这里使用了flapMap。</p>    <h3>完成!</h3>    <pre>  <code class="language-swift">switch model.characters[indexPath.row] {      case .hero(let hero):          print(hero.power)        case .princess(let princess):          print(princess.kingdom)        case .civilian(let civilian):          print(civilian.name)  }</code></pre>    <p>现在可以像最前面展示的那样使用了。</p>    <p>可以告别那些将数组类型声明为 <strong>Any</strong> , <strong>AnyObject</strong> 或者泛型,继承组合的model,使用时再转换类型的日子了。</p>    <h2>One More Thing: 模式匹配</h2>    <p>如果只处理枚举中的一种类型,我们会这么写:</p>    <pre>  <code class="language-swift">func printPower(character: Character) {      switch character {          case .hero(let hero):              print(hero.power)          default:               break  }</code></pre>    <p>然而我们可以利用swift提供的模式匹配,用这种更优雅的写法:</p>    <pre>  <code class="language-swift">func printPower(character: Character) {      if case .hero(let hero) = character {          print(hero.power)      }  }</code></pre>    <p>github上的源码: <a href="/misc/goto?guid=4959677092977288814" rel="nofollow,noindex"> <em>playgrounds</em> </a></p>    <p>欢迎关注我的微博:@没故事的卓同学</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/87255dc14331</p>    <p> </p>    <p><span style="background:rgb(189, 8, 28) url("data:image/svg+xml; border-radius:2px; border:medium none; color:rgb(255, 255, 255); cursor:pointer; display:none; font:bold 11px/20px "Helvetica Neue",Helvetica,sans-serif; left:30px; opacity:0.85; padding:0px 4px 0px 0px; position:absolute; text-align:center; text-indent:20px; top:5764px; width:auto; z-index:8675309">Save</span></p>