Objective-C - Переменные метода iVar Scoped?

Раньше я возился с Objective-C и столкнулся с довольно распространенной ситуацией:

У меня был класс, который не был синглтоном, которому требовалась переменная, разделяемая между вызовами методов, например static, но каждому экземпляру требовалась собственная переменная. Однако эту переменную нужно было использовать только в одном конкретном методе, мы назовем его -foo.

Что бы я хотел сделать, так это макрос, назовем его ivar, который позволяет мне делать следующее:

@implementation MyClass 

-(foo)
{
    ivar int someVal = 10; // default value, ivar scoped variable.
}

-(bar)
{
    someVal = 5; // error, outside of `foo`'s scope.
}

@end

Для меня не имеет значения, как определена переменная (будь то макрос типа OBJC_IVAR(Type, Name, Default) или ivar someType someName = value), если он соответствует следующим требованиям:

  • Имеет потокобезопасность
  • Может иметь переменную с тем же именем (но другим значением) в другом методе
  • Без типа (не имеет значения, какого типа переменная)
  • Поддержка значения по умолчанию
  • Переменная может быть объявлена ​​в одной строке (мне не нужно писать 15 строк кода только для того, чтобы поместить переменную в мой код)

В настоящее время я сам работаю над реализацией Objective-C++, мне просто интересно, есть ли у кого-нибудь еще какие-либо мысли (или существующие инструменты) о том, как это сделать.

Очевидно, это не обязательно делать с настоящим iVar. Скорее всего, это следует делать со связанными объектами во время выполнения, что также управляет освобождением за нас.


person Richard J. Ross III    schedule 17.08.2012    source источник
comment
Я озадачен - почему это не может быть обычный ivar, который вы используете только в одном методе? А затем использовать проверку кода, чтобы убедиться, что он используется только по назначению? Если вы считаете это необходимым, вы можете назвать его по методу -- например, foo_someVal.   -  person Hot Licks    schedule 17.08.2012
comment
@HotLicks, потому что это загромождает объявление iVar объекта. Кроме того, добавление iVar для метода, который может вызываться или не вызываться, немного излишне, не так ли? Это также полезно для категорий, где вы НЕ МОЖЕТЕ добавить iVar к классу.   -  person Richard J. Ross III    schedule 17.08.2012
comment
Есть много вещей, которые загромождают вещи. В Objective-C больше беспорядка, чем в коде. И почему добавление ivar для метода может быть более излишним, чем добавление метода?   -  person Hot Licks    schedule 17.08.2012
comment
@RichardJ.RossIII - почему это, скорее всего, нужно делать со связанными объектами? Вы пишете свой собственный компилятор! В рамках компиляции, почему бы просто не преобразовать ivar в переменную экземпляра и избежать любых коллизий с искажением имени. Если вы сделаете это, вы соединитесь с существующей поддержкой переменных экземпляра, а также существующие отладчики, по крайней мере, найдут ее, даже если они показывают искаженное имя. С небольшой предварительной обработкой (скажем, измените расширение файла и заставьте Xcode использовать скрипт для предварительной обработки до стандартного Obj-C) вы можете использовать этот подход, даже не создавая свой собственный (полный) компилятор.   -  person CRD    schedule 17.08.2012
comment
@CRD Моя цель здесь не написать собственный препроцессор, а использовать существующий. Гораздо полезнее иметь код, который любой может просто добавить в программу с заголовочным файлом и несколькими макросами, чем пытаться создать свой собственный компилятор.   -  person Richard J. Ross III    schedule 17.08.2012
comment
@RichardJ.RossIII - я неправильно понял вас. Я сейчас сам работаю над реализацией Objective-C++, я думал, вы пишете компилятор. Но даже если вы не сбрасываете со счетов скриптовый подход — легко предварительно обработать файл, скажем, с помощью ruby-скрипта, и другие могут добавить его в свои собственные проекты. Другой вопрос, насколько легко будет написать сценарий... Получайте удовольствие!   -  person CRD    schedule 17.08.2012
comment
Вам никогда не требовалось 15 строк кода, чтобы добавить переменную в ваш класс. Фактически, вам никогда не требовалось больше одного. Когда свойства были введены, вам требовалось целых три строки для объявления свойства, но теперь с Xcode 4.4 это можно сделать и одной строкой.   -  person Caleb    schedule 17.08.2012
comment
@Caleb Я больше не описываю ТОЛЬКО iVar, а особый вид искаженного имени iVar, который поддерживает область видимости. Это невозможно сделать с текущим состоянием переменных Objective-C, так как я также должен сделать его совместимым с подклассами.   -  person Richard J. Ross III    schedule 17.08.2012


