[Objective-C] Brincando com Categorias e Blocos

2010 November 28, 18:58 h

Continuando meus estudos com Objective-C, existem algumas funcionalidades que me deixam realmente surpreso. Duas delas são Categorias e Blocos.

Para facilitar, vamos ver um código Ruby para dar um exemplo do que quero fazer:

1
2
3
4
5
6
7
class Array
  def each_element
    for elem in self
      yield(elem)
    end
  end
end

Sabemos que em Ruby, todas as classes podem ser abertas e extendidas, incluindo classes padrão da linguagem como Array ou String. Isso permite extender a própria linguagem e é o que o pacote ActiveSupport do Rails faz, ao adicionar métodos como #days à classe Fixnum, permitindo operações como 2.days – 1.day, por exemplo.

Em linguagens como Java isso não é possível porque as classes são fechadas, e inclusive muitas delas são finals o que impedem criar sub-classes a partir delas. Por exemplo, não se pode criar uma classe herdando de String. Isso acaba dando origem a muitas classes acessórias como StringUtils, o que eu acho particularmente não elegante.

No exemplo acima, reabri a classe padrão Array do Ruby e fiz minha própria versão do método each, que já existe, chamando-o de each_element somente com objetivos didáticos para este artigo. Agora podemos pegar um array normal e chamar esse método nele:

1
2
3
4
list = ["a", "b", "c"]
list.each_element do |elem|
  puts elem
end

Mais do que extender classes, o Ruby possui outra funcionalidade muito flexível chamada blocos ou closures/fechamentos (eu já escrevi sobre blocos e closures antes pra RubyLearning. Sugiro ler para entender o conceito)

Essas são duas funcionalidades que muitos poderiam imaginar que só seriam possíveis em linguagens altamente dinâmicas como Ruby, Python ou Smalltalk. Já Objective-C é uma extensão da linguagem C, algo considerado por muitos como tão baixo nível que nem se imaginariam ser possível. Será?

Categorias

O Objective-C tem uma funcionalidade chamada Categories. Essencialmente isso permite “reabrir” classes já existentes e extendê-las com mais métodos.

Na minha interpretação o Obj-C, assim como Ruby, são linguagens orientadas a objeto mas, mais importante, eu diria que elas são orientadas a protocolo. Em vez de pensar em interfaces estáticas e “chamar um método” o correto é pensar em “enviar mensagens”. Protocolos definem quais mensagens o objeto sabe responder. A diferença é que você não busca uma coerência em tempo de compilação mas sim em tempo de execução. Você pode mandar mensagens que o objeto não entende se quiser sem receber um erro de compilação.

Uma convenção de nomenclatura que podemos usar é criar o arquivo header e a implementação usando o nome da classe a ser extendida, o símbolo “+”, e o nome da Categoria que queremos implementar. Por exemplo, digamos que eu queira a mesma funcionalidade do método each de Array do Ruby no equivalente NSArray do Obj-C, podemos fazer assim:

1
2
3
4
5
6
// NSArray+functional.h
@interface NSArray (functional)

- (void) each:(void (^) (id))block;

@end

E a implementação seria:

1
2
3
4
5
6
7
8
9
10
11
12
// NSArray+functional.m

#import "NSArray+functional.h"

@implementation NSArray (functional)
- (void) each:(void (^) (id))block {
    int i;
    for (i = 0; i < [self count]; i ++) {
        block([self objectAtIndex:i]);
    }
}
@end

A sintaxe é exatamente a mesma se estivéssemos definindo uma nova classe, mas neste caso declaramos o mesmo nome da classe que já existe NSArray e entre parênteses colocamos o nome da nossa categoria que, neste caso, chamei arbitrariamente de “functional” para ter diversos métodos funcionais.

Categorias não podem ter variáveis de instância entre chaves na declaração da interface, ela só pode comportar novos métodos, por isso falei que não era não flexível, mas isso já seria o bastante.

Essa funcionalidade pode ser usada principalmente para duas coisas: substituir a necessidade de criar sub-classes e, com isso, evitar usar herança sempre que possível e que fizer sentido; e a outra é quebrar classes com implementações muito grande em múltiplos arquivos de forma a melhor organizar os arquivos do projeto.

