我有一个家长班
@interface Parent : NSObject
@end
和一个儿童班
@interface Child : Parent
@end
通常,我们可以将子对象存储在像这样的父指针中
Parent *p = [Child alloc]init];
我很惊讶地知道,当我将父对象存储在像这样的子指针时
Child *c = [[Parent alloc] init];
虽然编译器发出警告“语义错误不兼容的指针”,但是当我运行它时就像第一种情况一样。我无法理解为什么运行时允许它工作?
答案 0 :(得分:4)
您没有说明为什么您对此行为感到惊讶,但一种可能性是静态和动态方法查找和输入之间的区别。
首先请注意,使用继承,无论您有Parent
引用,您实际上都可以拥有Child
- 后者可以执行前者可以替代的所有操作之一。
现在在许多面向对象的语言中,例如C ++,变量类型和方法查找基于静态(即声明的)类型。因此,例如在C ++中,您的变量c
被假定为Child
的实例,并且当在c
上调用方法时,C ++编译器基于此单独确定要调用的方法。因此,指定Parent
的实例不安全 - 此类实例没有Child
的方法,肯定会导致灾难。您可以使用强制转换器在C ++中使分配安全:
Parent *p;
Child *c;
...
c = (Child *)p;
演员表将在运行时检查p
引用的对象是Child
(或任何继承自Child
等的类),如果没有则产生错误。由于这是在运行时完成的,因此它通常包含在条件中,首先检查p
是否为Child
。
然而,在Obj-C方法中,查找是在运行时基于引用对象的实际类型而不是引用它的变量类型而动态动态。因此,如果您的c
变量包含对Parent
的引用,并且您尝试调用Child
方法,则会出现运行时错误(这将是一个干净的错误,在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
...
}
以上故意不讨论动态与静态的相对优缺点,你可以从一本好书中更好地阅读。
答案 1 :(得分:2)
原因很简单。保证子对象实现Parent实现的所有方法,因为Child是Parent的子类。因此,不必担心在类型为Child的对象上调用特定的Parent方法会导致异常。但是,Parent对象不一定实现Child实现的所有方法。因此,稍后的方法调用(消息发送到)c
可能会导致异常。编译器不知道c
不是Child对象,因为它是声明的类型。从本质上讲,这是编译器有一次机会警告你所做的事情可能是错误的。
运行时完全是另一回事。在运行时,对象指针是相同的。因此,没有理由不能在源代码中声明的指针中存储(引用)父对象作为类型Child *。由于上述原因,您通常不应该这样做。只要您只调用Parent定义的方法,它就不会导致问题,但如果您尝试调用Child特定的方法,您将获得运行时异常。
简而言之,让编译器尽可能严格地键入变量来帮助您捕获错误。如果您只打算使用Parent定义的功能,可以将Child对象存储在变量类型的Parent中。如果您确实需要通用/无类型对象变量,则可以使用id
。在这种情况下,如果您不确定相关对象是否实现了给定方法,则可以使用if ([object resondsToSelector:@selector(theMethodName:)])
括起方法调用。
答案 2 :(得分:2)
它有效,因为声明init
方法返回id
,而不是Parent*
或Child*
。 id
类型可以隐式转换为任何对象指针类型。
您收到警告,因为编译器知道alloc
/ init
序列通常会返回指向收到alloc
消息的类型的指针。
答案 3 :(得分:1)
我认为运行时允许它工作,就像你可以将NSMutableArray分配给NSArray一样。您正在为子类分配超类Parent的实例,这是不必要的,因为Child已经继承了父类。
答案 4 :(得分:1)
它“有效”,因为Objective-C实际处理传递给对象的消息的方式。从技术上讲,你可以将你的所有指针视为NSObject,你的程序将以完全相同的方式运行,因为最后像myObject someMessage这样的调用被编译到myObject指向的对象上的选择器查找(无论它是什么! ),然后调用该选择器(如果它由真实对象处理)的实现。 objc.h(IIRC)中有一组C函数可以用来自己完成这项工作 - 编译器基本上是使用[]语法将消息传递转换为这些C函数调用。