从错误到解决:Swift required构造函数逐步解析
从错误到解决:Swift required构造函数逐步解析

从错误到解决:Swift required构造函数逐步解析

本篇文章中主要涉及Class类的继承部分,会从required构造函数,到super.init()初始化父类部分谈起。全面梳理Class类的继承流程,通过代码样例等方式,学习并理解required关键词、super关键词等、父类和子类的构造函数、初始化等内容部分。

required简介

required 是 Swift 中用于构造函数(初始化方法)的关键字,表示这个构造函数必须在所有子类中实现。也就是说,如果父类的构造函数标记为 required,任何继承该类的子类都必须实现这个构造函数,甚至如果子类没有显式实现,它也会自动继承父类的 required 构造函数。

为什么要使用 required

使用 required 的主要原因是确保在继承链中,特定的初始化逻辑能够被所有子类继承和保持一致。通常在以下情况下会用到:

1、保证子类初始化的一致性:

如果你希望所有子类都具有某个特定的初始化方法,或者必须遵循相同的初始化规则,可以在父类中使用 required。

这样,无论子类是否显式实现了这个构造函数,它们都会自动继承。

2.、配合协议和 Codable 使用:

在 Codable 的场景中,类需要一个可以从 Decoder 初始化的构造函数 init(from:)。如果你希望这个类可以被子类化,并且所有子类都能够使用相同的方式进行解码,那么将 init(from:) 标记为 required 是很有用的。

这样做可以确保即使是子类,也能够通过相同的初始化方法进行解码。

class Animal {
    var name: String

    // required initializer
    required init(name: String) {
        self.name = name
    }
}

class Dog: Animal {
    // Automatically inherits the required initializer from the superclass
}

在上面的代码中,Animal 类有一个 required 构造函数。Dog 继承了 Animal,即使没有显式地定义 init(name:),也自动获得了这个构造函数。

举一反三

子类定义构造函数

根据上面的样例代码,如果我们想要Dog子类拥有自己的构造函数。

class Dog: Animal {
    var type: String
    
    init(type: String) {
        self.type = type
    }
}

这时Xcode会提示我们:required初始化程序init(name:)必须由Animal的子类提供。

'required' initializer 'init(name:)' must be provided by subclass of 'Animal'
Insert '
    required init(name: String) {
        fatalError("init(name:) has not been implemented")
    }
'

这个错误的原因是Animal类中定义的init(name:)构造函数被标记为required。这意味着任何继承Animal的子类都必须实现这个required标记的构造函数。如果子类没有显式的提供required init(name:),编译器就会报错。

为什么给子类添加构造函数会报错呢?

这是因为我们在Dog子类中自定义了一个init(type:)构造函数。根据Swift的规则,如果子类定义了自己的构造函数,它就不会自动继承父类的构造函数,包括required构造函数。这就是为什么编译器提示需要显式的提供required init(name:)。

解决方案为:给Dog子类中添加父类的required构造函数。

class Dog: Animal {
    var type: String
    
    init(type: String) {
        self.type = type
    }
    
    required init(name: String) {
        self.name = name
    }
}

子类初始化父类部分

下面是我们子类初始化父类部分,主要涉及如何通过super.init()调用父类的构造函数。

当我们运行上面的代码,给Dog子类自定义init(type:)构造函数之后,通过required重新实现父类的required构造函数之后,我们的Xcode仍然会报错:

'super.init' isn't called on all paths before returning from initializer
'self' used in property access 'name' before 'super.init' call

这是因为在Swift中,子类在初始化时必须确保父类的属性和状态被正确设置。Swift需要先初始化父类的部分,才能继续完成子类的初始化。这是对象初始化的一般规则,确保整个对象层级的所有部分都能够被正确配置。

父类Animal中,有一个属性name。这个属性是属于Animal的,当创建Dog实例时,Dog不仅有自己的属性(如type),还会继承Animal的属性(如name)。

因为只继承了属性,所以必须通过调用super.init(name:)来初始化它。

super.init(name: name) // 调用父类的初始化方法

如果子类的初始化方法没有调用父类的构造函数,父类的属性就无法被正确初始化,从而导致对象不完整并引发编译错误。

所以,Swift强制要求子类初始化时调用父类的构造函数,确保整个对象层级中每一部分的状态都被妥善处理。

