如何将类类型作为函数参数传递

169
我有一个通用函数,调用Web服务并将JSON响应序列化回对象。
class func invokeService<T>(service: String, withParams params: Dictionary<String, String>, returningClass: AnyClass, completionHandler handler: ((T) -> ())) {

            /* Construct the URL, call the service and parse the response */
}

我将尝试实现的目标是与此Java代码等效。
public <T> T invokeService(final String serviceURLSuffix, final Map<String, String> params,
                               final Class<T> classTypeToReturn) {
}
  • 我尝试实现的方法签名是否正确?
  • 更具体地说,将AnyClass指定为参数类型是否正确?
  • 在调用该方法时,我将MyObject.self作为返回类传递给它,但是我得到了编译错误"无法将表达式的类型“()”转换为类型“String”"
CastDAO.invokeService("test", withParams: ["test" : "test"], returningClass: CityInfo.self) { cityInfo in /*...*/

}

编辑:

我尝试使用 holex 提到的 object_getClass,但是现在我得到了以下错误信息:

错误:"类型 'CityInfo.Type' 未遵循协议 'AnyObject'"

需要做什么来遵循该协议?

class CityInfo : NSObject {

    var cityName: String?
    var regionCode: String?
    var regionName: String?
}

我认为Swift的泛型并不像Java那样工作。因此类型推断器可能就不那么聪明了。我会省略Class<T>,而是显式地指定泛型类型,并编写以下代码CastDAO.invokeService("test", withParams: ["test" : "test"]) { (ci:CityInfo) in } - Christian Dietrich
我曾经处于你的情况,每个人都写了同样的话——这是不可能完成的。但是,如果你采用这里所有写下的答案,你就可以实现它。请看我的回答:https://dev59.com/mWAf5IYBdhLWcg3wwk4V#68930681 - Shaybc
7个回答

157
你的方法不正确:在Swift中,与Objective-C不同,类有特定的类型,甚至还有继承层次结构(即如果类BA继承,则B.Type也从A.Type继承):
class A {}
class B: A {}
class C {}

// B inherits from A
let object: A = B()

// B.Type also inherits from A.Type
let type: A.Type = B.self

// Error: 'C' is not a subtype of 'A'
let type2: A.Type = C.self

这就是为什么你不应该使用AnyClass,除非你真的想允许任何类。在这种情况下,正确的类型应该是T.Type,因为它表达了returningClass参数和闭包参数之间的链接。
实际上,使用T.Type而不是AnyClass可以让编译器正确推断方法调用中的类型:
class func invokeService<T>(service: String, withParams params: Dictionary<String, String>, returningClass: T.Type, completionHandler handler: ((T) -> ())) {
    // The compiler correctly infers that T is the class of the instances of returningClass
    handler(returningClass())
}

现在的问题是构建一个实例T以传递给handler: 如果您尝试运行代码,编译器将抱怨T不能用()构造。这是合理的:必须显式约束T要求它实现特定的初始化程序。
可以使用以下协议来实现这一点:
protocol Initable {
    init()
}

class CityInfo : NSObject, Initable {
    var cityName: String?
    var regionCode: String?
    var regionName: String?

    // Nothing to change here, CityInfo already implements init()
}

然后您只需要将invokeService的通用约束从<T>更改为<T: Initable>

提示

如果出现奇怪的错误,例如“无法将表达式的类型'()'转换为类型'String'”,则通常将方法调用的每个参数移动到自己的变量中会很有用。 它有助于缩小导致错误的代码范围并揭示类型推断问题:

let service = "test"
let params = ["test" : "test"]
let returningClass = CityInfo.self

CastDAO.invokeService(service, withParams: params, returningClass: returningClass) { cityInfo in /*...*/

}

现在有两种可能性:错误会移动到其中一个变量(这意味着错误部分在那里),或者您会收到一条加密的消息,例如“无法将表达式的类型()转换为类型($T6) -> ($T6) -> $T5”。
后一种错误的原因是编译器无法推断您所写内容的类型。在这种情况下,问题在于T仅用于闭包的参数中,而您传递的闭包没有指示任何特定类型,因此编译器不知道要推断什么类型。通过更改returningClass的类型以包括T,您可以为编译器提供确定通用参数的方法。

感谢 T.Type 提示 - 这正是我需要将类类型作为参数传递的。 - Echelon
不必使用<T:Initiable>模板函数,也可以将returningClass作为Initiable.Type传递。 - cyril94440
在原始代码中,这将产生不同的结果。通用参数 T 用于表示 returningClass 和传递给 completionHandler 的对象之间的关系。如果使用 Initiable.Type,则会丢失此关系。 - EliaCereda
AND?泛型不允许编写 func somefunc<U>() - Gargo
Gargo,你指的是什么? - EliaCereda
显示剩余2条评论

39

您可以通过以下方式获得AnyObject的类:

Swift 3.x

let myClass: AnyClass = type(of: self)

Swift 2.x

let myClass: AnyClass = object_getClass(self)

如果您愿意,稍后可以将其作为参数传递。


1
你也可以使用 self.dynamicType - newacct
1
每当我尝试使用myClass时,它都会引发“使用未声明的类型”myClass”的错误。Swift 3,Xcode和iOS的最新版本。 - Confused
@Confused,我没有看到任何问题,你可能需要给我更多关于上下文的信息。 - holex
这是我在这里使用的方法和思维方式的延续:http://stackoverflow.com/questions/41092440/spritekit-start-button-that-calls-start-in-the-scene-its-in。我已经通过协议实现了一些逻辑,但很快就会面临一个事实,即我还需要弄清关联类型和泛型,以获得我认为可以用更简单的机制实现的东西。或者只是大量复制和粘贴代码。这正是我通常做的事情 ;) - Confused
@Confused,你可能需要在你的帖子中加入一些代码来扩展你的问题... 你能做到吗? - holex
显示剩余5条评论

