ObjC внутренности. Почему моя попытка утки не удалась?
Я пытался использовать id
создать утку, набрав в цель-c. Концепция выглядит хорошо в теории, но провалилась на практике. Я не смог использовать какие-либо параметры в моих методах. Методы были вызваны, но параметры были неправильными. Я получал BAD_ACESS для объектов и случайные значения для примитивов. Я приложил простой пример ниже.
Вопрос: кто-нибудь знает, почему параметры методов неверны? Что происходит под капотом объектива-с?
Примечание: меня интересуют детали. Я знаю, как сделать приведенный ниже пример.
Пример: я создал простой класс Test
который передается другому классу с помощью свойства id test
,
@implementation Test
- (void) aSampleMethodWithFloat:(float) f andInt: (int) i {
NSLog(@"Parameters: %f, %i\n", f, i);
}
@end
Затем в классе выполняется следующий цикл:
for (int i=0; i < 10; ++i) {
float f=i*0.1f;
[tst aSampleMethodWithFloat:f andInt:i]; // warning no method found.
}
Вот вывод, который я получаю. Как видите, метод был вызван, но параметры были неверными.
Parameters: 0.000000, 0
Parameters: -0.000000, 1069128089
Parameters: -0.000000, 1070176665
Parameters: 2.000000, 1070805811
Parameters: -0.000000, 1071225241
Parameters: 0.000000, 1071644672
Parameters: 2.000000, 1071854387
Parameters: 36893488147419103232.000000, 1072064102
Parameters: -0.000000, 1072273817
Parameters: -36893488147419103232.000000, 1072483532
Обновить:
Я случайно обнаружил, что когда я добавляю декларацию aSampleMethodWith...
в класс с for
В цикле предупреждение исчезает, и метод класса Test вызывается правильно.
Обновление 2: Как указывает JeremyP, непосредственная причина проблемы заключается в том, что числа с плавающей точкой рассматриваются как двойные. Но кто-нибудь знает почему? (следуя принципу 5).
Согласно @eman, вызов переводится в простой вызов функции C и директиву компилятора, чтобы получить SEL
, Таким образом, @selector запутывается. Но почему? Компилятор имеет всю необходимую информацию о типе в первом вызове метода. Кто-нибудь знает хороший источник информации о внутренностях Objective C, я искал Язык программирования Objective C, но я не нашел ответ.
5 ответов
По умолчанию значения с плавающей запятой передаются как двойные, а не как числа с плавающей запятой. Компилятор не знает, в точке, где [tst aSampleMethodWithFloat:f andInt:i];
Случается, что он должен только передать число с плавающей запятой, поэтому он увеличивает f в два раза. Это означает, что в методе, когда компилятор знает, что он имеет дело с плавающей точкой, f - это число с плавающей точкой, образованное первыми четырьмя байтами двойного числа, переданного методу, а i - целое число, образованное из вторых четырех байтов дважды прошло.
Вы можете исправить это либо
- изменив первый параметр aSampleMethodWithFloat: andInt: на двойной
- импортировать объявление интерфейса Test в файл, где вы его используете.
Обратите внимание, что при использовании чисел с плавающей запятой нет никакого выигрыша, за исключением небольшого количества места. Вы можете также использовать везде двойные числа.
Я думаю, что JeremyP прав насчет проблемы, связанной с удвоением против поплавков. Что касается деталей реализации, то при отправке сообщений в Objective-C используется objc_msgSend(id theReceiver, SEL theSelector, ..)
Функция С (для некоторых глубоких подробностей см. Здесь). Вы можете смоделировать те же результаты отправки метода следующим образом:
SEL theSelector = @selector(aSampleMethodWithFloat:andInt:);
objc_msgSend(self.test, theSelector, 1.5f, 5);
SEL
это просто число, которое соответствует функции (которая определяется динамически на основе сигнатуры метода). objc_msgSend
затем ищет фактический указатель на функцию (типа IMP) метода и вызывает его. поскольку objc_msgSend
имеет переменное количество аргументов, он просто будет использовать столько, сколько вы передадите. Если вы должны были сделать:
objc_msgSend(self.test, theSelector, 1.5f);
Было бы правильно использовать 1.5f и иметь мусор для другой переменной. Поскольку сигнатура метода обычно обозначает количество аргументов, это трудно сделать при обычном использовании.
Без подписи, доступной в вызывающей точке, неизвестно, какой тип должны иметь параметры. Предполагается, что неопределенные методы принимают ...
в качестве параметров, а это не то, что делает ваш. Если в этот момент компилятор видит какой-либо интерфейс, в котором существует рассматриваемый метод, будет использовано это определение.
Вы можете убрать предупреждение, создав такую категорию:
@interface NSObject (MyTestCategory)
- (void) aSampleMethodWithFloat:(float) f andInt: (int) i;
@end
Проблема здесь заключается в разделительной линии между C и Objective-C. Тип id определяет любой объект, но int и float не являются объектами. Компилятор должен знать тип C всех аргументов и тип возврата любого метода, который вы вызываете. Без объявления предполагается, что метод возвращает идентификатор и принимает произвольное количество аргументов идентификатора. Но id несовместим с int и float, поэтому значение не передается правильно. Вот почему он работает правильно, когда вы предоставляете объявление - тогда он знает, что ваш int - это int, а ваш float - это float.