- キー・バリュー・コーディング-
キー値の検証
キー・バリュー・コーディングは、プロパティの値の妥当性を検証するための API を提供しています。この検証を使うことで、値を受け入れるか、他の値を代わりに提供するか、新しい値を拒否してエラーを返すか、といった選択をすることができます。
目次
検証メソッドの名前規約
検証メソッドを実装する
検証メソッドを呼び出す
スカラー値の検証
検証メソッドとメモリ管理
検証メソッドの名前規約
アクセッサメソッドに名前規約があったように、検証メソッドにも名前規約があります。検証メソッドは、-validate<Key>:error: というフォーマットを持ちます。リスト 4-1 は、プロパティ name に対する検証メソッドの宣言の例です。
リスト 4-1 name プロパティの検証メソッドの宣言
- (BOOL)validateName:(id *)ioValue error:(NSError **)outError {
// コードの実装
return ...;
}
検証メソッドを実装する
検証メソッドには、2 つのパラメータが参照で渡されます。検証される値オブジェクトと、エラー情報を返すための NSError です。
検証メソッドには、3 つの結末があります。
- オブジェクトの値は有効なので、YES を返します。代わりの値もエラーも返しません。
- オブジェクトの値が変更されて、有効になります。この場合、値のパラメータに新しい有効な値を設定して、YES を返します。エラーは置き換えられません。
- オブジェクトの値は有効ではなく、変更もされません。この場合、検証が失敗した理由を示す NSError オブジェクトを設定して、NO を返します。
リスト 4-2 は、name プロパティに対する検証メソッドの実装の例です。このメソッドは、値オブジェクトが文字列で、大文字であることを保証します。
リスト 4-2 name プロパティの検証メソッド
- (BOOL)validateName:(id *)ioValue error:(NSError **)outError {
if (*ioValue == nil) {
return YES;
}
// 強制的に大文字にする
NSString *capitalizedName = [*ioValue capitalizedString];
*ioValue = capitalizedName;
return YES;
}
ioValue で返すオブジェクトは、自動解放しておくことに注意してください(「検証メソッドとメモリ管理」を参照)。
検証メソッドを呼び出す
検証メソッドは直接呼び出すこともできますし、キーを指定して validateValue:forKey:errror: を呼び出すことでも呼び出せます。validateValue:forKey:error: のデフォルト実装は、受け手のクラスで validate<Key>:error: のパターンにマッチする名前の検証メソッドを探します。そのようなメソッドが見つかったら、それが呼び出されて結果が返されます。そのようなメソッドが無かったら、validateValue:forKey:error: は YES を返して、値を検証します。
注意:キー・バリュー・コーディングは、自動的には検証を実行しません。検証メソッドを呼び出すのは、アプリケーションの責任です。また、プロパティの -set<Key> の実装は、決して健勝メソッドを呼び出すべきではありません。
スカラー値の検証
検証メソッドは、値パラメータがオブジェクトで、オブジェクトではないプロパティの値の結果は、「スカラー値と構造体のサポート」で議論されるように、NSValue や NSNumber でラップされることを期待します。リスト 4-3 の例は、スカラー値のプロパティである age の検証メソッドの例です。
リスト 4-3 スカラー値プロパティの検証メソッド
- (BOOL)validateAge:(id *)ioValue error:(NSError **)outError {
if (*ioValue == nil) {
// これは setNilValueForKey でひっかかります
// 0 の NSNubmer が新しく作られているでしょう
return YES;
}
if ([*ioValue floatValue] <= 0.0) {
NSString *errorString = NSLocalizedStringFromTable(
@"Age must greater then zero", @"Person",
@"validation: zeor age error");
NSDictionary *userInfoDict =
[NSDictionary dictionaryWithObject:errorString
forKey:NSLocalizedDescriptionKey];
NSError *error = [[[NSError alloc] initWithDomain:PERSON_ERROR_DOMAIN
code:PERSON_INVALID_AGE_CODE
userInfo:userInfoDict] autorelease];
*outError = error;
return NO;
}
else {
return YES;
}
// ...
検証メソッドとメモリ管理
検証メソッドは、参照で渡された値オブジェクトエラーオブジェクトの両方を置き換えることができるので、適切なメモリ管理が保証されるように気をつけないといけません。アプリケーションは、検証メソッドに渡されるオブジェクトは、検証メソッドを呼び出す前に自動解放されていることを保証しなくてはいけません。
例として、リスト 4-4 のコードは、リスト 4-2 のvalidateName:error: のメソッドを呼び出します。ここでは newName オブジェクトを作って、自動解放しないで validateName:error: メソッドの渡しています。しかし、検証メソッドは newName を参照するオブジェクトを置き換えるので、newName オブジェクトを明示的に解放しようとすると、検証されたオブジェクトが代わりに解放されてしまいます。これは、2 つの問題を引き起こします。後で検証された名前オブジェクトにアクセスしようとすると、クラッシュします。なぜなら、setValue:forKey: で保持されていたはずのオブジェクトが解放されているからです。さらに、validateName:error: に渡された名前オブジェクトはリークします。
リスト 4-4 validateName:error: の不正な呼び出し
NSString *newName;
NSError *outError;
newName = [[NSString alloc] initWithString:@"freddy"];
if ([person validateName:&newName error:&outError]) {
// 値を設定します
// これは newName オブジェクトを retain するでしょう
[person setValue:newName forKey:@"name"];
}
else {
// ユーザに値が有効ではなかったことを知らせます
}
[newName release];
リスト 4-5 の例は、validateName:error: を呼び出す前に newName オブジェクトを自動解放することで、この問題を防ぎます。オリジナルのオブジェクトは自動解放によって解放され、検証されたオブジェクトは setValue:forKey: メッセージの結果として、受け手によって保持されます。
リスト 4-5 validateName:error: の正しい呼び出し
NSString *newName;
NSError *outError;
newName = [[[NSString alloc] initWithString:@"freddy"] autorelease];
if ([person validateName:&newName error:&outError]) {
// 値を設定します
// これは newName オブジェクトを retain するでしょう
[person setValue:newName forKey:@"name"];
}
else {
// ユーザに値が有効ではなかったことを知らせます
}