猛戳查看最终版@SwiftGG

尽管 Swift 一直在强调强类型、编译时安全和静态调度,但它的标准库仍然提供了反射机制。可能你已经在很多博客文章或者类似Tuples、Midi Packets 和 Core Data 的项目中见过它。也许你刚好对在项目中使用反射机制感兴趣,或者你想更好滴了解反射可以应用的领域,那这篇文章就正是你需要的。文章的内容是基于我在德国法兰克福 Macoun会议上的一次演讲,它对 Swift 的反射 API 做了一个概述。

API 概述

理解这个主题最好的方式就是看API,看它都提供了什么功能。

Mirror

Swift 的反射机制是基于一个叫 Mirrorstruct 来实现的。你为具体的 subject 创建一个 Mirror,然后就可以通过它查询这个对象 subject

在我们创建 Mirror 之前,我们先创建一个可以让我们当做对象来使用的简单数据结构。

import Foundation.NSURL // [译者注]此处应该为import Foundation

public class Store {
    let storesTodisk: Bool = true
}
public class BookmarkStore: Store {
    let itemCount: Int = 10
}
public struct Bookmark {
   enum Group {
      case Tech
      case News
   }
   private let store = {
       return BookmarkStore()
   }()
   let title: String?
   let url: NSURL
   let keywords: [String]
   let group: Group
}

let aBookmark = Bookmark(title: "Appventure",url: NSURL(string: "appventure.me")!,keywords: ["Swift","iOS","OSX"],group: .Tech)

创建一个 Mirror

创建 Mirror 最简单的方式就是使用 reflecting 构造器:

public init(reflecting subject: Any)

然后在 aBookmark struct 上使用它:

let aMirror = Mirror(reflecting: aBookmark)
print(aMirror)
// 输出 : Mirror for Bookmark

这段代码创建了 Bookmark 的 Mirror。正如你所见,对象的类型是 Any。这是 Swift 中最通用的类型。Swift 中的任何东西至少都是 Any 类型的1。这样一来 mirror 就可以兼容 struct,class,enum,Tuple,Array,Dictionary,set 等。

Mirror 结构体还有另外三个构造器,但是这三个都是在你需要自定义 mirror 这种情况下使用的。我们会在接下来讨论自定义 mirror 时详细讲解这些额外的构造器。

Mirror 中都有什么?

Mirror struct 中包含几个 types 来帮助确定你想查询的信息。

第一个是 displayStyle enum,它会告诉你对象的类型:

public enum displayStyle {
    case Struct
    case Class
    case Enum
    case Tuple
    case Optional
    case Collection
    case Dictionary
    case Set
}

这些都是反射 API 的辅助类型。之前我们知道,反射只要求对象是 Any 类型,而且Swift 标准库中还有很多类型为 Any 的东西没有被列举在上面的 displayStyle enum 中。如果试图反射它们中间的某一个又会发生什么呢?比如 closure

let closure = { (a: Int) -> Int in return a * 2 }
let aMirror = Mirror(reflecting: closure)

这里你会得到一个 mirror,但是 displayStylenil 2

也有提供给 Mirror 的子节点使用的 typealias

public typealias Child = (label: String?,value: Any)

所以每个 Child 都包含一个可选的 labelAny 类型的 value。为什么 labelOptional 的?如果你仔细考虑下,其实这是非常有意义的,并不是所有支持反射的数据结构都包含有名字的子节点。 struct 会以属性的名字做为 label,但是 Collection 只有下标,没有名字。Tuple 同样也可能没有给它们的条目指定名字。

接下来是 AncestorRepresentation enum 3:

public enum AncestorRepresentation {
    /// 为所有 ancestor class 生成默认 mirror。
    case Generated
    /// 使用最近的 ancestor 的 customMirror() 实现来给它创建一个 mirror。 
    case Customized(() -> Mirror)
    /// 禁用所有 ancestor class 的行为。Mirror 的 superclassMirror() 返回值为 nil。
    case Suppressed
}

这个 enum 用来定义被反射的对象的父类应该如何被反射。也就是说,这只应用于 class 类型的对象。默认情况(正如你所见)下 Swift 会为每个父类生成额外的 mirror。然而,如果你需要做更复杂的操作,你可以使用 AncestorRepresentation enum 来定义父类被反射的细节。我们会在下面的内容中进一步研究这个。

