OC基础(五)——OC特有语法

目录


继承的本质
结构体
类的本质
SEL方法
点语法
property(重点,阐述了属性写法的发展过程)
synthesize
property增强
动态类型和静态类型
动态类型检测
id类型
构造方法

继承的本质

  • 创建1个对象,这个对象在内存中是如何分配的.
    1). 子类对象中有自己的属性和所有父类的属性.
    2). 代码段中的每1个类都有1个叫做isa的指针,这个指针指向它的父类.
    一直指到NSObject
    [p1 sayHi]; //假设p1是Person对象.
    先根据p1指针找到p1指向的对象,然后根据对象的isa指针找到Person类.
    搜索Person类中是否有这个sayHi方法 如果有执行
    如果没有 就根据类的isa指针找父类 。。。。。
    NSObject 中如果没有就报错. 如图所示:
01 继承的本质.png

结构体

  1. 结构体与类的相同点

    都可以将多个数据封装为1个整体.

    struct Date
    
    {
    
    int year;
    
    int month;
    
    int day;
    
    };
    
    @interface Date : NSObject
    
    {
    
    int year;
    
    int month;
    
    int day;
    
    }
    
    @end
    
  2. 结构体与类的不同点

    1). 结构体只能封装数据,而类不仅可以封装数据还可以封装行为.

    2). 结构体变量分配在栈空间 (如果是1个局部变量的情况下)

    而对象分配在堆空间.

    栈的特点: 空间相对较小. 但是存储在栈中的数据访问的效率更高一些.
    堆的特点: 空间相对较大. 但是存储在堆中的数据访问的效率相对要低

    3). 赋值.

    结构体 Student
    类: Person

    Student s1 = {"jack",19,GenderMale};
    Student s2 = s1;

    Person *p1 = [Person new];
    Person *p2 = p1;

  3. 应用场景

    1). 如果表示的这个实体 不仅是由多个数据组成, 这个是实体还有行为,只能使用类.

    2). 如果表示的实体没有行为.光有属性.

  • 如果属性较少.只有几个. 那么这个时候就定义为结构体 分配在栈 提高效率.

    • 如果属性较多.不要定义成结构体. 因为这样结构体变量会在栈中占据很大1块空间

      反而会影响效率. 那就定义为类.

类的本质

  1. 内存中的五大区域.



    BSS段
    数据段
    代码段.

    代码段:是用来存储代码的.

    类加载. 当类第1次被访问的时候 这个类就会被加载到代码段存储起来.

  2. 讨论三个问题

    1). 类什么时候加载到代码段.
    类第1次被访问的时候,类就会被加载到代码段存储 类加载.

    2). 类以什么样的形式存储在代码段.

    3). 类一旦被加载到代码段之后 什么时候回收.
    是不会被回收的 除非程序结束.

  3. 类是以什么样的形式存储在代码段的.

    • 任何存储在内存中的数据都有1个数据类型.

      int num = 12;
      
      float  12.12f
      
      'a'
      

      任何在内存中申请的空间也有自己的类型.

      Person *p1 = [Person new];

    • 在代码段存储类的那块空间是个什么类型的.

      在代码段中存储类的步骤

      a. 先在代码段中创建1个Class对象, Class是Foundation框架中的1个类.
      这个Class对象就是用来存储类信息的.

      b. 将类的信息存储在这个Class对象之中.

      这个Class对象.至少有3个属性
      类名: 存储的这个类的名称.
      属性s: 存储的这个类具有哪些属性
      方法s: 存储的这个类具有哪些方法.

      所以.类是以Class对象的形式存储在代码段的.
      存储类的这个Class对象 我们也叫做类对象. 用来存储类的1个对象.

      所以,存储类的类对象也有1个叫做isa指针的属性 这个指针指向存储父类的类对象.

  4. 如何拿到存储在代码段中的类对象.