Ответы (2)


Потратив много времени, я считаю, что у меня есть полностью работающее решение на Objective-C++. Некоторые особенности:

  • Переменные уникальны. Пока они имеют разную область действия, их значения независимы.
  • Каждый экземпляр имеет свои значения
  • Потокобезопасность (обеспечивается связанными объектами)
  • Простое объявление переменной:

    • Macro overloading: only specify the information that you need
    • Возможные способы определения OBJC_IVAR:

      OBJC_IVAR(); // creates a warning, does nothing
      OBJC_IVAR(Name); // creates an ivar named 'Name' of type 'id'
      OBJC_IVAR(Type, Name); // creates an ivar named 'Name' of type 'Type'
      OBJC_IVAR(Type, Name, Default); // creates an ivar named 'Name', of type 'Type', and a default value of 'Default' (which is only executed once);
      
  • Полная поддержка типов с шаблонами C++ (поддерживаются __weak, __strong, __autoreleasing, volatile и т. д.)

  • Подклассы не разделяют переменные со своими суперклассами (поэтому нет никаких шансов на конфликты, переменные действительно ограничены своей областью).
  • Может использоваться в синглтонах без проблем
  • Работает быстро, для поиска переменной требуется ~ 15-30 циклов ЦП, а после ее поиска требуется столько же времени, сколько для установки любой другой переменной.
  • Большую часть тяжелой работы выполняет препроцессор, что позволяет быстрее писать код.
  • Просто перетащите в существующий проект Xcode, не полагаясь на пользовательский процессор

Небольшие минусы реализации:

  • Объекты должны иметь спецификатор владения (ограничение для ссылок C++: Reference to non-const type 'id' with no explicit ownership). Легко исправить, добавив __strong, __weak или __autoreleasing к типу переменной

  • Реализация плохо читается. Поскольку он так сильно зависит от шаблонов C++ и Objective-C, работающих вместе в гармонии, трудно просто изменить «одну вещь» и надеяться, что она заработает. Я добавил обширные комментарии к реализации, так что, надеюсь, это освободит часть бремени.

  • Метод swizzling может сильно запутать это. Не самая большая проблема, но если вы начнете экспериментировать с перебором методов, не удивляйтесь, если вы получите неожиданные результаты.

  • Не может использоваться внутри объекта C++. К сожалению, C++ не поддерживает атрибуты времени выполнения, как это делает Objective-C, поэтому мы не можем рассчитывать на очистку наших переменных в конце концов. По этой причине вы не можете использовать OBJC_IVAR внутри объекта C++. Однако мне было бы интересно увидеть реализацию этого.

  • #line может сильно все испортить, так что не используйте его.

История версий

  • 1.0: Первоначальный выпуск
  • 1.1: OBJC_IVAR_NAME обновлено, чтобы полагаться только на препроцессор. В результате мы не можем использовать __func__.

Итак, без лишних слов, вот код:

OBJC_IVAR.hpp

//
//  OBJC_IVAR.h
//  TestProj
//
//  Created by Richard Ross on 8/17/12.
//  Copyright (c) 2012 Ultimate Computer Services, Inc. All rights reserved.
//
#ifndef OBJC_IVAR_HPP
#define OBJC_IVAR_HPP

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

#import "NSValue+CppObject.h"

// Argument counting algorithm. Not too complex
#define __NARG(_1, _2, _3, _4, _5, VAL, ...) VAL
#define NARG(...) __NARG(__VA_ARGS__, 5, 4, 3, 2, 1, 0)

// Different implementations based on number of parameters passed in
#define __OBJC_IVAR(N, ...) _OBJC_IVAR_ ## N (__VA_ARGS__)
#define _OBJC_IVAR(N, ...) __OBJC_IVAR(N, __VA_ARGS__)

// Usage: OBJC_IVAR(Type (optional), Name (required), Default (optional))
#define OBJC_IVAR(...) _OBJC_IVAR(NARG(__VA_ARGS__), __VA_ARGS__)

// create a unique name. we use '__COUNTER__' here to support scoping on the same line, for compressed source code
#define __OBJC_IVAR_STRINGIFY_NAME(file, line, name, counter) @file ":" #line " " #name ":" #counter
#define _OBJC_IVAR_NAME(file, line, name, counter) __OBJC_IVAR_STRINGIFY_NAME(file, line, name, counter)
#define OBJC_IVAR_NAME(name) _OBJC_IVAR_NAME(__FILE__, __LINE__, name, __COUNTER__)

