Classes - 类

来源于 Ry’s Objective-C Tutorial - RyPress

一个学习Objective-C基础知识的网站.

个人觉得很棒,所以决定抽时间把章节翻译一下.

本人的英语水平有限,有让大家误解或者迷惑的地方还请指正.

原文地址:http://rypress.com/tutorials/objective-c/classes.html

仅供学习,如有转摘,请注明出处.


正如其他许多面向对象的编程语言,OC类提供了一个创建对象的设计(图).首先,你在类中定义一组(系列)可重用的属性以及行为(方法).随后,使用该类实例化(一个)对象来与这些属性和行为交互.

OC与C++在从类实现中抽象出它的接口(这个方面)很相似.一个接口声明了类中的公共属性和方法,并在对应的实现文件中定义这些属性与方法实际执行的代码.这跟我们之前所见的函数声明实现分离关注点一样.


A class’s interface and implementation
A class’s interface and implementation

在这个模块,我们将探讨类的基础语法,涉及接口,实现,属性和方法,并探讨实例化对象的规范方法.我们也将会介绍一些OC的内省和反射的能力.

创建类 - 使用Xcode

接口

Car.h包含了一些模板代码,但我们将要把它改成下面所示的.(Car.h)声明了一个model的属性以及drive方法.

// Car.h
#import <Foundation/Foundation.h>

@interface Car : NSObject {
    // Protected instance variables (not recommended)
}

@property (copy) NSString *model;

 - (void)drive;

@end

用@interface指令来创建一个接口,跟在(@interface)后面是这个类以及其超类的名称,通过冒号分隔.受保护的变量可以定义在花括号内,但大多数开发人员把实例变量当做实现的细节并倾向于把它们放在.m(实现)文件中.

@property指令则声明了一个公共属性,并且(copy)决定该属性(property)的内存管理方式.在这种情况下,给模型分配的值会被作为副本存储,而不是被直接指向.属性模块(对这方面)论述更多细节.紧跟(@property)其后的是该属性的数据类型以及名称,跟常规的变量声明一样.

-(void)drive 这一行声明了一个无参的drive方法,(void)这部分规定了它的返回值类型.预置的减号(-)标识该方法是一个实例方法(与类方法对应).

实现

任何一个类的实现第一件事是导入相应的接口.@implementation指令与@interface指令除了不用包含它的超类之外,其他都类似.私有变量可以存储在跟在类名之后的花括号内.

// Car.m
#import "Car.h"

@implementation Car {
    // Private instance variables
    double _odometer;
}

@synthesize model = _model;    // Optional for Xcode 4.4+

- (void)drive {
    NSLog(@"Driving a %@. Vrooooom!", self.model);
}

@end

@synthesize是为属性生成存取方法的便捷指令(在Xcode4.4之后会自动生成存取(方法),不使用这个指令也行,但对于属性起别名还是有用的,不过很少人这么做).默认情况下,getter(获取)方法就是属性名称,setter(设置)方法则是在属性名加上set前缀并将属性名第一个字母大写(setModel).这比手动创建属性的存取(方法)容易得多.synthesize这个语句中的_model决定该属性要使用的私有实例变量名.

在Xcode4.4中,通过@property声明的属性会自动合成(存取器),所以如果你对默认的实例变量的命名规约习惯的话,你可以忽略@synthesize这一行.

drive的实现与接口中的定义有着相同的签名,但在其后跟着该方法调用是需要执行的代码.请注意我们是如何通过 self.model 来代替 _model 访问实例变量的值(的方式).这是一个很好的实践,因为它(self.model)利用了属性的存取方法.一般来说,唯一你要直接访问实例变量的地方是在init方法以及dealloc方法.

self关键字引用着(指着)调用方法的实例(类似C++与Java中的this关键字).除了访问属性之外,self也可以用来调用在同一个类中定义的方法(比如,[self anotherMethod]).我们将在该教程中看到很多这样的例子.

实例与使用