如何使用一个 Mirror

现在我们有了给 Bookmark 类型的对象aBookmark 做反射的实例变量 aMirror。可以用它来做什么呢?

下面列举了 Mirror 可用的属性 / 方法:

  • let children: Children:对象的子节点。
  • displayStyle: Mirror.displayStyle?:对象的展示风格
  • let subjectType: Any.Type:对象的类型
  • func superclassMirror() -> Mirror?:对象父类的 mirror

下面我们会分别对它们进行解析。

displayStyle

很简单,它会返回 displayStyle enum 的其中一种情况。如果你想要对某种不支持的类型进行反射,你会得到一个空的 Optional 值(这个之前解释过)。

print (aMirror.displayStyle)
// 输出: Optional(Swift.Mirror.displayStyle.Struct)
// [译者注]此处输出:Optional(Struct)

children

这会返回一个包含了对象所有的子节点的 AnyForwardCollection<Child>。这些子节点不单单限于 Array 或者 Dictionary 中的条目。诸如 struct 或者 class 中所有的属性也是由 AnyForwardCollection<Child> 这个属性返回的子节点。AnyForwardCollection 协议意味着这是一个支持遍历的 Collection 类型。

for case let (label?,value) in aMirror.children {
    print (label,value)
}
//输出:
//: store main.BookmarkStore
//: title Optional("Appventure")
//: url appventure.me
//: keywords ["Swift","iOS","OSX"]
//: group Tech

SubjectType

这是对象的类型:

print(aMirror.subjectType)
//输出 : Bookmark
print(Mirror(reflecting: 5).subjectType)
//输出 : Int
print(Mirror(reflecting: "test").subjectType)
//输出 : String
print(Mirror(reflecting: NSNull()).subjectType)
//输出 : NSNull

然而,Swift 的文档中有下面一句话:

“当 self 是另外一个 mirrorsuperclassMirror() 时,这个类型和对象的动态类型可能会不一样“

SuperclassMirror

这是我们对象父类的 mirror。如果这个对象不是一个类,它会是一个空的 Optional 值。如果对象的类型是基于类的,你会得到一个新的 Mirror

// 试试 struct
print(Mirror(reflecting: aBookmark).superclassMirror())
// 输出: nil
// 试试 class
print(Mirror(reflecting: aBookmark.store).superclassMirror())
// 输出: Optional(Mirror for Store)

实例

StructCore Data

假设我们在一个叫 Books Bunny 的新兴高科技公司工作,我们以浏览器插件的方式提供了一个人工智能,它可以自动分析用户访问的所有网站,然后把相关页面自动保存到书签中。

现在是 2016 年,Swift 已经开源,所以我们的后台服务端肯定是用 Swift 编写。因为在我们的系统中同时有数以百万计的网站访问活动,我们想用 struct 来存储用户访问网站的分析数据。不过,如果我们 AI 认定某个页面的数据是需要保存到书签中的话,我们需要使用 CoreData 来把这个类型的对象保存到数据库中。

现在我们不想为每个新建的 struct 单独写自定义的 Core Data 序列化代码。而是想以一种更优雅的方式来开发,从而可以让将来的所有 struct 都可以利用这种方式来做序列化。

那么我们该怎么做呢?

协议

记住,我们有一个 struct,它需要自动转换为 NSManagedobjectCore Data)。

如果我们想要支持不同的 struct 甚至类型,我们可以用协议来实现,然后确保我们需要的类型符合这个协议。所以我们假想的协议应该有哪些功能呢?

  • 第一,协议应该允许自定义我们想要创建的Core Data 实体的名字
  • 第二,协议需要提供一种方式来告诉它如何转换为 NSManagedobject

我们的 protocol 看起来是下面这个样子的:

protocol StructDecoder {
    // 我们 Core Data 实体的名字
    static var EntityName: String { get }
    // 返回包含我们属性集的 NSManagedobject
    func toCoreData(context: NSManagedobjectContext) throws -> NSManagedobject //[译者注]使用 NSManagedobjectContext 需要 import CoreData
}

toCoreData 方法使用了 Swift 2.0 新的异常处理来抛出错误,如果转换失败,会有几种错误情况,这些情况都在下面的 ErrorType enum 进行了列举:

