Objective-C父子指针及其关系

Objective-C父子指针及其关系,objective-c,Objective C,我有一个家长班 @interface Parent : NSObject @end @interface Child : Parent @end 还有一个儿童班 @interface Parent : NSObject @end @interface Child : Parent @end 通常,我们可以像这样将子对象存储在父指针中 Parent *p = [Child alloc]init]; 当我像这样将父对象存储在子指针中时,我很惊讶 Child *c = [[Parent al

我有一个家长班

@interface Parent : NSObject
@end
@interface Child : Parent
@end
还有一个儿童班

@interface Parent : NSObject
@end
@interface Child : Parent
@end
通常,我们可以像这样将子对象存储在父指针中

Parent *p = [Child alloc]init];
当我像这样将父对象存储在子指针中时,我很惊讶

Child *c = [[Parent alloc] init];

虽然编译器给出了“语义错误不兼容指针”的警告,但当我运行它时,它的工作方式和第一种情况类似。我无法理解为什么运行时允许它工作?

我假设运行时允许它工作,就像您可以将NSMutableArray分配给NSArray一样。您正在将父类的实例分配给子类,这是不必要的,因为子类已经继承父类

原因很简单。子对象保证实现父对象实现的所有方法,因为子对象是父对象的子类。因此,对Child类型的对象调用特定的父方法不会导致异常。但是,父对象不一定实现子对象实现的所有方法。因此,稍后的方法调用(消息发送到)
c
可能会导致异常。编译器不知道
c
不是子对象,因为这是它声明的类型。本质上,这是编译器有机会警告您所做的可能是错误的地方

运行时完全是另一回事。在运行时,对象指针是相同的。因此,没有理由不能在源代码中声明为Child*类型的指针中存储(对父对象的引用)。出于上述原因,你通常不应该这样做。只要只调用父级定义的方法,就不会引起问题,但如果尝试调用特定于子级的方法,就会出现运行时异常


简而言之,让编译器通过尽可能严格地键入变量来帮助您捕获错误。如果只想使用父对象定义的功能,可以将子对象存储在变量类型的父对象中。如果确实需要通用/非类型化对象变量,可以使用
id
。在这种情况下,如果您不确定所讨论的对象是否实现了给定的方法,则可以使用
if([object resondsToSelector:@selector(theMethodName:)])
括起方法调用。

之所以有效,是因为
init
方法声明为返回
id
,而不是
父*
子*
id
类型可以隐式转换为任何对象指针类型


您会收到警告,因为编译器知道
alloc
/
init
序列通常返回指向接收
alloc
消息的类型的指针。

它“工作”是因为Objective-C实际处理传递给对象的消息的方式。从技术上讲,您可以将所有指针视为NSObject,并且您的程序将以完全相同的方式运行,因为最终像[myObject someMessage]这样的调用被编译为myObject指向的对象的选择器查找(不管它是什么!),然后调用该选择器(如果它由真实对象处理的话)。在objc.h(IIRC)中有一组C函数可以自己完成这项工作-编译器基本上是使用[]语法将消息传递转换为这些C函数调用

您没有说为什么会对这种行为感到惊讶,但有一种可能是静态和动态方法查找和键入之间的差异

首先要注意的是,在继承中,无论你在哪里有一个
父引用,你实际上可能有一个
子引用,后者可以做任何事情,前者可以,也可以代替后者

现在,在许多面向对象语言中,例如C++,变量类型和方法查找是基于静态(即声明)类型的。因此,在C++中,变量<>代码> c>代码>被假定为子< /C>的一个实例,当调用了一个方法,在<代码> c>代码>时,C++编译器就决定了基于这个方法调用什么方法。因此,分配一个

父级
的实例是不安全的——这样的实例没有
子级
的方法,肯定会导致灾难。你可以用C++来安全地分配任务:
Parent *p;
Child *c;
...

c = (Child *)p;
cast将在运行时检查
p
引用的对象是否为
子对象(或从
子对象继承的任何类),如果不是,则生成错误。由于这是在运行时完成的,它通常被包装在一个条件中,该条件首先检查
p
是否是
子项

然而,在Obj-C中,方法查找是根据被引用对象的实际类型而不是引用它的变量类型在运行时动态完成的。因此,如果您的<代码> C>代码>变量包含对<代码>父< /代码>的引用,并且您试图调用<代码>子< /C>方法,您将得到运行时错误(这将是一个干净的错误,在C++的情况下,您的代码可能只是故障和/或爆破)。 Obj-C的这种动态特性使得它很容易错过许多编程错误,只有在代码运行时,这些错误才会显现出来——而且在代码交付给客户之后,很难对应用程序进行完全测试。因此,Obj-C编译器尽可能多地进行类型检查,以帮助最大限度地减少运行时出现的错误数量。然而,同样由于其动态特性,它有时只会发出警告,而不会报告错误,并拒绝编译—就像在您的示例中所做的那样

< Obj-C中的解决方案与上面的C++相同,使用一个强制状态声明对象必须是一个特定类型,并用条件:
Parent *p;
Child *c;
...

if([p isKindOfClass:[Child class]) // we need a Child
{
   c = (Child *)p;
  ...
}
else
{  // handle p not being a Child
   ...
}
上面故意没有讨论动态与静态的相对优缺点,读一本好书会更好。