首页 > 编程语言 >从 C++到 Objective-C ----2

从 C++到 Objective-C ----2

时间:2023-07-31 12:37:09浏览次数:35  
标签:初始化 对象 self C++ init Objective


从 C++到 Objective-C(7):继承



简单继承

Objective-C 也有继承的概念,但是不能多重继承。不过,它也有别的途径实现类似多重继承的机制,这个我们后面会讲到。

C++

Objective-C

class Foo : public Bar,

          

{

}
@interface Foo : Bar // 单继承

// 如果要同时“继承” Wiz,需要使用另外的技术

{

}

@end

在 C++ 中,一个类可以继承自一个或多个类,使用 public、protected 以及 private 修饰符。子类的函数如果要调用父类的版本,需要使用 :: 运算符,例如 Bar::,Wiz:: 等。

在 Objective-C中,一个类只能继承一个父类,并且只能是 public 的(这和 Java 是一致的)。同样类似 Java,如果你要在子类中调用父类的函数,需要使用 super。

多重继承

protocol 和分类 categories。我们将在后面的内容详细讲述这两种技术。

虚拟性

虚方法

在 Objective-C 中,所有方法都是虚的,因此,没有 virtual 关键字或其等价物。

虚方法重定义

在 Objective-C 中,你可以定义一个没有在 @interface 块里面声明的方法。但这并不是一种替代 private 的机制,因为这种方法实际是能够被调用的(回想下,Objective-C 中方法的调用是在运行期决定的)。不过,这确实能够把接口定义变得稍微干净了一些。

这并不是一种坏习惯,因为有时你不得不重定义父类的函数。由于所有方法都是虚的,你无需像 C++ 一样在声明中显式写明哪些函数是 virtual 的,这种做法就成为一种隐式的重定义。很多继承西 NSObject 的方法都是是用这种方法重定义的。例如构造方法 init,析构方法 dealloc,view 类的 drawRect: 等等。这样的话,接口就变得更简洁,更易于阅读。不好之处就是,你不能知道究竟哪些方法被重定义了。

formal protocols 来实现。

虚继承

Objective-C 中不允许多重继承,因此也就没有虚继承的问题。

协议

Java 和 C# 使用接口 interface 的概念来弥补多重继承的不足。Objective-C 也使用了类似的机制,成为协议 protocol。在 C++ 中,这种概念是使用抽象类。协议并不是真正的类:它只能声明方法,不能添加数据。有两种类型的协议:正式的 formal 和非正式的 informal。

正式协议

正式协议的方法,所有实现这个协议的类都必须实现。这就是一种验证,也就是说,只要这个类说实现这个协议,那么它肯定可以处理协议中规定的方法。一个类可以实现任意多个协议。

C++


class MouseListener
 
  
{
 
  
public:
 
  
  
 
  
  
 
  
};
 
  

 
  
class KeyboardListener
 
  
{
 
  
public:
 
  
  
 
  
};
 
  

 
  
class Foo : public MouseListener, 
 
  
// Foo 必须实现 mousePressed, mouseClicked 和 keyPressed
 
  
// 然后 Foo 就可以作为鼠标和键盘的事件监听器
 
 
 Objective-C
 
 
@protocol MouseListener
 
  

 
  
-(BOOL) mousePressed;
 
  
-(BOOL) mouseClicked;
 
  

 
  
@end
 
  

 
  
@protocol KeyboardListener
 
  

 
  
-(BOOL) keyPressed;
 
  

 
  
@end
 
  

 
  
@interface Foo : NSObject
 
  
{
 
  
...
 
  
}
 
  
@end
 
  
// Foo 必须实现 mousePressed, mouseClicked 和 keyPressed
 
  
// 然后 Foo 就可以作为鼠标和键盘的事件监听器


C++ 中,协议可以由抽象类和纯虚函数实现。C++ 的抽象类要比 Objective-C 的协议强大的多,因为抽象类可以带有数据。

Objective-C 中,协议是一个特殊的概念,使用尖括号 <…> 表明。注意,尖括号在 Objective-C 中不是模板的意思,Objective-C 中没有类似 C++ 模板的概念。

一个类也可以不经过协议声明,直接实现协议规定的方法。此时,conformsToProtocol: 方法依然返回 NO。出于性能考虑,conformsToProtocol: 方法只检查类接口的声明,不会一个方法一个方法的对比着检查。conformsToProtocol: 的返回值并不会作为是否调用方法的依据。下面是这个方法的原型:


