澳门新葡亰信誉平台游戏运行时-消息转发

by admin on 2020年1月17日

php没有内置相关函数,可以自己定义函数实现。

 

看上去最为简单的(public)继承的概念由两个单独部分组成:函数接口的继承和函数模板继承。这两种继承之间的区别同本书介绍部分讨论的函数声明和函数定义之间的区别完全对应。

1、最直观最容易理解的方法:

函数调用

Objective-C是一门动态语言,一个函数是由一个selector(SEL),和一个implement(IML)组成的。Selector相当于门牌号,而Implement才是真正的住户(函数实现)。

和现实生活一样,门牌可以随便发(@selector(XXX)),但是不一定都找得到住户,如果找不到系统会给程序几次机会来程序正常运行,实在没出路了才会抛出异常。下图是objc_msgSend调用时,查找SEL的IML的过程。咱们以这个流程为例看看其中涉及的很有用的函数。

 

 澳门新葡亰信誉平台游戏 1

图:运行时查找函数的流程

1. 类函数的三种实现

作为一个类设计者,有时候你只想派生类继承成员函数的接口(声明)。有时候你想让派生类同时继承接口和实现,但是你允许它们覆盖掉继承而来的函数实现。但有时候你却想让派生类继承一个函数的接口和实现并且不允许它们被覆盖掉

为了对这些不同的选择有一个更好的理解,考虑表示几何图形的类继承体系:

1 class Shape {
2 public:
3 virtual void draw() const = 0;
4 virtual void error(const std::string& msg);
5 int objectID() const;
6 ...
7 };
8 class Rectangle: public Shape { ... };
9 class Ellipse: public Shape { ... };

 

Shape是一个抽象类;是由纯虚函数draw所标记的。因此客户不能创建Shape类的实例,而只有它的派生类才可以。尽管如此,Shape对(public)继承自它的所有类会产生很大的影响,因为:

  • 成员函数接口总是被继承,正如Item
    32中解释的,public继承意味着“is-a”,也就是对基类来说为真的任何东西对派生类来说也必须为真。因此,如果一个函数可以被应用在一个类中,它也必须能被应用到它的派生类中。

Shape类中声明了三个函数。第一个,draw,画出当前对象;第二个,error,当error需要被记录的时候被调用。第三个,objectID,为当前对象返回一个唯一的整型标识符。每个函数以一种不同的方式被声明:draw是纯虚函数;error是简单的(不是纯的)虚函数;objectID是非虚函数。这些不同声明隐藏的含义是什么呢?

 

<?php
$a1 = [1,2,3,4,5];
$a2 = [2,4,6];

function array_minus($a1, $a2)
{
    $new_a = [];
    foreach ($a1 as $e) {
        if (!in_array($e, $a2)) {
            $new_a[] = $e;
        }
    }
    return $new_a;
}

print_r(array_minus($a1, $a2));

resolveInstanceMethod函数

原型:

+ (BOOL)resolveInstanceMethod:(SEL)name

这个函数在运行时(runtime),没有找到SEL的IML时就会执行。这个函数是给类利用class_addMethod添加函数的机会。

根据文档,如果实现了添加函数代码则返回YES,未实现返回NO。

实现的例子:

澳门新葡亰信誉平台游戏 2

//全局函数
void dynamicMethodIMP(id self, SEL _cmd)
{
    // implementation ....
}

@implementation MyTestObject
//…
//类函数
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically))
    {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSel];
}
//…
@end

澳门新葡亰信誉平台游戏 3

注意事项:

根据Demo实验,这个函数返回的BOOL值系统实现的objc_msgSend函数并没有参考,无论返回什么系统都会尝试再次用SEL找IML,如果找到函数实现则执行函数。如果找不到继续其他查找流程。

1.1 纯虚函数

考虑第一个纯虚函数draw:

1 class Shape {
2 public:
3 virtual void draw() const = 0;
4 ...
5 };

 

纯虚函数的两个最具特色的特征是:它们必须被继承它们的任何具现类重新声明;在抽象类中它们通常情况下没有定义。将这两个特征放在一起,你就会发现:

  • 声明纯虚函数的意图是让派生类只继承函数接口

