精华内容
下载资源
问答
  • 原文标题: Init Struct Pattern原文链接: https://xaeroxe.github.io/init-struct-pattern/Introduction让我们来讨论一下Rust中复杂结构体的初始化。在这方面已经有了一些比较流行的做法,其中包括常见的pub fn new...

    原文标题: Init Struct Pattern

    原文链接: https://xaeroxe.github.io/init-struct-pattern/

    Introduction

    让我们来讨论一下Rust中复杂结构体的初始化。在这方面已经有了一些比较流行的做法,其中包括常见的pub fn new()和生成器模式(builder pattern)。在这篇文章中,我会对比这些方式,并提出一种新的模式,我称之为初始化结构模式(Init Struct Pattern)

    New

    初始化结构体最开始也是最常见的方式是通过在结构体中声明一个签名如下面这样的函数。

    pub fn new() -> Self {
    Self {
    // init me
    }
    }

    这是相当直观的,并且对于简单的结构体也很适用。但是当结构体逐渐变得复杂的时候,它开始出现一些问题。例如:

    impl YakShaver {
    pub fn new(clipper_size: u32, gas_powered_clippers: bool, solar_powered_clippers: bool, color_to_dye_yak: &str) -> Self {
    // Body is irrelevant
    }
    }

    // In some other file, or maybe even another crate we now have to construct this type.
    // Unless you've looked at the definition for `fn new` recently, you might not remember that the second argument
    // creates some CO2 emissions if flipped.

    let yak_shaver = YakShaver::new(3, false, true, "magenta");

    这个模式还有另一个问题,它会让事物在不必要的时候打破变化。例如,我知道我的大多数用户想要他们的牦牛帆船(yak clippers)是黑色的,为什么你想要点不一样的呢?Bob想要点不一样的。Bob是我们的下游用户,他对帆船的颜色有意见。他想要红色的帆船。好,现在让我们为帆船的颜色添加一个参数:

    impl YakShaver {
    pub fn new(...same as above..., clipper_color: &str) -> Self {
    // Body is irrelevant
    }
    }

    let yak_shaver = YakShaver::new(3, false, true, "magenta", "red");

    但是现在我们有了一个问题。尽管超过99%的用户都想要帆船的颜色是黑色的,但是所有人都得指定帆船的颜色。这看起来愚蠢且啰嗦。而且直到下一个大版本之前,我们还不能发布Bob的新功能。否则,我们就会破坏所有人的代码。Bob对于要等一段时间感到非常不开心,显然我们也不太想让我们所有的用户都去指定颜色参数。
    生成器模式(build pattern)可以解决这个问题。我们可以在将来精心策划来避免这种问题。

    Builder Pattern

    生成器很优雅,因为它不需要我们在构建结构体的时候指定所有的东西,这意味着我们可以在一个小版本里发布Bob要的功能而不用破坏任何东西。它还在每个字段加上自己名字,这让代码变得更具有可读性。例如:

    pub struct YakShaverBuilder {
    clipper_size: u32,
    gas_powered_clippers: bool,
    solar_powered_clippers: bool,
    color_to_dye_yak: String,
    clipper_color: String,
    }

    impl YakShaverBuilder {
    pub fn new() -> Self {
    Self {
    clipper_size: 3,
    gas_powered_clippers: false,
    solar_powered_clippers: true,
    color_to_dye_yak: String::from("brown"),
    clipper_color: String::from("black"),
    }
    }

    pub fn clipper_size(mut self, v: u32) -> Self {
    self.clipper_size = v;
    self
    }

    pub fn gas_powered_clippers(mut self, v: bool) -> Self {
    self.gas_powered_clippers = v;
    self
    }

    pub fn solar_powered_clippers(mut self, v: bool) -> Self {
    self.solar_powered_clippers = v;
    self
    }

    pub fn color_to_dye_yak(mut self, v: String) -> Self {
    self.color_to_dye_yak = v;
    self
    }

    pub fn clipper_color(mut self, v: String) -> Self {
    self.clipper_color = v;
    self
    }

    pub fn build(self) -> YakShaver {
    YakShaver {
    clipper_size: self.clipper_size,
    gas_powered_clippers: self.gas_powered_clippers,
    solar_powered_clippers: self.solar_powered_clippers,
    color_to_dye_yak: self.color_to_dye_yak,
    clipper_color: self.clipper_color,
    }
    }
    }

    let yak_shaver = YakShaverBuilder::new()
    .clipper_size(4)
    .color_to_dye_yak(String::from("hot pink"))
    .clipper_color(String::from("red"))
    .build();

    但是这又暴露除了生成器模式的一个很大的缺点。它很啰嗦。代码量大约是pub fn new()->Self的两到三倍,当然这取决于你的统计方式。所以对于比较简单的结构体来讲,生成器模式可能过犹不及,但是对于大型结构体来讲,与带来的诸多好处相比,它的缺点则可以忽略不计。那么有没有两全其美的办法呢?我希望我的下一个建议可以做到。

    Init Struct Pattern

    我们可以组合Rust的一些特性来得到与生成器模式相同的好处,但是又不用那么啰嗦。我会从一个例子开始:

    pub struct YakShaverInit {
    pub clipper_size: u32,
    pub gas_powered_clippers: bool,
    pub solar_powered_clippers: bool,
    pub color_to_dye_yak: String,
    pub clipper_color: String,
    #[doc(hidden)]
    pub __non_exhaustive: () // This is a hack, we might be able to stop using it in the future.
    }

    impl Default for YakShaverInit {
    fn default() -> Self {
    Self {
    clipper_size: 3,
    gas_powered_clippers: false,
    solar_powered_clippers: true,
    color_to_dye_yak: String::from("brown"),
    clipper_color: String::from("black"),
    }
    }
    }

    impl YakShaverInit {
    pub fn init(self) -> YakShaver {
    YakShaver {
    clipper_size: self.clipper_size,
    gas_powered_clippers: self.gas_powered_clippers,
    solar_powered_clippers: self.solar_powered_clippers,
    color_to_dye_yak: self.color_to_dye_yak,
    clipper_color: self.clipper_color,
    }
    }
    }

    let yak_shaver = YakShaverInit {
    clipper_size: 4,
    color_to_dye_yak: String::from("hot pink"),
    clipper_color: String::from("red"),
    ..Default::default(),
    }.init();

    这看起来和生成器模式很像!的确,它也具备很多相同的好处。我们不需要为每个字段定义函数,并且它也不需要多次返回Self。如果我们的初始化需要用给定的输入来完成复杂的工作,那么可以在fn init()里面完成。所以我们不需要发布大版本就可以在结构体中添加新字段,我们不需要所有人都指定全部字段,并且相比于生成器模式,我们的定义已经精简了很多。我觉得这是个胜利!

    我在这里使用了一些可能并不是所有人都熟悉的特性(features)。..Default::default()是什么?这个叫做结构体更新语法(struct update syntax),它告诉编译器从impl Default for YakShaverInit中定义的Default::fault()的输出结果拷贝所有剩余的字段。其中还在一个pub字段上使用了#[doc(hidden)]属性,这是在文档中隐藏这个字段,从而不鼓励人们在对YakShaverInit进行结构体初始化的时候添加这个字段。如果他们添加了这字段,然后结构体构造可能提前结束从而无法执行在尾部指定的..Default::default(),那意味着如果我们在YakShaverInit结构体中添加新字段,别人的代码就会被破坏。我们现在还无法阻止这种情况,只能不鼓励使用。如果Rust能为我们添加更多的方式来使用non exhaustive 结构体,即用#[non_exhaustive]定义的结构体,我们可能就能在将来更简便地阻止这种情况。如果大家喜欢这个方法,我可能会写一个相关的rust-lang RFCs来让它进一步变得可能。

    在初始化结构中添加其他的私有字段也会打破变化!所有的字段都必须是公开的(public)。这似乎很愚蠢,因为下面的代码是合法的。

    pub struct HalfPublic {
    pub a: i32,
    b: u32,
    }

    impl Default for HalfPublic {
    fn default() -> Self {
    Self {
    a: 0,
    b: 0,
    }
    }
    }

    let mut half_public = HalfPublic::default();
    half_public.a = 10;

    所以我准备去写一个RFC给rust-lang,建议下面的代码是合法的:

    let half_public = HalfPublic {
    a: 10,
    ..Default::default(),
    }

    请留意我提到的RFCs,不管怎样,我希望你喜欢这篇文章,如果有任何建议或者意见请通过邮箱kieseljake+blog@gmail.com发送给我。

    欢迎关注我的微信公众号:

    ca45e1d06a57e116a361714328dbdedd.png

    展开全文
  • 初始化 初始化是准备使用的类,结构或枚举实例的过程。此过程涉及为该实例上的每个存储属性设置一个初始值,并执行新实例准备使用之前所需的任何其他设置或初始化。 您可以通过定义初始值设定项来实现此初始化过程,...

    初始化

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

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

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

    1. 设置存储属性的初始值

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

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

    注意

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

    1.1 初始化器

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

    init() {
        // perform some initialization here
    }
    

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

    struct Fahrenheit {
        var temperature: Double
        init() {
            temperature = 32.0
        }
    }
    var f = Fahrenheit()
    print("The default temperature is \(f.temperature)° Fahrenheit")
    // Prints "The default temperature is 32.0° Fahrenheit"
    

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

    1.2 默认属性值

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

    注意

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

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

    struct Fahrenheit {
        var temperature = 32.0
    }
    

    2 自定义初始化

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

    2.1 初始化参数

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

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

    struct Celsius {
        var temperatureInCelsius: Double
        init(fromFahrenheit fahrenheit: Double) {
            temperatureInCelsius = (fahrenheit - 32.0) / 1.8
        }
        init(fromKelvin kelvin: Double) {
            temperatureInCelsius = kelvin - 273.15
        }
    }
    let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
    // boilingPointOfWater.temperatureInCelsius is 100.0
    let freezingPointOfWater = Celsius(fromKelvin: 273.15)
    // freezingPointOfWater.temperatureInCelsius is 0.0
    

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

    2.2 参数名称和参数标签

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

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

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

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

    struct Color {
        let red, green, blue: Double
        init(red: Double, green: Double, blue: Double) {
            self.red   = red
            self.green = green
            self.blue  = blue
        }
        init(white: Double) {
            red   = white
            green = white
            blue  = white
        }
    }
    

    Color通过为每个初始化器参数提供命名值,可以使用这两个初始化器来创建新实例:

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

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

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

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

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

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

    struct Celsius {
        var temperatureInCelsius: Double
        init(fromFahrenheit fahrenheit: Double) {
            temperatureInCelsius = (fahrenheit - 32.0) / 1.8
        }
        init(fromKelvin kelvin: Double) {
            temperatureInCelsius = kelvin - 273.15
        }
        init(_ celsius: Double) {
            temperatureInCelsius = celsius
        }
    }
    let bodyTemperature = Celsius(37.0)
    // bodyTemperature.temperatureInCelsius is 37.0
    

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

    2.4 可选属性类型

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

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

    class SurveyQuestion {
        var text: String
        var response: String?
        init(text: String) {
            self.text = text
        }
        func ask() {
            print(text)
        }
    }
    let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
    cheeseQuestion.ask()
    // Prints "Do you like cheese?"
    cheeseQuestion.response = "Yes, I do like cheese."
    

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

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

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

    注意

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

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

    class SurveyQuestion {
        let text: String
        var response: String?
        init(text: String) {
            self.text = text
        }
        func ask() {
            print(text)
        }
    }
    let beetsQuestion = SurveyQuestion(text: "How about beets?")
    beetsQuestion.ask()
    // Prints "How about beets?"
    beetsQuestion.response = "I also like beets. (But not with cheese.)"
    

    3 默认初始化器

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

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

    class ShoppingListItem {
        var name: String?
        var quantity = 1
        var purchased = false
    }
    var item = ShoppingListItem()
    

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

    3.1 结构类型的成员初始化器

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

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

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

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

    struct Size {
        var width = 0.0, height = 0.0
    }
    let twoByTwo = Size(width: 2.0, height: 2.0)
    

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

    let zeroByTwo = Size(height: 2.0)
    print(zeroByTwo.width, zeroByTwo.height)
    // Prints "0.0 2.0"
    
    let zeroByZero = Size()
    print(zeroByZero.width, zeroByZero.height)
    // Prints "0.0 0.0"
    

    4 值类型的初始化程序委托

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

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

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

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

    注意

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

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

    struct Size {
        var width = 0.0, height = 0.0
    }
    struct Point {
        var x = 0.0, y = 0.0
    }
    

    您可以通过Rect以下三种方式之一来初始化下面的结构:使用其默认的零初始化值origin和size属性值,提供特定的原点和尺寸,或提供特定的中心点和尺寸。这些初始化选项由Rect结构定义中的三个自定义初始化器表示:

    struct Rect {
        var origin = Point()
        var size = Size()
        init() {}
        init(origin: Point, size: Size) {
            self.origin = origin
            self.size = size
        }
        init(center: Point, size: Size) {
            let originX = center.x - (size.width / 2)
            let originY = center.y - (size.height / 2)
            self.init(origin: Point(x: originX, y: originY), size: size)
        }
    }
    

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

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

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

    let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
                          size: Size(width: 5.0, height: 5.0))
    // 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:)初始化器,该初始化器将新的originsize值存储在适当的属性中:

    let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                          size: Size(width: 3.0, height: 3.0))
    // 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

    5 类继承和初始化

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

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

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

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

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

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

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

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

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

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

    init(parameters) {
        statements
    }
    

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

    convenience init(parameters) {
        statements
    }
    
    

    5.3 类类型的初始化程序委托

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

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

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

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

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

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

    注意

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

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

    5.4 两阶段初始化

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

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

    注意

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

    Swift的编译器执行四项有用的安全检查,以确保两阶段初始化完成且没有错误:
    安全检查1
    指定的初始化程序在将其委托给超类初始化程序之前,必须确保初始化其类引入的所有属性。
    如上所述,仅在知道对象所有存储属性的初始状态后,才认为该对象的内存已完全初始化。为了满足此规则,指定的初始值设定项必须确保在传递链之前初始化其自身的所有属性。

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

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

    阶段1

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

    阶段2

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

    以下是第1阶段寻找假设的子类和超类的初始化调用的方式:
    在这里插入图片描述
    在此示例中,初始化始于对子类的便捷初始化程序的调用。此便捷初始化程序尚无法修改任何属性。它委托来自同一类的指定初始化器。

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

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

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

    以下是第2阶段寻找相同初始化调用的方式:
    在这里插入图片描述
    现在,超类的指定初始化器有机会进一步自定义实例(尽管不必如此)。

    一旦超类的指定初始值设定项完成,子类的指定初始值设定项即可执行其他自定义(尽管不必如此)。

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

    5.5 初始化程序的继承和覆盖

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

    注意

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

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

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

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

    注意

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

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

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

    class Vehicle {
        var numberOfWheels = 0
        var description: String {
            return "\(numberOfWheels) wheel(s)"
        }
    }
    

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

    let vehicle = Vehicle()
    print("Vehicle: \(vehicle.description)")
    // Vehicle: 0 wheel(s)
    

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

    class Bicycle: Vehicle {
        override init() {
            super.init()
            numberOfWheels = 2
        }
    }
    

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

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

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

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

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

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

    class Hoverboard: Vehicle {
        var color: String
        init(color: String) {
            self.color = color
            // super.init() implicitly called here
        }
        override var description: String {
            return "\(super.description) in a beautiful \(color)"
        }
    }
    

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

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

    注意

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

    5.6 自动初始化程序继承

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

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

    规则1
    如果您的子类没有定义任何指定的初始值设定项,它将自动继承其所有超类指定的初始值设定项。
    规则二
    如果您的子类提供了其所有超类指定初始化器的实现(通过按规则1继承它们,或通过提供自定义实现作为其定义的一部分),那么它将自动继承所有超类便利性初始化器。
    即使您的子类添加了进一步的便捷初始化程序,这些规则也适用。

    注意

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

    5.7 指定的便捷初始化器

    下面的示例显示了指定的初始化程序,便捷初始化程序和自动初始化程序继承的运行方式。这个例子定义了三类所谓的层次Food,RecipeIngredient和ShoppingListItem,并演示了如何自己初始化互动。

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

    class Food {
        var name: String
        init(name: String) {
            self.name = name
        }
        convenience init() {
            self.init(name: "[Unnamed]")
        }
    }
    

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

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

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

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

    init(name: String)FoodFoodFoodinit(name: String)super.init()
    

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

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

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

    class RecipeIngredient: Food {
        var quantity: Int
        init(name: String, quantity: Int) {
            self.quantity = quantity
            super.init(name: name)
        }
        override convenience init(name: String) {
            self.init(name: name, quantity: 1)
        }
    }
    

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

    RecipeIngredient还定义了一个便利的初始化程序,该初始化程序仅用于按名称创建实例。对于所有创建的实例,该便利初始化程序均假定其数量为,而没有显式数量。此便捷初始化程序的定义使实例创建起来更快捷,更便捷,并且在创建多个单量实例时避免了代码重复。这种便利初始化跨越简单地委托类的指定初始化,传递一个值。init(name: String)RecipeIngredient

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

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

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

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

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

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

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

    class ShoppingListItem: RecipeIngredient {
        var purchased = false
        var description: String {
            var output = "\(quantity) x \(name)"
            output += purchased ? " ✔" : " ✘"
            return output
        }
    }
    

    注意

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

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

    下图显示了所有三个类的整体初始化程序链:
    在这里插入图片描述
    您可以使用所有继承的三个初始化器来创建一个新ShoppingListItem实例:

    var breakfastList = [
        ShoppingListItem(),
        ShoppingListItem(name: "Bacon"),
        ShoppingListItem(name: "Eggs", quantity: 6),
    ]
    breakfastList[0].name = "Orange juice"
    breakfastList[0].purchased = true
    for item in breakfastList {
        print(item.description)
    }
    // 1 x Orange juice ✔
    // 1 x Bacon ✘
    // 6 x Eggs ✘
    
    

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

    6 初始化失败

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

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

    注意

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

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

    注意

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

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

    let wholeNumber: Double = 12345.0
    let pi = 3.14159
    
    if let valueMaintained = Int(exactly: wholeNumber) {
        print("\(wholeNumber) conversion to Int maintains value of \(valueMaintained)")
    }
    // Prints "12345.0 conversion to Int maintains value of 12345"
    
    let valueChanged = Int(exactly: pi)
    // valueChanged is of type Int?, not Int
    
    if valueChanged == nil {
        print("\(pi) conversion to Int does not maintain value")
    }
    // Prints "3.14159 conversion to Int does not maintain value"
    
    

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

    struct Animal {
        let species: String
        init?(species: String) {
            if species.isEmpty { return nil }
            self.species = species
        }
    }
    

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

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

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

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

    注意

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

    6.1 枚举失败的初始化程序

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

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

    enum TemperatureUnit {
        case kelvin, celsius, fahrenheit
        init?(symbol: Character) {
            switch symbol {
            case "K":
                self = .kelvin
            case "C":
                self = .celsius
            case "F":
                self = .fahrenheit
            default:
                return nil
            }
        }
    }
    
    

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

    let fahrenheitUnit = TemperatureUnit(symbol: "F")
    if fahrenheitUnit != nil {
        print("This is a defined temperature unit, so initialization succeeded.")
    }
    // Prints "This is a defined temperature unit, so initialization succeeded."
    
    let unknownUnit = TemperatureUnit(symbol: "X")
    if unknownUnit == nil {
        print("This is not a defined temperature unit, so initialization failed.")
    }
    // Prints "This is not a defined temperature unit, so initialization failed."
    
    

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

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

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

    enum TemperatureUnit: Character {
        case kelvin = "K", celsius = "C", fahrenheit = "F"
    }
    
    let fahrenheitUnit = TemperatureUnit(rawValue: "F")
    if fahrenheitUnit != nil {
        print("This is a defined temperature unit, so initialization succeeded.")
    }
    // Prints "This is a defined temperature unit, so initialization succeeded."
    
    let unknownUnit = TemperatureUnit(rawValue: "X")
    if unknownUnit == nil {
        print("This is not a defined temperature unit, so initialization failed.")
    }
    // Prints "This is not a defined temperature unit, so initialization failed."
    
    

    6.3 初始化失败的传播

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

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

    注意

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

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

    class Product {
        let name: String
        init?(name: String) {
            if name.isEmpty { return nil }
            self.name = name
        }
    }
    
    class CartItem: Product {
        let quantity: Int
        init?(name: String, quantity: Int) {
            if quantity < 1 { return nil }
            self.quantity = quantity
            super.init(name: name)
        }
    }
    
    

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

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

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

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

    if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
        print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
    } else {
        print("Unable to initialize zero shirts")
    }
    // Prints "Unable to initialize zero shirts"
    
    

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

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

    6.4 覆盖失败的初始化程序

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

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

    注意

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

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

    class Document {
        var name: String?
        // this initializer creates a document with a nil name value
        init() {}
        // this initializer creates a document with a nonempty name value
        init?(name: String) {
            if name.isEmpty { return nil }
            self.name = name
        }
    }
    
    

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

    class AutomaticallyNamedDocument: Document {
        override init() {
            super.init()
            self.name = "[Untitled]"
        }
        override init(name: String) {
            super.init()
            if name.isEmpty {
                self.name = "[Untitled]"
            } else {
                self.name = name
            }
        }
    }
    
    

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

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

    class UntitledDocument: Document {
        override init() {
            super.init(name: "[Untitled]")!
        }
    }
    

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

    6.5 初始化!初始化失败

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

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

    7 required必需的初始化器

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

    class SomeClass {
        required init() {
            // initializer implementation goes here
        }
    }
    

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

    class SomeSubclass: SomeClass {
        required init() {
            // subclass implementation of the required initializer goes here
        }
    }
    

    注意

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

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

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

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

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

    class SomeClass {
        let someProperty: SomeType = {
            // create a default value for someProperty inside this closure
            // someValue must be of the same type as SomeType
            return someValue
        }()
    }
    

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

    注意

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

    下面的示例定义了一个名为的结构Chessboard,该结构为国际象棋的棋盘建模。国际象棋在8 x 8的棋盘上进行游戏,黑白方块交替出现。
    在这里插入图片描述
    为了表示此游戏板,该Chessboard结构具有一个称为的属性boardColors,该属性是64个Bool值的数组。true数组中的值表示黑色正方形,而的值false表示白色正方形。阵列中的第一项代表板上的左上角正方形,而阵列中的最后一项代表板上的右下角正方形。

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

    struct Chessboard {
        let boardColors: [Bool] = {
            var temporaryBoard = [Bool]()
            var isBlack = false
            for i in 1...8 {
                for j in 1...8 {
                    temporaryBoard.append(isBlack)
                    isBlack = !isBlack
                }
                isBlack = !isBlack
            }
            return temporaryBoard
        }()
        func squareIsBlackAt(row: Int, column: Int) -> Bool {
            return boardColors[(row * 8) + column]
        }
    }
    

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

    let board = Chessboard()
    print(board.squareIsBlackAt(row: 0, column: 1))
    // Prints "true"
    print(board.squareIsBlackAt(row: 7, column: 7))
    // Prints "false"
    

    参考

    https://docs.swift.org/swift-book/LanguageGuide/Initialization.html

    展开全文
  • 原文标题: Init Struct Pattern 原文链接: https://xaeroxe.github.io/init-struct-pattern/Introduction让我们来讨论一下Rust中复杂结构体的初始化。在这方面已经有了一些比较流行的做法,其中包括常见的pub fn new...

    81bea5d5bd032850b8da29a7f9131597.png
    原文标题: Init Struct Pattern
    原文链接: https://xaeroxe.github.io/init-struct-pattern/

    Introduction

    让我们来讨论一下Rust中复杂结构体的初始化。在这方面已经有了一些比较流行的做法,其中包括常见的pub fn new()和生成器模式(builder pattern)。在这篇文章中,我会对比这些方式,并提出一种新的模式,我称之为初始化结构模式(Init Struct Pattern)

    New

    初始化结构体最开始也是最常见的方式是通过在结构体中声明一个签名如下面这样的函数。

    pub fn new() -> Self {
        Self {
            // init me
        }
    }
    

    这是相当直观的,并且对于简单的结构体也很适用。但是当结构体逐渐变得复杂的时候,它开始出现一些问题。例如:

    impl YakShaver {
        pub fn new(clipper_size: u32, gas_powered_clippers: bool, solar_powered_clippers: bool, color_to_dye_yak: &str) -> Self {
            // Body is irrelevant
        }
    }
    
    // In some other file, or maybe even another crate we now have to construct this type.
    // Unless you've looked at the definition for `fn new` recently, you might not remember that the second argument
    // creates some CO2 emissions if flipped.
    
    let yak_shaver = YakShaver::new(3, false, true, "magenta");
    

    这个模式还有另一个问题,它会让事物在不必要的时候打破变化。例如,我知道我的大多数用户想要他们的牦牛帆船(yak clippers)是黑色的,为什么你想要点不一样的呢?Bob想要点不一样的。Bob是我们的下游用户,他对帆船的颜色有意见。他想要红色的帆船。好,现在让我们为帆船的颜色添加一个参数:

    impl YakShaver {
        pub fn new(...same as above..., clipper_color: &str) -> Self {
            // Body is irrelevant
        }
    }
    
    let yak_shaver = YakShaver::new(3, false, true, "magenta", "red");
    

    但是现在我们有了一个问题。尽管超过99%的用户都想要帆船的颜色是黑色的,但是所有人都得指定帆船的颜色。这看起来愚蠢且啰嗦。而且直到下一个大版本之前,我们还不能发布Bob的新功能。否则,我们就会破坏所有人的代码。Bob对于要等一段时间感到非常不开心,显然我们也不太想让我们所有的用户都去指定颜色参数。
    生成器模式(build pattern)可以解决这个问题。我们可以在将来精心策划来避免这种问题。

    Builder Pattern

    生成器很优雅,因为它不需要我们在构建结构体的时候指定所有的东西,这意味着我们可以在一个小版本里发布Bob要的功能而不用破坏任何东西。它还在每个字段加上自己名字,这让代码变得更具有可读性。例如:

    pub struct YakShaverBuilder {
        clipper_size: u32,
        gas_powered_clippers: bool,
        solar_powered_clippers: bool,
        color_to_dye_yak: String,
        clipper_color: String,
    }
    
    impl YakShaverBuilder {
        pub fn new() -> Self {
            Self {
                clipper_size: 3,
                gas_powered_clippers: false,
                solar_powered_clippers: true,
                color_to_dye_yak: String::from("brown"),
                clipper_color: String::from("black"),
            }
        }
    
        pub fn clipper_size(mut self, v: u32) -> Self {
            self.clipper_size = v;
            self
        }
    
        pub fn gas_powered_clippers(mut self, v: bool) -> Self {
            self.gas_powered_clippers = v;
            self
        }
    
        pub fn solar_powered_clippers(mut self, v: bool) -> Self {
            self.solar_powered_clippers = v;
            self
        }
    
        pub fn color_to_dye_yak(mut self, v: String) -> Self {
            self.color_to_dye_yak = v;
            self
        }
    
        pub fn clipper_color(mut self, v: String) -> Self {
            self.clipper_color = v;
            self
        }
    
        pub fn build(self) -> YakShaver {
            YakShaver {
                clipper_size: self.clipper_size,
                gas_powered_clippers: self.gas_powered_clippers,
                solar_powered_clippers: self.solar_powered_clippers,
                color_to_dye_yak: self.color_to_dye_yak,
                clipper_color: self.clipper_color,
            }
        }
    }
    
    let yak_shaver = YakShaverBuilder::new()
        .clipper_size(4)
        .color_to_dye_yak(String::from("hot pink"))
        .clipper_color(String::from("red"))
        .build();
    

    但是这又暴露除了生成器模式的一个很大的缺点。它很啰嗦。代码量大约是pub fn new()->Self的两到三倍,当然这取决于你的统计方式。所以对于比较简单的结构体来讲,生成器模式可能过犹不及,但是对于大型结构体来讲,与带来的诸多好处相比,它的缺点则可以忽略不计。那么有没有两全其美的办法呢?我希望我的下一个建议可以做到。

    Init Struct Pattern

    我们可以组合Rust的一些特性来得到与生成器模式相同的好处,但是又不用那么啰嗦。我会从一个例子开始:

    pub struct YakShaverInit {
        pub clipper_size: u32,
        pub gas_powered_clippers: bool,
        pub solar_powered_clippers: bool,
        pub color_to_dye_yak: String,
        pub clipper_color: String,
        #[doc(hidden)]
        pub __non_exhaustive: () // This is a hack, we might be able to stop using it in the future.
    }
    
    impl Default for YakShaverInit {
        fn default() -> Self {
            Self {
                clipper_size: 3,
                gas_powered_clippers: false,
                solar_powered_clippers: true,
                color_to_dye_yak: String::from("brown"),
                clipper_color: String::from("black"),
            }
        }
    }
    
    impl YakShaverInit {
        pub fn init(self) -> YakShaver {
            YakShaver {
                clipper_size: self.clipper_size,
                gas_powered_clippers: self.gas_powered_clippers,
                solar_powered_clippers: self.solar_powered_clippers,
                color_to_dye_yak: self.color_to_dye_yak,
                clipper_color: self.clipper_color,
            }
        }
    }
    
    let yak_shaver = YakShaverInit {
        clipper_size: 4,
        color_to_dye_yak: String::from("hot pink"),
        clipper_color: String::from("red"),
        ..Default::default(),
    }.init();
    

    这看起来和生成器模式很像!的确,它也具备很多相同的好处。我们不需要为每个字段定义函数,并且它也不需要多次返回Self。如果我们的初始化需要用给定的输入来完成复杂的工作,那么可以在fn init()里面完成。所以我们不需要发布大版本就可以在结构体中添加新字段,我们不需要所有人都指定全部字段,并且相比于生成器模式,我们的定义已经精简了很多。我觉得这是个胜利!

    我在这里使用了一些可能并不是所有人都熟悉的特性(features)。..Default::default()是什么?这个叫做结构体更新语法(struct update syntax),它告诉编译器从impl Default for YakShaverInit中定义的Default::fault()的输出结果拷贝所有剩余的字段。其中还在一个pub字段上使用了#[doc(hidden)]属性,这是在文档中隐藏这个字段,从而不鼓励人们在对YakShaverInit进行结构体初始化的时候添加这个字段。如果他们添加了这字段,然后结构体构造可能提前结束从而无法执行在尾部指定的..Default::default(),那意味着如果我们在YakShaverInit结构体中添加新字段,别人的代码就会被破坏。我们现在还无法阻止这种情况,只能不鼓励使用。如果Rust能为我们添加更多的方式来使用non exhaustive 结构体,即用#[non_exhaustive]定义的结构体,我们可能就能在将来更简便地阻止这种情况。如果大家喜欢这个方法,我可能会写一个相关的rust-lang RFCs来让它进一步变得可能。

    在初始化结构中添加其他的私有字段也会打破变化!所有的字段都必须是公开的(public)。这似乎很愚蠢,因为下面的代码是合法的。

    pub struct HalfPublic {
        pub a: i32,
        b: u32,
    }
    
    impl Default for HalfPublic {
        fn default() -> Self {
            Self {
                a: 0,
                b: 0,
            }
        }
    }
    
    let mut half_public = HalfPublic::default();
    half_public.a = 10;
    

    所以我准备去写一个RFC给rust-lang,建议下面的代码是合法的:

    let half_public = HalfPublic {
        a: 10,
        ..Default::default(),
    }
    

    请留意我提到的RFCs,不管怎样,我希望你喜欢这篇文章,如果有任何建议或者意见请通过邮箱kieseljake+blog@gmail.com发送给我。

    欢迎关注我的微信公众号:

    11b3522aae4f7d2bee1c826e4ddc9021.png
    展开全文
  • 本文仅用于学习记录,以下内容均为学习过程中,翻译记录所得,由于从word复制...在本节,我们提出了一个强壮的初始化方法,为训练非常深的rectifier networks扫清障碍。  最近的深度卷积网络大多通过随机符合高斯...

    本文仅用于学习记录,以下内容均为学习过程中,翻译记录所得,由于从word复制过来,公式出错,所以请结合原论文公式看。

    相比较于传统的sigmoid激活函数的网络,修正网络更容易去训练。但是一个不好的初始化可能在高度非线性的系统中阻碍学习。在本节,我们提出了一个强壮的初始化方法,为训练非常深的rectifier networks扫清障碍。

           最近的深度卷积网络大多通过随机符合高斯分布的权重来初始化参数。在VGG小组的报道和我们自己的实验中观察到非常深的模型()8层卷积层)难以收敛由于固定了标准差(0.01)。为了解决这个问题,VGG小组预训练了8层的卷积网络来初始化更深的模型。但是这种策略需要大量的训练时间,并且有可能导致较差局部最优。也有通过在中间层添加辅助分类器来帮助收敛。

           Glorot and Bengio 提出采用按适当比例缩小的均匀分作来初始化。这被称为“Xavier”初始化。他的推导是基于假设激活是线性的,这种假设对于ReLU和PReLU无效。

           接下来,我们从理论上推导出一种更适合ReLU和PReLU的初始化,在我们的试验中,我们的初始化方法可以使非常深的网络收敛(30层卷积或者全连接层),而‘Xavier’不行。

     

    前向传播的例子

           我们的推导式基于‘Xavier’的推导,中心思想是去研究每一层对策的方差。

           对于一个卷积层,一种对策是:

                  yl=Wlxl+bl

           这里的x 是一个k2c * 1的向量,就是对应的k*k*c的卷积核变成1维的向量,c为通道数,k是这层空间滤波器的大小。设n=k2c ,其中W 是一个d*n 的矩阵,d 是滤波器的个数,W 每一列是一个滤波器的权值,b 是向量的偏置,然后y 是输出层的像素向量。我们用l 来表示一层。我们得到xl=f(yl-1) ,其中f 是激活函数,我们还有cl=dl-1

           我们让Wl 中的初始化元素独立同分布。像‘Xavier’中一样,我们也假设xl  中的元素也是独立同分布的。且xlWl 相对独立。我们可以得到:

    Varyl=nlVar[wlxl]

             其中yl , xl , wl 分别是随机变量的元素。我们让wl 是零均值,可以得到:

    Varyl=nlVarwlE[xl2]

             其中E[xl2]xl2 的期望,我们不难得出E[xl2]≠Var[xl] 除非xl 为零均值。对于ReLU激活:xl=max⁡(0,yl-1) ,并且不是零均值,这里我们得到了论文[7]中不一样的结论。

             如果我们让wl-1 在0周围对称分布,并且bl-1=0 ,则yl-1 有零均值,并且在0周围对称分布。这就会导致Exl2=12Var[yl-1] ,当激活函数为ReLU的时候,将这个带入上式:

    Varyl=12nlVarwlVaryl-1

    将L层放在一起,我们得到:

    VaryL=Var[y1]l=2L12nlVarwl

    这个公式是初始化设计的关键,一个合适的初始化方式应该避免以指数形式减小或放大输入信号的大小,所以我们希望上面的式子可以有一个合适的标量(比如1),充分条件是:

    12nlVarwl=1  ∀l

    这会形成一个零均值的高斯分布,标准偏差为2n  ,这是我们初始化的方式,我们初始化

    b=0

             对于第一层来说(l=1 ),我们应该让n1Varw1=1 ,因为输入信号没有应用ReLU激活函数,但是1/2仅仅存在于1层,那么他就不重要了,所以方便起见,我们也在第一层使用上面的式子。

     

    反向传播的例子:

             对于反向传播来说,一个卷积层的梯度计算可以用公式:

    xl=Wlyl

             其中的∆xy 分别是梯度(∂εx∂ε∂y )的简单表示,y 表示k*k 像素,有d 个通道,被重新塑造成一个k2d*1 的向量,我们设n=k2d ,并且nn=k2c 那么W 就是一个c*n 的矩阵,是滤波器在反向传播中的重新排列。可以得到WW 可以通过转换得到彼此。∆x 是一个c*1 的向量,表示该一个像素在该层的梯度。综上,我们假设wlyl 相互独立,xl 对于所有l 都是零均值,wl 初始化为在0两边均匀分布。

             在反向传播中,我们依然存在yl=f'(yl)∆xl+1 ,其中f'f 的导数,对于ReLU来说f'(yl) 等于0或1,并且两者概率相等,我们假设f'(yl)xl+1 是相互独立的,因此我们可以得到:Eyl=Exl+12=0 ,并且有Eyl2=Varyl=12Var[∆xl+1] ,然后,我们计算
    xl=Wlyl
    梯度的方差:

    Varxl=nlVarwlVaryl=12nlVarwlVar[∆xl+1]

    我们可以发现1/2 在
    Varyl=12nlVarwlVaryl-1

    Varxl=nlVarwlVaryl=12nlVarwlVar[∆xl+1]

    两个式子中都存在,虽然推导方式不同,对于L层的输入,我们有:
    Varx2=Var[xL+1]l=2L12nlVarwl

    我们考虑一个充分条件,使得梯度不是呈指数增长的:

    12nlVarwl=1 ∀l

    这个式子和

    12nlVarwl=1  ∀l

    唯一的不同在于nl=kl2dlnl=kl2cl=kl2dl-1 .

    式子
    12nlVarwl=1 ∀l

    的结果是一个均值为0,标准差为2n 的高斯分布。

    对于第一层(l=1 ),我们需要去计算xl ,因为他表示的是图像域,但是我们在第一层还是采用上面的式子,和前向传播的原因相同(一层的系数 不会使得全部的乘积呈指数增长趋势)。

    对于上述的两个充分条件,我们只能单独使用,因为,如果我们使用了
    12nlVarwl=1 ∀l

    那么

    l=2L12nlVarwl=1

    但是,由于nl=kl2dlnl=kl2cl=kl2dl-1

    l=2L12nlVarwl=l=2Lnlnl=l=2Lcldl=c2dL

    这个乘积在一般的网络中不是递减的,也就是说如果我们可以找到一个对于反向传播好的初始化的比例,这也是前向的信号的情况,反之亦然,本文中所有的模型,两种形式都可以使得他们收敛。

     

    讨论:

           如果前向/后向信号在每一层是一个不合适的比例缩放β 的话,在L层之后的反向信号将被缩放βL 倍,L 可以代表一些层,也可以代表所有层。当L 很大时,如果β>1 ,将被极度放大,最后会输出无穷,如果β<1 的话,将会被极度缩小,也就是说,这个算法将不再收敛,前者是发散的,后者是停滞的。

             我们的推导也解释了为什么固定标准偏差为0.01会导致很深的网络凝滞。我们用VGG小组的model B 作为一个例子。模型由10层卷积层组成,且均使用3*3的滤波器,第一层和第二层滤波器数量(dl )为64.第三层和第四层是128,第五层和第六层是256,剩下的都为512。由12nlVarwl=1 ∀l ,我们可以知道标准差为2n ,带入数据算出0.059,0.042,0.029和0.021当滤波器的数量分别为64,128,256,512的时候,如果标准差固定为0.01,那么标准差从第 10层传递到第2层的梯度将会等于1(5.9*4.22*2.92*2.14)=11.7*104 。着个数据可以解释为什么梯度下降在试验中被观察。

             同样值得注意的是,输入信号的方差将被保留,从第一层到最后一层。所以如果输入不是归一化的,是range of [-128,128],那么最后的到数据会非常大,导致softmax操作溢出。

    那么就有一个问题就是去归一化输入数据,但是这会影响其他的超参数。另一个问题就是在一些或者所有层中的权值上加一个小系数,像L1/128 在L层上。在训练的时候,前两个全连接层我们会使用标准差为0.01的高斯分布,之后的所有层都用标准差为0.001的高斯分布。这些数据比实际的要小,并且这样可以解决[-128,128]随机图像的归一化问题。

             对于PReLU情况的初始化,我们容易得到12nlVarwl=1  ∀l  变为              12(1+a2)nlVarwl=1  ∀l  其中a 就是初始化的系数,当a=0 时,就是ReLU的情况,当a=1 时就是线性的情况,同理另一个式子也是这样。

     

     

    实施细节:

    训练:

           对于一个缩放后的图片,短边为s,从上面随机选取一个224*224的裁剪,然后每一个像素值减去均值。缩放的s从[256,512]中随机选择。一半的样本是水平转换得到的,随机改变颜色也被运用了。

             缩放规模的抖动不是只运用于微调阶段,我们把他运用在了训练的起始阶段。进一步的,我们不是使用浅层模型去初始化深层模型,我们直接训练非常深的模型,使用我们在2.2中提到的初始化方法,使用的是反向传播的公式。我们端对端的训练可能能帮助提高精度,因为他能避免较差的局部最优。

             其他可能重要的超参数:权重衰退参数为0.0005,动量为0.9,Dropout(50%),应用在前两个FC层,mini-batch的大小固定为128。学习率为0.01,0.001,0.0001,在错误达到稳定时切换,80次epochs一个模型。

    展开全文
  • 默认初始化初始化 作者 杰夫·格雷厄姆 最近更新时间 2015年6月4日 翻译自: https://css-tricks.com/getting-acquainted-with-initial/默认初始化初始化 ...
  • 神经网络的初始化基本初始化方法LeCun初始化Xavier初始化(Glorot初始化)何恺明初始化(He初始化)前向视角反向视角不要使用常数初始化小结参考文献 神经翻译笔记3扩展c. 神经网络的初始化 深度学习中,在具体网络...
  • 原文标题: Init Struct Pattern 原文链接: https://xaeroxe.github.io/init-struct-pattern/Introduction让我们来讨论一下Rust中复杂结构体的初始化。在这方面已经有了一些比较流行的做法,其中包括常见的pub fn new...
  • 初始化

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,141
精华内容 856
关键字:

初始化翻译