2015.09.01 RyPress_objective-C: Classes
和許多其他的物件導向語一樣,Objective-C的class提供了建造物的藍圖,首先你在class中先定義一些可以重複使用的性質和行為,然後你就可以將這個類別實體化,生出一個該類別的物件來使用這些性質和行為。
Objective-C和 C++有點類似,會把類別的implementation摘要出來成interface。interface 就負責公開的(public)性質和方法,搭配的implementation則定義了讓這些性質、方法實際運作的程式碼,這個分割的概念和function相同。
在這篇文章中,我們會介紹類別的interfaces、implementations、properties、methods的語法規則(syntax),以及實體物件的規範,還有一些Objective-C的We’ll also introduce some of Objective-C’s introspection and reflection capabilities.
###Creating Classes
好的,現在我們要來寫下一個名叫Car的類別,他的interface名為Car.h(interface檔案也被稱為"header"),implementation名為Car.m,.h和.m這兩個是Objective-C類別的標準副檔名,類別的header file是負責被其他的類別糟蹋用的,implementation file則只給編譯器(compiler)使用。
Xcode提供了方便的模版來新建類別,所以從上面方的工具列File > New > File...或者用快捷鍵cmd+n,選擇建立iOS > Cocoa touch class,接著填入 class: Car,Subclass of : NSObject然後next,選擇儲存位置,確認一下targets有打勾,這個會在complied sources中新增此類別。
NSObject是Objective-C的頂層類別,幾快所有的類別都繼承於他。按下create後,你就會看到Car.h和Car.m在Xcode的專案目錄導覽中。查看導覽中的專案名稱,可以在Build Phases tab裡的Compile Sources中看到Car.m,任何你想要編譯器看到的檔案一定要在這個列表裡,用在萬一你不是使用Xcode來產生source file,可以在這裡手動新增。
###Interfaces
Car.h已包含了一些預設的程式碼,試著把他改成下面的樣子,宣告一個性質:model和一個方法:drive:
// Car.h
#import <Foundation/Fundation.h>
@interface Car : NSObject {
// Protected instance variables (not recommended)
}
@property (copy) NSString *model;
-(void)drive;
@end
interface打從@interface開始宣告,接著是類別名稱和上級類別(superclass)的名稱,類別名稱和上級類別名稱用:分開,然後可以在{}內宣告Protected variables,不過多數的開發者會將實體變數(instance variables)視為implementation的一部份,存放在implementation中。
@property用於宣告一個公開的性質(public property),(copy)的屬性則是用來定義他的記憶體管理中的特性(memory management behavior),在這個案例中,model的值會存成copy來取代direct pointer,(copy)之後接著的是property的型別(data type)和名稱(name),如同宣告一般的變數。
關於性質(properties)的部份這裡會有更詳細的介紹。
-(void)drive這行的意思是有一個名叫drive的方法,沒有帶參數,(void)則是定義了drive所回傳的型別,至於-是指這個方法是一個實體方法,如果前方是+則表示這個方法是類別方法。
類別的implementation第一件要做的事:匯入他的interface,@implementation宣告和@interface類似,但是@implementation不需要加上他的上級類別,在類別名稱後接上{},可以將私有實體變數(private instance variables)寫在{}中:
// Car.m
#import "Car.h"
@implementation Car {
// Private instance variables 私有實體變數
double _odometer;
}
@synthesize model = _model; //這一行在Xcode 4.4版之後可以省略
- (void)drive {
NSLog(@"Driving a %@. Vrooooom!", self.model);
}
@end
@synthesize是一個會自動生出性質(property)的accessor方法的便利指令,在@synthesize預設中getter會是性質的名稱(如:model),setter則會使用大寫的性質名稱,然後在名稱前再加上set(如:setModel),這麼做是比為每一個性質手動寫accessors來得簡單,_modelThe _model portion of the synthesize statement defines the private instance variable name to use for the property.
Xcode 4.4版本,每當宣告@property就會自動synthesized,所以如果你對預設的實體變數命名慣例沒意見的話(如:getter: model, setter: setModel) ,省略@synthesize這行是不會出事的。
drive在implementation中和interface是一樣的開頭,接著
就開始寫入該方法被調用時實際執行的程式碼。不過有一點要注意的是:取得實體變數的值時,是用self.model而不是_model, This is a best practice step because it utilizes the property’s accessor methods. …這裡用了property的accessor方法?…只有在init method和dealloc method才會直接去調用實體變數。
把self這個當作是這個實體呼叫這個方法(像C++或者Jave中的this一樣),在調用性質的情形下,self通常被用來呼叫定義在同類別的中的其他方法,例如:[self anotherMethod],在這個教材中會有許多的這樣的例子。
###Instantiation and Usage 任何檔案在調用類別之前一定要先匯入類別的header檔(如:Car.h),絕對不要直接去呼叫implementation檔,直接匯入implementation會破壞將公開的API從他的實做部份分離的目標(That would defeat the goal of separating the public API from its underlying implementation),所以來看看Car類別,把main.m改成下面這樣:
// mian.m
#import <Foundation/Foundation.h>
#import "Car.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Car *toyota = [[Car alloc] init];
[toyota setModel:@"ToyotaCorolla"]'
NSLog(@"Created a %@", [toyota model]);
toyota.model = @"Toyota Camry";
NSLog(@"Changed th car to a %@", toyota.model);
[toyota drive];
}
return 0;
}
使用#import指令匯入interface之後,就可以用alloc/initpattern來實體化一個物件,如上面所示,實體化是個二步驟的流程,第一步:使用alloc方法分派記憶體位置給這個物件,第二步:使用init方法切始化這個物件,然後這個物件就生好準備給你用了。千千萬萬不要使用未初始化(uninitialized)的物件窩。
值得再次提醒:所有的物件必須被存成pointers。這就是為什麼上面是用Car *toyota而不是Car toyota來宣告變數。
在Objective-C的物件中呼叫一個方法:在[]放入實體物件和方法,兩者中間用 空白隔開,參數則在方法名後加上:傳送。假如你的背景是來自於C++、 Java或者Python,[toyota setModel:@"Toyota Corolla"]這個會被轉成:
toyota.setModel("Toyota Corolla");
[]這個語法對於學習Objective-C的新手可能會造成一點困擾,不過可以確定的是,在你讀完Methods這一部份之後,對於Objective-C的方法慣例你會適應許多。
在這個例子中,同時說明了二種使用物件性質(object's properties)的方法,你可以任意選用synthesized model或者setModel來調用方法,當然你也可以用更方便的.語法來調用方法,用.這個方式應該對於使用Simula-style語言的開發者來說會比較熟悉些。
上面這一段是定義實體層級(instance-level)的性質和方法,另外也可以以定義類別層級(class-level)的性質和方法,這些類別方法有其他的程式語言中常叫為static methods/properties。(別和static keyword搞混了)
類別方法的宣告方式和實體方法類似,只差在類別方法使用+作為前綴,實體方法則是用-,舉個例子,在Car.h中加上類別方法吧:
// Car.h
+ (void)setDefaultModel:(NSString *)aModel;
在implementation中類別方法同樣的也是用+, 在Objective-C中沒有類別變數(class-level variable),不過你可在implementation之前先宣告一個static variable來模仿這個變數的功能:
// Car.h
#import "Car.h"
static NSString *_defaultModel;
@implementation Car{
...
+ (void)setDefaultModel:(NSString *)aModel {
_defaultModel = [aModel copy];
}
@end
[aModel copy]作用是複製一個參數來代替直接指派一個,這就是當我們在model性質上使用(copy)這個屬性時,黑箱中所發生的事。
類別方法和實體方法一樣使用[]宣告語法,不過類別方法只能直接被類別所調用,他是沒辦法被類別中的實體所呼叫的,[toyota setDefaultModel:@"Model T"這樣會噴錯。
// main.m
[Car setDefaultModel:@"Nissan Versa"];
###Constructor" Methods
Objective-C沒有constructor methods構造方法?,一個物件在指派記憶體之後,會立刻呼叫init方法來進行初始化,這就是為什麼要將類別生出實體物件都要進行二步驟流程:指派記憶體,然後初始化,接著會對class-level的初始化做一點介紹。
init是預設的初始化方法,你也可以定義自己的版本,並帶入設定參數,客製初始化方法沒有什麼特別的,他們就如同一般的實體方法一樣,要注意的一點是方法名稱必須用init做開頭,來看看如何模仿constructor method:
// Car.h
- (id)initWithModel:(NSString *)aModel;
要實作這個方法,必須符合初始化pattern的規範,如initWithModel:以下所示,super表示父級類別(parent class),self代表實體呼叫該方法,讓我們繼續把方法加進Car.m中:
// Car.m
-(id)initWithModel:(NSString *)aModel {
self = [super init];
if (self) {
//Any custom set work goes here
_model = [aModel copy];
_odometer = 0;
}
return self;
}
- (id)init {
// Forward to the "designated" initialization method
return [self initWithModel:_defaultModel];
}
初始化方法一定是回傳物件本身,而且如果無法初始化,就要回傳nil,這就是為什麼我們在用self之前需要確認self是否存在,然後要告訴原本的init轉去調用designated initializer,在你有多個客製的init方法,這麼做會把模版的程式碼消除
and the rest should forward calls to this designated initializer. This eliminates boilerplate code when you have several custom init methods.
另外要注意的是,我們直接在initWithModel:方法中直接指派了實體變數_model和_odometer的值,千萬記得這是唯一你可以麼做的地方,在其他的方法中都應該要這麼寫:self.model、self.odometer。
###Class-Level Initialization
initialize method等同於class-level中的init,initialize讓你可以在別人使用這個類別之前設定該類別,例如:這裡把_defaultModel變數設成一個實值:
// Car.m
+ (void)initialize {
if (self == [Car class] {
// Makes sure this isn't executed more than once
_defaultModel = @"Nissan Versa";
}
}
在每個類別被使用前都會呼叫一次initialize這個類別方法,包括Car的所以subclasses,這意思是如果某個subclass沒有回應的話,Car會呼叫二次initialize。因此使用self == [Car class]條件來確何初始化的程式碼只會跑一次是個好的做法。記得在類別方法中,self指的是類別本身,而不是一個實體。
Objective-C不會限制你覆寫方法,即使init和initialize都是定義在Car類別的superclass: NSObject中,當你在Car.m中重新定義這兩個方法時,編譯器不會有什麼意見。
接著在main.m中來看看客製的初始化方法如何運作,在類別第一次被使用之前,[Car initialize]會自動被呼叫,將_defaultModel
設定為`@"Nissan Versa",可以在第一個NSLog()中看到結果,然後第二個NSLog()看到的是實體的客製初始化方法(initWithMOdel:)的結果。
// main.m
#import <Foundation/Foundation.h>
#import "Car.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Instantiating objects
Car *nissan = [[Car alloc] init];
NSLog(@"Created a %@", [nissan model]);
Car *chevy = [[Car alloc] initWithModel:@"Chevy Corvetter"];
NSLog(@"Created a %@", chevy.model);
}
return 0;
}
類別本身也是一種物件,所以能夠查問類別的性質(introspection),或者是動態改變類別的行為,這是個非常威的dynamic typing能力,這個讓你可以在不知道物件是什麼型別的情形下也能呼叫方法或者設定他的屬性。
簡單取得類別物件的方法是透過類別的class-level方法,舉例來說:[Car class]回傳一個代表Car類別的物件,你可以對這個常滿件傳送一些方法,例如:isMemberOfClass:或是isKindOfClass:,用來取得其他實體的資訊,來看看下面的實例:
// �main.m
#import <Foundation/Foundation.h>
#import "Car.h"
int mian(int argc, const char * argv[]) {
@autoreleasepool {
Car *delorean = [[Car alloc] initWithModel:@"Delorean"];
//Get the class of an object
NSLog(@"%@ is an instance of the %@ class", [delorean model], [delorean class]);
// Check an object against a class and all subclasses
if ([delorean isKindOfClass:[NSObject class]] {
NSLog(@"%@ is an instance of NSObject or one " " of its subclasses", [delorean model]);
} else {
NSLog(@"%@ is not an instance of NSObject or ""one of its subclasses
,[delorean model]);
}
// Check an object against a class, but not its subclasses
if ([delorean isMemberOfClass:[NSobject class]] {
NSLog(@"%@ is a instance of NSObject", [delorean model]);
} else {
NSLog(@"%@ is not an instance of NSObject", [delorean model]);
}
// Convert between strings and classes
if (NSClassFromString(@"Car") == [Car class]) {
NSLog(@"I can convert between strings and classes!");
}
}
return 0;
}
NSClassFromString()function是讓你取得手上的類別物件的方法之一,他能讓你在程式運作時動態地請求類別物件,整個非常的彈性,但相當的沒效率,你還是應該儘量使用類別方法來處理。
如果你想多了解dynamic typing一點,一定要再看看Selectors和The id Type。
###Summary 在這個部分,我們學到了如何新增一個類別、實體物件、定義初始化方法、還有使用class-level的方法和變數,並且提到了一點點的dynamic typing。
之前在function部份提到了Objective-C不支援namespace,因盤Cocoa functions會使用命名字首,如:NS、CA、AV等等來避免命名衝突,同樣的概念,在類別命名慣例上則是建議使用三個字的命名字首,如:在APP中新建了一個特殊的類別:XYZCar。
以上大概是開始寫自己的類別時所需要知道的一些東西,某些重要的細節在這裡先暫時掠過,例如properties和 methods,不過不用擔心,下一個部份:Properties就會充滿@property的指令還有介紹屬性是如何影響他們的行為。
學習程式兼翻譯紀綠,新手認知加語言轉換,難免錯誤疏漏不全,勞煩大大多多鞭打指教。ψ(`∇´)ψ