enum SerializationError: ErrorType {
    // 我们只支持 struct
    case Structrequired
    // 实体在 Core Data 模型中不存在
    case UnkNownEntity(name: String)
    // 给定的类型不能保存在 core data 中
    case UnsupportedSubType(label: String?)
}

上面列举了三种转换时需要注意的错误情况。第一种情况是我们试图把它应用到非 struct 的对象上。第二种情况是我们想要创建的 entity 在 Core Data 模型中不存在。第三种情况是我们想要把一些不能存储在 Core Data 中的东西保存到 Core Data 中(即 enum)。

让我们创建一个 struct 然后为其增加协议一致性:

Bookmark struct

struct Bookmark {
   let title: String
   let url: NSURL
   let pagerank: Int
   let created: NSDate
}

下一步,我们要实现 toCoreData 方法。

协议扩展

当然我们可以为每个 struct 都写新的 toCoreData 方法,但是工作量很大,因为 struct 不支持继承,所以我们不能使用基类的方式。不过我们可以使用 protocol extension 来扩展这个方法到所有相符合的 struct

extension StructDecoder {
    func toCoreData(context: NSManagedobjectContext) throws -> NSManagedobject {
    }
}

因为扩展已经被应用到相符合的 struct,这个方法就可以在 struct 的上下文中被调用。因此,在协议中,self 指的是我们想分析的 struct

所以,我们需要做的第一步就是创建一个可以写入我们 Bookmark struct 值的NSManagedobject。我们该怎么做呢?

一点 Core Data

Core Data 有点啰嗦,所以如果需要创建一个对象,我们需要如下的步骤:

  1. 获得我们需要创建的实体的名字(字符串)
  2. 获取 NSManagedobjectContext,然后为我们的实体创建 NSEntityDescription
  3. 利用这些信息创建 NSManagedobject

实现代码如下:

// 获取 Core Data 实体的名字
let entityName = self.dynamicType.EntityName

// 创建实体描述
// 实体可能不存在,所以我们使用 'guard let' 来判断,如果实体
// 在我们的 core data 模型中不存在的话,我们就抛出错误 
guard let desc = NSEntityDescription.entityForName(entityName,inManagedobjectContext: context)
    else { throw UnkNownEntity(name: entityName) } // [译者注] UnkNownEntity 为 SerializationError.UnkNownEntity

// 创建 NSManagedobject
let managedobject = NSManagedobject(entity: desc,insertIntoManagedobjectContext: context)

实现反射

下一步,我们想使用反射 API 来读取 bookmark 对象的属性然后把它写入到 NSManagedobject 实例中。

// 创建 Mirror
let mirror = Mirror(reflecting: self)

// 确保我们是在分析一个 struct
guard mirror.displayStyle == .Struct else { throw SerializationError.Structrequired }

我们通过测试 displayStyle 属性的方式来确保这是一个 struct

所以现在我们有了一个可以让我们读取属性的 Mirror,也有了一个可以用来设置属性的 NSManagedobject。因为 mirror 提供了读取所有 children 的方式,所以我们可以遍历它们并保存它们的值。方式如下:

for case let (label?,value) in mirror.children {
    managedobject.setValue(value,forKey: label)
}

太棒了!但是,如果我们试图编译它,它会失败。原因是 setValueForKey 需要一个 AnyObject? 类型的对象,而我们的 children 属性只返回一个 (String?,Any) 类型的 tuple——也就是说 valueAny 类型,但是我们需要 AnyObject 类型的。为了解决这个问题,我们要测试 valueAnyObject 协议一致性。这也意味着如果得到的属性的类型不符合 AnyObject 协议(比如 enum),我们就可以抛出一个错误。

let mirror = Mirror(reflecting: self)

guard mirror.displayStyle == .Struct 
  else { throw SerializationError.Structrequired }

for case let (label?,anyValue) in mirror.children {
    if let value = anyValue as? AnyObject {
    managedobject.setValue(child,forKey: label) // [译者注] 正确代码为:managedobject.setValue(value,forKey: label)
    } else {
    throw SerializationError.UnsupportedSubType(label: label)
    }
}

现在,只有在 childAnyObject 类型的时候我们才会调用 setValueForKey 方法。

