Edited at

Java使いのための「Objective-Cではこう書く」

More than 5 years have passed since last update.


はじめに

わたしがObjective-C を勉強し始めた時にこういう記事に出会えていれば…

と思った記事を作りたいということで書きました。

著者はもともと Java ユーザーで、 Objective-C は1~2年目のドシロートです。

記事内に誤りや改善点がありましたら、編集リクエストいただければ随時反映いたします。

(編集理由を併記していただけると嬉しいです)

2014/03/17時点で本稿の修正は打ち切りました。閲覧時点で情報が古い・または不正確な可能性がありますことを予めご了承下さい。


クラスと型

Objective-C 側はなるべく流行りの記法を意識しています。


整数値


プリミティブ型

Java

int num1 = 10;

long num2 = 20L;

Objective-C

NSInteger num1 = 10;

NSInteger num2 = 20L;

型やバイト長を特別強く意識しない限りは

int も long も NSInteger でOK


オブジェクト型

Java

Integer num1 = 10; // Integer.valueOf(10);

Long num2 = 20L; // Long.valueOf(20L);

Objective-C

NSNumber *num1 = @10;

NSNumber *num2 = @20L;

@10 は古い記法でいうと [NSNumber numberWithInt:10] に相当。

各型に対応した様々な書き方があります。



  • @10 (int)


  • @10U (unsigned int)


  • @10L (long)


  • @10UL (unsigned long)


  • @10.5f (float)


  • @10.5 (double)


  • @YES @NO (BOOL)

詳細な仕様は下記ドキュメントを参照。

Objective-C Literals

概要はこちらのブログ記事がわかりやすいです。

Modern Objective-Cで実現するシンプルコーディングのススメ


文字列

Java

String str1 = "hello!";

String jpStr = "こんにちは!";

// 連結
String str2 = str1 + "world!";
System.out.println(str2); // hello!world!

// 置換
String str3 = str1.replaceAll("l", "L");
System.out.println(str3); // heLLo!

Objective-C

// NSString リテラル記法

NSString *str1 = @"hello!";
NSString *jpStr = @"こんにちは!";

// 連結。他言語のように加算演算子 + は利用できない
NSString *str2 = [str1 stringByAppendingString:@"world!"];
NSLog(@"%@", str2); // hello!world!

// 置換
NSString *str3 = [str1 stringByReplacingOccurrencesOfString:@"l" withString:@"L"];
NSLog(@"%@", str3); // heLLo!

文字列を操作するだけでこんなにたくさんキーが打てます。

そう、 Objective-C ならね。

また、次のようにしてC言語の文字列からも NSString を得られます。

// C言語の文字列 "..."

char *cStr = "これはC言語の文字列です";
NSString *strFromC = @(cStr);

// 古い記法
// NSString *strFromC = [NSString stringWithUTF8String:cStr];


論理値

Java

boolean done = true;

boolean isEmpty = false;

Objective-C

BOOL done = YES; // 1

BOOL isEmpty = NO; // 0

実体が数値な点にご注意。


汎用のオブジェクト型

Java

Object obj = new Object();

Object foo = null;

Objective-C

id obj = [[NSObject alloc] init];

id foo = nil;

nil は Java の null とは違い、例えば上記サンプルの id foo

メッセージを送信(メソッド呼び出し)しても例外にはならない点にご注意。

詳細は Apple のドキュメントを参照

Objective-Cによるプログラミング


  • p.41「nilの取り扱い」

  • p.102「nilをNSNullで表す」


コレクション

Java
Objective-C

List
NSArray, NSMutableArray

Map
NSDictionary, NSMutableDictionary

Set
NSSet, NSMutableSet

Objective-C のコレクションクラスの要素は id 型で扱うので、

一つのコレクションに複数の異なる型を入れることができます。

逆に、取り出すときの型にも注意してください。

また、 各種コレクションは nil を格納できません。

値がないことを要素として格納したい場合は NSNull を使います。


List

Java

List<Integer> nums = new ArrayList<>();

nums.add(10);
nums.add(25);
nums.add(38);

for (Integer i : nums) {
System.out.println(i);
}

System.out.println(nums.size()); // 3
System.out.println(nums.isEmpty()); // false

Objective-C

// リテラル記法 @[ ... ] で作った NSArray は Immutable(不変)

// NSArray *nums = @[ @10, @25, @38 ];

// 要素の追加や削除がある場合は NSMutableArray を使う
NSMutableArray *nums = [NSMutableArray array];
[nums addObject:@10];
[nums addObject:@25];
[nums addObject:@38];

