development

Objective-C에서 추상 클래스 작성

big-blog 2020. 2. 12. 23:37
반응형

Objective-C에서 추상 클래스 작성


저는 원래 Objective-C와 함께 일하는 Java 프로그래머입니다. 추상 클래스를 만들고 싶지만 Objective-C에서는 불가능한 것으로 보입니다. 이게 가능해?

그렇지 않다면 Objective-C에서 추상 클래스에 얼마나 가까이 갈 수 있습니까?


일반적으로 Objective-C 클래스는 규칙에 의해서만 추상화됩니다. 작성자가 클래스를 추상으로 문서화하는 경우 서브 클래 싱없이 사용하지 마십시오. 그러나 추상 클래스의 인스턴스화를 막는 컴파일 타임 적용은 없습니다. 실제로, 사용자가 카테고리를 통해 (즉, 런타임에) 추상 메소드의 구현을 제공하는 것을 막을 수있는 것은 없습니다. 추상 클래스의 메소드 구현에서 예외를 발생시켜 사용자가 최소한 특정 메소드를 대체하도록 할 수 있습니다.

[NSException raise:NSInternalInconsistencyException 
            format:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)];

메소드가 값을 반환하면 사용하기가 조금 더 쉽습니다.

@throw [NSException exceptionWithName:NSInternalInconsistencyException
                               reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
                             userInfo:nil];

그런 다음 메소드에서 return 문을 추가 할 필요가 없습니다.

추상 클래스가 실제로 인터페이스 인 경우 (즉, 구체적인 메소드 구현이없는 경우) Objective-C 프로토콜을 사용하는 것이 더 적합한 옵션입니다.


아니요, Objective-C에서 추상 클래스를 만들 수있는 방법이 없습니다.

메소드 / 선택자가 doesNotRecognizeSelector를 호출하여 추상 클래스를 조롱 할 수 있으므로 클래스를 사용할 수 없게하는 예외가 발생합니다.

예를 들면 다음과 같습니다.

- (id)someMethod:(SomeObject*)blah
{
     [self doesNotRecognizeSelector:_cmd];
     return nil;
}

초기화를 위해이 작업을 수행 할 수도 있습니다.


위의 @Barry Wark의 답변을 riffing하고 iOS 4.3으로 업데이트하면 내 참조로 남겨 둘 수 있습니다.

#define mustOverride() @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%s must be overridden in a subclass/category", __PRETTY_FUNCTION__] userInfo:nil]
#define methodNotImplemented() mustOverride()

그런 다음 방법으로 이것을 사용할 수 있습니다

- (void) someMethod {
     mustOverride(); // or methodNotImplemented(), same thing
}



참고 : 매크로를 C 함수처럼 보이게 만드는 것이 좋은지 아닌지는 확실하지 않지만 반대로 학교에 갈 때까지 유지하겠습니다. 런타임 시스템 이 호출 에 응답하여 던지기 때문에 NSInvalidArgumentException(문서 대신 NSInternalInconsistencyException) 사용하는 것이 더 정확하다고 생각합니다 doesNotRecognizeSelector( NSObject문서 참조 ).


내가 생각해 낸 해결책은 다음과 같습니다.

  1. "추상"클래스에서 원하는 모든 것에 대한 프로토콜을 만듭니다.
  2. 프로토콜을 구현하는 기본 클래스 (또는 추상이라고도 함)를 작성하십시오. "추상적 인"방법을 원하는 모든 방법에 대해 .m 파일에는 구현하지만 .h 파일에는 구현하지 마십시오.
  3. 자녀 클래스가 기본 클래스에서 상속하고 프로토콜을 구현하도록하십시오.

이런 식으로 컴파일러는 자식 클래스에 의해 구현되지 않은 프로토콜의 모든 메소드에 대해 경고합니다.

Java처럼 간결하지는 않지만 원하는 컴파일러 경고가 표시됩니다.


로부터 옴니 그룹 메일 링리스트 :

현재 Objective-C에는 Java와 같은 추상 컴파일러 구성이 없습니다.

따라서 추상 클래스를 다른 일반 클래스로 정의하고 비어 있거나 선택기를 지원하지 않는 추상 메소드에 대한 메소드 스텁을 구현하기 만하면됩니다. 예를 들어 ...

- (id)someMethod:(SomeObject*)blah
{
     [self doesNotRecognizeSelector:_cmd];
     return nil;
}

또한 기본 초기화 프로그램을 통해 추상 클래스가 초기화되지 않도록 다음을 수행합니다.

- (id)init
{
     [self doesNotRecognizeSelector:_cmd];
     [self release];
     return nil;
}

