Objective-C 的方法原型及重载和覆盖

毫无疑问,Objective-C 也是一种面向对象语言,那么面向对象有三个基本特征(封装、继承、多态)。重载似乎与这三大特征没多大关系,方法覆盖才预示着多态。但重载还是与覆盖有一定的关联,它们都要求你能识别出方法的原型,这就引出方法原型与重载的关系。

Objective-C 与最常见的语言如,C++、C#、Java 在这方面是不同的,我们知道,Java、C++ 和 C# 识别方法原型是依据于方法名、参数个数、与参数类型这三要素决定,比如下面出自于一个类中的四个计算面积的方法,对于 Java、C++ 或 C#,它们是合法的:

float calculateArea()
float calculateArea(int width)
float calculateArea(float width)
float calculateArea(int width, int height)

因为它们的方法原型是不同的,分别是:

calculateArea()
calculateArea(int)
calculateArea(float)
calculateArea(int, int)

然而这应用到 Objective-C 中就不同的,因为 Objective-C 还为方法的引入了一个参数描述的概念。如果你在 Objective-C 的某个类中相应的写下四个类似的方法:

-(float) calculateArea
-(float) calculateArea: (int) width
-(float) calculateArea: (float) width
-(float) calculateArea: (int) width andHeight: (int) height

那是不行的,这时候第二个和第三个方法会有冲突,会得到错误:

error: duplicate declaration of method '-calculateArea:'

它们被认为是一样的方法,因为它们的方法原型是与参数描述有关,在 Objective-C 中方法的定义格式是:

-/+ (返回类型) 方法名: (参数1类型) 形参1 参数2描述: (参数2类型) 形参2 参数3描述: (参数3类型) 形参3 .......

Objective-C 对于每一个参数都要有一个参数描述,而第一个参数的描述由方法名本身来承载,所以有许多像

initWithFrame: stringWithString: 这样的方法,在方法名的后部分 frame 或 string 描述了第一个参数

基于上,Objective-C 的方法原型是:

方法名: 参数2描述:参数3描述: .......

应用于上面四个方法时,它们的原型分别是,(注意参数描述后的冒号,一个参数对应有一个冒号):

calculateArea
calculateArea:
calculateArea:
calculateArea:andHeight:

所以第二、三个方法就冲突了,这里还得讲一点,无参数的方法原型就是方法名,有一个参数的原型是方法名后带个冒号(:),所以执行事件时用 SEL(onclick:) 定位 IBAction 时方法名后要加个冒号,因为它后面有一个参数是 (id) sender。

另一方面,采用 Objective-C 的方法命名方式,即方法名须描述第一个参数的办法,上面的方法应重新声明如下,并纠正一个方法声明:

-(float) calculateArea
-(float) calculateByWidth: (int) width
-(float) calculateByFloatWidth: (float) width
-(float) calculateByWidth: (int) width andHeight: (int) height

从 Objective-C 对方法原型的认定,也告诉我们对方法的命名,还有每一个参数的描述更要讲究。而且它的方法调用方式(确切说是消息发送机制) 中使用到了参数描述,也类似于有些语言(如 VB、Groovy) 中的命名参数,使得代码更人性化,更易于理解。

再进一步演义,在 Objective-C 中如果两个方法,方法名相同、参数个数一个,并且每一个参数类型也相同,它们也是可以同时存在于一个类中,如:

-(float) calculateByWidth: (int) width andXxxHeight: (int) xxxHeight
-(float) calculateByWidth: (int) width andYyyHeight: (int) yyyHeight

它们也是互为重载的关系,因为它们的原型分别为:

calculateByWidth:andXxxHeight:
calculateByWidth:andYyyHeight:

这在 Java、C++、C# 等面向对象的语方中是不允许的。因此,对于 Objective-C 来说重载的几要素就变成了 方法名、参数个数 和参数描述,与类型无关。最后着重强调一个,Objective-C 的方法原型或重载是与参数类型无关的。

另外,也可以不给方法指定参数描述,或者个别指定个别不指定,这时候调用方式和方法原型也有所不同了,例如:

-(void) set: (int) x : (int) d

调用方式是: [cat set: 1 : 2]
原型是: set::

-(void) set: (int) x : (int) y param3: (int) z

调用方式是:[cat set : 1 : 2 param3:100] 不能用 [cat set : 1 : 2 : 100] 来调用
原型是:set::param3:

参考更官方的文档,之前说的参数描述称作 内部参数名(internal argument name),即给方法调用时用; 而形参被叫做外部参数名(external argument name)。当指定了内部参数名时,调用时一定要用内部参数名来指定参数。

不带参数描述的方式不推荐,会让程序不好理解,还有就是区别方法只能依据方法名与参数的个数了,方法重复定义的可能性就大了。像前面只要方法名是 set 的任何两个参数的方法都是相同的。

