`ExpressibleByArrayLiteral`符合类及其超类=>"<superclass>不可转换为<subclass>"

Saj*_*jon 9 uiview ios swift swift-protocols swift3

我希望能够使用数组文字实例化一个子类,这里MyLabel是一个子类,它是一个子类UILabel.我在我的框架ViewComposer中使用它,它允许使用枚举视图的数组来创建UIViews,如下所示:

let label: UILabel = [.text("Hello World"), .textColor(.red)]
Run Code Online (Sandbox Code Playgroud)

在这个问题中,我大大简化了用例,而是允许编写:

let vanilla: UILabel = [1, 2, 3, 4]  // works!
print(vanilla.text!) // prints: "Sum: 10"
Run Code Online (Sandbox Code Playgroud)

我想要做的是使用相同的ExpressibleByArrayLiteral语法,但使用的是被UILabel调用的子类MyLabel.但是当我尝试时,编译器会阻止我:

let custom: MyLabel = [1, 2, 3, 4] // Compilation error: "Could not cast value of type 'UILabel' to 'MyLabel'"
Run Code Online (Sandbox Code Playgroud)

UILabel由于符合Makeable以下自定义协议,使用数组文字的实例化可行.

是否有可能使编译器理解我指的是子类的数组文字初始值设定项MyLabel而不是它的超类UILabel

以下代码可能没有任何逻辑意义,但它是一个最小的例子,隐藏了我真正想要的东西:

// This protocol has been REALLY simplified, in fact it has another name and important code.
public protocol ExpressibleByNumberArrayLiteral: ExpressibleByArrayLiteral {
    associatedtype Element
}

public protocol Makeable: ExpressibleByNumberArrayLiteral {
    // we want `init()` but we are unable to satisfy such a protocol from `UILabel`, thus we need this work around
    associatedtype SelfType
    static func make(values: [Element]) -> SelfType
}

public protocol Instantiatable: ExpressibleByNumberArrayLiteral {
    init(values: [Element])
}

// My code might have worked if it would be possible to check for _NON-conformance_ in where clause
// like this: `extension Makeable where !(Self: Instantiatable)`
extension Makeable {
    public init(arrayLiteral elements: Self.Element...) {
        self = Self.make(values: elements) as! Self
    }
}

extension Instantiatable {
    init(arrayLiteral elements: Self.Element...) {
        self.init(values: elements)
    }
}

extension UILabel: Makeable {
    public typealias SelfType = UILabel
    public typealias Element = Int

    public static func make(values: [Element]) -> SelfType {
        let label = UILabel()
        label.text = "Sum: \(values.reduce(0,+))"
        return label
    }
}

public class MyLabel: UILabel, Instantiatable {
    public typealias Element = Int
    required public init(values: [Element]) {
        super.init(frame: .zero)
        text = "Sum: \(values.reduce(0,+))"
    }

    public required init?(coder: NSCoder) { fatalError() }
}

let vanilla: UILabel = [1, 2, 3, 4]
print(vanilla.text!) // prints: "Sum: 10"

let custom: MyLabel = [1, 2, 3, 4] // Compilation error: "Could not cast value of type 'UILabel' to 'MyLabel'"
Run Code Online (Sandbox Code Playgroud)

我也尝试ExpressibleByArrayLiteral通过扩展ExpressibleByNumberArrayLiteral来符合协议(我怀疑这两个解决方案可能是等效的,并编译为相同的代码..),如下所示:

extension ExpressibleByNumberArrayLiteral where Self: Makeable {
    public init(arrayLiteral elements: Self.Element...) {
        self = Self.make(values: elements) as! Self
    }
}

extension ExpressibleByNumberArrayLiteral where Self: Instantiatable {
    init(arrayLiteral elements: Self.Element...) {
        self.init(values: elements)
    }
}
Run Code Online (Sandbox Code Playgroud)

但这也不起作用.发生相同的编译错误.

我在上面的大代码块中写了一个注释,编译器可能已经能够确定我指的是哪个数组文字初始化器,如果我能够在where子句中使用否定: extension Makeable where !(Self: Instantiatable)

但AFAIK是不可能的,那个代码至少不能编译.也没有extension Makeable where Self != Instantiatable.

我想做什么?

我不得不做MyLabel一个final class.但这没有任何区别.

请说请说这是可能的.

jlm*_*rph 7

在浏览Apple Docs后,我最初认为这是不可能的.但是,我确实在这里发现了一个帖子,它适用于Strings和其他非UI类.从帖子中我合并了一个想法,你不能通过继承方法将ExpressibleByArrayLiteral应用于子类,这可能是你得到错误的原因(我可以用许多其他方法重现多次).

最后,通过将ExpressibleByArrayLiteral直接移植到您的UILabel子类上,它似乎正在工作!

public class MyLabel: UILabel, ExpressibleByArrayLiteral {

    public typealias Element = Int

    public override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required public init(values: [Element]) {
        super.init(frame: .zero)
        text = "Sum: \(values.reduce(0,+))"
    }