任何需要访问一个类的文件都必须导入那个类的头(接口)文件 - 决不能尝试直接导入类的实现文件.那样有悖于公有API与其底层实现分离的目标.为了查看Car类的行为,将main.m修改如下:

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Car *toyota = [[Car alloc] init];
        
        [toyota setModel:@"Toyota Corolla"];
        NSLog(@"Created a %@", [toyota model]);
        
        toyota.model = @"Toyota Camry";
        NSLog(@"Changed the car to a %@", toyota.model);
        
        [toyota drive];
        
    }
    return 0;
}

在使用#import指令导入接口之后,便可以使用上面写的alloc/init方式实例化对象.正如你所见,实例(一个对象)分成两步:首先,你必须通过调用alloc方法给该对象分配内存空间,然后初始化该对象以便使用.你绝不能使用一个未初始化的对象.

必须将对象存储为指针是值得重复(的话题).这也是为什么我们声明变量使用Car *toyota而不是Car toyota.

想调用OC对象的方法,可以通过在方括号内放置(该对象的)实例与方法,并且用空格隔开实现.参数在跟在方法名之后,且前面放置冒号.所以,如果你有从事C++,Java或者Python的背景,那么这种调用方式[toyota setModel:@"Toyota Corolla"]可以转化(理解)为:

toyota.setModel("Toyota Corolla");

对刚转到这门语言(OC)的人来说,这种方括号语法可能会不习惯,但请放心,在你阅读完[Methods]模块后,你会对OC方法的约定感到非常舒服(适应,不仅仅是舒服).

这个例子也向你展示了两种使用对象属性的方式.既可通过synthesize又可通过setModel存取方法,或者使用便捷的点语法,这对那些曾使用过Simula-style语言的开发人员来说,应该更熟悉.

<h4 id="jump">类方法与变量</h4>
上面的代码段定义了实例层次的属性以及方法,但也能定义类层次的.在其他编程语言中被统称为"static"方法/属性(不要与static关键字混淆).

类方法的声明跟实例方法的很像,除了它们的前缀用加号替换了减号.比如,给Car.h如下增加一个类层次的方法:

// Car.h
+ (void)setDefaultModel:(NSString *)aModel;

同样地,类方法的实现也在方法前有个加号.尽管OC中的类层次变量在技术没有这种(写法),但你可以在定义实现之前声明一个静态变量来与之对应(模拟):

// Car.m
#import "Car.h"

static NSString *_defaultModel;

