从 C++ 到Objective-C----3

从 C++ 到Objective-C(13):内存管理

new 和 delete

Objective-C 中没有 new 和 delete 这两个关键字(new 可以看作是一个函数,也就是 alloc+init)。它们实际是被 alloc 和 release 所取代。

引用计数

内存管理是一个语言很重要的部分。在 C 和 C++ 中,内存块有一次分配,并且要有一次释放。这块内存区可以被任意多个指针指向,但只能被其中一个指针释放。Objective-C 则使用引用计数。对象知道自己被引用了多少次,这就像狗和狗链的关系。如果对象是一条狗,每个人都可以拿狗链拴住它。如果有人不想再管它了,只要丢掉他手中的狗链就可以了。只要还有一条狗链,狗就必须在那里;但是只要所有的狗链都没有了,那么此时狗就自由了。换做技术上的术语,新创建的对象的引用计数器被设置为 1。如果代码需要引用这个对象,就可以发送一个 retain 消息,让计数器加 1。当代码不需要的时候则发送一个release 消息,让计数器减 1。

对象可以接收任意多的 retain 和 release 消息,只要计数器的值是正的。当计数器成 0 时,析构函数 dealloc 将被自动调用。此时再次发送 release 给这个对象就是非法的了,将引发一个内存错误。

这种技术并不同于 C++ STL 的 auto_ptr。Boost 库提供了一个类似的引用计数器,称为 shared_ptr,但这并不是标准库的一部分。

alloc, copy,mutableCopy, retain, release

明白了内存管理机制并不能很好的使用它。这一节的目的就是给出一些使用规则。这里先不解释 autorelease 关键字,因为它比较难理解。

基本规则是,所有使用 alloc,[mutable]copy[WithZone:] 或者是 retain 增加计数器的对象都要用 [auto]release 释放。事实上,有三种方法可以增加引用计数器,也就意味着仅仅有有限种情况下才要使用 release 释放对象:

·        使用alloc 显式实例化对象;

·        使用copy[WithZone:] 或者mutableCopy[WithZone:] 复制对象(不管这种克隆是不是伪克隆);

·        使用retain。

记住,默认情况下,给 nil 发送消息(例如 release)是合法的,不会引起任何后果。

autorelease

不一样的 autorelease

前面我们强调了,所有使用 alloc,[mutable]copy[WithZone:] 或者是 retain 增加计数器的对象都要用[auto]release 释放。事实上,这条规则不仅仅适用于alloc、retain 和 release。有些函数虽然不是构造函数,但也用于创建对象,例如 C++ 的二元加运算符(obj3 operator+(obj1, obj2))。在 C++ 中,返回值可以在栈上,以便在离开作用域的时候可以自动销毁。但在 Objective-C 中不存在这种对象。函数使用 alloc 分配的对象,直到将其返回栈之前不能释放。下面的代码将解释这种情况:

// 第一个例子
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
    Point2D* result = [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
                                            andY:([p1 getY] + [p2 getY])];
    return result;
}
 
// 错误!这个函数使用了 alloc,所以它将对象的引用计数器加 1。
// 根据前面的说法,它应该被销毁。
// 但是这将引起内存泄露:
[calculator add:[calculator add:p1 and:p2] and:p3];
// 第一个算式是匿名的,没有办法 release。所以引起内存泄露。
 
// 第二个例子
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
    return [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
                                 andY:([p1 getY] + [p2 getY])];
}
// 错误!这段代码实际上和上面的一样,
// 不同之处在于仅仅减少了一个中间变量。
 
// 第三个例子
-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
    Point2D* result = [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
                                            andY:([p1 getY] + [p2 getY])];
    [result release];
    return result;
}
// 错误!显然,这里仅仅是在对象创建出来之后立即销毁了。

这个问题看起来很棘手。如果没有 autorelease 的确如此。简单地说,给一个对象发送 autorelease 消息意味着告诉它,在“一段时间之后”销毁。但是这里的“一段时间之后”并不意味着“任何时间”。我们将在后面的章节中详细讲述这个问题。现在,我们有了上面这个问题的一种解决方案:

