Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.0k views
in Technique[技术] by (71.8m points)

swift - Why is generic specialization lost inside a generic function

When I create a computed property that depends on a generic type, the specific implementation is "lost" when the instance is passed in a generic function.

For example, I added the isBool on Array that returns true if Array.Element is Bool:

extension Array {
    var isBool: Bool {
        false
    }
}

extension Array where Element == Bool {
    var isBool: Bool {
        true
    }
}

Using it directly on the instance works fine

let boolArray: [Bool] = [true, false]
let intArray: [Int] = [1, 0]

boolArray.isBool // true
intArray.isBool // false

But inside a generic function it always uses the non specialized implementation:

func isBool<Element>(_ array: [Element]) -> Bool {
    array.isBool
}

isBool(boolArray) // false, instead of true
isBool(intArray) // false

This is not a real use case so I don't really need a way to "fix" this, but I would like to understand why it behave like that.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Specialization is not a replacement for inheritance. It should be used to improve performance, not change behavior.

For example, distance(from:to:) is usually O(k), where k is the distance. For RandomAccessCollection it can be performed in O(1) due to a specialization. But the result is the same either way.

Specialization is done at compile-time based on the information the compiler has. In your example, the compiler can see that boolArray is a [Bool], and so it uses the specialized extension. But inside of isBool, all that the compiler knows is that array is an Array. It doesn't know when compiling the function what kind of Array will be passed. So it picks the more general version to cover all cases.

(The compiler may create multiple versions of isBool in the binary for optimization purposes, but luckily I haven't found any situations where this impacts what extensions or overloads are called. Even if it actually creates an inlined, Bool-specific version of isBool, it will still use the more general Array extension. That's a good thing.)

Leaving your extensions in place, the following would do what you expect (though I don't encourage this):

func isBool<Element>(_ array: [Element]) -> Bool {
    array.isBool
}

func isBool(_ array: [Bool]) -> Bool {
    array.isBool
}

Now isBool is overloaded and the most specific one will be selected. Within the context of the second version, array is known to be [Bool], and so the more specialized extension will be selected.

Even though the above works, I would strongly recommend against using specialized extensions or ambiguous overloads that change behavior. It is fragile and confusing. If isBool() is called in the context of another generic method where Element is not known, it again may not work as you expect.

Since you want to base this on the runtime types, IMO you should query the type at runtime using is. That gets rid of all the ambiguity. For example:

extension Array {
    var isBool: Bool { Element.self is Bool.Type }
}

func isBool<Element>(_ array: [Element]) -> Bool {
    array.isBool
}

You can make this much more flexible and powerful by adding a protocol:

protocol BoolLike {}
extension Array {
    var isBool: Bool { Element.self is BoolLike.Type }
}

Now, any types you want to get "bool-like" behavior just need to conform:

extension Bool: BoolLike {}

This allows you all the flexibility of your extensions (i.e. the isBool code doesn't need to know all the types), while ensuring the behavior is applied based on runtime types rather than compile-time types.


Just in case it comes up, remember that protocols do not conform to themselves. So [BoolLike] would return isBool == false. The same is true for an extension with where Element: BoolLike. If you need that kind of thing to work, you need to deal with it explicitly.

extension Array {
    var isBool: Bool {
        Element.self is BoolLike.Type || Element.self == BoolLike.self
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...