for (NSNumber *i in nums) {
NSLog(@"%@", i);
}

NSLog(@"%d", [nums count]); // 3
NSLog(@"%d", ([nums count] == 0)); // 0

[nums count]nums.count でもOK。

Collection#isEmpty() にあたるメソッドがないので、空かどうかの検査は要素数を調べます。


Map

Java

Map<String, String> params = new HashMap<>();

params.put("Content-Type", "text/html");
params.put("Connection", "keep-alive");

Iterator<String> keysIt = params.keySet().iterator();

for (String key : keysIt) {
String value = params.get(key);
System.out.println(String.format("%s = %s", key, value));
}

Objective-C

NSMutableDictionary *params = [NSMutableDictionary dictionary];

params[@"Content-Type"] = @"text/html";
params[@"Connection"] = @"keep-alive";

// 古い記法
// [params setObject:@"text/html" forKey:@"Content-Type"];
// [params setObject:@"keep-alive" forKey:@"Connection"];

for (NSString *key in params) {
NSString *value = params[key];
NSLog(@"%@ = %@", key, value);

// 古い記法
// NSString *value = [params objectForKey:key];
}


Set

Java

Set<String> names = new HashSet<>();

names.add("john");
names.add("mary");
names.add("bob");

for (String name : names) {
System.out.println(name);
}

Objective-C

NSMutableSet *names = [NSMutableSet set];

[names addObject:@"john"];
[names addObject:@"mary"];
[names addObject:@"bob"];

for (NSString *name in names) {
NSLog(@"%@", name);
}

Set にはリテラル記法がありません。


文法とか


命名規則

Cocoa向けコーディングスタイルガイドライン を参照。

人気のオープンソースライブラリなどを見てみても、

これに準拠したスタイルが多く見受けられます。

基本的には iOS SDK のコンポーネントを真似すればだいたいOKです。

外部に公開するフレームワーク等、名前が衝突する可能性があるコードを書く場合は

自作するクラスに 大文字3文字 の接頭辞を付与します。

なお、大文字2文字の接頭辞はAppleによって予約されています。

Programming with Objective-C: Conventions


In order to keep class names unique, the convention is to use prefixes on all classes. […] Two-letter prefixes like these are reserved by Apple for use in framework classes. […] Your own classes should use three letter prefixes.


自作アプリなどの閉鎖されたコードであれば、接頭辞は無くても構いません。

ちなみに本稿の各サンプルコードは MySampleApp から MSA という接頭辞にしてみました。


メソッドの文化

[obj someMethodWithArg:myArg1 andSecondArg:myArg2];

Java から移ってくるとどうしてもメソッド呼び出しに違和感がありますが

やってればそのうち慣れます。こればっかりは慣れです。

例えば文字列型のところで使ったサンプルコードについて、

[str1 stringByAppendingString:@"world!"];

これは、ちょっと強引ですが次のように読めます。


  • str1 を使って

  • string を得たい。どんな string かというと(By)

  • 何かをつけたしたものだ。(Appending)

  • つけたすのは文字列 @"world!" だ。(String:@"world")

"stringByAppending" が「文字列を足した結果を得るメソッド」を表し、

@"world!" の直前にある "String" は第1引数の型を示唆しています。

区切って並べると英文のように読めるのが特徴ですね。


クラスの定義

Java


Parson.java

public class Parson {

private String name;

public Parson(String name) {
setName(name);
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void greet() {
String message = buildGreetingMessage(getName());
System.out.println(message);
}

@Override
public String toString() {
return "Parson{name=" + name + "}";
}

private static String buildGreetingMessage(String name) {
return String.format("hi, my name is %s", name);
}

}


Objective-C


Parson.h

#import <Foundation/Foundation.h>


@interface Parson : NSObject

@property (nonatomic, copy) NSString *name;

- (void)greet;

@end



Parson.m

#import "Parson.h"


// private なメンバは無名カテゴリを利用して定義する
@interface Parson()

+ (NSString *)buildGreetingMessage:(NSString *)name;

@end

@implementation Parson

- (void)greet
{
NSString *message = [self buildGreetingMessage:self.name];
NSLog(@"%@", message);
}

// JavaのObject#toString()に相当
- (NSString *)description {
return [NSString stringWithFormat:@"Parson{name=%@}", self.name];
}

+ (NSString *)buildGreetingMessage:(NSString *)name
{
return [NSString stringWithFormat:@"hi, my name is %@", name];
}

@end


ややムリヤリ感はありますが、基本文法を詰め込んでみました。

@property の属性については次の投稿がわかりやすいです。

Objective-C のプロパティ属性のガイドライン @uasi

よく使う型に関して、次の点に気をつけておけばとりあえず前に進めます。