然后唯一剩下的事情就是返回 NSManagedobject。完整的代码如下:

extension StructDecoder {
    func toCoreData(context: NSManagedobjectContext) throws -> NSManagedobject {
    let entityName = self.dynamicType.EntityName

    // 创建实体描述
    guard let desc = NSEntityDescription.entityForName(entityName,inManagedobjectContext: context)
        else { throw UnkNownEntity(name: entityName) } // [译者注] UnkNownEntity 为 SerializationError.UnkNownEntity

    // 创建 NSManagedobject
    let managedobject = NSManagedobject(entity: desc,insertIntoManagedobjectContext: context)

    // 创建一个 Mirror
    let mirror = Mirror(reflecting: self)

    // 确保我们是在分析一个 struct
    guard mirror.displayStyle == .Struct else { throw SerializationError.Structrequired }

    for case let (label?,anyValue) in mirror.children {
        if let value = anyValue as? AnyObject {
        managedobject.setValue(child,forKey: label)
        } else {
        throw SerializationError.UnsupportedSubType(label: label)
        }
    }

    return managedobject
    }
}

搞定,我们现在已经把 struct 转换为 NSManagedobject 了。

性能

那么,速度如何呢?这个方法可以在生产中应用么?我做了一些测试:

创建 2000 个 NSManagedobject
原生: 0.062 seconds
反射: 0.207 seconds

这里的原生是指创建一个 NSManagedobject,然后通过 setValueForKey 设置属性值。如果你在 Core Data 内创建一个 NSManagedobject 子类然后把值直接设置到属性上(没有了动态 setValueForKey 的开销),速度可能更快。

所以正如你所见,使用反射使创建 NSManagedobject 的性能下降了3.5倍。当你在数量有限的项目上使用这个方法,或者你不关心处理速度时,这是没问题的。但是当你需要反射大量的 struct 时,这个方法可能会大大降低你 app 的性能。

自定义 Mirror

我们之前已经讨论过,创建 Mirror 还有其他的选项。这些选项是非常有用的,比如,你想自己定义 mirror对象的哪些部分是可访问的。对于这种情况 Mirror Struct 提供了其他的构造器。

Collection

第一个特殊 init 是为 Collection 量身定做的:

public init<T,C : CollectionType where C.Generator.Element == Child>
  (_ subject: T,children: C,displayStyle: Mirror.displayStyle? = default,ancestorRepresentation: Mirror.AncestorRepresentation = default)

与之前的 init(reflecting:) 相比,这个构造器允许我们定义更多反射处理的细节。

  • 它只对 Collection 有效
  • 我们可以设定被反射的对象以及对象的 childrenCollection 的内容)

class 或者 struct

第二个可以在 class 或者 struct 上使用。

public init<T>(_ subject: T,children: DictionaryLiteral<String,Any>,ancestorRepresentation: Mirror.AncestorRepresentation = default)

有意思的是,这里是由你指定对象的 children (即属性),指定的方式是通过一个 DictionaryLiteral,它有点像字典,可以直接用作函数参数。如果我们为 Bookmark struct 实现这个构造器,它看起来是这样的:

extension Bookmark: Customreflectable {
    func customMirror() -> Mirror { // [译者注] 此处应该为 public func customMirror() -> Mirror {
    let children = DictionaryLiteral<String,Any>(dictionaryLiteral: 
    ("title",self.title),("pagerank",self.pagerank),("url",self.url),("created",self.created),("keywords",self.keywords),("group",self.group))

    return Mirror.init(Bookmark.self,children: children,displayStyle: Mirror.displayStyle.Struct,ancestorRepresentation:.Suppressed)
    }
}

如果现在我们做另外一个性能测试,会发现性能甚至略微有所提升:

创建 2000 个 NSManagedobject
原生: 0.062 seconds
反射: 0.207 seconds
反射: 0.203 seconds

但这个工作几乎没有任何价值,因为它与我们之前反射 struct 成员变量的初衷是相违背的。

用例

所以留下来让我们思考的问题是什么呢?好的反射用例又是什么呢?很显然,如果你在很多 NSManagedobject 上使用反射,它会大大降低你代码的性能。同时如果只有一个或者两个 struct,根据自己掌握的struct 领域的知识编写一个序列化的方法会更容易,更高性能且更不容易让人困惑。

