Переменная-член класса какао, расположенная внутри вызова функции nil, если не инициирована init / load - PullRequest
0 голосов
/ 31 января 2019

Я родом из C / C ++ и сейчас немного изучаю Cocoa и Objective-C.

У меня странное поведение, связанное с ленивой инициализацией (если я не ошибаюсь), и я чувствую, что я 'Мне не хватает чего-то очень простого.

Настройка:

  • Xcode 10.1 (10B61)
  • macOS High Sierra 10.13.6
  • началось с нуляКакао-проект
  • использует раскадровку
  • добавить файлы TestMainView.m / .h
  • под контроллером представления в main.storyboard, установите пользовательский класс NSView как TestMainView
  • протестировано в сборках отладки и выпуска

По сути, я создаю NSTextView внутри контроллера представления, чтобы иметь возможность писать некоторый текст.В TestMainView.m я создаю цепочку объектов программно, как описано здесь

Существует два пути:

  • Первый из них включается установкой USE_FUNCTION_CALLв 0, весь код запускается внутри awakeFromNib().
  • . Второй путь включается путем установки USE_FUNCTION_CALL в 1. Это делает текстовый контейнер и текстовое представление выделенными из вызова функции addNewPage ()и возвращает контейнер текста для дальнейшего использования.

Первый путь к коду работает так же, как и ожидалось: я могу написать некоторый текст.

Однако второй путь к коду просто не работает, потому что при возврате, textContainer.textView - это ноль (textContainer само значение вполне в порядке).

Что еще более тревожно (и именно здесь я подозреваю, что виноват ленивый init), так это если я "принудительно" textContainer.textView значение в то время как внутри вызова функции, то все работает просто отлично.Вы можете попробовать это, установив FORCE_VALUE_LOAD в 1.

Это не обязательно должен быть if(), он также работает с NSLog().Это даже работает, если вы устанавливаете точку останова в строке возврата и используете отладчик для печати значения ("p textContainer.textView")

Так что мои вопросы:

  • этосвязано с ленивой инициализацией?
  • это ошибка?Есть ли обходной путь?
  • я думаю о программировании Какао / ObjC неправильно?

Я действительно надеюсь, что что-то здесь упущено, потому что я не могу ожидать случайной проверки переменных здесьи там в классах Какао, надеясь, что они не повернутся nil.Он даже не работает тихо (без сообщения об ошибке, ничего).

TestMainView.m

#import "TestMainView.h"

#define USE_FUNCTION_CALL 1
#define FORCE_VALUE_LOAD 0

@implementation TestMainView

NSTextStorage* m_mainStorage;

- (void)awakeFromNib
{
    [super awakeFromNib];

    m_mainStorage = [NSTextStorage new];
    NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];
#if USE_FUNCTION_CALL == 1
    NSTextContainer* textContainer = [self addNewPage:self.bounds];
#else
    NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:NSMakeSize(FLT_MAX, FLT_MAX)];

    NSTextView* textView = [[NSTextView alloc] initWithFrame:self.bounds textContainer:textContainer];
#endif
    [layoutManager addTextContainer:textContainer];
    [m_mainStorage addLayoutManager:layoutManager];

    // textContainer.textView is nil unless forced inside function call
    [self addSubview:textContainer.textView];
}

#if USE_FUNCTION_CALL == 1
- (NSTextContainer*)addNewPage:(NSRect)containerFrame
{
    NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:NSMakeSize(FLT_MAX, FLT_MAX)];

    NSTextView* textView = [[NSTextView alloc] initWithFrame:containerFrame textContainer:textContainer];
    [textView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];

#if FORCE_VALUE_LOAD == 1
    // Lazy init ? textContainer.textView is nil unless we force it
    if (textContainer.textView)
    {

    }
#endif
    return textContainer;
}
#endif

- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];

    // Drawing code here.
}

@end

TestMainView.h

#import <Cocoa/Cocoa.h>

NS_ASSUME_NONNULL_BEGIN

@interface TestMainView : NSView

@end

NS_ASSUME_NONNULL_END

Ответы [ 2 ]

0 голосов
/ 31 января 2019

ответ cekisakurek правильный.Объекты освобождаются, если на них нет собственной (/ "сильной") ссылки.Ни текстовый контейнер, ни текстовое представление не имеют ссылок на друг друга.Контейнер имеет слабую ссылку на представление, что означает, что он автоматически устанавливается на nil, когда представление умирает.(Представление имеет ненулевую ссылку на контейнер, что означает, что у вас будет висячий указатель в textView.textContainer, если контейнер освобожден, пока представление еще живо.)

Текстовый контейнер остается живымпотому что он возвращается из метода и присваивается переменной, которая создает ссылку на владельца, пока эта переменная находится в области видимости.Единственная ссылка на представление была внутри метода addNewPage:, поэтому он не переживает эту область.

«Принудительная загрузка» не имеет ничего общего с отложенной инициализацией;Как заметил Ббум, это «работает», скорее всего, случайно.Я сильно подозреваю, что этого не произойдет в оптимизированной сборке.

Позвольте мне заверить вас, что вы , а не нуждаетесь в том, чтобы ковыряться в свойствах, так или иначе, в программировании Какао.Но вам нужно учитывать отношения собственности между вашими объектами.В этом случае что-то еще должно владеть как контейнером, так и представлением.Это может быть ваш класс здесь, через ivar / свойство, или другой объект, который подходит для API NSText {Wh независимо} (который мне не знаком).

0 голосов
/ 31 января 2019

Я не очень знаком с какао, но думаю, что проблема в ARC (автоматический подсчет ссылок).

NSTextView* textView = [[NSTextView alloc] initWithFrame:containerFrame textContainer:textContainer];

В файле .h NSTextContainer вы можете видеть, что NSTextView является слабым ссылочным типом.

enter image description here

Итак, после возвратаиз функции он освобождается

Но если вы сделаете textView переменной экземпляра TestMainView, он будет работать как положено.Не совсем уверен, почему это также работает, если вы заставляете это все же.~~ (Может быть, оптимизация компилятора?) ~~

Кажется принудительным, то есть вызов

if (textContainer.textView) {

вызывает вызовы retain / autorelease, поэтому до следующего вызова стока autorelease текстовое представление все еще живо. (Я предполагаю, что это не истощается, пока функция awakeFromNib не вернется).Причина, по которой это работает, заключается в том, что вы добавляете textView в иерархию представлений (сильная ссылка) до того, как пул авто-выпусков освобождает его.

...