1). 调用类的类方法 class 就可以得到存储类的类对象的地址.
2). 调用对象的对象方法 class 就可以得到存储这个对象所属的类的Class对象的地址.
3). 对象中的isa指针的值其实就是代码段中存储类的类对象的地址.
注意:
声明Class指针的时候 不需要加* 因为在typedef的时候已经加了*了.

  1. 如何使用类对象.

    • 拿到存储类的类对象以后.

      Class c1 = [Person class];
      c1对象就是Person类.
      c1 完全等价于 Person

    • 使用类对象来调用类的类方法.

      因为类对象就代表存储在这个类对象中的类.
      Class c1 = [Person class];
      c1就代表Person类.

      所以在使用Person的地方完全可以使用c1代替.

      比如我们使用类名来调用类方法.
      [Person sayHi];
      完全可以使用c1来调用. 因为c1就是Person
      [c1 sayHi];

    • 可以使用类对象来调用new方法 创建存储在类对象中的类的对象.

      Person *p1 = [Person new];

      Class c1 = [Person class];
      其实创建Person对象 也可以这么做.

      Person *p2 = [c1 new];

    • 注意:

      使用类对象 只能调用类的类方法,因为类对象就等价于存在其中的类.

      Class c1 = [Person class];

      c1就是Person。

类是以Class对象的形式存储在代码段之中的.

如何拿到存储类的类对象.

有神马用?
可以使用类对象调用类的类方法.

Class c1 = [Person class];

要调用Person的类方法 可以使用Person去调用.
也可以使用c1去调用.

SEL方法

  1. SEL 全称叫做 selector 选择器.
    SEL 是1个数据类型. 所以要在内存中申请空间存储数据.
    SEL其实是1个类. SEL对象是用来存储1个方法的.

  2. 类是以Class对象的形式存储在代码段之中.

    类名:存储的这个类的类名. NSString

    还要将方法存储在类对象之中.如何将方法存储在类对象之中.

    1). 先创建1个SEL对象.
    2). 将方法的信息存储在这个SEL对象之中.
    3). 再将这个SEL对象作为类对象的属性.

  3. 拿到存储方法的SEL对象.

    1). 因为SEL是1个typedef类型的 在自定义的时候已经加了.
    所以 我们在声明SEL指针的时候 不需要加

    2). 取到存储方法的SEL对象,
    SEL s1 = @selector(方法名);

  4. 调用方法的本质.

    [p1 sayHi];
    内部的原理:
    1). 先拿到存储sayHi方法的SEL对象,也就是拿到存储sayHi方法的SEL数据. SEL消息.

    2). 将这个SEL消息发送给p1对象.

    3). 这个时候,p1对象接收到这个SEL消息以后 就知道要调用方法

    4). 根据对象的isa指针找到存储类的类对象.

    5). 找到这个类对象以后 在这个类对象中去搜寻是否有和传入的SEL数据相匹配的.
    如果有 就执行 如果没有再找父类 直到NSObject

    OC最重要的1个机制:消息机制.

    调用方法的本质其实就是为对象发送SEL消息.

    [p1 sayHi]; 为p1对象发送1条sayHi消息.

  5. 重点掌握:
    1).方法是以SEL对象的形式存储起来.
    2).如何拿到存储方法的SEL对象.

  6. 手动的为对象发送SEL消息.

    1). 先得到方法的SEL数据.

    2). 将这个SEL消息发送给p1对象.

    调用对象的方法 将SEL数据发送给对象.

    -(id)performSelector:(SEL)aSelector;
    Person *p1 = [Person new];
    SEL s1 = @selector(sayHi);
    [p1 performSelector:s1]; 与 [p1 sayHi]效果是完全一样的.
    

    3). 调用1个对象的方法有两种.

    • [对象名 方法名];
    • 手动的为对象发送SEL消息.
  7. 注意事项:

    1). 如果方法有参数 那么方法名是带了冒号的.

    2). 如果方法有参数,如何传递参数.
    那么就调用另外1个方法.

    3). 如果有多个参数

    - (id)performSelector:(SEL)aSelector withObject:(id)object;
    - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
    

    总结:

  • 类是以Class对象的形式存储在代码段.
    1. 如何取到存储类的类对象.
    2. 如何使用类对象调用类的类方法
    3. 方法是以SEL数据的形式存储的.
    4. 调用方法的两种方式.