-(Point2D*) add:(Point2D*)p1 and:(Point2D*)p2
{
    Point2D* result = [[Point2D alloc] initWithX:([p1 getX] + [p2 getX])
                                            andY:([p1 getY] + [p2 getY])];
    [result autorelease];
    return result; // 更简短的代码是:return [result autorelease];
}
// 正确!result 将在以后自动释放

从 C++ 到Objective-C(14):内存管理(续)

autorelease 池

上一节中我们了解到 autorelease 的种种神奇之处:它能够在合适的时候自动释放分配的内存。但是如何才能让便以其之道什么时候合适呢?这种情况下,垃圾收集器是最好的选择。下面我们将着重讲解垃圾收集器的工作原理。不过,为了了解垃圾收集器,就不得不深入了解 autorelease 的机制。所以我们要从这里开始。当对象收到 autorelease 消息的时候,它会被注册到一个“autorelease 池”。当这个池被销毁时,其中的对象也就被实际的销毁。所以,现在的问题是,这个池如何管理?

答案是丰富多彩的:如果你使用 Cocoa 开发 GUI 界面,基本不需要做什么事情;否则的话,你应该自己创建和销毁这个池。

拥有图形界面的应用程序都有一个事件循环。这个循环将等待用户动作,使应用程序响应动作,然后继续等待下一个动作。当你使用 Cocoa 创建 GUI 程序时,这个 autorelease 池在事件循环的一次循环开始时被自动创建,然后在循环结束时自动销毁。这是合乎逻辑的:一般的,一个用户动作都会触发一系列任务,临时变量的创建和销毁一般不会影响到下一个事件。如果必须要有可持久化的数据,那么你就要手动地使用 retain 消息。

另一方面,如果没有 GUI,你必须自己建立 autorelease 池。当对象收到 autorelease 消息时,它能够找到最近的 autorelease 池。当池可以被清空时,你可以对这个池使用 release 消息。一般的,命令行界面的 Cocoa 程序都会有如下的代码:

int main(int argc, char* argv[])

{

    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

    //...

    [pool release];

    return 0;

}

注意在 Mac OS X 10.5 的 NSAutoreleasePool 类新增加了一个 drain 方法。这个方法等价于:当垃圾收集器可用时做 release 操作;否则则触发运行垃圾收集。这对编写在两种情况下都适用的代码时是很有用的。注意,这里实际上是说,现在有两种环境:引用计数和垃圾回收。Mac OS 的新版本都会支持垃圾收集器,但是 iOS 却不支持。在引用计数环境下,NSAutoreleasePool 的 release 方法会给池中的所有对象发送 release 消息,如果对象注册了多次,就会多次给它发 release。drain 和 release 在应用计数环境下是等价的。在垃圾收集的环境下,release 不做任何事情,drain 则会触发垃圾收集。

使用多个 autorelease 池

在一个程序中使用多个 autorelease 池也是可以的。对象收到 autorelease 消息时会注册到最近的池。因此,如果一个函数需要创建并使用很大数量临时对象,为了提高性能,可以创建一个局部的 autorelease 池。这种情况下,这些临时变量就可以及时的被销毁,从而在函数返回时就将内存释放出来。

autorelease 的注意点

使用 autorelease 可能会有一些误用情况,需要我们特别注意。

 

·        首先,非必要地发送多个 autorelease 类似发送多个 release 消息,在内存池清空时会引起内存错误;

·        其次,即使 release 可以由 autorelease 替代,也不能滥用 autorelease。因为 autorelease 要比正常的 release 消耗资源更多。另外,不必要的推迟 release 操作无疑会导致占用大量内存,容易引起内存泄露。

autorelease 和 retain

多亏了 autorelease,方法才能够创建能够自动释放的对象。但是,长时间持有对象是一种很常见的需求。在这种情形下,我们可以向对象发送 retain 消息,然后在后面手动的 release。这样,这个对象实际上可以从两个角度去看待:

·        从函数开发者的角度,对象的创建和释放都是有计划的;

·        从函数调用者的角度,使用了 retain 之后,对象的生命期变长了(使用 retain 将使其引用计数器加 1),为了让对象能够正确地被释放,调用者必须负责将计数器再减 1。