这使得Shape::draw函数是非常有意义的,因为对于所有的Shape对象来说能够被画出来是一个合理的需求,但是Shape类不能为这个函数提供合理的默认实现,比如,画一个椭圆的算法和画一个矩形的算法是不一样的。Shape::draw的声明对派生具现类的设计者说,“你必须提供一个draw函数,但是我并不知道你该如何实现它。”

顺便说一下,为一个纯虚函数提供一个定义也是可能的。也就是你可以为Shape::draw提供一个实现,C++不会发出抱怨,但是调用它的唯一方式是在函数名前加上类名限定符:

 1 Shape *ps = new Shape;     // error! Shape is abstract
 2 
 3 Shape *ps1 = new Rectangle; // fine
 4 
 5  
 6 
 7 ps1->draw();                // calls Rectangle::draw
 8 
 9 Shape *ps2 = new Ellipse;      // fine
10 
11  
12 
13 ps2->draw();        // calls Ellipse::draw
14 
15 ps1->Shape::draw(); // calls Shape::draw
16 
17 ps2->Shape::draw(); // calls Shape::draw

 

除了帮助你在鸡尾酒会上给你的程序员伙伴留下深刻印象之外,这个特性通常来说效用有限。然而,你在下面会看到,它可以作为一种机制为简单的(非纯的)虚函数提供比平常更加安全的默认实现。

2、用差集和交集实现:

forwardingTargetForSelector:

原型:

- (id)forwardingTargetForSelector:(SEL)aSelector

流程到了这里,系统给了个将这个SEL转给其他对象的机会。

返回参数是一个对象,如果这个对象非nil、非self的话,系统会将运行的消息转发给这个对象执行。否则,继续查找其他流程。

实现示例:

澳门新葡亰信誉平台游戏 4

//转发目标类
@interface NoneClass : NSObject
@end

@implementation NoneClass
+(void)load
{
    NSLog(@"NoneClass _cmd: %@", NSStringFromSelector(_cmd));
}

- (void) noneClassMethod
{
    NSLog(@"_cmd: %@", NSStringFromSelector(_cmd));
}
@end

@implementation MyTestObject
//…
//将消息转出某对象
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSLog(@"MyTestObject _cmd: %@", NSStringFromSelector(_cmd));

    NoneClass *none = [[NoneClass alloc] init];
    if ([none respondsToSelector: aSelector]) {
        return none;
    }

    return [super forwardingTargetForSelector: aSelector];
}
//…
@end

澳门新葡亰信誉平台游戏 5

当执行MyTestObject对象执行[myTestObject
nonClassMethod]函数时,消息会抛到NoneClass对象中执行。

 

1.2 非纯的虚函数

 

简单虚函数背后的故事同纯虚函数有些不太一样。通常情况下来说,派生类继承函数接口,但是简单虚函数提供了可能会被派生类覆盖的实现。如果你再想想,你会意识到:

  • 声明一个简单虚函数的目的是让派生类继承一个函数接口或者一个默认实现。

考虑Shape::error的情况:

1 class Shape {
2 public:
3 virtual void error(const std::string& msg);
4 ...
5 };

 

这个接口表明在遇到错误的时候每个类必须提供一个错误函数,但是每个类对错误如何进行处理可以自由控制。如果一个类不想做任何特殊的事情,那么调用基类Shape中error的默认实现就可以了。也就是Shape::error的声明对派生类的设计者说,“你可以支持error函数,但如果你不想自己实现,你可以使用Shape类中的默认版本。”

<?php
$a1 = [1,2,3,4,5];
$a2 = [2,4,6];

function array_minus($a1, $a2)
{
    return array_diff($a1, array_intersect($a1, $a2));
}

print_r(array_minus($a1, $a2));

methodSignatureForSelector:

原型:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

这个函数和后面的forwardInvocation:是最后一个寻找IML的机会。这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation:去执行。

1.2.1 同时为简单虚函数提供函数接口和默认实现是危险的

同时为简单虚函数提供函数接口和默认实现是危险的。为什么?考虑为XYZ航空公司设计了飞机继承体系。XYZ只有两种类型的的飞机,型号A和型号B,同种飞机的飞行方式相同。因此,XYZ设计了如下的继承体系:

 1 class Airport { ... };                                                                    // represents airports
 2 
 3 class Airplane {                                                                       
 4 
 5 public:                                                                                     
 6 
 7 virtual void fly(const Airport& destination);                           
 8 
 9 ...                                                                                             
10 
11 };                                                                                             
12 
13 void Airplane::fly(const Airport& destination)                        
14 
15 {                                                                                              
16 
17 default code for flying an airplane to the given destination      
18 
19 }                                                                                              
20 
21 class ModelA: public Airplane { ... };                                         
22 
23 class ModelB: public Airplane { ... };

 