// old style creation. advantage: uses __func__ to determine calling function
// #define OBJC_IVAR_NAME(Name) [NSString stringWithFormat:@"%s:%i %s:%s:%i", __FILE__, __LINE__, __func__, #Name, __COUNTER__]

// implemenations for each of the overloads
#define _OBJC_IVAR_0(...) _Pragma("message \"Cannot call OBJC_IVAR with 0 params!\"")
#define _OBJC_IVAR_1(Name) _OBJC_IVAR_2(__strong id, Name)

// first major implemenation. because we do no assignment here, we don't have to check for is_set
#define _OBJC_IVAR_2(Type, Name) Type& Name = (_OBJC_IVAR::IMPL<Type>(self, OBJC_IVAR_NAME(Name)))

// this is where things get fun. we have 'OBJC_IVAR_CUR_NAME', instead of calling OBJC_IVAR_NAME
// multiple times, because we must ensure that COUNTER does not change during the course of the macro
// this is the 'inner bowels' of C, and it's quite hacky. Returns a reference to an associated object
// which is wrapped in a NSValue. Note that we only evaluate 'default' once throught the course of the
// application's cycle, so you can feel free to put intensive loading code there.
static NSString *_OBJC_IVAR_CUR_NAME;
#define _OBJC_IVAR_3(Type, Name, Default) Type& Name = (_OBJC_IVAR::IS_SET(self, (_OBJC_IVAR_CUR_NAME = OBJC_IVAR_NAME(Name))) ? _OBJC_IVAR::IMPL<Type>(self, _OBJC_IVAR_CUR_NAME) : _OBJC_IVAR::IMPL<Type>(self, _OBJC_IVAR_CUR_NAME, Default))

// namespace to wrap al lof our functions
namespace _OBJC_IVAR
{
    // internal dictionary of all associated object names, so that we don't run
    // into memory management issues.  we use a set here, because we should never
    // have duplicate associated object names.
    static NSMutableSet *_names = [NSMutableSet set];

    // wraps a value and a reference to a value. used over std::reference_wrapper,
    // as that doesn't actually copy in the value passed. That is required for what
    // we are doing, as we cannot be assigning to constants.
    template<typename T>
    class Wrapper {
    private:
        // private value wrapped by this object.
        T _value;
        // private reference wrapped by this object. should always point to _value.
        T& _ref;

    public:
        // default constructor. assumes 'T' has a valid 0-argument constructor
        Wrapper() : _value(), _ref(_value) { }

        // argument constructor. makes sure that value is initialized properly
        Wrapper(T val) : _value(val), _ref(_value) { }

        // returns the reference wrapped by this object
        operator T& () {
            return _ref;
        }

        T& get() {
            return _ref;
        }
    };

    // interns a name. because objc_getAssociatedObject works only by comparing
    // pointers (and +stringWithFormat: isn't guaranteed to return the same pointer),
    // we have to make sure that we maintain a list of all valid associated object
    // names. these are NOT linked to specific objects, which allows us to reuse some
    // memory
    inline NSString *name_intern(NSString *name)
    {
        // intern the value. first check if the object has been interned already,
        // and if it is, return that interned value
        if (id tmpName = [_names member:name])
        {
            name = tmpName;
        }

        // if we haven't interned this value before, then add it to the list and return it.
        else
        {
            [_names addObject:name];
        }

        return name;
    }

    // check and see if the requested iVar has been set yet. used for default value setting
    BOOL IS_SET(id target, NSString *name)
    {
        // first intern the name
        name = name_intern(name);

        // check if the object has this property. objc_getAssociatedObject will ALWAYS
        // return NULL if the object doesn't exist. Note the bridged cast. This is because
        // objc_getAssociatedObject doesn't care what you throw into the second parameter,
        // as long as it is a pointer. That gives us the flexibility at a later date, to,
        // for example, just pass a pointer to a single byte, and pull out the value that
        // way. However, we pass in a NSString pointer, because it makes it easy for us to
        // use and to re-use later.
        id val = objc_getAssociatedObject(target, (__bridge const void *) name);

        return val != nil;
    }