我们来理解一下这句话。对于一个函数的开发者,如果他不使用 autorelease,那么,他使用 alloc 创建了一个对象并返回出去,那么,他需要负责在合适的时候对这个对象做 release 操作。也就是说,从函数开发者的角度,这个对象的计数器始终是 1,一次 release 是能够被正常释放的。此时,函数调用者却使用 retain 将计数器加 1,但是开发者不知道对象的计数器已经变成 2 了,一次 release 不能释放对象。所以,调用者必须注意维护计数器,要调用一次 release 将其恢复至 1。

Convenience constructor,virtual constructor

将构造对象的过程分成 alloc 和 init 两个阶段,有时候显得很罗嗦。好在我们有一个 convenience constructor 的概念。这种构造函数应该使用类名做前缀,其行为类似 init,同时要实现 alloc。但是,它的返回对象需要注册到一个内部的 autorelease 池,如果没有给它发送 retain 消息时,这个对象始终是一个临时对象。例如:

// 啰嗦的写法

NSNumber* zero_a = [[NSNumber alloc] initWithFloat:0.0f];

...

[zero_a release];

...

// 简洁一些的

NSNumber* zero_b = [NSNumber numberWithFloat:0.0f];

...

// 不需要 release

根据我们前面对内存管理的介绍,这种构造函数的实现是基于 autorelease 的。但是其底层代码并不那么简单,因为这涉及到对 self 的正确使用。事实上,这种构造函数都是类方法,所以 self 指向的是 Class 类型的对象,就是元类类型的。在初始化方法,也就是一个实例方法中,self 指向的是这个类的对象的实例,也就是一个“普通的”对象。

编写错误的这种构造函数是很容易的。例如,我们要创建一个 Vehicle 类,包含一个 color 数据,编写如下的代码:

// The Vehicle class

@interface Vehicle : NSObject

{

    NSColor* color;

}

 

-(void) setColor:(NSColor*)color;

 

// 简洁构造函数

+(id) vehicleWithColor:(NSColor*)color;

 

@end

其对应的实现是:

// 错误的实现

+(Vehicle*) vehicleWithColor:(NSColor*)color

{

    // self 不能改变

    self = [[self alloc] init]; // 错误!

    [self setColor:color];

    return [self autorelease];

}

记住我们前面所说的,这里的 self 指向的是 Class 类型的对象。

// 比较正确的实现

+(id) vehicleWithColor:(NSColor*)color

{

    id newInstance = [[Vehicle alloc] init]; // 正确,但是忽略了有子类的情况

    [newInstance setColor:color];

    return [newInstance autorelease];

}

我们来改进一下。Objective-C 中,我们可以实现 virtual constructor。这种构造函数通过内省的机制来了解到自己究竟应该创建哪种类的对象,是这个类本身的还是其子类的。然后它直接创建正确的类的实例。我们可以使用一个 class 方法(注意,class 在 Objective-C 中不是关键字);这是 NSObject 的一个方法,返回当前对象的类对象(也就是 meta-class 对象)。

@implementation Vehicle

 

+(id) vehicleWithColor:(NSColor*)color

{

    id newInstance = [[[self class] alloc] init]; // 完美!我们可以在运行时识别出类

    [newInstance setColor:color];

    return [newInstance autorelease];

}

 

@end

 

@interface Car : Vehicle {...}

@end

...

// 创建一个 red Car

id car = [Car vehicleWithColor:[NSColor redColor]];

类似于初始化函数的 init 前缀,这种简洁构造函数最好使用类名作前缀。不过也有些例外,例如 [NSColor redColor] 返回一个预定义的颜色,按照我们的约定,使用 [NSColor colorRed] 更合适一些。

最后,我们要重复一下,所有使用 alloc、[mutable]copy[WithZone:] 增加引用计数器值的对象,都必须相应地调用 [auto]release。当调用简洁构造函数时,你并没有显式调用 alloc,也就不应该调用 release。但是,在创建这种构造函数时,一定不要忘记使用 autorelease。

从 C++到 Objective-C(15):内存管理(续二)

Setters

