• Swift5.3 语言指南(十六) 初始化


    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
    ➤微信公众号:山青咏芝(shanqingyongzhi)
    ➤博客园地址:山青咏芝(https://www.cnblogs.com/strengthen/
    ➤GitHub地址:https://github.com/strengthen/LeetCode
    ➤原文地址:https://www.cnblogs.com/strengthen/p/9739365.html 
    ➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
    ➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

    初始化是准备使用的类,结构或枚举实例的过程。此过程涉及为该实例上的每个存储属性设置一个初始值,并执行新实例准备使用之前所需的任何其他设置或初始化。

    您可以通过定义初始化器来实现此初始化过程,这些初始化器类似于可以调用以创建特定类型新实例的特殊方法。与Objective-C初始化程序不同,Swift初始化程序不会返回值。它们的主要作用是确保在首次使用类型之前,正确初始化类型的新实例。

    类类型的实例还可以实现一个deinitializer,它在释放该类的实例之前执行任何自定义清除。有关反初始化程序的更多信息,请参见反初始化

    设置存储属性的初始值

    类和结构必须在创建该类或结构的实例时将其所有存储的属性设置为适当的初始值。存储的属性不能处于不确定状态。

    您可以在初始化程序中为存储的属性设置初始值,或者通过将默认属性值分配为属性定义的一部分来设置初始值。以下各节介绍了这些操作。

    注意

    当您为存储的属性分配默认值,或在初始化程序中设置其初始值时,将直接设置该属性的值,而无需调用任何属性观察器。

    初始化器

    调用初始化程序以创建特定类型的新实例。最简单的形式是,初始化程序就像没有参数的实例方法,使用init关键字编写

    1. init() {
    2. // perform some initialization here
    3. }

    下面的示例定义了一个新结构,称为Fahrenheit存储以华氏度表示的温度。Fahrenheit结构具有一个存储属性temperature,其类型为Double

    1. struct Fahrenheit {
    2. var temperature: Double
    3. init() {
    4. temperature = 32.0
    5. }
    6. }
    7. var f = Fahrenheit()
    8. print("The default temperature is (f.temperature)° Fahrenheit")
    9. // Prints "The default temperature is 32.0° Fahrenheit"

    该结构定义了一个init没有参数的初始化程序,该初始化程序使用值32.0(水的冰点,以华氏度为单位)初始化存储的温度

    默认属性值

    您可以从初始化程序中设置存储属性的初始值,如上所示。或者,在属性声明中指定默认属性值您可以通过在定义属性时为其分配初始值来指定默认属性值。

    注意

    如果属性始终使用相同的初始值,请提供默认值,而不要在初始化程序中设置值。最终结果是相同的,但是默认值将属性的初始化与其声明紧密联系在一起。它使初始化程序更短,更清晰,并使您能够从其默认值推断属性的类型。默认值还使您更容易利用默认初始化程序和初始化程序继承,如本章稍后所述。

    您可以Fahrenheit通过temperature在声明属性时为其属性提供默认值,以更简单的形式从上面编写结构

    1. struct Fahrenheit {
    2. var temperature = 32.0
    3. }

    自定义初始化

    您可以使用输入参数和可选属性类型,或通过在初始化过程中分配常量属性来自定义初始化过程,如以下各节所述。

    初始化参数

    您可以提供初始化参数作为初始化程序定义的一部分,以定义自定义初始化过程的值的类型和名称。初始化参数具有与功能和方法参数相同的功能和语法。

    以下示例定义了一个名为的结构Celsius,该结构存储以摄氏度表示的温度。Celsius结构实现了两个名为init(fromFahrenheit:)和的自定义初始化程序init(fromKelvin:),它们使用不同温度范围内的值初始化该结构的新实例:

    1. struct Celsius {
    2. var temperatureInCelsius: Double
    3. init(fromFahrenheit fahrenheit: Double) {
    4. temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    5. }
    6. init(fromKelvin kelvin: Double) {
    7. temperatureInCelsius = kelvin - 273.15
    8. }
    9. }
    10. let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
    11. // boilingPointOfWater.temperatureInCelsius is 100.0
    12. let freezingPointOfWater = Celsius(fromKelvin: 273.15)
    13. // freezingPointOfWater.temperatureInCelsius is 0.0

    第一个初始化程序具有单个初始化参数,其参数标签为fromFahrenheit,参数名称为fahrenheit第二个初始化程序具有一个初始化参数,其参数标签为fromKelvin,参数名称为kelvin两个初始化程序都将其单个参数转换为相应的摄氏度值,并将此值存储在名为的属性中temperatureInCelsius

    参数名称和参数标签

    与函数和方法参数一样,初始化参数可以具有在初始化程序的主体内使用的参数名称和在调用初始化程序时使用的参数标签。

    但是,初始化程序在其括号前没有以函数和方法那样的方式标识函数的名称。因此,初始化器参数的名称和类型在确定应调用哪个初始化器中起着特别重要的作用。因此,如果不提供初始化功能,Swift会为初始化程序中的每个参数提供一个自动参数标签

    下面的例子定义了一个名为结构Color,具有三个恒定属性叫做redgreen,和blue这些属性存储一个介于0.0之间的值1.0以指示颜色中红色,绿色和蓝色的数量。

    Color为初始化程序Double的红色,绿色和蓝色分量提供三个适当命名的类型的参数Color还提供了带有单个white参数的第二个初始化器,该初始化器用于为所有三个颜色分量提供相同的值。

    1. struct Color {
    2. let red, green, blue: Double
    3. init(red: Double, green: Double, blue: Double) {
    4. self.red = red
    5. self.green = green
    6. self.blue = blue
    7. }
    8. init(white: Double) {
    9. red = white
    10. green = white
    11. blue = white
    12. }
    13. }

    Color通过为每个初始值设定项参数提供命名值,两个初始值设定项均可用于创建新实例:

    1. let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
    2. let halfGray = Color(white: 0.5)

    请注意,如果不使用参数标签,则无法调用这些初始化程序。如果已定义参数标签,则必须始终在初始化程序中使用它们,而忽略它们是编译时错误:

    1. let veryGreen = Color(0.0, 1.0, 0.0)
    2. // this reports a compile-time error - argument labels are required

    不带参数标签的初始化参数

    如果不想为初始化参数使用参数标签,请为该参数写下划线(_)而不是显式参数标签,以覆盖默认行为。

    这是上述“ 初始化参数 ”中Celsius示例的扩展版本,带有一个附加的初始化程序,该初始化程序可根据已经在摄氏度范围内创建一个新实例CelsiusDouble

    1. struct Celsius {
    2. var temperatureInCelsius: Double
    3. init(fromFahrenheit fahrenheit: Double) {
    4. temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    5. }
    6. init(fromKelvin kelvin: Double) {
    7. temperatureInCelsius = kelvin - 273.15
    8. }
    9. init(_ celsius: Double) {
    10. temperatureInCelsius = celsius
    11. }
    12. }
    13. let bodyTemperature = Celsius(37.0)
    14. // bodyTemperature.temperatureInCelsius is 37.0

    初始化调用Celsius(37.0)的意图很明确,不需要参数标签。因此,应将此初始化程序编写为:可以通过提供未命名的来调用它init(_ celsius: Double)Double

    可选属性类型

    如果您的自定义类型的存储属性在逻辑上被允许为“无值”(可能是因为初始化期间无法设置其值,或者因为稍后某个时候它被允许为“无值”),请使用可选类型。可选类型的属性将使用值自动初始化nil,表明该属性在初始化过程中故意具有“没有值”。

    以下示例定义了一个名为的类SurveyQuestion,其具有一个可选String属性response

    1. class SurveyQuestion {
    2. var text: String
    3. var response: String?
    4. init(text: String) {
    5. self.text = text
    6. }
    7. func ask() {
    8. print(text)
    9. }
    10. }
    11. let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
    12. cheeseQuestion.ask()
    13. // Prints "Do you like cheese?"
    14. cheeseQuestion.response = "Yes, I do like cheese."

    直到询问问题后,才能知道对调查问题的回答,因此,该response属性用String?的类型声明为“可选String”。nilSurveyQuestion初始化的新实例时,会自动为其分配默认值,表示“没有字符串” 

    在初始化期间分配常量属性

    您可以在初始化期间的任何时候为常量属性分配一个值,只要在初始化完成时将其设置为确定值即可。为常数属性分配值后,就无法再对其进行修改。

    注意

    对于类实例,只能在引入常量的类的初始化期间对其进行修改。子类不能修改它。

    您可以SurveyQuestion从上面修改示例,以text对问题属性使用常量属性而不是变量属性,以表明一旦SurveyQuestion创建的实例,问题就不会更改即使该text属性现在是一个常量,仍可以在类的初始化程序中对其进行设置:

    1. class SurveyQuestion {
    2. let text: String
    3. var response: String?
    4. init(text: String) {
    5. self.text = text
    6. }
    7. func ask() {
    8. print(text)
    9. }
    10. }
    11. let beetsQuestion = SurveyQuestion(text: "How about beets?")
    12. beetsQuestion.ask()
    13. // Prints "How about beets?"
    14. beetsQuestion.response = "I also like beets. (But not with cheese.)"

    默认初始化器

    迅速提供了一个默认初始值对于所有其属性提供缺省值,并且不提供至少一个初始值设定本身的任何结构或类。默认初始化程序仅创建一个新实例,并将其所有属性设置为其默认值。

    本示例定义了一个名为的类ShoppingListItem该类将购物清单中商品的名称,数量和购买状态封装起来:

    1. class ShoppingListItem {
    2. var name: String?
    3. var quantity = 1
    4. var purchased = false
    5. }
    6. var item = ShoppingListItem()

    因为ShoppingListItem该类的所有属性都具有默认值,并且由于它是没有超类的基类,所以它会ShoppingListItem自动获得一个默认的初始化器实现,该实现会创建一个新实例,并将其所有属性设置为其默认值。(该name属性是一个可选String属性,因此nil即使该值未写在代码,它也会自动接收默认值。)上面的示例使用ShoppingListItem该类的默认初始化程序来使用初始化程序创建该类的新实例。语法,编写为ShoppingListItem(),并将此新实例分配给名为的变量item

    结构类型的成员初始化器

    如果结构类型未定义任何自己的自定义初始化程序,则它们会自动收到一个成员初始化程序。与默认初始化程序不同,该结构即使在存储了没有默认值的属性的情况下,也会接收成员初始化程序。

    逐成员初始化器是初始化新结构实例的成员属性的简便方法。可以通过名称将新实例的属性的初始值传递给成员初始化器。

    下面的示例定义了一个结构Size该结构具有两个称为width和的属性heightDouble通过指定默认值,可以推断这两个属性均为类型0.0

    Size结构会自动接收一个init(height:)成员级初始化程序,您可以使用该初始化程序来初始化新Size实例:

    1. struct Size {
    2. var width = 0.0, height = 0.0
    3. }
    4. let twoByTwo = Size( 2.0, height: 2.0)

    调用成员初始化器时,可以忽略具有默认值的任何属性的值。在上面的示例中,该Size结构的heightwidth属性均具有默认值您可以省略一个属性或两个属性,并且初始化程序将对所有省略的内容使用默认值,例如:

    1. let zeroByTwo = Size(height: 2.0)
    2. print(zeroByTwo.width, zeroByTwo.height)
    3. // Prints "0.0 2.0"
    4. let zeroByZero = Size()
    5. print(zeroByZero.width, zeroByZero.height)
    6. // Prints "0.0 0.0"

    值类型的初始化程序委托

    初始化程序可以调用其他初始化程序来执行实例初始化的一部分。此过程称为初始化程序委托,可避免在多个初始化程序之间重复代码。

    对于值类型和类类型,初始化程序委派的工作方式以及允许哪种形式的委派的规则是不同的。值类型(结构和枚举)不支持继承,因此它们的初始化程序委托过程相对简单,因为它们只能委托给自己提供的另一个初始化程序。但是,类可以从其他类继承,如Inheritance中所述这意味着类还有其他责任,以确保在初始化期间为它们继承的所有存储属性分配适当的值。这些职责在下面的类继承和初始化进行了描述

    对于值类型,self.init在编写自己的自定义初始化程序时,通常使用同一值类型引用其他初始化程序。self.init只能在初始化程序中调用

    请注意,如果您为值类型定义自定义初始化程序,则将不再有权使用该类型的默认初始化程序(或成员初始化程序,如果它是结构)。此约束防止了使用自动初始化程序之一的人意外绕过更复杂的初始化程序中提供的其他基本设置的情况。

    注意

    如果您希望自定义值类型可以使用默认的初始值设定项和成员明智的初始值设定项以及您自己的自定义初始值设定项进行初始化,请在扩展名中编写自定义初始值设定项,而不是将其作为值类型的原始实现的一部分。有关更多信息,请参见扩展

    以下示例定义了一个自定义Rect结构来表示几何矩形。该示例需要两个称为Size和的支持结构Point,这两个结构0.0为其所有属性提供默认值

    1. struct Size {
    2. var width = 0.0, height = 0.0
    3. }
    4. struct Point {
    5. var x = 0.0, y = 0.0
    6. }

    您可以通过Rect以下三种方式之一来初始化下面结构:使用其默认的零初始化originsize属性值,提供特定的原点和大小,或提供特定的中心点和大小。这些初始化选项由三个自定义初始化程序表示,它们是Rect结构定义的一部分:

    1. struct Rect {
    2. var origin = Point()
    3. var size = Size()
    4. init() {}
    5. init(origin: Point, size: Size) {
    6. self.origin = origin
    7. self.size = size
    8. }
    9. init(center: Point, size: Size) {
    10. let originX = center.x - (size.width / 2)
    11. let originY = center.y - (size.height / 2)
    12. self.init(origin: Point(x: originX, y: originY), size: size)
    13. }
    14. }

    如果结构没有自己的自定义初始化Rect程序,init()第一个初始化程序在功能上与该结构将收到的默认初始化程序相同。此初始值设定项的主体为空,由一对空的花括号表示{}调用此初始值设定项将返回一个Rect实例,实例的originsize属性均使用从其属性定义的默认值初始化Point(x: 0.0, y: 0.0)Size( 0.0, height: 0.0)

    1. let basicRect = Rect()
    2. // basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)

    第二个Rect初始化程序,init(origin:size:)在功能上与结构不具有自己的自定义初始化程序时将接收的成员初始化程序相同。此初始化程序仅将originsize参数值分配给适当的存储属性:

    1. let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
    2. size: Size( 5.0, height: 5.0))
    3. // originRect's origin is (2.0, 2.0) and its size is (5.0, 5.0)

    第三个Rect初始化init(center:size:)器稍微复杂一些。首先根据一个center点和一个size计算一个合适的原点然后,它调用(或委托init(origin:size:)初始化器,该初始化器将新的origin和size值存储在适当的属性中:

    1. let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
    2. size: Size( 3.0, height: 3.0))
    3. // centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

    init(center:size:)初始化可能分配的新值origin,并size以相应的属性本身。但是,对于init(center:size:)初始化程序而言,利用已经完全提供该功能的现有初始化程序更加方便(意图更清晰)

    注意

    有关无需自行定义init()init(origin:size:)初始化程序的示例编写方式,请参见Extensions

    类继承和初始化

    在初始化期间,必须类的所有存储属性(包括该类从其超类继承的所有属性)分配一个初始值。

    Swift为类类型定义了两种初始化器,以帮助确保所有存储的属性均接收初始值。这些被称为指定的初始化程序和便捷初始化程序。

    指定的初始化程序和便利性初始化程序

    指定的初始化器是类的主要初始化器。指定的初始化程序将完全初始化该类引入的所有属性,并调用适当的超类初始化程序以继续超类链中的初始化过程。

    类往往只有很少的指定初始化器,而一个类通常只有一个。指定的初始化程序是“漏斗”点,通过该“漏斗”点进行初始化,并通过该“漏斗”点继续超类链中的初始化过程。

    每个类必须至少有一个指定的初始化程序。在某些情况下,可以通过从超类继承一个或多个指定的初始化程序来满足此要求,如下面的“ 自动初始化程序继承”中所述。

    便利的初始值设定项是辅助的,支持类的初始值设定项。您可以定义一个便捷初始化程序,以从与便捷初始化程序相同的类中调用一个指定初始化程序,并将某些指定初始值设定项的参数设置为默认值。您还可以定义一个便捷初始化程序,以针对特定用例或输入值类型创建该类的实例。

    如果您的类不需要便利初始化器,则不必提供它们。只要通向通用初始化模式的快捷方式可以节省时间或使类的初始化更清晰,就可以创建方便的初始化器。

    指定和便捷初始化程序的语法

    指定的类初始化器的编写方式与值类型的简单初始化器的编写方式相同:

    1. init(parameters) {
    2. statements
    3. }

    便捷初始化程序以相同的样式编写,但convenience修饰符放在init关键字之前,并用空格分隔:

    1. convenience init(parameters) {
    2. statements
    3. }

    类类型的初始化程序委托

    为了简化指定初始化器和便捷初始化器之间的关系,Swift将以下三个规则应用于初始化器之间的委托调用:

    规则1
    指定的初始值设定项必须从其直接超类调用指定的初始值设定项。
    规则二
    便捷初始化程序必须从同一调用另一个初始化程序
    规则三
    便利初始化程序必须最终调用指定的初始化程序。

    记住这一点的一种简单方法是:

    • 指定的初始值必须始终委派
    • 便利的初始化必须始终委派跨越

    这些规则如下图所示:

    ../_images/initializerDelegation01_2x.png

    在这里,超类具有一个指定的初始值设定项和两个便利的初始化项。一个便利初始化程序调用另一个便利初始化程序,后者又调用单个指定的初始化程序。这从上方满足规则2和3。超类本身没有其他超类,因此规则1不适用。

    该图中的子类具有两个指定的初始化程序和一个便捷的初始化程序。便捷初始化程序必须调用两个指定的初始化程序之一,因为它只能调用同一类中的另一个初始化程序。这从上方满足规则2和3。两个指定的初始值设定项都必须从超类中调用单个指定的初始值设定项,以满足上方的规则1。

    注意

    这些规则不会影响您的类的用户如何创建每个类的实例。上图中的任何初始化程序都可用于创建它们所属类的完全初始化的实例。这些规则仅影响您如何编写类的初始化程序的实现。

    下图显示了四个类的更复杂的类层次结构。它说明了此层次结构中指定的初始化程序如何充当类初始化的“漏斗”点,从而简化了链中各类之间的相互关系:

    ../_images/initializerDelegation02_2x.png

    两阶段初始化

    Swift中的类初始化是一个分为两个阶段的过程。在第一阶段,每个存储的属性都由引入它的类分配一个初始值。一旦确定了每个存储属性的初始状态,便开始第二阶段,并且在认为新实例可以使用之前,每个类都有机会自定义其存储属性。

    两阶段初始化过程的使用使初始化安全,同时仍为类层次结构中的每个类提供了完全的灵活性。两阶段初始化可防止在初始化属性值之前对其进行访问,并防止其他初始化程序意外地将属性值设置为其他值。

    注意

    Swift的两阶段初始化过程类似于Objective-C中的初始化。主要区别在于,在阶段1中,Objective-C为每个属性分配零或空值(例如0nil)。Swift的初始化流程更加灵活,因为它允许您设置自定义初始值,并且可以处理有效值0nil无效值的类型。

    Swift的编译器执行四项有用的安全检查,以确保两阶段初始化完成且没有错误:

    安全检查1
    指定的初始值设定项必须确保由其类引入的所有属性在委托给超类初始值设定项之前都已初始化。

    如上所述,仅在知道对象所有存储属性的初始状态后,才认为该对象的内存已完全初始化。为了满足此规则,指定的初始值设定项必须确保在传递链之前初始化其自身的所有属性。

    安全检查2
    在将值分配给继承的属性之前,指定的初始值设定项必须委托一个超类初始值设定项。如果不是这样,则指定的初始化器分配的新值将被超类覆盖,作为其自身初始化的一部分。
    安全检查3
    便利初始化程序必须在将值分配给任何属性(包括由同一类定义的属性)之前委托给另一个初始化程序如果不是,便利初始化程序分配的新值将被其自己类的指定初始化程序覆盖。
    安全检查4
    在初始化self的第一阶段完成之前,初始化器无法调用任何实例方法,读取任何实例属性的值或将其称为值。

    在第一阶段结束之前,该类实例并不完全有效。一旦在第一阶段结束时知道类实例是有效的,就只能访问属性,并且只能调用方法。

    根据上述四个安全检查,以下是两阶段初始化如何进行:

    阶段1

    • 在类上调用了指定的或便捷的初始化程序。
    • 分配该类的新实例的内存。内存尚未初始化。
    • 该类的指定初始化程序确认该类引入的所有存储属性都具有值。这些存储的属性的内存现在已初始化。
    • 指定的初始值设定项移交给超类初始值设定项,以为其自身的存储属性执行相同的任务。
    • 这将继续类继承链,直到到达链的顶部。
    • 一旦到达链的顶部,并且链中的最后一个类已确保其所有存储属性都具有值,则实例的内存被视为已完全初始化,并且阶段1已完成。

    阶段2

    • 从链的顶部向下追溯,链中的每个指定的初始化程序都可以选择进一步自定义实例。初始化程序现在可以访问self并可以修改其属性,调用其实例方法,等等。
    • 最后,链中的所有便利初始化程序都可以选择自定义实例并使用self

    以下是第1阶段寻找假设的子类和超类的初始化调用的方式:

    ../_images/twoPhaseInitialization01_2x.png

    在此示例中,初始化始于对子类的便捷初始化程序的调用。此便捷初始化程序尚无法修改任何属性。它委托来自同一类的指定初始化器。

    根据安全检查1,指定的初始化器确保子类的所有属性都有一个值。然后,它在其父类上调用指定的初始化器,以继续进行链上的初始化。

    超类的指定初始化器确保所有超类属性都有一个值。没有其他要初始化的超类,因此不需要进一步的委派。

    一旦超类的所有属性都具有初始值,就将其内存视为已完全初始化,并且阶段1已完成。

    以下是第2阶段寻找相同初始化调用的方式:

    ../_images/twoPhaseInitialization02_2x.png

    现在,超类的指定初始化器有机会进一步自定义实例(尽管不必如此)。

    一旦超类的指定初始化器完成,子类的指定初始化器就可以执行其他自定义操作(尽管再次,它不必这样做)。

    最后,一旦子类的指定初始化程序完成,最初调用的便捷初始化程序就可以执行其他自定义。

    初始化程序的继承和覆盖

    与Objective-C中的子类不同,Swift子类默认情况下不会继承其超类初始化程序。Swift的方法可以防止这样的情况,即超类的简单初始化程序被更专门的子类继承,并用于创建未完全或正确初始化的子类的新实例。

    注意

    超类初始化程序在某些情况下会继承,但是只有在安全且适当的情况下才可以这样做。有关更多信息,请参见下面的自动初始化继承

    如果希望自定义子类提供一个或多个与其父类相同的初始化器,则可以在子类中提供这些初始化器的自定义实现。

    当编写与超类指定的初始化程序匹配的子类初始化程序时,实际上是在提供该指定的初始化程序的替代。因此,必须override在子类的初始化程序定义之前编写修饰符。即使您要覆盖自动提供的默认初始化程序,这也是正确的,如Default Initializers中所述

    与覆盖属性,方法或下标一样,override修饰符的存在会提示Swift检查超类是否具有匹配的指定初始化器要被覆盖,并验证是否已按预期指定了覆盖初始化器的参数。

    注意

    override重写超类指定的初始值设定项时,您始终会编写修饰符,即使您的子类对初始值设定项的实现是便捷的初始值设定项也是如此。

    相反,如果您编写与超类便利性初始化程序匹配的子类初始化程序,则根据上面在类类型的初始化程序委托中所述的规则,该子类将永远不能直接调用该超类便利性初始化程序因此,您的子类(严格地说)没有提供超类初始值设定项的替代。因此,override在提供超类便捷性初始化程序的匹配实现时,您无需编写修饰符。

    下面的示例定义了一个名为的基类Vehicle此基类声明一个称为的存储属性numberOfWheels,默认Int值为0numberOfWheels属性由计算属性使用,该属性称为description来创建String车辆特性的描述:

    1. class Vehicle {
    2. var numberOfWheels = 0
    3. var description: String {
    4. return "(numberOfWheels) wheel(s)"
    5. }
    6. }

    Vehicle类提供了其唯一的存储属性的默认值,并没有提供任何自定义初始化本身。结果,它会自动接收一个默认的初始化程序,如Default Initializers中所述默认的初始值设定项(如果有)始终是类的指定初始值设定项,可用于创建Vehicle带有的numberOfWheelsof 的新实例0

    1. let vehicle = Vehicle()
    2. print("Vehicle: (vehicle.description)")
    3. // Vehicle: 0 wheel(s)

    下一个示例定义了一个Vehicle名为的子类Bicycle

    1. class Bicycle: Vehicle {
    2. override init() {
    3. super.init()
    4. numberOfWheels = 2
    5. }
    6. }

    Bicycle子类定义指定初始化一个自定义的,init()此指定的初始值设定项与的超类中的指定的初始值设定项匹配Bicycle,因此Bicycle此初始值设定项版本标记有override修饰符。

    init()初始值设定项Bicycle始于super.init(),该会调用Bicycle该类的超类的默认初始值设定Vehicle这样可以确保numberOfWheels继承的属性VehicleBicycle有机会修改属性之前被初始化调用后super.init(),的原始值将numberOfWheels替换为的新值2

    如果创建的实例Bicycle,则可以调用其继承的description计算属性,以查看其numberOfWheels属性如何更新:

    1. let bicycle = Bicycle()
    2. print("Bicycle: (bicycle.description)")
    3. // Bicycle: 2 wheel(s)

    如果子类初始化程序在初始化过程的第2阶段未执行任何自定义操作,并且超类具有零参数指定的初始化程序,则可以super.init()在将值分配给所有子类的所有存储属性后省略对的调用

    本示例定义的另一个子类Vehicle,称为Hoverboard在其初始化程序中,Hoverboard该类仅设置其color属性。super.init()该初始化程序没有显式调用而是依靠对其超类的初始化程序的隐式调用来完成该过程。

    1. class Hoverboard: Vehicle {
    2. var color: String
    3. init(color: String) {
    4. self.color = color
    5. // super.init() implicitly called here
    6. }
    7. override var description: String {
    8. return "(super.description) in a beautiful (color)"
    9. }
    10. }

    的实例Hoverboard使用Vehicle初始化程序提供的默认轮子数量

    1. let hoverboard = Hoverboard(color: "silver")
    2. print("Hoverboard: (hoverboard.description)")
    3. // Hoverboard: 0 wheel(s) in a beautiful silver

    注意

    子类可以在初始化期间修改继承的变量属性,但不能修改继承的常量属性。

    自动初始化程序继承

    如上所述,默认情况下,子类不继承其超类初始化程序。但是,如果满足某些条件则会自动继承超类初始化实际上,这意味着您不需要在许多常见情况下编写初始化程序覆盖,并且可以在安全的情况下以最小的努力继承超类初始化程序。

    假设为子类中引入的任何新属性提供默认值,则适用以下两个规则:

    规则1
    如果您的子类没有定义任何指定的初始化器,它将自动继承其所有超类的指定初始化器。
    规则二
    如果您的子类提供了所有超类指定初始化器的实现(通过按规则1继承它们,或通过提供自定义实现作为其定义的一部分),那么它将自动继承所有超类便利性初始化器。

    即使您的子类添加了进一步的便利初始化程序,这些规则也适用。

    注意

    子类可以将超类指定的初始化器实现为子类便捷性初始化器,作为满足规则2的一部分。

    指定的便捷初始化器

    以下示例显示了实际的指定初始化器,便捷初始化器和自动初始化器继承。这个例子定义了三类所谓的层次FoodRecipeIngredientShoppingListItem,并演示了如何自己初始化互动。

    层次结构中的基类称为Food,这是一个封装食品名称的简单类。Food课程介绍一个String叫做物业name并提供两个初始化创建Food实例:

    1. class Food {
    2. var name: String
    3. init(name: String) {
    4. self.name = name
    5. }
    6. convenience init() {
    7. self.init(name: "[Unnamed]")
    8. }
    9. }

    下图显示了Food该类的初始化程序链

    ../_images/initializersExample01_2x.png

    类没有默认的成员初始化器,因此Food该类提供了一个指定的初始化器,该初始化器带有一个称为的参数name此初始化程序可用于创建Food具有特定名称的新实例:

    1. let namedMeat = Food(name: "Bacon")
    2. // namedMeat's name is "Bacon"

    该类初始值设定项作为指定的初始值设定项提供,因为它可确保新实例的所有存储属性都已完全初始化。类没有超类,所以初始化不需要调用来完成初始化。init(name: String)FoodFoodFoodinit(name: String)super.init()

    Food级还提供了一个方便的初始化,init()不带参数。init()初始化通过委派跨越到一个新的食品提供了默认的占位符名称Food类的具有的价值init(name: String)name[Unnamed]

    1. let mysteryMeat = Food()
    2. // mysteryMeat's name is "[Unnamed]"

    层次结构中的第二类的子类FoodRecipeIngredientRecipeIngredient类机型中的烹饪配方的成分。它引入了一个Int称为属性quantity(除了name继承自属性之外Food),并定义了两个用于创建RecipeIngredient实例的初始化程序

    1. class RecipeIngredient: Food {
    2. var quantity: Int
    3. init(name: String, quantity: Int) {
    4. self.quantity = quantity
    5. super.init(name: name)
    6. }
    7. override convenience init(name: String) {
    8. self.init(name: name, quantity: 1)
    9. }
    10. }

    下图显示了RecipeIngredient该类的初始化程序链

    ../_images/initializersExample02_2x.png

    RecipeIngredient班有一个单一的指定初始化,,它可以用来填充的所有新的属性的实例。此初始化程序首先将传递的参数分配给属性,这是引入的唯一新属性这样做之后,初始化器将委托给该类初始化器该过程满足上述两阶段初始化的安全检查1 init(name: String, quantity: Int)RecipeIngredientquantityquantityRecipeIngredientinit(name: String)Food

    RecipeIngredient还定义了一个便利的初始化程序,初始化程序仅用于按名称创建实例。对于所有创建的实例,该便利初始化程序均假设其数量为而没有明确的数量。此便捷初始化程序的定义使实例创建起来更快捷,更便捷,并且在创建多个单量实例时避免了代码重复这个方便的初始化方法只是将委派给类的指定初始化方法,并传入一个init(name: String)RecipeIngredient1RecipeIngredientRecipeIngredientRecipeIngredientquantity1

    提供便捷初始化程序采用与从中指定的初始化程序相同的参数因为此便利初始化程序会覆盖其父类中的指定初始化程序,所以必须使用修饰符对其进行标记(如Initializer Inheritance和Overriding中所述)。init(name: String)RecipeIngredientinit(name: String) Foodoverride

    尽管RecipeIngredient初始化程序提供为方便的初始化程序,但仍提供了其超类的所有指定初始化程序的实现。因此,它也会自动继承其超类的所有便利初始化程序。init(name: String)RecipeIngredientRecipeIngredient

    在该示例中,超类为RecipeIngredientIS Food,其具有单一的方便称为初始化init()因此,此初始值设定项由继承RecipeIngredientinit()函数的继承版本与版本完全相同Food,只不过它是委派给RecipeIngredient版本而不是版本。init(name: String)Food

    这三个初始化程序均可用于创建新RecipeIngredient实例:

    1. let oneMysteryItem = RecipeIngredient()
    2. let oneBacon = RecipeIngredient(name: "Bacon")
    3. let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

    层次结构中的第三和最后一类是一个子类RecipeIngredientShoppingListItemShoppingListItem级车型,因为它出现在购物清单配方成分。

    购物清单中的每个项目都以“未购买”开始。为了表示这一事实,我们ShoppingListItem引入了一个布尔属性purchased,其默认值为falseShoppingListItem还添加了计算description属性,该属性提供了ShoppingListItem实例的文本描述

    1. class ShoppingListItem: RecipeIngredient {
    2. var purchased = false
    3. var description: String {
    4. var output = "(quantity) x (name)"
    5. output += purchased ? " ✔" : " ✘"
    6. return output
    7. }
    8. }

    注意

    ShoppingListItem并未定义初始值设定purchased来为提供初始值,因为购物清单中的商品(如此处建模)始终始于未购买的商品。

    因为它为引入的所有属性提供默认值,并且自身未定义任何初始化程序,所以它会ShoppingListItem自动从其超类继承所有指定的初始化和便捷初始化程序。

    下图显示了所有三个类的整体初始化程序链:

    ../_images/initializersExample03_2x.png

    您可以使用所有继承的三个初始化器来创建新ShoppingListItem实例:

    1. var breakfastList = [
    2. ShoppingListItem(),
    3. ShoppingListItem(name: "Bacon"),
    4. ShoppingListItem(name: "Eggs", quantity: 6),
    5. ]
    6. breakfastList[0].name = "Orange juice"
    7. breakfastList[0].purchased = true
    8. for item in breakfastList {
    9. print(item.description)
    10. }
    11. // 1 x Orange juice ✔
    12. // 1 x Bacon ✘
    13. // 6 x Eggs ✘

    在这里,breakfastList从包含三个新ShoppingListItem实例的数组文字中创建了一个名为的新数组数组的类型推断为[ShoppingListItem]创建阵列后,阵列ShoppingListItem开头的的名称从更改为"[Unnamed]"并标记为已购买。打印阵列中每个项目的描述将表明它们的默认状态已按预期设置。"Orange juice"

    初始化失败

    有时,定义初始化可能失败的类,结构或枚举有时很有用。无效的初始化参数值,缺少必需的外部资源或其他阻止初始化成功的条件可能触发此失败。

    为了应对可能失败的初始化条件,请将一个或多个可失败的初始化程序定义为类,结构或枚举定义的一部分。通过在init关键字(init?后面放置问号,可以编写失败的初始化程序

    注意

    您不能使用相同的参数类型和名称来定义可失败的初始化程序和不可失败的初始化程序。

    失败的初始化程序会创建一个初始化类型可选值。在有故障的初始化程序中编写代码,以指示可以触发初始化失败的点。return nil

    注意

    严格来说,初始化器不返回值。相反,它们的作用是确保self在初始化结束时已完全正确地对其进行了初始化。尽管您编写触发初始化失败的代码,但是您并未使用关键字来指示初始化成功。return nilreturn

    例如,为数字类型转换实现了失败的初始化器。为确保数字类型之间的转换准确地保留了该值,请使用init(exactly:)初始化程序。如果类型转换不能保持该值,则初始化程序将失败。

    1. let wholeNumber: Double = 12345.0
    2. let pi = 3.14159
    3. if let valueMaintained = Int(exactly: wholeNumber) {
    4. print("(wholeNumber) conversion to Int maintains value of (valueMaintained)")
    5. }
    6. // Prints "12345.0 conversion to Int maintains value of 12345"
    7. let valueChanged = Int(exactly: pi)
    8. // valueChanged is of type Int?, not Int
    9. if valueChanged == nil {
    10. print("(pi) conversion to Int does not maintain value")
    11. }
    12. // Prints "3.14159 conversion to Int does not maintain value"

    以下示例定义了一个名为的结构Animal,其常量String属性为speciesAnimal结构还使用一个名为的单个参数定义了一个失败的初始化程序species此初始化程序检查species传递给初始化程序值是否为空字符串。如果找到空字符串,则会触发初始化失败。否则,将species设置属性的值,并且初始化成功:

    1. struct Animal {
    2. let species: String
    3. init?(species: String) {
    4. if species.isEmpty { return nil }
    5. self.species = species
    6. }
    7. }

    您可以使用此失败的初始化程序尝试初始化新Animal实例,并检查初始化是否成功:

    1. let someCreature = Animal(species: "Giraffe")
    2. // someCreature is of type Animal?, not Animal
    3. if let giraffe = someCreature {
    4. print("An animal was initialized with a species of (giraffe.species)")
    5. }
    6. // Prints "An animal was initialized with a species of Giraffe"

    如果将空字符串值传递给失败的初始值设定项的species参数,则初始设定项将触发初始化失败:

    1. let anonymousCreature = Animal(species: "")
    2. // anonymousCreature is of type Animal?, not Animal
    3. if anonymousCreature == nil {
    4. print("The anonymous creature could not be initialized")
    5. }
    6. // Prints "The anonymous creature could not be initialized"

    注意

    检查空字符串值(例如""而不是"Giraffe")与进行检查nil以指示缺少可选 String值不同。在上面的示例中,空字符串("")是有效的,非可选的String但是,让动物使用空字符串作为其species属性值是不合适的为了对此限制建模,如果发现空字符串,则可失败的初始化程序将触发初始化失败。

    枚举失败的初始化程序

    您可以使用故障初始化程序基于一个或多个参数来选择适当的枚举用例。如果提供的参数与适当的枚举情况不匹配,则初始化器可能会失败。

    下面的例子定义称为枚举TemperatureUnit,具有三种可能的状态(kelvincelsius,和fahrenheit)。一个有故障的初始化器用于为Character代表温度符号找到合适的枚举形式:

    1. enum TemperatureUnit {
    2. case kelvin, celsius, fahrenheit
    3. init?(symbol: Character) {
    4. switch symbol {
    5. case "K":
    6. self = .kelvin
    7. case "C":
    8. self = .celsius
    9. case "F":
    10. self = .fahrenheit
    11. default:
    12. return nil
    13. }
    14. }
    15. }

    您可以使用此故障初始化程序为三种可能的状态选择合适的枚举情况,并在参数与以下状态之一不匹配时导致初始化失败:

    1. let fahrenheitUnit = TemperatureUnit(symbol: "F")
    2. if fahrenheitUnit != nil {
    3. print("This is a defined temperature unit, so initialization succeeded.")
    4. }
    5. // Prints "This is a defined temperature unit, so initialization succeeded."
    6. let unknownUnit = TemperatureUnit(symbol: "X")
    7. if unknownUnit == nil {
    8. print("This is not a defined temperature unit, so initialization failed.")
    9. }
    10. // Prints "This is not a defined temperature unit, so initialization failed."

    带有原始值的枚举失败的初始化程序

    具有原始值的枚举会自动接收一个失败的初始化器,init?(rawValue:)初始化器采用称为rawValue适当原始值类型的参数,并在找到匹配的枚举情况下选择匹配的枚举情况,如果不存在匹配值,则触发初始化失败。

    您可以TemperatureUnit从上面重写示例,以使用type的原始值Character并利用init?(rawValue:)初始化程序:

    1. enum TemperatureUnit: Character {
    2. case kelvin = "K", celsius = "C", fahrenheit = "F"
    3. }
    4. let fahrenheitUnit = TemperatureUnit(rawValue: "F")
    5. if fahrenheitUnit != nil {
    6. print("This is a defined temperature unit, so initialization succeeded.")
    7. }
    8. // Prints "This is a defined temperature unit, so initialization succeeded."
    9. let unknownUnit = TemperatureUnit(rawValue: "X")
    10. if unknownUnit == nil {
    11. print("This is not a defined temperature unit, so initialization failed.")
    12. }
    13. // Prints "This is not a defined temperature unit, so initialization failed."

    初始化失败的传播

    一个类,结构或枚举的故障初始化器可以委托同一类,结构或枚举的另一个故障初始化器。类似地,子类可故障初始化器可以委托最多超类可故障初始化器。

    在任何一种情况下,如果委托给另一个导致初始化失败的初始化程序,则整个初始化过程将立即失败,并且不会执行其他初始化代码。

    注意

    一个失败的初始化器也可以委派给一个不失败的初始化器。如果您需要将潜在的失败状态添加到不会失败的现有初始化过程中,请使用此方法。

    以下示例定义了一个Product名为的子类CartItemCartItem级车型在在线购物车的商品。CartItem引入一个称为的存储常量属性,quantity并确保该属性始终具有至少一个值1

    1. class Product {
    2. let name: String
    3. init?(name: String) {
    4. if name.isEmpty { return nil }
    5. self.name = name
    6. }
    7. }
    8. class CartItem: Product {
    9. let quantity: Int
    10. init?(name: String, quantity: Int) {
    11. if quantity < 1 { return nil }
    12. self.quantity = quantity
    13. super.init(name: name)
    14. }
    15. }

    失败的初始化程序CartItem通过验证其是否已接收到或更大quantity启动1如果quantity无效,则整个初始化过程将立即失败,并且不再执行任何初始化代码。同样,失败的初始化程序用于Product检查该name值,如果name为空字符串,则初始化程序会立即失败

    如果CartItem使用非空名称创建实例并且数量等于1或大于0,则初始化成功:

    1. if let twoSocks = CartItem(name: "sock", quantity: 2) {
    2. print("Item: (twoSocks.name), quantity: (twoSocks.quantity)")
    3. }
    4. // Prints "Item: sock, quantity: 2"

    如果您尝试创建一个值为的CartItem实例,则初始化程序将导致初始化失败:quantity0CartItem

    1. if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
    2. print("Item: (zeroShirts.name), quantity: (zeroShirts.quantity)")
    3. } else {
    4. print("Unable to initialize zero shirts")
    5. }
    6. // Prints "Unable to initialize zero shirts"

    同样,如果您尝试CartItem使用空name创建实例,则超类Product初始化程序会导致初始化失败:

    1. if let oneUnnamed = CartItem(name: "", quantity: 1) {
    2. print("Item: (oneUnnamed.name), quantity: (oneUnnamed.quantity)")
    3. } else {
    4. print("Unable to initialize one unnamed product")
    5. }
    6. // Prints "Unable to initialize one unnamed product"

    覆盖失败的初始化程序

    您可以在子类中覆盖超类可失败的初始化程序,就像其他任何初始化程序一样。或者,您可以使用子类不可失败的初始化程序来覆盖超类可失败的初始化程序。这使您可以定义一个子类,即使允许超类的初始化失败,其初始化也不会失败。

    请注意,如果使用不可失败的子类初始化器覆盖了可失败的超类初始化器,则委派给超类初始化器的唯一方法是强制展开可失败的超类初始化器的结果。

    注意

    您可以使用不可失败的初始值设定项来覆盖可失败的初始设定项,但反之则不能。

    下面的示例定义了一个名为的类Document此类可为文档建模,该文档可以使用name非空字符串值或nil,但不能为空字符串属性进行初始化

    1. class Document {
    2. var name: String?
    3. // this initializer creates a document with a nil name value
    4. init() {}
    5. // this initializer creates a document with a nonempty name value
    6. init?(name: String) {
    7. if name.isEmpty { return nil }
    8. self.name = name
    9. }
    10. }

    下一个示例定义了一个Document名为的子类AutomaticallyNamedDocumentAutomaticallyNamedDocument子类覆盖都是由引入的指定初始化的Document这些覆盖可确保AutomaticallyNamedDocument实例的初始name值是,"[Untitled]"如果该实例是在没有名称的情况下初始化的,或者是否将空字符串传递给了init(name:)初始化程序:

    1. class AutomaticallyNamedDocument: Document {
    2. override init() {
    3. super.init()
    4. self.name = "[Untitled]"
    5. }
    6. override init(name: String) {
    7. super.init()
    8. if name.isEmpty {
    9. self.name = "[Untitled]"
    10. } else {
    11. self.name = name
    12. }
    13. }
    14. }

    AutomaticallyNamedDocument覆盖其超类的failable init?(name:)与nonfailable初始化init(name:)初始化。因为AutomaticallyNamedDocument以不同于其超类的方式处理空字符串情况,所以其初始化程序不需要失败,因此它提供了初始化程序的非失败版本。

    您可以在初始化器中使用强制展开来从超类中调用可失败的初始化器,作为子类不可失败初始化器的实现的一部分。例如,UntitledDocument下面子类始终被命名为"[Untitled]",并且init(name:)在初始化期间使用其父类中的故障初始化器。

    1. class UntitledDocument: Document {
    2. override init() {
    3. super.init(name: "[Untitled]")!
    4. }
    5. }

    在这种情况下,如果init(name:)曾经用空字符串作为名称调用超类初始化程序,则强制展开操作将导致运行时错误。但是,由于使用字符串常量调用了它,因此可以看到初始化程序不会失败,因此在这种情况下不会发生运行时错误。

    初始化!初始化失败

    通常,您可以定义一个失败的初始化程序,该初始化程序通过在init关键字(init?后面放置问号来创建适当类型的可选实例另外,您可以定义一个失败的初始化程序,该初始化程序创建适当类型的隐式展开的可选实例。为此,可以在init关键字(init!后面而不是问号旁放置一个感叹号

    您可以从委托init?init!,反之亦然,你可以覆盖init?init!反之亦然。您也可以从委托initinit!,但如果这样做会引发一个断言init!初始化原因初始化失败。

    必需的初始化器

    required在类初始化器的定义之前编写修饰符,以指示该类的每个子类都必须实现该初始化器:

    1. class SomeClass {
    2. required init() {
    3. // initializer implementation goes here
    4. }
    5. }

    您还必须required在所需的初始化程序的每个子类实现之前编写修饰符,以指示初始化程序要求适用于链中的其他子类。override覆盖必需的指定初始值设定项时,您无需编写修饰符:

    1. class SomeSubclass: SomeClass {
    2. required init() {
    3. // subclass implementation of the required initializer goes here
    4. }
    5. }

    注意

    如果可以通过继承的初始化程序满足要求,则不必提供所需的初始化程序的显式实现。

    使用闭包或函数设置默认属性值

    如果存储的属性的默认值需要一些自定义或设置,则可以使用闭包或全局函数为该属性提供自定义的默认值。每当初始化属性所属类型的新实例时,都会调用闭包或函数,并将其返回值分配为属性的默认值。

    这些类型的闭包或函数通常会创建与属性相同类型的临时值,定制该值以表示所需的初始状态,然后返回该临时值以用作属性的默认值。

    这是有关如何使用闭包提供默认属性值的框架概述:

    1. class SomeClass {
    2. let someProperty: SomeType = {
    3. // create a default value for someProperty inside this closure
    4. // someValue must be of the same type as SomeType
    5. return someValue
    6. }()
    7. }

    请注意,闭包的末大括号后跟一对空括号。这告诉Swift立刻执行关闭。如果省略这些括号,则尝试将闭包本身分配给属性,而不是闭包的返回值。

    注意

    如果使用闭包来初始化属性,请记住在执行闭包时实例的其余部分尚未初始化。这意味着您无法从闭包内部访问任何其他属性值,即使这些属性具有默认值也是如此。您也不能使用隐式self属性,也不能调用实例的任何方法。

    下面的示例定义了一个名为的结构Chessboard,该结构为国际象棋的棋盘建模。国际象棋在8 x 8的棋盘上进行游戏,黑白方块交替出现。

    ../_images/chessBoard_2x.png

    为了表示此游戏板,该Chessboard结构具有一个称为的属性boardColors,该属性是64个Bool的数组true数组中的表示黑色正方形,而的值false表示白色正方形。数组中的第一项代表板上的左上角正方形,而数组中的最后一项代表板上的右下角正方形。

    boardColors使用闭包初始化数组以设置其颜色值:

    1. struct Chessboard {
    2. let boardColors: [Bool] = {
    3. var temporaryBoard = [Bool]()
    4. var isBlack = false
    5. for i in 1...8 {
    6. for j in 1...8 {
    7. temporaryBoard.append(isBlack)
    8. isBlack = !isBlack
    9. }
    10. isBlack = !isBlack
    11. }
    12. return temporaryBoard
    13. }()
    14. func squareIsBlackAt(row: Int, column: Int) -> Bool {
    15. return boardColors[(row * 8) + column]
    16. }
    17. }

    每当Chessboard创建实例时,都会执行闭包,并boardColors计算并返回默认值上面的示例中的闭包在称为的临时数组中为板上的每个正方形计算并设置适当的颜色temporaryBoard,并在完成设置后将该临时数组作为闭包的返回值返回。返回的数组值存储在其中,boardColors并可以使用squareIsBlackAt(row:column:)实用程序函数进行查询

    1. let board = Chessboard()
    2. print(board.squareIsBlackAt(row: 0, column: 1))
    3. // Prints "true"
    4. print(board.squareIsBlackAt(row: 7, column: 7))
    5. // Prints "false"
  • 相关阅读:
    数据库自动备份
    VC查找文件特定位置的记录方法
    MFC利用ADO连接ACCESS数据库及其操作数据库的方法
    VC利用ODBC连接MySql数据库的方法及其操作数据的方法
    uwsgi和nginx的故事
    A JavaScript Image Gallery
    The DOM in JavaScript
    A brief look at the Objects in JavaScript
    3 ways of including JavaScript in HTML
    #3 working with data stored in files && securing your application (PART II)
  • 原文地址:https://www.cnblogs.com/strengthen/p/9739365.html
Copyright © 2020-2023  润新知