为了表示所有的飞机必须支持fly函数,还有不同型号的飞机可能需要fly的不同实现,因此Airplane::fly被声明为virtual。然而,为了防止在ModelA和ModelB中实现同一份代码,我们为Airplane::fly提供了默认实现,ModelA和ModelB可以同时继承。

 

这是典型的面向对象设计。两个类分享同一个特征(实现fly的方式),所以一般的特征都会移到基类中,然后被派生类继承。这种设计使得类的普通特性比较清晰,防止代码重复,可以促进将来的增强实现,使长期维护更加容易——这是面向对象如此受欢迎的原因,XYZ应该为此感到骄傲。

 

现在假设XYZ公司界定引入新类型的飞机,Model
C。型号C和型号A和B不一样,它的飞行方式变了。

 

XYZ的程序员为Model
C在继承体系中添加了新类,但是他们如此匆忙的添加新类,以至于忘了重新定义fly函数:

1 class ModelC: public Airplane {
2 
3  
4 
5 ...                                            // no fly function is declared
6 
7 };            

                         

 

在他们的代码中有类似下面的实现:

1 Airport PDX(...);                              // PDX is the airport near my home
2 
3 Airplane *pa = new ModelC;          
4 
5 ...                                                   
6 
7 
8 pa->fly(PDX); // calls Airplane::fly!

 

这会是一个灾难:型号C的飞机尝试用型号A或者型号B的飞行方式去飞行。这不是增加旅客信心的行为。

 

forwardInvocation:

原型:

- (void)forwardInvocation:(NSInvocation *)anInvocation

真正执行从methodSignatureForSelector:返回的NSMethodSignature。在这个函数里可以将NSInvocation多次转发到多个对象中,这也是这种方式灵活的地方。(forwardingTargetForSelector只能以Selector的形式转向一个对象)

下面这个示例代码,诠释了这种实现优势:

澳门新葡亰信誉平台游戏 6

#import <Foundation/Foundation.h>

@interface Book : NSObject
{
    NSMutableDictionary *data;
}
//声明了两个setter/getter
@property (retain) NSString *title; 
@property (retain) NSString *author;
@end

@implementation Book
@dynamic title, author; //不自动生成实现

- (id)init
{
    if ((self = [super init])) {
        data = [[NSMutableDictionary alloc] init];
        [data setObject:@"Tom Sawyer" forKey:@"title"];
        [data setObject:@"Mark Twain" forKey:@"author"];
    }
    return self;
}

- (void)dealloc
{
    [data release];
    [super dealloc];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    NSString *sel = NSStringFromSelector(selector);
    if ([sel rangeOfString:@"set"].location == 0) {
        //动态造一个 setter函数
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    } else {
        //动态造一个 getter函数
        return [NSMethodSignature signatureWithObjCTypes:"@@:"];
    }
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    //拿到函数名
    NSString *key = NSStringFromSelector([invocation selector]);
    if ([key rangeOfString:@"set"].location == 0) {
        //setter函数形如 setXXX: 拆掉 set和冒号 
        key = [[key substringWithRange:NSMakeRange(3, [key length]-4)] lowercaseString];
        NSString *obj;
        //从参数列表中找到值
        [invocation getArgument:&obj atIndex:2];
        [data setObject:obj forKey:key];
    } else {
        //getter函数就相对简单了,直接把函数名做 key就好了。
        NSString *obj = [data objectForKey:key];
        [invocation setReturnValue:&obj];
    }
}

@end

澳门新葡亰信誉平台游戏 7

1.2.2 解决方法一,将默认实现分离成单独函数