추상 기본 클래스를 작성하는 대신 프로토콜 (Java 인터페이스와 유사) 사용을 고려하십시오. 이를 통해 메소드 세트를 정의한 후 프로토콜을 준수하는 모든 오브젝트를 승인하고 메소드를 구현할 수 있습니다. 예를 들어, Operation 프로토콜을 정의한 후 다음과 같은 기능을 수행 할 수 있습니다.

- (void)performOperation:(id<Operation>)op
{
   // do something with operation
}

여기서 op는 작업 프로토콜을 구현하는 모든 개체가 될 수 있습니다.

단순히 메소드를 정의하는 것 이상을 수행하기 위해 추상 기본 클래스가 필요한 경우 일반 Objective-C 클래스를 작성하여 인스턴스화되지 않도록 할 수 있습니다. -(id) init 함수를 무시하고 nil 또는 assert (false)를 리턴하도록하십시오. 매우 깨끗한 솔루션은 아니지만 Objective-C는 완전히 동적이기 때문에 추상 기본 클래스와 직접적으로 동등한 것은 없습니다.


이 스레드는 오래되었으며 공유하고 싶은 대부분은 이미 여기에 있습니다.

그러나 내가 가장 좋아하는 방법은 언급되어 있지 않으며 AFAIK는 현재 Clang에서 기본 지원이 없으므로 여기로 이동하십시오.

첫째, 다른 사람들이 이미 지적했듯이 추상 클래스는 Objective-C에서 매우 드문 일입니다. 우리는 대개 컴포지션 (때로는 위임을 통해)을 사용합니다. 이것은 아마도 이러한 기능이 언어 / 컴파일러에 존재하지 않는 이유 일 것입니다. @dynamic속성 과는 별도로 IIRC가 CoreData의 도입과 함께 ObjC 2.0에 추가되었습니다.

그러나 위임 (또는 일반적으로 조성물) 당신의 문제를 해결하는 데 적합하지 않습니다 있다는 결론에 도달했다 (! 상황의주의 깊은 평가 후) 여기 방법은 주어진 나는 그것을 할 :

  1. 기본 클래스에서 모든 추상 메소드를 구현하십시오.
  2. 구현하십시오 [self doesNotRecognizeSelector:_cmd];
  3. __builtin_unreachable();공백하지 않은 방법에 대해 경고 메시지를 끄면“반환없이 비 공백 기능의 끝에 도달했음을 제어합니다”라는 메시지가 나타납니다.
  4. 매크로에서 2 단계와 3 단계를 결합하거나 구현없이 카테고리에서 -[NSObject doesNotRecognizeSelector:]사용하여 주석 달아 해당 메소드의 원래 구현을 대체하지 말고 해당 카테고리의 헤더를 프로젝트 PCH에 포함 시키십시오.__attribute__((__noreturn__))

필자는 개인적으로 상용구를 최대한 줄일 수있는 매크로 버전을 선호합니다.

여기있어:

// Definition:
#define D12_ABSTRACT_METHOD {\
 [self doesNotRecognizeSelector:_cmd]; \
 __builtin_unreachable(); \
}

// Usage (assuming we were Apple, implementing the abstract base class NSString):
@implementation NSString

#pragma mark - Abstract Primitives
- (unichar)characterAtIndex:(NSUInteger)index D12_ABSTRACT_METHOD
- (NSUInteger)length D12_ABSTRACT_METHOD
- (void)getCharacters:(unichar *)buffer range:(NSRange)aRange D12_ABSTRACT_METHOD

#pragma mark - Concrete Methods
- (NSString *)substringWithRange:(NSRange)aRange
{
    if (aRange.location + aRange.length >= [self length])
        [NSException raise:NSInvalidArgumentException format:@"Range %@ exceeds the length of %@ (%lu)", NSStringFromRange(aRange), [super description], (unsigned long)[self length]];

    unichar *buffer = (unichar *)malloc(aRange.length * sizeof(unichar));
    [self getCharacters:buffer range:aRange];

    return [[[NSString alloc] initWithCharactersNoCopy:buffer length:aRange.length freeWhenDone:YES] autorelease];
}
// and so forth…

@end

보시다시피, 매크로는 추상 메소드의 전체 구현을 제공하여 보일러 플레이트의 필요한 양을 최소한으로 줄입니다.

더 나은 옵션은 Clang 팀 에 기능 요청을 통해이 경우에 대한 컴파일러 속성을 제공 하도록 로비하는 입니다. (이것은 NSIncrementalStore와 같은 서브 클래스 시나리오에서 컴파일 타임 진단을 가능하게하기 때문에 더 좋습니다.)