点语法

  1. Java、 C# 对象可以使用点语法来访问对象的成员.
    OC中也有点语法. OC中也可以使用点语法来访问对象的属性.
    但是OC的点语法和Java C# 是完全不一样的.

    OC的对象如果要为属性赋值或者取值 就要调用对应的getter或者setter.

  2. 使用点语法来访问对象的属性.

    语法:
    对象名.去掉下划线的属性名;
    p1.name = @"jack"; 这个时候就会将@"jack"赋值给p1对象的_name属性.
    NSString *name = p1.name;p1对象的_name属性的值取出来.

  3. 点语法的原理.

    p1.age = 18;
    这句话的本质并不是把18直接赋值给p1对象的_age属性.

    点语法在编译器编译的时候.其实会将点语法转换为调用setter、getter的代码.

    (这就是懒加载的本质。self.属性触发getter方法,实现懒加载)

    1). 当使用点语法赋值的时候. 这个时候编译器会将点语法转换为调用setter方法的代码.

    对象名.去掉下划线的属性名 = 数据;
    转换为:

    [对象名 set去掉下划线的属性名首字母大写:数据];

    p1.age = 10;
    [p1 setAge:10];

    2).当使用点语法取值的时候.这个时候编译器会将点语法转换为调用getter方法的代码.

    对象名.去掉下划线的属性名;
    转换为:
    [对象名 去掉下划线的属性名];

    int age = p1.age;
    int age = [p1 age];

  4. 注意.

    1). 在getter和setter中慎用点语法,因为有可能会造成无限递归 而程序崩溃,

    2). 点语法在编译器编译的时候 会转换为调用setter getter方法的代码.
    p1.name = @"jack";
    [p1 setName:@"jack"]

    NSString *name = p1.name;
    NSString *name = [p1 name];

    如果我们的setter方法和getter方法名不符合规范 那么点语法就会出问题.

    3). 如果属性没有封装getter setter 是无法使用点语法的
    因为点语法的本质是getter setter方法.

property(重点,阐述了属性写法的发展过程)

  1. 我们写1个类.
    a. 要先为类写属性.(成员变量)
    b. 在声明属性的getter setter
    c. 再实现getter setter

    哎! 1点点技术含量都没有.有没有更为简单的方式来实现同样的代码效果呢

  2. @property

    1). 作用: 自动生成getter、setter方法的声明.
    因为是生成方法的声明,所以应该写在@interface类的声明之中.

    2). 语法:
    @property 数据类型 名称;
    @property int age;

    3). 原理:
    编译器在编译的时候.会根据@property生成getter和setter方法的声明!

    @property 数据类型 名称;
    生成为:

    - (void)set首字母大写的名称:(数据类型)名称;
    - (数据类型)名称;
    
    eg:
    @property int age;
    //就会声明下面的句子
    
    - (void)setAge:(int)age;
    - (int)age;
    
  3. 使用@property注意.

    1). @property的类型和属性的类型一致.
    @property的名称和属性的名称一致(去掉下划线)
    不要乱写.

    2). @property的名称决定了生成的getter和setter方法的名称.
    所以,@property的名称要和属性的名称一致 去掉下划线 否则生成的方法名就是不符合规范的
    @property的数据类型决定了生成的setter方法的参数类型 和 getter方法的返回值类型.

    3). @property只是生成getter和setter方法的声明. 实现还要自己来. 属性还要自己定义.