    // the actual implementation for setting the iVar. luckily this code isn't too hacky,
    // but it is a bit confusing.
    template<typename T>
    Wrapper<T>& IMPL(id target, NSString *name)
    {
        // first intern the name
        name = name_intern(name);

        // define a reference. we use pointers & new here, because C++ memory managment is
        // weird at best. Most of the time, you should be using RAII, but when dealing with
        // templates & objective-c interpolation, it is almost required that you use pointers
        // with new.
        Wrapper<T> *reference = nullptr;

        // check and see if the object already contains this property, if so, return that value
        NSValue *result = objc_getAssociatedObject(target, (__bridge const void *) name);
        if (result == nil)
        {
            // at this point, we need to create a new iVar, with the default constructor for the type.
            // for objective-c objects this is 'nil', for integers and floating point values this is 0,
            // for C++ structs and classes, this calls the default constructor. If one doesn't exist,
            // you WILL get a compile error.
            reference = new Wrapper<T>();

            // we now set up the object that will hold this wrapper. This is an extension on NSValue
            // which allows us to store a generic pointer (in this case a C++ object), and run desired
            // code on -dealloc (which will be called at the time the parent object is destroyed), in
            // this case, free the memory used by our wrapper.
            result = [NSValue valueWithCppObject:reference onDealloc:^(void *) {
                delete reference;
            }];

            // finally, set the associated object to the target, and now we are good to go.
            // We use OBJC_ASSOCIATION_RETAIN, so that our NSValue is properly freed when done.
            objc_setAssociatedObject(target, (__bridge const void *) name, result, OBJC_ASSOCIATION_RETAIN);
        }

        // from result, we cast it's -cppObjectValue to a Wrapper, to pull out the value.
        reference = static_cast<Wrapper<T> *>([result cppObjectValue]);

        // finally, return the pointer as a reference, not a pointer
        return *reference;
    }

    // this is pretty much the same as the other IMPL, but it has specific code for default values.
    // I will ignore everything that is the same about the two functions, and only focus on the
    // differences, which are few, but mandatory.
    template<typename T>
    Wrapper<T>& IMPL(id target, NSString *name, const T& defVal)
    {
        name = name_intern(name);

        Wrapper<T> *reference = nullptr; // asign to be the default constructor for 'T'

        NSValue *result = objc_getAssociatedObject(target, (__bridge const void *) name);
        if (result == nil)
        {
            // this is the only difference. Instead of constructing with the default constructor,
            // simply pass in our new default value as a copy.
            reference = new Wrapper<T>(defVal);
            result = [NSValue valueWithCppObject:reference onDealloc:^(void *) {
                delete reference;
            }];

            objc_setAssociatedObject(target, (__bridge const void *) name, result, OBJC_ASSOCIATION_RETAIN);
        }

        reference = static_cast<Wrapper<T> *>([result cppObjectValue]);
        return *reference;
    }
}

#endif // OBJC_IVAR_HPP

NSValue+CppObject.h

//
//  NSValue+CppObject.h
//  TestProj
//
//  Created by Richard Ross on 8/17/12.
//  Copyright (c) 2012 Ultimate Computer Services, Inc. All rights reserved.
//

#import <Foundation/Foundation.h>

// Extension on NSValue to add C++ object support. Because of the difficulty
// involved in templates, I took the easy way out and simply passed in a block
// of code to be run at dealloc.
@interface NSValue (CppObject)

// create a new NSValue instance that holds ptr, and calls 'deallocBlock' on destruction.
+(id) valueWithCppObject:(void *) ptr onDealloc:(void (^)(void *)) deallocBlock;
-(id) initWithCppObject:(void *)  ptr onDealloc:(void (^)(void *)) deallocBlock;

// get the held pointer of this object. I called it -cppObjectValue, so
// there was no confusion with -pointerValue.
-(void *) cppObjectValue;

@end

NSValue+CppObject.m

//
//  NSValue+CppObject.m
//  TestProj
//
//  Created by Richard Ross on 8/17/12.
//  Copyright (c) 2012 Ultimate Computer Services, Inc. All rights reserved.
//

#import "NSValue+CppObject.h"

// the concrete NSValue subclass for supporting C++ objects. Pretty straight-forward interface.
@interface ConcreteCppObject : NSValue
{
    // the underlying object that is being pointed to
    void *_object;
    // the block that is called on -dealloc
    void (^_deallocBlock)(void *);
}

@end

@implementation ConcreteCppObject

// object initialization
+(id) valueWithCppObject:(void *)ptr onDealloc:(void (^)(void *))deallocBlock
{
    return [[self alloc] initWithCppObject:ptr onDealloc:deallocBlock];
}

-(id) initWithCppObject:(void *)ptr onDealloc:(void (^)(void *))deallocBlock
{
    if (self = [super init])
    {
        _object = ptr;
        _deallocBlock = deallocBlock;
    }

    return self;
}

// required methods for subclassing NSValue
-(const char *) objCType
{
    return @encode(void *);
}

-(void) getValue:(void *)value
{
    *((void **) value) = _object;
}