由前面,我们知道,在 Objective-C 中方法原型的得来是这样的,假如方法声明是:

-/+ (返回类型) 方法名: (参数1类型) 形参1 参数2描述: (参数2类型) 形参2 参数3描述: (参数3类型) 形参3 .......

那么它的方法原型则是:

方法名: 参数2描述:参数3描述: .......

关键的是它与参数类型无关。

重载的时候我们应避免方法原型相同,而子父类间进行方法覆盖的时候我们又应该要保持一样的方法原型。

我们可以再一次感性认识一下 Objective-C 的方法原型,从 Xcode 中,当 Command + 鼠标悬停 在调用方法上时:

   


在 Xcode 中把方法原型给标识了出来,把上蓝色部分连缀起来就是方法的原型,即签名,如上面两方法的签名分别是:

initWithFrame:
presentPopoverFromRect:inView:permittedArrowDirections:animated:

好了,该说今天的主要话题了,体现在多态中,要达到方法的动态绑定,必须使得父子类的两个方法具体一样的原型。因为传统的语方,如 Java/C++/C# 的方法原型与类型是悉悉相关的,对于相互覆盖的两个方法必须接受一样的类型; 而这对于 Objective-C 就宽松了许多。

比如在 Objective-C 中有这样的应用场景,当父类有个方法是, handleData: (Data1 *) data,子类有个方法是,handleData: (Data2 *) data; 虽然它们处理的数据类型不同,Data2 和 Data1 间也没有继承关系,但是它们的方法原型相同,所以可以形成覆盖,这时候当用父类型去引用子实例时,调用 handleData 时可以动态绑定到子类实现上去。

上面的说法会让人感觉太模糊,下面例子看下:

#import <Foundation/Foundation.h> 
 
@interface Data1 : NSObject
-(void) foo;
@end
@implementation Data1
-(void) foo {
    NSLog(@"I'm Data1 type");
}
@end
 
@interface Data2 : NSObject
-(void) foo;
@end
@implementation Data2
-(void) foo {
    NSLog(@"I'm Data2 type");
}
@end
 
@interface Parent : NSObject
-(void) handleData: (Data1 *) data;
@end
@implementation Parent
-(void) handleData:(Data1 *)data {
    [data foo];
}
@end
 
@interface Child : Parent
-(void) handleData: (Data2 *) data;
@end
@implementation Child
-(void) handleData:(Data2 *)data {
    [data foo];
}
@end
 
int main (int argc, const char * argv[])
{
     
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
     
    Parent *pp = [[Child alloc] init];
     
    Data2 *data2 = [[Data2 alloc] init];
    [pp handleData:data2]; //这行输出  I'm Data2 type
     
    Data1 *data1 = [[Data1 alloc] init];
    [pp handleData:data1]; //这行输出  I'm Data1 type
     
    [pool drain];
    return 0;
}

Data1 和 Data2 之间没有父子关系,也就是传入不同的参数类型时可以动态绑定到相不同的方法上去。更要命的是,当类型为 Data1 时执行的是 Parent 的 handleData: 方法,子类和父类的两个 handleData: 形成了互为重载的关系。

上面的代码会出现警告: warning: Semantic Issue: Incompatible pointer types sending 'Data2 *' to parameter of type 'Data1 *',所以一般来说我们会让 Data2 作为 Data1 的子类的。

Objective-C 的这种特性我们可以用在 IBAction 方法中,例如在 C# 的 WinForm 处理事件时是:

private void button_Click(object sender, EventArgs e)
{
  Button button = (Button) sender;
  button.someAttribute = ......     
}

对于 sender 要转型为 Button,其实 IBAction 的默认形式大至如此:
- (IBAction) buttonClicked: (id) sender {
  UIButton *button = (UIButton *) sender;    
  //do something with the button
}

但我们直接改变参数的类型,则为:
- (IBAction) buttonClicked: (UIButton *) button {  
  //do something with the button
}

这样可以省去了强制转型,方法的表意也更直接。

到了这里,如果您还有点耐心的话,可以看看 Java 在碰到这种情况时的表现,运行如下代码:
package cc.unmi;
 
public class TestOverride {
    public static void main(String[] args) {
        Parent p = new Child();
        p.foo("String"); //输出 Parent:foo,虽然 String 是 Object 的子类
        p.foo(new Object()); //自然输出的是 Parent:foo
         
        new Child().foo("String"); //只有这样才输出 Child:foo
    }
}
 
class Parent {
    public void foo(Object obj){
        System.out.println("Parent:foo");
    }
}
 
class Child extends Parent {
    public void foo(String s){
        System.out.println("Child:foo");
    }
}

对于 new Child() 其实是有两个 foo 方法的选择,可以调用 foo(Object),也可以调用 foo(String)。


本文链接 http://unmi.cc/objectivec-prototype-overload-override, 来自 隔叶黄莺 Unmi Blog

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值