  • 基本的に nonatomic を指定

  • NSString や NSArray など、NSMutable~ なサブクラスがある型は copy を追加

  • Java/Android開発でWeakReferenceを使いたくなるようなモノには weak を追加



アクセサに副作用を持たせたりする必要がなければ、プロパティの定義は

ヘッダーファイルの @property による宣言だけでOKです。

この場合の内部動作はこちらの記事で分かりやすく解説されています。

Modern Objective-Cでのシンプルなプロパティ記述方式 @KUMAN

詳しい言語仕様は Apple のドキュメントを参照。

Objective-Cによるプログラミング


列挙の定義

Java


Issue.java

public class Issue {

private String title;

private String message;

private Status status = Status.NEW;

public static enum Status {
NEW,
IN_PROCESS,
CLOSED,
}

// getter, setter, constructor...

}


Objective-C


MySampleApp/MSAIssue.h

#import <Foundation/Foundation.h>


typedef NS_ENUM(NSInteger, MSAIssueStatus) {
MSAIssueStatusNew,
MSAIssueStatusInProcess,
MSAIssueStatusClosed,
};

@interface MSAIssue : NSObject

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *message;
@property (nonatomic) MSAIssueStatus status;

@end


列挙定義のところは、

typedef enum MSAIssue : NSInteger {

MSAIssueStatusNew,
...
};

と書いても ほぼ 同じですが、 NS_ENUM を使って定義した場合は

switch 文で利用するときに case がすべての値を網羅しているかチェックされるようになります。


行の折り返し位置

[obj someMethod:arg1

withFoo:foo
withBar:bar];

Obejctive-C では、メソッドシグネチャの ":" の高さを合わせます。


改行位置

- (void)myMethod

{ // メソッドのブラケットは新しい行

if (condition) { // ステートメントのブラケットは行内
}

// 辞書は任意で読みやすいように
NSDictionary *dict = @{ @"foo": @"bar",
@"hoge": @"huga", };

NSDictionary *otherDict = @{
@"foo": @"bar",
@"hoge": @"huga",
@"sample": @"test",
};
}


ドキュメンテーションコメントと文書生成

HeaderDoc が利用できます。

HeaderDoc User Guide : Introduction

サードパーティー製の appledoc も人気のようです。

こちらの記事で詳しく解説されています。

appledocでドキュメント生成 @mtgto


Q. 文書生成しなくていいからとりあえずコードアシストに説明出したい

A. 行コメントはトリプルスラッシュ、ブロックコメントはJavadocと同じでOK。

#import <Foundation/Foundation.h>


@interface Hoge : NSObject

/// なまえ
@property (nonatomic, copy) NSString *name;

@end

@implementation Hoge

- (void)test
{
self.na/* ここでコードアシストを起動 */
}

@end

結果

スクリーンショット 2014-03-14 17.07.03.png


グローバルな定数の定義

Java


MyUrlConstants.java

package com.example.myapp;

public class MyUrlConstants {

/** 基底URL */
public static final String BASE_URL = "http://qiita.com";

/** ログインページのパス */
public static final String PATH_LOGIN = "/login";

}



SomeLogic.java

package com.example.myapp.foo;

import com.example.myapp.MyUrlConstants;

// あるいは
// import static com.example.myapp.MyUrlConstants.BASE_URL;
// import static com.example.myapp.MyUrlConstants.PATH_LOGIN;

public class SomeLogic {

public void someMethod(){
System.out.println(MyUrlConstants.BASE_URL);
System.out.println(MyUrlConstants.PATH_LOGIN);

// import static の場合
// System.out.println(BASE_URL);
// System.out.println(PATH_LOGIN);
}

}


Objective-C


MySampleApp/MSAMyUrlConstants.h

#import <Foundation/Foundation.h>


@interface MSAMyUrlConstants : NSObject

/// 基底URL
UIKIT_EXTERN NSString *const MSAMyUrlBaseUrl;

/// ログインページのパス
UIKIT_EXTERN NSString *const MSAMyUrlPathLogin;

@end



MySampleApp/MSAMyUrlConstants.m

#import "MSAMyUrlConstants.h"


@implementation MSAMyUrlConstants

NSString *const MSAMyUrlBaseUrl = @"http://qiita.com";

NSString *const MSAMyUrlPathLogin = @"/login";

@end



MySampleApp/MSASomeLogic.h

#import <Foundation/Foundation.h>


@interface MSASomeLogic :NSObject

- (void)someMethod;

@end



MySampleApp/MSASomeLogic.m

#import "MSASomeLogic.h"

#import "MSAMyUrlConstants.h"

@implementation MSASomeLogic

- (void)someMethod
{
NSLog(@"%@", MSAMyUrlBaseUrl);
NSLog(@"%@", MSAMyUrlPathLogin);
}

@end


Objective-C (というか C言語)の static 修飾子は Java と意味が違うのでご注意。

UIKIT_EXTERN は基本的に extern と同じですが、

下に引用したマクロ定義からも分かる通り C と C++ の差を吸収してくれます。


UIKitDefines.h

#ifdef __cplusplus

#define UIKIT_EXTERN extern "C" __attribute__((visibility ("default")))
#else
#define UIKIT_EXTERN extern __attribute__((visibility ("default")))
#endif

C++ を使う可能性がある、または特に理由がなければ、 UIKIT_EXTERN の方が安全です。

こちらの stackoverflow の記事も参考になります。

When to use UIKIT_EXTERN vs just extern


Q. #define MY_CONST_STR @"hoge" とかじゃダメ?

A. マクロを定数として使うのは好ましくない場合が多いようです。

プリプロセッサマクロの定義は、ソース上のマクロ名の部分を文字列置換するようもので、

Java 系で例えるならば Gradle で定数を ReplaceTokens によって埋め込むような行為に近いです。

さらに再定義によって変更できるため、定数として使うのは良くない場合が多いです。


Q. コレクション型の静的定数が作れないんだけど

A. 仕様です。

静的メソッドとして定義し、GCD dispatch_once を使って初期化します。


MySampleApp/MSAMyAppConstants.h

#import <Foundation/Foundation.h>


@interface MSAMyAppConstants : NSObject

+ (NSArray *)MSAMyAppSearchEngines;

@end



MySampleApp/MSAMyAppConstants.m

#import "MSAMyAppConstants.h"


@implementation MSAMyAppConstants

+ (NSArray *)MSAMyAppSearchEngines
{
static NSArray *searchEngines;
static dispatch_once_t onceToken;
dispatch_once (&onceToken, ^{
searchEngines = @[ @"Google", @"Yahoo!", @"Bing" ];
});
return searchEngines;
}

@end


メソッド内の static は、「そのメソッド内で静的」。

dispatch_once() は、渡された実行ブロックを一度だけ実行します。

動作的には同一視できないものですが、イメージ的には Java でいうと

静的初期化子を使って凌ぐ書き方に近いですね。


(unsigned) int, long と NSInteger, NSUInteger

特別な理由がない限り、整数型は NS~ を使っておけばOK。

NS~ は、実行環境のアーキテクチャに応じて実際の型が適切に変化する模様。

お手元の Xcode で適当に NSInteger の変数を⌘+クリックして

マクロ定義を見るとわかりやすいです。


objc/NSObjCRuntime.h

#if __LP64__ || (TARGET_OS_EMBEDDED && !TARGET_OS_IPHONE) || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64

typedef long NSInteger;
typedef unsigned long NSUInteger;
#else
typedef int NSInteger;
typedef unsigned int NSUInteger;
#endif

32bit環境と64bit環境で自動的に適切な大きさの型を採用しています。


float, double と CGFloat

int vs NSInteger とほぼ同じ理由で、 CGFloat を使っておけばOK。


CoreGraphics/CGBase.h

#if defined(__LP64__) && __LP64__

# define CGFLOAT_TYPE double
# define CGFLOAT_IS_DOUBLE 1
# define CGFLOAT_MIN DBL_MIN
# define CGFLOAT_MAX DBL_MAX
#else
# define CGFLOAT_TYPE float
# define CGFLOAT_IS_DOUBLE 0
# define CGFLOAT_MIN FLT_MIN
# define CGFLOAT_MAX FLT_MAX
#endif

これも実行環境によって double, float どちらを型とするか判断しています。

"CG~" の名が示すように CoreGraphics フレームワークの一部ですが

iOSアプリケーションプロジェクトを生成するとデフォルトでリンクされているので

特に使用をためらう必要はないと思います。

UIViewのサイズ指定などでよく目にすることになります。


以上です。

思いついたことがあれば随時書き足していこうと思います。