因此,我们需要给Dog子类自定义的构造函数以及父类的required构造函数中,都添加super.init()。

class Dog: Animal {
    var type: String
    
    init(name:String, type: String) {
        self.type = type
        super.init(name: name)
    }
    
    required init(name: String) {
        self.name = name
        super.init(name: name)
    }
}

在Dog子类自定义的构造函数中:如果Dog子类定义了一个自定义的构造函数,比如init(name:String, type: String),那么就必须调用super.init(name:name)才能确保Animal的属性name被正确初始化。

因为Animal类中的init(name:)是required,子类Dog必须提供一个实现。所以也需要在这个构造函数中调用super.init(name:name),否则父类的name属性将无法被初始化。

那么是否可以不使用super.init?

答案是不行的,每个子类的构造函数都必须调用super.init,否则编译器就会报错。只有以下几种情况,可以不显式调用super.init:

  • 子类没有定义自己的属性:如果子类没有添加任何新属性,并且不需要额外的构造逻辑,那么 Swift 可以自动继承父类的构造函数,你不需要手动写 super.init。
  • 子类使用默认的构造函数:如果你没有在子类中定义任何构造函数,并且父类有默认的构造函数,子类会自动继承它。此时你无需显式调用 super.init。

因此,子类的构造函数都必须调用super.init,以确保父类的属性得到正确初始化。

这是Swift确保类继承链中所有属性都能得到完整初始化的一种方式。

初始化顺序

那么,我们的代码现在无问题了么?还是有报错。现在的报错是:

'self' used in property access 'name' before 'super.init' call
Property 'self.type' not initialized at super.init call

这次报错的原因是Dog子类声明的required构造函数没有初始化type,以及在调用super.init之前,使用了self访问name属性。

self.type = "Type"  // 在required构造函数中初始化type

required构造函数之所以需要初始化type,是因为Swift 要求所有属性在构造函数结束之前必须被初始化。子类的 required 构造函数也需要遵守这个规则。因此,即使是继承自父类的 required 构造函数,子类的属性仍然需要在这个函数中得到初始化。

这两个问题在于,我们如果想要Dog子类使用required构造函数,那么我们也应该给Dog子类的属性进行初始化,因此,在required构造函数中应该添加self.type。

required init(name: String) {
    self.name = name
    self.type = type
    super.init(name: name)
}

我们在添加self.type之后,我们需要type拥有required构造函数的入参。

required init(name: String,type: String) {
    self.name = name
    self.type = type
    super.init(name: name)
}

当我们修改required构造函数,添加type入参时,又会报新的报错:

Invalid redeclaration of 'init(name:type:)'
'required' initializer 'init(name:)' must be provided by subclass of 'Animal'

这个报错在于required初始化程序init(name:)必须由Dog子类提供,required关键字的构造函数必须严格匹配父类定义,所以我们无法任意修改参数列表,让Dog子类进行传参。

所以,Dog子类的type没有默认值,那么我们只能给Dog子类的type进行赋值的方法:

required init(name: String,type: String) {
    self.name = name
    self.type = "Type"
    super.init(name: name)
}

赋值之后,就只剩下最后一个报错了:

'self' used in property access 'name' before 'super.init' call

这个报错是在访问或修改继承父类的self属性之前,应该先使用super.init进行父类的初始化。因为当前对象还没有完全初始化,

因此,我们只能先用super.init进行父类的初始化后,再使用self.name进行赋值。此外,因为我们required构造函数就是self.name = name,我们只需要调用父类的构造函数即可。

required init(name: String) {
    self.type = "Type"
    super.init(name: name)
}

最后,我们的代码就正常运行。

这里我们了解到required构造函数中,应先初始化子类的属性(type),然后尽早调用super.init(name:),确保子类和父类的所有部分都正确初始化。

此外,在super.init之前,不要访问父类self的属性。

完整代码:

class Animal {
    var name: String

    // required initializer
    required init(name: String) {
        self.name = name
    }
}

class Dog: Animal {
    var type: String
    
    init(name:String, type: String) {
        self.type = type
        super.init(name: name)
    }
    
    required init(name: String) {
        self.type = "Type"
        super.init(name: name)
    }
}

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注