synthesize

  1. @property 只能生成getter和setter的声明.
    实现还要我们自己来.

    而实现也是没有什么任何技术含量. 方法实现的代码能不能也可以自动生成呢?

  2. @synthesize

    1).作用: 自动生成getter、setter方法的实现.
    
    所以,应该写在类的实现之中.
    
    2).语法: 
    
    @synthesize @property名称;
    
    //这就是我之前再看前辈们代码时候他们的写法(我困惑了很久哈哈)
    @interface Person : NSObject
    
    {
    
    int _age;
    
    }
    
    @property int age;
    
    @end
    
    @implmentation Person
    
    @synthesize age;
    
    @end
    
    3).@synthesize做的事情.
    
    @implmentation Person
    
    @synthesize age;
    
    @end
    
    ---
    
    @implementaion Person
    
    {
    
    int age;
    
    }
    
    - (void)setAge:(int)age
      {
      self->age = age;
      }
    - (int)age
      {
      return age;
      }
      @end
    
    a.  生成1个真私有的属性.属性的类型和@synthesize对应的@property类型一致.
    
    属性的名字和@synthesize对应的@property名字一致.
    
    b.  自动生成setter方法的实现.
    
    实现的方式: 将参数直接赋值给自动生成的那个私有属性.并且没有做任何的逻辑验证.
    
    c.  自动生成getter方法的实现.
    
    实现的方式: 将生成的私有属性的值返回.
    
  3. 希望@synthesize不要去自动生成私有属性了.
    getter setter的实现中操作我们已经写好的属性就可以了.

    语法:

    @synthesize @property名称 = 已经存在的属性名;

    @synthesize age = _age;
    1). 不会再去生成私有属性.

    2). 直接生成setter getter的实现,

    setter的实现: 把参数的值直接赋值给指定的属性.
    gettter的实现: 直接返回指定的属性的值.

  4. 注意:

    1). 如果直接写1个@synthesize
    @synthesize name;

    2). 如果指定操作的属性.
    @synthesize name = _name;

    3). 生成的setter方法实现中 是没有做任何逻辑验证的 是直接赋值.
    生成的getter方法的实现中 是直接返回属性的值.

如果setter或者getter有自己的逻辑验证 那么就自己在类的实现中重写就可以了.
  1. 批量声明(这个我还真没有怎么使用过,以后可以使用一下)

    1). 如果多个@property的类型一致. 可以批量声明.

    @property float height,weight;

    2). @synthesize 也可以批量声明.
    @synthesize name = _name,age = _age,weight = _weight,height = _height;

property增强

@property只是生成getter setter 的声明.

@synthesize是生成getter setter 的实现.

这种写法是Xcode4.4之前的写法. 从Xcode4.4以后.Xcode对@property做了1个增强

@property增强

只需要写1个@property 编译器就会自动

  1. 生成私有属性.
    2).生成getter setter的声明.
    3).生成getter setter的实现.

@property NSString *name;

做的事情

1). 自动的生成1个私有属性,属性的类型和@property类型一致 属性的名称和@property的名称一致 属性的名称自动的加1个下划线.

2). 自动的生成这个属性的getter setter方法的声明

3). 自动的生成这个属性的getter setter方法的实现.

setter的实现: 直接将参数的值赋值给自动生成的私有属性.

getter的实现: 直接返回生成的私有属性的值.

使用注意

1). 名称要和属性的名称一致 只是去掉下划线.

2). 也可以批量声明相同类型的@property

3). @property生成的方法实现没有做任何逻辑验证.

setter: 直接赋值
getter: 直接返回.

所以,我们可以重写setter来自定义验证逻辑.如果重写了setter 还会自动生成getter
如果重写了getter 还会自动生成setter

这个注意点从来没注意过:

如果同时重写getter setter 那么就不会自动生成私有属性了.

解决方法就是自己在大括号里写一个私有属性嘛

如果你想为类写1个属性 并且为这个属性封装getter setter
1个@property就搞定.

继承.

父类的@property一样可以被子类继承.
@property生成的属性是私有的 在子类的内部无法直接访问生成的私有属性。
但是可以通过setter getter来访问。