이 방법을 선택하는 이유

  1. 효율적이고 다소 편리하게 작업이 완료됩니다.
  2. 이해하기 쉽습니다. (좋아요, __builtin_unreachable()사람들을 놀라게 할 수도 있지만 이해하기도 쉽습니다.)
  3. 어설 션 매크로 중 하나를 기반으로하는 접근 방식과 달리 다른 컴파일러 경고 나 오류를 생성하지 않으면 릴리스 빌드에서 제거 할 수 없습니다.

마지막 요점은 약간의 설명이 필요합니다.

릴리스 빌드에서 일부 (가장?) 사람들이 어설 션을 제거합니다. (나는 그 습관에 동의하지 않지만, 그것은 또 다른 이야기이다 ...) 그러나 필요한 방법을 구현하지 못하는 것은 – 나쁜 , 끔찍한 , 잘못된 , 기본적으로 당신의 프로그램을위한 우주의 끝이다 . 프로그램이 정의되지 않았기 때문에 프로그램이 이와 관련하여 올바르게 작동 할 수 없으며 정의되지 않은 동작이 최악입니다. 따라서 새로운 진단을 생성하지 않고 해당 진단을 제거 할 수 있으면 완전히 수용 할 수 없습니다.

그러한 프로그래머 오류에 대해 적절한 컴파일 타임 진단을 얻을 수없고 런타임시 발견에 의지해야하는 것은 나쁘지만 릴리스 빌드에서이를 극복 할 수 있다면 왜 처음?


사용 @property하고 @dynamic작동 할 수도 있습니다. 동적 속성을 선언하고 일치하는 메서드 구현을 제공하지 않으면 모든 것이 여전히 경고없이 컴파일 unrecognized selector되며 런타임에 액세스하려고하면 런타임에 오류가 발생합니다. 이것은 본질적으로을 호출하는 것과 동일 [self doesNotRecognizeSelector:_cmd]하지만 타이핑이 훨씬 적습니다.


Xcode (clang 등을 사용 __attribute__((unavailable(...)))하여)에서 추상 클래스에 태그를 지정하여 사용하려고하면 오류 / 경고가 발생하는 것을 좋아합니다.

이 방법을 실수로 사용하지 못하도록 보호합니다.

기본 클래스 @interface태그에서 "abstract"메소드 :

- (void)myAbstractMethod:(id)param1 __attribute__((unavailable("You should always override this")));

이 한 단계 더 나아가 매크로를 만듭니다.

#define UnavailableMacro(msg) __attribute__((unavailable(msg)))

이를 통해 다음을 수행 할 수 있습니다.

- (void)myAbstractMethod:(id)param1 UnavailableMacro(@"You should always override this");

내가 말했듯이, 이것은 실제 컴파일러 보호는 아니지만 추상 메소드를 지원하지 않는 언어를 사용하는 것만 큼 좋습니다.


질문에 대한 답변은 이미 주어진 답변 아래 주석에 흩어져 있습니다. 그래서 나는 여기서 요약하고 단순화하고 있습니다.

옵션 1 : 프로토콜

구현하지 않고 추상 클래스를 만들려면 '프로토콜'을 사용하십시오. 프로토콜을 상속받는 클래스는 프로토콜의 메소드를 구현해야합니다.

@protocol ProtocolName
// list of methods and properties
@end

옵션 2 : 템플릿 방법 패턴

"템플릿 메소드 패턴"과 같은 부분 구현으로 추상 클래스를 작성하려면 이것이 솔루션입니다. Objective-C-템플릿 메소드 패턴?


다른 대안

Abstract 클래스와 Assert 또는 Exception에서 클래스를 확인하십시오.

@implementation Orange
- (instancetype)init
{
    self = [super init];
    NSAssert([self class] != [Orange class], @"This is an abstract class");
    if (self) {
    }
    return self;
}
@end

이것은 무시할 필요성을 제거합니다 init


(관련 제안 더)

프로그래머에게 "자식에서 전화하지 마십시오"를 알리고 완전히 재정의하는 방법을 원했습니다 (내 경우에는 확장되지 않은 경우 부모 대신 기본 기능을 여전히 제공합니다).

typedef void override_void;
typedef id override_id;

@implementation myBaseClass

// some limited default behavior (undesired by subclasses)
- (override_void) doSomething;
- (override_id) makeSomeObject;

// some internally required default behavior
- (void) doesSomethingImportant;

@end