而本文展示反射技巧可以当你在有很多复杂的 struct ,且偶尔想对它们中的一部分进行存储时使用。

例子如下:

  • 设置收藏夹
  • 收藏书签
  • 加星
  • 记住上一次选择
  • 在重新启动时存储AST打开的项目
  • 在特殊处理时做临时存储

除了这些,反射当然还有其他的使用场景:

  • 遍历 tuple
  • 对类做分析
  • 运行时分析对象的一致性
  • 自动生成详细日志 / 调试信息(即外部生成对象)

讨论

反射 API 主要做为 Playground 的一个工具。符合反射 API 的对象可以很轻松滴就在 Playground 的侧边栏中以分层的方式展示出来。尽管它的性能不是最优的,在 Playground 之外仍然有很多有趣的应用场景,这些应用场景我们在用例章节中都讲解过。

更多信息

反射 API 的源文件注释非常详细,我强烈建议每个人都去看看。

同时,GitHub 上的 CoreValue 项目展示了关于这个技术更详尽的实现,它可以让你很轻松滴把 struct 编码成 CoreData,或者把 CoreData 解码成 struct

1、实际上,Any 是一个空的协议,所有的东西都隐式滴符合这个协议。
2、更确切地说,是一个空的可选类型。
3、我对注释稍微做了简化。

附:
文章可执行代码工程地址

Swift反射API及其用法的更多相关文章

  1. HTML5 WebSocket实现点对点聊天的示例代码

    这篇文章主要介绍了HTML5 WebSocket实现点对点聊天的示例代码的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. ios – 在Swift的UIView中找到UILabel

    我正在尝试在我的UIViewControllers的超级视图中找到我的UILabels.这是我的代码:这是在Objective-C中推荐的方式,但是在Swift中我只得到UIViews和CALayer.我肯定在提供给这个方法的视图中有UILabel.我错过了什么?我的UIViewController中的调用:解决方法使用函数式编程概念可以更轻松地实现这一目标.

  3. ios – 在Swift中将输入字段字符串转换为Int

    所以我非常擅长制作APP广告Swift,我试图在文本字段中做一些非常简单的输入,取值,然后将它们用作Int进行某些计算.但是’vardistance’有些东西不正确它是导致错误的最后一行代码.它说致命错误:无法解开Optional.None解决方法在你的例子中,距离是一个Int?否则称为可选的Int..toInt()返回Int?因为从String到Int的转换可能失败.请参阅以下示例:

  4. 如何在iOS中检测文本(字符串)语言?

    例如,给定以下字符串:我想检测每个声明的字符串中使用的语言.让我们假设已实现函数的签名是:如果没有检测到语言,则返回可选字符串.因此,适当的结果将是:有一个简单的方法来实现它吗?

  5. xamarin – 崩溃在AccountStore.Create().保存(e.Account,“);

    在Xamarin.Forms示例TodoAwsAuth中https://developer.xamarin.com/guides/xamarin-forms/web-services/authentication/oauth/成功登录后,在aOnAuthenticationCompleted事件中,应用程序在尝试保存到Xamarin.Auth时崩溃错误说不能对钥匙串说期待着寻求帮助.解决方法看看你

  6. ios – 将视频分享到Facebook

    我正在编写一个简单的测试应用程序,用于将视频从iOS上传到Facebook.由于FacebookSDK的所有文档都在Objective-C中,因此我发现很难在线找到有关如何使用Swift执行此操作的示例/教程.到目前为止我有这个在我的UI上放置一个共享按钮,但它看起来已禁用,从我读到的这是因为没有内容设置,但我看不出这是怎么可能的.我的getVideoURL()函数返回一个NSURL,它肯定包含视

  7. xcode – 错误“线程1:断点2.1”

    我正在研究RESTAPI管理器.这是一个错误,我无法解决它.我得到的错误在下面突出显示.当我打电话给这个班级获取资源时:我评论的线打印:Thread1:breakpoint2.1我需要修复错误的建议.任何建议都非常感谢解决方法您可能在不注意的情况下意外设置了断点.单击并拖动代表断路器外部断点的蓝色刻度线以将其擦除.

  8. ios – 更改导航栏标题swift中的字符间距

    类型的值有人可以帮我这个或建议一种不同的方式来改变swift中导航栏标题中的字符间距吗?解决方法您无法直接设置属性字符串.你可以通过替换titleView来做一个技巧

  9. ios – 如何从变量访问属性或方法?

    是否可以使用变量作为Swift中方法或属性的名称来访问方法或属性?在PHP中,您可以使用$object->{$variable}.例如编辑:这是我正在使用的实际代码:解决方法你可以做到,但不能使用“纯粹的”Swift.Swift的重点是防止这种危险的动态属性访问.你必须使用Cocoa的Key-ValueCoding功能:非常方便,它完全穿过你要穿过的字符串到属性名称的桥,但要注意:这里是龙.

  10. ios – 如果我将自动释放的对象桥接到Core Foundation,我必须使用__bridge或__bridge_retained吗?

    ARC迁移工具遇到了这个问题:特别是,它不确定它是否应该执行__bridge或__bridge_retained.而我也是.-fileURLWithPath返回一个自动释放的对象,在这个地方我不是fileURL的所有者.但与此同时,该对象的保留计数至少为1.我敢打赌,这只能用__bridge来完成.解决方法您只想为此使用常规__bridge强制转换.仅当您想要管理强制转换CF对象的生命周期时,才会使用__bridge_retained.例如:所以__bridge_retained确实告诉编译器你有一个AR