这里的问题不在于Airplane::fly有默认的行为,而在于允许 Model
C在没有明确说明它需要基类行为的情况下继承了基类的行为。幸运的是,很容易为派生类提供只有在它们需要的情况下才为其提供的默认行为。这个窍门断绝了虚函数接口和默认实现之间的联系。下面是实现的方法:

 1 class Airplane {
 2 public:
 3 virtual void fly(const Airport& destination) = 0;
 4 ...
 5 protected:
 6 void defaultFly(const Airport& destination);
 7 };
 8 void Airplane::defaultFly(const Airport& destination)
 9 {
10 default code for flying an airplane to the given destination
11 }

 

注意Airplane::fly已经转成了一个纯虚函数。它为飞行提供了接口。在Airplane类中同样展示出了默认实现,但是现在它是以独立函数的形式存在,defaultFly。像ModelA和ModelB这样的类如果想使用默认实现,只要在fly函数体内调用Inline函数defaultFly就可以了(Item30中有inline函数和虚函数之间交互的信息):

 1 class ModelA: public Airplane {
 2 public:
 3 virtual void fly(const Airport& destination)
 4 { defaultFly(destination); }
 5 ...
 6 };
 7 class ModelB: public Airplane {
 8 public:
 9 virtual void fly(const Airport& destination)
10 { defaultFly(destination); }
11 ...
12 };

 

对于ModelC类来说,偶然的继承fly的不正确实现将不再可能,因为Airplane中的纯虚函数强制ModelC提供它自己版本的fly。

1 class ModelC: public Airplane {
2 public:
3 virtual void fly(const Airport& destination);
4 ...
5 };
6 void ModelC::fly(const Airport& destination)
7 {
8 code for flying a ModelC airplane to the given destination
9 }

 

 

这个机制也不是十分安全的(程序员仍然能够复制粘贴而导致错误),但是它比原来的设计可靠多了。因为对于Airplane::defaultFly来说,它是protected的是因为它是Airplane和它的派生类中的实现细节。使用airplane的客户只关心它们能够起飞,而不管飞行是如何实现的。

Airplane::defaultFly是一个非虚函数同样重要。因为没有派生类可以重定义这个函数,这也是Item36所描述的真理。如果defaultFly是虚的,就会有一个循环问题:当派生类想重新定义defaultFly但是忘了会怎样? 

doesNotRecognizeSelector:

原型:

- (void)doesNotRecognizeSelector:(SEL)aSelector

作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。

虽然理论上可以重载这个函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。

1.2.3 解决方法二,利用纯虚函数提供默认实现

一些人反对将函数接口和默认实现分离的想法,就像上面的fly和defaultFly一样。首先,它们意识到,繁殖出十分相关的函数名字污染了类命名空间。但是它们仍然同意将函数接口和默认实现分离。它们如何处理这种看上去矛盾的事情呢?通过利用纯虚函数必须在具现派生类中重新声明这个事实,但是它们也有可能有自己的实现。下面的例子展示了Airplane继承体系是如何利用定义纯虚函数的能力的:

 1 class Airplane {
 2 public:
 3 virtual void fly(const Airport& destination) = 0;
 4 ...
 5 };
 6 
 7 void Airplane::fly(const Airport& destination) // an implementation of
 8 { // a pure virtual function
 9 default code for flying an airplane to
10 the given destination
11 }
12 class ModelA: public Airplane {
13 public:
14 virtual void fly(const Airport& destination)
15 { Airplane::fly(destination); }
16 ...
17 };
18 class ModelB: public Airplane {
19 public:
20 virtual void fly(const Airport& destination)
21 { Airplane::fly(destination); }
22 ...
23 };
24 class ModelC: public Airplane {
25 public:
26 virtual void fly(const Airport& destination);
27 ...
28 };
29 void ModelC::fly(const Airport& destination)
30 {
31 code for flying a ModelC airplane to the given destination
32 }

 

这个设计同前面的设计是基本相同的,除了纯虚函数体Airplane::fly代替了独立函数Airplane::defaultFly。从本质上来说,fly已经被分成了两个基本的组件。它的声明指定了接口(派生类必须使用它),同时它的定义指定了默认行为(派生类可能会使用,但是只有在显示的请求的时候才会使用)。将fly和defaultFly合并到一起,你就会失去为两个函数提供不同保护级别的能力:过去是protected的代码(在defaultFly中)现在变成了public的(因为它在fly中)。

 

使用场景

在一个函数找不到时,Objective-C提供了三种方式去补救:

1、调用resolveInstanceMethod给个机会让类添加这个实现这个函数