动态类型和静态类型

  1. OC是1门弱语言.

    编译器在编译的时候.语法检查的时候没有那么严格.
    不管你怎么写都是可以的.

    int num = 12.12;

    优点: 灵活 咋个行都写.
    缺点: 太灵活

    强类型的语言: 编译器在编译的时候 做语法检查的时候 行就是行 不行就是不行.

  2. 静态类型:
    指的是1个指针指向的对象是1个本类对象.
    动态类型:
    指的是1个指针指向的对象不是本类对象.

  3. 编译检查.

    编译器在编译的时候,能不能通过1个指针去调用指针指向的对象的方法.

    判断原则: 看指针所属的类型之中是有这个方法,如果有就认为可以调用 编译通过.
    如果这个类中没有 那么编译报错.

    这个叫做编译检查. 在编译的时候 能不能调用对象的方法主要是看指针的类型.

    我们可以将指针的类型做转换,来达到骗过编译器的目的.

  4. 运行检查.

    编译检查只是骗过了编译器. 但是这个方法究竟能不能执行.

    所以在运行的时候.运行时会去检查对象中是否真的有这个方法.如果有就执行 如果没有就报错误.

  5. LSP 里氏替换原则

    父类指针指向子类对象.

    实际上任意的指针可以执行任意的对象.编译器是不会报错的.

    当1个子类指针执行1个父类对象的时候,编译器运行通过子类指针去调用子类独有的方法.

    但是在运行的时候是会出问题的.因为父类对象中根本没有子类成员.

动态类型检测

LLVM(苹果编译器的名字)

  1. 编译检查.

    编译器在编译的时候. 判断1个指针是否可以调用指向的对象的方法.

    判断的准则就是指针的类型.

    我们可以很轻松的把编译器给骗过.

  2. 就算骗过了编译器,程序在运行的时候还会做运行检查.

    我们写的程序就算编译通过了.不意味着可以完美的执行

  3. 我们就希望.我们可以写代码来先判断1下.对象中是否有这个方法.如果有再去执行.

    如果没有就别去执行.

    1). 判断对象中是否有这个方法可以执行.

    -(BOOL)respondsToSelector:(SEL)aSelector;

    最常用的是这个方法.

    2). 判断类中是否有指定的类方法.

    -(BOOL)instancesRespondToSelector:(SEL)aSelector;

    3). 判断指定的对象是否为 指定类的对象或者子类对象.

    -(BOOL)isKindOfClass:(Class)aClass;

    BOOL b1 = [s1 isKindOfClass:[Person class]];

    判断s1对象是否为Person对象或者Person的子类对象.

    4). 判断对象是否为指定类的对象 不包括子类.

    (BOOL)isMemberOfClass:(Class)aClass;
    [s1 isMemberOfClass:[Student class]];
    判断s1对象是否为1个Student对象. 不包括Student的子类对象.

    5). 判断类是否为另外1个类的子类.

    (BOOL)isSubclassOfClass:(Class)aClass;

id类型

  1. NSObject.

    是OC中所有类的基类.根据LSP NSObject指针就可以指向任意的OC对象.

    所以.NSObject指针是1个万能指针.可以执行任意的OC对象.

    缺点: 如果要调用指向的子类对象的独有的方法.就必须要做类型转换.

  2. id指针.

    是1个万能指针,可以指向任意的OC对象.

    1). id是1个typedef自定义类型 在定义的时候已经加了*
    所以,声明id指针的时候不需要再加*了.

    1. id指针是1个万能指针,任意的OC对象都可以指.
  3. NSObject和id的异同.

    相同点: 万能指针 都可以执行任意的OC对象.

    不同点: 通过NSObject指针去调用对象的方法的时候.编译器会做编译检查.

    通过id类型的指针去调用对象的方法的时候,编译器直接通过.无论你调用什么方法.

    注意: id指针只能调用对象的方法 不能使用点语法.如果使用点语法就会直接报编译错误 。

    如果我们要声明1个万能指针 千万不要使用NSObject 而是使用id

  4. 父类中的类方法创建1个父类对象返回.

    1). 如果返回值写为父类类型的.那么子类来调用这个方法得到的就是父类指针.

    解决的方式: 把返回值改为id类型的.

    2). 方法的内部创建的对象的是 不要写死. 因为写死创建的对象就固定了.

    我们希望那1个类来调用这个方法就创建那1个类的对象.

    把类名写为self 哪1个类来调用这个方法 self就指的是那1个类.创建的就是那1个类的对象.

    3). 方法的返回值是id类型的.问题就是任意指针都可以接收这个方法的返回值.
    编译器连个警告都没有.

    如果方法的返回值是instancetype
    代表方法的返回值是当前这个类的对象.

    5). 使用建议

    • 如果方法内部是在创建当前类的对象,不要写死成类名 [类名 new];

      而是用self代替类名.

    • 如果方法的返回值是当前类的对象,也不要写死了. 而是写instancetype

    6). id和instancetype的区别.

    • instancetype只能作为方法的返回值.不能在别的地方使用.

      id既可以声明指针变量 也可以作为参数 也可以作为返回值.

    • instancetype 是1个有类型的 代表当前类的对象.

      id是1个无类型的指针 仅仅是1个地址.没有类型的指针.