// comparison
-(BOOL) isEqual:(id)compare
{
    if (![compare isKindOfClass:[self class]])
        return NO;

    return [compare cppObjectValue] == [self cppObjectValue];
}

// cleanup
-(void) dealloc
{
    // this should manage cleanup for us
    _deallocBlock(_object);
}

// value access
-(void *) cppObjectValue
{
    return _object;
}


@end

// NSValue additions for creating the concrete instances
@implementation NSValue (CppObject)

// object initialization
+(id) valueWithCppObject:(void *)ptr onDealloc:(void (^)(void *))deallocBlock
{
    return [[ConcreteCppObject alloc] initWithCppObject:ptr onDealloc:deallocBlock];
}

-(id) initWithCppObject:(void *)ptr onDealloc:(void (^)(void *))deallocBlock
{
    return [[self class] valueWithCppObject:ptr onDealloc:deallocBlock];
}

// unless the NSValue IS a ConcreteCppObject, then we shouldn't do anything here
-(void *) cppObjectValue
{
    [self doesNotRecognizeSelector:_cmd];

    return nil;
}

@end

Пример использования:

#import "OBJC_IVAR.hpp"

@interface SomeObject : NSObject

-(void) doSomething;

@end

@implementation SomeObject

-(void) doSomething
{
    OBJC_IVAR(__strong id, test, @"Hello World!");
    OBJC_IVAR(int, test2, 15);

    NSLog(@"%@", test);
    NSLog(@"%i", test2 += 7);

    // new scope
    {
        OBJC_IVAR(int, test, 100);

        NSLog(@"%i", ++test);
    }

    [self somethingElse];
}

-(void) somethingElse
{
    OBJC_IVAR(int, newVar, 7);

    NSLog(@"%i", newVar++);
}

@end

int main()
{
    SomeObject *obj = [SomeObject new];

    [obj doSomething];
    [obj doSomething];
    [obj doSomething];
}
person Richard J. Ross III    schedule 17.08.2012
comment
@CodaFi спасибо, я стараюсь: P. Как я уже говорил, среда выполнения Objective-C — моя страсть, и полезное манипулирование ею — это просто бонус. - person Richard J. Ross III; 18.08.2012
comment
По какой-то причине это просто заставляет меня думать о десятом правиле Greenspun. - person jscs; 19.08.2012
comment
@W'rkncacnter, за исключением того, что этот алгоритм далеко не медленный, но действительно очень быстрый. - person Richard J. Ross III; 19.08.2012

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

В этом случае переменная является частью состояния объекта, и поэтому наиболее целесообразно использовать переменную экземпляра (или свойство). Это именно то, для чего нужны ивары, независимо от того, используются ли они в дюжине методов или только в одном.

В настоящее время я сам работаю над реализацией Objective-C++, мне просто интересно, есть ли у кого-нибудь еще какие-либо мысли (или существующие инструменты) о том, как это сделать.

Мой совет — вообще не делать этого. Если ваша цель — избежать беспорядка, не пытайтесь без необходимости добавлять в язык новый класс хранения.

Однако, если вы полны решимости следовать этой линии, я бы рассмотрел использование блоков вместо связанных объектов. Блоки получают свои собственные копии переменных, которые ограничены временем существования блока. Например, вы можете сделать это:

- (void)func
{
    __block int i = 0;
    void (^foo)() = ^{
        i++;
        NSLog(@"i = %d", i);
    };

    foo();
    foo();
    foo();
}

и результат, который вы получаете:

i = 1
i = 2
i = 3

Возможно, вы сможете найти умный способ обернуть это в макрос, но мне кажется, что избежать объявления переменной экземпляра очень проблематично.

person Caleb    schedule 17.08.2012
comment
Хотя я согласен с вами, что вообще не следует использовать такие конструкции... как ваш пример блока не использует iVar? Ваш void (^fooBlock)(void) объявляет переменную экземпляра типа void (^)(void), почти как здесь. - person Thorsten Karrer; 17.08.2012
comment
@ThorstenKarrer Вы правы - я сосредоточился на постоянном характере блочных переменных, но вы правы в том, что вам все равно нужно отслеживать сам блок. Удален последний пример, который в значительной степени просто вызывает вопрос. Я все еще думаю, что блоки — это лучшая надежда на решение, но это, прежде всего, смехотворная проблема. - person Caleb; 17.08.2012
comment
Это противоречит цели. Да, это гениальное использование блоков, но это не переменная экземпляра, а функциональная переменная. Теперь у меня есть решение, которое я опубликую после того, как добавлю к нему некоторую документацию. - person Richard J. Ross III; 17.08.2012