【Objective-C】5 特有语法
- 第五节 特有语法
- 01 类的本质
- 1.1 继承的本质
- 1.2 结构体 & 类
- 1.3 类的本质:类对象
- 02 SEL
- 2.1 方法的存储
- 2.2 方法调用的本质:SEL 消息
- 03 点语法
- 04 @property
- 05 @synthesize
- 06 @property 增强
- 07 动态类型 & 静态类型
- 7.1 概念
- 7.2 编译检查
- 7.3 运行检查
- 08 id类型
- 8.1 NSObject 指针
- 8.2 id 指针
- 8.3 instancetype
- 09 动态类型检测
- 10 构造方法
- 10.1 new 方法的本质
- 10.2 构造方法(init)
- 10.3 自定义构造方法
- 01 类的本质
第五节 特有语法
01 类的本质
1.1 继承的本质
创建一个对象时的内存分配:
-
子类对象有自己的属性和所有父类的属性
-
代码段中每一个类都有一个 isa 指针,指向当前类的父类(最终指向 NSObject)
例:[p sayHi]; // p 是 Person 类的对象
1)先根据 p 指针(栈)找到 p 指向的对象(堆)
2)然后根据对象中的 isa 指针找到其指向的 Person 类(代码段)
3)在 Person 类中搜索是否包含 sayHi 这一方法,若有则执行
4)若没有找到,就根据 Person 类中的 isa指针 找到其指向的父类,继续搜索
5)如果直至 NSObject 类中仍没有 sayHi 方法,则报错
- 勘误:4)是错误的 !!!
解析:
? 结合 1.3 的内容,其实 OC 中无论是对象(instance)还是类(class),本质上他们都是对象,只不过 instance 对象可以依照其所声明的类创建多个,而每种 class 对象只有唯一一个(逻辑上理解就是每个类型的模版只有一个也只需要有一个)。在此前的讲解中,我们知道 instance 对象中存储的是该对象具体的属性值,而 class 对象中存储的是这个类的属性模板以及方法。但实际上,类中的方法是在两个地方存储的:对象方法存储在 class 对象中,而类方法是存储在 meta-class 对象中(也称为元类)。
? 无论是 instance 对象,还是 class 对象 & meta-class 对象,他们都包含 isa 指针。instance 的 isa 指针指向 class,class 的 isa 指针指向 meta-class。而 meta-class 的 isa 指针比较特殊,全部直接指向了基类的 meta-class(包括基类的 meta-class自身)
? 除此之外,class 对象 & instance 对象中还包含 superclass 指针。class 的 superclass 指针指向其父类的 class,meta-class 的 superclass 指针指向其父类的 meta-class。instance 不具有 superclass 指针,因为一个具体的实例是没有父类(对象)的概念的。(只有类型有父与子关系,实例之间不存在继承关系)而关于基类的 superclass指针(基类没有父类),meta-class 指向 class,class 指向 nil。
? 因此,当通过一个 instance 对象来调用对象方法时(此处仅描述指针流动方向,不包含 SEL相关内容),首先是根据 instance 对象中的 isa 指针找到其对应的模板 class 对象,在 class 对象中检索指定的方法,若没有找到,则通过当前 class 对象中的 superclass 指针前往其父类的 class 对象...,直到最终找到该方法为止。
? 当通过 class 调用类方法时,首先通过 class 的 isa 指针找到 meta-class,后续过程类似对象方法调用。
1.2 结构体 & 类
-
相同点:都可以将多个数据封装为一个整体
-
不同点:
1)结构体只能封装数据,而类还可以封装行为(方法)
2)结构体变量分配在栈空间(如果是局部变量),类的对象存储在堆空间
-
栈的特点:空间相对较小,但存储在栈中的数据访问效率较高
-
堆的特点:空间相对大,访问数据效率相对低
---> 结论:只有当表示的实体仅包含属性,无行为,且属性较少的话,才推荐用结构体存储(效率高)
3)赋值的本质不同
// Student - 结构体,Person - 类 Student s1 = {@"jack", 19, GenderMale}; Person *p1 = [Person new]; Student s2 = s1; // 赋值时,将 s1 的值拷贝给 s2 Person *p2 = p1; // 赋值时,将 p1 地址赋给 p2 --> p1 p2 指向同一个对象
-
1.3 类的本质:类对象
-
内存中的五大区域:栈,堆,代码段,BSS, 数据段 / 常量区
-
三个问题:
-
类何时存储到代码段?
第一次访问类的时候 ---> 类加载
-
类何时回收?
不会回收,直到程序结束才会释放空间
-
类以什么形式存储在代码段?
任何存储在内存中的数据都有一个数据类型(存储的模版:存储几个字节)
任何在内存中申请的空间也有自己的类型
---> 在代码段中,类以什么类型存储?
(Person 类显然不是 Person 类型的,只有类的对象才是 Person 类型的)
-
在代码段中存储类的步骤(类加载):
-
先在代码段中创建一个 Class 对象。
Class 是 Foundation 框架中的一个类,用于存储类
-
将类的信息存储到这个 Class 对象中
Class 对象至少有 3 个属性:
1)类名:存储当前类的名称
2)属性:存储当前类具有哪些属性
3)方法:存储当前类具有哪些方法
存储类的 Class 对象被称为类对象,类(存储类的 Class 对象)中的 isa 指针实际指向的是存储父类的类对象。
注意:此处关于 isa 指针的说法是错误的!详见 ---> 1.1 节 勘误解析
-
如何拿到存储在代码段中的类对象
方法一:调用类的类方法 class ,就可以得到存储类的 class 对象的地址
方法二:调用对象的对象方法 class,可以得到存储这个对象所属的类的class对象的地址
Class c1 = [Person class]; //此处 class 是类方法,用类名调用 // 此时 c1 完全等价于 / 就是 Person 类 Person *p = [Person new]; Class c2 = [p class]; // 此处 class 是对象方法,用对象名调用 NSLog(@"%p, %p", c1, c2); // c1 存储的地址 = c2 存储的地址 // 该地址为代码段中存储 Person 类的 class 对象的地址 // 该地址就是对象中的 isa指针 存储的值
注意:声明 Class 类指针变量时,不需要加 * (系统内部已经在 typedef 时将 * 封装起来了)
-
类对象使用情景
- 使用类对象来调用类的类方法
- 使用类对象来调用 new 方法
类对象 = Class 对象中存储的类,二者完全等价 ---> 可以指定一个类对象,用类对象名来替换类名
Class c = [Person class];
[c sayHi]; // 完全等价于 [Person sayHi];
Person *p = [c new]; // 完全等价于 [Person new];
注意:不可以用于调用类的对象方法(类名 != 对象名)
02 SEL
SEL := selector 选择器,是一个数据类型 ---> 用于在内存中申请空间存储数据
本质上,SEL 是一个类,一个SEL 对象用来存储一个方法(再存储到 Class 对象中)
注意:这种说法只是用来理解,实际上并不正确
SEL 类型的变量本质类型是 const char *,也就说,存储的是方法名的字符串
2.1 方法的存储
-
如何将方法存储在类对象中?
-
先创建一个 SEL 对象
-
将方法的信息存储到这个对象当中
-
再将这个 SEL 对象作为类对象的属性 存储在代码段中
---> 在类对象中创建一个 SEL 指针,指向这个SEL 对象,也就是方法
(类比 - 对象之间的关联关系:Student 类中包含 Book *_book; 这一属性 ,也是创建了一个指针)
-
-
如何拿到存储方法的 SEL 对象?
SEL s = @selector(sayHi); // SEL 类型已在 typedef 中将 * 封装起来 NSLog(@"s = %p", s); // 输出的是 s 中存储的值,即【sayHi 方法所在的地址】
2.2 方法调用的本质:SEL 消息
-
调用方法的本质
内部原理:[p1 sayHi];
- 根据调用语句,先拿到存储对应的方法: sayHi 方法的 SEL 对象,即拿到存储 sayHi 的 SEL 数据
- 将这个 SEL 数据(通常叫做 SEL 消息)发送给 p1 对象(向 p1 对象发送一条 sayHi 消息)
- p1 对象接收到这个 SEL 消息,明确了即将调用的方法
- 根据对象的 isa 指针找到存储类的类对象
- 在类对象中搜索当前类中是否存在和传入的 SEL 消息相匹配的方法,若有则执行
- 若没有在当前类找到,则根据类对象中的 isa 指针找到其父类,继续搜索,直到到达 NSObject
---> OC 中最重要的机制:消息机制
调用方法其实就是为对象发送一条 SEL 消息。(实际在对象方法调用时,会转化为Objc_msgSend(self, SEL, ...)函数,然后通过 SEL 在 class 对象中查找方法进行调用)
方法默认有id,SEL两种类型的参数;id是消息的接收者(也就是 class 对象),SEL是该类方法的编号;
方法查找的本质就是通过对象 & SEL 查找该方法对应的IMP( IMP 表示方法存储的地址)
方法查找的具体过程:
首先通过汇编查找cache中是否缓存了该方法,如果缓存了返回对应方法的IMP;如果没有缓存,在方法列表中查找该方法IMP,如果找到了对该方法进行缓存并且返回IMP;
那么在方法列表中是怎样查找的呢?
首先,查找当前类的方法列表中,是否有该方法的实现;如果没有向父类中查找,一直判断父类是否为nil;如果不为nil一直向上查找,直至找到对应的IMP;找父类时先调用cache_getImp,父类是否缓存了该方法,如果缓存了直接返回,如果没有缓存查找对应类的方法列表;如果是类方法,在元类的方法列表中查找,直至查找到对应的IMP或者父类为nil;
-
手动为对象发送 SEL 消息 ---> 可行且合法
Person *p = [Person new]; // [p sayHi]; 执行的本质(以下两条语句) SEL s = @selector(sayHi); // 获取方法的 SEL数据 [p performSelector:s1]; // 效果完全等价于 [p sayHi]; // SEL 类的 performSelector 方法: // - (id)performSelector:(SEL)aSelector;
-
问题:如果方法有参数( sayHiWith:(Person *)p ),如何手动发送SEL 消息?
-
注意!此时方法名为【 sayHiWith: 】,:冒号不可缺!
-
如果有参数,可以调用其他方法来发送 SEL 消息
// 以下为SEL 类的有参数的 performSelector 方法声明: // 带一个参数: - (id)performSelector:(SEL)aSelector withObject:(id)object; // 带两个参数: - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
-
如果有更多参数,可以将参数封装在一个对象中
// 方法的原始版本: - (void)test1With:(int)num1 And:(int)num2 And:(int)num3; // 封装后: @interface Paras : NSObject{ int _num1; int _num2; int _num3; } @end @interface Person : NSObject - (void)test2With:(Paras *)paras; // 将三个参数封装到一个对象中,将一个对象作为方法的唯一的参数传入方法 @end int main(){ Person *p = [Person new]; Paras *paras = [Paras new]; // 先将三个参数的值存入 paras 指向的对象(过程略) // 然后手动发送: SEL s = @selector(test2With:); [p performSelector:s withObject:paras]; }
-
03 点语法
OC 中可以使用点语法来访问对象的属性(注意:OC中的点语法与 Java C# 不同)
使用点语法访问对象的属性:【对象名.去掉下划线的属性名】
(注意:这种说法只是方便初学者理解,实际上是用点语法直接调用 setter / getter 方法)
Person *p = [Person new];
p.name = @"jack"; // 将 @"jack" 赋值给 p 的 _name 属性
内部实现原理:
编译器编译时,会将点语法转化为调用 setter / getter 的代码
p.age = 10; // 赋值时,转换为:[p setAge:10]; 执行 setAge: 方法
int num = p.age; // 取值时,转换为:int num = [p age]; 执行 age 方法
---> 使用点语法更便于调用,后面直接写点语法即可
注意:
-
在 setter & getter 方法中,慎用点语法(有可能造成无限递归)
- (void)setAge:(int)num{ self.age = age; // self.age 等价于 [self setAge:]; 相当于当前对象循环调用 setAge 方法 }
-
如果 setter & getter 方法的命名不符合规范 或 没有为属性封装 setter & getter,点语法失效无法使用
04 @property
到目前为止,每创建一个类,就要手动书写大量的 setter & getter 方法
---> @property:自动生成属性的 setter & getter 方法的声明
@interface Person : NSObject
{
int _age;
}
@property int age; // 注意:属性名要去掉下划线@end
实现原理:编译器在编译时,会根据 @property 生成 setter & getter 方法的声明
注意:
- @property 的名称和去掉下划线的属性名一致,决定了 setter & getter 方法的名称
- @property 的类型和属性类型一致,对应了 setter 的参数类型和 getter 的返回值类型
- 位置:应书写在 @interface 中的大括号外 --> 方法声明的位置
- @property 只能生成声明,方法的实现需要自己写
05 @synthesize
@property 简化了方法的声明,如何简化方法的实现?
---> @synthesize:自动生成 getter & setter 方法的实现
@implementation Person
@synthesize age;
@end
实现原理:
- 生成了一个真私有的属性,属性的类型和名字(不带下划线) 与 @synthesize 对应的 @property 一致
- 生成 setter 方法,在 setter 方法内部:将参数直接直接赋给自动生成的私有属性(并没有赋给声明中的属性)
- 生成 getter 方法,在 getter 方法内部:将自动生成的私有属性的值返回
// @implementation Person
// @synthesize age;
// @end
//// 等价于:
@implementation Person{
int age;
}
- (void)setAge:(int)age{
self->age = age;
}
- (int)age{
return age;
}
@end
@synthesize 会额外生成与声明中对应的一套私有属性,如何不生成?
@synthesize age = _age;
// 可以自动生成 setter & getter,且不会生成私有属性(可看作是给 _age 这个成员变量添加了一个别名 age)
// setter 会将值直接赋给指定的属性 _age
// getter 会直接返回指定属性 _age 的值
注意:
- 使用@synthesize 生成的别名 & 方法实现是不包含逻辑验证的(若需要,直接重写方法即可)
- @property 可以批量声明数据类型相同的属性
- @synthesize 可以批量声明所有属性(无论类型是否相同)
06 @property 增强
上述内容为 Xcode 4.4 前的语法(依旧可用)。此后,Xcode 对 @property 做了增强:
当写下一个 @property 语句后,编译器会自动:
- 生成私有属性,且会在属性名称前自动添加下划线
- 生成 getter & setter 的声明
- 生成 getter & setter 的实现
---> 此后,无需再写属性 及 @synthesize
注意:
- 注意书写规范:@property的类型一定要与属性一致,@property的名称应为去掉下划线的属性名
- 可以批量声明
- 方法的实现中不包含逻辑验证 ---> 可重写
- 如果同时重写了 setter & getter,@property 不会再创建对应的私有属性 --> 自己写属性
- 子类可继承,但不可以通过 self 访问(因为生成的是私有属性),但可以使用 super 访问
07 动态类型 & 静态类型
OC 是一门弱语言 ---> 编译器在编译阶段检查语法时没有那么严格(例:int num = 12.12;)
---> 优点:灵活;缺点:太灵活。。
什么是强语言?-- 编译器严格检查语法(Java / ...)
7.1 概念
静态类型:表示一个指针指向的对象是一个当前类的对象
动态类型:表示一个指针指向的对象是非当前类的对象
Book *b = @"jack"; // 合法
NSLog(@"%@", b); // 输出 jack
7.2 编译检查
Xcode 中编译器名称:LLVM(可编译 C / OC / C++)
编译器在编译时,会判断能不能通过指针调用指针指向的对象中的方法
判断原则:指针所属的类型之中是否包含该方法。有则编译通过;没有就报错,编译失败
---> 能不能调用方法由指针的类型决定
// Pig 是 Animal 的子类,eat 是 Pig 中的方法
Animal *a = [Pig new];
// 指针 a 是Animal 类的,但指向的对象是 Pig 类的对象
[a eat]; // a --> Animal,Animal 中找不到 eat,编译失败
[(Pig *)a eat]; // 将 a 强制转换为指向 Pig 对象的指针,此时可通过编译
// 注意:此时 a 仍是一个 Animal 类型指针
7.3 运行检查
在运行时,会检查对象中是否真的存在这个方法。有则执行;没有就报错
[(Pig *)a eat];
// 该语句可以通过编译,也可以运行。因为 a 指向的对象是 Pig 类的,类中有 eat 方法
08 id类型
8.1 NSObject 指针
NSObject:是OC 中所有类的基类
---> 根据 LSP(里氏替换原则),NSObject 指针可以指向任意的OC对象,是一个万能指针
缺点:如果要调用 NSObject 指针指向的子类对象中的方法,必须要有类型转换才能通过编译
8.2 id 指针
-
id 指针是一个万能指针,可以指向任意的 OC 对象
-
id 是一个 typedef 自定义类型,在定义该类型时已经封装了 * ,所以声明 id 指针时无需添加 *
-
与 NSObject 指针对比
使用 id 指针调用对象的方法可直接通过编译检查,不会报错
---> 使用 id指针 更好
-
id 指针不能使用点语法访问属性(会报错),只能用来调用方法
8.3 instancetype
父类的类方法可以被子类继承。举例:
// Student 为Perosn 的子类,Person 中包含一个类方法 person,该方法返回一个新创建的 Person 对象
// + (Person *)person;
Person *p = [Person new];
Student *s = [Student person];
// 可以用子类调用 person,因为子类继承了父类的全部成员,包括类方法
但此时指针 s 接收的是一个 Person 对象,怎样能接收到一个当前类的对象呢?
---> 改变父类中该方法的返回值类型和实现
@implementation Person
+ (id)person{
// id 指针可接收任意的 OC 对象,这样就不必指定传出的对象类型
return [self new];
// self 表示调用这个类方法的当前类 -> 哪个类调用方法,就创建哪个类的对象并返回
}@end
--> 在写方法的实现时,不要写死。使用关键字 (self / super)来规避明确的类名
存在的问题:使用 id 指针会使任意指针都能接收这个方法的返回值(编译器甚至不会警告)
如何指定接收返回值的指针类型?---> 令返回值类型为 instancetype
instancetype 表示方法的返回值为当前这个类的对象
- id 与 instancetype 的区别
- instancetype 只能作为方法的返回值,而 id 指针不受限
- instancetype 有类型(当前类),id 指针无类型
09 动态类型检测
-
注意:严格意义上讲,存储在堆区的对象中只包含属性,不包含方法。方法只存储在类中,若要调用方法,需要根据对象的私有属性 isa 指针,找到位于代码段中的类,并在类中搜索方法。若找到,则执行;若未找到,则根据类中 isa指针,访问当前类的父类,再在父类中查找 。。。直到到达 NSObject
---> 因此,当提到【对象中的方法】,实际上是指【对象所属的类的方法以及当前类从父类继承的方法】
在第 7 节中,会发现就算通过了编译检查,也不一定能执行成功(因为可能存在动态类型,即在指针指向的地址空间中存储的并不是声明指针时指定的类 / 类型)
---> 希望能够先判断该方法是否存在于当前对象 / 类中,若不存在就放弃执行函数调用语句
查询方法(前两个方法是最常用的方式)
-
- (BOOL)respondsToSelector:(SEL)aSelector; // 判断当前类 / 指针所指向的对象中是否有这个方法
Person *p = [Person new]; BOOL b1 = [p respondsToSelector:@selector(length)]; // 检查 p 能否调用 length 对象方法 // @selector(length) 是一个 SEL 类型的指针,即一个对应着 length 对象方法 SEL 消息 // 本质上,该函数检查的是对象 p 会不会响应这个 SEL 消息 BOOL b2 = [Person respondsToSelector:@selsector(setName:)]; // 检查 Person 类能否调用 setName: 这个类方法 if(b1 == YES){...} // b1 = 1 else{...}
-
判断指针指向的对象是否是指定的类或其子类的对象
NSSSSString *str = [NSSSSString new]; BOOL b = [str isKindOfClass:[NSString class]]; // 检查 str 是否对应一个 NSString类或其子类的对象 // 即检查 NSSSSString 是否是 NSString 的子类
-
判断指针指向的对象是否为指定类的对象
NSSSSString *str = [NSSSSString new]; BOOL b = [str isMemberOfClass:[NSString class]]; // 检查 str 是否对应一个 NSString 类的对象
-
判断当前类是否是另一个类的子类
BOOL b = [NSSSSString isSubclassOfClass:[NSString class]]; // 检查 NSSSSString 是否为 NSString 的子类
10 构造方法
在此之前,若需要创建对象,需要用类名调用 new 方法,new 方法会完成:
1)在堆区开辟一块空间,按照类模版创建一个对象;2)初始化该对象;3)将对象的地址返回
本质上,new 方法是通过【先调用 alloc 方法,再调用 init 方法】来实现上述内容的
10.1 new 方法的本质
// Person *p1 = [Person new]; 该语句等价于:
Person *p = [Person alloc];
Person *p1 = [p init];
// 也等价于:Person *p1 = [[Person alloc] init];
alloc 方法:一个类方法,依照调用该方法的类创建一个对象,并将该对象返回
init 方法:一个对象方法,用于初始化对象
注意:虽然使用未初始化的对象是合法的,但这种做法极其危险,不建议使用
10.2 构造方法(init)
调用 init 方法,会为对象做初始化赋值,也就是属性的默认值
(基本数据类型 - 0;C 指针 - NULL; OC 指针 -nil)
如何改变这些默认值?令属性在初始化时的默认值不是 0 / NULL / nil?
---> 重写 init 方法,在方法中实现新的初始化
重写 init 方法的规范:
- 必须先调用当前对象的类的父类的 init 方法,然后将该方法的返回值赋给 self
- 调用 init 方法初始化对象有可能失败,若失败则会返回 nil (即对象赋值为空 --> 返回一个空地址 --->无法通过对象名访问对象属性,可以使用对象名访问方法(因为方法在类中,类非空),但方法不会执行)
- 判断父类是否初始化成功 --> 判断 self 是否为 nil
- 若初始化成功(self 的值非空),就继续初始化当前对象的属性
- 返回 self 的值
// Student 是 Person 类 的子类
@implementation Student
- (instancetype)init{
self = [super init];
if(self){
// 在此处给子类的属性做初始化赋值
self.name = @"jack";
}
return self;
}
@end
注意:
-
为什么要调用父类的 init 方法?
逻辑上讲:先有父后有子
代码上讲:因为要给当前对象中的从父类继承来的属性做初始化
-
为什么要赋值给 self?
[super init] :使用super 来调用方法时,返回值是子类的类型(super 只是告知系统应跨过当前类,直接去它的父类中寻找该方法)
调用方法后,需要用当前待赋值的对象(alloc 方法创建的新对象)来接收,所以用 self
-
分析:如果初始化失败,返回了 nil ...
首先,如果父类属性的初始化就失败了,那么子类必定也失败(逻辑上:没有父就不应该有子)
当 self 指针的值为 nil 时,表示返回的当前对象指针值是空 --> 指针指向空
即 [[Student alloc] init] 方法调用返回的值为 nil
此时,Student *s = [[Student alloc] init]; 等号左侧等待接收对象地址的 Student 类型指针 s 被赋值为 nil,即s 指针指向空地址
那么,就无法通过 s 指针访问对象的属性,因为 s 指针根本就不指向一个对象,空地址也不可能有属性
但是,可以通过 s 指针(也就是【对象名】)访问对象方法,因为 s 指针已声明是 Student 类型的,可以找到存储在类中的方法,所以可以通过编译。但方法不会执行,因为根据 2.2节中所述,在给 s 指针发送与方法对应的 SEL消息时,s 指针不会有响应,因为 s 值为空,不指向对象。
-
简化写法:
- (instancetype)init{ if(self = [super init]){ // 判断语句合法,判断的是 self 的值是否为 nil(0) // 注意:要与 == 区别开 self.name = @"jack"; } return self; }
-
当属性的类型是一个类时,可以在 init 方法中调用 [属性类型的类名 new] 来直接给这个属性创建一个对象并初始化
-
在重写 init 之后,再调用 new 方法时,会使用重写后的方法做初始化赋值
10.3 自定义构造方法
使用 init 方法做初始化,会使每一个新建的对象的属性初始值全部相同
怎样能将自定义的初值赋给新建的对象?
---> 自定义构造方法
书写规范:
- 返回值必须是 instancetype ---> 与原 init 方法保持一致
- 命名:名称必须以 initWith 开头(严格区分大小写!!否则系统无法识别)
- 方法的实现与原 init 方法的要求一致
@implemnetation Student
- (instancetype)initWithName:(NSString *)name andAge:(int)age{
if(self = [super init]){
// 如果不遵守命名规范,系统无法由 init 转到自定义的构造方法
// 但 init 不等价于 initWith,在其他位置调用方法时都应该写 initWith
// 所以,在给新创建的对象做初始化时,如果要使用 initWith 来初始化,就不要使用 new 方法
self.name = name;
self.age = age;
}
return self;
}
@end
int main(){
Student *s = [[Student alloc] initWithName:@"jack" andAge:18];
}
---> 此后应使用 [[类名 alloc] initWith方法名] 来创建对象并初始化,而不再使用 new