如果不对 Objective-C 的内存管理机制有深刻的理解,是很难写出争取的 setter 的。假设一个类有一个名为 title 的 NSString 类型的属性,我们希望通过 setter 设置其值。这个例子虽然简单,但已经表现出 setter 所带来的主要问题:参数如何使用?不同于 C++,在 Objective-C 中,对象只能用指针引用,因此 setter 虽然只有一种原型,但是却可 以有很多种实现:可以直接指定,可以使用 retain 指定,或者使用 copy。每一种实现都有特定的目的,需要考虑你 set 新的值之后,新值和旧值之间的关系(是否相互影响等)。另外,每一种实现 都要求及时释放旧的资源,以避免内存泄露。直接指定(不完整的代码)

外面传进来的对象仅仅使用引用,不带有 retain。如果外部对象改变了,当前类也会知 道。也就是说,如果外部对象被释放掉,而当前类在使用时没有检查是否为 nil,那么当前类就会持有一个非法引用。

-(void) setString:(NSString*)newString

{

    ... 稍后解释内存方面的细节

    self->string = newString; // 直接指定

}

使用 retain 指定(不完整的代码)

外部对象被引用,并且使用 retain 将其引用计数器加 1。外部对象的改变对于当前类也是可见的,不过,外部对象不能被释 放,因为当前类始终持有一个引用。

-(void) setString:(NSString*)newString

{

    ... 稍后解释内存方面的细节

    self-> string = [newString retain]; // 使用 retain 指定

}

复制(不完整的代码)

外部对象实际没有被引用,使用的是其克隆。此时,外部对象的改变对于当前类是不可变的。也就是说,当前类持有的是这个对象的克隆, 这个对象的生命周期不会比持有者更长。

-(void) setString:(NSString*)newString

{

    ... 稍后解释内存方面的细节

    self->string = [newString copy]; // 克隆

    // 使用 NSCopying 协议

}

为了补充完整这些代码,我们需要考虑这个对象在前一时刻的状态:每一种情形下,setter 都需要释放掉旧的资源,然后建立新的。这些代码看起来比较麻烦。

直接指定( 完整代码)

这是最简单的情况。旧的引用实际上被替换成了新的。

-(void) setString:(NSString*)newString

{

    // 没有强链接,旧值被改变了

    self->string = newString; // 直接指定

}

使用 retain 指定(完整代码)

在这种情况下,旧值需要被释放,除非旧值和新值是一样的。

// ------ 不正确的实现 ------

-(void) setString:(NSString*)newString

{

    self->string = [newString retain];

    // 错误!内存泄露,没有引用指向旧的“string”,因此再也无法释放

}

 

-(void) setString:(NSString*)newString

{

    [self->string release];

    self->string = [newString retain];

    // 错误!如果 newString == string(这是可能的),

    // newString 引用是 1,那么在 [self->string release]之后

    // 使用 newString 就是非法的,因为此时对象已经被释放

}

 

-(void) setString:(NSString*)newString

{

    if (self->string != newString)

        [self->string release]; // 正确:给 nil 发送 release 是安全的

    self->string = [newString retain];  // 错误!应该在 if 里面

                                        // 因为如果 string == newString,

                                        // 计数器不会被增加

}

 

// ------ 正确的实现 ------

// 最佳实践:C++ 程序员一般都会“改变前检查”

-(void) setString:(NSString*)newString

{

    // 仅在必要时修改

    if (self->string != newString) {

        [self->string release]; // 释放旧的

        self->string = [newString retain]; // retain 新的

    }

}

 

// 最佳实践:自动释放旧值

-(void) setString:(NSString*)newString

{

    [self->string autorelease]; // 即使 string == newString 也没有关系,

                                // 因为 release 是被推迟的

    self->string = [newString retain];

    //... 因此这个 retain 要在 release 之前发生

}

 

// 最佳实践:先 retain 在 release

-(void) setString:(NSString*)newString

{

    [self->newString retain]; // 引用计数器加 1(除了 nil)

    [self->string release]; // release 时不会是 0

    self->string = newString; // 这里就不应该再加 retain 了

}

复制(完整代码)

无论是典型的误用还是正确的解决方案,都和前面使用 retain 指定一样,只不过把 retain 换成 copy。

伪克隆

有些克隆是伪克隆,不过对结果没有影响。

从 C++到 Objective-C(16):内存管理(续三)

Getters