-(BOOL) conformsToProtocol:(Protocol*)protocol
 
  
// Protocol 对象可以由 @protocol(协议名) 返回



实现了正式协议的对象的类型同协议本身是兼容的。这一机制可以作为协议的筛选操作。例如:



// 下面方法是 Cocoa 提供的标准方法
 
  
// 方法参数可以是任意类型 id,但是必须兼容 NSDraggingInfo 协议
 
  
-(NSDragOperation) draggingEntered:(id )sender;



可选方法

有时我们需要这么一种机制:我们的类需要实现一部分协议中规定的方法,而不是整个协议。例如在 Cocoa 中,代理的概念被广泛使用:一个类可以给定一个辅助类,由这个辅助类去完成部分任务。

一种实现是将一个协议分割成很多小的协议,然后这个类去实现一个协议的集合。不过这并不具有可操作性。更好的解决方案是使用非正式协议。在 Objective-C 1.0 中就有非正式协议了,Objective-C 2.0 则提出了新的关键字 @optional 和 @required,用以区分可选方法和必须方法。


@protocol Slave
 
  

 
  
@required // 必须部分
 
  
-(void) makeCoffee;
 
  
-(void) duplicateDocument:(Document*)document count:(int)count;
 
  

 
  
@optional // 可选部分
 
  
-(void) sweep;
 
  

 
  
@required // 又是一个必须部分
 
  
-(void) bringCoffee;
 
  
@end


非正式协议

非正式协议并不是真正的协议,它对代码没有约束力。非正式协议允许开发者将一些方法进行归类,从而可以更好的组织代码。所以,非正式协议并不是协议的宽松版本。另外一个相似的概念就是分类。

让我们想象一个文档管理的服务。假设有绿色、蓝色和红色三种文档,一个类只能处理蓝色文档,而 Slave 类使用三个协议 manageBlueDocuments, manageGreenDocuments 和 manageRedDocuments。Slave 可以加入一个分类 DocumentsManaging,用来声明它能够完成的任务。分类名在小括号中被指定:


@interface Slave (DocumentsManaging)
 
  

 
  
-(void) manageBlueDocuments:(BlueDocument*)document;
 
  
-(void) trashBlueDocuments:(BlueDocument*)document;
 
  

 
  
@end

任何类都可以加入 DocumentsManaging 分类,加入相关的处理方法:


@interface PremiumSlave (DocumentsManaging)
 
  

 
  
-(void) manageBlueDocuments:(BlueDocument*)document;
 
  
-(void) manageRedDocuments:(RedDocument*)document;
 
  

 
  
@end


另一个开发者就可以浏览源代码,找到了 DocumentsManaging 分类。如果他觉得这个分类中有些方法可能对自己,就会检查究竟哪些能够使用。即便他不查看源代码,也可以在运行时指定:



