如何将类对象转换为符合协议的对象

7
我将尝试将一个类对象转换为特定协议,该协议定义了该类实现的类方法(+)。
我知道如何使用(id )来完成这个操作,如此问题中所述,但我似乎无法找到正确的方法来处理类对象。
基本情况如下。
我有一个协议:
@protocol Protocol <NSObject>
+ (id)classMethod:(id)arg;
@end

我有一个函数可以接受一个类对象,该函数知道有时根据另一个参数,该类对象符合协议(这显然非常简化)。
- (id)someMethodWithClass:(Class)cls andUseArg:(BOOL)arg
{
    id instance;
    if (arg != nil) {
        instance = [(Class<Protocol>)cls classMethod:arg];
    }
}

现在我没有收到任何警告,看起来这是正确的。(无论如何,我可以保证如果arg!= nil那么类就符合要求,因此我不会看到任何错误。)

然而,在Xcode中我没有得到自动补全提示,这让我怀疑这是否是正确的做法。 有什么想法吗?(请注意,我对instance being id<Protocol>不感兴趣。)


与其将其转换为协议,您应该更改类本身,使其符合该协议(例如,在.h中使用@interface MyClass <MyProtocol>或在.m中使用@interface MyClass () <MyProtocol>)。然后,编译器将允许使用该协议的方法进行自动完成,而无需转换。通常最好修复对象或类定义,而不是强制转换它。另外,通过将类定义为符合特定协议,编译器还会警告您是否未实现某些所需方法的协议。 - Rob
谢谢,但也许我没有表达清楚。cls是一个类对象(Class类型的对象),而不是我定义的类(换句话说,它是作为[SomeClass class]传递的)。在这个简单的例子中,如果arg!= nil,则我知道cls是实现Protocol的类。我只是希望Xcode能够意识到这一点。 - sapi
我非常惊讶你在这里没有得到编译器警告,因为首先你正在将BOOL与指针进行比较(传递nilarg与传递FALSE相同),其次你没有从此方法中返回任何内容。也许您正在使用旧版本的编译器,该编译器不会生成警告,或者您已关闭某些警告。如果argid或指针,则您的构造是有意义的,但如果是BOOL则不是。另外,您没有返回instance。当我编译您的代码时,我会收到这两个警告。 - Rob
那段代码显然不是整个函数,只是前几行。arg原本应该是id,但我复制时弄错了。我唯一担心的是类转换。 - sapi
2个回答

3
如果您想确定 cls 是否符合特定协议(假设 classMethod: 是该协议所需的类方法),您可以简单地:
- (id)someMethodWithClass:(Class)cls andUseArg:(BOOL)arg
{
    id instance;
    if ([cls conformsToProtocol:@protocol(Protocol)]) {
        instance = [cls classMethod:arg];
    }

    return instance;
}

或者,只需查看它是否响应特定的类方法选择器:

- (id)someMethodWithClass:(Class)cls andUseArg:(BOOL)arg
{
    id instance;
    if ([cls respondsToSelector:@selector(classMethod:)]) {
        instance = [cls classMethod:arg];
    }

    return instance;
}

嗨,Rob,我知道cls已经符合给定的协议了(这是函数的前提条件,如果arg不为nil,则cls将符合)。我想要的是一种让阅读我的代码的人明确这一点的方法,而不需要显式地测试符合性(就像我在上面链接的问题中所述)。我只是好奇Class<Protocol>是否是有效的转换,如果不是,那么有效的类将是什么。 - sapi
@sapi Class<Protocol> 是有效的,尽管不必要和不典型。但是如果你想执行这个转换,这种语法是合适的。 - Rob
顺带一提,你所提到的问题确实明确测试符合性(这很好,是防御性编程),并且只将指针转换为协议是为了防止编译器警告。恕我直言,这两个条件在这里都不成立。你要求未来的开发人员查看实现细节以推断应传递哪个类,但却没有优雅地处理他们意外传递错误类的情况。我会主张使用 conformsToProtocolrespondsToSelector(无论是和你的转换一起使用还是代替它)。 - Rob
请恕我直言,我不同意这个观点。如果我为一个函数指定了前置条件,那么我就不会在运行时进行测试(除非使用断言)。如果前置条件未满足,那么该函数或应用程序无法执行任何有意义的操作,因此协议检查的开销是没有用处的。 - sapi
@sapi 好的,我们同意在这个问题上有不同的看法。 :) - Rob

