複製を作るためにNSCopyingプロトコルに準拠する
NSCopyingプロトコルについて
RMGWindowControllerの「モデルオブジェクトを追加、削除、複製する」-「MasterMotifオブジェクトを追加、削除、複製する」で、さりげなくMasterMotifクラスのインスタンスオブジェクトにcopyメッセージを送っていますが、このメッセージに対応するにはそれなりの処理が必要です。
NSObjectのcopyメソッドの説明を読んでみましょう。大体以下の様な事が書いてあります。
- copyメソッドはcopyWithZone:メソッドが返したオブジェクトを返す。引数のzoneはnilを指定して呼んでいる。
- copyWithZone:はNSCopyingプロトコルのメソッドである。
- copyメソッドはNSCopyingプロトコルに準拠するクラスにとって便利な様に用意したメソッドである。copyWithZone:メソッドを実装していないと例外を発生する。
- NSObject自体はNSCopyingプロトコルをサポートしない。サブクラスはNSCopyingプロトコルをサポートしてcopyWithZone:メソッドを実装しなければならない。サブクラスのcopyWithZone:メソッドはスーパークラスと連携して動作する為に、最初にsuperにcopyWithZone:メッセージを送るべきである。ただしNSObjectの直接のサブクラスである場合は除く。
- このメソッドを適切に実装する為に、以下の事に注意する事。返されるオブジェクトは、暗黙のうちにこのメソッドを呼びだしたオブジェクトがretainした事になっている。したがってreleaseする責任はこのメソッドを呼びだしたオブジェクトにある。
関連する情報として"Memory Management Programming Guide for Cocoa"の"Implementing Object Copy"がありますので、こちらも参考にするとよいかもしれません。
- 深いコピーと浅いコピー
- "alloc, init..."アプローチを使う
- NSCopyObject()を使う
- 可変オブジェクトと不変オブジェクトのコピー
について書かれています。日本語で読みたい方は「Objective-C MacOS Xプログラミング」の第11章「オブジェクトのコピーと保存」を読まれるとよいでしょう。
インターフェイスファイルの実装
それではMasterMotifをNSCopyingプロトコルに準拠させていきましょう。まずインターフェイスファイルでこのクラスがNSCopyingプロトコルに従う事を宣言します。
@class Mixer,Oscillator;
@interface MasterMotif : NSObject <NSCoding,NSCopying>
{
int resolution,oscillatorSelection;
NSBezierPath *motif;
NSImage *image;
Mixer *mixerX,*mixerY;
Oscillator *selectedOscillator;
NSUndoManager *undoManager;
}
- (void)updateMotif;
- (int)oscillatorSelection;
- (void)selectOscillator:(int)selection;
- (int)resolution;
- (NSBezierPath *)motif;
- (NSImage *)image;
- (Mixer *)x;
- (Mixer *)y;
- (Oscillator *)selectedOscillator;
- (NSUndoManager *)undoManager;
- (void)setResolution:(int)newResolution;
- (void)setMotif:(NSBezierPath *)newMotif;
- (void)setImage:(NSBezierPath *)newMotif;
- (void)setSelectedOscillator:(Oscillator *)oscillator;
- (void)setUndoManager:(NSUndoManager *)newManager;
@end
copyWithZone:を実装する
次にインプリメンテーションファイルでcopyWithZone:メソッドを実装します。実行すべき事はメモリーをアロケートして初期化し、パラメータを自分と同じに設定してから返す事です。メモリーのアロケートはクラスメソッドallocWithZone:を使えばよいのですが、問題は初期化です。
ここでinitメソッドを使ってしまうと、MixerクラスのオブジェクトであるmixerXとmixerYがデフォルトで初期化された新しいオブジェクトになってしまう問題があります。mixerXとmixerYは自分のものをコピーしたものでないといけません。デフォルトで初期化されたオブジェクトを作ってから、そのパラメータを変更していくのは二度手間です。また、そもそもコピー機能の実装はMixerクラス自身に任せるべきです。(Mixerもコピーすべきオブジェクトを内部に持っていますからね)
解決策は新しいイニシャライザの導入です。コピーしたいオブジェクトを引数に持つイニシャライザを導入すれば解決します。
- (id)initWithMixerX:(Mixer *)x mixerY:(Mixer *)y
{
self = [super init];
if(self)
{
mixerX = [x copy];
mixerY = [y copy];
motif = nil;
undoManager = nil;
image = [self newImage];
[mixerX setMasterMotif:self];
[mixerY setMasterMotif:self];
[self setResolution:200];
[self selectOscillator:0];
}
return self;
}
mixerXとmixerYをコピーしている部分を除けば、initメソッドと全く同じ内容です。このイニシャライザを使ってcopyWithZone:メソッドを以下の様に実装します。
- (id)copyWithZone:(NSZone *)zone
{
MasterMotif *clone =
[[[self class] allocWithZone:zone] initWithMixerX:mixerX
mixerY:mixerY];
[clone setResolution:[self resolution]];
[clone selectOscillator:[self oscillatorSelection]];
[clone setUndoManager:[self undoManager]];
return clone;
}
2006/11/12
「Objective-C MacOS Xプログラミング」の第11章「オブジェクトのコピーと保存」を読まれるとよいでしょう、などと書きながら実は自分でちゃんと読んでいませんでした。サブクラスを作成した場合でも問題が無いようにするためには、メソッド内に自分自身の固定したクラス名を書かない方がよいのだそうです。
[MasterMotif allocWithZone:zone]の部分を[[self class] allocWithZone:zone]に修正しました。