Objective-Cで可変引数

※本記事は、http://www.cocoawithlove.com/2009/05/variable-argument-lists-in-cocoa.html を参考に(というかほぼそのまま意訳)しました。

 

Objective-CはC言語を拡張した言語なのでC言語と同じように可変引数も扱えます。

一般的に、書式文字列と、nil終端リストによる2つのパターンで可変引数を扱います。

 

書式文字列による可変引数

C言語のprintf系で使われている書き方で、書式文字列中のプレースホルダーの位置に引数を埋めてやるやり方です。プレースホルダは%で始まる書式指定子で指定します。

以下は、NSStringのstringWithFormathメソッドの例です。


NSString *myString = [NSString stringWithFormat:@"Number %d, String: %@, Float: %g", 123, @"SomeString", 34.5];
このメソッドは、

+ (id)stringWithFormat:(NSString *)format, …;

と宣言されています。「…」の部分が可変引数を受け取ることを意味しています。

第一引数の文字列中に埋め込まれた書式指定子で、引数の数と各引数の型がわかります。上の例の場合は、%dが整数型、%@がオブジェクト型、%fが 浮動小数点型を示しています。

 

nil終端リスト

nil終端リストで可変引数を渡す場合は、最後の要素がnilで終わる、一連のオブジェクト型の引数を渡します。例えば次のような書き方をします。


NSArray *myArray = [NSArray arrayWithObjects:@"One", @"Two", @"Three", nil];

最後に渡す値はnilでなければなりません。

なぜこのような書き方をするかというと、引数を渡された側のメソッドが、引数がいくつあるのかを判断できないからです。そのために、書式文字列中にプレースホルダーを置くか、引数の最後にnilを置いて引数の終わりを示しているのです。

 

C言語でもそうですが、Objective-Cではメソッドに渡された引数の数や型は実行時に取得できないため、可変引数を受け取る場合、なんらかの取り決めに従って、引数の数と各引数の型を判断する必要があります。

書式文字列を使ったパターンの場合は、書式文字列中のプレースホルダの数で引数の数を、そして各プレースホルダのタイプによって引数の型を判定しています。また、nil終端のパターンの場合は、引数は全てObject型(ポインタ)に限定し、nilが現れるまで捜査することで引数の終わりを判定しています。

 

指定した引数の型と数が実際の引数と合ってっていないとクラッシュする等の誤作動を招きます。

コンパイラの-Wformatを使うと、引数の型と数が合っているかチェックして合っていないと警告を出してくれます。Xcodeでは「Build Settings」の「Typecheck Calls to printf/scanf」がこれに該当します。デフォルトで有効になっていると思います。

 

可変引数のメソッドを実装する。

可変引数を扱うメソッドを実装してみましょう。

次のようなクラスを宣言します。


@interface StringContainer : NSObject
{
    NSString *contents;
}
@end

可変の引数を受け取り、contentsに連結するメソッドを作ろうとおもいます。ここでは、nil終端によるパターンを使います。メソッドの宣言は次のようになります。


- (void)setContentByAppendingStrings:(NSString *)firstString, ...NS_REQUIRES_NIL_TERMINATION;

NS_REQUIRES_NIL_TERMINATIONは、nil終端の引数を受け取ることをコンパイラに指示するマクロです。

 

実装は次のようになります。


- (void)setContentByAppendingStrings:(NSString *)firstArg, ...
{
    NSMutableString *newContentString = [NSMutableString string];
    va_list args;
    va_start(args, firstArg);
    for (NSString *arg = firstArg; arg != nil; arg = va_arg(args, NSString*))
    {
        [newContentString appendString:arg];
    }
    va_end(args);
     
    [contents autorelease];
    contents = [newContentString retain];
}

va_list, va_start, va_arg, va_end は全てC言語の文法で可変引数を扱う時に使用するものです。次のような意味があります。

 

va_list
可変引数へのポインタを保持する変数を宣言します。
va_start
va_listの開始位置を指定します。
va_arg
va_listから次の引数を取得します。取得する引数の型を指定する必要があります。
va_end
va_listで保持されたメモリを開放します。

 

今回の場合は、全てNSString型を受け取ることを想定しているので、va_arg()には、NSString*を指定しています。

 

可変引数を別のメソッドに渡す。

可変引数を扱うメソッドの中で、受け取った引数を可変引数を扱う別のメソッドに渡したい場合はどうればよいでしょう。

Cocoaには可変引数を受け取る多くのメソッドがありますが、多くの場合、va_listを受け取る別バージョンのメソッドも用意されています。

例えば、NSStringのstringWithFormat:...メソッドは可変引数を受け取りますが、同じような働きをするinitWithFormat:arguments:メソッドのargumentosはva_listで宣言されています。このメソッドを使うことで、可変引数を扱う自分のメソッドの中から、その可変引数を直接、initWithFormat:argumens:に渡して呼び出すことができます。

上の例で示したクラスで、可変引数を扱う次のようなメソッドを宣言したとします。


- (void)setContentsWithFormat:(NSString *)formatString, ...;

実装は次のようになります。


- (void)setContentsWithFormat:(NSString *)formatString, ...
{
    [contents autorelease];
 
    va_list args;
    va_start(args, formatString);
    contents = [[NSString alloc] initWithFormat:formatString arguments:args];
    va_end(args);
}

受け取った可変引数をそのまま、initWithFormat:argumens:メソッドに渡しています。

 

可変引数を受け取るメソッドに配列を渡す。

可変引数を受け取るメソッドに配列を渡す場合はどうすればいいでしょう。

例えば、動的に生成した複数の要素をもつ配列があるとします。しかしこれを、NSstringのstringWithFormat:...や、initWithFormat:arguments:に直接渡すことはできません。

MacやiPhoneの環境では、va_listは、引数を含むバイトバッファ(char *)として実装されています。これを踏まえると次のような実装ができます。


- (void)setContentsWithFormat:(NSString *)formatString arguments:(NSArray *)arguments;

というメソッドを作ったとします。formatString中の書式指定子は全てオブジェクト型であると想定すると次のように実装できます。


- (void)setContentsWithFormat:(NSString *)formatString arguments:(NSArray *)arguments
{
    [contents autorelease];	// ARCを使わない場合
 
    char *argList = (char *)malloc(sizeof(NSString *) * [arguments count]);
    [arguments getObjects:(id *)argList];
     
    contents = [[NSString alloc] initWithFormat:formatString arguments:argList];
     
    free(argList);
}

可変引数の内容を、バイトバッファにコピーして、それをinitWithFormat:arguments:に渡しています。

オブジェクト型以外の書式指定子も想定する場合は、それぞれの型のサイズを考慮してバッファを生成する必要があります。