0
问题已经11年了,Rob's answer没有任何问题,但我觉得不幸的是,它的核心部分(关于使用协议转换Class对象是否正确)从未得到适当的关注。
首先,在Objective-C中静态类型很虚假,它存在的唯一目的是让编译器发出警告(甚至不是错误)。让我们从Class对象的真正含义开始 - 如果您查看文档,您会发现Class类型实际上是objc_class *类型的别名:
typedef struct objc_class *Class;

您可以在Apple的objc运行时库源代码中找到objc_class类型的定义:

// inherits objc_object with some adjustments
struct objc_class : objc_object { ... }

正如您所看到的,objc_class只是objc_object的扩展。任何Objective-C类实际上都是这个objc_object的实例。例如,下面是NSObjectid别名的样子:


// "translation" of an Objective-C class declaration
typedef struct objc_object NSObject;

// the same for `id` type but with the pointer type included
typedef struct objc_object *id;

这意味着 Objective-C 中不存在“静态类型”,一个实例的“类型”是通过给定实例的内省(不同种类的元信息 objc_object 存储)来发生的。这使得所有的 Objective-C 类彼此兼容(首先 - 因为它是一个指针,其次 - 因为它是指向相同结构的指针)。例如,您可以编写如下代码:

Class obj = [NSObject new];

..然后它会愉快地编译。

然而,这种语言的纯动态性使其非常容易出错,暴露了程序员可能犯的各种错误。为了避免这种情况,clang实际上对指定类型进行编译时检查,但它完全依赖于程序员提供正确的数据类型,如果从Objective-C的角度来看类型不兼容,则编译器可以为您发出警告。这适用于实例对象,但不幸的是,在Objective-C中没有语法来为类对象打类型,除了使用Class别名。这意味着对于编译器,在编译时所有这些对象都是无法区分的。

并且这一点也适用于协议类型。在这里,我的意思是当您将协议符合标记添加到变量类型(id<TDWLoadable> var)时,您只是要求编译器检查所分配给变量的对象是否符合给定的协议:

@protocol TDWLoadable

+ (void)classMethod;
- (void)instanceMethod;

@end

@interface TDWObject : NSObject
@end

// Initializing '__strong id<TDWLoadable>' with an expression of incompatible type 'TDWObject *'
id<TDWLoadable> loadable = [TDWObject new];

对于类对象,相同的检查被忽略了,因为 Class 对象无法被类型化:

Class<TDWLoadable> loadable = [[TDWObject new] class];

这种行为在《Objective-C编程语言》中协议部分的类型检查章节中有所描述(重点强调)。

...the declaration

id <Formatting> anObject;

groups all objects that conform to the Formatting protocol into a type, regardless of their positions in the class hierarchy. The compiler can make sure only objects that conform to the protocol are assigned to the type.

In each case, the type groups similar objects—either because they share a common inheritance, or because they converge on a common set of methods.

The two types can be combined in a single declaration:

Formatter <Formatting> *anObject;

Protocols can’t be used to type class objects. Only instances can be statically typed to a protocol, just as only instances can be statically typed to a class. (However, at runtime, both classes and instances respond to a conformsToProtocol: message.)

此外,如果我们考虑到objc_class实际上只是objc_object的扩展,那么以下两个表达式:
  • Class<TDWLoadable> classObj;
  • id<TDWLoadable> obj;
应该遵循相同的约定(即+(void)classMethod必须引用classObj的元类,而-(void)instanceMethod则引用类对象本身)。

话虽如此,由于语法本质上没有影响并且被编译器忽略,因此您可以自由地提出自己的约定来进行 Class<Protocol> 类型。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接