Objective-C 中,所有对象都是动态分配的,使用指针引用。一般的,getter 仅仅返回指针的值,而不应该复制对象。getter 的名字一般和数据成员的名字相同(这一点不同于 Java,JavaBean 规范要求以 get 开头),这并不会引起任何问题。如果是布尔变量,则使用 is 开头(类似 JavaBean 规范),这样可以让程序更具可读性。

@interface Button
{
    NSString* label;
    BOOL      pressed;
}
 
-(NSString*) label;
-(void) setLabel:(NSString*)newLabel;
-(BOOL) isPressed;
@end
 
@implementation Button
-(NSString*) label
{
    return label;
}
 
-(BOOL) isPressed
{
    return pressed;
}
 
-(void) setLabel:(NSString*)newLabel {...}
@end

当返回实例数据指针时,外界就可以很轻松地修改其值。这可能是很多 getter 不希望的结果,因为这样一来就破坏了封装性。

@interface Button
{
    NSMutableString* label;
}
 
-(NSString*) label;
@end
 
@implementation Button
-(NSString*) label
{
    return label; // 正确,但知道内情的用户可以将其强制转换成 NSMutableString,
                  // 从而改变字符串的值
}
 
-(NSString*) label
{
    // 解决方案 1 :
    return [NSString stringWithString:label];
    // 正确:实际返回一个新的不可变字符串
    // 解决方案 2 :
    return [[label copy] autorelease];
    // 正确:返回一个不可变克隆,其值是一个 NSString(注意不是 mutableCopy)
}
@end

循环 retain

必须紧身避免出现循环 retain。如果对象 A retain 对象 B,B 和 C 相互 retain,那么 B 和 C 就陷入了循环 retain:

A → B ↔ C

如果 A release B,B 不会真正释放,因为 C 依然持有 B。C 也不能被释放,因为 B 持有 C。因为只有 A 能够引用到 B,所以一旦 A release B,就再也没有对象能够引用这个循环,这样就不可避免的造成内存泄露。这就是为什么在一个树结构中,一般是父节点 retain 子节点,而子节点不 retain 父节点。

垃圾收集器

Objective-C 2.0 实现了一个垃圾收集器。换句话说,你可以将所有内存管理交给垃圾收集器,再也不用关心什么 retain、release 之类。但是,不同于 Java,Objective-C 的垃圾收集器是可选的:你可以选择关闭它,从而自己管理对象的生命周期;或者你选择打开,从而减少很多可能有 bug 的代码。垃圾收集器是以一个程序为单位的,因此,打开或者关闭都会影响到整个应用程序。

如果开启垃圾收集器,retain、release 和autorelease 都被重定义成什么都不做。因此,在没有垃圾收集器情况下编写的代码可以不做任何改变地移植到有垃圾收集器的环境下,理论上只要重新编译一遍就可以了。“理论上”意思是,很多情况下涉及到资源释放处理的时候还是需要特别谨慎地对待。因此,编写同时满足两种情况的代码是不大容易的,一般开发者都会选择重新编写。下面,我们将逐一解释这两者之间的区别,这些都是需要特别注意的地方。

finalize

在有垃圾收集器的环境下,对象的析构顺序是未定义的,因此使用 dealloc 就不大适合了。NSObject 增加了一个 finalize 方法,将析构过程分解为两步:资源释放和有效回收。一个好的 finalize 方法是相当精妙的,需要很好的设计。

weak, strong

很少会见到 __weak 和 __strong 出现在声明中,但我们需要对它们有一定的了解。

默认情况下,一个指针都会使用 __strong 属性,表明这是一个强引用。这意味着,只要引用存在,对象就不能被销毁。这是一种所期望的行为:当所有(强)引用都去除时,对象才能被收集和释放。不过,有时我们却希望禁用这种行为:一些集合类不应该增加其元素的引用,因为这会引起对象无法释放。在这种情况下,我们需要使用弱引用(不用担心,内置的集合类就是这么干的),使用 __weak 关键字。NSHashTable 就是一个例子。当被引用的对象消失时,弱引用会自动设置为 nil。Cocoa 的Notification Center 就是这么一个例子,虽然这已经超出纯Objective-C 的语言范畴。

NSMakeCollectable()

