【问题标题】:Objective-C class returned by alloc being confused with wrong class when initializing初始化时 alloc 返回的 Objective-C 类与错误的类混淆
【发布时间】:2011-04-23 17:27:22
【问题描述】:

我以为我了解基本的 Objective-C 就 alloc 和 init... 方法而言,但显然我不了解。我将遇到的问题归结为下面的最小示例。 (例如,我将所有源文件放入一个文件中,但如果将源文件拆分为多个源文件和头文件,问题也会像往常一样)。

以下是代码的作用以及运行时发生的情况的概要。

我定义了两个类 MyInteger 和 MyFloat,它们几乎相同,只是一个处理 int 类型,另一个处理 float。两者都具有称为 initWithValue: 的初始化方法:但是具有不同类型的参数。有一个#define 来控制是否定义了 MyInteger 类,原因是,即使从未使用过该类,它也会导致程序的不同行为。

main() 只使用 MyFloat。前两行执行此操作:分配 MyFloat 的一个实例,并将其初始化为 50.0。然后打印该值。根据是否定义了 MyInteger,我会得到两个不同的输出。

正如我所料,没有定义 MyInteger:


[4138:903] float parameter value:50.000000
[4138:903] Value:50.000000
(next two output lines omitted)

定义了 MyInteger,令我惊讶的是:


[4192:903] float parameter value:0.000000
[4192:903] Value:0.000000
(next two output lines omitted)

在我看来,编译器将调用 initWithValue: 视为属于 MyInteger 类。接下来的两行 main() 通过将 [MyFloat alloc] 转换为 MyFloat* 来测试这一点。即使定义了 MyInteger,它也会按预期生成输出:


[4296:903] float parameter value:0.000000
[4296:903] Value:0.000000
[4296:903] float parameter value:50.000000
[4296:903] Value with cast:50.000000

请解释发生了什么!我已经为此苦苦挣扎了超过 24 小时,甚至到了打开门让一些热量散发出来让我的电脑冷却的地步:-) 谢谢!

另一个奇怪的地方是,如果我将 MyInteger 的定义移到 MyFloat 的定义之下,那么一切都是“好的”——正如我所期望的那样工作。历史证明我经常犯错,以至于我怀疑编译器是罪魁祸首。无论如何,这里是编译器和项目信息:使用 Xcode 4.0.2。尝试了所有 3 个编译器选项(GCC 4.2、LLVM GCC 4.2 和 LLVM Compiler 2.0)。此示例的 Xcode 项目是使用基于 Foundation 的 Mac OS X 命令行工具的标准配置设置的。


#define DO_DEFINE_MYINTEGER 1

//------------- define MyInteger --------------
#if DO_DEFINE_MYINTEGER
@interface MyInteger : NSObject {
    int _value;
}
-(id)initWithValue:(int)value;
@end

@implementation MyInteger
-(id)initWithValue:(int)value {
    self= [super init];
    if (self) {
        _value= value;
    }
    return self;
}
@end
#endif


//------------- define MyFloat --------------
@interface MyFloat : NSObject {
    float _value;
}
-(id)initWithValue:(float)value;
-(float)theValue;
@end

@implementation MyFloat
-(id)initWithValue:(float)value {
    self= [super init];
    if (self) {
        NSLog(@"float parameter value:%f",value);
        _value= value;
    }
    return self;
}
-(float)theValue {
    return _value;
}
@end

//--------------- main ------------------------
int main (int argc, const char * argv[])
{
    MyFloat *mf1= [[MyFloat alloc] initWithValue:50.0f];
    NSLog(@"Value:%f",[mf1 theValue]);

    MyFloat *mf2= [((MyFloat*)[MyFloat alloc]) initWithValue:50.0f];
    NSLog(@"Value with cast:%f",[mf2 theValue]);

    return 0;
}

【问题讨论】:

  • 我已将 Sherm Pendley 的答案标记为正确答案,因为它解释了发生这种情况的机制。 NSResponder 的答案也是一个不错的答案,因为它教授了一种签名命名实践,有助于避免该问题。谢谢两位!

标签: objective-c xcode4


【解决方案1】:

+alloc 的原型是返回id,当编译器面临多个-initWithValue: 方法的选择时,它会生成代码来调用它找到的第一个方法。当 MyInteger 被定义时,这意味着编译器将生成代码来转换 50.0 并将其作为整数参数传递。请注意,整数和浮点参数的传递方式不同,前者在堆栈中,后者在浮点寄存器中。

在运行时,因为消息分发是动态处理的,所以会调用正确的方法 - 但该方法假定 value 参数是在浮点寄存器中传递的。但这不是调用代码放置它的地方,因此被调用的方法在读取该寄存器时会得到不正确的结果。

类型转换之所以有效,是因为它明确地告诉编译器将在运行时调用哪些方法,这允许它生成正确的调用代码,将value 传递到浮点寄存器而不是堆栈中。

编辑:话虽如此,NSResponder 的回答也提出了一些非常好的观点。在 Objective-C 中,声明具有相同名称但具有不同签名(即参数和返回类型)的方法是一个非常糟糕的主意,并且名为 -initWithValue: 的方法意味着它的参数是一个 NSValue 对象。

【讨论】:

  • 谢谢!这听起来就像正在发生的事情。 Apple 的 +alloc 文档说“新实例的 isa 实例变量被初始化为描述类的数据结构”,我认为这意味着编译器知道哪个类已被实例化,并且能够生成正确的代码一。但我现在意识到它在编译时没有帮助。 Xcode 的自动补全功能足够智能,只建议正确的方法(使用 float 类型)——这让我更加困惑。
  • 您引用的文档描述了运行时发生的情况 - 在编译时,编译器所知道的只是它正在向一个 ID 发送 -initWithValue: 消息,该 ID 可以是任何类。
【解决方案2】:

在方法名称相同但类型不相同的情况下,拥有两个方法签名是个坏主意,尤其是当它们在同一个文件中编译时。编译器在这里对消息表达式应该传递 int 还是 float 感到困惑。

如果您查看NSNumber,您会发现有单独的-initWithFloat:-initWithDouble:-initWithInt: 方法。

此外,如果您有一个名称中包含“值”一词的方法,大多数 Cocoa 开发人员会假定它需要一个 NSValue 参数。

【讨论】:

  • 谢谢。我编辑了问题以澄清当源被拆分为不同的 .m 和 .h 文件时问题仍然存在。当然, MyInteger.h 和 MyFloat.h 都将包含在进行调用的文件中,因此编译器仍然可能会像您指出的那样感到困惑。我会听取您的建议并更改签名以反映值类型。尽管如此,我发现我无法预测行为并且行为会根据 MyInteger.h 和 MyFloat.h 导入的顺序而变化,这很麻烦。这真的是它应该的样子吗?
  • 但是他们在不同的班级,它应该知道你在哪个班级发送消息?
猜你喜欢
  • 1970-01-01
  • 2015-05-17
  • 2020-12-14
  • 1970-01-01
  • 2015-08-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多