14

我在Swift5中有一个类似的用例:

class PlistUtils {

    static let shared = PlistUtils()

    // write data
    func saveItem<T: Encodable>(url: URL, value: T) -> Bool{
        let encoder = PropertyListEncoder()
        do {
            let data = try encoder.encode(value)
            try data.write(to: url)
            return true
        }catch {
            print("encode error: \(error)")
            return false
        }
    }

    // read data

    func loadItem<T: Decodable>(url: URL, type: T.Type) -> Any?{
        if let data = try? Data(contentsOf: url) {
            let decoder = PropertyListDecoder()
            do {
                let result = try decoder.decode(type, from: data)
                return result
            }catch{
                print("items decode failed ")
                return nil
            }
        }
        return nil
    }

}


我试图做类似的事情,但是在调用站点上出现了错误:Static method… requires that 'MyDecodable.Type' conform to 'Decodable'。你介意更新一下你的回答,给出一个调用loadItem的示例吗? - Barry Jones
原来这就是我需要学习.Type.self(类型和元类型)之间区别的时候了。 - Barry Jones

4

只需将每个代码复制粘贴到Swift文件中:

# 另存为:APICaller.swift

import Foundation

struct APICaller
{
    public static func get<T: Decodable>(url: String, receiveModel: T.Type, completion:@escaping (Decodable) -> ())
    {
        send(url: url, json: nil, receiveModel: receiveModel, completion: completion, httpMethod: "GET")
    }
    
    public static func post<T: Decodable>(url: String, json: [String: Any]?, receiveModel: T.Type, completion:@escaping (Decodable) -> ())
    {
        send(url: url, json: nil, receiveModel: receiveModel, completion: completion, httpMethod: "POST")
    }
    
    public static func delete<T: Decodable>(url: String, json: [String: Any]?, receiveModel: T.Type, completion:@escaping (Decodable) -> ())
    {
        send(url: url, json: nil, receiveModel: receiveModel, completion: completion, httpMethod: "DELETE")
   }

    private static func send<T: Decodable>(url: String, json: [String: Any]?, receiveModel: T.Type, completion:@escaping (Decodable) -> (), httpMethod: String)
    {
        // create post request
        let urlURL: URL = URL(string: url)!
        var httpRequest: URLRequest = URLRequest(url: urlURL)
        httpRequest.httpMethod = httpMethod
        
        if(json != nil)
        {
            // serialize map of strings to json object
            let jsonData: Data = try! JSONSerialization.data(withJSONObject: json!)
            // insert json data to the request
            httpRequest.httpBody = jsonData
            httpRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
        }
        
        // create an asynchronus task to post the request
        let task = URLSession.shared.dataTask(with: httpRequest)
        { jsonData, response, error in
            // on callback parse the json into the receiving model object
            let receivedModelFilled: Decodable = Bundle.main.decode(receiveModel, from: jsonData!)

            // cal the user callback with the constructed object from json
            DispatchQueue.main.async {
                completion(receivedModelFilled)
            }
        }
        task.resume()
    }
}

# 另存为:TestService.swift

import Foundation

struct TestService: Codable
{
    let test: String
}

那么你可以像这样使用它:

let urlString: String = "http://localhost/testService"  <--- replace with your actual service url

// call the API in post request
APICaller.post(url: urlString, json: ["test": "test"], receiveModel: TestService.self, completion: { testReponse in
    // when response is received - do something with it in this callback
    let testService: TestService = testReponse as! TestService
    print("testService: \(testService)")
})

提示: 我使用在线服务将我的JSON转换为Swift文件,这样我只需要编写调用和处理响应的代码了。 我使用的是https://app.quicktype.io,但你可以搜索你喜欢的服务。


3

Swift 5

虽然不完全相同,但我遇到了类似的问题。最终帮助我的是这个:

func myFunction(_ myType: AnyClass)
{
    switch myType
    {
        case is MyCustomClass.Type:
            //...
            break

        case is MyCustomClassTwo.Type:
            //...
            break

        default: break
    }
}

然后,您可以在该类的实例中像这样调用它:

myFunction(type(of: self))

希望这能帮助到和我处境相同的人。

2

最近我在寻找一种方法,让我的UINavigationController除了子视图按钮以外不可见。我将以下代码放入自定义导航控制器中:

// MARK:- UINavigationBar Override
private extension UINavigationBar {
    
    override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        // Make the navigation bar ignore interactions unless with a subview button
        return self.point(inside: point, with: event, type: UIButton.self)
    }
    
}

// MARK:- Button finding hit test
private extension UIView {
    
    func point<T: UIView>(inside point: CGPoint, with event: UIEvent?, type: T.Type) -> Bool {
        
        guard self.bounds.contains(point) else { return false }
        
        if subviews.contains(where: { $0.point(inside: convert(point, to: $0), with: event, type: type) }) {
            return true
        }

        return self is T
    }
    
}

不要忘记使用边界而不是框架,因为在调用之前会转换点。


1
使用 obj-getclass
CastDAO.invokeService("test", withParams: ["test" : "test"], returningClass: obj-getclass(self)) { cityInfo in /*...*/

}

假设self是一个城市信息对象。

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