随机推荐

  1. Swift UITextField,UITextView,UISegmentedControl,UISwitch

    下面我们通过一个demo来简单的实现下这些控件的功能.首先,我们拖将这几个控件拖到storyboard,并关联上相应的属性和动作.如图:关联上属性和动作后,看看实现的代码:

  2. swift UISlider,UIStepper

    我们用两个label来显示slider和stepper的值.再用张图片来显示改变stepper值的效果.首先,这三个控件需要全局变量声明如下然后,我们对所有的控件做个简单的布局:最后,当slider的值改变时,我们用一个label来显示值的变化,同样,用另一个label来显示stepper值的变化,并改变图片的大小:实现效果如下:

  3. preferredFontForTextStyle字体设置之更改

    即:

  4. Swift没有异常处理,遇到功能性错误怎么办?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. 字典实战和UIKit初探

    ios中数组和字典的应用Applicationschedule类别子项类别名称优先级数据包contactsentertainment接触UIKit学习用Swift调用CocoaTouchimportUIKitletcolors=[]varbackView=UIView(frame:CGRectMake(0.0,0.0,320.0,CGFloat(colors.count*50)))backView

  6. swift语言IOS8开发战记21 Core Data2

    上一话中我们简单地介绍了一些coredata的基本知识,这一话我们通过编程来实现coredata的使用。还记得我们在coredata中定义的那个Model么,上面这段代码会加载这个Model。定义完方法之后,我们对coredata的准备都已经完成了。最后强调一点,coredata并不是数据库,它只是一个框架,协助我们进行数据库操作,它并不关心我们把数据存到哪里。

  7. swift语言IOS8开发战记22 Core Data3

    上一话我们定义了与coredata有关的变量和方法,做足了准备工作,这一话我们来试试能不能成功。首先打开上一话中生成的Info类,在其中引用头文件的地方添加一个@objc,不然后面会报错,我也不知道为什么。

  8. swift实战小程序1天气预报

    在有一定swift基础的情况下,让我们来做一些小程序练练手,今天来试试做一个简单地天气预报。然后在btnpressed方法中依旧增加loadWeather方法.在loadWeather方法中加上信息的显示语句:运行一下看看效果,如图:虽然显示出来了,但是我们的text是可编辑状态的,在storyboard中勾选Editable,再次运行:大功告成,而且现在每次单击按钮,就会重新请求天气情况,大家也来试试吧。

  9. 【iOS学习01】swift ? and !  的学习

    如果不初始化就会报错。

  10. swift语言IOS8开发战记23 Core Data4

    接着我们需要把我们的Rest类变成一个被coredata管理的类,点开Rest类,作如下修改:关键字@NSManaged的作用是与实体中对应的属性通信,BinaryData对应的类型是NSData,CoreData没有布尔属性,只能用0和1来区分。进行如下操作,输入类名:建立好之后因为我们之前写的代码有些地方并不适用于coredata,所以编译器会报错,现在来一一解决。

返回
顶部