Все больше и больше статей на тему «добавь функциональные косты плюшки в свой любимый императивный язык программирования». Вот недавний пример для Java.
В Objective-C не так давно были добавлены блоки (blocks), с помощью которых реализованы замыкания. Но хочется чего-то большего. Например сопоставления с образом (Pattern Matching) и параметризованные методы.
Исключительно Just For Fun попробуем добавить их в язык без патчинга компилятора и танцев с препроцессором, только средствами самого языка.
Что из этого получилось?
Все описанное далее исключительно пример иллюстрирующий идею и использование его в своих проектах вы можете на свой страх и риск.
Cопоставление с образом позволяет написать что-то на подобии:
Для начала, пробуем сделать сопоставление с образом. Итак, создаем расширение NSObject и объявляем тип блока, в который мы и будем заворачивать дополнительные реализации.
Теперь нужно подменить реализацию селектора чтобы иметь возможность выбирать необходимую реализацию в зависимости от переданного фактического параметра. В этом поможет прием подмены метода (Method Swizzling). Каждый метод — это структура типа
И собственно реализация расширения NSObject:
Вся магия — в методе [-NSObject method:withParameter:useBlock:]. Для переданного селектора мы создаем объект PMImplementation, который сохраняет в себе указатель на реализацию. После чего заменяем его на метод «swizzledMethod:». Трюк в том, что мы можем узнать какой селектор был вызван на самом деле через параметр _cmd, который неявно передается при вызове любого селектора. Теперь при вызове swizzledMethod вызывается [-PMImplementation forObject:invokeWithParameter:] а там мы уже либо находим по объекту блок и выполняем его, либо используем реализацию по умолчанию.
Метод [-NSObject impls] добавляет в рантаме для self словарь, хранящий объекты PMImplementation для разных селекторов.
Теперь то, ради всего все это затевалось — например класс для нахождения факториала:
Работает он как и предполагалось:
Для параметризации методов напишем небольшой класс-обертку, у которого переопределим метод «isEqual:» и удобный макрос.
Теперь реализацию можно выбирать и в зависимости от типа аргумента.
Пробуем:
Что можно было сделать еще? Как минимум работу с методами у которых больше одного аргумента, но это сильно бы увеличило статью.
В итоге получены реализации сопоставления с образом и параметризованных методов (правда с жутким синтаксисом) в Objective-C.
Очевидные минусы — в качестве параметров и «образов» можно использовать только объекты.
Надеюсь эта статья была хоть кому-то полезна и после прочтения возникло желание узнать побольше о магии рантайма Objective-C. Если да, то увлекательное чтиво на ночь:
Objective-C Runtime Programming Guide
Objective-C Runtime Reference
В Objective-C не так давно были добавлены блоки (blocks), с помощью которых реализованы замыкания. Но хочется чего-то большего. Например сопоставления с образом (Pattern Matching) и параметризованные методы.
Исключительно Just For Fun попробуем добавить их в язык без патчинга компилятора и танцев с препроцессором, только средствами самого языка.
Что из этого получилось?
Все описанное далее исключительно пример иллюстрирующий идею и использование его в своих проектах вы можете на свой страх и риск.
Cопоставление с образом позволяет написать что-то на подобии:
factorial( 0 ) -> 1
factorial( 1 ) -> 1
factorial( n ) -> n * factorial( n - 1 )
Для начала, пробуем сделать сопоставление с образом. Итак, создаем расширение NSObject и объявляем тип блока, в который мы и будем заворачивать дополнительные реализации.
typedef id (^PatternMatchingBlock)(id obj);
@interface NSObject (PatternMatching)
-(void)method:(SEL)sel_ withParameter:(id)object_ useBlock:(PatternMatchingBlock)block_;
@end
Теперь нужно подменить реализацию селектора чтобы иметь возможность выбирать необходимую реализацию в зависимости от переданного фактического параметра. В этом поможет прием подмены метода (Method Swizzling). Каждый метод — это структура типа
objc_method
из которой нас интересует поле method_imp
типа IMP
. IMP
— это указатель на C-функцию реализации метода. Смысл подмены метода — в замене этих указателей у 2-х методов. Создаем класс, который будет хранить в себе указатель на изначальную реализацию метода и словарь, ключами которого будут объекты «образов», а значениями — блоки реализаций:@interface PMImplementation : NSObject
@property ( nonatomic, retain ) NSMutableDictionary* impls;
@property ( nonatomic, retain ) NSValue* defaultImpl;
+(id)implementationWithDefaultImpl:(IMP)impl_;
-(id)forObject:(id)object_ invokeWithParameter:(id)parameter_;
@end
static char* PMimplsKey = nil;
@implementation PMImplementation
@synthesize impls = _impls;
@synthesize defaultImpl = _default_impl;
-(void)dealloc {
[_impls release];
[_default_impl release];
[super dealloc];
}
-(id)initWithDefaultImpl:(IMP)impl_ {
if ( !(self = [super init]) )
return nil;
self.defaultImpl = [NSValue valueWithPointer:impl_];
self.impls = [NSMutableDictionary dictionary];
return self;
}
+(id)implementationWithDefaultImpl:(IMP)impl_ {
return [[[self alloc] initWithDefaultImpl:impl_] autorelease];
}
-(id)forObject:(id)object_ invokeWithParameter:(id)parameter_ {
for( id key_ in [self.impls allKeys] )
if ( [key_ isEqual:parameter_] ) {
PatternMatchingBlock block_ = [self.impls objectForKey:key_];
return block_( parameter_ );
}
IMP impl_ = [self.defaultImpl pointerValue];
return impl_(object_, _cmd, parameter_);
}
@end
И собственно реализация расширения NSObject:
@implementation NSObject (PatternMatching)
-(NSMutableDictionary*)impls{
NSMutableDictionary* impls_ = objc_getAssociatedObject(self, &PMImplsKey);
if ( !impls_ ) {
impls_ = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &PMImplsKey, impls_
, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return impls_;
}
-(void)method:(SEL)sel_ withParameter:(id)object_
useBlock:(PatternMatchingBlock)block_ {
NSString* selector_key_ = NSStringFromSelector(sel_);
PMImplementation* impl_ = [self.impls objectForKey:selector_key_];
if ( !impl_ ) {
Method default_ = class_getInstanceMethod([self class], sel_);
IMP default_impl_ = method_getImplementation(default_);
impl_ = [PMImplementation implementationWithDefaultImpl:default_impl_];
[self.impls setObject:impl_ forKey:selector_key_];
Method swizzed_method_ = class_getInstanceMethod([self class]
,@selector(swizzledMethod:));
method_setImplementation(default_, method_getImplementation(swizzed_method_));
}
[impl_.impls setObject:block_ forKey:object_];
}
-(id)swizzledMethod:(id)obj_ {
PMImplementation* impl_ = [self.impls objectForKey:NSStringFromSelector(_cmd)];
return [impl_ forObject:self invokeWithParameter:obj_];
}
@end
Вся магия — в методе [-NSObject method:withParameter:useBlock:]. Для переданного селектора мы создаем объект PMImplementation, который сохраняет в себе указатель на реализацию. После чего заменяем его на метод «swizzledMethod:». Трюк в том, что мы можем узнать какой селектор был вызван на самом деле через параметр _cmd, который неявно передается при вызове любого селектора. Теперь при вызове swizzledMethod вызывается [-PMImplementation forObject:invokeWithParameter:] а там мы уже либо находим по объекту блок и выполняем его, либо используем реализацию по умолчанию.
Метод [-NSObject impls] добавляет в рантаме для self словарь, хранящий объекты PMImplementation для разных селекторов.
Теперь то, ради всего все это затевалось — например класс для нахождения факториала:
@interface Factorial : NSObject
-(NSDecimalNumber*)factorial:( NSDecimalNumber* )number_;
@end
@implementation Factorial
-(id)init {
if ( !( self = [super init] ) )
return nil;
NSDecimalNumber* zero_ = [NSDecimalNumber numberWithInteger:0];
[self method:@selector(factorial:) withParameter:zero_ useBlock: ^(id obj_){
return (id)[NSDecimalNumber numberWithInteger:1];
}];
NSDecimalNumber* one_ = [NSDecimalNumber numberWithInteger:1];
[self method:@selector(factorial:) withParameter:one_ useBlock: ^(id obj_){
return (id)[NSDecimalNumber numberWithInteger:1];
}];
return self;
}
-(NSDecimalNumber*)factorial:(NSDecimalNumber*)number_ {
return [number_ decimalNumberByMultiplyingBy:
[self factorial:[number_ decimalNumberBySubtracting:
[NSDecimalNumber numberWithInteger:1]]]];
}
Работает он как и предполагалось:
Factorial* factorial_ = [[Factorial new] autorelease];
NSNumber* number_ = [NSDecimalNumber numberWithInteger:10];
NSLog( @"factorial %@ = %@", number_, [factorial_ factorial:number_]);
factorial 10 = 3628800
Для параметризации методов напишем небольшой класс-обертку, у которого переопределим метод «isEqual:» и удобный макрос.
#define PMCLASS( x ) [[[PMClass alloc] initWith:[x class]] autorealese]
@interface PMClass : NSObject <NSCopying>
@property ( nonatomic, retain ) Class class;
-(id)initWith:( Class )class_;
@end
@implementation PMClass
@synthesize class = _class;
-(void)dealloc {
[_class release];
[super dealloc];
}
-(id)initWith:(Class)class_ {
if ( !( self = [super init] ) )
return nil;
self.class = class_;
return self;
}
-(BOOL)isEqual:(id)object_ {
return [self.class isEqual:[object_ class]];
}
-(id)copyWithZone:(NSZone *)zone_ {
return [[PMClass alloc] initWith:self.class];
}
@end
Теперь реализацию можно выбирать и в зависимости от типа аргумента.
@interface Test : NSObject
-(void)test:(id)obj_;
@end
@implementation Test
-(id)init {
if ( ! (self = [super init] ) )
return nil;
[self method:@selector(test:) withParameter:PMCLASS(NSNull) useBlock:^(id obj_){
NSLog(@"implementation for Null: %@", [obj_ class]);
return (id)nil;
}];
return self;
}
-(void)test:(id)obj_ {
NSLog(@"default impl for Object: %@", [obj_ class]);
}
@end
Пробуем:
Test* test_ = [[Test new] autorealese];
[test_ test:@"String"];
[test_ test:[NSNull null]];
default impl for Object: NSCFString
implementation for Null: NSNull
Что можно было сделать еще? Как минимум работу с методами у которых больше одного аргумента, но это сильно бы увеличило статью.
В итоге получены реализации сопоставления с образом и параметризованных методов (правда с жутким синтаксисом) в Objective-C.
Очевидные минусы — в качестве параметров и «образов» можно использовать только объекты.
Надеюсь эта статья была хоть кому-то полезна и после прочтения возникло желание узнать побольше о магии рантайма Objective-C. Если да, то увлекательное чтиво на ночь:
Objective-C Runtime Programming Guide
Objective-C Runtime Reference