2、调用forwardingTargetForSelector让别的对象去执行这个函数

3、调用methodSignatureForSelector(函数符号制造器)和forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。

如果都不中,调用doesNotRecognizeSelector抛出异常。

1.3 非虚函数

最后,让我们看一看Shape的非虚函数,objectID:

1 class Shape {
2 public:
3 int objectID() const;
4 ...
5 };

 

 

当一个成员函数是非虚的,就不想其在派生类中有不同的行为。事实上,一个非虚成员函数指定了一种超越特化的不变性(invariant
over specialization
),无论一个派生类被如何特化,它的行为不可改变。

  • 声明一个非虚函数的意图在于让派生类继承一个函数接口,并且有一个强制的实现

你可以将Shape::objectID的声明想象成如下,“每一个Shape对象都有一个函数来产生一个对象标识符,这个对象标识符以相同的方式计算出来。计算方式由Shape::objectID的定义来决定,任何派生类都不应该尝试去修改它的定义”。因为一个非虚函数确定了一个超越特化的不变性,它永远不会在派生类中被定义,这一点将在Item36中进行讨论。 

 

文章参考

 

2. 类设计者容易犯的两种错误

对纯虚函数,简单虚函数和非虚函数进行声明的不同点在于允许你精确的指定派生类会继承什么:只继承接口,继承接口和默认实现或者接口和强制实现。因为从根本上来说这些不同的声明类型意味着不同的东西,在你声明成员函数的时候你必须在他们之间进行选择。如果你这么做了,你就应该能够避免没有经验的类设计者才会犯的两种普通错误。

补充:respondsToSelector

原型:

+ (BOOL)respondsToSelector:(SEL)aSelector

这个函数大家再熟悉不过了,用来检查对象是否实现了某函数。

此函数通常是不需要重载的,但是在动态实现了查找过程后,需要重载此函数让对外接口查找动态实现函数的时候返回YES,保证对外接口的行为统一。

示例代码(接forwardInvocation的例子):

澳门新葡亰信誉平台游戏 8

@implementation Book
//…
- (BOOL) respondsToSelector:(SEL)aSelector
{
    if (@selector(setTitle:) == aSelector ||
        @selector(title) == aSelector ||
        @selector(setAuthor:) == aSelector ||
        @selector(author) == aSelector)
    {
        return YES;
    }

    return [super respondsToSelector: aSelector];
}
 //…
@end

澳门新葡亰信誉平台游戏 9

 

 2.1 错误一,将所有函数声明为非虚

第一种错误是将所有函数声明成非虚。这没有给派生类的特化留下任何余地;特别对于非虚析构函数来说是有问题的(Item
7)。当然,我们有足够的理由设计一个不被当作基类的类,在这种情况下,只声明非虚函数是合适的。然而通常情况下,这些类在下面两种情况下被创建出来:要么是忽略了虚函数和非虚函数的区别,要么就是过度担心虚函数所花费的开销。事实是基本上任何被用作基类的类都会使用虚函数。(Item
7)

 

如果你关心虚函数的开销,允许我拿出80-20法则(Item
30也提到了),它表明了在一个典型的程序中,20%的代码会花费80%的运行时间。这个法则很重要,因为它意味着,平均来说,你的程序中的80%的函数调用可以是虚函数调用,但对你的程序的性能影响却是很轻微的。在你对能否负担的起虚函数的开销进行担心之前,确保你所关注的代码是对程序有重大影响的20%的那一部分。

 

 2.1 错误二,将所有函数声明为虚函数

另外一个普通的问题是将所有成员函数声明成虚函数。有时候这么做是对的——Item
31中的接口类就是这么做的。然而,这也是一个类设计者缺乏坚定立场的标志。一些函数不应该在派生类中被重定义,当碰到这种情况,你就应该把这个函数定义为非虚。不是说只要花费一点时间对函数进行重定义,就能使使类满足所有人的需求。如果你需要特化上的不变性,不要害怕说不!

3. 总结

  • 接口继承不同于实现继承。在public继承下,派生类总是会继承基类接口。
  • 纯虚函数只是指定了接口继承。
  • 简单虚函数指定了接口继承外加一个默认实现。
  • 非虚函数指定了一个接口继承外加一个强制实现。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图