    public convenience required init(arrayLiteral: Element...) {
        self.init()
        self.text = "Sum: \(arrayLiteral.reduce(0,+))"
    }


    public required init?(coder: NSCoder) { fatalError() }
}

let vanilla: MyLabel = [1, 2, 3, 4]

print(vanilla.text) // prints Sum: 10 
Run Code Online (Sandbox Code Playgroud)

事实证明,我们甚至无法在元素类型类(字符串,Ints等)上继承Expressibility,您仍然需要为它重新指定初始化程序.

通过一些调整,我还应用了其他方法进行思考!

public class MyLabel: UILabel, ExpressibleByArrayLiteral {

    public typealias Element = Int

    private var values : [Element]?

    public var append : Element? {
        didSet {
            if let t = append {
                values?.append(t)
            }
        }
    }

    public var sum : Element {
        get {
            guard let s = values else {
                return 0
            }
            return s.reduce(0,+)
        }
    }

    public var sumString : String {
        get {
            return "\(sum)"
        }
    }

    public var label : String {
        get {
            guard let v = values, v.count > 0 else {
                return ""
            }
            return "Sum: \(sumString)"
        }
    }

    public override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required public init(values: [Element]) {
        super.init(frame: .zero)
        text = "Sum: \(values.reduce(0,+))"
    }


    public convenience required init(arrayLiteral: Element...) {
        self.init()
        self.values = arrayLiteral
        self.text = label
    }


    public required init?(coder: NSCoder) { fatalError() }
}

let vanilla: MyLabel = [1, 2, 3, 4]

print(vanilla.label) //prints out Sum: 10 , without unwrapping ;)
Run Code Online (Sandbox Code Playgroud)

就目前而言,我似乎无法将Expressibility应用于您所采用的协议方法.但是,随着变通办法的出现,这似乎可以解决问题.现在猜猜我们只需要将初始化器应用于每个子类.不幸的是,但还是值得一看!

用扩展方法重新确认问题的最新情况

Swift的继承禁止方便,因为它不能保证超类不会被戏剧性地改变.虽然您的init不会更改UILABEL的属性,但扩展的严格类型不支持此类初始化程序所需和方便的组合.

我从这篇文章中拿到了这篇文章,它包含在上面的链接中:

因为这是一个非最终类.考虑是否有一个Stack的子类具有自己的必需初始化程序.你如何确保init(arrayLiteral :)调用它?它不能称之为(因为它不知道它存在).因此必须要求init(arrayLiteral :)(这意味着它需要是主声明的一部分而不是扩展),或者Stack必须是最终的.

如果你把它标记为最终,这就像你期望的那样.如果您希望将其子类化,只需将其移出扩展并进入主体.

如果我们看看你得到的两个错误,只需尝试ExpressibleByArrayLiteral直接扩展UILabel ,而不是通过嵌套的协议网络,就像你正在做的那样:

Initializer requirement 'init(arrayLiteral:)' can only be satisfied by a 'required' initializer in the definition of non-final class 'UILabel'

'required' initializer must be declared directly in class 'UILabel' (non in an extension).
Run Code Online (Sandbox Code Playgroud)

首先.ExpressibleByArrayLiteral需要一个'required'初始化方法来符合它,编译器说:你不能直接在你希望定制的超类的扩展内部实现.不幸的是,单凭这个逻辑......你想要的方法是有缺陷的.

第二.你想要的非常具体的初始化程序'init(arrayLiteral :),用于最终类型的类.就像在你的声明标题中用关键字FINAL标记的类,或类TYPES(字符串是一个,数字类也是如此).UILabel根本不是允许子类化的最终类,你不能在不破坏语言的情况下改变它.为了说明非final和final,尝试子类String,你将得到一个错误,因为它不是一个classprotocol.这不会让你通过App Store无论如何.因此,通过DESIGN,您无法通过扩展在UILabel上使用此方法.

三.你采用自定义协议方法,并试图UILabel通过扩展和继承来应用它.我道歉,但Swift只是因为你在constrtaint的两端之间分层一些自定义代码而忽略了它的语言结构.你的protocol方法尽管很优雅,但它只是将问题嵌套在这里,而不是解决问题.这是因为它只是将这些初始化约束重新应用到UILabel而不管您自己的中间件.

第四.在这里有一点逻辑思路.如果你看看苹果的文档上Expressibles的XCode(代码文件本身)的内部,你会注意到协议特别适用于RawRepresentable类和类型(Strings,Ints,Doubles等):

对于具有字符串,整数或浮点原始类型的任何枚举,Swift编译器会自动添加RawRepresentable 一致性.定义自己的自定义枚举时,通过将原始类型指定为枚举类型继承列表中的第一项来为其指定原始类型.您还可以使用文字来指定一个或多个案例的值.

