How can I convert between related types through a common initializer?

前端 未结 1 552
孤独总比滥情好
孤独总比滥情好 2020-12-11 11:08

I\'m trying to build up a family of types that can be converted to each other. For example, Float and Double can be converted to each other through their initializers. I\'

相关标签:
1条回答
  • 2020-12-11 11:35

    The problem is that in your init(_ x:FloatConvertible), Swift cannot infer what the concrete type of x is. It just knows that it's a FloatConvertible. Therefore when you try to do Self(x), while it can infer the concrete type of Self, it doesn't know which initialiser you want to call, meaning that it will default to your init(_ x:FloatConvertible) initialiser, thus creating an infinite loop.

    If you give your custom initialiser an argument name, you'll see that Swift complains that it can't find the correct initialiser:

    protocol FloatConvertible {
        init(c x:FloatConvertible)
    }
    
    extension FloatConvertible {
        init(c x:FloatConvertible) {
            // error: missing argument name 'c:' in call
            // (i.e it can't find the concrete type's initialiser)
            self.init(Self(x)) 
        }
    }
    

    A potential solution therefore is to resolve this at runtime by switching over the concrete types that x could be. However this isn't nearly as good as resolving this statically, as you can benefit from increased safety and in some cases increased performance.

    In order to do this statically, you could add a generic _asOther 'shadow' function to your protocol that can convert a given floating point type to another, as well as adding the concrete type's initialisers to your protocol requirement.

    This will save you from having to list out all the possible combinations of conversions – you can now just invoke _asOther from your initialiser.

    protocol FloatConvertible {
        init(_ other:Float)
        init(_ other:Double)
        init(_ other:CGFloat)
        init(fromOther x:FloatConvertible)
    
        func _asOther<T:FloatConvertible>() -> T
    }
    
    extension FloatConvertible {
        init(fromOther x:FloatConvertible) {self = x._asOther()}
    }
    
    // note that we have to implement these for each extension,
    // so that Swift uses the concrete types of self, preventing an infinite loop
    extension Float : FloatConvertible {
        func _asOther<T:FloatConvertible>() -> T {return T(self)}
    }
    
    extension Double : FloatConvertible {
        func _asOther<T:FloatConvertible>() -> T {return T(self)}
    }
    
    extension CGFloat : FloatConvertible {
        func _asOther<T:FloatConvertible>() -> T {return T(self)}
    
        // note that CGFloat doesn't implement its own initialiser for this,
        // so we have to implement it ourselves
        init(_ other:CGFloat) {self = other}
    }
    
    func transmute<T:FloatConvertible, U:FloatConvertible>(value: T, to: U.Type) -> U {
        return U(fromOther: value)
    }
    
    let f = transmute(value: CGFloat(2.6), to: Float.self)
    print(type(of: f), f) // prints: Double 2.59999990463257
    

    In the initialiser, _asOther will be called on the input value, with the type of self being inferred for the generic parameter T (in this context self is guaranteed to be a concrete type). The _asOther function will then get called on x, which will return the value as the given destination type.

    Note that you don't have to use the fromOther: argument label for your custom initialiser – this will still work without any label. Although I would strongly advocate for using it to catch any problems with your code at compile time (Swift would accept code that would cause infinite loops at runtime otherwise).


    Also as a side note, you should maybe re-think your design for how you want your * overload to work. It would make more sense to be returning the more precise type that you input into it (i.e Float * Double = Double) – otherwise you're just needlessly losing precision.

    0 讨论(0)
提交回复
热议问题