장점은 프로그래머가 선언에서 "재정의"를보고 호출하지 않아야한다는 것을 알 수 있다는 것 [super ..]입니다.

물론, 이것에 대해 개별 리턴 유형을 정의해야하는 것은 좋지만 시각적으로 충분한 힌트가되고 서브 클래스 정의에서 "override_"부분을 쉽게 사용할 수 없습니다.

물론 확장이 선택적 일 때 클래스는 여전히 기본 구현을 가질 수 있습니다. 그러나 다른 답변에서 알 수 있듯이 추상 (가상) 클래스와 같이 적절한 경우 런타임 예외를 구현하십시오.

주석과 문서를 파헤 치거나 대신 가정하는 대신 슈퍼의 구현을 사전 / 사후 호출하는 것이 가장 좋은시기에 대한 힌트조차도 이와 같은 컴파일러 힌트를 내장하는 것이 좋습니다.

힌트의 예


다른 언어로 추상 인스턴스화 위반을 포착하는 컴파일러에 익숙하다면 Objective-C 동작이 실망입니다.

늦은 바인딩 언어로서 Objective-C는 클래스가 실제로 추상적인지 아닌지에 대한 정적 결정을 내릴 수 없다는 것이 분명하지만 (런타임에 함수를 추가 할 수 있습니다 ...) 일반적인 사용 사례의 경우 단점으로 보입니다. 컴파일러에서 런타임에 오류를 발생시키는 대신 추상 클래스의 인스턴스화를 방지하는 것이 좋습니다.

다음은 이니셜 라이저를 숨기는 몇 가지 기술을 사용하여 이러한 유형의 정적 검사를 얻는 데 사용하는 패턴입니다.

//
//  Base.h
#define UNAVAILABLE __attribute__((unavailable("Default initializer not available.")));

@protocol MyProtocol <NSObject>
-(void) dependentFunction;
@end

@interface Base : NSObject {
    @protected
    __weak id<MyProtocol> _protocolHelper; // Weak to prevent retain cycles!
}

- (instancetype) init UNAVAILABLE; // Prevent the user from calling this
- (void) doStuffUsingDependentFunction;
@end

//
//  Base.m
#import "Base.h"

// We know that Base has a hidden initializer method.
// Declare it here for readability.
@interface Base (Private)
- (instancetype)initFromDerived;
@end

@implementation Base
- (instancetype)initFromDerived {
    // It is unlikely that this becomes incorrect, but assert
    // just in case.
    NSAssert(![self isMemberOfClass:[Base class]],
             @"To be called only from derived classes!");
    self = [super init];
    return self;
}

- (void) doStuffUsingDependentFunction {
    [_protocolHelper dependentFunction]; // Use it
}
@end

//
//  Derived.h
#import "Base.h"

@interface Derived : Base
-(instancetype) initDerived; // We cannot use init here :(
@end

//
//  Derived.m
#import "Derived.h"

// We know that Base has a hidden initializer method.
// Declare it here.
@interface Base (Private)
- (instancetype) initFromDerived;
@end

// Privately inherit protocol
@interface Derived () <MyProtocol>
@end

@implementation Derived
-(instancetype) initDerived {
    self= [super initFromDerived];
    if (self) {
        self->_protocolHelper= self;
    }
    return self;
}

// Implement the missing function
-(void)dependentFunction {
}
@end

아마도 이런 종류의 상황은 개발 시점에만 발생해야하므로 다음과 같이 작동 할 수 있습니다.

- (id)myMethodWithVar:(id)var {
   NSAssert(NO, @"You most override myMethodWithVar:");
   return nil;
}

@Yar에서 제안한 방법을 사용할 수 있습니다 (일부 수정).

#define mustOverride() @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%s must be overridden in a subclass/category", __PRETTY_FUNCTION__] userInfo:nil]
#define setMustOverride() NSLog(@"%@ - method not implemented", NSStringFromClass([self class])); mustOverride()

여기에 다음과 같은 메시지가 나타납니다.