所以任何数据表示作为根级别的东西都class可以采用这个extension.你可以清楚地看到上面的内容,当你protocol立即添加它时UILabel,你也会将RawRepresentable协议强加给它.它不能采用我的本性,我愿意打赌是"MyLabel无法转换为UILabel"错误的来源.UILabel它不是这些东西中的一个,这就是它获得这个non-final class属性的原因:它是一个UI元素,它是许多RawRepresentables的组合.因此,你不应该直接初始化一个RawRepresentables集合的类是有道理的,因为如果在init编译器端发生了一些混乱,并且它没有改变你想要的Raw类型,它可能只是完全破坏了类实例并将每个人都调整为调试噩梦.

为了说明RawRepresentable我试图让一点,这里是当你将这种方法用于会发生什么String,一个RawRepresentable-conforming类型:

extension String: ExpressibleByArrayLiteral {
    public typealias Element = Int

    public init(arrayLiteral elements: Element...) {
        self.init()
        self = "Sum: \(elements.reduce(0,+))"//WE GET NO ERRORS
    }

}


let t : String = [1,2,3,4]

print(t) // prints -> Sum: 10
Run Code Online (Sandbox Code Playgroud)

而...

extension UILabel: ExpressibleByArrayLiteral {
    public typealias Element = Int

    public convenience required init(arrayLiteral elements: Element...) { //The compiler even keeps suggesting you add the method types here illogically without taking to account what is there.. ie: it's confused by what you're trying to do..
        self.init()
        self.text = "Sum: \(elements.reduce(0,+))" //WE GET THE ERRORS
    }

}

//CANNOT PRINT.....
Run Code Online (Sandbox Code Playgroud)

我甚至会演示约束在UILabel子类上添加Expressibles的程度,以及它的第二层子类:

class Label : UILabel, ExpressibleByArrayLiteral {

    public typealias Element = Int

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    public required init(arrayLiteral elements: Element...) {
        super.init(frame: .zero)
        self.text = "Sum: \(elements.reduce(0,+))"
    }

    public required init?(coder: NSCoder) { fatalError() }
}

let te : Label = [1,2,3,4]

print(te.text!) //prints:  Sum: 10

class SecondLabel : Label {

    public typealias Element = Int


    required init(arrayLiteral elements: Element...) {
        //THIS IS STILL REQUIRED... EVEN THOUGH WE DO NOT NEED TO MANUALLY ADOPT THE PROTOCOL IN THE CLASS HEADER
        super.init(frame: .zero)
        self.text = "Sum: \(elements.reduce(0,+))"
    }

    public required init?(coder: NSCoder) { fatalError() }

}

let ta : SecondLabel = [1,2,3,4]

print(ta.text!) //prints:  Sum: 10
Run Code Online (Sandbox Code Playgroud)

结论.Swift就是这样设计的.你不能直接将这个特定的协议应用到它上面,因为它UILabel是一个语言级别的超类,并且提出这些东西的人不希望你有这么多的覆盖范围UILabel.因此,您根本无法做到这一点,因为non-final由于UILabel协议本身的性质,该协议无法通过超类的扩展来应用.它们只是不兼容这种方式.但是,您可以基于每个子类在其子类上应用它.这意味着您每次都必须重新声明符合标准的初始值设定项.太糟糕了!但它只是它的工作原理.

我推荐你的方法,似乎差不多把扩展方法降到了T.但是,似乎有一些关于如何构建Swift的东西,你无法避免.我不是唯一一个肯定这个结论的人(只是查看链接),所以我会要求你删除你的downvote.你有一个我给你的解决方案,我已经给你提供了验证我的观点的参考资料,并且我还提供了代码来向你展示如何在语言的本质中纠正这种约束.有时候,没有解决方案.而其他时候,另一种方法是解决问题的唯一方法.


Saj*_*jon 3

我最终得到了使用后缀运算符的解决方案。

由于我需要能够UILabel使用 来实例化 UIKits ExpressibleByArrayLiteral,因此我无法将 murphguys 提出的解决方案与Label和 一起使用SecondLabel

我的原始代码通过添加此后缀运算符来工作:

postfix operator ^
postfix func ^<I: Instantiatable>(attributes: [I.Element]) -> I {
    return I(values: attributes)
}
Run Code Online (Sandbox Code Playgroud)

这使得代码可以编译并运行。虽然感觉有点“黑”……

let custom: MyLabel = [1, 2, 3, 4]^ // note use of caret operator. Now compiles
print(custom.text!) // prints "Sum 10"
Run Code Online (Sandbox Code Playgroud)

如果您对我使用此功能的原因和方式感兴趣,您可以查看我的框架 ViewComposer,它支持此语法:

let label: UILabel = [.text("Hello World"), .textColor(.red)]
Run Code Online (Sandbox Code Playgroud)

但我也希望能够创建我自己的Composable子类MyLabel或只是Label..)

let myLabel: MyLabel = [.text("Hello World"), .textColor(.red)] // does not compile
Run Code Online (Sandbox Code Playgroud)

之前不起作用,但现在使用插入符后缀运算符可以工作^,如下所示:

let myLabel: MyLabel = [.text("Hello World"), .textColor(.red)]^ // works!
Run Code Online (Sandbox Code Playgroud)

目前我认为这是最优雅的解决方案。