【2020年版】Objectcive-C で AtCoder の問題を解いてみた話

8 min read読了の目安(約7300字

zenn を使うのが初めてなので他所で書いたものを試験的に投稿してみた記事です。
【2020年版】って書いておきながら投稿日が2021年なのはそういう理由です。

要約

Objective-C で AtCoder に参加するのは、2020年においても茨の道です。

前提

筆者は

  • プログラミング経験3年程度の初級者です。
  • C言語は体系立てて学んだことがないです。
  • Objective-C は不思議な縁でめぐりあい、今は仕事で使っています。
  • 一昨年くらいに Objective-C でAtCoderに挑戦しようとして、Welcome to AtCoder すら断念した過去があります。

以降の文中で「以前」という表現が出たら、それはこの時のことを指しています。

動機

最近 AtCoder で Swift 5.2.1 が使えるようになったことを知り、しかも競技プログラミングにかなり向いていそうだったので、Objective-C ももしや何か変化があるのか[1]と気になりました。

やったこと

AtCoder Beginners Selection の Welcome to AtCoder などを、Objective-C で解いてみました。

やってみた

実際に書いてみて、いくつか気づきがありましたが、全体を通して やはりObjective-Cらしく書くのが難しい といった印象です。

Objective-C を選んだとしてもC言語標準の関数が使えるので、Cで書けば通っちゃいます。(例: 提出 #18636624)
ただそれだと「最初からCで書け」みたいな話になるので、今回の試行では scanfprintf といったものは封印しています[2]

Welcome to AtCoder は以下のように解きました。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSData *stdinData = [[NSFileHandle fileHandleWithStandardInput] availableData];
        NSString *stdinString = [[NSString alloc] initWithData:stdinData encoding:NSUTF8StringEncoding];
        NSMutableArray<NSMutableArray<NSString *> *> *canonicalInput = [NSMutableArray new];
        for (NSString *str in [stdinString componentsSeparatedByString:@"\n"]) {
            [canonicalInput addObject:[[str componentsSeparatedByString:@" "] mutableCopy]];
        }

    	NSString *a = [[canonicalInput objectAtIndex:0] objectAtIndex:0];
    	NSString *b = [[canonicalInput objectAtIndex:1] objectAtIndex:0];
    	NSString *c = [[canonicalInput objectAtIndex:1] objectAtIndex:1];
    	NSString *s = [[canonicalInput objectAtIndex:2] objectAtIndex:0];
    	int sumOfABC = a.intValue + b.intValue + c.intValue;
    	NSString *answer = [NSString stringWithFormat:@"%i %@", sumOfABC, s];

        NSFileHandle *fileHandle = [NSFileHandle fileHandleWithStandardOutput];
        [fileHandle writeData:[answer dataUsingEncoding:NSUTF8StringEncoding]];
        [fileHandle writeData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]];
        [fileHandle closeFile];

    }
    return 0;
}

ARC は無効のまま?

ARC(Automatic Reference Counting) は以前と同じく、無効であると考えられます。

しかし、 @autoreleasepool { ... }省略しても警告が出ず、エラーにもなりませんでした。(例: 提出 #18627186)

コードテストで @autoreleasepool { ... } を使わずに書いた時に限り、標準エラー出力に autorelease called without pool for object とメッセージが出ます。

この挙動がAtCoderのサイトの仕様によるものかどうかは現状確認できていません。

いずれにしても、@autoreleasepool を省いても動くからと言って 「ARCが有効になっている」と考えるのは無理がありそうです

なお、エラーにこそならないものの、実行時間は露骨に長くなります
内部で警告を出力しているからでしょうか...?

@autoreleasepool 実行時間(ms) リンク
あり 124 提出 #18627168
なし 224 提出 #18627186

参考までに ABC163のC問題で試したところ、@autoreleasepool を外したら TLE しました。

@autoreleasepool 実行時間(ms) リンク
あり 992 提出 #18559763
なし 2251(TLE) 提出 #18627491

最近だとプログラミング初心者がいきなりObjective-Cを選ぶとは考えにくいですが、@autoreleasepool を省略しても警告が出ないという点においては 「ハマりどころ」の素質がある ように思います。

Lightweight Generics は使える?

以前と違って[3]、利用できるようです。
改めて確認すると、下記のようなコードは特にエラーにはならず、処理してもらえました。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray<NSNumber *> *array1 = @[@123, @456, @789];
        NSString *className1 = NSStringFromClass([[array1 objectAtIndex:0] class]);
        NSArray<NSString *> *array2 = @[@"abc", @"de", @"f"];
        NSString *className2 = NSStringFromClass([[array2 objectAtIndex:0] class]);
	    
        NSFileHandle *fileHandle = [NSFileHandle fileHandleWithStandardOutput];
        
        [fileHandle writeData:[[NSString stringWithFormat:@"Hello! %@", className1] dataUsingEncoding:NSUTF8StringEncoding]];        
        [fileHandle writeData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]];
        // -> Hello! NSIntNumber
        
        [fileHandle writeData:[[NSString stringWithFormat:@"Hello! %@", className2] dataUsingEncoding:NSUTF8StringEncoding]];
        [fileHandle writeData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]];
        // -> Hello! NSConstantString
        
        [fileHandle closeFile];
    }
    return 0;
}