<Date> ProjectName[7921:1967092] <Class where method not implemented> - method not implemented
<Date> ProjectName[7921:1967092] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[<Base class (if inherited or same if not> <Method name>] must be overridden in a subclass/category'

또는 주장 :

NSAssert(![self respondsToSelector:@selector(<MethodName>)], @"Not implemented");

이 경우 다음을 얻을 수 있습니다.

<Date> ProjectName[7926:1967491] *** Assertion failure in -[<Class Name> <Method name>], /Users/kirill/Documents/Projects/root/<ProjectName> Services/Classes/ViewControllers/YourClass:53

또한 프로토콜 및 기타 솔루션을 사용할 수 있지만 가장 간단한 솔루션 중 하나입니다.


코코아 는 abstract라는 것을 제공하지 않습니다. 런타임시에만 검사되는 클래스 추상을 만들 수 있으며 컴파일 타임에는 검사되지 않습니다.


나는 보통 추상화하려는 클래스에서 init 메소드를 비활성화합니다.

- (instancetype)__unavailable init; // This is an abstract class.

해당 클래스에서 init를 호출 할 때마다 컴파일 타임에 오류가 발생합니다. 그런 다음 다른 모든 것에 클래스 메소드를 사용합니다.

Objective-C에는 추상 클래스를 선언하기위한 기본 제공 방법이 없습니다.


@dotToString의 주석을 적용하여 @redfood가 제안한 내용을 약간 변경하면 실제로 Instagram의 IGListKit에서 채택한 솔루션이 있습니다.

  1. 기본 (추상) 클래스에서 정의 할 수없는 모든 메소드에 대한 프로토콜을 작성하십시오. 즉, 하위에서 특정 구현이 필요합니다.
  2. 이 프로토콜을 구현 하지 않는 기본 (추상) 클래스를 만듭니다 . 이 클래스에 공통 구현이 필요한 다른 메소드를 추가 할 수 있습니다.
  3. 프로젝트의 어느 곳에서나 AbstractClass어떤 방법 으로 자식을 입력하거나 출력 해야하는 경우 AbstractClass<Protocol>대신 입력하십시오 .

AbstractClass이 구현하지 않기 때문에 인스턴스 Protocol를 갖는 유일한 방법 AbstractClass<Protocol>은 서브 클래 싱입니다. 으로 AbstractClass혼자이 프로젝트에 어디서나 사용할 수 없습니다, 그것은 추상적된다.

물론, 이것은 조언을받지 않은 개발자가 단순히를 참조하는 새로운 메소드를 추가하는 것을 막지 않습니다. AbstractClass결국 더 이상은 아닌 추상 클래스의 인스턴스를 허용하게됩니다.

실제 예 : IGListKit 에는 IGListSectionController프로토콜을 구현하지 않는 기본 클래스 IGListSectionType있지만 해당 클래스의 인스턴스가 필요한 모든 메소드는 실제로 type을 요청합니다 IGListSectionController<IGListSectionType>. 따라서 IGListSectionController프레임 워크에 유용한 어떤 유형의 객체도 사용할 수 없습니다 .


실제로 Objective-C에는 추상 클래스가 없지만 프로토콜사용 하여 동일한 효과를 얻을 수 있습니다 . 샘플은 다음과 같습니다.

CustomProtocol.h

#import <Foundation/Foundation.h>

@protocol CustomProtocol <NSObject>
@required
- (void)methodA;
@optional
- (void)methodB;
@end

TestProtocol.h

#import <Foundation/Foundation.h>
#import "CustomProtocol.h"

@interface TestProtocol : NSObject <CustomProtocol>

@end

TestProtocol.m

#import "TestProtocol.h"

@implementation TestProtocol

- (void)methodA
{
  NSLog(@"methodA...");
}

- (void)methodB
{
  NSLog(@"methodB...");
}
@end

추상 클래스를 만드는 간단한 예

// Declare a protocol
@protocol AbcProtocol <NSObject>

-(void)fnOne;
-(void)fnTwo;

@optional

-(void)fnThree;

@end

// Abstract class
@interface AbstractAbc : NSObject<AbcProtocol>

@end

@implementation AbstractAbc

-(id)init{
    self = [super init];
    if (self) {
    }
    return self;
}

-(void)fnOne{
// Code
}

-(void)fnTwo{
// Code
}

@end

// Implementation class
@interface ImpAbc : AbstractAbc

@end

@implementation ImpAbc

-(id)init{
    self = [super init];
    if (self) {
    }
    return self;
}

// You may override it    
-(void)fnOne{
// Code
}
// You may override it
-(void)fnTwo{
// Code
}

-(void)fnThree{
// Code
}

@end

대리인을 만들 수 없습니까?

델리게이트는 어떤 함수를 정의해야하는지 말한다는 점에서 추상 기본 클래스와 비슷하지만 실제로 정의하지는 않습니다.

그런 다음 델리게이트 (예 : 추상 클래스)를 구현할 때마다 컴파일러에서 동작을 정의해야하는 선택적 및 필수 함수에 대해 경고합니다.

이것은 나에게 추상 기본 클래스처럼 들립니다.

참고 URL : https://stackoverflow.com/questions/1034373/creating-an-abstract-class-in-objective-c



반응형