Cocoa 并不是 Mac OS X 唯一的 API。Core Foundation 就是另外一个。它们是兼容的,可以共享数据和对象。但是 Core Foudation 是由纯 C 编写的。或许你会认为,Objective-C 的垃圾收集器不能处理 Core Foundation 的指针。但实际上是可以的。感兴趣的话可以关注一下 NSMakeCollectable 的文档。

AutoZone

由 Apple 开发的 Objective-C 垃圾收集器叫做 AutoZone。这是一个公开的开源库,我们可以看到起源代码。不过在 Mac OS X 10.6 中,垃圾收集器可能有了一些变化。这里对此不再赘述。

从 C++ 到Objective-C(17):异常处理和多线程

异常处理

比起 C++ 来,Objective-C中的异常处理更像 Java,这主要是因为 Objective-C 有一个 @finally 关键字。Java 中也有一个类似的 finally 关键字,但 C++ 中则没有。finally 是 try()…catch() 块的一个可选附加块,其中的代码是必须执行的,不管有没有捕获到异常。这种设计可以很方便地写出简短干净的代码,比如资源释放等。除此之外,Objective-C 中的 @try…@catch…@finally 是很经典的设计,同大多数语言没有什么区别。但是,不同于 C++ 的还有一点,Objective-C 只有对象可以被抛除。

不带 finally

带有 finally

BOOL problem = YES;
@try {
    dangerousAction();
    problem = NO;
} @catch (MyException* e) {
    doSomething();
    cleanup();
} @catch (NSException* e) {
    doSomethingElse();
    cleanup();
    // 重新抛出异常
    @throw
}
if (!problem)
    cleanup();
@try {
    dangerousAction();
} @catch (MyException* e) {
    doSomething();
} @catch (NSException* e) {
    doSomethingElse();
    @throw // 重新抛出异常
} @finally {
    cleanup();
}

严格说来,@finally 不是必要的,但是确实是处理异常强有力的工具。正如前面的例子所示,我们也可以在 @catch 中将异常重新抛出。事实上,@finally 在 @try 块运行结束之后才会执行。对此我们将在下面进行解释。

int f(void)
{
    printf("f: 1-you see me\n");
    // 注意看输出的字符串,体会异常处理流程
    @throw [NSException exceptionWithName:@"panic"
                                   reason:@"you don’t really want to known"
                                 userInfo:nil];
    printf("f: 2-you never see me\n");
}
 
int g(void)
{
    printf("g: 1-you see me\n");
    @try {
        f();
        printf("g: 2-you do not see me (in this example)\n");
    } @catch(NSException* e) {
        printf("g: 3-you see me\n");
        @throw;
        printf("g: 4-you never see me\n");
    } @finally {
        printf("g: 5-you see me\n");
    }
    printf("g: 6-you do not see me (in this example)\n");
}

最后一点,C++ 的 catch(…) 可以捕获任意值,但是Objective-C 中是不可以的。事实上,只有对象可以被抛出,也就是说,我们可以始终使用 id 捕获异常。

另外注意,Cocoa 中有一个 NSException 类,推荐使用此类作为一切异常类的父类。因此,catch(NSException *e) 相当于 C++ 的 catch(…)。

多线程

线程安全

在Objective-C 中可以很清晰地使用 POSIX APIs 2 实现多线程。Cocoa 提供了自己的类管理多线程。有一点是需要注意的:多个线程同时访问同一个内存区域时,可能会导致不可预料的结果。POSIX APIs 和 Cocoa 都提供了锁和互斥对象。Objective-C提供了一个关键字 @synchronized,与 Java 的同名关键字是一样的。

@synchronized

由 @synchronized(…) 包围的块会自动加锁,保证一次只有一个线程使用。在处理并发时,这并不是最好的解决方案,但却是对大多数关键块的最简单、最轻量、最方便的解决方案。@synchonized 要求使用一个对象作为参数(可以是任何对象,比如 self),将这个对象作为锁使用。

@implementation MyClass
 
-(void) criticalMethod:(id) anObject {
    @synchronized(self) {
        // 这段代码对其他 @synchronized(self) 都是互斥的
        // self 是同一个对象
    }
    @synchronized(anObject) {
        // 这段代码对其他 @synchronized(anObject) 都是互斥的
        // anObject 是同一个对象
    }
}
@end
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值