"'let' property may not be initialized directly" in internal struct init

I'm having a bit of a strange issue with one of my struct initializers.

I have an initializer which assigns every property of the struct, init<EncodeSet>(byte: UInt8, encodeSet: EncodeSet). Then I split one branch of it in to a second initializer, init(percentEncoded byte: UInt8). However, when I replace that branch in the original initializer with a call to that second initializer, the compiler starts complaining about the other branch, telling me that I'm not allowed to set these properties directly.

public protocol PercentEncodeSet {
    func shouldPercentEncode(ascii codePoint: UInt8) -> Bool
}

internal struct _EncodedByte {

  internal let byte: UInt8
  internal let count: UInt8
  internal let isEncodedOrSubstituted: Bool

  internal init<EncodeSet>(byte: UInt8, encodeSet: EncodeSet) where EncodeSet: PercentEncodeSet {
    if !encodeSet.shouldPercentEncode(ascii: byte) {
      self.byte = byte   // โŒ 'let' property 'byte' may not be initialized directly; use "self.init(...)" or "self = ..." 
      self.count = 1     // โŒ 'let' property 'count' may not be initialized directly; use "self.init(...)" or "self = ..." instead
      self.isEncodedOrSubstituted = false   // โŒ 'let' property 'isEncodedOrSubstituted' may not be initialized directly; use "self.init(...)" or "self = ..." 
      
    } else {
        // This is the call which creates the problems
        self.init(percentEncoded: byte)

        // Assigning the values directly works, but why do I need to duplicate this code?
        // self.byte = byte
        // self.count = 3
        // self.isEncodedOrSubstituted = true
    }
  }

  internal init(percentEncoded byte: UInt8) {
    self.byte = byte
    self.count = 3
    self.isEncodedOrSubstituted = true
  }
}

This issue has come up before, but in a cross-module context when extending a public struct from a dependency. That doesn't apply in this case - this is an internal struct, which I'm refactoring in a very minor way.

I don't understand why this is happening. Is there a reason for it, or could it be a bug?

3 Likes

Smaller example of the same thing:

struct S {
    let a: UInt8
    
    init(a: UInt8) {
        if a < 123 {
            self.a = a // โŒ 'let' property 'a' may not be initialized directly; use "self.init(...)" or "self = ..." instead
        } else {
            self.init()
        }
    }
    
    init() {
        self.a = 123
    }
}

?

Maybe there's a rule that an intializer must
either (A) delegate to other initializers, ie use self = โ€ฆ / self.init(โ€ฆ)
or (B) initialize all stored properties directly.

Couldn't find any documention about it, and I can't see why doing both A and B (in separate branches) shouldn't be allowed by the compiler. It seems more like a linter rule.

1 Like

That's pretty much the rule, yeah, and it's more important for classes than for structs, which actually distinguish designated and convenience initializers. You can work around it using assignment instead of delegation syntax:

struct S {
    let a: UInt8
    
    init(a: UInt8) {
        if a < 123 {
            self.a = a
        } else {
            self = .init()
        }
    }
    
    init() {
        self.a = 123
    }
}

Looks like this was originally filed as [SR-6945] Error in struct init, saying 'self' used before assignment for the assignment itself ยท Issue #49493 ยท apple/swift ยท GitHub, and I can see a few more instances in the bug tracker that should probably be marked as duplicates. (To be clear, I agree that this restriction should be lifted for structs and enums.)

8 Likes