Swift- variable not initialized before use (but it

2019-01-27 17:17发布

问题:

Currently I've got some swift code like this:

class C {
   let type: Type;
   var num = 0;
   init() {
       self.type = Type({ (num: Int) -> Void in
           self.num = num;
       });
   }
}

The Swift compiler refuses to permit it, saying that I've referenced self.type before it's initialized, even though that's clearly completely untrue. Furthermore, I can't employ the workaround found in other questions/answers, because the type is not optional, and it's immutable, so it can't be initialized with nil pointlessly first.

How can I make the Swift compiler accept this perfectly valid code?

This has nothing to do with returning from the initializer early. The callback is executed asynchronously- it is stored and then used later.

I also have a few further lets that are initialized after this one. I would have to turn them all into mutable optionals, even though they're not optional and can't be mutated.

回答1:

This works:

class C {
    var type: Type?;
    var num = 0;
    init() {
        self.type = Type({ (num: Int) -> Void in
            self.num = num;
        });
    }
}

I assume you knew that. But you want to know why your version isn't working.

Now for the tricky part: for the line

self.num = num;

to work, the compiler has to pass self to inside the closure. The closure could be and probably is executed inside of the constructor of Type.

This is as if you had written

self.type = Type({ (self: C, num: Int) -> Void in
    self.num = num    
});

which is syntactically wrong but explains what the compiler has to do to compile your code.

To pass this necessary instance of self to the constructor of Type, self has to be initialized. But self isn't initialized, because you are still in the constructor.

The compiler tells you which part of self is not initialized, when you try to pass self to the constructor of Type.

P.S.

obviously Type knows num in your code. If you want to use let in C instead of var you could do...

class Type {
    let num: Int
    init () {
        num = 3
    }
}
class C {
    let type: Type;
    var num = 0;
    init() {
        self.type = Type();
        num = type.num
    }
}

or even

class C {
    let type: Type;
    var num: Int {
        return type.num
    }
    init() {
        self.type = Type();
    }
}

depending on whether you want num to change or not. Both examples compile without error.



回答2:

First, it's important to explain why this is not perfectly valid code, and that it isn't clear at all that self.type is not used before it is initialized. Consider the following extension of your code:

struct A {
    init(_ f: (Int) -> Void) { f(1) }
}

class C {
    let type: A
    var num = 0 {
        didSet { print(type) }
    }
    init() {
        self.type = A({ (num: Int) -> Void in
            self.num = num
        })
    }
}

If you walk through the logic, you'll note that self.type is accessed via print before it has been initialized. Swift can't currently prove that this won't happen, and so doesn't allow it. (A theoretical Swift compiler might prove that it wouldn't happen for some particular cases, but for most non-trivial code it would likely bump into the halting problem. In any case, the current Swift compiler isn't powerful enough to make this proof, and it's a non-trivial proof to make.)

One solution, though somewhat unsatisfying, is to use implicitly unwrapped optionals:

private(set) var type: A! = nil

Except for the declaration, every other part of the code is the same. You don't have to treat it as optional. In practice, this just turns off the "used before initialization" checks for this variable. It also unfortunately makes it settable inside of the current file, but does make it immutable to everyone else.

This is the technique I've most often used, though often I try to rework the system so that it doesn't require this kind of closure (not always possible, but I often rack my brain to try). It's not beautiful, but it is consistent and bounds the ugly.

Another technique that can work in some cases is laziness:

class C {
    lazy var type: A = {
        A({ (num: Int) -> Void in self.num = num })}()
    var num = 0
    init() {}
}

Sometimes that works, sometimes it doesn't. In your case it might. When it does work, it's pretty nice because it makes the property truly immutable, rather than just publicly immutable, and of course because it doesn't require !.



回答3:

Interesting.

It looks like the error goes away if you avoid referencing self inside the closure.

If the callback is synchronous you can change your code as follow:

class C {
    let type: Type
    var num = 0
    init() {
        var numTemp = 0 // create a temporary local var
        let initialType = Type({ (num: Int) -> () in
            numTemp = num // avoid self in the closure
        });
        self.type = initialType
        self.num = numTemp
    }
}

Important: this will NOT work if the closure is async.

Tested with Xcode (Playground) 6.4 + Swift 1.2

Hope this helps.



回答4:

As appzYourLife said, a temporary variable for num will suffice:

class Type{
    var y: (Int)->Void
    init(y2:((Int)->Void)){
        self.y = y2
    }
}

class C {
    let type: Type
    var num: Int = 0
    init() {
        var num2 = 0 
        self.type = Type(y2: { (num3: Int) -> () in
            num2 = num3
        });
        self.num = num2
    }
}

However, you do not need a temporary variable for type, this error message is misleading.