このあたり「何を以て Lightweight Generics が使える とするのか」というのは本当はもっときちんとした定義をして確認が必要だと感じるのですが、私の知識不足や時間不足によりそこまで厳密には検証できていません。

確認した限りでは

  • 型のうしろに<...> [4]を書いただけでエラーになったりしない
  • コレクションから取り出した時点で型についての情報を保持していた[5]
  • 想定されない型の値を代入したときに警告が出る[6]

といった事があったので、Lightweight Generics が使えるのではないか、と考えました。

モダン記法は使える?

そこまでたくさん試したわけではないのですが、以前と同じく、利用できないようです。

特に item[1] みたいな書き方はエラー[7]になるので、 [item objectAtIndex:1] のように書かないといけません。

-[NSFileHandle writeData:error:] は使える?

以前と同じく、利用できないようです[8]

そのため、廃止予定となっている-[NSFileHandle writeData:] を引き続き使わないといけません。

感想

AtCoderにObjective-Cで挑戦するのは、2020年においても茨の道でした。

参考

この記事の数倍以上詳しいので、AtCoderでObjective-Cを用いてチャレンジする際はぜひ読んでみてください。

脚注
  1. 実際に確かめると、去年の時点で Clang 3.8.0 で、最新は Clang 10.0.0 だったので、変化はありました。 ↩︎

  2. ただ、そうなると今度は「どのようなルールで書けばObjective-Cらしく書いたと言えるのか」が気になるところです。
    残念ながら私はまだこれに関する明確な答えは持っていませんので、今回はなんとなく雰囲気で(つまり主観で) Objective-Cっぽければヨシ! としています。 ↩︎

  3. 以前は利用できなかった記憶があるのですが、私の書き方が間違っていたせいでエラーになった可能性もありえないとは断定できないため、実は以前から利用できていた可能性もあります。 ↩︎

  4. 要するに NSArray<NSNumber *> みたいに、型名に続けて「山カッコで型の名前を囲んだ文字列」を記述した文字列のことです。 ↩︎

  5. NSStringFromClass を使って Array の要素を確認すると、 NSIntNumberNSConstantString といった文字列が得られたので、型についての情報を保持していると判断しています。 ↩︎

  6. 警告についてですが

    NSArray<NSNumber *> *array = @[@"abc", @"de", @"f"];
    

    のように、NSNumber * の配列に NSString * の配列を代入しようとすると、コードテストでは以下のような警告を確認できます。

    ./Main.m:8:41: warning: object of type 'NSString *' is not compatible with array element type 'NSNumber *' [-Wobjc-literal-conversion]
            NSArray<NSNumber *> *array = @[@"abc", @"de", @"f"];
                                           ^~~~~~
    

    終了コードが0だとなぜか出ないようですが、コードテストの仕様なのかObjective-Cの仕様なのかはわかりません... ↩︎

  7. たとえば

    NSArray<NSNumber *> *array1 = @[@123, @456, @789];
    NSLog(array1[0]);
    

    だと、

    error: passing 'NSArray<NSNumber *>' to parameter of incompatible type 'id'
    

    という風にエラーになります。 ↩︎

  8. 使うと unrecognized selector sent to instance ... とエラーになります。 ↩︎