Objective-C 的 Category 与 Extension

Category(分类)

  • 在本节中,被扩展的类,统一称为"本类"

  • Category 简介

    CategoryObjective-C 2.0 之后添加的语言特性,其主要作用是为已经存在的类添加方法(无论工程中是否有该类的源码)。Category 可以做到在既不子类化,也不侵入一个类的源码的情况下,为原有的类添加新的方法,从而实现扩展一个类或者分离一个类的目的。在程序运行时,RunTime 会把 Category 中的内容附加到本类中
    Apple 的 SDK 中就大面积地使用了Category 这一特性。比如 UIKit.framework 中的 UIView,Apple 把不同功能的 API 进行了分类,这些分类包括:UIViewGeometryUIViewHierarchyUIViewRendering

    Category 的特点:

    1. Category 中能添加 对象方法、类方法、协议、属性(属性可以添加,但只会生成getter setter 的声明,不会生成 getter setter 的实现,也不会生成对应的成员变量),Category 中不能添加成员变量
    2. Category 中的内容(对象方法、类方法、协议、属性)是在运行阶段加入对应的本类中的,这意味着 Category 不写方法的实现也可以编译通过,但运行时调用会报错
    3. Category 有独立的 .h 和 .m 文件,通常 Category 的 .h 文件命名为Class+Category.h,.m 文件命名为 Class+Category.m
    4. 如果 Category 中存在和本类同名的方法,调用时会执行 Category 的方法(即会忽略本类的方法)。所以同名方法调用的优先级为:分类 > 本类 > 父类
    5. 如果多个 Category 中存在同名的方法,调用时会执行最后参与编译的 Category 的方法
    6. 一个类的 Category 的数量是没有限制的,但是每一个 Category 的名字不能相同(同名的 Category 编译会报错)
    7. Category 如果想要访问本类中公开的 成员变量、属性、方法,则需要导入本类的头文件
    8. Category 能访问本类中 @public@protected 权限的成员变量,不能访问 @private 权限的成员变量。Category 和子类一样,只能通过本类暴露的方法来访问 @private 权限的成员变量
    9. Category 是 Objective-C 中特有的语法,其本质是一个指向 struct category_t 的指针

    Category 的作用:

    1. 可以在不修改原有类的基础上,为原有类添加方法,无论工程中是否有该类的源码(最主要的作用是给系统类扩展自己定义的方法)
    2. 可以实现多个开发者共同开发同一个类
    3. 可以将类的实现按功能拆解成多个模块,分散到不同的文件中,减小单一类文件的体积。使用时,按功能需求选择性加载不同的 Category
    4. 模拟 Objective-C 不支持的多继承(使用 Protocol 也可以模拟多继承)
    5. Category 可以用来公开本类中私有的属性和方法
    6. 向类中添加非正式协议
  • 创建 Category

    例如,给 UICoclor 添加一个把 16 进制字符串转换为 RGB 颜色的分类,在工程中 - New File - iOS - Objective-C File,文件类型(File Type)选择 Category:
    在这里插入图片描述
    在这里插入图片描述
    创建的 Category 如下所示:
    在这里插入图片描述
    在这里插入图片描述

  • Category 中可以添加哪些内容

    Objective-C 的分类是由 Category 类型来表示的,而 Category 类型实际上是一个指向 struct category_t 的指针,它的定义如下:

    typedef struct category_t *Category;
    struct category_t {
        const char *name;  							// 宿主类名称
        classref_t cls;  							// 宿主类对象,在运行时阶段通过(宿主类名称)找到对应的(宿主类对象)
        struct method_list_t *instanceMethods;  	// 分类的对象方法列表
        struct method_list_t *classMethods;  		// 分类的类方法列表
        struct protocol_list_t *protocols;  		// 分类所遵守的协议列表(注意:不是分类所定义的协议列表)
        struct property_list_t *instanceProperties;	// 分类的属性列表
    };
    

    struct category_t 的定义可知,
    分类中可以添加:对象方法、类方法、协议、属性
    分类中不能添加:成员变量(因为没有成员变量列表 struct objc_ivar_list *ivars

    因为在分类中,没有用于存储成员变量的 struct objc_ivar_list *ivars
    所以在分类中,虽然可以使用 @property 添加属性,但是编译器只会生成属性 getter setter 方法的声明,并不会生成带下划线的成员变量,更不会生成属性 getter setter 方法的实现

    因此,如果在分类中添加了属性,并且代码中使用 点语法 调用了属性的 getter 或者 setter,那么在程序运行时会报方法找不到的错误

  • 为什么 Category 中不能添加成员变量

    Question ①:
    为什么表示分类的结构体 struct category_t 中不添加一个存储成员变量的字段 struct objc_ivar_list *ivars,这样分类就可以存储成员变量,并且能正常地使用属性了?

    Answer ①:
    在程序运行时,RunTime 会把 分类中的内容 附加到 本类 中

    Objective-C 的类是由 Class 类型来表示的,而 Class 类型实际上是一个指向struct objc_class 的指针,它的定义如下:

    typedef struct objc_class *Class;
    
    struct objc_class {
      Class isa 								OBJC_ISA_AVAILABILITY; // is-a 指针,对象的 is-a 指针指向类对象,类对象的 is-a 指针指向元类对象,元类对象的 is-a 指针指向根源类对象
    #if !__OBJC2__
      Class super_class						OBJC2_UNAVAILABLE; // 父类
      const char *name						OBJC2_UNAVAILABLE; // 类名
      long version         			OBJC2_UNABAILABLE; // 类的版本信息,默认为 0
      long info            			OBJC2_UNAVAILABLE; // 类信息,供运行时使用的一些标识位
      long instance_size      			OBJC2_UNAVAILABLE; // 该类的实例变量大小
      struct objc_ivar_list *ivars  			OBJC2_UNAVAILABLE; // 该类的成员变量链表(一维)
      struct objc_method_list **methodLists 	OBJC2_UNAVAILABLE; // 该类的方法列表链表(二维)
      struct objc_cache *cache   			OBJC2_UNAVAILABLE; // 该类的方法缓存
      struct objc_protocol_list *protocoles 	OBJC2_UNAVAILABLE; // 该类的协议链表
    #endif  
    } OBJC_UNAVAILABLE;
    

    在运行时,struct objc_class 的大小是固定的,不能往 struct objc_class 中添加数据,只能修改数据。在 struct objc_class 中:
    ivars(成员变量列表)struct objc_ivar_list 类型的指针,并且 ivars(成员变量列表) 指向的是一块大小固定的区域:只能修改成员变量的值,不能增加新的成员变量
    methodLists(方法列表数组)objc_method_list * 类型的指针(指向指针的指针,即指向方法列表的指针)。即,methodLists(方法列表数组) 是一个二维数组,虽然没有办法扩展 methodLists 指向的内存区域,但是可以修改 *methodLists 的值来增加成员方法
    因此,在运行时,Class 中不能动态地添加成员变量,只能动态地添加方法
    又因为,Category 中的内容在运行时会被附加到 Class 中,所以 Category 中不能添加成员变量,只能添加方法(Category 的属性,本质上是 getter setter 方法,所以 Category 可以添加属性)

    Question ②:
    为什么不把类的成员变量列表设计成可变的呢?

    Answer ②:
    因为在 RunTime 初始化时,对象的内存布局已经确定,如果此时再向对象中添加成员变量就会破坏类的内部布局,这对编译型语言来说是灾难性的

  • 如何向 Category 中添加属性

    一般情况下,属性(property) 是对 成员变量(instance variable) 的封装
    既然在 Category 中使用 @property 添加属性时,编译器只会生成属性 getter setter 方法的声明,并不会生成带下划线的成员变量,更不会生成属性 getter setter 方法的实现(这将导致我们无法访问成员变量,也无法自己手动实现成员变量的存取方法)
    那么在 Category 中添加一个不能访问的属性有什么意义呢?

    实际上可以使用 RunTime 的关联对象为 Category 已有的属性设置一个关联值,并添加 getter setter 方法的实现

    Person 类:

    // Person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @property (nonatomic, strong) NSString* name;
    @property (nonatomic, assign) int age;
    
    @end
    ------------------------------------------------------------------------------------------------
    // Person.m
    #import "Person.h"
    
    @implementation Person
    
    @end
    

    Person(SportLover) 分类:

    // Person+SportLover.h
    #import "Person.h"
    
    @interface Person (SportLover)
    
    @property (nonatomic, strong) NSString* sportName;
    
    @end
    ------------------------------------------------------------------------------------------------
    // Person+SportLover.m
    #import "Person+SportLover.h"
    #import <objc/runtime.h>
    
    // 关联对象中属性的 key 值,需要确保全局唯一性
    static NSString* kSportName = @"Person+SportLover.sportName";
    
    @implementation Person (SportLover)
    
    // 通过 RunTime 的关联对象,为分类的属性实现 getter
    -(NSString *)sportName {
        return objc_getAssociatedObject(self, &kSportName);
    }
    
    // 通过 RunTime 的关联对象,为分类的属性实现 setter
    -(void)setSportName:(NSString *)sportName {
        objc_setAssociatedObject(self, &kSportName, sportName, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    @end
    

    调用 Person(SportLover) 分类的属性:

    -(void)test {
        Person* p = [[Person alloc] init];
        p.sportName = @"basketball";
        NSLog(@"p.sportName = %@", p.sportName );
        // 输出:
        // 2021-01-21 19:05:27.374669+0800 CategoryDemo[8978:621582] p.sportName = basketball
    }
    

    注意:
    虽然可以通过 RunTime 为 Category 的属性动态地添加 getter setter 方法的实现
    但是编译器自始至终都没有为 Category 的属性生成带下划线的成员变量
    所以在代码中,如果使用 _成员变量 的方式调用 Category 属性对应的成员变量,程序还是会报错

    RunTime 中用于 添加、获取、移除 指定对象上关联值的函数,如下所示:

    // 用给定的 key 与关联策略为指定的对象设置关联的值
    // param0.object 	源对象
    // param1.key		用来标记关联值的 key
    // param2.value		关联值
    // param3.policy	关联策略
    void
    objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                             id _Nullable value, objc_AssociationPolicy policy)
                             
    // 用给定的 key 获取指定对象的关联值
    // param0.object 	源对象
    // param1.key		用来标记关联值的 key
    // return			关联值
    id _Nullable
    objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    
    // 移除指定对象上的所有关联值
    // param0.object 	源对象
    void
    objc_removeAssociatedObjects(id _Nonnull object)
    
    // 关联策略
    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
        OBJC_ASSOCIATION_ASSIGN = 0,           	//关联对象的属性是弱引用
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,	//关联对象的属性是强引用并且关联对象不使用原子性                   
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,	//关联对象的属性是 copy 并且关联对象不使用原子性   
        OBJC_ASSOCIATION_RETAIN = 01401, 		//关联对象的属性是强引用并且关联对象使用原子性     
        OBJC_ASSOCIATION_COPY = 01403  			//关联对象的属性是 copy 并且关联对象使用原子性        
    };
    
    // 注意:
    objc_removeAssociatedObjects(...) 函数的作用是移除指定对象上的所有关联值
    如果要移除指定对象上的某个关联值,可以使用 objc_setAssociatedObject(object, &key, nil, policy),即关联值 value 传 nil
    
    关联对象由 AssociationsManager 统一管理
    所有对象的关联内容统一放在一个容器 全局哈希表 AssociationsHashMap 中
    
  • Category、Class、Super Class 方法调用的优先级

    如果 Category 中存在和本类同名的方法,调用时会执行 Category 中的方法,即会忽略本类的方法。所以同名方法调用的优先级为:分类 > 本类 > 父类
    如果多个 Category 中存在同名的方法,调用时会执行最后参与编译的 Category 的方法

    在项目 CategoryDemo 中:
    ① Man 继承 Person,Man+Chinese 和 Man+Japanese 是 Man 的两个分类
    ② 在这 4 个类中都声明和定义了一个 +run 方法,让其简单地输出方法名
    ③ 到 Target - Build Phases - Compile Sources 下,设置这 4 个 .m 文件的编译顺序
    ④ 各自调用 Person 和 Man 的 +run 方法
    根据不同的编译顺序,+run 方法的调用结果如下

    -(void)test {
        [Person run];
        [Man run];
    }
    

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    注意:
    在程序运行时,RunTime 会把 分类中的内容 附加到 本类 中,但是这里的同名方法并没有相互覆盖
    也就是说:在 Category 附加完成后,Man 元类的方法列表里会有 3 个 +run 方法
    其实所有同名的方法都会存储在方法列表中,只是先编译的同名方法访问不到了而已
    因为 RunTime 在动态地往本类添加方法时,是倒序遍历方法列表的,而最后编译的 Category 的方法会被放在方法列表的最前面。但是 RunTime 在查找方法的时候是沿着方法列表的正序查找的,它只要一找到对应名称的方法,就会马上调用,殊不知后面可能还有一样名称的方法
    Target - Build Phases - Compile Sources 中,编译顺序是从上往下的,想要 Category 中的同名方法调用的优先级越高,其源文件就需要越往后编译,其源文件就需要越往下放

    输出 Man 的类方法列表,可以看到 Man、Man(Chinese)、Man(Japanese) 的 +run 方法,都在 Man 的类方法列表中:

    #import <objc/runtime.h>
    
    -(void)printMethodName {
        // 注意:类方法存储在元类对象中
        unsigned int count = 0;
        Class metaClass = object_getClass([Man class]);
        Method* methodList = class_copyMethodList(metaClass, &count);
        for (unsigned i = 0; i < count; i++) {
            Method method = methodList[i];
            NSString* methodName = NSStringFromSelector(method_getName(method));
            NSLog(@"method(%02d) : %@", i, methodName);
        }
        free(methodList);
    }
    // 输出结果如下:Man、Man+Chinese、Man+Japanese 的 +run 方法,都存在于元类的方法列表中
    2021-01-22 16:26:19.627455+0800 CategoryDemo[10773:834896] method(00) : run
    2021-01-22 16:26:19.628006+0800 CategoryDemo[10773:834896] method(01) : run
    2021-01-22 16:26:19.628348+0800 CategoryDemo[10773:834896] method(02) : run
    
  • Category 与 Class 的 +load 方法的调用顺序

    +load 方法不是通过消息传递方式调用(objc_msgSend),而是直接通过函数指针调用
    因为 +load 方法是不可以继承的,所以 +load 方法的调用不存在类的层级遍历
    本类中有 +load 方法,Category 中也有 +load 方法
    本类中的 +load 方法和 Category 中的 +load 方法,并不是简单的继承或者覆盖的关系,而是独立的两个 +load 方法

    RunTime 加载时,调用 +load 方法的顺序为:父类 > 子类 > 分类
    并且 Category 中 +load 方法的调用顺序取决于 Category 的编译顺序,先编译的 Category 的 +load 方法会先调用

    在项目 CategoryDemo 中:
    ① Man 继承 Person,Person+Category 为 Person 的分类,Man+Category 为 Man 的分类
    ② 重写这 4 个类的 +load 方法,让其简单地输出方法名
    ③ 到 Target - Build Phases - Compile Sources 下,设置这 4 个 .m 文件的编译顺序
    根据不同的编译顺序,+load 方法的调用结果如下
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 利用 Category 公开本类中私有的:属性 && 方法

    可以通过在 Category 的头文件中声明本类中私有的属性或方法的方式
    来达到公开本类私有属性或者方法的目的

    Person 类:

    // Person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @end
    ------------------------------------------------------------------------------------------------
    // Person.m
    #import "Person.h"
    
    @interface Person ()
    // Person 类私有的属性
    @property (nonatomic, strong) NSString* name;
    @property (nonatomic, assign) int age;
    
    @end
    
    
    @implementation Person
    // Person 类私的有对象方法
    -(void)breathe {
        NSLog(@"呼吸...");
    }
    
    // Person 类的私有类方法
    +(void)protectEarth {
        NSLog(@"保护地球!!!");
    }
    
    @end
    

    Person + Export 分类:

    // Person+Export.h
    #import "Person.h"
    
    @interface Person (Export)
    // 在分类的头文件中声明 Person 类的私有属性
    @property (nonatomic, strong) NSString* name;
    @property (nonatomic, assign) int age;
    // 在分类的头文件中声明 Person 类的私有方法
    -(void)breathe;
    +(void)protectEarth;
    
    @end
    ------------------------------------------------------------------------------------------------
    // Person+Export.m
    #pragma clang diagnostic push
    #import "Person+Export.h"
    // 因为在分类的实现文件里,没有关于分类头文件中属性 getter 和 setter 的定义,也没有关于分类头文件中声明的方法的定义
    // 所以 clang 编译器会报属性未定义和方法未定义的警告
    // 这两个宏的作用只是为了告诉 clang 编译器,在本文件中忽略属性未定义和方法未定义的警告
    #pragma clang diagnostic ignored "-Wincomplete-implementation"
    #pragma clang diagnostic ignored "-Wobjc-property-implementation"
    
    @implementation Person (Export)
    
    // 在分类的实现文件里面,并没有定义分类头文件中属性的 getter 和 setter,也没有定义分类头文件中声明的方法
    // 此时 RunTime 会去调用本类中对应的私有属性和私有方法
    
    @end	
    #pragma clang diagnostic pop
    

    调用 Person 类中的私有属性和方法:

    #import "Person+Export.h"
    
    -(void)test {
        Person* p = [[Person alloc] init];
        // 访问 Person 类的私有属性
        p.name = @"hcg";
        p.age = 20;
        NSLog(@"p.name = %@, p.age = %d", p.name, p.age);
        // 调用 Person 对象的私有方法
        [p breathe];
        // 调用 Person 类的私有方法
        [Person protectEarth];
    }
    // 输出如下:
    2021-01-22 11:27:33.342818+0800 CategoryDemo[9651:707598] p.name = hcg, p.age = 20
    2021-01-22 11:27:33.343048+0800 CategoryDemo[9651:707598] 呼吸...
    2021-01-22 11:27:33.343201+0800 CategoryDemo[9651:707598] 保护地球!!!
    

    注意:
    Category 中公开的属性的类型和名称,必须和本类中私有的属性的类型和名称相同
    Category 中公开的方法的类型和名称,必须和本类中私有的方法的类型和名称相同

  • Category 与(本类、子类)的关系

    在运行时,Category 中的方法会成为本类的一部分,与本类中原有的方法没有任何区别
    通过 Category 添加到本类中的方法会被本类所有的子类继承,就和本类的其它方法一样
    Category 中的方法能做,任何在本类中正常定义的方法所能做的事
    但是,如果子类要调用父类 Category 中的方法,则需要导入父类 Category 的头文件

    子类中定义的方法,不会包含在父类的方法列表中
    父类的分类定义的方法,会被包含在父类的方法列表中

Extension(扩展)

  • 在本节中,被扩展的类,统一称为"本类"

  • Extension 简介

    Extension 能为指定的类声明额外的:成员变量、属性、对象方法、类方法
    因为 Extension 只有 .h 文件(没有独立的 .m 文件),所以 Extension 所声明的内容,必须依靠本类的 .m 文件实现。因此,只能为已知源代码的类添加 Extension(即无法为系统的类添加 Extension)

    Extension 的常用形式并不是以一个单独的. h 文件存在,而是寄生在本类的文件中:
    Extension 如果定义在本类的 .h 文件中,那么其声明的内容就是公有的
    Extension 如果定义在本类的 .m 文件中,那么其声明的内容就是私有的
    一般情况下,Extension 会被定义在本类的 .m 文件中,用于声明本类私有的 成员变量、属性、对象方法、类方法

    Extension 中所声明的内容会在编译阶段被附加到本类中,它就是本类的一部分。即,在编译阶段,Extension 会和本类 .h 文件里的 @interface 以及本类 .m 件里的 @implement 一起形成一个完整的类,Extension 伴随着本类的产生而产生,亦随着本类的消亡而消亡

    虽然 Extension 听上去很抽象,但是其实在我们日常的开发中,会经常用到它
    回忆一下:(继承自 UIViewController 的 ViewController)与(继承自 NSObject 的 Person),在 .m 文件中有何不同?
    在这里插入图片描述
    下面是通过 New File - iOS - Objective-C File 创建的 ViewController 的独立的 Extension 文件
    单独创建的 Extension 只有一个 .h 头文件,基于此:
    Extension 可以理解成是本类多了一个 .h 头文件,在该 .h 头文件里面声明的 成员变量、属性、方法 等,都需要在本类的 .m 文件中有对应的实现
    在这里插入图片描述

  • Extension 的两种定义方式

    ① Extension 定义在本类的 .h 文件中,用于声明公有的:成员变量、属性、对象方法、类方法(不常用)
    在这里插入图片描述
    ② Extension 定义在本类的 .m 文件中,用于声明私有的:成员变量、属性、对象方法、类方法(很常用)
    Extension 定义在 .m 文件中的所有内容,默认都是 @private 类型,其作用范围只能在自身类,子类和分类都访问不到
    在这里插入图片描述
    注意:
    这里说的私有并不是绝对的私有,只是一般情况下可以达到私有的效果,Objective-C 中没有绝对的 私有变量 和 私有方法
    因为即使这些 私有变量、私有方法 隐藏在 .m 实现文件里,不暴露在头文件中,开发者仍然可以利用 RunTime 对其进行暴力访问

  • 使用 Extension 声明对外是 readonly,对内是 readwrite 的属性
    在这里插入图片描述

Category 与 Extension 的区别

  • Category 在运行时被附加到本类,Extension 在编译时被附加到本类
  • Category 不能为本类声明成员变量,Extension 可以为本类声明成员变量
  • Category 中声明的属性只会自动生成 getter setter 的声明,不会生成对应的成员变量,不会生成 getter setter 的实现
    Extension 中声明的属性会自动生成 getter setter 的声明,对应的成员变量,以及 getter setter 的实现
  • 添加 Category 不需要知道本类的源码,添加 Extension 需要知道本类的源码
  • 程序文件:类接口 vs 分类 vs 扩展
    在这里插入图片描述
    注意:
    虽然 Extension 的定义和 Category 的定义在格式上非常的类似(Extension 就像是一个没有名字的 Category)
    但是因此就把 Extension 称之为 匿名分类,是不合适的
    因为通过前面对 Category 和 Extension 的介绍我们知道了:从底层机制上来说,Category 和 Extension 是两个完全不同的东西

其他

  • 一些常见的英文单词
    成员变量(instance variable = ivar)
    属性(property)
    对象方法(instance method)
    类方法(class method)
    协议(protocol)

  • 方法的声明 && 方法的定义(方法的实现) && 方法的调用
    在这里插入图片描述

    -(void)personTest {
    	// 方法的调用
    	[Person doSomething];
    }
    
  • 如何修改对象的私有属性
    在这里插入图片描述

    #import "Person.h"
    #import <objc/runtime.h>
    
    -(void)personTest {
        Person* p = [[Person alloc] init];
        // 打印 p 对象的成员变量, 获取私有的成员变量名
        unsigned int count = 0;
        Ivar* ivarList = class_copyIvarList([p class], &count);
        for (int i = 0; i < count; i++) {
            Ivar variable = ivarList[i];
            const char* name = ivar_getName(variable);
            const char* typeEncoding = ivar_getTypeEncoding(variable);
            ptrdiff_t offset = ivar_getOffset(variable);
            NSLog(@"name = %s, type = %s, offset = %ld", name, typeEncoding, offset);
        }
        free(ivarList);
        
        // 设置 p 对象的私有成员变量
        [p setValue:@20 forKey:@"_age"];
        NSNumber* personAge = [p valueForKey:@"_age"];
        NSLog(@"p.age = %d", [personAge intValue]);
        
        [p setValue:@"hcg" forKey:@"_name"];
        NSString* personName = [p valueForKey:@"_name"];
        NSLog(@"p.name = %@", personName);
    }
    
    2021-01-27 16:11:23.221590+0800 CategoryDemo[16017:1679710] name = _age, type = i, offset = 8
    2021-01-27 16:11:23.221825+0800 CategoryDemo[16017:1679710] name = _name, type = @"NSString", offset = 16
    2021-01-27 16:11:23.222245+0800 CategoryDemo[16017:1679710] p.name = hcg
    2021-01-27 16:11:23.222643+0800 CategoryDemo[16017:1679710] p.age = 20
    
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值