@implementation Car {
...

+ (void)setDefaultModel:(NSString *)aModel {
    _defaultModel = [aModel copy];
}

@end

[aModel copy]这种调用方式创建了这个参数的副本,而不是直接分配(给_defaultModel).这就是我们在给model属性使用copy特性时真正做的事情.

Class methods use the same square-bracket syntax as instance methods, but they must be called directly on the class, as shown below. They cannot be called on an instance of that class ([toyota setDefaultModel:@"Model T"] will throw an error).

// main.m
[Car setDefaultModel:@"Nissan Versa"];

"构造"方法

(准确来说)OC中并没有构造方法.相应的,OC中,一个对象在申请(分配内存)之后就立即调用init方法来完成初始化操作.这也是为什么实例化操作总是两步过程:分配(内存空间),然后初始化.稍后将讨论一下类层次的初始化方法.

init是默认的用来初始化的方法,但你仍可以定义你自己的可以接受配置参数的初始化版本(方法).即使自定义的初始化方法也没有什么特写-除了方法名总是用init开头之外,它们也只是普通的实例方法.下面展示了一个"构造"方法原型(模板):

// Car.h
- (id)initWithModel:(NSString *)aModel;

若要实现上述的方法,你应该遵循initWithModel:所示的公认的(权威的)初始化模式.其中super关键字引用着父类,再(强调)一次,self关键字引用着调用该方法的实例.下一步,我们将下述的方法添加到Car.m中.

// Car.m
- (id)initWithModel:(NSString *)aModel {
    self = [super init];
    if (self) {
        // Any custom setup work goes here
        _model = [aModel copy];
        _odometer = 0;
    }
    return self;
}

- (id)init {
    // Forward to the "designated" initialization method
    return [self initWithModel:_defaultModel];
}

初始化方法应永远都是返回一个指向对象本身的引用,并且如果未能完成初始化,应该返回nil.这就是为什么我们在使用对象之前要核查self是否存在的原因.通常也只有初始化方法才需要做这件事,剩下的只需要调用(该类)指定的初始化器就行了.这样的话,当你有几个自定义的init方法时便可忽略样板代码(更加通用)了.

请注意我们是怎样在initWithModel:方法给_model和_odometer赋值的.记住,这是唯一的地方(初始化方法),你应该这么做-而在其他的方法中,你应该这么用-self.model,self.odometer.

类层次的初始化

initialize方法是类层次中与init等同的(初始化方法).它能让你在使用类之前对类进行设置.比如,我们可以(通过这方法)来给_defaultModel填充一个合法的值,就像下面这样:

// Car.m
+ (void)initialize {
    if (self == [Car class]) {
        // Makes sure this isn't executed more than once
        _defaultModel = @"Nissan Versa";
    }
}

initialize类方法对于每个类来说,都是在使用之前仅调用一次.这种情况包括Car的所有子类,也就意味着假如Car的其中一个子类没有重新实现(重写)initialize方法,那么Car将会被调用两次.所以,使用self == [Car class]条件来保证initialization的代码只执行一次是很好的做法.也需要注意,在类方法中,self关键字引用着类本身,而不是一个实例.

OC不强制要求标识一个方法是被重写的.虽然init和initialize都是在它们的超类NSObject中定义的,但你在Car.m中重定义它们,编译器也不会抱怨.

下一个重复的main.m将展示我们自定义的初始化方法.在第一次使用类之前,[Car initialize]会被自动调用,设置_defaultModel为@"Nissan Versa".可以在第一个NSLog()看到(结论).当然,也可以第二个日志输出中看到自定义方法的结果.

// 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 Corvette"];
        NSLog(@"Created a %@, too.", chevy.model);
        
    }
    return 0;
}

动态类型

类本身就代表着对象,这也就是的它们可以查看自己的属性(内省),甚至改变自己的行为(通过反射).这些时非常强大的动态类型能力,因为这种能力可以让你即使在不知道对象是什么类型的情况下调用对象的方法或者设置对象的属性.

最容易得到一个类对象的方式时通过类层次的方法(对(经常使用)这些冗余术语标识歉意).例如,[Car class]返回一个代表Car本身的对象.你可以将该对象传递给类似isMemberOfClass:以及isKindOfClass这样的方法来获取其他实例的信息.下面给出了一个综合的例子.

// main.m
#import <Foundation/Foundation.h>
#import "Car.h"

int main(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;
}

NSClassFormString()函数时其中一种让你触摸(获取)到类对象的方式.这种方式很灵活,因为它能让你在运行时动态的请求类对象;然而,它也是相当低效.由于这个原因,你应该尽可能地选择类方法(来请求).

如果你对动态类型感兴趣,请务必查阅选择器以及id类型.

总结

在这个模块,我们学习了怎么创建类,实例化对象,定义初始化方法,以及操作类层次的方法以及变量.我们也简单的看了动态类型.

前一个模块提到OC不支持命名空间,那也是Cocoa函数需要像NS,CA,AV等这样的前缀去避免命名冲突.这(使用前缀)对类也有同样要求.推荐的规约是使用三个字母前缀用于你应用程序中的特定类(比如,XYZCar).

即使你对属性和方法感觉还不是很舒服也请不要担心,因为这些(上述的内容)基本上是你在开始写自己类时要知道的了,尽管我们略过了一些重要的细节.在下一个模块,我们将通过详细的地查看@property指令以及影响其行为的所有特性来填补这些孔(略过的重要细节).


写于15年09月05号

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容