objective c - ¿Objetivo C-vista personalizada e implementación del método init?
objective-c initwithframe (3)
Tengo una vista personalizada que deseo poder inicializar tanto in-code
como en la nib
.
¿Cuál es la forma correcta de escribir los métodos initWithFrame
y initWithCoder
? Ambos comparten un bloque de código que se utiliza para cierta inicialización.
La solución no es tan simple como parece inicialmente. Hay algunos peligros en la inicialización, más en los que están más abajo. por estas razones, generalmente tomo uno de los dos enfoques siguientes en los programas objc:
para casos triviales, la duplicación no es una mala estrategia:
- (id)initOne
{
self = [super init];
if (nil != self) { monIntIvar = SomeDefaultValue; }
return self;
}
- (id)initTwo
{
self = [super init];
if (nil != self) { monIntIvar = SomeDefaultValue; }
return self;
}
para casos no triviales, recomiendo una función de inicialización estática que toma la forma general:
// MONView.h
@interface MONView : UIView
{
MONIvar * ivar;
}
@end
// MONView.m
static inline bool InitMONView(MONIvar** ivar) {
*ivar = [MONIvar new];
return nil != *ivar;
}
@implementation MONView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (nil != self) {
if (!InitMONView(&ivar)) {
[self release];
return nil;
}
}
return self;
}
- (id)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (nil != self) {
if (!InitMONView(&ivar)) {
[self release];
return nil;
}
}
return self;
}
// …
@end
Objective-C ++:
Si está usando objc ++, simplemente puede implementar constructores predeterminados adecuados para sus ivars c ++ y omitir la mayoría de los andamios de inicialización y desasignación (suponiendo que haya habilitado los indicadores del compilador correctamente).
Actualizar:
Voy a explicar por qué esta es la manera segura de inicializar un objeto y describir algunas razones por las que las implementaciones típicas de las otras respuestas son peligrosas.
El problema típico de los inicializadores comunes que llaman a los métodos de instancia durante la inicialización es que abusan del gráfico de herencia, típicamente presentan complejidad y errores.
Recomendación: invocar métodos de instancia anulados en un objeto parcialmente construido (por ejemplo, durante la inicialización y dealloc) no es seguro y debe evitarse. Los accesores son particularmente malos. En otros idiomas, este es un error del programador (por ejemplo, UB). Mire los documentos de objc sobre el tema (ref: "Implementando un Inicializador"). Considero que esto es obligatorio, pero sigo sabiendo que las personas que insisten en que los métodos de instancia y los accesores son mejores en estados parcialmente construidos porque "por lo general les funciona".
Requisito: Respetar el gráfico de herencia. inicializar desde la base hacia arriba. Destruye de arriba abajo. siempre.
Recomendación: mantener la inicialización consistente para todos. Si su base devuelve algo de init, debe asumir que todo está bien. no introduzca un baile de inicialización frágil para que lo implementen sus clientes y subclases (es probable que vuelva como un error). necesita saber si tiene una retención en una instancia válida. también, los subclasificadores asumirán (con razón) que su base está correctamente inicializada cuando devuelve un objeto desde un inicializador designado. puede reducir la probabilidad de esto haciendo que los ivars de la clase base sean privados. Una vez que regresa de init, los clientes / subclases asumen que el objeto del que se derivan es utilizable e inicializado correctamente. a medida que crecen los gráficos de clase, la situación se vuelve muy compleja y los errores comienzan a aparecer.
Recomendación: comprobar errores en init. También mantener el manejo y detección de errores consistentes. devolver nil es la convención obvia para determinar si ha habido un error durante la inicialización. detectalo temprano
Ok, pero ¿qué pasa con un método de instancia compartida?
Ejemplo prestado y alterado de otro post:
@implementation MONDragon
- (void)commonInit
{
ivar = [MONIvar new];
}
- (id)initWithFrame:(CGRect)aRect
{
if ((self = [super initWithFrame:aRect])) {
[self commonInit];
}
return self;
}
- (id)initWithCoder:(NSCoder*)coder
{
if ((self = [super initWithCoder:coder])) {
[self commonInit];
}
return self;
}
// …
(por cierto, no hay manejo de errores en ese ejemplo)
Caleb: el mayor "peligro" que veo en el código anterior es que alguien podría crear una subclase de la clase en cuestión, anular -commonInit y potencialmente inicializar el objeto dos veces.
específicamente, la subclase - [MONDragon commonInit] se llamaría dos veces (los recursos con fugas ya que se crearían dos veces) y el inicializador de base y el manejo de errores no se realizarían.
Caleb: si eso es un riesgo real ...
cualquiera de los efectos puede equipararse a un programa no confiable. El problema se evita fácilmente mediante el uso de la inicialización convencional.
Caleb:… la forma más fácil de lidiar con esto es mantener la privacidad de -commonInit y / o documentarla como algo que no se debe anular.
ya que el tiempo de ejecución no distingue la visibilidad cuando se envía un mensaje, este enfoque es peligroso porque cualquier subclase podría declarar fácilmente el mismo método de inicialización privada (ver más abajo).
documentar un método como algo que no se debe anular, expone las cargas en los subclases e introduce complejidades y problemas que se pueden evitar fácilmente, utilizando otros enfoques. también es propenso a errores ya que el compilador no lo marca.
Si se insiste en usar un método de instancia, una convención que reserve como -[MONDragon constructMONDragon]
y -[MONKomodo constructMONKomodo]
podría reducir significativamente el error en la mayoría de los casos. es probable que el inicializador sea visible solo para la TU de la implementación de la clase, por lo que el compilador puede marcar algunos de nuestros errores potenciales.
nota al margen: un constructor de objetos comunes como:
- (void)commonInit
{
[super commonInit];
// init this instance here
}
(lo que también he visto) es aún peor porque restringe la inicialización, elimina el contexto (por ejemplo, los parámetros) y aún así las personas mezclan su código de inicialización entre clases entre el inicializador designado y -commonInit
.
a lo largo de todo esto, desperdiciando mucho tiempo en la depuración de todos los problemas anteriores de un malentendido general y errores / descuidos tontos, he llegado a la conclusión de que una función estática es la más fácil de entender y mantener cuando necesita implementar una inicialización común para una clase. las clases deberían aislar sus clentes de los peligros, un problema en el que el ''inicializador común a través del método de instancia'' ha fallado repetidamente.
no es una opción en el OP basada en el método especificado, pero como una nota general: normalmente puede consolidar la inicialización común más fácilmente utilizando constructores de conveniencia. esto es particularmente útil para minimizar la complejidad cuando se trata de grupos de clases, clases que pueden devolver especializaciones e implementaciones que pueden optar por seleccionar desde varios inicializadores internos.
Lo correcto en ese caso es crear otro método que contenga el código común a ambos -initWithFrame:
y -initWithCoder:
y luego llamar a ese método desde ambos -initWithFrame:
y -initWithCoder:
::
- (void)commonInit
{
// do any initialization that''s common to both -initWithFrame:
// and -initWithCoder: in this method
}
- (id)initWithFrame:(CGRect)aRect
{
if ((self = [super initWithFrame:aRect])) {
[self commonInit];
}
return self;
}
- (id)initWithCoder:(NSCoder*)coder
{
if ((self = [super initWithCoder:coder])) {
[self commonInit];
}
return self;
}
Preste atención a las inquietudes descritas en la respuesta de Justin, en particular que las subclases no deben anular -commonInit
. Utilicé ese nombre aquí por su valor ilustrativo, pero probablemente querrá uno que esté más vinculado a su clase y que sea menos probable que se anule accidentalmente. Si está creando una subclase UIView diseñada específicamente para este fin, es poco probable que se clasifique por sí misma, usar un método de inicialización común como el anterior es perfectamente correcto. Si está escribiendo un marco para que otros lo usen, o si no comprende el problema pero quiere hacer lo más seguro posible, use una función estática en su lugar.
Si comparten código, solo pídales que llamen un tercer método de inicialización.
Por ejemplo, initWithFrame
podría verse algo como esto:
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
[self doMyInitStuff];
}
return self;
}
Tenga en cuenta que si está en OS X (a diferencia de iOS), el marco será NSRect
lugar de CGRect
.
Si necesita realizar una comprobación de errores, haga que su método de inicialización devuelva un estado de error así:
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
if (![self doMyInitStuff]) {
[self release];
self = nil;
}
}
return self;
}
Esto supone que el método doMyInitStuff
devuelve NO
en caso de error.
Además, si aún no lo ha mirado, hay un poco de documentación sobre la inicialización que podría ser útil para usted (aunque no aborda directamente esta pregunta):
Pautas de codificación para el cacao: consejos y técnicas para desarrolladores de marcos