构造方法

  1. 创建对象,我们之前说:

    类名 *指针名 = [类名 new];

    new实际上是1个类方法.

    new方法的作用:
    -> 创建对象。

    ->初始化对象

    -> 把对象的地址返回.

    new方法的内部,其实是先调用的alloc方法. 再调用的init方法.

    alloc方法是1个类方法,作用: 哪1个类调用这个方法 就创建哪个类的对象,并把对象返回.
    init方法 是1个对象方法,作用: 初始化对象.

    创建对象的完整步骤:

    应该是先使用alloc创建1个对象,然后再使用init初始化这个对象 才可以使用这个对象.

    虽然没有初始化的对象 有的时候 也可以使用. 但是千万不要这么做.

    使用1个未经初始化的对象是极其危险的.

    Person *p1 = [Person new];
    完全等价于
    Person *p1 = [[Person alloc] init];

  2. init方法.

    作用: 初始化对象,为对象的属性赋初始值 这个init方法我们叫做构造方法.

    init方法做的事情:初始化对象.

    为对象的属性赋默认值.

    如果属性的类型是基本数据类型就赋值为0
    C指针 NULL
    OC指针 nil

    所以.我们创建1个对象如果没有为这个对象的属性赋值 这个对象的属性是有默认值的.
    所以,我们每次新创建1个对象,这个对象的属性都被初始化了.

  3. 我们想要让创建的对象的属性的默认值不是 nil NULL 0
    而是我们自定义的.
    那么这个时候,我们就可以重写init方法. 在这个方法中按照我们自己的想法为对象的属性赋值.

重写init方法的规范:

1). 必须要先调用父类的init方法.然后将方法的返回值赋值给self

2). 调用init方法初始化对象有可能会失败,如果初始化失败.返回的就是nil

3). 判断父类是否初始化成功. 判断self的值是否为nil 如果不为nil说明初始化成功.

4). 如果初始化成功 就初始化当前对象的属性.

5). 最后 返回self的值.

解惑

1). 为什么要调用父类的init方法.
因为父类的init方法 会初始化父类的属性(比如父类的真私有属性). 所以必须要保证当前对象中的父类属性也同时被初始化.

2). 为什么要赋值给self?
因为.调用父类的init方法 会返回初始化成功的对象
实际上返回的就是当前对象。但是我们要判断是否初始化成功.

无论如何,记住重写init方法的规范.

-(instancetype)init

{

if(self = [super init])

{

//初始化当前类的属性的代码;

}

retrun self;

}

什么时候需要重写init方法:
如果你希望创建出来的对象的属性的默认值不是 nil NULL 0 而是我们指定的值.
那么这个时候我们就可以重写init方法.

重写init方法以后.

稍稍不爽的: 这样每次创建出来的对象的属性的值都是一样的.

创建对象的时候,对象的属性的值由创建对象的人来指定.而不是写死在init方法中

自定义构造方法.

规范:
1). 自定义构造方法的返回值必须是instancetype
2). 自定义构造方法的名称必须以initWith开头.
3). 方法的实现和init的要求一样.

你可能感兴趣的:(OC基础(五)——OC特有语法)