if([mySlave respondsToSelector:@selector(manageBlueDocuments:)])
 
  
   [mySlave



严格说来,除了原型部分,非正式协议对编译器没有什么意义,因为它并不能约束代码。不过,非正式协议可以形成很好的自解释性代码,让 API 更具可读性。



从 C++到 Objective-C(8):继承(续)



Protocol 对象

运行时,协议就像是类对象,其类型是 Protocol*。例如,conformsToProtocol: 方法就需要接受一个 Protocol* 类型的参数。@protocol 关键字不仅用于声明协议,还可以用于根据协议名返回 Protocol* 对象。



Protocol* myProtocol = @protocol(协议名)



远程对象的消息传递

由于 Objective-C 的动态机制,远程对象之间的消息传递变得很简单。所谓远程对象,是指两个或多个处于不同程序,甚至不同机器,但是可以通过代理完成同一任务,或者交换信息的对象。正式协议就是一种可以确保对象提供了这种服务的有效手段。正式协议还提供了很多额外的关键字,可以更好的说明各种参数。这些关键字分别是 in, out, inout, bycopy, byref 和 oneway。这些关键字仅对远程对象有效,并且仅可以在协议中使用。出了协议,它们就不被认为是关键字。这些关键字被插入到在协议中声明的方法原型之中,提供它们所修饰的参数的额外信息。它们可以告知,哪些是输入参数,哪些是输出参数,哪些使用复制传值,哪些使用引用传值,方法是否是同步的等等。以下是详细说明:

       in:参数是输入参数;

       out:参数是输出参数;

       inout:参数即是输入参数,又是输出参数;

       bycopy:复制传值;

       byref:引用传值;

       oneway:方法是异步的,也就是不会立即返回,因此它的返回值必须是 void。

例如,下面就是一个返回对象的异步方法:



-(oneway void) giveMeAnObjectWhenAvailable:(bycopy out id *)anObject;


默认情况下,参数都被认为是 inout 的。如果参数由 const 修饰,则被当做 in 参数。为参数选定是 in 还是 out,可以作为一种优化手段。参数默认都是传引用的,方法都是同步的(也就是不加 oneway)。对于传值的参数,也就是非指针类型的,out 和 inout 都是没有意义的,只有 in 是正确的选择。

分类

创建类的分类 categories,可以将一个很大的类分割成若干小部分。每个分类都是类的一部分,一个类可以使用任意多个分类,但都不可以添加实例数据。分类的好处是:

       对于精益求精的开发者,分类提供了一种划分方法的机制。对于一个很大的类,它可以将其划分成不同的角色;

       分类允许分开编译,也就是说,同一个类也可以进行多人的分工合作;

       如果把分类的声明放在实现文件(.m)中,那么这个分类就只在文件作用域中可见(虽然这并没有调用上的限制,如果你知道方法原型,依然可以调用)。这样的分类可以取一个合适的名字,比如 FooPrivateAPI;

       一个类可以在不同程序中有不同的扩展,而不需要丢弃通用代码。所有的类都可以被扩展,甚至是 Cocoa 中的类。

最后一点尤其重要。很多开发人员都希望标准类能够提供一些对他们而言很有用的方法。这并不是一个很困难的问题,使用继承即可实现。但是,在单继承的环境下,这会造成出现很多的子类。仅仅为了一个方法就去继承显得有些得不偿失。分类就可以很好的解决这个问题:

C++

Objective-C

class MyString : public string

{

public:

  

  

};



int MyString::vowelCount(void)

{

...

}
@interface NSString (VowelsCounting)

// 注意并没有使用 {}

-(int) vowelCount; // 统计元音的数目

@end



@implementation NSString (VowelsCounting)

-(int) vowelCount

{

...

}

@end

在 C++ 中,这是一个全新的类,可以自由使用。

在 Objective-C 中,NSString 是 Cocoa 框架的一个标准类。它是使用分类机制进行的扩展,只能在当前程序中使用。注意此时并没有新增加类。每一个 NSString 对象都可以从这个扩展获得统计元音数目的能力,甚至常量字符串也可以。同时注意,分类不能增加实例数据,因此没有花括号块。

分类也可以使匿名的,更适合于 private 的实现:


@interface NSString ()
 
  
// 注意并没有使用 {}
 
  
-(int) myPrivateMethod;
 
  
@end
 
  

 
  
@implementation NSString ()
 
  
-(int) myPrivateMethod
 
  
{
 
  
...
 
  
}
 
  
@end



混合使用协议、分类和子类

混合使用协议、分类和子类的唯一限制在于,你不能同时声明子类和分类。不过,你可以使用两步来绕过这一限制:



@interface Foo1 : SuperClass //ok
 
  
@end
 
  

 
  
@interface Foo2 (Category)  //ok
 
  
@end
 
  

 
  
// 下面代码会有编译错误
 
  
@interface Foo3 (Category) 
 
  
@end
 
  

 
  
// 一种解决方案
 
  
@interface Foo3 : SuperClass // 第一步
 
  
@end
 
  

 
  
@interface Foo3 (Category) // 第二步
 
  
@end



从 C++到 Objective-C(9):实例化



类的实例化位导致两个问题:构造函数、析构函数和赋值运算符如何实现,以及如何分配内存。

在 C++ 中,变量默认是“自动的”:除非被声明为 static,否则变量仅在自己的定义块中有意义。动态分配的内存可以一直使用,直到调用了 free() 或者 delete。C++ 中,所有对象都遵循这一规则。

然而在Objective-C 中,所有对象都是动态分配的。其实这也是符合逻辑的,因为 C++ 更加 static,而Objective-C 则更加动态。除非能够在运行时动态分配内存,否则 Objective-C 实现不了这么多动态的特性。

构造函数和初始化函数

分配 allocation和初始化 initialization的区别

在 C++ 中,内存分配和对象初始化都是在构造函数中完成的。在 Objective-C 中,这是两个不同的函数。

内存分配由类方法 alloc 完成,此时将初始化所有的实例数据。实例数据将被初始化为 0,除了一个名为 isa 的 NSObject 的指针。这个指针将在运行时指向对象的实际类型。实例数据根据传入的参数初始化为某一特定的值,这一过程将在一个实例方法 instance method 中完成。这个方法通常命名为 init。因此,构造过程被明确地分为两步:内存分配和初始化。alloc 消息被发送给类,而 init 消息则被发送给由 alloc 创建出来的新的对象。初始化过程不是可选的,alloc 之后应该跟着 init,之后,父类的 init 也会被调用,直到 NSObject 的 init 方法。这一方法完成了很多重要的工作。

在 C++ 中,构造函数的名字是规定好的,必须与类名一致。在 Objective-C 中,初始化方法与普通方法没有什么区别。你可以用任何名字,只不过通常都是选用 init 这个名字。然而,我们还是强烈建议,初始化方法名字一定要用 init 或者 init 开头的字符串

使用 alloc 和 init

调用 alloc 之后将返回一个新的对象,并且应该给这个对象发送一个 init 消息。init 调用之后也会返回一个对象。通常,这就是初始化完成的对象。有时候,如果使用单例模式,init 可能会返回另外的对象(单例模式要求始终返回同一对象)。因此,init 的返回值不应该被忽略。通常,alloc 和 init 都会在一行上。

C++
 
 
Foo* foo = new Foo;
 
 
 Objective-C
 
 
Foo* foo1 = [Foo alloc];
 
  
[foo1 init]; // 这是不好的行为:应该使用 init 的返回值
 
  
Foo* foo2 = [Foo alloc];
 
  
foo2 = [foo2 init]; // 正确,不过看上去很啰嗦
 
  
Foo* foo3 = [[Foo alloc] init]; // 正确,这才是通常的做法



初始化方法的正确示例代码

一个正确的初始化方法应该有如下特点:

       名字以init 开始;

       返回能够使用的对象;

       调用父类的 init 方法,直到 NSObject 的init 方法被调用;

       保存[super init...] 的返回值;

       处理构造期间出现的任何错误,无论是自己的还是父类的。

下面是一些代码:

C++
 
 
class Point2D
 
  
{
 
  
public:
 
  
  
 
  
private:
 
  
  
 
  
   int y;
 
  
};
 
  
Point2D::Point2D(int anX, int anY) {x = anX; y = anY;}
 
  
...
 
  

 
  
Point2D 
 
  
Point2D* p2 = new Point2D(5, 6);
 
 
 Objective-C
 
 
@interface Point2D : NSObject
 
  
{
 
  
  
 
  
  
 
  
}
 
  

 
  
// 注意,在 Objective-C 中,id 类似于 void*
 
  
// (id) 就是对象的“一般”类型
 
  
-(id) initWithX:(int)anX andY:(int)anY;
 
  
@end
 
  

 
  
@implementation Point2D
 
  

 
  
-(id) initWithX:(int)anX andY:(int)anY
 
  
{
 
  
  // 调用父类的初始化方法
 
  
   if (!(self = [super init])) // 如果父类是 NSObject,必须进行 init 操作
 
  
       return nil; // 如果父类 init 失败,返回 nil
 
  
  // 父类调用成功,进行自己的初始化操作
 
  
  self->x = anX;
 
  
  
 
  
   return self; // 返回指向自己的指针
 
  
}
 
  
@end
 
  

 
  
...
 
  
Point2D* p1 = [[Point2D alloc] initWithX:3 andY:4];



从 C++到 Objective-C(10):实例化(续)



self = [super init...]

在上一篇提到的代码中,最不可思议的可能就是这句 self = [super init...]。回想一下,self 是每个方法的一个隐藏参数,指向当前对象。因此,这是一个局部变量。那么,为什么我们要改变一个局部变量的值呢?事实上,self 必须要改变。我们将在下面解释为什么要这样做。

[super init] 实际上返回不同于当前对象的另外一个对象。单例模式就是这样一种情况。然而, 有一个 API 可以用一个对象替换新分配的对象。Core Data(Apple 提供的 Cocoa 里面的一个 API)就是用了这种 API,对实例数据做一些特殊的操作,从而让这些数据能够和数据库的字段关联起来。当继承 NSManagedObject 类的时候,就需要仔细对待这种替换。在这种情形下,self 就要指向两个对象:一个是 alloc 返回的对象,一个是 [super init] 返回的对象。修改 self 的值对代码有一定的影响:每次访问实例数据的时候都是隐式的。正如下面的代码所示:



@interface B : A
{
int i;
}

@end

@implementation B

-(id) init
{
   // 此时,self 指向 alloc 返回的值
   // 假设 A 进行了替换操作,返回一个不同的 self
  
   NSLog(@"%d", i); // 输出 self->i 的值
   self = newSelf; // 有人会认为 i 没有变化
   NSLog(@"%d", i); // 事实上,此时的 self->i, 实际是 newSelf->i,
                    // 和之前的值可能不一样了
  
}

@end
...
B* b = [[B alloc] init];



self = [super init] 简洁明了,也不必担心以后会引入 bug。然而,我们应该注意旧的 self 指向的对象的命运:它必须被释放。第一规则很简单:谁替换 self 指针,谁就要负责处理旧的 self 指针。在这里,也就是 [super init] 负责完成这一操作。例如,如果你创建 NSManagedObject 子类(这个类会执行替换操作),你就不必担心旧的 self 指针。事实上,NSManagedObject 的开发者必须考虑这种处理。因此,如果你要创建一个执行替换操作的类,你必须知道如何在初始化过程中释放旧有对象。这种操作同错误处理很类似:如果因为非法参数、不可访问的资源造成构造失败,我们要如何处理?

初始化错误

初始化出错可能发生在三个地方:

   调用 [super init...] 之前:如果构造函数参数非法,那么初始化应该立即停止;

   调用 [super init...] 期间:如果父类调用失败,那么当前的初始化操作也应该停止;

   调用 [super init...] 之后:例如资源分配失败等。

在上面每一种情形中,只要失败,就应该返回 nil;相应的处理应该由发生错误的对象去完成。这里,我们主要关心的是1, 3情况。要释放当前对象,我们调用 [self release] 即可。

在调用 dealloc 之后,对象的析构才算完成。因此,dealloc 的实现必须同初始化方法兼容。事实上,alloc 将所有的实例数据初始化成 0 是相当有用的。


@interface A : NSObject {
  
}

-(id) initWithN:(unsigned int)value;
@end

@implementation A

-(id) initWithN:(unsigned int)value
{
   // 第一种情况:参数合法吗?
   if (value == 0) // 我们需要一个正值
  
      
      
  
   // 第二种情况:父类调用成功吗?
   if (!(self = [super init])) // 即是 self 被替换,它也是父类
       return nil; // 错误发生时,谁负责释放 self?
   // 第三种情况:初始化能够完成吗?
  
   void* p = malloc(n); // 尝试分配资源
   if (!p) // 如果分配失败,我们希望发生错误
  
      
      
  
}
@end


将构造过程合并为 alloc+init

有时候,alloc 和 init 被分割成两个部分显得很罗嗦。幸运的是,我们也可以将其合并在一起。这主要牵扯到 Objective-C 的内存管理机制。简单来说,作为一个构造函数,它的名字必须以类名开头,其行为类似 init,但要自己实现 alloc。然而,这个对象需要注册到 autorelease 池中,除非发送 retain 消息,否则其生命周期是有限制的。以下即是示例代码:


// 啰嗦的写法
NSNumber* tmp1 = [[NSNumber alloc] initWithFloat:0.0f];
...
[tmp1 release];
// 简洁一些
NSNumber* tmp2 = [NSNumber numberWithFloat:0.0f];
...
// 无需调用 release



从 C++到 Objective-C(11):实例化(续二)



默认构造函数:指定初始化函数

在 Objective-C 中,默认构造函数没有实在的意义,因为所有对象都是动态分配内存,也就是说,构造函数都是确定的。但是,一个常用的构造函数确实可以精简代码。事实上,一个正确的初始化过程通常类似于:


if (!(self = [super init])) // "init" 或其他父类恰当的函数
 
  
  
 
  
// 父类初始化成功,继续其他操作……
 
  
return self;

剪贴复制代码是一个不良习惯。好的做法是,将共同代码放到一个独立的函数中,通常称为“指定初始化函数”。通常这种指定初始化函数会包含很多参数,因为 Objective-C 不允许参数有默认值。


-(id) initWithX:(int)x
 
  
{
 
  
  
 
  
}
 
  

 
  
-(id) initWithX:(int)x andY:(int)y
 
  
{
 
  
  
 
  
}
 
  

 
  
// 指定初始化函数
 
  
-(id) initWithX:(int)x andY:(int)y andZ:(int)z
 
  
{
 
  
  
 
  
      
 
  
  
 
  
  
 
  
  
 
  
  
 
  
}


如果指定初始化函数没有最大数量的参数,那基本上就没什么用处:

// 以下代码就有很多重复部分
 
  
-(id) initWithX:(int)x // 指定初始化函数
 
  
{
 
  
  
 
  
      
 
  
  
 
  
  
 
  
}
 
  

 
  
-(id) initWithX:(int)x andY:(int)y
 
  
{
 
  
  
 
  
      
 
  
  
 
  
  
 
  
}
 
  

 
  
-(id) initWithX:(int)x andY:(int)y andZ:(int)z
 
  
{
 
  
  
 
  
      
 
  
  
 
  
  
 
  
  
 
  
}

初始化列表和实例数据的默认值

Objective-C 中不存在 C++ 构造函数的初始化列表的概念。然而,不同于 C++,Objective-C的 alloc 会将所有实例数据初始化成 0,因此指针也会被初始化成 nil。C++ 中,对象属性不同于指针,但是在 Objective-C 中,所有对象都被当做指针处理。

虚构造函数

Objective-C 中存在虚构造函数。我们将在后面的章节中详细讲诉这个问题。

类构造函数

在 Objective-C 中,类本身就是对象,因此它也有自己的构造函数,并且也能够被重定义。它显然是一个类函数,继承自 NSObject,其原型是 +(void) initialize;。

第一次使用这个类或其子类的时候,这个函数将被自动调用。但这并不意味着,对于指定的类,这个函数只被调用一次。事实上,如果子类没有定义 +(void) initialize;,那么 Objective-C 将调用其父类的 +(void) initialize;。

析构函数

在 C++ 中,析构函数同构造函数一样,是一个特殊的函数。在 Objective-C 中,析构函数也是一个普通的实例函数,叫做 dealloc。C++ 中,当对象被释放时,析构函数将自动调用;Objective-C 也是类似的,但是释放对象的方式有所不同。

析构函数永远不应该被显式调用。在 C++ 中存在这么一种情况:开发者自己在析构时管理内存池。但是在 Objective-C 中没有这种限制。你可以在 Cocoa 中使用自定义的内存区域,但是这并不会影响平常的内存的分配、释放机制。

C++
 
 
class Point2D
 
  
{
 
  
public:
 
  
  
 
  
};
 
  

 
  
Point2D::~Point2D() {}
 
 
 Objective-C
 
 
@interface Point2D : NSObject
 
  
-(void) dealloc; // 该方法可以被重定义
 
  
@end
 
  

 
  
@implementation Point2D
 
  
// 在这个例子中,重定义并不需要
 
  
-(void) dealloc
 
  
{
 
  
   [super dealloc]; // 不要忘记调用父类代码
 
  
}
 
  
@end


从 C++到 Objective-C(12):实例化(续三)



复制运算符

典型 cloning, copy, copyWithZone:, NSCopyObject()

在 C++ 中,定义复制运算符和相关的操作是很重要的。在 Objective-C 中,运算法是不允许重定义的,所能做的就是要求提供一个正确的复制函数。

克隆操作在 Cocoa 中要求使用 NSCopying 协议实现。该协议要求一个实现函数:



-(id) copyWithZone:(NSZone*)zone;


这个函数的参数是一个内存区,用于指明需要复制那一块内存。Cocoa 允许使用不同的自定义区块。大多数时候默认的区块就已经足够,没必要每次都单独指定。幸运的是,NSObject 有一个函数



-(id) copy;



封装了 copyWithZone:,直接使用默认的区块作为参数。但它实际相当于 NSCopying 所要求的函数。另外,NSCopyObject() 提供一个不同的实现,更简单但同样也需要注意。下面的代码没有考虑 NSCopyObject():

// 如果父类没有实现 copyWithZone:,并且没有使用 NSCopyObject()
 
  
-(id) copyWithZone:(NSZone*)zone
 
  
{
 
  
   // 创建对象
 
  
  
 
  
   // 实例数据必须手动复制
 
  
   clone->integer = self->integer; // "integer" 是 int 类型的
 
  
   // 使用子对象类似的机制复制
 
  
  
 
  
   // 有些子对象不能复制,但是可以共享
 
  
  
 
  
   // 如果有设置方法,也可以使用
 
  
   [clone setObject:self->object];
 
  
   return clone;
 
  
}

注意,我们使用的是 allocWithZone: 而不是 alloc。alloc 实际上封装了allocWithZone:,它传进的是默认的 zone。但是,我们应该注意父类的 copyWithZone: 的实现。


// 父类实现了 copyWithZone:,并且没有使用 NSCopyObject()
 
  
-(id) copyWithZone:(NSZone*)zone
 
  
{
 
  
   Foo* clone = [super copyWithZone:zone]; // 创建新的对象
 
  
   // 必须复制当前子类的实例数据
 
  
   clone->integer = self->integer; // "integer" 是 int 类型的
 
  
   // 使用子对象类似的机制复制
 
  
  
 
  
  // 有些子对象不能复制,但是可以共享
 
  
  
 
  
   // 如果有设置方法,也可以使用
 
  
   [clone setObject:self->object];
 
  
   return clone;
 
  
}



NSCopyObject()

NSObject 事实上并没有实现 NSCopying 协议(注意函数的原型不同),因此我们不能简单地使用 [super copy...] 这样的调用,而是类似 [[... alloc] init] 这种标准调用。NSCopyObject() 允许更简单的代码,但是需要注意指针变量(包括对象)。这个函数创建一个对象的二进制格式的拷贝,其原型是:


// extraBytes 通常是 0,可以用于索引实例数据的空间
 
  
id


二进制复制可以复制非指针对象,但是对于指针对象,需要时刻记住它会创建一个指针所指向的数据的新的引用。通常的做法是在复制完之后重置指针。


// 如果父类没有实现 copyWithZone:
 
  
-(id) copyWithZone:(NSZone*)zone
 
  
{
 
  
   Foo* clone = NSCopyObject(self, 0, zone); // 以二进制形式复制数据
 
  
   // clone->integer = self->integer; // 不需要,因为二进制复制已经实现了
 
  
   // 需要复制的对象成员必须执行真正的复制
 
  
  
 
  
   // 共享子对象必须注册新的引用
 
  
   [clone->objectToShare retain];
 
  
   // 设置函数看上去应该调用 clone->object. 但实际上是不正确的,
 
  
   // 因为这是指针值的二进制复制。
 
  
   // 因此在使用 mutator 前必须重置指针
 
  
  
 
  
   [clone setObject:self->object];
 
  
   return clone;
 
  
}
 
  

 
  
// 如果父类实现了 copyWithZone:
 
  
-(id) copyWithZone:(NSZone*)zone
 
  
{
 
  
  
 
  
   // 父类实现 NSCopyObject() 了吗?
 
  
   // 这对于知道如何继续下面的代码很重要
 
  
   clone->integer = self->integer; // 仅在 NSCopyObject() 没有使用时调用
 
  
   // 如果有疑问,一个需要复制的子对象必须真正的复制
 
  
  
 
  
   // 不管 NSCopyObject() 是否实现,新的引用必须添加
 
  
  
 
  
   clone->object = nil; // 如果有疑问,最好重置
 
  
   [clone setObject:self->object];
 
  
   return clone;
 
  
}



Dummy-cloning,mutability, mutableCopy and mutableCopyWithZone:

如果需要复制不可改变对象,一个基本的优化是假装它被复制了,实际上是返回一个原始对象的引用。从这点上可以区分可变对象与不可变对象。

不可变对象的实例数据不能被修改,只有初始化过程能够给一个合法值。在这种情况下,使用“伪克隆”返回一个原始对象的引用就可以了,因为它本身和它的复制品都不能够被修改。此时,copyWithZone: 的一个比较好的实现是:


-(id) copyWithZone:(NSZone*)zone
 
  
{
 
  
   // 返回自身,增加一个引用
 
  
   return [self retain];
 
  
}

retain 操作意味着将其引用加 1。我们需要这么做,因为当原始对象被删除时,我们还会持有一个复制品的引用。

“伪克隆”并不是无关紧要的优化。创建一个新的对象需要进行内存分配,相对来说这是一个比较耗时的操作,如果可能的话应该注意避免这种情况。这就是为什么需要区别可变对象和不可变对象。因为不可变对象可以在复制操作上做文章。我们可以首先创建一个不可变类,然后再继承这个类增加可变操作。Cocoa 中很多类都是这么实现的,比如 NSMutableString 是 NSString 的子类;NSMutableArray是 NSArray 的子类;NSMutableData 是 NSData 的子类。

然而根据我们上面描述的内容,似乎无法从不可变对象安全地获取一个完全的克隆,因为不可变对象只能“伪克隆”自己。这个限制大大降低了不可变对象的可用性,因为它们从“真实的世界”隔离了出来。

除了 NSCopy 协议,还有一个另外的 NSMutableCopying 协议,其原型如下:


-(id) mutableCopyWithZone:(NSZone*)zone;



mutableCopyWithZone: 必须返回一个可变的克隆,其修改不能影响到原始对象。类似 NSObject 的 copy 函数,也有一个mutableCopy 函数使用默认区块封装了这个操作。mutableCopyWithZone:的实现类似前面的 copyWithZone: 的代码:


// 如果父类没有实现 mutableCopyWithZone:
 
  
-(id) mutableCopyWithZone:(NSZone*)zone
 
  
{
 
  
   Foo* clone = [[Foo allocWithZone:zone] init]; // 或者可用 NSCopyObject()
 
  
  
 
  
   // 类似 copyWithZone:,有些子对象需要复制,有些需要增加引用
 
  
   // 可变子对象使用 mutableCopyWithZone: 克隆
 
  
   //...
 
  
   return clone;
 
  
}
 
 
 不要忘记我们可以使用父类的 mutableCopyWithZone:
 
 
// 如果父类实现了 mutableCopyWithZone:
 
  
-(id) mutableCopyWithZone:(NSZone*)zone
 
  
{
 
  
  
 
  
   //...
 
  
   return clone;
 
  
}

标签:初始化,对象,self,C++,init,Objective
From: https://blog.51cto.com/u_548275/6907004

相关文章

  • 从 C++ 到Objective-C
    从C++到Objective-C(1):前言DevBean 日期:2011年03月18日Objective-C可以算作Apple平台上“唯一的”开发语言。很多Objective-C的教程往往直接从Objective-C开始讲起。不过,在我看来,这样做有时候是不合适的。很多程序员往往已经掌握了另外一种开发语言,如果对一门新......
  • Name Mangling and extern “C” in C++
    SinceC++supportsfunctionoverloading,additionalinformationhastobeaddedtofunctionnames(calledNamemangling)toavoidconflictsinbinarycode.2.FunctionnamesmaynotbechangedinCasitdoesn'tsupportfunctionoverloading. Toavoid......
  • C++11新特性
    一.智能指针1.std::shared_ptrshared_ptr使用了引用计数,每一个shared_ptr的拷贝都指向相同的内存,每次拷贝都会触发引用计数+1,每次生命周期结束析构的时候引用计数-1,在最后一个shared_ptr析构的时候,内存才会释放。2.std::weak_ptrweak_ptr是用来监视shared_ptr的生命周......
  • 代码随想录-哈希表-c++总结
    哈希表内容整体简单,关键是要有利用map映射的思想,以及巩固一些c++标准库的操作这次三数之和一题没有直接做出来,关键在于如何查重一点比较绕15.三数之和-力扣(LeetCode)利用排序+双指针解决三数之和的思路更加清楚此外,四数之和中,四个数相加会溢出int,应改为 ......
  • 【C++学习之路】#include头文件包含
    头文件包含:在预处理结果将头文件的内容原封不动包含在目标文件中#include建议<>包含系统头文件<>从系统指定目录寻找head.h头文件#include"head.h"建议“”包含自定义头文件“”先从当前目录寻找head.h头文件,如果找不到,再到系统指定的目录下寻找......
  • vs(visual stuiod)中vc++工程的Filter和Folder及vcxproj知识
    vs中创建Filter在一个新项目中右键-Add-New,默认只有一选项NewFilter。创建出来的Filter可以理解为是VS的过滤器(虚拟目录),它不会在本地的磁盘上新建目录,而是修改了.filters文件,把这种目录关系记录在.filters文件中。新建一个vc++project,默认有这几种Filter,当然在实际的......
  • C++ 核心指南之 C++ P.哲学/基本理念(上)
    C++核心指南(C++CoreGuidelines)是由BjarneStroustrup、HerbSutter等顶尖C+专家创建的一份C++指南、规则及最佳实践。旨在帮助大家正确、高效地使用“现代C++”。这份指南侧重于接口、资源管理、内存管理、并发等High-level主题。遵循这些规则可以最大程度地保证静......
  • C++ Primer 学习笔记——第八章
    第八章IO库前言C++语言并不会直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO。这些类型支持从设备中读取数据、向设备写入数据IO操作。设备可以是文件、控制台窗口等,还有一些类型允许内存IO。IO库定义了读写内置类型值的操作。8.1IO类在之前我们使用的IO类型......
  • C++入门:命名空间
    在C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称都将存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。1.命名空间的定义定义命名空间,需要使用到namespace关键字,......
  • C++入门:缺省参数
    1.缺省参数的概念缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的参数。#include<iostream>usingnamespacestd;voidFunc(inta=0){ cout<<a<<endl;}intmain(){ Func(); Func(1);......