Blocos

Como vocês devem ter suspeitado, implementar o método each semelhante ao Ruby significa que deveríamos poder usar blocos. E o Obj-C também permite blocos, na forma de métodos anônimos (sem nome).

O parâmetro que implementamos é (void (^) (id))block. “block” é o nome da variável que vai receber o bloco. Seu tipo é de retorno “void” e parâmetro “id”. O “^” é o “nome” do bloco, no caso, sem nome algum, ou anônimo. Tendo em mãos o bloco podemos executá-la assim: block([self objectAtIndex:i]). O tipo id em Obj-C é um tipo genérico que denota um objeto arbitrário. Ele é usado por todo o framework Cocoa e seria, mais ou menos, o equivalente a dizer que o método recebe qualquer tipo de objeto.

E como podemos usar essa nova categoria com o novo método? Vejamos:

1
2
3
4
5
6
7
8
9
#import "NSArray+functional.h"

- (IBAction) foo:(id)sender {    
  NSMutableArray *list = [NSMutableArray arrayWithObjects:@"a", @"b", @"c", nil ];
  NSString *msg = @"elemento: %@";
  [list each:^(id obj) {
      NSLog(msg, obj); 
  }];
}

No caso, faça de conta que estamos numa aplicação de iPhone ou Mac, por isso criei um método que retorna IBAction. Para quem não sabe, IBAction é a mesma coisa que void, ou seja, que o método não retorna nada. A diferença é que o Interface Builder reconhece métodos que retornam IBAction como métodos que podem ser ligados diretamente a ações de um elemento visual na tela, por exemplo.

No corpo do método começamos criando um NSMutableArray. Fiz isso de propósito porque essa classe herda de NSArray e, portanto, também herdará o novo método each que implementamos.

Agora a parte importante é como chamamos o método nesse objeto: [list each:^(id obj) { … }]. Ou seja, estamos enviando a mensagem each ao objeto list passando como parâmetro um bloco anônimo “^” que tem parâmetro id obj. Se você não conhece blocos de Ruby ou de outra linguagem como Lisp, pode ser difícil entender o que está acontecendo, por isso recomendo novamente ler meu artigo sobre blocos antes.

Note que dentro do bloco o NSLog está usando a string msg que foi criada fora do bloco, exatamente como eu poderia fazer em Ruby, o bloco tem conhecimento do ambiente ao seu redor e eu posso usar variáveis criadas fora do bloco. Isso é uma das coisas que torna essa funcionalidade de blocos diferente de um simples “delegate” ou simplesmente passar um ponteiro de uma função.

Em Ruby, eu posso capturar um bloco em uma variável, assim:

1
2
3
bloco = lambda { |a| puts a }
bloco.call("bla")
# => "bla"

No exemplo acima, criamos um bloco e em seguida executamos esse bloco usando o método call. Em Obj-C podemos fazer algo similar assim:

1
2
3
4
void (^bloco)(NSString*) = ^(NSString* msg) {
  NSLog(msg);
};
bloco(@"bla");

Veja que é muito parecido só que precisamos declarar os tipos. Na primeira linha definimos uma variável do tipo bloco, com o nome de “bloco” (o nome vem logo depois do “^”). Antes do nome temos o tipo de retorno, void, e depois o tipo do parâmetro que ele aceita, ponteiro de NSString. Daí criamos o bloco propriamente dito.

Em seguida basta chamar o bloco como se fosse uma função normal de C, usando a notação de passar parâmetros entre parênteses.

Conclusão

Categorias e Blocos podem ajudar muito a tornar a programação em Obj-C mais flexível e mais próxima dos conceitos de linguagens mais dinâmicas como Ruby. Como exercício que tal completar a minha Categoria NSArray+functional.h e acrescentar métodos como select, map, sort, etc? Coloquem links para Gist nos comentários com sugestões de implementação.

Para aprender mais sobre blocos, recomendo ler:

tags: learning beginner apple objective-c

Comments

comentários deste blog disponibilizados por Disqus